zidane 6.5.6 → 6.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/{acp-Dm95NwNM.js → acp-EFzMJPxa.js} +3 -3
  2. package/dist/{acp-Dm95NwNM.js.map → acp-EFzMJPxa.js.map} +1 -1
  3. package/dist/acp-cli.js +5 -5
  4. package/dist/acp.d.ts +2 -2
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-CcTWL-Z6.js → agent-ut6nQRnI.js} +2 -2
  7. package/dist/{agent-CcTWL-Z6.js.map → agent-ut6nQRnI.js.map} +1 -1
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +1 -1
  10. package/dist/{anthropic-DLc_Kw-1.js → anthropic-BomWPIYw.js} +31 -3
  11. package/dist/anthropic-BomWPIYw.js.map +1 -0
  12. package/dist/{auth-BAclQnl3.js → auth-yAckG81N.js} +4 -4
  13. package/dist/auth-yAckG81N.js.map +1 -0
  14. package/dist/chat/pure.d.ts +3 -3
  15. package/dist/chat.d.ts +4 -4
  16. package/dist/chat.js +3 -3
  17. package/dist/contexts/daytona.d.ts +14 -6
  18. package/dist/contexts/daytona.d.ts.map +1 -1
  19. package/dist/contexts/daytona.js +4 -9
  20. package/dist/contexts/daytona.js.map +1 -1
  21. package/dist/contexts/docker.d.ts +10 -2
  22. package/dist/contexts/docker.d.ts.map +1 -1
  23. package/dist/contexts/docker.js +44 -6
  24. package/dist/contexts/docker.js.map +1 -1
  25. package/dist/contexts/e2b.d.ts +10 -3
  26. package/dist/contexts/e2b.d.ts.map +1 -1
  27. package/dist/contexts/e2b.js +4 -2
  28. package/dist/contexts/e2b.js.map +1 -1
  29. package/dist/contexts/sandbox.d.ts +1 -1
  30. package/dist/contexts/sandbox.js +2 -2
  31. package/dist/contexts/sandbox.js.map +1 -1
  32. package/dist/{contexts-DWVt7MvF.js → contexts-BSFQVclW.js} +4 -3
  33. package/dist/contexts-BSFQVclW.js.map +1 -0
  34. package/dist/contexts.d.ts +3 -3
  35. package/dist/contexts.js +1 -1
  36. package/dist/eval.d.ts +1 -1
  37. package/dist/eval.js +1 -1
  38. package/dist/extensions.d.ts +1 -1
  39. package/dist/extensions.js +1 -1
  40. package/dist/headless.d.ts +1 -1
  41. package/dist/headless.js +2 -2
  42. package/dist/{index-ZWibV230.d.ts → index-A1qz0l55.d.ts} +2 -2
  43. package/dist/{index-ZWibV230.d.ts.map → index-A1qz0l55.d.ts.map} +1 -1
  44. package/dist/{index-C8TwH34B.d.ts → index-B6FkK6ZN.d.ts} +2 -2
  45. package/dist/index-B6FkK6ZN.d.ts.map +1 -0
  46. package/dist/index.d.ts +4 -4
  47. package/dist/index.js +3 -3
  48. package/dist/mcp.d.ts +1 -1
  49. package/dist/output/stream-json.d.ts +1 -1
  50. package/dist/output/terminal.d.ts +1 -1
  51. package/dist/plugins.d.ts +1 -1
  52. package/dist/presets.d.ts +1 -1
  53. package/dist/presets.js +1 -1
  54. package/dist/providers/anthropic.d.ts +1 -1
  55. package/dist/providers/anthropic.js +1 -1
  56. package/dist/providers/arcee.d.ts +1 -1
  57. package/dist/providers/baseten.d.ts +1 -1
  58. package/dist/providers/cerebras.d.ts +1 -1
  59. package/dist/providers/cerebras.js +1 -1
  60. package/dist/providers/cerebras.js.map +1 -1
  61. package/dist/providers/local.d.ts +1 -1
  62. package/dist/providers/openai-compat.d.ts +1 -1
  63. package/dist/providers/openai.d.ts +1 -1
  64. package/dist/providers/openrouter.d.ts +1 -1
  65. package/dist/providers/xai.d.ts +1 -1
  66. package/dist/providers.d.ts +1 -1
  67. package/dist/providers.js +1 -1
  68. package/dist/restate.d.ts +2 -2
  69. package/dist/restate.js +1 -1
  70. package/dist/{sandbox-DLY6uah7.d.ts → sandbox-CDl9tfTo.d.ts} +4 -3
  71. package/dist/sandbox-CDl9tfTo.d.ts.map +1 -0
  72. package/dist/session/sqlite.d.ts +1 -1
  73. package/dist/session.d.ts +1 -1
  74. package/dist/skills.d.ts +1 -1
  75. package/dist/{tool-formatters-D3wSS2zz.d.ts → tool-formatters-OobhXAK1.d.ts} +2 -2
  76. package/dist/{tool-formatters-D3wSS2zz.d.ts.map → tool-formatters-OobhXAK1.d.ts.map} +1 -1
  77. package/dist/tools/fetch-url.d.ts +1 -1
  78. package/dist/tools/web-search.d.ts +1 -1
  79. package/dist/tools.d.ts +1 -1
  80. package/dist/tools.js +1 -1
  81. package/dist/{transcript-anchors-llwMrLRG.d.ts → transcript-anchors-0midxNxf.d.ts} +2 -2
  82. package/dist/{transcript-anchors-llwMrLRG.d.ts.map → transcript-anchors-0midxNxf.d.ts.map} +1 -1
  83. package/dist/{transcript-anchors-D-Yuimif.js → transcript-anchors-BYmStvQP.js} +3 -3
  84. package/dist/{transcript-anchors-D-Yuimif.js.map → transcript-anchors-BYmStvQP.js.map} +1 -1
  85. package/dist/tui.d.ts +3 -3
  86. package/dist/tui.js +4 -4
  87. package/dist/{turn-operations-C_XvfBU3.d.ts → turn-operations-JtKQfSFC.d.ts} +3 -3
  88. package/dist/{turn-operations-C_XvfBU3.d.ts.map → turn-operations-JtKQfSFC.d.ts.map} +1 -1
  89. package/dist/{types-M2DauK2D.d.ts → types-U-rIVUDw.d.ts} +11 -11
  90. package/dist/types-U-rIVUDw.d.ts.map +1 -0
  91. package/dist/types.d.ts +3 -3
  92. package/docs/EXECUTION_CONTEXT.md +5 -2
  93. package/package.json +8 -2
  94. package/dist/anthropic-DLc_Kw-1.js.map +0 -1
  95. package/dist/auth-BAclQnl3.js.map +0 -1
  96. package/dist/contexts-DWVt7MvF.js.map +0 -1
  97. package/dist/index-C8TwH34B.d.ts.map +0 -1
  98. package/dist/sandbox-DLY6uah7.d.ts.map +0 -1
  99. package/dist/types-M2DauK2D.d.ts.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-BAclQnl3.js","names":["CLIENT_ID","AUTHORIZE_URL","TOKEN_URL","CALLBACK_PORT","CALLBACK_PATH","PROVIDER_NAME","parseAuthorizationInput","exchangeAuthorizationCode","getModels","getModel"],"sources":["../src/chat/oauth-page/server.ts","../src/chat/oauth-page/anthropic.ts","../src/chat/oauth-page/openai-codex.ts","../src/chat/oauth-page/render.ts","../src/chat/oauth-page/index.ts","../src/chat/providers.ts","../src/chat/credentials.ts","../src/chat/auth.ts"],"sourcesContent":["/**\n * Local HTTP callback server primitive shared by the Anthropic + Codex\n * login flows. Owns three concerns:\n *\n * 1. Bind a loopback listener (configurable port + host via\n * `PI_OAUTH_CALLBACK_HOST`, matching pi-ai's semantics).\n * 2. Parse the redirect query string into `{ code, state }` or surface\n * a structured error.\n * 3. Render the result page through the caller-supplied renderer.\n *\n * Each provider clones the lifecycle wrapper (`waitForCode` / `cancelWait`\n * / `close`) so the higher-level login code reads the same on either\n * side. The only thing the server itself decides is the HTML.\n */\n\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { createServer } from 'node:http'\n\nconst CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || '127.0.0.1'\n\nexport interface CallbackServerOptions {\n /** Loopback port. Must match the `redirect_uri` registered with the provider. */\n port: number\n /** Path component of the redirect URI (e.g. `/callback`, `/auth/callback`). */\n path: string\n /**\n * Expected `state` value. The server rejects any callback whose `state`\n * doesn't match, exactly. For Anthropic this is the PKCE verifier; for\n * Codex it's a random 16-byte hex string.\n */\n expectedState: string\n /** Display name passed through to {@link OAuthCallbackPageRenderer}. */\n providerName: string\n /** HTML renderer for both success + error pages. */\n renderPage: OAuthCallbackPageRenderer\n /** Success-page body shown after a clean redirect. */\n successMessage: string\n /**\n * How to react when `server.listen` itself fails (typically `EADDRINUSE`\n * when another instance is mid-flow). `\"reject\"` propagates the error to\n * the awaiting `start` caller; `\"resolveWithStub\"` returns a degraded\n * handle whose `waitForCode` immediately resolves to `null`, so the\n * caller can fall back to manual paste. Anthropic uses `\"reject\"`,\n * Codex uses `\"resolveWithStub\"` to match pi-ai's per-provider behavior.\n */\n onListenError?: 'reject' | 'resolveWithStub'\n}\n\nexport interface CallbackServerHandle {\n /** Fully-qualified redirect URI to advertise to the provider. */\n redirectUri: string\n /**\n * Resolves when the redirect lands (with `{ code, state? }`) or when\n * {@link cancelWait} is invoked (with `null`). Never throws.\n */\n waitForCode: () => Promise<{ code: string, state?: string } | null>\n /** Unblock `waitForCode` early (e.g. manual paste won the race). */\n cancelWait: () => void\n /** Close the listener. Idempotent. */\n close: () => void\n}\n\ninterface PendingResolution {\n settle: (value: { code: string, state?: string } | null) => void\n}\n\nfunction buildRequestHandler(opts: CallbackServerOptions, pending: PendingResolution) {\n return (req: IncomingMessage, res: ServerResponse) => {\n try {\n const url = new URL(req.url || '', 'http://localhost')\n\n if (url.pathname !== opts.path) {\n respondError(res, 404, opts, 'Callback route not found.')\n return\n }\n\n const error = url.searchParams.get('error')\n if (error) {\n respondError(res, 400, opts, `${opts.providerName} authentication did not complete.`, `Error: ${error}`)\n return\n }\n\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code || !state) {\n respondError(res, 400, opts, 'Missing code or state parameter.')\n return\n }\n\n if (state !== opts.expectedState) {\n respondError(res, 400, opts, 'State mismatch.')\n return\n }\n\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })\n res.end(opts.renderPage({\n kind: 'success',\n provider: opts.providerName,\n message: opts.successMessage,\n }))\n pending.settle({ code, state })\n }\n catch {\n // We intentionally swallow the actual error here: surfacing a stack\n // to the user's browser would leak internals, and the login flow\n // is about to fall back to manual paste anyway.\n respondError(res, 500, opts, 'Internal error while processing the callback.')\n }\n }\n}\n\nfunction respondError(\n res: ServerResponse,\n status: number,\n opts: CallbackServerOptions,\n message: string,\n details?: string,\n) {\n res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' })\n res.end(opts.renderPage({\n kind: 'error',\n provider: opts.providerName,\n message,\n details,\n }))\n}\n\nfunction buildStubHandle(redirectUri: string, server: Server | null): CallbackServerHandle {\n return {\n redirectUri,\n waitForCode: async () => null,\n cancelWait: () => {},\n close: () => {\n try { server?.close() }\n catch { /* already closed */ }\n },\n }\n}\n\n/**\n * Start the loopback HTTP callback server. The returned handle is the\n * caller's contract — they `await waitForCode()`, race it against manual\n * paste, then `close()` in a `finally`.\n */\nexport async function startCallbackServer(opts: CallbackServerOptions): Promise<CallbackServerHandle> {\n const onListenError = opts.onListenError ?? 'reject'\n const redirectUri = `http://localhost:${opts.port}${opts.path}`\n\n return new Promise<CallbackServerHandle>((resolve, reject) => {\n let settled = false\n const pending: PendingResolution = {\n settle: () => {},\n }\n const waitPromise = new Promise<{ code: string, state?: string } | null>((resolveWait) => {\n pending.settle = (value) => {\n if (settled)\n return\n settled = true\n resolveWait(value)\n }\n })\n\n const server = createServer(buildRequestHandler(opts, pending))\n\n server.on('error', (err) => {\n if (onListenError === 'resolveWithStub') {\n pending.settle(null)\n resolve(buildStubHandle(redirectUri, server))\n return\n }\n reject(err)\n })\n\n server.listen(opts.port, CALLBACK_HOST, () => {\n resolve({\n redirectUri,\n waitForCode: () => waitPromise,\n cancelWait: () => pending.settle(null),\n close: () => {\n try { server.close() }\n catch { /* already closed */ }\n },\n })\n })\n })\n}\n","/**\n * Anthropic OAuth flow with a customisable callback page.\n *\n * Mirrors `loginAnthropic` from `@earendil-works/pi-ai/oauth` 1:1 except:\n *\n * 1. The HTTP callback server's response HTML routes through\n * {@link OAuthCallbackPageRenderer} instead of pi-ai's baked-in page.\n * 2. The lifecycle is delegated to {@link startCallbackServer} so the\n * Codex sibling and any future loopback provider share one HTTP impl.\n *\n * Refresh + `getApiKey` delegate to `src/providers/oauth-registry` — which\n * wraps pi-ai's canonical `auth.oauth.refresh` flow — those don't touch\n * HTML and there's no upside to forking them.\n *\n * Endpoints / client id / port / scopes verified against pi-ai `0.82.1`\n * (`dist/auth/oauth/anthropic.js`). Re-verify in lockstep on the next\n * `@earendil-works/pi-ai` upgrade, or delete this file once pi-ai exposes\n * a callback-page hook upstream.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n} from '@earendil-works/pi-ai/oauth'\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { refreshAnthropicToken } from '../../providers/oauth-registry'\nimport { generatePkce } from './pkce'\nimport { startCallbackServer } from './server'\n\n// Pulled verbatim from pi-ai (verified at 0.82.1) — `dist/auth/oauth/anthropic.js`.\nconst CLIENT_ID = atob('OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl')\nconst AUTHORIZE_URL = 'https://claude.ai/oauth/authorize'\nconst TOKEN_URL = 'https://platform.claude.com/v1/oauth/token'\nconst CALLBACK_PORT = 53692\nconst CALLBACK_PATH = '/callback'\nconst SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload'\nconst PROVIDER_NAME = 'Anthropic'\n\n/**\n * Parse what the user pasted into the manual-code prompt. Accepts:\n * - the bare authorization code\n * - the full `redirect_uri?code=...&state=...` URL\n * - the `code#state` shorthand Anthropic surfaces on the page\n * - a raw query string with `code=...&state=...`\n *\n * Matches pi-ai's parse table so an existing user's muscle memory still works.\n */\nfunction parseAuthorizationInput(input: string): { code?: string, state?: string } {\n const value = input.trim()\n if (!value)\n return {}\n\n try {\n const url = new URL(value)\n return {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n }\n }\n catch {\n // fall through to non-URL parsing below\n }\n\n if (value.includes('#')) {\n const [code, state] = value.split('#', 2)\n return { code, state }\n }\n if (value.includes('code=')) {\n const params = new URLSearchParams(value)\n return {\n code: params.get('code') ?? undefined,\n state: params.get('state') ?? undefined,\n }\n }\n return { code: value }\n}\n\nasync function postJson(url: string, body: Record<string, unknown>): Promise<string> {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(30_000),\n })\n const responseBody = await response.text()\n if (!response.ok)\n throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`)\n return responseBody\n}\n\nasync function exchangeAuthorizationCode(\n code: string,\n state: string,\n verifier: string,\n redirectUri: string,\n): Promise<OAuthCredentials> {\n const responseBody = await postJson(TOKEN_URL, {\n grant_type: 'authorization_code',\n client_id: CLIENT_ID,\n code,\n state,\n redirect_uri: redirectUri,\n code_verifier: verifier,\n })\n\n const tokenData = JSON.parse(responseBody) as {\n refresh_token: string\n access_token: string\n expires_in: number\n }\n\n return {\n refresh: tokenData.refresh_token,\n access: tokenData.access_token,\n // -5min so a refresh fires before the upstream expiry; matches pi-ai.\n expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n }\n}\n\nexport interface LoginAnthropicWithPageOptions extends Pick<\n OAuthLoginCallbacks,\n 'onAuth' | 'onPrompt' | 'onProgress' | 'onManualCodeInput'\n> {\n renderPage: OAuthCallbackPageRenderer\n}\n\nexport async function loginAnthropicWithCustomPage(\n options: LoginAnthropicWithPageOptions,\n): Promise<OAuthCredentials> {\n const { verifier, challenge } = await generatePkce()\n const server = await startCallbackServer({\n port: CALLBACK_PORT,\n path: CALLBACK_PATH,\n expectedState: verifier,\n providerName: PROVIDER_NAME,\n renderPage: options.renderPage,\n successMessage: 'Anthropic authentication completed. You can close this window.',\n onListenError: 'reject',\n })\n\n let code: string | undefined\n let state: string | undefined\n\n try {\n const authParams = new URLSearchParams({\n code: 'true',\n client_id: CLIENT_ID,\n response_type: 'code',\n redirect_uri: server.redirectUri,\n scope: SCOPES,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n state: verifier,\n })\n\n options.onAuth({\n url: `${AUTHORIZE_URL}?${authParams.toString()}`,\n instructions: 'Complete login in your browser. If the browser is on another machine, paste the final redirect URL here.',\n })\n\n if (options.onManualCodeInput) {\n let manualInput: string | undefined\n let manualError: Error | undefined\n const manualPromise = options\n .onManualCodeInput()\n .then((input) => {\n manualInput = input\n server.cancelWait()\n })\n .catch((err) => {\n manualError = err instanceof Error ? err : new Error(String(err))\n server.cancelWait()\n })\n\n const result = await server.waitForCode()\n if (manualError)\n throw manualError\n\n if (result?.code) {\n code = result.code\n state = result.state\n }\n else if (manualInput) {\n const parsed = parseAuthorizationInput(manualInput)\n if (parsed.state && parsed.state !== verifier)\n throw new Error('OAuth state mismatch')\n code = parsed.code\n state = parsed.state ?? verifier\n }\n\n if (!code) {\n await manualPromise\n if (manualError)\n throw manualError\n if (manualInput) {\n const parsed = parseAuthorizationInput(manualInput)\n if (parsed.state && parsed.state !== verifier)\n throw new Error('OAuth state mismatch')\n code = parsed.code\n state = parsed.state ?? verifier\n }\n }\n }\n else {\n const result = await server.waitForCode()\n if (result?.code) {\n code = result.code\n state = result.state\n }\n }\n\n if (!code) {\n const input = await options.onPrompt({\n message: 'Paste the authorization code or full redirect URL:',\n placeholder: server.redirectUri,\n })\n const parsed = parseAuthorizationInput(input)\n if (parsed.state && parsed.state !== verifier)\n throw new Error('OAuth state mismatch')\n code = parsed.code\n state = parsed.state ?? verifier\n }\n\n if (!code)\n throw new Error('Missing authorization code')\n if (!state)\n throw new Error('Missing OAuth state')\n\n options.onProgress?.('Exchanging authorization code for tokens...')\n return await exchangeAuthorizationCode(code, state, verifier, server.redirectUri)\n }\n finally {\n server.close()\n }\n}\n\n/**\n * Build an `OAuthProviderInterface` that behaves identically to pi-ai's\n * `anthropicOAuthProvider` except for the callback page HTML. Drop this\n * onto `ProviderDescriptor.oauthProvider` to override.\n */\nexport function createAnthropicOAuthProviderWithCustomPage(\n renderPage: OAuthCallbackPageRenderer,\n): OAuthProviderInterface {\n return {\n id: 'anthropic',\n name: 'Anthropic (Claude Pro/Max)',\n usesCallbackServer: true,\n async login(callbacks: OAuthLoginCallbacks) {\n return loginAnthropicWithCustomPage({\n renderPage,\n onAuth: callbacks.onAuth,\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n onManualCodeInput: callbacks.onManualCodeInput,\n })\n },\n async refreshToken(credentials: OAuthCredentials) {\n return refreshAnthropicToken(credentials.refresh)\n },\n getApiKey(credentials: OAuthCredentials) {\n return credentials.access\n },\n }\n}\n","/**\n * OpenAI Codex (ChatGPT Plus/Pro) OAuth flow with a customisable callback\n * page. 1:1 mirror of pi-ai's `loginOpenAICodex` (endpoints verified at\n * `0.82.1`, `dist/auth/oauth/openai-codex.js`) except the HTTP callback\n * server routes through {@link OAuthCallbackPageRenderer}.\n *\n * Refresh delegates to `refreshOpenAICodexToken` in\n * `src/providers/oauth-registry`, which wraps pi-ai's canonical\n * `auth.oauth.refresh` flow — Codex's refresh path involves JWT decoding for\n * `accountId`, and we don't want to duplicate it. The HTML override is\n * purely about the landing page.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n} from '@earendil-works/pi-ai/oauth'\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { Buffer } from 'node:buffer'\nimport { randomBytes } from 'node:crypto'\nimport { refreshOpenAICodexToken } from '../../providers/oauth-registry'\nimport { generatePkce } from './pkce'\nimport { startCallbackServer } from './server'\n\n// Pulled verbatim from pi-ai (verified at 0.82.1) — `dist/auth/oauth/openai-codex.js`.\nconst CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'\nconst AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize'\nconst TOKEN_URL = 'https://auth.openai.com/oauth/token'\nconst CALLBACK_PORT = 1455\nconst CALLBACK_PATH = '/auth/callback'\nconst SCOPE = 'openid profile email offline_access'\nconst JWT_CLAIM_PATH = 'https://api.openai.com/auth'\nconst PROVIDER_NAME = 'OpenAI'\n\nfunction createState(): string {\n return randomBytes(16).toString('hex')\n}\n\nfunction parseAuthorizationInput(input: string): { code?: string, state?: string } {\n const value = input.trim()\n if (!value)\n return {}\n\n try {\n const url = new URL(value)\n return {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n }\n }\n catch {\n // fall through\n }\n\n if (value.includes('#')) {\n const [code, state] = value.split('#', 2)\n return { code, state }\n }\n if (value.includes('code=')) {\n const params = new URLSearchParams(value)\n return {\n code: params.get('code') ?? undefined,\n state: params.get('state') ?? undefined,\n }\n }\n return { code: value }\n}\n\ninterface JwtPayload {\n [key: string]: unknown\n [JWT_CLAIM_PATH]?: { chatgpt_account_id?: string }\n}\n\nfunction decodeJwt(token: string): JwtPayload | null {\n try {\n const parts = token.split('.')\n if (parts.length !== 3)\n return null\n const payload = parts[1] ?? ''\n // JWT segments are base64url (RFC 7515) — `-`/`_` alphabet, no padding.\n // `atob` expects standard base64 and throws on those chars, which made\n // extraction fail for perfectly valid tokens whose payload happened to\n // contain them. `Buffer` handles both alphabets and missing padding.\n const decoded = Buffer.from(payload, 'base64url').toString('utf8')\n return JSON.parse(decoded) as JwtPayload\n }\n catch {\n return null\n }\n}\n\nfunction getAccountId(accessToken: string): string | null {\n const payload = decodeJwt(accessToken)\n const claim = payload?.[JWT_CLAIM_PATH] as { chatgpt_account_id?: string } | undefined\n const accountId = claim?.chatgpt_account_id\n return typeof accountId === 'string' && accountId.length > 0 ? accountId : null\n}\n\ninterface TokenExchangeSuccess {\n type: 'success'\n access: string\n refresh: string\n expires: number\n}\n\ninterface TokenExchangeFailure {\n type: 'failed'\n message: string\n}\n\nasync function exchangeAuthorizationCode(\n code: string,\n verifier: string,\n redirectUri: string,\n): Promise<TokenExchangeSuccess | TokenExchangeFailure> {\n const response = await fetch(TOKEN_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: CLIENT_ID,\n code,\n code_verifier: verifier,\n redirect_uri: redirectUri,\n }),\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => '')\n return {\n type: 'failed',\n message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,\n }\n }\n\n const json = await response.json() as {\n access_token?: string\n refresh_token?: string\n expires_in?: number\n }\n if (!json.access_token || !json.refresh_token || typeof json.expires_in !== 'number') {\n return {\n type: 'failed',\n message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,\n }\n }\n return {\n type: 'success',\n access: json.access_token,\n refresh: json.refresh_token,\n expires: Date.now() + json.expires_in * 1000,\n }\n}\n\nexport interface LoginOpenAICodexWithPageOptions extends Pick<\n OAuthLoginCallbacks,\n 'onAuth' | 'onPrompt' | 'onProgress' | 'onManualCodeInput'\n> {\n renderPage: OAuthCallbackPageRenderer\n /** OAuth `originator` parameter. Defaults to `\"pi\"` to stay observable upstream. */\n originator?: string\n}\n\nexport async function loginOpenAICodexWithCustomPage(\n options: LoginOpenAICodexWithPageOptions,\n): Promise<OAuthCredentials> {\n const { verifier, challenge } = await generatePkce()\n const state = createState()\n const originator = options.originator ?? 'pi'\n\n const server = await startCallbackServer({\n port: CALLBACK_PORT,\n path: CALLBACK_PATH,\n expectedState: state,\n providerName: PROVIDER_NAME,\n renderPage: options.renderPage,\n successMessage: 'OpenAI authentication completed. You can close this window.',\n // Matches pi-ai: Codex falls back to manual paste on port collision.\n onListenError: 'resolveWithStub',\n })\n\n const authUrl = new URL(AUTHORIZE_URL)\n authUrl.searchParams.set('response_type', 'code')\n authUrl.searchParams.set('client_id', CLIENT_ID)\n authUrl.searchParams.set('redirect_uri', server.redirectUri)\n authUrl.searchParams.set('scope', SCOPE)\n authUrl.searchParams.set('code_challenge', challenge)\n authUrl.searchParams.set('code_challenge_method', 'S256')\n authUrl.searchParams.set('state', state)\n authUrl.searchParams.set('id_token_add_organizations', 'true')\n authUrl.searchParams.set('codex_cli_simplified_flow', 'true')\n authUrl.searchParams.set('originator', originator)\n\n options.onAuth({\n url: authUrl.toString(),\n instructions: 'A browser window should open. Complete login to finish.',\n })\n\n let code: string | undefined\n\n try {\n if (options.onManualCodeInput) {\n let manualCode: string | undefined\n let manualError: Error | undefined\n const manualPromise = options\n .onManualCodeInput()\n .then((input) => {\n manualCode = input\n server.cancelWait()\n })\n .catch((err) => {\n manualError = err instanceof Error ? err : new Error(String(err))\n server.cancelWait()\n })\n\n const result = await server.waitForCode()\n if (manualError)\n throw manualError\n\n if (result?.code) {\n code = result.code\n }\n else if (manualCode) {\n const parsed = parseAuthorizationInput(manualCode)\n if (parsed.state && parsed.state !== state)\n throw new Error('State mismatch')\n code = parsed.code\n }\n\n if (!code) {\n await manualPromise\n if (manualError)\n throw manualError\n if (manualCode) {\n const parsed = parseAuthorizationInput(manualCode)\n if (parsed.state && parsed.state !== state)\n throw new Error('State mismatch')\n code = parsed.code\n }\n }\n }\n else {\n const result = await server.waitForCode()\n if (result?.code)\n code = result.code\n }\n\n if (!code) {\n const input = await options.onPrompt({\n message: 'Paste the authorization code (or full redirect URL):',\n })\n const parsed = parseAuthorizationInput(input)\n if (parsed.state && parsed.state !== state)\n throw new Error('State mismatch')\n code = parsed.code\n }\n\n if (!code)\n throw new Error('Missing authorization code')\n\n const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri)\n if (tokenResult.type !== 'success')\n throw new Error(tokenResult.message)\n\n const accountId = getAccountId(tokenResult.access)\n if (!accountId)\n throw new Error('Failed to extract accountId from token')\n\n return {\n access: tokenResult.access,\n refresh: tokenResult.refresh,\n expires: tokenResult.expires,\n accountId,\n }\n }\n finally {\n server.close()\n }\n}\n\n/**\n * Build an `OAuthProviderInterface` that behaves identically to pi-ai's\n * `openaiCodexOAuthProvider` except for the callback page HTML.\n */\nexport function createOpenAICodexOAuthProviderWithCustomPage(\n renderPage: OAuthCallbackPageRenderer,\n): OAuthProviderInterface {\n return {\n id: 'openai-codex',\n name: 'ChatGPT Plus/Pro (Codex Subscription)',\n usesCallbackServer: true,\n async login(callbacks: OAuthLoginCallbacks) {\n return loginOpenAICodexWithCustomPage({\n renderPage,\n onAuth: callbacks.onAuth,\n onPrompt: callbacks.onPrompt,\n onProgress: callbacks.onProgress,\n onManualCodeInput: callbacks.onManualCodeInput,\n })\n },\n async refreshToken(credentials: OAuthCredentials) {\n return refreshOpenAICodexToken(credentials.refresh)\n },\n getApiKey(credentials: OAuthCredentials) {\n return credentials.access\n },\n }\n}\n","/**\n * HTML rendered by the local OAuth callback server when the browser hits\n * `redirect_uri`. This is the page the user sees the moment authentication\n * finishes — by default pi-ai bakes in its own dark page; this module lets\n * a host swap it for something branded without forking the package.\n *\n * Renderer is a single pure function — `(page) => string` — so a host can\n * inline a literal template, pull from a theme registry, or proxy a static\n * asset built at compile time. Anything that returns a `string`.\n */\n\n/**\n * Outcome rendered on the callback page. `kind` drives status code on the\n * underlying HTTP response (`200` for success, `4xx` for error); the rest\n * is content. `provider` is the human-readable name of the OAuth provider\n * (e.g. `\"Anthropic\"`, `\"OpenAI Codex\"`) so a renderer can swap copy or\n * logos per provider.\n */\nexport interface OAuthCallbackPage {\n kind: 'success' | 'error'\n /** Headline message — usually one sentence. Safe to include user-visible details. */\n message: string\n /** Optional secondary details (e.g. provider error code). Rendered in mono. */\n details?: string\n /** Provider display name passed through from the login function. */\n provider: string\n}\n\nexport type OAuthCallbackPageRenderer = (page: OAuthCallbackPage) => string\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll('\\'', '&#39;')\n}\n\n/**\n * Default zidane-themed page. Visually neutral but distinguishable from\n * pi-ai's stock page — dark background, mono headings, no logo (host can\n * pass a custom renderer to add one).\n */\nexport const renderDefaultCallbackPage: OAuthCallbackPageRenderer = (page) => {\n const title = page.kind === 'success'\n ? `Signed in to ${page.provider}`\n : `Could not sign in to ${page.provider}`\n const heading = escapeHtml(title)\n const message = escapeHtml(page.message)\n const details = page.details ? escapeHtml(page.details) : undefined\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${heading}</title>\n <style>\n :root {\n --text: #f4f4f5;\n --text-dim: #a1a1aa;\n --accent: ${page.kind === 'success' ? '#22d3ee' : '#f87171'};\n --page-bg: #0a0a0a;\n --panel-bg: #131316;\n --border: #27272a;\n --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n }\n * { box-sizing: border-box; }\n html { color-scheme: dark; }\n body {\n margin: 0;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--page-bg);\n color: var(--text);\n font-family: var(--font-sans);\n }\n main {\n width: 100%;\n max-width: 520px;\n padding: 32px;\n background: var(--panel-bg);\n border: 1px solid var(--border);\n border-radius: 12px;\n }\n .label {\n font-family: var(--font-mono);\n font-size: 12px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--accent);\n margin-bottom: 12px;\n }\n h1 {\n margin: 0 0 12px;\n font-size: 22px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--text);\n }\n p {\n margin: 0;\n line-height: 1.6;\n color: var(--text-dim);\n font-size: 14px;\n }\n .details {\n margin-top: 18px;\n padding: 12px 14px;\n background: var(--page-bg);\n border: 1px solid var(--border);\n border-radius: 8px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--text-dim);\n white-space: pre-wrap;\n word-break: break-word;\n }\n </style>\n</head>\n<body>\n <main>\n <div class=\"label\">zidane · OAuth · ${escapeHtml(page.provider)}</div>\n <h1>${heading}</h1>\n <p>${message}</p>\n ${details ? `<div class=\"details\">${details}</div>` : ''}\n </main>\n</body>\n</html>`\n}\n\n/**\n * Tiny helper kept private to the module so renderer authors can reuse\n * the same escaping rules as the default theme. Re-exported in case a\n * host wants to embed user-supplied messages safely.\n */\nexport const escapeHtmlForCallbackPage: (value: string) => string = escapeHtml\n","/**\n * Custom OAuth callback page — drop-in replacements for pi-ai's built-in\n * Anthropic + OpenAI Codex providers that route the post-redirect HTML\n * through your own renderer.\n *\n * Why this module exists\n * ----------------------\n * `@earendil-works/pi-ai` (still true at `0.82.1`) bakes its callback page\n * into its Anthropic / OpenAI Codex login flows with no override, so this\n * module forks the two flows in one self-contained place — everything else\n * (token refresh, `getApiKey`, env handling) delegates back to pi-ai's\n * canonical flows via `src/providers/oauth-registry` unchanged. Since pi-ai\n * `0.80.8` the OAuth architecture is `Provider.auth.oauth` + `AuthInteraction`\n * (the old `OAuthLoginCallbacks` registry surface was removed upstream and is\n * now reproduced locally in `src/providers/oauth-registry.ts`).\n *\n * How to use\n * ----------\n *\n * ```ts\n * import { createCustomCallbackOAuthProviders, renderDefaultCallbackPage } from './chat/oauth-page'\n * import { BUILTIN_PROVIDERS } from './chat/providers'\n *\n * const { anthropic, openaiCodex } = createCustomCallbackOAuthProviders(renderDefaultCallbackPage)\n *\n * const providers = {\n * ...BUILTIN_PROVIDERS,\n * anthropic: { ...BUILTIN_PROVIDERS.anthropic, oauthProvider: anthropic },\n * openai: { ...BUILTIN_PROVIDERS.openai, oauthProvider: openaiCodex },\n * }\n * ```\n *\n * Or override per provider with `createAnthropicOAuthProviderWithCustomPage`\n * / `createOpenAICodexOAuthProviderWithCustomPage` directly.\n *\n * Deleting this module\n * --------------------\n * The upstream deletion path now runs through pi-ai's canonical\n * `Provider.auth.oauth` + `CredentialStore` architecture (a page-render hook\n * alone no longer exists as a plan). Migrating means adopting that whole\n * stack in place of `src/providers/oauth.ts`'s credential handling — see\n * `src/providers/oauth-registry.ts` for the seam.\n *\n * Endpoints verified against pi-ai `0.82.1`. Re-verify endpoints + ports on\n * every pi-ai bump (auth URLs, client IDs, callback ports are duplicated\n * verbatim here; see `anthropic.ts` and `openai-codex.ts` for the\n * source-of-truth pointers).\n */\n\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { registerOAuthProvider } from '../../providers/oauth-registry'\nimport { createAnthropicOAuthProviderWithCustomPage } from './anthropic'\nimport { createCursorOAuthProvider } from './cursor'\nimport { createOpenAICodexOAuthProviderWithCustomPage } from './openai-codex'\nimport { createXaiOAuthProvider } from './xai'\n\nexport type {\n LoginAnthropicWithPageOptions,\n} from './anthropic'\nexport {\n createAnthropicOAuthProviderWithCustomPage,\n loginAnthropicWithCustomPage,\n} from './anthropic'\nexport {\n createCursorOAuthProvider,\n CURSOR_OAUTH_PROVIDER_ID,\n loginCursor,\n refreshCursorToken,\n} from './cursor'\nexport type {\n LoginOpenAICodexWithPageOptions,\n} from './openai-codex'\nexport {\n createOpenAICodexOAuthProviderWithCustomPage,\n loginOpenAICodexWithCustomPage,\n} from './openai-codex'\nexport type {\n OAuthCallbackPage,\n OAuthCallbackPageRenderer,\n} from './render'\nexport {\n escapeHtmlForCallbackPage,\n renderDefaultCallbackPage,\n} from './render'\nexport {\n createXaiOAuthProvider,\n loginXai,\n refreshXaiToken,\n XAI_OAUTH_PROVIDER_ID,\n xaiBaseUrl,\n} from './xai'\nexport type { XaiOAuthCredentials } from './xai'\n\nexport interface CustomCallbackOAuthProviders {\n anthropic: OAuthProviderInterface\n openaiCodex: OAuthProviderInterface\n}\n\n/**\n * Bundle helper — returns both Anthropic + OpenAI Codex providers wired\n * with the same renderer. Hosts that want different renderers per provider\n * should call the individual `create…WithCustomPage` factories instead.\n */\nexport function createCustomCallbackOAuthProviders(\n renderPage: OAuthCallbackPageRenderer,\n): CustomCallbackOAuthProviders {\n return {\n anthropic: createAnthropicOAuthProviderWithCustomPage(renderPage),\n openaiCodex: createOpenAICodexOAuthProviderWithCustomPage(renderPage),\n }\n}\n\nlet cursorRegistered = false\n\n/**\n * Register Cursor with pi-ai's OAuth registry.\n *\n * Unlike Anthropic / Codex, Cursor is **not** built into pi-ai, so\n * `getOAuthApiKey('cursor', …)` (used by `resolveOAuthApiKey` for lazy\n * token refresh) would throw \"Unknown OAuth provider\" until we register it.\n * Call once at startup from both the CLI auth path and the TUI. Idempotent.\n */\nexport function registerCursorOAuthProvider(): OAuthProviderInterface {\n const provider = createCursorOAuthProvider()\n if (!cursorRegistered) {\n registerOAuthProvider(provider)\n cursorRegistered = true\n }\n return provider\n}\n\nlet xaiRegistered = false\n\n/**\n * Register xAI Grok with pi-ai's OAuth registry.\n *\n * pi-ai ships an xAI *API-key* provider, but no OAuth one — so\n * `getOAuthApiKey('xai-oauth', …)` (lazy refresh in `resolveOAuthApiKey`)\n * would throw \"Unknown OAuth provider\" until we register our flow. The\n * callback page routes through the supplied renderer so the post-redirect\n * HTML matches zidane's theme. Call once at startup; idempotent.\n */\nexport function registerXaiOAuthProvider(\n renderPage: OAuthCallbackPageRenderer,\n): OAuthProviderInterface {\n const provider = createXaiOAuthProvider(renderPage)\n if (!xaiRegistered) {\n registerOAuthProvider(provider)\n xaiRegistered = true\n }\n return provider\n}\n","/**\n * Provider registry — the customization seam for the TUI.\n *\n * A {@link ProviderDescriptor} carries everything the TUI needs to know about\n * a provider in one place: how to instantiate it, what to show in the UI,\n * how to detect credentials, how to OAuth (if applicable), and where to look\n * up its models. Hosts can ship their own descriptors instead of the built-in\n * four — see {@link BUILTIN_PROVIDERS}.\n */\n\nimport type { Provider } from '../providers'\nimport type { ModelCatalogEntry } from '../providers/model-catalog'\nimport type { OAuthProviderInterface } from '../providers/oauth-registry'\nimport { getBuiltinModel as getModel, getBuiltinModels as getModels } from '@earendil-works/pi-ai/providers/all'\nimport { anthropic, arcee, baseten, cerebras, cursor, local, mistral, openai, openrouter, xai, zai } from '../providers'\nimport { listModelOverlay, listModels, lookupModel, mergeCatalogEntry } from '../providers/model-catalog'\nimport { ANTHROPIC_EXTRA_MODELS, FAST_MODE_OPTIONS } from './anthropic-models'\nimport { createCustomCallbackOAuthProviders, registerCursorOAuthProvider, registerXaiOAuthProvider, renderDefaultCallbackPage } from './oauth-page'\n\n/**\n * pi-ai's stock Anthropic + Codex OAuth providers bake their own callback\n * HTML; we route both through `renderDefaultCallbackPage` so the post-redirect\n * page matches zidane's theme. The override lives in `src/chat/oauth-page/`\n * — see that folder's `index.ts` for the deletion path once pi-ai exposes\n * a `renderCallbackPage` hook upstream.\n */\nconst { anthropic: anthropicOAuthProvider, openaiCodex: openaiCodexOAuthProvider }\n = createCustomCallbackOAuthProviders(renderDefaultCallbackPage)\n\n/**\n * Cursor isn't built into pi-ai, so registering it makes lazy token refresh\n * (`getOAuthApiKey('cursor', …)` inside `resolveOAuthApiKey`) resolve to our\n * Cursor flow. The poll-based login has no callback server, so no themed page.\n */\nconst cursorOAuthProvider = registerCursorOAuthProvider()\n\n/**\n * xAI Grok isn't built into pi-ai's OAuth registry (only its API-key provider\n * is), so register our loopback-PKCE flow — themed via `renderDefaultCallbackPage`\n * — so lazy token refresh resolves through it.\n */\nconst xaiOAuthProvider = registerXaiOAuthProvider(renderDefaultCallbackPage)\n\n/**\n * Structural model metadata — compatible with pi-ai's `Model` interface but\n * not coupled to it, so hosts can pass either pi-ai-shaped objects or a\n * custom registry of their own.\n *\n * Deliberately a structural duplicate of pi-ai's `Model` rather than a\n * re-export: keeps the public API stable when pi-ai bumps shape, and lets\n * hosts implement their own registry without depending on pi-ai's types.\n *\n * Lives here (rather than `./config`) because `ProviderDescriptor.models`\n * needs it — pushing it to `config.tsx` created an import cycle.\n */\nexport interface ModelInfo {\n id: string\n name?: string\n contextWindow: number\n maxTokens?: number\n reasoning?: boolean\n input?: readonly ('text' | 'image')[]\n cost?: { input: number, output: number, cacheRead?: number, cacheWrite?: number }\n provider?: string\n /**\n * Model-specific toggleable knobs surfaced in the UI (e.g. Anthropic's\n * \"fast mode\"). Each option is an independent boolean the user flips on the\n * active model; enabled options are collected into `StreamOptions.modelOptions`\n * keyed by {@link ModelOption.id} and applied to the wire by the provider.\n * Omitted / empty → the model has no custom options and the picker is hidden.\n */\n options?: readonly ModelOption[]\n}\n\n/**\n * A single toggleable model option. Declarative metadata only — the provider\n * owns the actual request-body translation (keyed on {@link id}). Kept generic\n * (not \"fast mode\"-specific) so new per-model knobs land without a type change.\n */\nexport interface ModelOption {\n /** Stable identifier; the key under which the toggle persists + reaches the provider. */\n id: string\n /** Short label shown in the picker (e.g. \"Fast mode\"). */\n label: string\n /** One-line description of the trade-off (shown in the picker row). */\n description?: string\n /**\n * Optional cost multipliers applied to the model's base rates while the\n * option is enabled. `{ input: 2, output: 2 }` doubles both. Used by the\n * cost estimator so the footer reflects premium-priced modes (fast mode).\n */\n costMultiplier?: { input?: number, output?: number, cacheRead?: number, cacheWrite?: number }\n}\n\n/**\n * Free-form configuration field that a provider needs the user to supply\n * alongside (or instead of) an API key — e.g. a self-hosted base URL, an\n * Azure deployment name, a default model id.\n *\n * The wizard prompts for each field in order. Submitted values are persisted\n * into the apikey credential (as `customFields[key]`) and mirrored into\n * `process.env[envVar]` so the provider's factory can read them via the same\n * env-var convention as `envKey`.\n *\n * Descriptors without an `envKey` use `customFields` as the sole credential\n * mechanism — the wizard skips its API-key step in that case.\n */\nexport interface CustomField {\n /** Storage key inside the credential's `customFields` record. */\n key: string\n /** Human-readable label shown in the wizard step's title. */\n label: string\n /** Env var name to mirror this field into at TUI launch + on submit. */\n envVar: string\n /** Optional placeholder shown in the wizard input. */\n placeholder?: string\n /** Optional helper copy rendered above the input. */\n hint?: string\n /** When true, an empty submission is rejected by the wizard. Default: false. */\n required?: boolean\n}\n\nexport interface ProviderDescriptor {\n /**\n * Unique identifier. Used as the registry key, persisted in `state.json`\n * as the resumed provider, and (by default) as the credential-file key.\n */\n key: string\n /** Display name shown in the TUI's provider picker and labels. */\n label: string\n /**\n * Factory that builds a fresh `Provider` instance. Called on every session\n * activation. Some factories (e.g. zidane's built-in `anthropic`) eagerly\n * resolve credentials at construction time and throw when none are\n * configured — set {@link defaultModel} on the descriptor to avoid the TUI\n * calling the factory before the user has picked + authed a provider.\n */\n factory: () => Provider\n /**\n * Default model id to seed the picker with. When omitted, the TUI falls\n * back to `descriptor.factory().meta.defaultModel`, which constructs the\n * provider — fine for lazy-credential factories, but breaks for factories\n * that throw on missing credentials before the wizard runs. Setting this\n * eagerly lets the TUI render the auth wizard without ever instantiating\n * the provider.\n */\n defaultModel?: string\n /**\n * Env var checked when detecting whether the user has an API key\n * configured for this provider. Optional — set to `undefined` when the\n * provider only supports OAuth (or only credentials via a custom path).\n */\n envKey?: string\n /**\n * Key under which credentials live in `credentials.json`. Defaults to\n * `key`. The only built-in that overrides this is OpenAI Codex\n * (`openai` → `openai-codex`) to stay compatible with the harness\n * provider's existing lookup.\n */\n credentialFileKey?: string\n /** Placeholder shown in the wizard's API-key input. */\n apiKeyPlaceholder?: string\n /**\n * pi-ai (or compat) OAuth provider. When present, the wizard offers an\n * OAuth option in addition to API key.\n */\n oauthProvider?: OAuthProviderInterface\n /**\n * Optional copy appended to the wizard's \"OAuth\" method description.\n * Use to communicate why a user might pick OAuth (e.g. subscription tier,\n * org-wide SSO). Shown after `browser-based sign-in`. Skip the leading\n * space — the wizard adds it. Example: `'Claude Pro/Max subscription'`.\n */\n oauthHint?: string\n /**\n * pi-ai provider id used to look up models in pi-ai's built-in registry.\n * Defaults to `key`. Only Codex differs (`openai` → `openai-codex`).\n */\n piProviderId?: string\n /**\n * Back-compat aliases for persisted model ids whose upstream slug changed.\n * Used for metadata lookups only; aliases are not surfaced as picker rows.\n */\n modelAliases?: Readonly<Record<string, string>>\n /**\n * Override the model list returned for this provider's picker. When set,\n * skips pi-ai's registry entirely. Useful for hosts maintaining their\n * own model catalogue or for custom providers pi-ai doesn't know about.\n */\n models?: readonly ModelInfo[]\n /**\n * Additional free-form fields the wizard should collect from the user\n * (e.g. base URL for a self-hosted endpoint, default model slug for a\n * provider with no fixed catalogue). Each field is persisted in the\n * apikey credential's `customFields` map and mirrored into the matching\n * `envVar` at TUI launch.\n *\n * When `envKey` is also set, the wizard collects custom fields **before**\n * the API key. When `envKey` is omitted, the wizard skips the API-key\n * step entirely — `customFields` becomes the sole credential mechanism\n * (used by the `local` provider, which authenticates via baseURL only).\n */\n customFields?: readonly CustomField[]\n /**\n * Extra models merged AHEAD of pi-ai's registry list (de-duped by id, pi-ai\n * wins on collision). Unlike {@link models} this augments rather than\n * replaces — used to surface freshly-shipped models pi-ai hasn't bundled\n * yet without losing the rest of the registry. Ignored when {@link models}\n * is set.\n */\n extraModels?: readonly ModelInfo[]\n /**\n * Resolve the custom {@link ModelOption}s a given model id supports. Used to\n * attach options (e.g. fast mode) to models that come from pi-ai's registry —\n * which {@link extraModels} can't reach because pi-ai wins the id collision.\n * Returns `undefined` / `[]` for models with no options. Models that already\n * carry `options` (e.g. via {@link extraModels}) keep theirs.\n */\n optionsFor?: (modelId: string) => readonly ModelOption[] | undefined\n /**\n * Env var holding a user-supplied context window (in tokens) for providers\n * that have no model registry to advertise one — chiefly the `local`\n * provider, whose runtime (Ollama, vLLM, …) doesn't expose its loaded\n * model's window over the OpenAI-compat API. When set, {@link getContextWindow}\n * falls back to resolving this env var (via {@link resolveContextWindow})\n * after the registry lookup comes up empty. The value is either a single\n * window (`\"32768\"`, applied to every model) or a per-model map\n * (`\"llama3.1:8b=32768, qwen=128k, 64k\"`, with an optional bare fallback).\n * Pair it with a {@link CustomField} that mirrors the user's value into the\n * same `envVar` so the footer's context indicator and auto-compaction work\n * for local models.\n */\n contextWindowEnvVar?: string\n}\n\n/** Convenience accessor — returns `credentialFileKey ?? key`. */\nexport function credKeyOf(desc: ProviderDescriptor): string {\n return desc.credentialFileKey ?? desc.key\n}\n\n/** Convenience accessor — returns `piProviderId ?? key`. */\nexport function piIdOf(desc: ProviderDescriptor): string {\n return desc.piProviderId ?? desc.key\n}\n\n/**\n * Catalog keys a descriptor answers to, most specific first.\n *\n * Usually one: for every provider except Codex the descriptor `key`, the pi-ai\n * registry id, and the `Provider.name` used on the wire are the same string.\n * Codex is the exception — metadata lives under `openai-codex` while its cost\n * accounting runs under `openai` — so a consumer's single entry has to work\n * under either. The wire side does the same via `aliasCatalogProviders`; if\n * these two disagreed, an override would half-apply (right invoice, wrong\n * context gauge), which is harder to notice than failing outright.\n */\nfunction catalogProvidersOf(desc: ProviderDescriptor): readonly string[] {\n const piId = piIdOf(desc)\n return piId === desc.key ? [piId] : [piId, desc.key]\n}\n\n// ---------------------------------------------------------------------------\n// Built-in descriptors\n// ---------------------------------------------------------------------------\n\nexport const anthropicDescriptor: ProviderDescriptor = {\n key: 'anthropic',\n label: 'Anthropic',\n factory: anthropic,\n defaultModel: 'claude-opus-4-8',\n envKey: 'ANTHROPIC_API_KEY',\n apiKeyPlaceholder: 'sk-ant-…',\n oauthProvider: anthropicOAuthProvider,\n oauthHint: 'Claude Pro/Max subscription',\n extraModels: ANTHROPIC_EXTRA_MODELS,\n optionsFor: id => FAST_MODE_OPTIONS[id] ? [FAST_MODE_OPTIONS[id]] : undefined,\n}\n\nexport const openaiDescriptor: ProviderDescriptor = {\n key: 'openai',\n label: 'OpenAI',\n factory: openai,\n defaultModel: 'gpt-5.5',\n envKey: 'OPENAI_CODEX_API_KEY',\n credentialFileKey: 'openai-codex',\n piProviderId: 'openai-codex',\n // Both credential shapes stream: the provider auto-detects the backend —\n // an `sk-…` platform key routes to the standard Responses API\n // (api.openai.com, platform billing), a Codex OAuth JWT (`eyJ…`, via the\n // login flow) to the ChatGPT Codex backend (subscription billing).\n apiKeyPlaceholder: 'sk-… (platform) or eyJ… (Codex)',\n oauthProvider: openaiCodexOAuthProvider,\n // OpenAI publishes `gpt-5.6` as an alias for the Sol flagship; pi-ai's\n // registry only carries the concrete ids, so resolve the shorthand for\n // metadata lookups (context window, reasoning, cost). Wire-side mirror\n // lives in `MODEL_ALIASES` in `src/providers/openai.ts`.\n modelAliases: {\n 'gpt-5.6': 'gpt-5.6-sol',\n },\n optionsFor: id => id === 'gpt-5.5'\n ? [{\n id: 'fast',\n label: 'Fast mode',\n description: 'Priority tier at 2.5× pricing',\n costMultiplier: { input: 2.5, output: 2.5, cacheRead: 2.5, cacheWrite: 2.5 },\n }]\n : undefined,\n}\n\nexport const openrouterDescriptor: ProviderDescriptor = {\n key: 'openrouter',\n label: 'OpenRouter',\n factory: openrouter,\n defaultModel: 'anthropic/claude-sonnet-4.6',\n envKey: 'OPENROUTER_API_KEY',\n apiKeyPlaceholder: 'sk-or-…',\n // Metadata-side mirror of the OpenRouter provider's wire alias (see\n // `MODEL_ALIASES` in `src/providers/openrouter.ts`). Keeps context-window /\n // reasoning lookups resolving for sessions persisted with the old slug.\n modelAliases: {\n 'anthropic/claude-sonnet-4-6': 'anthropic/claude-sonnet-4.6',\n },\n}\n\nexport const cerebrasDescriptor: ProviderDescriptor = {\n key: 'cerebras',\n label: 'Cerebras',\n factory: cerebras,\n defaultModel: 'zai-glm-4.7',\n envKey: 'CEREBRAS_API_KEY',\n apiKeyPlaceholder: 'csk-…',\n}\n\n/**\n * xAI Grok. Supports BOTH a static API key (`XAI_API_KEY`) and the\n * SuperGrok / X Premium+ OAuth subscription, so the wizard offers both methods.\n * Models come from pi-ai's `xai` registry (OpenAI-compatible endpoint), shared\n * by both auth modes.\n */\nexport const xaiDescriptor: ProviderDescriptor = {\n key: 'xai',\n label: 'xAI Grok',\n factory: xai,\n defaultModel: 'grok-4.3',\n envKey: 'XAI_API_KEY',\n apiKeyPlaceholder: 'xai-…',\n oauthProvider: xaiOAuthProvider,\n oauthHint: 'SuperGrok / X Premium+ subscription',\n // pi-ai stores the OAuth credential under the provider id, distinct from the\n // `xai` descriptor key so the API-key and OAuth credentials never collide.\n credentialFileKey: 'xai-oauth',\n piProviderId: 'xai',\n}\n\n/**\n * Arcee Trinity models (Platform API slugs). pi-ai has no Arcee registry, so we\n * ship a curated list. `reasoning: true` surfaces the effort/thinking UI — the\n * `arcee()` factory round-trips Trinity's `reasoning_content` across turns so the\n * model resumes its chain-of-thought through tool calls.\n *\n * Costs/context per Arcee's published Trinity catalogue.\n */\nconst ARCEE_MODELS: readonly ModelInfo[] = [\n { id: 'trinity-large-thinking', name: 'Trinity Large (Thinking)', contextWindow: 256_000, reasoning: true, input: ['text'], cost: { input: 0.25, output: 0.8 } },\n { id: 'trinity-large-preview', name: 'Trinity Large (Preview)', contextWindow: 128_000, input: ['text'], cost: { input: 0.45, output: 0.15 } },\n { id: 'trinity-mini', name: 'Trinity Mini', contextWindow: 128_000, input: ['text'], cost: { input: 0.045, output: 0.15 } },\n]\n\nexport const arceeDescriptor: ProviderDescriptor = {\n key: 'arcee',\n label: 'Arcee',\n factory: arcee,\n defaultModel: 'trinity-large-thinking',\n envKey: 'ARCEE_API_KEY',\n apiKeyPlaceholder: 'arcee-…',\n models: ARCEE_MODELS,\n}\n\n/**\n * Baseten Model APIs catalogue. pi-ai has no Baseten registry, so we ship a\n * curated list mirroring https://docs.baseten.co/inference/model-apis/overview\n * (context windows) and Baseten's published per-MTok rates. Models without a\n * `cost` (GLM 5.1, Nemotron Ultra) have no published rate yet — the footer\n * cost indicator hides for those. `/v1/models` is the live source of truth.\n * Keep the rates here in sync with `MODEL_PRICES` in `../providers/cost.ts`:\n * this table drives the footer indicator, that one drives `fillEstimatedCost`\n * for turns the provider doesn't price itself.\n *\n * All listed models support reasoning (`reasoning_content`); the `baseten()`\n * factory maps the thinking level to `reasoning_effort` / `chat_template_args`\n * per model.\n */\nconst BASETEN_MODELS: readonly ModelInfo[] = [\n { id: 'zai-org/GLM-5.2', name: 'GLM 5.2', contextWindow: 1_000_000, reasoning: true, input: ['text'], cost: { input: 1.4, output: 4.4, cacheRead: 0.14 } },\n { id: 'zai-org/GLM-5.1', name: 'GLM 5.1', contextWindow: 200_000, reasoning: true, input: ['text'] },\n { id: 'zai-org/GLM-5', name: 'GLM 5', contextWindow: 200_000, reasoning: true, input: ['text'], cost: { input: 0.95, output: 3.15, cacheRead: 0.2 } },\n { id: 'zai-org/GLM-4.7', name: 'GLM 4.7', contextWindow: 200_000, reasoning: true, input: ['text'], cost: { input: 0.6, output: 2.2, cacheRead: 0.12 } },\n { id: 'moonshotai/Kimi-K2.6', name: 'Kimi K2.6', contextWindow: 256_000, reasoning: true, input: ['text', 'image'], cost: { input: 1, output: 3.9, cacheRead: 0.2 } },\n { id: 'moonshotai/Kimi-K2.5', name: 'Kimi K2.5', contextWindow: 256_000, reasoning: true, input: ['text', 'image'], cost: { input: 0.6, output: 3, cacheRead: 0.12 } },\n { id: 'deepseek-ai/DeepSeek-V4-Pro', name: 'DeepSeek V4 Pro', contextWindow: 131_000, reasoning: true, input: ['text'], cost: { input: 1.74, output: 3.48, cacheRead: 0.145 } },\n { id: 'nvidia/Nemotron-120B-A12B', name: 'Nemotron Super', contextWindow: 200_000, reasoning: true, input: ['text'], cost: { input: 0.3, output: 0.75, cacheRead: 0.06 } },\n { id: 'nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B', name: 'Nemotron Ultra', contextWindow: 200_000, reasoning: true, input: ['text'] },\n { id: 'openai/gpt-oss-120b', name: 'GPT-OSS 120B', contextWindow: 128_000, reasoning: true, input: ['text'], cost: { input: 0.1, output: 0.5 } },\n]\n\nexport const basetenDescriptor: ProviderDescriptor = {\n key: 'baseten',\n label: 'Baseten',\n factory: baseten,\n defaultModel: 'zai-org/GLM-5.1',\n envKey: 'BASETEN_API_KEY',\n apiKeyPlaceholder: 'baseten-api-key…',\n models: BASETEN_MODELS,\n}\n\n/**\n * Z.ai (Zhipu) GLM Open Platform catalogue. pi-ai has no Z.ai registry, so we\n * ship a curated list mirroring https://docs.z.ai/guides/overview/pricing and\n * per-model context windows from the GLM-4.7 overview page. `reasoning: true`\n * surfaces the effort/thinking UI — the `zai()` factory maps the thinking\n * level to Z.ai's `extra_body.thinking` toggle and round-trips\n * `reasoning_content` across turns. GLM-4.7-Flash and GLM-4.5-Flash are\n * Z.ai's zero-cost tiers (`cost` omitted).\n */\nconst ZAI_MODELS: readonly ModelInfo[] = [\n { id: 'glm-4.7-flash', name: 'GLM-4.7-Flash', contextWindow: 200_000, maxTokens: 128_000, reasoning: true, input: ['text'] },\n { id: 'glm-4.7', name: 'GLM-4.7', contextWindow: 200_000, maxTokens: 128_000, reasoning: true, input: ['text'], cost: { input: 0.6, output: 2.2, cacheRead: 0.11 } },\n { id: 'glm-4.6', name: 'GLM-4.6', contextWindow: 200_000, maxTokens: 128_000, reasoning: true, input: ['text'], cost: { input: 0.6, output: 2.2, cacheRead: 0.11 } },\n { id: 'glm-4.5-flash', name: 'GLM-4.5-Flash', contextWindow: 128_000, reasoning: true, input: ['text'] },\n { id: 'glm-4.5-air', name: 'GLM-4.5-Air', contextWindow: 128_000, reasoning: true, input: ['text'], cost: { input: 0.2, output: 1.1, cacheRead: 0.03 } },\n]\n\n/**\n * Mistral La Plateforme. Models come from pi-ai's `mistral` registry (plain\n * slugs, same ids the OpenAI-compatible `/v1/chat/completions` route accepts).\n * The `mistral()` factory maps the thinking level to `reasoning_effort` for the\n * families that document it and parses Mistral's `thinking` content chunks.\n */\nexport const mistralDescriptor: ProviderDescriptor = {\n key: 'mistral',\n label: 'Mistral',\n factory: mistral,\n defaultModel: 'mistral-medium-latest',\n envKey: 'MISTRAL_API_KEY',\n apiKeyPlaceholder: 'mistral-api-key…',\n piProviderId: 'mistral',\n}\n\nexport const zaiDescriptor: ProviderDescriptor = {\n key: 'zai',\n label: 'Z.ai',\n factory: zai,\n defaultModel: 'glm-4.7-flash',\n envKey: 'ZAI_API_KEY',\n apiKeyPlaceholder: 'zai-api-key…',\n models: ZAI_MODELS,\n}\n\n/**\n * Conservative context-window assumption for a user-configured local model.\n * Local runtimes expose no reliable per-model metadata over the OpenAI-compat\n * `/v1/models` endpoint (it returns ids, not context sizes), so we pick a\n * floor that fits the smallest mainstream OSS models (Llama 3.x 8B, Qwen2.5)\n * without over-promising. The footer's context indicator and auto-compaction\n * lean on this — under-estimating is the safe direction (compact early rather\n * than overflow the server's real window).\n *\n * Users running larger-context models (e.g. a 128K Qwen) can raise it via the\n * `LOCAL_LLM_CONTEXT_WINDOW` env var / customField, which supports a single\n * value or a per-model map (resolved by {@link resolveContextWindow}).\n */\nconst LOCAL_DEFAULT_CONTEXT_WINDOW = 8_192\n\n/**\n * Local OpenAI-compatible LLM (Ollama, vLLM, LM Studio, Lemonade, llama.cpp).\n *\n * No fixed base URL or model catalogue — both come from the user via\n * `customFields`. No `envKey`, so the wizard skips the API-key prompt and\n * treats the customFields-only credential as the auth signal (see\n * `applyApiKeyEnv` + `detectAuth`).\n *\n * Vision / cache / reasoning are off by default in the factory — flip them\n * by passing a custom descriptor that calls `local({ capabilities })`.\n *\n * `models` is a getter, not a static list: there's no fixed catalogue to ship,\n * but once the user has configured a default model (mirrored into\n * `LOCAL_LLM_DEFAULT_MODEL` by `applyApiKeyEnv`), we surface it as a\n * single-entry list so the model picker + footer aren't empty. Resolved at\n * access time because the env var is populated at TUI launch, after this\n * module loads. Empty list when unconfigured — the picker hides, the user\n * types a slug at the prompt as before.\n */\nexport const localDescriptor: ProviderDescriptor = {\n key: 'local',\n label: 'Local LLM',\n factory: local,\n get models(): readonly ModelInfo[] {\n const configured = process.env.LOCAL_LLM_DEFAULT_MODEL?.trim()\n if (!configured)\n return []\n // Resolve the window from the user's `LOCAL_LLM_CONTEXT_WINDOW` (single\n // value or per-model map), falling back to the conservative default. Uses\n // the same parser as `getContextWindow`'s `contextWindowEnvVar` path so\n // both surfaces agree on the number.\n const resolved = resolveContextWindow(process.env.LOCAL_LLM_CONTEXT_WINDOW, configured)\n return [{\n id: configured,\n name: configured,\n contextWindow: resolved ?? LOCAL_DEFAULT_CONTEXT_WINDOW,\n input: ['text'],\n }]\n },\n customFields: [\n {\n key: 'baseURL',\n label: 'Base URL',\n envVar: 'LOCAL_LLM_BASE_URL',\n placeholder: 'http://localhost:11434/v1',\n hint: 'Where your local LLM server is reachable. Examples: Ollama → http://localhost:11434/v1, vLLM → http://localhost:8000/v1, LM Studio → http://localhost:1234/v1.',\n required: true,\n },\n {\n key: 'apiKey',\n label: 'API key',\n envVar: 'LOCAL_LLM_API_KEY',\n placeholder: 'leave blank if your server is unauthenticated',\n },\n {\n key: 'model',\n label: 'Default model',\n envVar: 'LOCAL_LLM_DEFAULT_MODEL',\n placeholder: 'e.g. llama3.1:8b, qwen2.5-coder, mistral-small',\n hint: 'Model id your server exposes. Leave blank to pick later from the model picker.',\n },\n {\n key: 'contextWindow',\n label: 'Context window (tokens)',\n envVar: 'LOCAL_LLM_CONTEXT_WINDOW',\n placeholder: 'e.g. 32768 · or per-model: llama3.1:8b=32768, qwen2.5-coder=128k, 64k',\n hint: 'Context length(s) for your local model(s) — local runtimes don\\'t advertise this, so set it here to enable the context indicator and auto-compaction. Use a single value (32768, 128k) for all models, or a comma-separated map of `model=window` with an optional bare value as the fallback. Press',\n },\n ],\n contextWindowEnvVar: 'LOCAL_LLM_CONTEXT_WINDOW',\n}\n\n/**\n * Cursor models. pi-ai has no Cursor registry, so we ship a small curated\n * list (Cursor proxies these). Context windows are nominal — Cursor inference\n * isn't wired yet (the provider's `stream()` throws), so these only drive the\n * picker once the protobuf transport lands.\n */\nconst CURSOR_MODELS: readonly ModelInfo[] = [\n { id: 'claude-4.6-sonnet', name: 'Claude Sonnet 4.6', contextWindow: 200_000, reasoning: true, input: ['text', 'image'] },\n { id: 'claude-4.8-opus', name: 'Claude Opus 4.8', contextWindow: 200_000, reasoning: true, input: ['text', 'image'] },\n { id: 'gpt-5.5', name: 'GPT-5.5', contextWindow: 272_000, reasoning: true, input: ['text', 'image'] },\n { id: 'composer-2', name: 'Composer 2', contextWindow: 200_000, input: ['text'] },\n { id: 'composer-2.5', name: 'Composer 2.5', contextWindow: 200_000, input: ['text'] },\n]\n\nexport const cursorDescriptor: ProviderDescriptor = {\n key: 'cursor',\n label: 'Cursor',\n factory: cursor,\n defaultModel: 'claude-4.6-sonnet',\n // OAuth-only: no API-key env var.\n envKey: undefined,\n oauthProvider: cursorOAuthProvider,\n oauthHint: 'Cursor subscription',\n models: CURSOR_MODELS,\n}\n\n/**\n * Default provider registry. Passed verbatim when `runTui` is invoked without\n * an explicit `providers` option. Hosts that want to override per-provider\n * metadata can spread this and replace specific entries:\n *\n * ```ts\n * runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })\n * ```\n *\n * `cursor` is intentionally NOT registered here: OAuth works, but inference\n * isn't wired (`cursor.stream()` throws — Cursor speaks a protobuf/HTTP-2\n * agent protocol, not OpenAI-compat). Shipping it in the picker only lets users\n * select a model that errors every turn. The descriptor stays exported so a\n * host can opt in (`{ ...BUILTIN_PROVIDERS, cursor: cursorDescriptor }`) once\n * the transport lands — re-add it here at that point.\n */\nexport const BUILTIN_PROVIDERS: Readonly<Record<string, ProviderDescriptor>> = {\n anthropic: anthropicDescriptor,\n openai: openaiDescriptor,\n openrouter: openrouterDescriptor,\n cerebras: cerebrasDescriptor,\n xai: xaiDescriptor,\n arcee: arceeDescriptor,\n baseten: basetenDescriptor,\n zai: zaiDescriptor,\n mistral: mistralDescriptor,\n local: localDescriptor,\n}\n\n// ---------------------------------------------------------------------------\n// Model registry helper\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the model list for a given provider. Honors `descriptor.models`\n * when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns\n * `[]` for descriptors with no known mapping (custom providers without a\n * model list) — callers should hide the model picker in that case.\n */\nexport function modelsForDescriptor(descriptor: ProviderDescriptor): readonly ModelInfo[] {\n if (descriptor.models)\n return descriptor.models\n const piId = piIdOf(descriptor)\n const catalogKeys = catalogProvidersOf(descriptor)\n // A catalog whose `list` answers OWNS this provider's models — bundled\n // entries are not consulted, so it can hide as well as add. The picker\n // resolves metadata before any agent exists, so only the process-wide\n // catalog is visible here; `behavior.modelCatalog` governs the wire path.\n const owned = firstDefined(catalogKeys, key => listModels(key))\n let bundled: readonly ModelInfo[]\n if (owned) {\n // An id-less list entry is unusable downstream (the picker selects by id,\n // every lookup keys on it), so drop it with one warning rather than\n // rendering a blank selectable row. `ModelCatalogEntry.id` is optional for\n // the patch/table form, which is exactly how this mistake happens.\n const usable = owned.filter(entry => entry.id !== undefined && entry.id !== '')\n if (usable.length !== owned.length)\n warnOnce(`model catalog: dropped ${owned.length - usable.length} \"${piId}\" list entr${owned.length - usable.length === 1 ? 'y' : 'ies'} with no id`)\n bundled = usable.map(entry => toModelInfo(entry, piId))\n }\n else {\n try {\n bundled = getModels(piId as never) as readonly ModelInfo[]\n }\n catch {\n bundled = []\n }\n // Table-form entries OVERLAY: patch matching ids, append unknown ones,\n // leave everything they don't name intact.\n const overlay = dedupeById(catalogKeys.flatMap(key => listModelOverlay(key)))\n if (overlay.length > 0) {\n const known = new Set(bundled.map(m => m.id))\n bundled = [\n ...bundled.map((m) => {\n const entry = overlay.find(e => e.id === m.id)\n return entry ? mergeCatalogEntry(m, entry) : m\n }),\n ...overlay.filter(e => e.id !== undefined && !known.has(e.id)).map(entry => toModelInfo(entry, piId)),\n ]\n }\n }\n // `extraModels` is OUR stopgap for models pi-ai hasn't bundled yet — the\n // same role the catalog now fills properly. An owning catalog therefore\n // suppresses them too, otherwise a consumer couldn't hide a model we\n // hardcoded and \"owns the list\" would be a half-truth.\n if (!owned && descriptor.extraModels?.length) {\n // Merge extras ahead of the bundled list, de-duped by id (bundled wins, so a\n // model that lands in pi-ai later transparently supersedes the local entry).\n const bundledIds = new Set(bundled.map(m => m.id))\n const extras = descriptor.extraModels.filter(m => !bundledIds.has(m.id))\n bundled = [...extras, ...bundled]\n }\n return descriptor.optionsFor ? bundled.map(m => decorateOptions(m, descriptor)) : bundled\n}\n\n/** First non-`undefined` result of `resolve` across `keys`. */\nfunction firstDefined<T>(keys: readonly string[], resolve: (key: string) => T | undefined): T | undefined {\n for (const key of keys) {\n const value = resolve(key)\n if (value !== undefined)\n return value\n }\n return undefined\n}\n\n/** De-dupe catalog entries by id, first occurrence winning. */\nfunction dedupeById(entries: readonly ModelCatalogEntry[]): readonly ModelCatalogEntry[] {\n if (entries.length < 2)\n return entries\n const seen = new Set<string>()\n return entries.filter((entry) => {\n const id = entry.id ?? ''\n if (seen.has(id))\n return false\n seen.add(id)\n return true\n })\n}\n\n/**\n * Attach descriptor-resolved {@link ModelOption}s to a model that doesn't\n * already declare its own. Pure / non-mutating — returns the input untouched\n * when the model has options already or the descriptor resolves none.\n */\nfunction decorateOptions(model: ModelInfo, descriptor: ProviderDescriptor): ModelInfo {\n if (model.options?.length || !descriptor.optionsFor)\n return model\n const options = descriptor.optionsFor(model.id)\n return options?.length ? { ...model, options } : model\n}\n\n/**\n * Resolve a single model's metadata via the descriptor's model source.\n * Mirrors {@link modelsForDescriptor} routing: descriptor's own list wins,\n * pi-ai's registry is the fallback. Returns `null` when the model isn't\n * known.\n */\nexport function getModelInfo(descriptor: ProviderDescriptor, modelId: string): ModelInfo | null {\n const lookupId = descriptor.modelAliases?.[modelId] ?? modelId\n if (descriptor.models)\n return descriptor.models.find(m => m.id === lookupId) ?? null\n const piId = piIdOf(descriptor)\n const entry = firstDefined(catalogProvidersOf(descriptor), key => lookupModel(key, lookupId))\n try {\n const bundled = getModel(piId as never, lookupId as never) as ModelInfo | undefined\n if (bundled)\n return decorateOptions(mergeCatalogEntry(bundled, entry), descriptor)\n }\n catch {\n // fall through to the catalog entry, then extras\n }\n // Registry miss: a catalog entry stands on its own — that's the point, these\n // are models the bundled snapshot has never heard of.\n if (entry)\n return decorateOptions(toModelInfo({ ...entry, id: entry.id ?? lookupId }, piId), descriptor)\n const extra = descriptor.extraModels?.find(m => m.id === lookupId)\n return extra ? decorateOptions(extra, descriptor) : null\n}\n\n/**\n * De-duped warning sink. `modelsForDescriptor` runs on every picker render, so\n * a malformed catalog entry must not spam the terminal. Mirrors the\n * `[zidane:schema]` seen-set convention in `providers/schema-sanitize.ts`.\n */\nconst warnedCatalogLines = new Set<string>()\nfunction warnOnce(line: string): void {\n if (warnedCatalogLines.has(line))\n return\n warnedCatalogLines.add(line)\n console.warn(`[zidane:model-catalog] ${line}`)\n}\n\n/**\n * Catalog entry → `ModelInfo`. `contextWindow` is required on `ModelInfo` but\n * optional on an entry (most overrides only correct a ceiling or a rate), so a\n * missing one becomes `0`. That placeholder is NOT a window: `getContextWindow`\n * treats non-positive values as unknown and falls through to\n * `contextWindowEnvVar` / `null`, which is what keeps the context panel from\n * rendering percentages against a 1-token window.\n */\nfunction toModelInfo(entry: ModelCatalogEntry, provider: string): ModelInfo {\n return {\n id: entry.id ?? '',\n ...(entry.name !== undefined ? { name: entry.name } : {}),\n contextWindow: entry.contextWindow ?? 0,\n ...(entry.maxTokens !== undefined ? { maxTokens: entry.maxTokens } : {}),\n ...(entry.reasoning !== undefined ? { reasoning: entry.reasoning } : {}),\n ...(entry.input !== undefined ? { input: entry.input } : {}),\n ...(entry.cost !== undefined\n ? {\n cost: {\n input: entry.cost.input ?? 0,\n output: entry.cost.output ?? 0,\n ...(entry.cost.cacheRead !== undefined ? { cacheRead: entry.cost.cacheRead } : {}),\n ...(entry.cost.cacheWrite !== undefined ? { cacheWrite: entry.cost.cacheWrite } : {}),\n },\n }\n : {}),\n provider,\n }\n}\n\n/**\n * Parse a user-supplied context-window string into a token count.\n *\n * Accepts a plain integer (`\"32768\"`), or a number with a `k`/`m` suffix\n * (×1_000 / ×1_000_000, case-insensitive, optional space) — so `\"128k\"`\n * yields `128_000`, matching how the footer renders windows (`fmtTokens`).\n * Fractional values are floored (`\"1.5k\"` → `1500`). Returns `null` for\n * empty, malformed, zero, or negative input so callers fall through to\n * \"window unknown\" rather than a bogus ceiling.\n */\nexport function parseContextWindow(raw: string | undefined | null): number | null {\n if (raw == null)\n return null\n const trimmed = raw.trim()\n if (trimmed.length === 0)\n return null\n const match = /^(\\d+(?:\\.\\d+)?)\\s*([km])?$/i.exec(trimmed)\n if (!match)\n return null\n const suffix = match[2]?.toLowerCase()\n const mult = suffix === 'k' ? 1_000 : suffix === 'm' ? 1_000_000 : 1\n const value = Math.floor(Number.parseFloat(match[1]) * mult)\n return value >= 1 ? value : null\n}\n\n/**\n * Resolve a per-model context window from a user-supplied spec string.\n *\n * A spec is a comma-separated list of entries. An entry is either:\n * - `model=window` — a per-model override (split on the **first** `=`, so\n * model ids containing `:` or `/` survive), or\n * - a bare `window` — the fallback applied to any model not named above.\n *\n * Windows are parsed by {@link parseContextWindow} (plain int or k/m suffix).\n * Whitespace around entries and the `=` is ignored; malformed entries are\n * skipped rather than poisoning the whole spec; a later duplicate key wins.\n *\n * Lookup order for `modelId`: exact map entry → bare fallback → `null`. A\n * lone bare value (`\"32768\"`) therefore behaves as a single global window,\n * keeping the simple single-model config working.\n *\n * @example resolveContextWindow('llama3.1:8b=32768, qwen=128k, 64k', 'qwen') // 128000\n */\nexport function resolveContextWindow(spec: string | undefined | null, modelId: string): number | null {\n if (spec == null)\n return null\n const overrides = new Map<string, number>()\n let fallback: number | null = null\n for (const entry of spec.split(',')) {\n const eq = entry.indexOf('=')\n if (eq === -1) {\n const bare = parseContextWindow(entry)\n if (bare != null)\n fallback = bare\n continue\n }\n const key = entry.slice(0, eq).trim()\n const window = parseContextWindow(entry.slice(eq + 1))\n if (key.length > 0 && window != null)\n overrides.set(key, window)\n }\n return overrides.get(modelId) ?? fallback\n}\n\n/**\n * Look up the model's max context window via the descriptor's model source.\n * Falls back to {@link ProviderDescriptor.contextWindowEnvVar} (parsed via\n * {@link resolveContextWindow}, which supports a per-model map) when the\n * registry has nothing — this is how local LLMs, whose runtimes don't\n * advertise a window, get one. Returns `null` when neither source resolves a\n * value (custom slugs, providers without a registry or a configured window);\n * callers should hide the context indicator and skip auto-compaction then.\n */\nexport function getContextWindow(descriptor: ProviderDescriptor, modelId: string): number | null {\n // `> 0`, not just non-null: a model record can carry a placeholder `0` (a\n // catalog entry that only corrects a ceiling or a rate, a descriptor entry\n // with no advertised window). Returning it would make the caller treat 0 as\n // a REAL window — `effectiveContextWindow(0)` clamps to 1 token, and the\n // context panel then renders absurd percentages instead of hiding.\n const fromRegistry = getModelInfo(descriptor, modelId)?.contextWindow\n if (fromRegistry != null && fromRegistry > 0)\n return fromRegistry\n if (descriptor.contextWindowEnvVar)\n return resolveContextWindow(process.env[descriptor.contextWindowEnvVar], modelId)\n return null\n}\n\n// `OUTPUT_RESERVE_TOKENS` + `effectiveContextWindow` moved to the core\n// `src/compact/policy.ts` so the agent loop (which can't import `src/chat/`)\n// shares the same window math as the chat footer + auto-compact trigger.\n// Re-exported here so existing `chat/providers` importers keep working.\nexport { effectiveContextWindow, OUTPUT_RESERVE_TOKENS } from '../compact/policy'\n\n/**\n * Whether the given model exposes a reasoning / extended-thinking knob.\n * Drives the TUI's effort picker visibility — the `ctrl+n` shortcut and\n * the bottom-bar `effortName` segment only surface when this is `true`.\n * Returns `false` for unknown models (no registry entry → no advertised\n * capability).\n */\nexport function modelSupportsReasoning(descriptor: ProviderDescriptor, modelId: string): boolean {\n return getModelInfo(descriptor, modelId)?.reasoning === true\n}\n\n/**\n * Custom {@link ModelOption}s the given model supports (e.g. fast mode), or `[]`\n * when none. Drives the TUI/GUI options picker visibility — surfaces only when\n * non-empty.\n */\nexport function modelOptionsFor(descriptor: ProviderDescriptor, modelId: string): readonly ModelOption[] {\n return getModelInfo(descriptor, modelId)?.options ?? []\n}\n\n/**\n * Normalize a model-options blob to an enabled-only `{ id: true }` map.\n *\n * Single source of truth shared by every surface that reads or persists model\n * options — the TUI run path, the GUI engine reader, and the GUI IPC handlers.\n * Tolerant of arbitrary input (persisted JSON metadata may be anything): a\n * non-object → `{}`, and only entries strictly equal to `true` survive. This\n * keeps the persisted shape minimal (no `{ fast: false }` noise) and means\n * callers never forward a disabled option to `agent.run`.\n */\nexport function enabledModelOptions(raw: unknown): Record<string, boolean> {\n if (!raw || typeof raw !== 'object')\n return {}\n const out: Record<string, boolean> = {}\n for (const [id, on] of Object.entries(raw as Record<string, unknown>)) {\n if (on === true)\n out[id] = true\n }\n return out\n}\n\n/**\n * Resolve a model's remembered options for a (re)pick: keep only options the\n * model still declares AND that are enabled, dropping any that no longer apply\n * (e.g. switching away from a model that supported `fast`). Returns `undefined`\n * when nothing applies so callers can omit the field entirely.\n *\n * Shared by the TUI pick path and the launch-time resume path so \"which options\n * survive a model switch / relaunch\" is decided in exactly one place.\n */\nexport function restoreModelOptions(\n descriptor: ProviderDescriptor,\n modelId: string,\n remembered: Record<string, Record<string, boolean>> | undefined,\n): Record<string, boolean> | undefined {\n const validIds = new Set(modelOptionsFor(descriptor, modelId).map(o => o.id))\n if (validIds.size === 0)\n return undefined\n const enabled = enabledModelOptions(remembered?.[modelId])\n const filtered: Record<string, boolean> = {}\n for (const id of Object.keys(enabled)) {\n if (validIds.has(id))\n filtered[id] = true\n }\n return Object.keys(filtered).length > 0 ? filtered : undefined\n}\n","/**\n * Credential storage for the TUI.\n *\n * Lives at `<dataDir>/credentials.json` (default `~/.zidane/credentials.json`,\n * overridable via `ZIDANE_STORAGE_DIR`). Owner-only (0o600) so refresh tokens\n * and API keys don't leak on shared boxes.\n *\n * Two credential kinds:\n * - `apikey` — a plain API key entered by the user (e.g. via the setup wizard).\n * - `oauth` — tokens obtained via a provider's OAuth flow (refresh-aware).\n *\n * **Schema note.** File keys come from {@link ProviderDescriptor.credentialFileKey}\n * (falling back to `descriptor.key`). They must match what the harness providers\n * look up at runtime — for built-ins this is `anthropic`, `openai-codex`,\n * `openrouter`, `cerebras`. Hosts that ship custom providers control the file\n * key via the descriptor.\n *\n * The legacy `cwd/.credentials.json` (written by `bun run auth` in older\n * versions) is migrated on first read if the new file doesn't exist yet. The\n * legacy file is left in place — we don't delete user data behind their back.\n */\n\nimport type { ProviderDescriptor } from './providers'\nimport { existsSync, readFileSync, realpathSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, resolve } from 'node:path'\nimport { writeFileAtomic } from '../atomic-write'\nimport { ALTERNATE_CRED_KEY_SUFFIX, alternateCredKey, isReservedCredKey } from './cred-keys'\nimport { credKeyOf } from './providers'\n\nexport { ALTERNATE_CRED_KEY_SUFFIX, alternateCredKey, isReservedCredKey } from './cred-keys'\n\n/** POSIX mode for the credentials file. Ignored on Windows. */\nconst FILE_MODE = 0o600\n\nexport interface ApiKeyCredential {\n kind: 'apikey'\n /**\n * Bearer API key. May be empty when the descriptor has no `envKey` and\n * authenticates entirely via {@link customFields} (e.g. the `local`\n * provider, where the auth signal is the configured base URL).\n */\n value: string\n /**\n * Free-form fields collected by the wizard from the descriptor's\n * `customFields` definitions. Keys match the descriptor field's `key`.\n * Mirrored into `process.env[field.envVar]` by `applyApiKeyEnv` so the\n * provider's factory can read them via the existing env-var convention.\n */\n customFields?: Record<string, string>\n}\nexport interface OAuthCredential {\n kind: 'oauth'\n access: string\n refresh?: string\n expires?: number\n /** Provider-specific extras (e.g. OpenAI Codex `accountId`). */\n [extra: string]: unknown\n}\nexport type ProviderCredential = ApiKeyCredential | OAuthCredential\n\n/** Top-level shape of `credentials.json` — keys are credential-file keys. */\nexport type CredentialsFile = Record<string, ProviderCredential>\n\n/**\n * Resolve the credentials file path given the resolved TUI data directory\n * (typically `~/.zidane`, i.e. `config.paths.dir`).\n *\n * Matches the convention used elsewhere in the TUI (sessions.db, state.json)\n * so a single `ZIDANE_STORAGE_DIR` override moves the entire data root.\n */\nexport function credentialsPath(dataDir: string): string {\n return resolve(dataDir, 'credentials.json')\n}\n\n/**\n * Read credentials from disk.\n *\n * Returns `{}` when the file is missing or corrupt (last-ditch tolerance —\n * a hand-edit gone wrong shouldn't lock the user out of re-authing). On first\n * call with no file present, attempts a migration from `cwd/.credentials.json`\n * (the legacy location used by `bun run auth`).\n */\nexport function readCredentials(dataDir: string): CredentialsFile {\n const path = credentialsPath(dataDir)\n\n if (!existsSync(path)) {\n const migrated = migrateLegacyFile(path)\n if (migrated)\n return migrated\n return {}\n }\n\n try {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw)\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))\n return {}\n return normalizeCredentials(parsed as Record<string, unknown>)\n }\n catch {\n return {}\n }\n}\n\n/**\n * Re-tag legacy / refresh-stripped entries so the discriminated union holds.\n *\n * An untagged object with a string `access` is an OAuth credential — either the\n * pre-TUI `bun run auth` shape, or one whose `kind` was dropped when a token\n * refresh was persisted (a provider refresh returns no `kind`). Without\n * re-tagging, {@link detectAuth} and {@link applyApiKeyEnv} stop recognizing it\n * and force a spurious re-login. Mirrors the provider-side tolerance in\n * `readOAuthCredentials`. Entries with an explicit `kind` and anything we can't\n * classify pass through untouched.\n */\nfunction normalizeCredentials(parsed: Record<string, unknown>): CredentialsFile {\n const result: CredentialsFile = {}\n for (const [key, value] of Object.entries(parsed)) {\n // Only re-tag entries with no explicit `kind`; `isOAuthLegacy` (shared with\n // the legacy-file migration) is the \"object carrying an access token\" guard.\n const untagged = !!value && typeof value === 'object' && (value as Record<string, unknown>).kind === undefined\n if (untagged && isOAuthLegacy(value))\n result[key] = { ...value, kind: 'oauth' }\n else\n result[key] = value as ProviderCredential\n }\n return result\n}\n\n/**\n * Canonical file key for a descriptor, rejecting the reserved alternate suffix.\n *\n * Loud on a misconfigured provider instead of quietly mixing its credentials\n * into another provider's alternate slot (extension registration refuses the\n * suffix upfront; this covers descriptors passed in directly). Read paths stay\n * tolerant — only the functions that can WRITE a pair go through here.\n */\nfunction assertableCredKey(descriptor: ProviderDescriptor): string {\n const fileKey = credKeyOf(descriptor)\n if (isReservedCredKey(fileKey)) {\n throw new Error(\n `Provider \"${descriptor.key}\" uses credential key \"${fileKey}\", but the `\n + `\"${ALTERNATE_CRED_KEY_SUFFIX}\" suffix is reserved for a provider's second credential slot.`,\n )\n }\n return fileKey\n}\n\n/** Read a single provider's credential (translating via the descriptor). */\nexport function readProviderCredential(dataDir: string, descriptor: ProviderDescriptor): ProviderCredential | undefined {\n return readCredentials(dataDir)[credKeyOf(descriptor)]\n}\n\n/**\n * Both stored credentials for a provider: `active` is the one every reader\n * resolves, `alternate` is the stored-but-not-preferred one (if any).\n */\nexport function readProviderCredentialPair(\n dataDir: string,\n descriptor: ProviderDescriptor,\n): { active?: ProviderCredential, alternate?: ProviderCredential } {\n const all = readCredentials(dataDir)\n const fileKey = credKeyOf(descriptor)\n return { active: all[fileKey], alternate: all[alternateCredKey(fileKey)] }\n}\n\n/**\n * Write credentials atomically (write-then-rename) with mode 0o600.\n *\n * Atomic on the same filesystem — readers either see the previous file or the\n * new one, never a half-written intermediate. Creates the parent dir if needed\n * (first launch on a fresh machine: `~/.zidane/` may not exist yet).\n */\nexport function writeCredentials(dataDir: string, creds: CredentialsFile): void {\n writeFileAtomic(\n credentialsPath(dataDir),\n `${JSON.stringify(creds, null, 2)}\\n`,\n { ensureDir: true, mode: FILE_MODE },\n )\n}\n\n/**\n * Store a credential as the provider's preferred one.\n *\n * A credential of the OTHER kind already in the canonical slot is demoted to\n * {@link alternateCredKey}, not destroyed: authenticating with OAuth used to\n * silently delete a working API key (and vice versa), leaving the user to\n * re-enter it. Replacing a credential of the SAME kind is a plain overwrite —\n * that's a re-auth, not a second source.\n *\n * The pair holds AT MOST ONE credential of each kind. A same-kind alternate is\n * therefore stale — no code path here can produce one, but a file written by an\n * older build can carry it, and it would surface as two identical source rows\n * that \"switch\" between each other. Dropping it on the next write self-heals.\n */\nexport function setProviderCredential(\n dataDir: string,\n descriptor: ProviderDescriptor,\n cred: ProviderCredential,\n): void {\n const all = readCredentials(dataDir)\n const fileKey = assertableCredKey(descriptor)\n const altKey = alternateCredKey(fileKey)\n const previous = all[fileKey]\n if (previous && previous.kind !== cred.kind)\n all[altKey] = previous\n else if (all[altKey]?.kind === cred.kind)\n delete all[altKey]\n all[fileKey] = cred\n writeCredentials(dataDir, all)\n}\n\n/**\n * Make the provider's stored `source` credential the preferred one.\n *\n * No-op unless a credential of that kind is sitting in the alternate slot, so\n * calling it for the already-active source is harmless.\n */\nexport function setPreferredCredential(\n dataDir: string,\n descriptor: ProviderDescriptor,\n source: ProviderCredential['kind'],\n): void {\n const all = readCredentials(dataDir)\n const fileKey = assertableCredKey(descriptor)\n const altKey = alternateCredKey(fileKey)\n const alternate = all[altKey]\n if (!alternate || alternate.kind !== source)\n return\n const active = all[fileKey]\n all[fileKey] = alternate\n if (active)\n all[altKey] = active\n else\n delete all[altKey]\n writeCredentials(dataDir, all)\n}\n\n/**\n * Sign a provider out — drops BOTH stored credentials.\n *\n * \"Signed out\" has to mean the provider resolves nothing afterwards, which is\n * what this has always guaranteed and what callers named it for; promoting the\n * alternate instead would silently leave the provider authenticated through the\n * other source. Callers that want the fall-back behavior can read the pair\n * first ({@link readProviderCredentialPair}) and re-store the survivor.\n */\nexport function removeProviderCredential(dataDir: string, descriptor: ProviderDescriptor): void {\n const all = readCredentials(dataDir)\n const fileKey = credKeyOf(descriptor)\n const altKey = alternateCredKey(fileKey)\n if (!(fileKey in all) && !(altKey in all))\n return\n delete all[fileKey]\n delete all[altKey]\n writeCredentials(dataDir, all)\n}\n\n/**\n * Reconcile `process.env` with the stored credentials so the harness providers\n * pick up the wizard's output via their existing env-var resolution. Called\n * once at TUI launch after the credentials file has been resolved.\n *\n * **Precedence: stored credential > ambient env.** When the user has run the\n * auth wizard, that's an explicit, recent action — it MUST win over whatever\n * ambient value is already in `process.env`. Otherwise an upgrade reliably\n * breaks for anyone whose cwd happens to contain a stale `.env`: Bun\n * auto-loads `.env` from cwd into `process.env` before our code runs, the\n * legacy `bun run auth` used to upsert tokens into `cwd/.env`, and even a\n * one-off `export ANTHROPIC_API_KEY=…` from months ago in the user's shell rc\n * can outlive its usefulness. None of those should override a key the user\n * just typed into the wizard.\n *\n * Three cases per registered provider with an `envKey`:\n *\n * 1. Stored `kind: 'apikey'` → overwrite env with the stored value. Wizard\n * wins. A debug log fires when this clobbers a different ambient value.\n * 2. Stored `kind: 'oauth'` → DELETE env, so {@link resolveOAuthApiKey}\n * falls through to the file-read + refresh path. A stale OAuth token\n * in env would otherwise short-circuit refresh and get rejected.\n * 3. No stored entry → leave env alone. Env-only setups (no wizard run yet,\n * hosts driving zidane purely from secrets management) keep working.\n *\n * To opt out and force ambient env to win again, the user signs the provider\n * out via the wizard (which deletes the stored entry → case 3 above).\n *\n * Descriptors without an `envKey` (OAuth-only providers, custom providers\n * that bypass env-var resolution) are skipped silently.\n */\nexport function applyApiKeyEnv(\n dataDir: string,\n registry: Readonly<Record<string, ProviderDescriptor>>,\n opts: { log?: (message: string) => void } = {},\n): void {\n // Diagnostics default to stderr, which is correct for the ONE launch-time\n // call this was written for — it runs before the renderer owns the terminal.\n // Callers that run while a full-screen TUI is up (a credential swap from the\n // settings modal) must pass a sink instead: a raw stderr write mid-render\n // paints straight over the frame.\n const log = opts.log ?? (message => void process.stderr.write(message))\n const creds = readCredentials(dataDir)\n for (const descriptor of Object.values(registry)) {\n const cred = creds[credKeyOf(descriptor)]\n\n // Mirror customFields → env regardless of whether the descriptor has an\n // envKey. Same \"stored beats ambient\" semantics as the API-key path.\n if (cred?.kind === 'apikey' && descriptor.customFields) {\n const stored = cred.customFields ?? {}\n for (const field of descriptor.customFields) {\n const value = stored[field.key]\n if (typeof value === 'string' && value.length > 0) {\n const ambient = process.env[field.envVar]\n if (ambient && ambient !== value && process.env.ZIDANE_DEBUG) {\n log(\n `[zidane/chat] applyApiKeyEnv: overriding ambient \\`${field.envVar}\\` `\n + `with stored value from credentials.json (provider=${descriptor.key}, field=${field.key}).\\n`,\n )\n }\n process.env[field.envVar] = value\n }\n }\n }\n\n if (!descriptor.envKey)\n continue\n const envKey = descriptor.envKey\n const ambient = process.env[envKey]\n\n if (cred?.kind === 'apikey' && cred.value) {\n if (ambient && ambient !== cred.value && process.env.ZIDANE_DEBUG) {\n log(\n `[zidane/chat] applyApiKeyEnv: overriding ambient \\`${envKey}\\` `\n + `with stored API key from credentials.json (provider=${descriptor.key}). `\n + `Sign out via the auth wizard if the ambient value was intended to win.\\n`,\n )\n }\n process.env[envKey] = cred.value\n continue\n }\n\n if (cred?.kind === 'oauth' && cred.access) {\n if (ambient !== undefined && process.env.ZIDANE_DEBUG) {\n log(\n `[zidane/chat] applyApiKeyEnv: clearing ambient \\`${envKey}\\` because `\n + `credentials.json has stored OAuth for ${descriptor.key} — refresh path needs the file.\\n`,\n )\n }\n delete process.env[envKey]\n continue\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Legacy migration\n// ---------------------------------------------------------------------------\n\n/**\n * `bun run auth` (pre-TUI) wrote `cwd/.credentials.json` with an entry per\n * provider mapping directly to an OAuthCredentials payload, e.g.:\n *\n * {\n * \"anthropic\": { \"access\": \"...\", \"refresh\": \"...\", \"expires\": 123 },\n * \"openai-codex\": { \"access\": \"...\", \"refresh\": \"...\", \"expires\": 123, \"accountId\": \"...\" }\n * }\n *\n * We don't delete the legacy file — it might still be used by a host that\n * imports the harness directly. We just copy its contents into the new\n * location under the kind-tagged shape so the TUI picks them up.\n *\n * Migration is provider-agnostic: any top-level entry with an `access` field\n * is preserved verbatim (extras included), under the same key. The TUI's\n * detection then looks them up via the matching descriptor's `credentialFileKey`.\n *\n * Returns the migrated credentials when the migration ran, or `null` when\n * there's no legacy file to migrate.\n */\nfunction migrateLegacyFile(targetPath: string): CredentialsFile | null {\n const legacyPath = resolve(process.cwd(), '.credentials.json')\n // Only trust a legacy file that sits directly in the user's home dir (where\n // `bun run auth` was typically run) or next to the migration target. A\n // cwd-relative read from an arbitrary checkout would let an untrusted repo\n // plant a `.credentials.json` that gets persisted as the USER-LEVEL\n // credential file. Compared via realpath so symlinked dirs (e.g. macOS\n // `/var` → `/private/var`) don't defeat the check.\n const real = (p: string): string => {\n try {\n return realpathSync(p)\n }\n catch {\n return p\n }\n }\n const legacyDir = real(dirname(legacyPath))\n if (legacyDir !== real(homedir()) && legacyDir !== real(dirname(resolve(targetPath))))\n return null\n if (!existsSync(legacyPath))\n return null\n\n let legacy: Record<string, unknown>\n try {\n legacy = JSON.parse(readFileSync(legacyPath, 'utf-8'))\n }\n catch {\n return null\n }\n if (!legacy || typeof legacy !== 'object' || Array.isArray(legacy))\n return null\n\n const migrated: CredentialsFile = {}\n for (const [fileKey, value] of Object.entries(legacy)) {\n if (!isOAuthLegacy(value))\n continue\n const { access, refresh, expires, ...extras } = value\n migrated[fileKey] = {\n kind: 'oauth',\n access,\n ...(typeof refresh === 'string' ? { refresh } : {}),\n ...(typeof expires === 'number' ? { expires } : {}),\n ...extras,\n }\n }\n\n if (Object.keys(migrated).length === 0)\n return null\n\n // Persist immediately so subsequent reads use the new file directly.\n writeFileAtomic(\n targetPath,\n `${JSON.stringify(migrated, null, 2)}\\n`,\n { ensureDir: true, mode: FILE_MODE },\n )\n\n return migrated\n}\n\nfunction isOAuthLegacy(value: unknown): value is { access: string, refresh?: string, expires?: number, [extra: string]: unknown } {\n return (\n typeof value === 'object'\n && value !== null\n && 'access' in value\n && typeof (value as { access: unknown }).access === 'string'\n )\n}\n","import type { ProviderCredential } from './credentials'\nimport type { ProviderDescriptor } from './providers'\nimport { alternateCredKey, readCredentials } from './credentials'\nimport { credKeyOf } from './providers'\n\n/**\n * Provider identifier as known to the TUI. Built-in keys are `anthropic`,\n * `openai`, `openrouter`, `cerebras`, but hosts can register additional\n * keys via {@link ProviderDescriptor} — so the type is open.\n */\nexport type ProviderKey = string\n\nexport interface AuthMethod {\n source: 'env' | 'oauth' | 'apikey'\n /** Human-readable detail (env var name, expiry timestamp, …). */\n detail: string\n /**\n * True for the credential every reader actually resolves.\n *\n * Exactly one STORED credential is active: the one in the canonical file key\n * (see `alternateCredKey`). A provider can now hold both an API key and an\n * OAuth grant, so the list alone no longer says which one wins. `env` is\n * never marked active — it is an observable signal, not a stored credential\n * the user can promote.\n *\n * Optional so hosts that build `ProviderAuth` themselves (this type is public\n * via `zidane/chat`) keep compiling; absent reads as \"not active\".\n * {@link detectAuth} always sets it.\n */\n active?: boolean\n}\n\nexport interface ProviderAuth {\n key: ProviderKey\n label: string\n /** True when at least one credential source is present. */\n available: boolean\n methods: AuthMethod[]\n /**\n * Env var this provider reads, when it declares one.\n *\n * Carried separately from {@link methods} because the settings list shows the\n * env slot even when it holds nothing. `methods` only ever lists sources that\n * are actually present, since `available` is derived from it.\n */\n envKey?: string\n}\n\n/**\n * Detect available auth for every registered provider.\n *\n * A method appears in `methods` for every layer that has a credential:\n *\n * - `kind: 'apikey'` from `credentials.json` (injected into env at TUI launch)\n * - explicit env var (descriptor's `envKey`)\n * - `kind: 'oauth'` from `credentials.json` (or legacy `cwd/.credentials.json`)\n *\n * A provider may store one of EACH kind. The preferred one lives in the\n * canonical file key and is flagged {@link AuthMethod.active}; the other sits\n * in `alternateCredKey` and is reported too, so the UI can offer to promote it.\n * Order within `methods` is presentational — it is no longer a resolution\n * order, because the active flag answers that directly.\n *\n * Pure read — never refreshes or rewrites the credentials file.\n */\nexport function detectAuth(\n dataDir: string,\n registry: Readonly<Record<string, ProviderDescriptor>>,\n env: Record<string, string | undefined> = process.env,\n): ProviderAuth[] {\n const creds = readCredentials(dataDir)\n\n return Object.values(registry).map((descriptor) => {\n const methods: AuthMethod[] = []\n const fileKey = credKeyOf(descriptor)\n const fileEntry = creds[fileKey] as ProviderCredential | undefined\n const altEntry = creds[alternateCredKey(fileKey)] as ProviderCredential | undefined\n\n // 1. Stored API key / OAuth. Both slots go through the same description so\n // a demoted credential renders identically to a preferred one, differing\n // only in `active`.\n const describeStored = (entry: ProviderCredential | undefined, active: boolean): AuthMethod | undefined => {\n if (entry?.kind === 'apikey' && entry.value)\n return { source: 'apikey', detail: 'credentials.json', active }\n if (entry?.kind === 'oauth' && entry.access) {\n const detail = typeof entry.expires === 'number'\n ? `oauth · expires ${new Date(entry.expires).toLocaleString()}`\n : 'oauth · credentials.json'\n return { source: 'oauth', detail, active }\n }\n return undefined\n }\n\n const activeMethod = describeStored(fileEntry, true)\n if (activeMethod)\n methods.push(activeMethod)\n\n // 1b. customFields-only credential (descriptor has no `envKey` and is\n // authenticated entirely by configuration — e.g. the `local` provider,\n // where the auth signal is the configured base URL). The credential\n // is \"present\" when every required field has a stored value.\n if (\n fileEntry?.kind === 'apikey'\n && !fileEntry.value\n && descriptor.customFields\n && descriptor.customFields.length > 0\n ) {\n const stored = fileEntry.customFields ?? {}\n const required = descriptor.customFields.filter(f => f.required)\n const allRequiredPresent = required.every(\n f => typeof stored[f.key] === 'string' && (stored[f.key] as string).length > 0,\n )\n if (allRequiredPresent) {\n methods.push({ source: 'apikey', detail: 'credentials.json (configured)', active: true })\n }\n }\n\n // 1c. Env-only custom field configuration. This lets local/OpenAI-compatible\n // endpoints participate in auth/model detection when configured through\n // `LOCAL_LLM_BASE_URL` without first writing credentials.json.\n if (descriptor.customFields && descriptor.customFields.length > 0) {\n const required = descriptor.customFields.filter(f => f.required)\n const allRequiredPresent = required.length > 0 && required.every(\n f => typeof env[f.envVar] === 'string' && (env[f.envVar] as string).length > 0,\n )\n\n if (allRequiredPresent) {\n methods.push({ source: 'env', detail: required.map(f => f.envVar).join(', '), active: false })\n }\n }\n\n // 2. The stored-but-not-preferred credential, offered so the UI can promote\n // it. Listed before env so both stored credentials sit together.\n const alternateMethod = describeStored(altEntry, false)\n if (alternateMethod)\n methods.push(alternateMethod)\n\n // 3. Env var (may be set by `applyApiKeyEnv` from a stored key, or by the\n // user's shell. Either way, treated as the same observable signal.)\n if (descriptor.envKey && env[descriptor.envKey])\n methods.push({ source: 'env', detail: descriptor.envKey, active: false })\n\n // `source` IS the selection identity — `onSetPreferredCredential` takes one\n // and the renderer keys rows by it — so two methods sharing a source are not\n // a choice the user can express, they are a duplicate. Several shapes can\n // produce one: a same-kind alternate written by an older build, or a\n // customFields-configured credential sitting beside a stored alternate.\n // Collapsing here fixes every caller at once, and keeps the FIRST, which the\n // order above makes the active/preferred one.\n const unique: AuthMethod[] = []\n for (const method of methods) {\n if (!unique.some(seen => seen.source === method.source))\n unique.push(method)\n }\n\n return {\n key: descriptor.key,\n label: descriptor.label,\n available: unique.length > 0,\n methods: unique,\n ...(descriptor.envKey ? { envKey: descriptor.envKey } : {}),\n }\n })\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"contexts-DWVt7MvF.js","names":[],"sources":["../src/contexts/process.ts","../src/contexts/types.ts"],"sourcesContent":["/**\n * In-process execution context.\n *\n * Runs everything in the current Node/Bun process.\n * No isolation — fastest, used as the default.\n */\n\nimport type { Buffer } from 'node:buffer'\nimport type { ChildProcess } from 'node:child_process'\nimport type { WriteStream } from 'node:fs'\nimport type { CappedSlot } from './output-cap'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesEntry, SpawnConfig, StreamHandle, TaskEntry, TaskExitInfo, TaskHandle, TaskStallInfo } from './types'\nimport { createWriteStream } from 'node:fs'\nimport { mkdir, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, relative, resolve } from 'node:path'\nimport { lazyAsync } from '../lazy'\nimport { globFilesLocal, globLiteralPrefix } from './glob'\nimport { appendCapped, DEFAULT_MAX_OUTPUT_BUFFER, outputExceededMessage } from './output-cap'\n\n/**\n * Lazily load `node:child_process`'s `spawn` (memoized after first use).\n *\n * Deliberately NOT a static top-level import. `createAgent` pulls this module\n * into the graph as the default execution context, so a static\n * `import … from 'node:child_process'` would make merely importing zidane\n * depend on subprocess support. Runtimes that gate that builtin at import time\n * — Supabase Edge Functions, Cloudflare `nodejs_compat`, other shell-less V8\n * isolates — would then fail to load the package at all. Deferring the import\n * to the first `exec` keeps `import 'zidane'` + the provider loop runnable\n * there; only actually invoking a shell command touches subprocess, which is\n * exactly where an unsupported host should fail (the agent loop turns that\n * throw into a clean tool-result error).\n */\nconst loadSpawn = lazyAsync(async () => (await import('node:child_process')).spawn)\n\n/**\n * Whether the host supports POSIX process groups (the `detached: true` +\n * `process.kill(-pid)` combination). Windows doesn't — its job-object\n * model is shaped differently — so on win32 we fall back to killing the\n * shell wrapper alone (matches pre-fix behavior; better than nothing).\n */\nexport const SUPPORTS_PROCESS_GROUPS = process.platform !== 'win32'\n\n/**\n * Default cap on captured stdout / stderr per child. Matches the\n * pre-fix `child_process.exec` setting so existing callers see the\n * same buffer envelope. Output beyond this is truncated and a\n * marker is appended to stderr.\n */\n\n/**\n * How long `destroy()` waits for a SIGTERM'd background task to settle\n * before escalating to SIGKILL and abandoning the wait. Matches the MCP\n * connection's default `closeTimeout` so `agent.destroy()` stays bounded\n * by a single grace period regardless of which leg is slowest.\n */\nconst DESTROY_SIGTERM_GRACE_MS = 5_000\n\n/**\n * How long an `exec` kill (timeout / abort / output-overflow) waits after\n * SIGTERM before escalating the process group to SIGKILL. A child that\n * traps or ignores TERM (docker CLI parked on a container, a shell trap,\n * an uninterruptible wait) otherwise never exits — the exec promise never\n * settles and the caller's transport deadline fires instead of a clean\n * `command timed out` result (prod incident 2026-07-17).\n */\nconst EXEC_KILL_ESCALATION_GRACE_MS = 5_000\n\n/**\n * How long after the child's `exit` event we keep waiting for `close`\n * before settling with the output collected so far.\n *\n * `close` fires only once every stdio pipe has closed — and a\n * daemonized grandchild that inherited stdout/stderr (a dev server\n * started with `&`, `nohup` without redirection, simulator helpers, …)\n * keeps those pipes open indefinitely. Pre-fix, a foreground `exec`\n * whose command finished in milliseconds would hang until its timeout,\n * then `killProcessGroup` took the intentionally-backgrounded daemon\n * down with it. The process itself is done at `exit`; this grace only\n * exists to let normally-closing pipes flush their tail bytes.\n */\nconst EXIT_PIPE_DRAIN_GRACE_MS = 1_500\n\n/**\n * Sanitize a task id before it's joined into a filesystem path.\n *\n * We mint `bash_<n>` ids ourselves (no user input flows into the path\n * for `ProcessContext`), so this is defensive — but third-party contexts\n * MAY accept caller-provided ids, so the helper exists for them too.\n * Anything that doesn't match the expected shape is rejected — never\n * coerced — so the call site sees a clear error rather than a\n * traversal-shaped path.\n */\nconst TASK_ID_RE = /^[a-z][\\w-]*$/i\n\nfunction assertSafeTaskId(taskId: string): void {\n if (!TASK_ID_RE.test(taskId))\n throw new Error(`Invalid task id \"${taskId}\" — must match ${TASK_ID_RE}.`)\n}\n\n/**\n * Format `date` as `YYYYMMDD-HHMMSS-mmm` in UTC.\n *\n * Pinned to UTC so the lexical sort of two timestamps always matches\n * their chronological order (local time + DST does not). Used as the\n * per-context suffix on background-task log filenames; see the field\n * doc on `contextTimestamp` for the why.\n */\nexport function formatContextTimestamp(date: Date): string {\n const pad2 = (n: number): string => n.toString().padStart(2, '0')\n const pad3 = (n: number): string => n.toString().padStart(3, '0')\n const y = date.getUTCFullYear()\n const M = pad2(date.getUTCMonth() + 1)\n const d = pad2(date.getUTCDate())\n const h = pad2(date.getUTCHours())\n const m = pad2(date.getUTCMinutes())\n const s = pad2(date.getUTCSeconds())\n const ms = pad3(date.getUTCMilliseconds())\n return `${y}${M}${d}-${h}${m}${s}-${ms}`\n}\n\n/** Pattern of a background-task log filename. Used by tests + tooling. */\nexport const TASK_LOG_FILENAME_RE = /^(bash_\\d+)\\.(\\d{8}-\\d{6}-\\d{3})\\.log$/\n\n/** Copy only the named keys (when present) from an env object. */\nfunction pickEnv(env: NodeJS.ProcessEnv, keys: string[]): NodeJS.ProcessEnv {\n const out: NodeJS.ProcessEnv = {}\n for (const key of keys) {\n if (env[key] !== undefined)\n out[key] = env[key]\n }\n return out\n}\n\nexport function createProcessContext(config?: SpawnConfig): ExecutionContext {\n let counter = 0\n const handles = new Map<string, ExecutionHandle>()\n const defaultCwd = config?.cwd ?? process.cwd()\n const defaultEnv = config?.env\n const destroyGraceMs = config?.destroyGraceMs ?? DESTROY_SIGTERM_GRACE_MS\n\n // Base environment children inherit. Defaults to the full parent env\n // (back-compat); when `inheritEnv: false`, only a minimal allow-list is\n // forwarded so parent secrets (e.g. API keys Bun loaded from `.env`) aren't\n // exposed to tool commands. Explicit `env` / per-call env always apply on top.\n const baseEnv: NodeJS.ProcessEnv = config?.inheritEnv === false\n ? pickEnv(process.env, ['PATH', 'HOME', 'SHELL', 'LANG', 'LC_ALL', 'USER', 'TERM', 'TMPDIR'])\n : process.env\n\n // Optional workspace containment for the file methods. Unset = no checks.\n const workspaceRoot = config?.workspaceRoot !== undefined\n ? resolve(defaultCwd, config.workspaceRoot)\n : undefined\n\n /** True when `target` is the root itself or lives under it. */\n function isWithinRoot(root: string, target: string): boolean {\n const rel = relative(root, target)\n return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))\n }\n\n /**\n * Resolve `path` against the handle cwd and, when a `workspaceRoot` is\n * configured, reject anything that escapes it. Uses `realpath` on the\n * nearest existing ancestor so symlink escapes (and `..`) are caught for\n * both existing and not-yet-created targets.\n */\n async function resolveContained(handle: ExecutionHandle, path: string): Promise<string> {\n const full = resolve(handle.cwd, path)\n if (workspaceRoot === undefined)\n return full\n\n // Canonicalize the deepest existing ancestor to defeat symlink escapes;\n // the non-existent tail (for writes) is appended back verbatim.\n let existing = full\n const tail: string[] = []\n while (true) {\n try {\n existing = await realpath(existing)\n break\n }\n catch {\n const parent = dirname(existing)\n if (parent === existing) {\n // Reached the filesystem root without an existing ancestor.\n existing = parent\n break\n }\n // `relative` (not slice) so the segment is correct even on Windows\n // drive roots where `dirname('C:\\\\foo')` is `C:\\\\` (no 1-char sep).\n tail.unshift(relative(parent, existing))\n existing = parent\n }\n }\n const canonical = tail.length ? resolve(existing, ...tail) : existing\n const canonicalRoot = await realpath(workspaceRoot).catch(() => workspaceRoot)\n if (!isWithinRoot(canonicalRoot, canonical)) {\n throw new Error(\n `Path escapes the workspace root: ${path} (resolved outside ${canonicalRoot})`,\n )\n }\n return full\n }\n\n /**\n * Per-context background-task registry. Entries live for the context's\n * lifetime — even after the child exits — so the model can read output\n * of completed tasks until `destroy()` tears everything down. Kept as\n * a plain Map (not a class) per code-quality checklist #7: no premature\n * abstraction. The state and the operations on it live inline.\n */\n const tasks = new Map<string, TaskState>()\n let taskCounter = 0\n\n /**\n * Per-context UTC timestamp segment baked into every background task's\n * log filename. Same `taskCounter` value across two contexts (e.g. a\n * TUI restart) would otherwise re-open the SAME `bash_<n>.log` file —\n * we open with `flags: 'a'` so the new task would APPEND into the old\n * log, producing scrambled output. The timestamp guarantees each\n * context owns a distinct log filename without forcing the\n * model-facing task id (`bash_<n>`) to grow longer.\n *\n * Format: `YYYYMMDD-HHMMSS-mmm` in UTC.\n * - Sortable (lexical sort = chronological sort).\n * - Unambiguous (UTC sidesteps DST / locale shifts).\n * - Filesystem-safe (digits + hyphens only).\n * - Millisecond precision avoids same-second-restart collisions.\n *\n * The timestamp is computed once per context, NOT per task — all of a\n * context's tasks share the same suffix so a directory listing groups\n * cleanly by \"which run produced these\".\n */\n const contextTimestamp = formatContextTimestamp(new Date())\n\n /**\n * Last-resort orphan reaper. With `detached: true`, background tasks\n * are in their OWN process group — when the parent (zidane TUI) dies,\n * the OS does NOT send them SIGHUP and they keep running indefinitely.\n * The user's \"Ctrl+C the TUI\" intent is \"stop everything I started\",\n * not \"leak a `sleep 60` into the background\".\n *\n * `process.on('exit')` fires SYNCHRONOUSLY on `process.exit()` AND on\n * natural shutdown — exactly the seam we need. The handler can only\n * do synchronous work (Node ignores async), but `process.kill` IS\n * synchronous, so a SIGTERM-the-group sweep lands cleanly. The kill\n * is best-effort: already-dead children throw ESRCH (swallowed).\n *\n * Registered lazily on first `execBackground` so contexts that never\n * background a task don't pay the listener cost. Deregistered in\n * `destroy()` so reconstructed contexts don't accumulate listeners\n * (Node warns past 10 — a long-running session that switches sessions\n * frequently would otherwise hit that).\n */\n /** Live `execStream` children → owning handle id. See `exitHandler`. */\n const streamChildren = new Map<ChildProcess, string>()\n\n let exitHandlerRegistered = false\n const exitHandler = (): void => {\n for (const task of tasks.values()) {\n if (task.status !== 'running')\n continue\n const pid = task.child?.pid\n if (pid === undefined)\n continue\n try {\n if (SUPPORTS_PROCESS_GROUPS)\n process.kill(-pid, 'SIGTERM')\n else\n process.kill(pid, 'SIGTERM')\n }\n catch {\n // ESRCH (already dead) / EPERM (lost ownership). Swallow —\n // the process either won't kill cleanly OR is already gone,\n // both acceptable at shutdown.\n }\n }\n // Stream children (execStream) share the reaper but NOT the `tasks`\n // registry: they're caller-owned (an MCP transport's close()), not\n // model-facing. They still spawn detached for kill-tree semantics,\n // which is exactly what makes them survive an unclean host exit —\n // a leaked long-lived MCP server per crash without this sweep.\n for (const child of streamChildren.keys())\n killProcessGroup(child, 'SIGTERM')\n }\n const registerExitHandler = (): void => {\n if (!exitHandlerRegistered) {\n process.on('exit', exitHandler)\n exitHandlerRegistered = true\n }\n }\n\n return {\n type: 'process',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n stream: true,\n // Background tasks are OS children of this process's machine —\n // they survive `agent.destroy()` reassignment games but NOT a\n // host machine reboot, and the orphan reaper kills them on\n // process exit. That's the 'process-lifetime' tier.\n detachedTasks: 'process-lifetime',\n } satisfies ContextCapabilities,\n\n async spawn(overrides?: SpawnConfig): Promise<ExecutionHandle> {\n const id = `process-${++counter}`\n const cwd = overrides?.cwd ?? defaultCwd\n\n // Only create `cwd` when it is genuinely missing. An unconditional\n // `mkdir(cwd, { recursive: true })` on an already-existing directory is a\n // wasted write that needlessly demands FS-write permission — which breaks\n // read-only / no-write runtimes (Supabase Edge Functions write only to\n // `/tmp`; Deno edge isolates) where the cwd already exists. `stat` is a\n // read (permitted there); the write is attempted only on a confirmed\n // ENOENT. Any other stat failure (e.g. EACCES on a sandbox-restricted\n // path) must NOT escalate to a mkdir we'd also be denied — assume present\n // and let a later file op surface a precise error if it truly isn't.\n let needsCreate = false\n try {\n await stat(cwd)\n }\n catch (err) {\n needsCreate = (err as NodeJS.ErrnoException)?.code === 'ENOENT'\n }\n if (needsCreate)\n await mkdir(cwd, { recursive: true })\n\n const handle: ExecutionHandle = { id, type: 'process', cwd }\n handles.set(id, handle)\n return handle\n },\n\n async exec(\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, timeout?: number, signal?: AbortSignal },\n ): Promise<ExecResult> {\n const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd\n\n // Pre-aborted fast path: skip the spawn entirely and synthesize a\n // killed-by-signal result. Saves a spawn round-trip + dodges Node\n // emitting an immediate `AbortError`.\n if (options?.signal?.aborted) {\n return { stdout: '', stderr: 'aborted by signal before spawn', exitCode: 143 }\n }\n\n const timeoutMs = (options?.timeout ?? config?.limits?.timeout ?? 30) * 1000\n const maxBuffer = DEFAULT_MAX_OUTPUT_BUFFER\n\n // Lazy subprocess load (see `loadSpawn`) — awaited before the Promise\n // executor so the spawn primitive is ready synchronously inside it.\n const spawnChild = await loadSpawn()\n\n return new Promise<ExecResult>((resolveP) => {\n // Spawn as a NEW process group leader so we can kill the whole\n // subtree on abort. Without `detached: true`, sending SIGTERM to\n // the shell's pid only kills the shell wrapper — its child\n // processes (the actual `sleep`, `npm`, `python`, …) get\n // reparented to init and keep running. `process.kill(-pid, …)`\n // with a NEGATIVE pid targets the whole process group; that's\n // the POSIX idiom for \"shut down everything I started\".\n //\n // On Windows there are no process groups in the POSIX sense, so\n // we leave `detached` off and accept the shell-only kill — the\n // platform's job-object machinery is the path forward there if\n // we ever need it, but it's not the bug zidane's users are\n // hitting today.\n const child = spawnChild('/bin/sh', ['-c', command], {\n cwd,\n env: { ...baseEnv, ...defaultEnv, ...options?.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n detached: SUPPORTS_PROCESS_GROUPS,\n })\n\n // Byte-capped accumulation via the shared exec-transport primitive\n // (see contexts/output-cap.ts) — byte accounting + codepoint-safe\n // cuts. Overflow orchestration (kill the process group, matching\n // `execAsync`'s maxBuffer behavior) stays here.\n const stdoutSlot: CappedSlot = { text: '', bytes: 0 }\n const stderrSlot: CappedSlot = { text: '', bytes: 0 }\n let bufferTruncated = false\n let timedOut = false\n let killedByAbort = false\n let settled = false\n\n // Every exec-initiated kill escalates: SIGTERM now, SIGKILL after a\n // short grace unless the child settled in between. Without this, a\n // TERM-ignoring child leaves the promise pending forever.\n const killWithEscalation = (): void => {\n killProcessGroup(child, 'SIGTERM')\n const escalation = setTimeout(() => {\n if (!settled)\n killProcessGroup(child, 'SIGKILL')\n }, EXEC_KILL_ESCALATION_GRACE_MS)\n escalation.unref?.()\n }\n\n const onChunk = (slot: CappedSlot, chunk: Buffer): void => {\n const outcome = appendCapped(slot, chunk, maxBuffer)\n if (outcome !== 'ok' && !bufferTruncated) {\n bufferTruncated = true\n killWithEscalation()\n }\n }\n\n child.stdout?.on('data', chunk => onChunk(stdoutSlot, chunk as Buffer))\n child.stderr?.on('data', chunk => onChunk(stderrSlot, chunk as Buffer))\n\n const timeoutTimer = timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true\n killWithEscalation()\n }, timeoutMs)\n : undefined\n\n const onAbort = (): void => {\n killedByAbort = true\n killWithEscalation()\n }\n const userSignal = options?.signal\n if (userSignal)\n userSignal.addEventListener('abort', onAbort, { once: true })\n\n let exitGraceTimer: NodeJS.Timeout | undefined\n\n const settle = (exitCode: number, extraStderr?: string): void => {\n if (settled)\n return\n settled = true\n if (timeoutTimer)\n clearTimeout(timeoutTimer)\n if (exitGraceTimer)\n clearTimeout(exitGraceTimer)\n if (userSignal)\n userSignal.removeEventListener('abort', onAbort)\n const finalStderr = extraStderr\n ? (stderrSlot.text ? `${stderrSlot.text}\\n${extraStderr}` : extraStderr)\n : stderrSlot.text\n resolveP({ stdout: stdoutSlot.text, stderr: finalStderr, exitCode })\n }\n\n // Shared classification ladder for `close` (fast path: pipes\n // drained normally) and the post-`exit` grace (pipes held open\n // by a daemonized grandchild). Order matters: killed-by-our-\n // abort wins over timeout wins over natural exit, because the\n // abort listener fires first when the user cancels mid-\n // timeout-window.\n const settleFromStatus = (code: number | null, signal: NodeJS.Signals | null): void => {\n if (killedByAbort) {\n settle(143, 'aborted by signal')\n return\n }\n if (timedOut) {\n settle(124, `command timed out after ${timeoutMs}ms`)\n return\n }\n if (bufferTruncated) {\n settle(143, outputExceededMessage(maxBuffer, 'process killed'))\n return\n }\n if (signal) {\n // Killed by some other signal we didn't issue. Treat as\n // signal-killed for consumer compat.\n settle(128 + 15, `terminated by signal ${signal}`)\n return\n }\n settle(typeof code === 'number' ? code : 1)\n }\n\n child.on('error', (err) => {\n // Spawn failure (ENOENT on `/bin/sh`, EACCES, …). Mirror\n // `execAsync`'s \"rejects with an Error\" shape by surfacing\n // the message on stderr and a non-zero exit.\n settle(1, err.message)\n })\n\n child.on('close', (code, signal) => settleFromStatus(code, signal))\n\n child.on('exit', (code, signal) => {\n // The command is DONE here — `close` only adds \"every stdio\n // pipe closed\", which a daemonized grandchild can block\n // forever (see EXIT_PIPE_DRAIN_GRACE_MS). Give the pipes a\n // short drain grace, then settle with what we have and\n // release them. The daemon keeps running — that was the\n // command's intent; killing the group here would be wrong.\n exitGraceTimer = setTimeout(() => {\n if (settled)\n return\n child.stdout?.destroy()\n child.stderr?.destroy()\n settleFromStatus(code, signal)\n }, EXIT_PIPE_DRAIN_GRACE_MS)\n exitGraceTimer.unref?.()\n })\n })\n },\n\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n return readFile(await resolveContained(handle, path), 'utf-8')\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n try {\n // Same containment as every other file method. `stat` (NOT lstat):\n // the caller wants the TARGET's size — an lstat of a symlink\n // reports the link itself and a link to a 10GB file would sail\n // past the whole-read guard this probe exists to feed.\n const info = await stat(await resolveContained(handle, path))\n return info.isFile() ? info.size : null\n }\n catch {\n return null\n }\n },\n\n async readFileBinary(handle: ExecutionHandle, path: string): Promise<Uint8Array> {\n // No encoding → returns a Buffer (which is a Uint8Array). Used by\n // read_file to ferry image / binary content into the multimodal route.\n const buf = await readFile(await resolveContained(handle, path))\n return new Uint8Array(buf)\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n const fullPath = await resolveContained(handle, path)\n await mkdir(dirname(fullPath), { recursive: true })\n await writeFile(fullPath, content, 'utf-8')\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options): Promise<ListFilesEntry[]> {\n if (options?.glob) {\n await resolveContained(handle, globLiteralPrefix(path) || '.')\n const entries = await globFilesLocal(path, handle.cwd, {\n limit: options?.limit ?? 1000,\n metadata: options?.metadata,\n gitignore: options?.gitignore,\n signal: options?.signal,\n })\n if (workspaceRoot === undefined)\n return entries\n const contained: ListFilesEntry[] = []\n for (const entry of entries) {\n try {\n await resolveContained(handle, entry.path)\n contained.push(entry)\n }\n catch {\n // Symlink escapes can be discovered by broad globs like `**/*`.\n // Keep the process context's workspaceRoot invariant by filtering\n // them exactly as read/write paths are filtered.\n }\n }\n return contained\n }\n // Literal listing — suffix directories with `/` so consumers (the\n // `list_files` tool, path suggestions) can tell files from dirs\n // without a second probe. Mirrors `ls -p` in the docker context.\n const dirents = await readdir(await resolveContained(handle, path), { withFileTypes: true })\n return dirents.map(d => (d.isDirectory() ? `${d.name}/` : d.name))\n },\n\n /**\n * Live bidirectional stream to a child process — see\n * `ExecutionContext.execStream` for the seam contract.\n *\n * The reference implementation: an ordinary piped `spawn`, so both\n * streams stay separate and no terminal line discipline sits between\n * the caller and the child. Sandbox-backed contexts have to work\n * harder to reach the same shape.\n *\n * Unlike `execBackground`, nothing here lands in the `tasks` registry:\n * this handle is owned by its caller (an MCP transport's `close()`),\n * not by the model-facing background-task list that `shell_kill` /\n * `wait_task` walk. It IS tracked in `streamChildren` so the shared\n * orphan reaper and `destroy()` can sweep it — detached process-group\n * semantics (needed so `kill()` takes the child's own children with\n * it) are exactly what would otherwise leak a live server per crash.\n */\n async execStream(\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ): Promise<StreamHandle> {\n const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd\n const spawnChild = await loadSpawn()\n\n options?.signal?.throwIfAborted()\n\n const child = spawnChild('/bin/sh', ['-c', command], {\n cwd,\n env: { ...baseEnv, ...defaultEnv, ...options?.env },\n stdio: ['pipe', 'pipe', 'pipe'],\n detached: SUPPORTS_PROCESS_GROUPS,\n })\n\n // Same orphan-reaper coverage as `execBackground`: the detached\n // spawn (needed for kill-tree) means an unclean host exit (Ctrl+C,\n // uncaught exception, OOM) would otherwise leave the server running\n // indefinitely. Tracked in a side map rather than `tasks` — these\n // are caller-owned, invisible to `shell_kill` / `wait_task`.\n registerExitHandler()\n streamChildren.set(child, handle.id)\n\n // A spawn failure (ENOENT on /bin/sh, EACCES on cwd) arrives as an\n // async 'error' event, never a throw. Without a listener Node\n // escalates it to an uncaughtException and takes the host down.\n let spawnError: Error | null = null\n child.on('error', (err) => { spawnError = err })\n\n const exited = new Promise<number>((resolveExit) => {\n // 'close' rather than 'exit': it fires after the stdio streams\n // have flushed and ended, so a consumer awaiting `exited` can\n // trust that it has already seen every byte. `exit` can beat the\n // final stdout chunk and truncate the last protocol frame.\n child.once('close', (code, signalName) => {\n streamChildren.delete(child)\n // Signal-killed children report a null code. Same mapping as\n // `execBackground`'s exit handler below — SIGTERM (our own\n // `kill()`) is the common case and lands on 143.\n resolveExit(\n code !== null\n ? code\n : signalName === 'SIGTERM'\n ? 143\n : signalName\n ? 128\n : 1,\n )\n })\n })\n\n const kill = async (): Promise<void> => {\n if (child.exitCode !== null || child.signalCode !== null)\n return\n const pid = child.pid\n if (pid === undefined)\n return\n try {\n if (SUPPORTS_PROCESS_GROUPS)\n process.kill(-pid, 'SIGTERM')\n else\n process.kill(pid, 'SIGTERM')\n }\n catch {\n // ESRCH (already reaped) / EPERM (lost ownership) — both mean\n // \"no live child of ours to signal\". Idempotent by contract.\n }\n }\n\n if (options?.signal) {\n if (options.signal.aborted) {\n void kill()\n }\n else {\n // Detach on exit: the signal is typically the RUN-long abort\n // signal, so a listener left behind after the child exits retains\n // the `kill` closure (and through it the ChildProcess) for the\n // rest of the run — one dead closure per exec on shell-heavy runs.\n const onAbort = () => { void kill() }\n const detach = () => options.signal!.removeEventListener('abort', onAbort)\n options.signal.addEventListener('abort', onAbort, { once: true })\n exited.then(detach, detach)\n }\n }\n\n return {\n pid: child.pid ?? -1,\n // Node's Readable is an async iterable of Buffer, and Buffer is a\n // Uint8Array — no adaptation needed on this context.\n stdout: child.stdout!,\n stderr: child.stderr!,\n write: async (data: Uint8Array): Promise<void> => {\n if (spawnError)\n throw spawnError\n const stdin = child.stdin\n if (!stdin || stdin.destroyed)\n throw new Error(`execStream: stdin is closed (pid ${child.pid ?? -1})`)\n // Resolve from the write callback, which fires once the chunk is\n // actually flushed to the pipe — that IS the backpressure signal,\n // so awaiting it pushes backpressure out to the caller. (Watching\n // the boolean return + 'drain' instead is a trap: for a small\n // write the callback runs synchronously, before the return value\n // is even bound.)\n await new Promise<void>((resolveWrite, rejectWrite) => {\n stdin.write(data, err => err ? rejectWrite(err) : resolveWrite())\n })\n },\n kill,\n exited,\n }\n },\n\n async execBackground(\n handle: ExecutionHandle,\n command: string,\n options: {\n cwd?: string\n env?: Record<string, string>\n outputDir: string\n onExit?: (info: TaskExitInfo) => void\n maxOutputBytes?: number\n stallTimeoutMs?: number\n onStall?: (info: TaskStallInfo) => void\n },\n ): Promise<TaskHandle> {\n const cwd = options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd\n\n // Resolve the subprocess primitive up front (see `loadSpawn`) so a runtime\n // without `node:child_process` fails BEFORE we allocate the log dir +\n // output stream — no orphaned fd / log file on an unsupported host.\n const spawnChild = await loadSpawn()\n\n await mkdir(options.outputDir, { recursive: true })\n\n // Mint id + path. The id is sequential per context (model-facing,\n // short, ergonomic for `shell_kill`). The log FILENAME embeds the\n // context's start timestamp so two contexts sharing an `outputDir`\n // (TUI restart on the same session, concurrent zidane instances,\n // …) never resolve to the same file — we open with `flags: 'a'`\n // and a name collision would interleave their output. Path\n // validation is defensive — we mint our own ids so it never trips\n // today, but it pins the invariant for forks / third parties.\n const taskId = `bash_${++taskCounter}`\n assertSafeTaskId(taskId)\n const outputPath = resolve(options.outputDir, `${taskId}.${contextTimestamp}.log`)\n\n // Install the orphan reaper on first task. See `exitHandler`'s\n // JSDoc for the kill-on-shutdown rationale.\n registerExitHandler()\n\n // Open the output file. The timestamped path is unique per context\n // so a brand-new file is the expected outcome; `flags: 'a'` is kept\n // as the safe default (preserves bytes if the path collides for any\n // reason — same-millisecond context creation, manual pre-population,\n // etc.) rather than blindly truncating. Streams are opened BEFORE\n // the spawn to avoid a race where the child writes before the stream\n // is ready — `child_process` buffers stdio until the consumer\n // attaches, but the FS handle has to exist either way for our pipe.\n const outputStream: WriteStream = createWriteStream(outputPath, { flags: 'a' })\n // Surface FS errors (ENOSPC, EACCES on a remounted FS, etc.)\n // under ZIDANE_DEBUG instead of crashing the host via an\n // unhandled 'error' event. Without a listener Node escalates\n // any stream-level error to an uncaughtException and the whole\n // process exits — the model and the user would lose every\n // unrelated in-flight piece of work to one bad task's disk\n // hiccup. Swallow + log is the safer default for a fire-and-\n // forget log writer; the task's exit code still reports.\n outputStream.on('error', (err) => {\n if (process.env.ZIDANE_DEBUG)\n process.stderr.write(`[zidane/contexts] task ${taskId} log stream error: ${err.message}\\n`)\n })\n\n // Spawn as a NEW process group leader so we can kill the whole\n // subtree on demand. Same primitive `exec` uses for foreground\n // shells — see the long comment in that method for the\n // process-group rationale.\n const child = spawnChild('/bin/sh', ['-c', command], {\n cwd,\n env: { ...baseEnv, ...defaultEnv, ...options.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n detached: SUPPORTS_PROCESS_GROUPS,\n })\n\n const state: TaskState = {\n taskId,\n handleId: handle.id,\n pid: child.pid ?? -1,\n command,\n cwd,\n startedAt: Date.now(),\n outputPath,\n outputStream,\n child,\n status: 'running',\n bytesWritten: 0,\n settled: false,\n onExit: options.onExit,\n }\n tasks.set(taskId, state)\n\n // Output cap (optional). Beyond the cap the process keeps\n // running — head-priority retention: bytes already on disk stay,\n // subsequent bytes are counted but dropped, and a structured\n // truncation marker is appended at settle so the model can\n // pattern-match the loss. Killing on overflow (what the\n // foreground `exec` does) would be wrong here: long-running\n // servers legitimately log forever.\n const maxOutputBytes = typeof options.maxOutputBytes === 'number' && options.maxOutputBytes > 0\n ? options.maxOutputBytes\n : undefined\n let droppedBytes = 0\n // Latched on the first overflowing chunk. Without it, a codepoint-\n // boundary backoff (below) leaves a few bytes of \"room\" that later\n // chunks would dribble into — appending disjoint fragments from\n // much-later output right after the cut point.\n let capExhausted = false\n\n // Stall watchdog (optional). One-shot per quiet period: fires\n // `onStall` after `stallTimeoutMs` of no output, then stays quiet\n // until fresh output re-arms it. `unref()` so a pending timer\n // never holds the host process open.\n const stallTimeoutMs = typeof options.stallTimeoutMs === 'number' && options.stallTimeoutMs > 0\n ? options.stallTimeoutMs\n : undefined\n const onStall = options.onStall\n let stallTimer: NodeJS.Timeout | undefined\n let lastOutputAt = Date.now()\n const armStallTimer = (): void => {\n if (!stallTimeoutMs || !onStall)\n return\n if (stallTimer)\n clearTimeout(stallTimer)\n stallTimer = setTimeout(() => {\n stallTimer = undefined\n if (state.settled)\n return\n try {\n onStall({\n taskId,\n command,\n outputPath,\n stalledForMs: Date.now() - lastOutputAt,\n bytesWritten: state.bytesWritten,\n })\n }\n catch (err) {\n if (process.env.ZIDANE_DEBUG)\n process.stderr.write(`[zidane/contexts] task ${taskId} onStall threw: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n }, stallTimeoutMs)\n stallTimer.unref?.()\n }\n const clearStallTimer = (): void => {\n if (stallTimer) {\n clearTimeout(stallTimer)\n stallTimer = undefined\n }\n }\n armStallTimer()\n\n // Pipe both streams into the same file. Order between stdout and\n // stderr is preserved per-stream; cross-stream ordering depends on\n // Node's event loop — acceptable interleaving for log-shaped\n // output. Tracked-bytes is updated on every chunk for the\n // listBackground UX (counted even past the cap, so the UX shows\n // the task's TRUE output volume).\n const appendChunk = (chunk: Buffer): void => {\n state.bytesWritten += chunk.length\n lastOutputAt = Date.now()\n armStallTimer()\n if (maxOutputBytes !== undefined) {\n const onDisk = state.bytesWritten - droppedBytes - chunk.length\n if (capExhausted || onDisk >= maxOutputBytes) {\n capExhausted = true\n droppedBytes += chunk.length\n return\n }\n const room = maxOutputBytes - onDisk\n if (chunk.length > room) {\n capExhausted = true\n // Cut on a UTF-8 codepoint boundary, same as the foreground\n // exec cap: back off while the byte after the cut is a\n // continuation byte (0b10xxxxxx) so the log never ends in a\n // mangled half-codepoint right before the truncation marker.\n let end = room\n while (end > 0 && (chunk[end] & 0xC0) === 0x80)\n end--\n droppedBytes += chunk.length - end\n if (end > 0)\n outputStream.write(chunk.subarray(0, end))\n return\n }\n }\n outputStream.write(chunk)\n }\n child.stdout?.on('data', chunk => appendChunk(chunk as Buffer))\n child.stderr?.on('data', chunk => appendChunk(chunk as Buffer))\n\n // Settle path — at-most-once via `settled` flag (checklist #14).\n // Three trigger sources: `close` (natural OR signal-killed),\n // `error` (spawn failure), explicit `killBackground` (which\n // routes through `close` itself after the SIGTERM lands).\n const settle = (cause: 'close' | 'error', code: number | null, signal: NodeJS.Signals | null, errMessage?: string): void => {\n if (state.settled)\n return\n state.settled = true\n clearStallTimer()\n state.endedAt = Date.now()\n\n // Determine final status from cause + signal.\n const status: TaskExitInfo['status']\n = signal === 'SIGTERM' || state.killRequested\n ? 'killed'\n : 'exited'\n // Signal-killed children report null `code` from Node; map back\n // to the POSIX `128 + signum` convention so consumers can read\n // an integer either way.\n const exitCode = code !== null\n ? code\n : signal === 'SIGTERM'\n ? 143\n : signal\n ? 128\n : 1\n state.status = status\n state.exitCode = exitCode\n if (signal)\n state.signal = signal\n\n // Flush + close the WriteStream BEFORE firing onExit — model\n // may read the file in the same turn it receives the\n // notification, and a still-open stream can hold tail bytes\n // back from disk. `stream.end(callback)` is the documented\n // \"all queued writes are flushed when this fires\" idiom.\n //\n // ORDER MATTERS: any error preamble we want in the log file\n // (spawn failures with no stdout, buffer overflows) MUST be\n // written BEFORE `end()` — once `end()` is called the stream\n // is closed for writing and subsequent `.write()` calls are\n // dropped. Earlier revisions had this reversed and silently\n // lost ENOENT-on-`/bin/sh` messages.\n if (errMessage) {\n try {\n outputStream.write(`\\n${errMessage}\\n`)\n }\n catch {\n // Stream may have errored before this — best-effort only.\n }\n }\n if (droppedBytes > 0) {\n try {\n // Structured + loud — the model can pattern-match the tag\n // rather than misread a truncated log as the full output.\n outputStream.write(`\\n<output-truncated bytes-dropped=\"${droppedBytes}\"/>\\n`)\n }\n catch {\n // Best-effort only, same as the error preamble above.\n }\n }\n outputStream.end(() => {\n // `stateToTaskExitInfo` reads the same fields we just set\n // on `state`, so the snapshot the consumer gets matches the\n // post-settle state exactly. `onExit` is optional — pull-based\n // consumers reconcile via `listBackground` instead.\n try {\n state.onExit?.(stateToTaskExitInfo(state))\n }\n catch (err) {\n // Defensive — a buggy onExit callback shouldn't crash the\n // host. Surface via stderr under ZIDANE_DEBUG; otherwise\n // swallow. Matches the spawn-tool's bubbleError pattern.\n if (process.env.ZIDANE_DEBUG)\n process.stderr.write(`[zidane/contexts] task ${taskId} onExit threw: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n })\n // Release the process + stream references at settle: entries\n // outlive their processes in the registry (until explicit\n // `destroy`), and a session that backgrounds many tasks would\n // otherwise retain every dead ChildProcess and ended\n // WriteStream — plus the closures they anchor — for its whole\n // lifetime. Everything the post-exit readers (`listBackground` /\n // `waitBackground` / `killBackground`) project stays on `state`.\n state.child = undefined\n state.outputStream = undefined\n }\n\n child.on('close', (code, signal) => settle('close', code, signal))\n child.on('error', err => settle('error', null, null, `[spawn error] ${err.message}`))\n\n child.on('exit', (code, signal) => {\n // Same pipe-hostage hazard as foreground `exec`: a grandchild\n // that inherited the pipes blocks `close` forever, leaving the\n // task `running` (and any waiter parked) long after the\n // command exited. Settle after a short drain grace; `settle`'s\n // latch makes this a no-op when `close` already fired.\n const exitGraceTimer = setTimeout(() => {\n if (state.settled)\n return\n child.stdout?.destroy()\n child.stderr?.destroy()\n settle('close', code, signal)\n }, EXIT_PIPE_DRAIN_GRACE_MS)\n exitGraceTimer.unref?.()\n })\n\n return { taskId, pid: state.pid, outputPath }\n },\n\n async killBackground(handle: ExecutionHandle, taskId: string): Promise<TaskExitInfo | null> {\n const state = tasks.get(taskId)\n // Two miss cases collapse into one `null` return: unknown id, AND\n // known-id-but-not-owned-by-this-handle. The second case is the\n // subagent-can't-kill-parent-tasks defense; surfacing it as a\n // distinct error would leak the existence of the parent's task\n // to the subagent's model, which violates the per-handle\n // isolation contract.\n if (!state || state.handleId !== handle.id)\n return null\n // Already exited — return the cached info. We don't keep a\n // separate cached exit; the state itself carries every field\n // `TaskExitInfo` needs and `stateToTaskExitInfo` projects it.\n if (state.status !== 'running')\n return stateToTaskExitInfo(state)\n\n // Mark the intent BEFORE issuing the kill so the close handler\n // classifies the exit as `'killed'` even on platforms where the\n // SIGTERM-via-group lands faster than the close event drains.\n // (Checklist #14: at-most-once settle, plus correct status\n // classification regardless of event ordering.)\n state.killRequested = true\n\n // Wait for the existing close listener to fire — `settle()` does\n // all the flushing + onExit work. We just sit on a one-shot\n // promise tied to the `child.on('close')` we already registered\n // at spawn time.\n const closed = new Promise<void>((resolveP) => {\n if (state.settled) {\n resolveP()\n return\n }\n const originalOnExit = state.onExit\n state.onExit = (info) => {\n originalOnExit?.(info)\n resolveP()\n }\n })\n\n killProcessGroup(state.child, 'SIGTERM')\n await closed\n return stateToTaskExitInfo(state)\n },\n\n async waitBackground(\n handle: ExecutionHandle,\n taskId: string,\n options?: { timeoutMs?: number, signal?: AbortSignal },\n ): Promise<TaskExitInfo | null> {\n const state = tasks.get(taskId)\n // Same two-misses-collapse-to-null contract as `killBackground`:\n // unknown id AND known-but-other-handle both return `null` so a\n // subagent can't observe the parent's tasks through the wait seam.\n if (!state || state.handleId !== handle.id)\n return null\n if (state.settled)\n return stateToTaskExitInfo(state)\n\n return new Promise<TaskExitInfo | null>((resolveP) => {\n let done = false\n let timer: NodeJS.Timeout | undefined\n const signal = options?.signal\n const finish = (value: TaskExitInfo | null): void => {\n if (done)\n return\n done = true\n if (timer)\n clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n resolveP(value)\n }\n function onAbort(): void {\n finish(null)\n }\n\n // Chain onto the live exit callback rather than replacing it —\n // the original (notification enqueue, kill-waiter, a sibling\n // waiter) must keep firing. The `done` latch makes a late exit\n // after timeout a no-op for THIS waiter.\n const originalOnExit = state.onExit\n state.onExit = (info) => {\n originalOnExit?.(info)\n finish(stateToTaskExitInfo(state))\n }\n\n if (signal) {\n if (signal.aborted) {\n finish(null)\n return\n }\n signal.addEventListener('abort', onAbort, { once: true })\n }\n const timeoutMs = options?.timeoutMs\n if (typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0) {\n timer = setTimeout(finish, timeoutMs, null)\n timer.unref?.()\n }\n })\n },\n\n async reassignBackgroundTasks(\n fromHandle: ExecutionHandle,\n toHandle: ExecutionHandle,\n newOnExit?: (info: TaskExitInfo) => void,\n ): Promise<readonly TaskEntry[]> {\n // No-op when source = destination — keeps the spawn.ts call site\n // unconditional without forcing it to dedupe.\n if (fromHandle.id === toHandle.id)\n return []\n const promoted: TaskEntry[] = []\n for (const state of tasks.values()) {\n if (state.handleId !== fromHandle.id || state.status !== 'running')\n continue\n state.handleId = toHandle.id\n // Replace the natural-exit callback. The original closed over\n // the spawning agent's hook bus, which is about to be destroyed\n // — without rewiring, the task's eventual `background:exit`\n // fires into a torn-down hookable and the parent never learns.\n if (newOnExit)\n state.onExit = newOnExit\n promoted.push(stateToTaskEntry(state))\n }\n return promoted\n },\n\n async listBackground(handle: ExecutionHandle): Promise<readonly TaskEntry[]> {\n // Snapshot — callers must not assume the returned array stays\n // in sync with the live registry. Sorted by startedAt so the\n // model / UI sees consistent ordering across calls. Scoped to\n // the calling handle so subagents don't see the parent's tasks\n // (and vice versa) in their listing.\n return [...tasks.values()]\n .filter(s => s.handleId === handle.id)\n .sort((a, b) => a.startedAt - b.startedAt)\n .map(stateToTaskEntry)\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n // Kill every still-running background task SPAWNED THROUGH THIS\n // HANDLE before tearing the handle down. SIGTERM the groups,\n // await the close + flush, THEN drop the registry entries.\n // Sequential — destroy is one-shot teardown, the few ms of extra\n // latency aren't worth the synchronization complexity.\n //\n // The handle scope matters when the same `ExecutionContext` is\n // shared across a parent agent and its `spawn`-ed subagents (the\n // default — `spawn.ts` passes `execution: ctx.execution`). Each\n // agent mints its own `ExecutionHandle` and registers its\n // background tasks under that handle's id. Without the filter,\n // a child agent's `destroy()` (fired by `spawn.ts`'s `finally`\n // when the subagent finishes / is cancelled) would walk the\n // shared registry and SIGTERM the parent's tasks too. So\n // cancelling a subagent that has its own background shells now\n // correctly kills JUST those subagent shells, leaving the\n // parent's intact.\n const survivors = [...tasks.values()].filter(s => s.handleId === handle.id && !s.settled)\n await Promise.all(survivors.map(async (state) => {\n state.killRequested = true\n await new Promise<void>((resolveP) => {\n let graceTimer: NodeJS.Timeout | undefined\n const originalOnExit = state.onExit\n state.onExit = (info) => {\n originalOnExit?.(info)\n if (graceTimer)\n clearTimeout(graceTimer)\n resolveP()\n }\n killProcessGroup(state.child, 'SIGTERM')\n // Bounded drain: a task that traps/ignores SIGTERM — or whose\n // `close` event is held back by an escaped grandchild (setsid'd\n // out of the process group) keeping the inherited stdio pipe\n // open — must not wedge `agent.destroy()` forever. After the\n // grace period, SIGKILL the group and stop waiting: `close`\n // may STILL never fire (the pipe holder isn't in the group),\n // so resolving here is the only way destroy() stays bounded.\n graceTimer = setTimeout(() => {\n killProcessGroup(state.child, 'SIGKILL')\n // settle() normally flushes + ends the log stream, but it\n // only runs off `close` — which we just gave up on. Tear the\n // stream down so the fd doesn't leak into a long-lived host.\n state.outputStream?.destroy()\n resolveP()\n }, destroyGraceMs)\n graceTimer.unref?.()\n })\n }))\n // Drop only this handle's tasks from the registry. Other handles\n // (siblings, parent) keep their entries.\n for (const [taskId, state] of tasks) {\n if (state.handleId === handle.id)\n tasks.delete(taskId)\n }\n // Stream children are caller-owned (the MCP transport's close(), which\n // the agent runs BEFORE executionContext.destroy()), so live entries\n // here mean the owner leaked them. Sweep this handle's slice — same\n // scoping rationale as the task loop above.\n for (const [child, ownerId] of streamChildren) {\n if (ownerId === handle.id) {\n killProcessGroup(child, 'SIGTERM')\n streamChildren.delete(child)\n }\n }\n handles.delete(handle.id)\n // Drop the orphan reaper ONLY when no handles remain — otherwise\n // a child's `destroy()` would strip the parent's safety net. The\n // reaper protects every still-tracked task in the context, so it\n // sticks around until the LAST handle is gone.\n //\n // Without this guard, the spawn-tool sequence\n // parent.spawn(child) → child.run() → child.destroy() (auto)\n // would deregister the handler mid-parent-lifetime. The parent's\n // own subsequent Ctrl+C orphan-kill safety would silently degrade.\n if (exitHandlerRegistered && handles.size === 0) {\n process.off('exit', exitHandler)\n exitHandlerRegistered = false\n }\n },\n }\n}\n\n/**\n * Per-task state. Lives in the context's `tasks` registry. Fields are\n * mutated in place by the spawn / close / kill / destroy code paths —\n * the registry isn't immutable. Treat the type as a record-of-cells,\n * not a value.\n */\ninterface TaskState {\n taskId: string\n /**\n * `ExecutionHandle.id` of the spawning agent. The registry is\n * context-scoped (one Map shared across all handles a context minted),\n * so a per-task owner tag is what scopes `listBackground` /\n * `killBackground` / `destroy` to the calling handle's slice.\n *\n * Without this, a subagent spawned via `spawn` tool — which inherits\n * the parent's `ExecutionContext` but mints its OWN handle — would\n * see (and accidentally kill on `destroy()`) every task the parent\n * had running. Came up the first time the model spawned a subagent\n * that ran a background task: the subagent's run-end `destroy()`\n * SIGTERMed the parent's `npm run dev` mid-flight.\n */\n handleId: string\n pid: number\n command: string\n cwd: string\n startedAt: number\n outputPath: string\n /**\n * Live-only references, released (set `undefined`) at settle so the\n * registry doesn't retain dead `ChildProcess` and ended `WriteStream`\n * objects until `destroy()`. Present iff `settled` is false.\n */\n outputStream?: WriteStream\n child?: ChildProcess\n status: 'running' | 'exited' | 'killed'\n exitCode?: number\n signal?: NodeJS.Signals\n bytesWritten: number\n /**\n * `at-most-once` settle latch. Multiple trigger sources (`close`,\n * `error`, `kill`) can race; the flag dedupes so `onExit` fires\n * exactly once per task (checklist #14).\n */\n settled: boolean\n /**\n * Set by `killBackground` / `destroy` before issuing SIGTERM so the\n * close handler classifies the exit as `'killed'` even when the\n * platform delivers the close event ahead of our intent record.\n */\n killRequested?: boolean\n /**\n * `Date.now()` at settle time. Lets `durationMs` (and pull-based\n * reconcile consumers reading `TaskEntry.endedAt`) report the task's\n * real lifetime instead of a projection-time delta.\n */\n endedAt?: number\n /**\n * Optional push-style exit callback. Undefined for pull-based\n * consumers (remote / durable hosts reconcile via `listBackground`).\n * Mutated in place by the kill / destroy / wait chains.\n */\n onExit?: (info: TaskExitInfo) => void\n}\n\n/**\n * Send `signal` to the child's whole process group. Falls back to a\n * single-process kill on Windows (no POSIX process groups). Shared\n * across the foreground `exec` path, the background spawn / kill\n * paths, and the shutdown-time orphan reaper — keeping one definition\n * so the kill semantics can't drift between them.\n */\nexport function killProcessGroup(child: ChildProcess | undefined, signal: NodeJS.Signals): void {\n const pid = child?.pid\n if (pid === undefined)\n return\n try {\n if (SUPPORTS_PROCESS_GROUPS)\n process.kill(-pid, signal)\n else\n process.kill(pid, signal)\n }\n catch {\n // ESRCH / EPERM — process is already gone (race with natural exit)\n // or we lost the right to kill it. Both are safe to swallow.\n }\n}\n\n/**\n * Project a `TaskState` to the `TaskEntry` shape `listBackground` and\n * `reassignBackgroundTasks` return. Single helper keeps the shape\n * consistent across both call sites (and a future addition of fields\n * to `TaskEntry` only needs to land here).\n */\nfunction stateToTaskEntry(state: TaskState): TaskEntry {\n return {\n taskId: state.taskId,\n pid: state.pid,\n command: state.command,\n cwd: state.cwd,\n startedAt: state.startedAt,\n ...(state.endedAt !== undefined ? { endedAt: state.endedAt } : {}),\n outputPath: state.outputPath,\n status: state.status,\n ...(state.exitCode !== undefined ? { exitCode: state.exitCode } : {}),\n ...(state.signal ? { signal: state.signal } : {}),\n bytesWritten: state.bytesWritten,\n }\n}\n\n/**\n * Project a settled `TaskState` to the `TaskExitInfo` shape `settle()`\n * fires from `onExit` and `killBackground` returns on the\n * cached-exit path. Pre-condition: `state.status !== 'running'`\n * (callers gate on this).\n */\nfunction stateToTaskExitInfo(state: TaskState): TaskExitInfo {\n return {\n taskId: state.taskId,\n status: state.status as Exclude<TaskState['status'], 'running'>,\n exitCode: state.exitCode ?? 0,\n ...(state.signal ? { signal: state.signal } : {}),\n outputPath: state.outputPath,\n // Settle-time delta when available (always set since `endedAt` was\n // introduced); the `Date.now()` fallback covers defensive callers\n // projecting an unsettled state.\n durationMs: (state.endedAt ?? Date.now()) - state.startedAt,\n command: state.command,\n }\n}\n","/**\n * Execution context types.\n *\n * An execution context defines *where* and *how* an agent's tools run.\n * The agent loop and tools interact through this interface without knowing\n * whether they're running in-process, in a Docker container, or in a\n * remote sandbox.\n */\n\n// ---------------------------------------------------------------------------\n// Capabilities\n// ---------------------------------------------------------------------------\n\n/**\n * Lifetime guarantee of background tasks started through\n * {@link ExecutionContext.execBackground}.\n *\n * - `'none'` — the context cannot detach tasks at all.\n * - `'process-lifetime'` — tasks live as long as the HOST process; a\n * crash/restart of the host orphans or kills them (`ProcessContext`).\n * - `'durable'` — tasks live on a remote runner and survive host process\n * death (remote execution contexts driven by durable runtimes).\n *\n * Durable-execution adapters (e.g. `zidane/restate`) consult this to\n * decide whether backgrounding is safe to expose: a `'durable'` context\n * keeps its tasks across worker crashes, so there is no reason to strip\n * the capability from the model.\n */\nexport type DetachedTasksCapability = 'none' | 'process-lifetime' | 'durable'\n\nexport interface ContextCapabilities {\n /** Can execute shell commands */\n shell: boolean\n /** Can read/write files in a workspace */\n filesystem: boolean\n /** Can enumerate glob matches through `ExecutionContext.listFiles(..., { glob: true })`. */\n glob?: boolean\n /** Can make outbound network requests */\n network: boolean\n /** Has GPU access */\n gpu: boolean\n /**\n * Can open a bidirectional byte stream to a long-lived process through\n * {@link ExecutionContext.execStream}. Absent / `false` = the context\n * only supports the buffered {@link ExecutionContext.exec} and (maybe)\n * the write-to-a-log-file {@link ExecutionContext.execBackground}.\n *\n * Consumers that NEED live stdin (stdio MCP servers routed into the\n * context) must check this before calling; the seam is optional so\n * contexts backed by a request/response-only API stay valid.\n */\n stream?: boolean\n /**\n * Background-task lifetime guarantee. Optional for backward\n * compatibility — when absent, callers infer `'process-lifetime'`\n * if the context implements `execBackground`, `'none'` otherwise\n * (see {@link resolveDetachedTasksCapability}).\n */\n detachedTasks?: DetachedTasksCapability\n}\n\n/**\n * Effective {@link DetachedTasksCapability} of a context, with the\n * backward-compatible inference for contexts that predate the field.\n */\nexport function resolveDetachedTasksCapability(context: ExecutionContext): DetachedTasksCapability {\n return context.capabilities.detachedTasks\n ?? (context.execBackground ? 'process-lifetime' : 'none')\n}\n\n// ---------------------------------------------------------------------------\n// Execution handle\n// ---------------------------------------------------------------------------\n\n/** Opaque handle to a running execution context instance */\nexport interface ExecutionHandle {\n id: string\n type: ContextType\n /** Working directory within the context */\n cwd: string\n}\n\n// ---------------------------------------------------------------------------\n// Exec result\n// ---------------------------------------------------------------------------\n\nexport interface ExecResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\nexport interface ListFileMetadata {\n path: string\n size?: number\n mtimeMs?: number\n}\n\nexport type ListFilesEntry = string | ListFileMetadata\n\nexport interface ListFilesOptions {\n /** Treat `path` as a glob match pattern instead of a literal directory path. */\n glob?: boolean\n limit?: number\n metadata?: boolean\n /**\n * Skip git-ignored files when globbing. Default `true`. Contexts that can\n * resolve git's ignore rules (e.g. the local process context) honor it; the\n * rest treat it as a no-op. Set `false` to match every file, ignored or not.\n */\n gitignore?: boolean\n /** Abort the listing early — forwarded from the calling tool's `ctx.signal`. */\n signal?: AbortSignal\n}\n\n// ---------------------------------------------------------------------------\n// Spawn config\n// ---------------------------------------------------------------------------\n\nexport interface SpawnConfig {\n /** Working directory (created if it doesn't exist) */\n cwd?: string\n /**\n * Optional workspace-containment root for the file methods (`readFile`,\n * `writeFile`, `listFiles`) of the process context.\n *\n * When set, a resolved path that escapes this root (via absolute path,\n * `..`, or a symlink pointing outside) is rejected before any I/O. Default\n * unset = no containment (back-compat; the agent can read/write anywhere\n * the host process can). Set this for untrusted workloads to confine file\n * tools to a directory. Shell commands are not covered — use a sandbox /\n * docker context for full isolation.\n */\n workspaceRoot?: string\n /** Environment variables */\n env?: Record<string, string>\n /**\n * Whether spawned shells inherit the parent `process.env` (process context).\n *\n * Default `true` (preserves existing behavior — children see the parent\n * environment including anything Bun auto-loaded from `.env`). Set `false`\n * for untrusted workloads so secrets in the parent environment aren't\n * readable by tool commands; only `env` / per-call env plus a minimal base\n * (`PATH`, `HOME`, `SHELL`, `LANG`, `LC_ALL`, `USER`, `TERM`, `TMPDIR`) are\n * passed through.\n */\n inheritEnv?: boolean\n /** Docker image (only for 'docker' context) */\n image?: string\n /** Docker container name prefix (only for 'docker' context) */\n name?: string\n /** Host paths mounted into the context (only for 'docker' context today) */\n mounts?: ContextMount[]\n /** Resource limits */\n limits?: {\n /** Memory limit in MB */\n memory?: number\n /** CPU limit (e.g. '1.0' = 1 core) */\n cpu?: string\n /** Timeout in seconds for the entire context lifetime */\n timeout?: number\n }\n /** Sandbox provider config (only for 'sandbox' context) */\n sandbox?: {\n provider: string\n apiKey?: string\n /**\n * Attach to a pre-existing sandbox by id rather than creating one\n * (provider-specific; E2B maps it to `Sandbox.connect`). The provider\n * leaves a connected sandbox running on teardown.\n */\n sandboxId?: string\n [key: string]: unknown\n }\n\n /**\n * How long `destroy()` waits for a SIGTERM'd background task to settle\n * before escalating to SIGKILL and abandoning the wait (process context\n * only). Bounds `agent.destroy()` against tasks that trap SIGTERM or\n * whose stdio pipes are held open by escaped grandchildren.\n *\n * Default: `5000`.\n */\n destroyGraceMs?: number\n\n /**\n * Publish container ports on the host (docker context only).\n *\n * Each entry maps a container port to either an explicit host port or\n * (when `host` is omitted) a Docker-assigned random port. Retrieve the\n * actual host port at runtime via `getMappedPort(container)` on the\n * docker context.\n */\n ports?: Array<{ container: number, host?: number, proto?: 'tcp' | 'udp' }>\n\n /**\n * UID/GID the container should run as (docker context only).\n *\n * Accepts the same forms Docker's `--user` does: `uid`, `uid:gid`, or a\n * named user that exists in the image. Default is the image's default\n * user (typically root). Setting this to the host user's `uid:gid`\n * avoids the EACCES-on-cleanup problem when sharing a workspace via\n * a `shared` mount.\n */\n user?: string\n\n /**\n * User-defined Docker network to join (docker context only).\n *\n * Defaults to Docker's default bridge. Use a user network when you\n * need multiple sibling containers (e.g. agent + database + dev\n * server) to discover each other by name.\n */\n network?: string\n\n /**\n * Docker labels to attach to the container (docker context only).\n *\n * Useful for ownership tracking — callers can sweep abandoned\n * containers (e.g. from a crashed parent process) by filtering on\n * a label they own: `docker ps -aq --filter label=my-app=true`.\n */\n labels?: Record<string, string>\n\n /**\n * Container hardening options (docker context only).\n *\n * All fields are opt-in and OFF by default to preserve existing behavior\n * (containers run with the image's default user/capabilities). Enable them\n * for untrusted workloads. `dropAllCapabilities` and a non-root `user`\n * (see {@link SpawnConfig.user}) can break images that expect root or\n * specific capabilities, so they are not applied unless requested.\n */\n hardening?: ContextHardening\n}\n\n/**\n * Opt-in container hardening for the docker context. Every field defaults to\n * \"unset\" so an omitted `hardening` (or omitted field) reproduces the prior,\n * unrestricted behavior — this keeps the option purely additive.\n */\nexport interface ContextHardening {\n /**\n * Drop all Linux capabilities (`CapDrop: ['ALL']`). Strong isolation, but\n * breaks images needing capabilities (e.g. binding low ports, `ping`).\n * Default: `false`.\n */\n dropAllCapabilities?: boolean\n /**\n * Set `no-new-privileges` so processes can't gain privileges via setuid\n * binaries. Low blast radius; safe for most workloads. Default: `false`.\n */\n noNewPrivileges?: boolean\n /**\n * Mount the container root filesystem read-only. Pair with writable mounts\n * for scratch space. Default: `false`.\n */\n readonlyRootfs?: boolean\n /**\n * Max number of processes (`PidsLimit`) — caps fork-bombs. Default: unset\n * (no limit). A few hundred is usually plenty for an agent sandbox.\n */\n pidsLimit?: number\n}\n\nexport interface ContextMount {\n /** Absolute host path to mount. */\n source: string\n /** Absolute path inside the execution context. */\n target: string\n /** Mount read-only. Defaults to false for Docker's native bind behavior. */\n readonly?: boolean\n /**\n * Apply the SELinux shared label (`:z`) so the host user and the container\n * user can both read/write the mount (docker context only). No-op on\n * non-SELinux hosts. Combine with `SpawnConfig.user` to avoid root-owned\n * files leaking onto the host. Mutually exclusive with `readonly`.\n */\n shared?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Execution context interface\n// ---------------------------------------------------------------------------\n\nexport type ContextType = 'process' | 'docker' | 'sandbox'\n\nexport interface ExecutionContext {\n /** Context type identifier */\n readonly type: ContextType\n\n /** What this context supports */\n readonly capabilities: ContextCapabilities\n\n /** Spawn a new execution environment */\n spawn: (config?: SpawnConfig) => Promise<ExecutionHandle>\n\n /**\n * Execute a shell command in the context.\n *\n * `signal` propagates abort all the way down to the underlying child\n * process — the implementation is expected to wire it into whatever\n * spawn primitive it uses so the OS receives a SIGTERM (or equivalent)\n * when the caller aborts. Without it, a cancelled tool returns its\n * cancellation marker to the model but the underlying process keeps\n * running in the background, orphaning compute / locks / IO. Pass\n * `ctx.signal` from a tool body to inherit the per-call + run-level\n * abort union; pass a freshly-built one for a host-driven kill.\n *\n * Implementations are free to ignore `signal` (the contract degrades\n * gracefully — the process simply won't be killed), but the in-process\n * default DOES honor it via `child_process.exec`'s native `signal`\n * option.\n */\n /**\n * Run a shell command. `timeout` is in SECONDS (default 30) — every\n * implementation converts internally (process ×1000 to ms, docker wraps\n * with coreutils `timeout`, sandbox SDKs take seconds/ms per their API).\n * Documented here because the unit is invisible at call sites and a\n * caller assuming milliseconds would set an instant-kill timeout.\n */\n exec: (handle: ExecutionHandle, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number, signal?: AbortSignal }) => Promise<ExecResult>\n\n /**\n * Start a process in the background. Settles as soon as `spawn` returns\n * — does NOT wait for the child to exit. Stdout + stderr stream\n * interleaved to the file at the returned `outputPath`. The caller\n * (typically the agent) reads incremental output via the regular\n * {@link ExecutionContext.readFile} seam.\n *\n * Optional — contexts without background support (some remote sandboxes)\n * just don't implement it. The shell tool surfaces a clean\n * \"background mode is not supported in this execution context\" error\n * when this is undefined.\n *\n * `onExit` is called once when the child terminates (natural, killed,\n * or error). The same instance that called `execBackground` is the\n * exclusive owner of the callback — it's not a multi-cast bus. Hosts\n * wire this to the agent's pending-notification queue so the model\n * gets a `<task-notification>` on its next turn.\n *\n * `onExit` is OPTIONAL: remote / durable contexts may have no legal\n * way to push a callback from a timer back into the host (a Restate\n * journal, for instance, forbids out-of-band writes). Such contexts\n * simply record the exit in their registry; the agent loop reconciles\n * by polling {@link ExecutionContext.listBackground} at run\n * boundaries, where a host journal wrapper is legal (the\n * `background:reconcile` hook is the journalable seam).\n *\n * See `docs/RUN_IN_BACKGROUND.md` for the broader design contract\n * (file location, replay semantics, suppression rules).\n */\n execBackground?: (\n handle: ExecutionHandle,\n command: string,\n options: {\n cwd?: string\n env?: Record<string, string>\n /**\n * Absolute directory the context appends `<task-id>.log` to. The\n * agent owns this path because it carries session-shaped knowledge\n * (`<userDir>/tasks/<sessionId>/`) the context can't synthesize.\n * Must already exist OR be creatable by the context — the\n * implementation handles `mkdir -p` defensively.\n */\n outputDir: string\n /** Push-style exit callback. Optional — see the method doc. */\n onExit?: (info: TaskExitInfo) => void\n /**\n * Cap on bytes written to the output file. Beyond the cap the\n * process KEEPS RUNNING; further output is counted but dropped,\n * and a `<output-truncated bytes-dropped=\"N\"/>` marker is\n * appended when the task settles. Unset / non-positive = no cap.\n */\n maxOutputBytes?: number\n /**\n * Stall watchdog: when the task produces no output for this many\n * milliseconds, `onStall` fires ONCE (one-shot — re-arms only\n * after fresh output arrives). The process is NOT killed; the\n * consumer decides (typically by telling the model the task may\n * be stuck at an interactive prompt). Unset = no watchdog.\n */\n stallTimeoutMs?: number\n /** One-shot stall callback — see `stallTimeoutMs`. */\n onStall?: (info: TaskStallInfo) => void\n },\n ) => Promise<TaskHandle>\n\n /**\n * Open a LIVE bidirectional byte stream to a long-lived process in the\n * context. The third execution mode, alongside the buffered `exec`\n * (request/response) and `execBackground` (fire-and-forget into a log\n * file): neither of those can carry a protocol that interleaves reads\n * and writes over the child's lifetime.\n *\n * Motivating consumer: stdio MCP servers. An MCP server is an ordinary\n * process doing `fs.readFileSync` / `fs.writeFile` against whatever\n * filesystem it was spawned on. Spawned on the host (the default — see\n * `McpServerConfig.scope`) it sees the HOST filesystem while the agent's\n * own tools see the context's, so the two disagree about what a path\n * means. Routing the server through this seam puts both on the same\n * filesystem.\n *\n * Optional — a context backed by a request/response-only API can't\n * implement it. Advertise support via `capabilities.stream`; callers\n * check that (or `execStream !== undefined`) and degrade with a clear\n * error rather than silently falling back to a host spawn, which would\n * reintroduce exactly the split this seam exists to close.\n *\n * IMPLEMENTOR CONTRACT — the shape is deliberately the SUPERSET of what\n * the backing APIs can do, because they do not agree:\n *\n * - `stdout` / `stderr` are byte streams, never pre-decoded strings.\n * A provider SDK that hands back `string` has already destroyed any\n * multibyte codepoint split across a chunk boundary; the caller\n * needs the raw bytes to run its own streaming decoder.\n * - `stderr` is OPTIONAL because a PTY-backed context physically\n * cannot separate it — one terminal, one stream. Such contexts\n * redirect the child's stderr to a file; they may then surface it\n * here out-of-band (the Daytona context polls that file back), but\n * they are not obliged to, so consumers must tolerate its absence.\n * - A PTY-backed context MUST also neutralise terminal semantics\n * before the child's first byte (echo of everything written to\n * stdin, `\\n`→`\\r\\n` rewriting, and the ~4096-byte canonical-mode\n * line cap that arbitrary protocol frames exceed). In practice:\n * launch through `stty raw -echo; exec <cmd> 2><file>`.\n * - `exited` resolves with the exit code. On a PTY the child is an\n * interactive shell that never exits on its own, so `kill()` is\n * the normal termination path, not an abnormal one.\n * - Providers that impose a default command timeout (E2B and Daytona\n * both default to 60s) MUST disable or maximise it here. A stream\n * opened by this method is expected to live for the whole session.\n */\n execStream?: (\n handle: ExecutionHandle,\n command: string,\n options?: {\n cwd?: string\n env?: Record<string, string>\n /** Aborts startup and kills the child once running. */\n signal?: AbortSignal\n },\n ) => Promise<StreamHandle>\n\n /**\n * SIGTERM the whole process group of a running background task.\n * Idempotent — second call returns `null` (or the cached exit info).\n * Resolves once the process has exited AND its output stream has\n * been flushed + closed.\n *\n * `null` return on miss (unknown id, already cleaned up) so the\n * shell_kill tool can surface a clean \"no such task\" message\n * without throwing.\n */\n killBackground?: (\n handle: ExecutionHandle,\n taskId: string,\n ) => Promise<TaskExitInfo | null>\n\n /**\n * Snapshot of every task in the context's registry — running AND\n * terminated (entries remain until the next context destroy, so\n * the model can still read output of exited tasks).\n */\n listBackground?: (\n handle: ExecutionHandle,\n ) => Promise<readonly TaskEntry[]>\n\n /**\n * Block until a background task terminates, then resolve with its\n * exit info. Resolves immediately for already-terminated tasks.\n *\n * Returns `null` when:\n * - the task id is unknown (or owned by another handle — same\n * isolation contract as `killBackground`), OR\n * - `timeoutMs` elapsed / `signal` aborted before the task exited.\n *\n * Callers that need to distinguish \"unknown\" from \"still running\"\n * should consult `listBackground` first (the `wait_task` tool does).\n *\n * This is the injectable wait seam for the `wait_task` tool:\n * in-process contexts implement it on the existing `onExit`\n * machinery; durable hosts implement it as an awakeable park\n * (runner fires task-exit over their bridge → resolveAwakeable).\n */\n waitBackground?: (\n handle: ExecutionHandle,\n taskId: string,\n options?: { timeoutMs?: number, signal?: AbortSignal },\n ) => Promise<TaskExitInfo | null>\n\n /**\n * Transfer ownership of every still-running task from `fromHandle`\n * to `toHandle`. Used by the spawn tool to \"promote\" a subagent's\n * background tasks up to the parent's handle so they outlive the\n * subagent's destroy() — matching shell semantics, where a `&`-ed\n * command outlives the parent process.\n *\n * Side effects:\n * - The task's `handleId` is rewritten, so subsequent\n * `listBackground(toHandle)` / `killBackground(toHandle, …)` /\n * `destroy(toHandle)` see it (and `fromHandle`-scoped operations\n * don't).\n * - When `newOnExit` is provided, the original `onExit` is\n * REPLACED with it. Critical: the task's natural exit callback\n * was captured against the spawning agent's hook bus, which is\n * about to be destroyed; without rewiring, the parent never\n * learns when the task terminates. Pass a closure that fires\n * the parent agent's `background:exit` hook.\n *\n * Returns the entries that were actually reassigned (running tasks\n * only; terminated ones stay where they are). Implementations that\n * don't support reassignment can leave this undefined; the caller\n * (spawn.ts) falls back to the current behavior of killing child\n * tasks at subagent shutdown.\n */\n reassignBackgroundTasks?: (\n fromHandle: ExecutionHandle,\n toHandle: ExecutionHandle,\n newOnExit?: (info: TaskExitInfo) => void,\n ) => Promise<readonly TaskEntry[]>\n\n /** Read a file from the context's filesystem */\n readFile: (handle: ExecutionHandle, path: string) => Promise<string>\n\n /**\n * Read a file from the context's filesystem as raw bytes.\n *\n * Used by `read_file` to dispatch image / binary files into the multimodal\n * `ToolResultContent[]` route. Optional — when not implemented, the tool\n * falls back to `base64 < path` via the `exec` seam, which works in any\n * shell-capable context. Implementations that already have a native\n * binary read (in-process `fs.readFile` without encoding, container API,\n * sandbox SDK) should override for the latency win.\n */\n readFileBinary?: (handle: ExecutionHandle, path: string) => Promise<Uint8Array>\n\n /**\n * Cheap size probe for a file in the context's filesystem, in bytes.\n * `null` = unknowable (missing file, not a regular file, probe failed,\n * or containment rejected the path — a probe must not leak even the\n * size of files outside the workspace root).\n *\n * Exists because `readFile` has no partial-read form: tools that must\n * materialize a whole file (read_file's text path, edit/multi_edit,\n * grep's no-rg fallback) consult this first to refuse multi-GB targets\n * instead of OOMing the HOST — the read lands in host memory no matter\n * where the file lives. Per-context posture:\n *\n * - `process` — implements it (local stat, µs).\n * - `docker` — implements it via `wc -c` through its exec; its\n * `readFile` gets transport headroom above the tool ceiling so\n * the probe (tool policy), not the 10MB exec cap, decides read\n * limits — same experience as every other context.\n * - `sandbox` (and the e2b / daytona providers behind it) —\n * implements it via `wc -c` through the provider's exec: one extra\n * round-trip per whole-read tool call, paid so a multi-GB sandbox\n * file is refused BEFORE the SDK transfers it into host memory.\n *\n * KNOWN RESIDUAL (sandbox providers): this guards the `readFile` path\n * only. Their `exec` output is buffered inside the provider SDKs\n * (e2b `commands.run`, daytona `executeCommand`), which we cannot cap\n * from this side — a command that streams GBs to stdout within its\n * timeout window can still balloon host memory. Docker doesn't share\n * the residual (we own its transport; see `output-cap.ts`). A shell\n * wrapper (`| head -c`) was considered and rejected: preserving exit\n * codes, stderr separation, and timeout semantics through a pipe for\n * EVERY command is fragile, and the timeout already time-bounds the\n * exposure.\n */\n fileSize?: (handle: ExecutionHandle, path: string) => Promise<number | null>\n\n /** Write a file to the context's filesystem */\n writeFile: (handle: ExecutionHandle, path: string, content: string) => Promise<void>\n\n /**\n * List files in the context filesystem.\n *\n * Plain paths list immediate directory entries (legacy behavior). Paths that\n * contain glob metacharacters (`*`, `?`, `[]`, `{}`) request matched file\n * enumeration inside the target context. Contexts that cannot support\n * globbing should throw a clear error rather than silently returning no\n * matches. `metadata` is best-effort and only required when the context can\n * provide it without expensive per-file shell calls.\n */\n listFiles: (handle: ExecutionHandle, path: string, options?: ListFilesOptions) => Promise<ListFilesEntry[]>\n\n /**\n * Resolve the host-side port that a container port was published on\n * (docker context only, and only when the matching entry was created\n * with `host` omitted in `SpawnConfig.ports`).\n *\n * Resolves to `null` if the container port isn't published. Other\n * contexts (process, sandbox) don't implement this; they don't have\n * a port-mapping concept.\n */\n getMappedPort?: (handle: ExecutionHandle, containerPort: number) => Promise<number | null>\n\n /** Destroy the execution environment and clean up resources */\n destroy: (handle: ExecutionHandle) => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Background task types\n// ---------------------------------------------------------------------------\n\n/**\n * Lifecycle status of a background task.\n *\n * - `'running'` — process is still live; `exitCode` / `signal` unset.\n * - `'exited'` — process terminated on its own (clean or non-zero exit).\n * - `'killed'` — the host issued `killBackground` (SIGTERM to the group).\n *\n * The status is a coarse-grained signal; the exit code carries the\n * fine-grained detail (e.g. `143 = SIGTERM` for `'killed'`).\n */\nexport type BackgroundTaskStatus = 'running' | 'exited' | 'killed'\n\n/**\n * Returned synchronously by `execBackground` — the handle the model and\n * the framework use to refer to the task until it terminates.\n *\n * `outputPath` is an absolute path to the log file the context is\n * appending stdout + stderr to (interleaved by emit order). The model\n * reads it via the normal `read_file` tool; no special tool is required.\n */\nexport interface TaskHandle {\n /**\n * Stable id minted by the context — typically `bash_<n>` for\n * `ProcessContext`. Sequential within a single context instance,\n * resets when a new context is constructed. Forwarded to `killBackground` /\n * `listBackground` and stamped into every `<task-notification>` block.\n */\n taskId: string\n /** OS pid of the spawned shell wrapper (process-group leader on POSIX). */\n pid: number\n /** Absolute path to the log file the context is streaming output into. */\n outputPath: string\n}\n\n/**\n * A live bidirectional stream to a process in the context. Returned by\n * {@link ExecutionContext.execStream}; see that method for the full\n * implementor contract (why the streams are bytes, why `stderr` is\n * optional, what PTY-backed contexts must do before the first byte).\n *\n * Ownership: the caller owns the lifetime. Nothing reaps this handle\n * automatically, so `kill()` belongs in the consumer's teardown path —\n * for MCP that is the transport's `close()`, reached via the agent's\n * `mcpConnection.close()` during `destroy()`.\n */\nexport interface StreamHandle {\n /** Process id inside the context — NOT meaningful on the host. */\n pid: number\n /**\n * Child's stdout as raw byte chunks, in emit order.\n *\n * CONSUMER OBLIGATION: drain this (and {@link stderr}) for the stream's\n * lifetime. Nothing here is bounded, and the consequence of ignoring it\n * varies by backend and by host runtime, none of it good:\n *\n * - `process` / `docker` hand back real OS pipes. Under Node an unread\n * stream stops draining the fd and the child BLOCKS once the pipe\n * fills (~64 KiB) — a deadlock, not a slowdown. Under Bun the runtime\n * buffers eagerly instead, so it presents as unbounded host memory\n * (measured: 32 MiB of unread stderr never blocked the child).\n * - `e2b` / `daytona` buffer in a `StreamQueue`, which grows without\n * limit. It cannot do otherwise: both SDKs push output through a\n * synchronous `(data) => void` callback, so there is no channel to\n * apply backpressure, and dropping bytes would desynchronise a framed\n * protocol permanently. The queue logs a one-shot warning once the\n * unread backlog passes 8 MiB so a stalled consumer is diagnosable.\n *\n * `createContextStdioTransport` satisfies this for both streams.\n */\n stdout: AsyncIterable<Uint8Array>\n /**\n * Child's stderr, when the implementation can provide it separately.\n * Present on pipe-backed contexts. A PTY-backed context cannot split it\n * from the terminal stream, so it redirects stderr to a file and either\n * surfaces it here out-of-band or leaves this unset — expect a lower\n * latency guarantee there than on stdout.\n */\n stderr?: AsyncIterable<Uint8Array>\n /** Write to the child's stdin. Resolves once the bytes are handed off. */\n write: (data: Uint8Array) => Promise<void>\n /** Terminate the child. Idempotent — killing a dead child is a no-op. */\n kill: () => Promise<void>\n /**\n * Resolves with the exit code when the child terminates, including\n * termination via {@link kill}. Never rejects.\n */\n exited: Promise<number>\n}\n\n/**\n * Fired exactly once per task when the child process terminates, via\n * `execBackground`'s `onExit` callback. The agent layer translates this\n * into a queued `<task-notification>` for the next turn.\n *\n * `signal` is set when the child was terminated by a signal (e.g.\n * SIGTERM from our own kill-tree, SIGKILL from oom-killer); absent on\n * natural exit. `exitCode` is `128 + signal-number` on signal-killed\n * children, matching POSIX shell conventions — `143` for SIGTERM, etc.\n */\nexport interface TaskExitInfo {\n taskId: string\n status: Exclude<BackgroundTaskStatus, 'running'>\n exitCode: number\n signal?: NodeJS.Signals\n outputPath: string\n /** `Date.now()` delta between spawn and exit. */\n durationMs: number\n /** The original command string the model invoked — useful for telemetry / banner summary. */\n command: string\n}\n\n/**\n * One row in `listBackground`'s snapshot. Living entries (status `'running'`)\n * have `exitCode` / `signal` / `endedAt` unset; terminated entries carry the\n * same data `TaskExitInfo` returned at exit time.\n */\nexport interface TaskEntry {\n taskId: string\n pid: number\n command: string\n cwd: string\n startedAt: number\n /**\n * Set when the task terminated. Lets pull-based consumers (the agent's\n * run-boundary reconcile) derive `durationMs = endedAt - startedAt`\n * without depending on when the snapshot was taken.\n */\n endedAt?: number\n outputPath: string\n status: BackgroundTaskStatus\n exitCode?: number\n signal?: NodeJS.Signals\n /** Total bytes written to the output file so far — useful for \"task X has produced N KB\" UX hints. */\n bytesWritten: number\n}\n\n/**\n * Fired by the optional stall watchdog (see `execBackground`'s\n * `stallTimeoutMs`) when a running task has produced no output for the\n * configured window. The process is still alive — this is a signal, not\n * a state transition.\n */\nexport interface TaskStallInfo {\n taskId: string\n command: string\n outputPath: string\n /** Milliseconds since the last output chunk (>= the configured window). */\n stalledForMs: number\n /** Total bytes the task has written so far. */\n bytesWritten: number\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,YAAY,UAAU,aAAa,MAAM,OAAO,sBAAA,CAAuB,KAAK;;;;;;;AAQlF,MAAa,0BAA0B,QAAQ,aAAa;;;;;;;;;;;;;AAe5D,MAAM,2BAA2B;;;;;;;;;AAUjC,MAAM,gCAAgC;;;;;;;;;;;;;;AAetC,MAAM,2BAA2B;;;;;;;;;;;AAYjC,MAAM,aAAa;AAEnB,SAAS,iBAAiB,QAAsB;CAC9C,IAAI,CAAC,WAAW,KAAK,MAAM,GACzB,MAAM,IAAI,MAAM,oBAAoB,OAAO,iBAAiB,WAAW,EAAE;AAC7E;;;;;;;;;AAUA,SAAgB,uBAAuB,MAAoB;CACzD,MAAM,QAAQ,MAAsB,EAAE,SAAS,CAAC,CAAC,SAAS,GAAG,GAAG;CAChE,MAAM,QAAQ,MAAsB,EAAE,SAAS,CAAC,CAAC,SAAS,GAAG,GAAG;CAQhE,OAAO,GAPG,KAAK,eAOL,IANA,KAAK,KAAK,YAAY,IAAI,CAMtB,IALJ,KAAK,KAAK,WAAW,CAKb,EAAE,GAJV,KAAK,KAAK,YAAY,CAIT,IAHb,KAAK,KAAK,cAAc,CAGP,IAFjB,KAAK,KAAK,cAAc,CAEH,EAAE,GADtB,KAAK,KAAK,mBAAmB,CACH;AACvC;;AAMA,SAAS,QAAQ,KAAwB,MAAmC;CAC1E,MAAM,MAAyB,CAAC;CAChC,KAAK,MAAM,OAAO,MAChB,IAAI,IAAI,SAAS,KAAA,GACf,IAAI,OAAO,IAAI;CAEnB,OAAO;AACT;AAEA,SAAgB,qBAAqB,QAAwC;CAC3E,IAAI,UAAU;CACd,MAAM,0BAAU,IAAI,IAA6B;CACjD,MAAM,aAAa,QAAQ,OAAO,QAAQ,IAAI;CAC9C,MAAM,aAAa,QAAQ;CAC3B,MAAM,iBAAiB,QAAQ,kBAAkB;CAMjD,MAAM,UAA6B,QAAQ,eAAe,QACtD,QAAQ,QAAQ,KAAK;EAAC;EAAQ;EAAQ;EAAS;EAAQ;EAAU;EAAQ;EAAQ;CAAQ,CAAC,IAC1F,QAAQ;CAGZ,MAAM,gBAAgB,QAAQ,kBAAkB,KAAA,IAC5C,QAAQ,YAAY,OAAO,aAAa,IACxC,KAAA;;CAGJ,SAAS,aAAa,MAAc,QAAyB;EAC3D,MAAM,MAAM,SAAS,MAAM,MAAM;EACjC,OAAO,QAAQ,MAAO,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,WAAW,GAAG;CAChE;;;;;;;CAQA,eAAe,iBAAiB,QAAyB,MAA+B;EACtF,MAAM,OAAO,QAAQ,OAAO,KAAK,IAAI;EACrC,IAAI,kBAAkB,KAAA,GACpB,OAAO;EAIT,IAAI,WAAW;EACf,MAAM,OAAiB,CAAC;EACxB,OAAO,MACL,IAAI;GACF,WAAW,MAAM,SAAS,QAAQ;GAClC;EACF,QACM;GACJ,MAAM,SAAS,QAAQ,QAAQ;GAC/B,IAAI,WAAW,UAAU;IAEvB,WAAW;IACX;GACF;GAGA,KAAK,QAAQ,SAAS,QAAQ,QAAQ,CAAC;GACvC,WAAW;EACb;EAEF,MAAM,YAAY,KAAK,SAAS,QAAQ,UAAU,GAAG,IAAI,IAAI;EAC7D,MAAM,gBAAgB,MAAM,SAAS,aAAa,CAAC,CAAC,YAAY,aAAa;EAC7E,IAAI,CAAC,aAAa,eAAe,SAAS,GACxC,MAAM,IAAI,MACR,oCAAoC,KAAK,qBAAqB,cAAc,EAC9E;EAEF,OAAO;CACT;;;;;;;;CASA,MAAM,wBAAQ,IAAI,IAAuB;CACzC,IAAI,cAAc;;;;;;;;;;;;;;;;;;;;CAqBlB,MAAM,mBAAmB,uCAAuB,IAAI,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;CAsB1D,MAAM,iCAAiB,IAAI,IAA0B;CAErD,IAAI,wBAAwB;CAC5B,MAAM,oBAA0B;EAC9B,KAAK,MAAM,QAAQ,MAAM,OAAO,GAAG;GACjC,IAAI,KAAK,WAAW,WAClB;GACF,MAAM,MAAM,KAAK,OAAO;GACxB,IAAI,QAAQ,KAAA,GACV;GACF,IAAI;IACF,IAAI,yBACF,QAAQ,KAAK,CAAC,KAAK,SAAS;SAE5B,QAAQ,KAAK,KAAK,SAAS;GAC/B,QACM,CAIN;EACF;EAMA,KAAK,MAAM,SAAS,eAAe,KAAK,GACtC,iBAAiB,OAAO,SAAS;CACrC;CACA,MAAM,4BAAkC;EACtC,IAAI,CAAC,uBAAuB;GAC1B,QAAQ,GAAG,QAAQ,WAAW;GAC9B,wBAAwB;EAC1B;CACF;CAEA,OAAO;EACL,MAAM;EAEN,cAAc;GACZ,OAAO;GACP,YAAY;GACZ,MAAM;GACN,SAAS;GACT,KAAK;GACL,QAAQ;GAKR,eAAe;EACjB;EAEA,MAAM,MAAM,WAAmD;GAC7D,MAAM,KAAK,WAAW,EAAE;GACxB,MAAM,MAAM,WAAW,OAAO;GAW9B,IAAI,cAAc;GAClB,IAAI;IACF,MAAM,KAAK,GAAG;GAChB,SACO,KAAK;IACV,cAAe,KAA+B,SAAS;GACzD;GACA,IAAI,aACF,MAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;GAEtC,MAAM,SAA0B;IAAE;IAAI,MAAM;IAAW;GAAI;GAC3D,QAAQ,IAAI,IAAI,MAAM;GACtB,OAAO;EACT;EAEA,MAAM,KACJ,QACA,SACA,SACqB;GACrB,MAAM,MAAM,SAAS,MAAM,QAAQ,OAAO,KAAK,QAAQ,GAAG,IAAI,OAAO;GAKrE,IAAI,SAAS,QAAQ,SACnB,OAAO;IAAE,QAAQ;IAAI,QAAQ;IAAkC,UAAU;GAAI;GAG/E,MAAM,aAAa,SAAS,WAAW,QAAQ,QAAQ,WAAW,MAAM;GACxE,MAAM,YAAY;GAIlB,MAAM,aAAa,MAAM,UAAU;GAEnC,OAAO,IAAI,SAAqB,aAAa;IAc3C,MAAM,QAAQ,WAAW,WAAW,CAAC,MAAM,OAAO,GAAG;KACnD;KACA,KAAK;MAAE,GAAG;MAAS,GAAG;MAAY,GAAG,SAAS;KAAI;KAClD,OAAO;MAAC;MAAU;MAAQ;KAAM;KAChC,UAAU;IACZ,CAAC;IAMD,MAAM,aAAyB;KAAE,MAAM;KAAI,OAAO;IAAE;IACpD,MAAM,aAAyB;KAAE,MAAM;KAAI,OAAO;IAAE;IACpD,IAAI,kBAAkB;IACtB,IAAI,WAAW;IACf,IAAI,gBAAgB;IACpB,IAAI,UAAU;IAKd,MAAM,2BAAiC;KACrC,iBAAiB,OAAO,SAAS;KAKjC,iBAJoC;MAClC,IAAI,CAAC,SACH,iBAAiB,OAAO,SAAS;KACrC,GAAG,6BACM,CAAC,CAAC,QAAQ;IACrB;IAEA,MAAM,WAAW,MAAkB,UAAwB;KAEzD,IADgB,aAAa,MAAM,OAAO,SAChC,MAAM,QAAQ,CAAC,iBAAiB;MACxC,kBAAkB;MAClB,mBAAmB;KACrB;IACF;IAEA,MAAM,QAAQ,GAAG,SAAQ,UAAS,QAAQ,YAAY,KAAe,CAAC;IACtE,MAAM,QAAQ,GAAG,SAAQ,UAAS,QAAQ,YAAY,KAAe,CAAC;IAEtE,MAAM,eAAe,YAAY,IAC7B,iBAAiB;KACf,WAAW;KACX,mBAAmB;IACrB,GAAG,SAAS,IACZ,KAAA;IAEJ,MAAM,gBAAsB;KAC1B,gBAAgB;KAChB,mBAAmB;IACrB;IACA,MAAM,aAAa,SAAS;IAC5B,IAAI,YACF,WAAW,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;IAE9D,IAAI;IAEJ,MAAM,UAAU,UAAkB,gBAA+B;KAC/D,IAAI,SACF;KACF,UAAU;KACV,IAAI,cACF,aAAa,YAAY;KAC3B,IAAI,gBACF,aAAa,cAAc;KAC7B,IAAI,YACF,WAAW,oBAAoB,SAAS,OAAO;KACjD,MAAM,cAAc,cACf,WAAW,OAAO,GAAG,WAAW,KAAK,IAAI,gBAAgB,cAC1D,WAAW;KACf,SAAS;MAAE,QAAQ,WAAW;MAAM,QAAQ;MAAa;KAAS,CAAC;IACrE;IAQA,MAAM,oBAAoB,MAAqB,WAAwC;KACrF,IAAI,eAAe;MACjB,OAAO,KAAK,mBAAmB;MAC/B;KACF;KACA,IAAI,UAAU;MACZ,OAAO,KAAK,2BAA2B,UAAU,GAAG;MACpD;KACF;KACA,IAAI,iBAAiB;MACnB,OAAO,KAAK,sBAAsB,WAAW,gBAAgB,CAAC;MAC9D;KACF;KACA,IAAI,QAAQ;MAGV,OAAO,KAAU,wBAAwB,QAAQ;MACjD;KACF;KACA,OAAO,OAAO,SAAS,WAAW,OAAO,CAAC;IAC5C;IAEA,MAAM,GAAG,UAAU,QAAQ;KAIzB,OAAO,GAAG,IAAI,OAAO;IACvB,CAAC;IAED,MAAM,GAAG,UAAU,MAAM,WAAW,iBAAiB,MAAM,MAAM,CAAC;IAElE,MAAM,GAAG,SAAS,MAAM,WAAW;KAOjC,iBAAiB,iBAAiB;MAChC,IAAI,SACF;MACF,MAAM,QAAQ,QAAQ;MACtB,MAAM,QAAQ,QAAQ;MACtB,iBAAiB,MAAM,MAAM;KAC/B,GAAG,wBAAwB;KAC3B,eAAe,QAAQ;IACzB,CAAC;GACH,CAAC;EACH;EAEA,MAAM,SAAS,QAAyB,MAA+B;GACrE,OAAO,SAAS,MAAM,iBAAiB,QAAQ,IAAI,GAAG,OAAO;EAC/D;EAEA,MAAM,SAAS,QAAyB,MAAsC;GAC5E,IAAI;IAKF,MAAM,OAAO,MAAM,KAAK,MAAM,iBAAiB,QAAQ,IAAI,CAAC;IAC5D,OAAO,KAAK,OAAO,IAAI,KAAK,OAAO;GACrC,QACM;IACJ,OAAO;GACT;EACF;EAEA,MAAM,eAAe,QAAyB,MAAmC;GAG/E,MAAM,MAAM,MAAM,SAAS,MAAM,iBAAiB,QAAQ,IAAI,CAAC;GAC/D,OAAO,IAAI,WAAW,GAAG;EAC3B;EAEA,MAAM,UAAU,QAAyB,MAAc,SAAgC;GACrF,MAAM,WAAW,MAAM,iBAAiB,QAAQ,IAAI;GACpD,MAAM,MAAM,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;GAClD,MAAM,UAAU,UAAU,SAAS,OAAO;EAC5C;EAEA,MAAM,UAAU,QAAyB,MAAc,SAAoC;GACzF,IAAI,SAAS,MAAM;IACjB,MAAM,iBAAiB,QAAQ,kBAAkB,IAAI,KAAK,GAAG;IAC7D,MAAM,UAAU,MAAM,eAAe,MAAM,OAAO,KAAK;KACrD,OAAO,SAAS,SAAS;KACzB,UAAU,SAAS;KACnB,WAAW,SAAS;KACpB,QAAQ,SAAS;IACnB,CAAC;IACD,IAAI,kBAAkB,KAAA,GACpB,OAAO;IACT,MAAM,YAA8B,CAAC;IACrC,KAAK,MAAM,SAAS,SAClB,IAAI;KACF,MAAM,iBAAiB,QAAQ,MAAM,IAAI;KACzC,UAAU,KAAK,KAAK;IACtB,QACM,CAIN;IAEF,OAAO;GACT;GAKA,QAAO,MADe,QAAQ,MAAM,iBAAiB,QAAQ,IAAI,GAAG,EAAE,eAAe,KAAK,CAAC,EAAA,CAC5E,KAAI,MAAM,EAAE,YAAY,IAAI,GAAG,EAAE,KAAK,KAAK,EAAE,IAAK;EACnE;;;;;;;;;;;;;;;;;;EAmBA,MAAM,WACJ,QACA,SACA,SACuB;GACvB,MAAM,MAAM,SAAS,MAAM,QAAQ,OAAO,KAAK,QAAQ,GAAG,IAAI,OAAO;GACrE,MAAM,aAAa,MAAM,UAAU;GAEnC,SAAS,QAAQ,eAAe;GAEhC,MAAM,QAAQ,WAAW,WAAW,CAAC,MAAM,OAAO,GAAG;IACnD;IACA,KAAK;KAAE,GAAG;KAAS,GAAG;KAAY,GAAG,SAAS;IAAI;IAClD,OAAO;KAAC;KAAQ;KAAQ;IAAM;IAC9B,UAAU;GACZ,CAAC;GAOD,oBAAoB;GACpB,eAAe,IAAI,OAAO,OAAO,EAAE;GAKnC,IAAI,aAA2B;GAC/B,MAAM,GAAG,UAAU,QAAQ;IAAE,aAAa;GAAI,CAAC;GAE/C,MAAM,SAAS,IAAI,SAAiB,gBAAgB;IAKlD,MAAM,KAAK,UAAU,MAAM,eAAe;KACxC,eAAe,OAAO,KAAK;KAI3B,YACE,SAAS,OACL,OACA,eAAe,YACb,MACA,aACE,MACA,CACV;IACF,CAAC;GACH,CAAC;GAED,MAAM,OAAO,YAA2B;IACtC,IAAI,MAAM,aAAa,QAAQ,MAAM,eAAe,MAClD;IACF,MAAM,MAAM,MAAM;IAClB,IAAI,QAAQ,KAAA,GACV;IACF,IAAI;KACF,IAAI,yBACF,QAAQ,KAAK,CAAC,KAAK,SAAS;UAE5B,QAAQ,KAAK,KAAK,SAAS;IAC/B,QACM,CAGN;GACF;GAEA,IAAI,SAAS,QACX,IAAI,QAAQ,OAAO,SACjB,KAAU;QAEP;IAKH,MAAM,gBAAgB;KAAE,KAAU;IAAE;IACpC,MAAM,eAAe,QAAQ,OAAQ,oBAAoB,SAAS,OAAO;IACzE,QAAQ,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;IAChE,OAAO,KAAK,QAAQ,MAAM;GAC5B;GAGF,OAAO;IACL,KAAK,MAAM,OAAO;IAGlB,QAAQ,MAAM;IACd,QAAQ,MAAM;IACd,OAAO,OAAO,SAAoC;KAChD,IAAI,YACF,MAAM;KACR,MAAM,QAAQ,MAAM;KACpB,IAAI,CAAC,SAAS,MAAM,WAClB,MAAM,IAAI,MAAM,oCAAoC,MAAM,OAAO,GAAG,EAAE;KAOxE,MAAM,IAAI,SAAe,cAAc,gBAAgB;MACrD,MAAM,MAAM,OAAM,QAAO,MAAM,YAAY,GAAG,IAAI,aAAa,CAAC;KAClE,CAAC;IACH;IACA;IACA;GACF;EACF;EAEA,MAAM,eACJ,QACA,SACA,SASqB;GACrB,MAAM,MAAM,QAAQ,MAAM,QAAQ,OAAO,KAAK,QAAQ,GAAG,IAAI,OAAO;GAKpE,MAAM,aAAa,MAAM,UAAU;GAEnC,MAAM,MAAM,QAAQ,WAAW,EAAE,WAAW,KAAK,CAAC;GAUlD,MAAM,SAAS,QAAQ,EAAE;GACzB,iBAAiB,MAAM;GACvB,MAAM,aAAa,QAAQ,QAAQ,WAAW,GAAG,OAAO,GAAG,iBAAiB,KAAK;GAIjF,oBAAoB;GAUpB,MAAM,eAA4B,kBAAkB,YAAY,EAAE,OAAO,IAAI,CAAC;GAS9E,aAAa,GAAG,UAAU,QAAQ;IAChC,IAAI,QAAQ,IAAI,cACd,QAAQ,OAAO,MAAM,0BAA0B,OAAO,qBAAqB,IAAI,QAAQ,GAAG;GAC9F,CAAC;GAMD,MAAM,QAAQ,WAAW,WAAW,CAAC,MAAM,OAAO,GAAG;IACnD;IACA,KAAK;KAAE,GAAG;KAAS,GAAG;KAAY,GAAG,QAAQ;IAAI;IACjD,OAAO;KAAC;KAAU;KAAQ;IAAM;IAChC,UAAU;GACZ,CAAC;GAED,MAAM,QAAmB;IACvB;IACA,UAAU,OAAO;IACjB,KAAK,MAAM,OAAO;IAClB;IACA;IACA,WAAW,KAAK,IAAI;IACpB;IACA;IACA;IACA,QAAQ;IACR,cAAc;IACd,SAAS;IACT,QAAQ,QAAQ;GAClB;GACA,MAAM,IAAI,QAAQ,KAAK;GASvB,MAAM,iBAAiB,OAAO,QAAQ,mBAAmB,YAAY,QAAQ,iBAAiB,IAC1F,QAAQ,iBACR,KAAA;GACJ,IAAI,eAAe;GAKnB,IAAI,eAAe;GAMnB,MAAM,iBAAiB,OAAO,QAAQ,mBAAmB,YAAY,QAAQ,iBAAiB,IAC1F,QAAQ,iBACR,KAAA;GACJ,MAAM,UAAU,QAAQ;GACxB,IAAI;GACJ,IAAI,eAAe,KAAK,IAAI;GAC5B,MAAM,sBAA4B;IAChC,IAAI,CAAC,kBAAkB,CAAC,SACtB;IACF,IAAI,YACF,aAAa,UAAU;IACzB,aAAa,iBAAiB;KAC5B,aAAa,KAAA;KACb,IAAI,MAAM,SACR;KACF,IAAI;MACF,QAAQ;OACN;OACA;OACA;OACA,cAAc,KAAK,IAAI,IAAI;OAC3B,cAAc,MAAM;MACtB,CAAC;KACH,SACO,KAAK;MACV,IAAI,QAAQ,IAAI,cACd,QAAQ,OAAO,MAAM,0BAA0B,OAAO,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GAAG;KAChI;IACF,GAAG,cAAc;IACjB,WAAW,QAAQ;GACrB;GACA,MAAM,wBAA8B;IAClC,IAAI,YAAY;KACd,aAAa,UAAU;KACvB,aAAa,KAAA;IACf;GACF;GACA,cAAc;GAQd,MAAM,eAAe,UAAwB;IAC3C,MAAM,gBAAgB,MAAM;IAC5B,eAAe,KAAK,IAAI;IACxB,cAAc;IACd,IAAI,mBAAmB,KAAA,GAAW;KAChC,MAAM,SAAS,MAAM,eAAe,eAAe,MAAM;KACzD,IAAI,gBAAgB,UAAU,gBAAgB;MAC5C,eAAe;MACf,gBAAgB,MAAM;MACtB;KACF;KACA,MAAM,OAAO,iBAAiB;KAC9B,IAAI,MAAM,SAAS,MAAM;MACvB,eAAe;MAKf,IAAI,MAAM;MACV,OAAO,MAAM,MAAM,MAAM,OAAO,SAAU,KACxC;MACF,gBAAgB,MAAM,SAAS;MAC/B,IAAI,MAAM,GACR,aAAa,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC;MAC3C;KACF;IACF;IACA,aAAa,MAAM,KAAK;GAC1B;GACA,MAAM,QAAQ,GAAG,SAAQ,UAAS,YAAY,KAAe,CAAC;GAC9D,MAAM,QAAQ,GAAG,SAAQ,UAAS,YAAY,KAAe,CAAC;GAM9D,MAAM,UAAU,OAA0B,MAAqB,QAA+B,eAA8B;IAC1H,IAAI,MAAM,SACR;IACF,MAAM,UAAU;IAChB,gBAAgB;IAChB,MAAM,UAAU,KAAK,IAAI;IAGzB,MAAM,SACF,WAAW,aAAa,MAAM,gBAC5B,WACA;IAIN,MAAM,WAAW,SAAS,OACtB,OACA,WAAW,YACT,MACA,SACE,MACA;IACR,MAAM,SAAS;IACf,MAAM,WAAW;IACjB,IAAI,QACF,MAAM,SAAS;IAcjB,IAAI,YACF,IAAI;KACF,aAAa,MAAM,KAAK,WAAW,GAAG;IACxC,QACM,CAEN;IAEF,IAAI,eAAe,GACjB,IAAI;KAGF,aAAa,MAAM,sCAAsC,aAAa,MAAM;IAC9E,QACM,CAEN;IAEF,aAAa,UAAU;KAKrB,IAAI;MACF,MAAM,SAAS,oBAAoB,KAAK,CAAC;KAC3C,SACO,KAAK;MAIV,IAAI,QAAQ,IAAI,cACd,QAAQ,OAAO,MAAM,0BAA0B,OAAO,iBAAiB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,GAAG;KAC/H;IACF,CAAC;IAQD,MAAM,QAAQ,KAAA;IACd,MAAM,eAAe,KAAA;GACvB;GAEA,MAAM,GAAG,UAAU,MAAM,WAAW,OAAO,SAAS,MAAM,MAAM,CAAC;GACjE,MAAM,GAAG,UAAS,QAAO,OAAO,SAAS,MAAM,MAAM,iBAAiB,IAAI,SAAS,CAAC;GAEpF,MAAM,GAAG,SAAS,MAAM,WAAW;IAajC,iBAPwC;KACtC,IAAI,MAAM,SACR;KACF,MAAM,QAAQ,QAAQ;KACtB,MAAM,QAAQ,QAAQ;KACtB,OAAO,SAAS,MAAM,MAAM;IAC9B,GAAG,wBACU,CAAC,CAAC,QAAQ;GACzB,CAAC;GAED,OAAO;IAAE;IAAQ,KAAK,MAAM;IAAK;GAAW;EAC9C;EAEA,MAAM,eAAe,QAAyB,QAA8C;GAC1F,MAAM,QAAQ,MAAM,IAAI,MAAM;GAO9B,IAAI,CAAC,SAAS,MAAM,aAAa,OAAO,IACtC,OAAO;GAIT,IAAI,MAAM,WAAW,WACnB,OAAO,oBAAoB,KAAK;GAOlC,MAAM,gBAAgB;GAMtB,MAAM,SAAS,IAAI,SAAe,aAAa;IAC7C,IAAI,MAAM,SAAS;KACjB,SAAS;KACT;IACF;IACA,MAAM,iBAAiB,MAAM;IAC7B,MAAM,UAAU,SAAS;KACvB,iBAAiB,IAAI;KACrB,SAAS;IACX;GACF,CAAC;GAED,iBAAiB,MAAM,OAAO,SAAS;GACvC,MAAM;GACN,OAAO,oBAAoB,KAAK;EAClC;EAEA,MAAM,eACJ,QACA,QACA,SAC8B;GAC9B,MAAM,QAAQ,MAAM,IAAI,MAAM;GAI9B,IAAI,CAAC,SAAS,MAAM,aAAa,OAAO,IACtC,OAAO;GACT,IAAI,MAAM,SACR,OAAO,oBAAoB,KAAK;GAElC,OAAO,IAAI,SAA8B,aAAa;IACpD,IAAI,OAAO;IACX,IAAI;IACJ,MAAM,SAAS,SAAS;IACxB,MAAM,UAAU,UAAqC;KACnD,IAAI,MACF;KACF,OAAO;KACP,IAAI,OACF,aAAa,KAAK;KACpB,QAAQ,oBAAoB,SAAS,OAAO;KAC5C,SAAS,KAAK;IAChB;IACA,SAAS,UAAgB;KACvB,OAAO,IAAI;IACb;IAMA,MAAM,iBAAiB,MAAM;IAC7B,MAAM,UAAU,SAAS;KACvB,iBAAiB,IAAI;KACrB,OAAO,oBAAoB,KAAK,CAAC;IACnC;IAEA,IAAI,QAAQ;KACV,IAAI,OAAO,SAAS;MAClB,OAAO,IAAI;MACX;KACF;KACA,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;IAC1D;IACA,MAAM,YAAY,SAAS;IAC3B,IAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,KAAK,YAAY,GAAG;KAChF,QAAQ,WAAW,QAAQ,WAAW,IAAI;KAC1C,MAAM,QAAQ;IAChB;GACF,CAAC;EACH;EAEA,MAAM,wBACJ,YACA,UACA,WAC+B;GAG/B,IAAI,WAAW,OAAO,SAAS,IAC7B,OAAO,CAAC;GACV,MAAM,WAAwB,CAAC;GAC/B,KAAK,MAAM,SAAS,MAAM,OAAO,GAAG;IAClC,IAAI,MAAM,aAAa,WAAW,MAAM,MAAM,WAAW,WACvD;IACF,MAAM,WAAW,SAAS;IAK1B,IAAI,WACF,MAAM,SAAS;IACjB,SAAS,KAAK,iBAAiB,KAAK,CAAC;GACvC;GACA,OAAO;EACT;EAEA,MAAM,eAAe,QAAwD;GAM3E,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CACvB,QAAO,MAAK,EAAE,aAAa,OAAO,EAAE,CAAC,CACrC,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS,CAAC,CACzC,IAAI,gBAAgB;EACzB;EAEA,MAAM,QAAQ,QAAwC;GAkBpD,MAAM,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,QAAO,MAAK,EAAE,aAAa,OAAO,MAAM,CAAC,EAAE,OAAO;GACxF,MAAM,QAAQ,IAAI,UAAU,IAAI,OAAO,UAAU;IAC/C,MAAM,gBAAgB;IACtB,MAAM,IAAI,SAAe,aAAa;KACpC,IAAI;KACJ,MAAM,iBAAiB,MAAM;KAC7B,MAAM,UAAU,SAAS;MACvB,iBAAiB,IAAI;MACrB,IAAI,YACF,aAAa,UAAU;MACzB,SAAS;KACX;KACA,iBAAiB,MAAM,OAAO,SAAS;KAQvC,aAAa,iBAAiB;MAC5B,iBAAiB,MAAM,OAAO,SAAS;MAIvC,MAAM,cAAc,QAAQ;MAC5B,SAAS;KACX,GAAG,cAAc;KACjB,WAAW,QAAQ;IACrB,CAAC;GACH,CAAC,CAAC;GAGF,KAAK,MAAM,CAAC,QAAQ,UAAU,OAC5B,IAAI,MAAM,aAAa,OAAO,IAC5B,MAAM,OAAO,MAAM;GAMvB,KAAK,MAAM,CAAC,OAAO,YAAY,gBAC7B,IAAI,YAAY,OAAO,IAAI;IACzB,iBAAiB,OAAO,SAAS;IACjC,eAAe,OAAO,KAAK;GAC7B;GAEF,QAAQ,OAAO,OAAO,EAAE;GAUxB,IAAI,yBAAyB,QAAQ,SAAS,GAAG;IAC/C,QAAQ,IAAI,QAAQ,WAAW;IAC/B,wBAAwB;GAC1B;EACF;CACF;AACF;;;;;;;;AAyEA,SAAgB,iBAAiB,OAAiC,QAA8B;CAC9F,MAAM,MAAM,OAAO;CACnB,IAAI,QAAQ,KAAA,GACV;CACF,IAAI;EACF,IAAI,yBACF,QAAQ,KAAK,CAAC,KAAK,MAAM;OAEzB,QAAQ,KAAK,KAAK,MAAM;CAC5B,QACM,CAGN;AACF;;;;;;;AAQA,SAAS,iBAAiB,OAA6B;CACrD,OAAO;EACL,QAAQ,MAAM;EACd,KAAK,MAAM;EACX,SAAS,MAAM;EACf,KAAK,MAAM;EACX,WAAW,MAAM;EACjB,GAAI,MAAM,YAAY,KAAA,IAAY,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC;EAChE,YAAY,MAAM;EAClB,QAAQ,MAAM;EACd,GAAI,MAAM,aAAa,KAAA,IAAY,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;EACnE,GAAI,MAAM,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EAC/C,cAAc,MAAM;CACtB;AACF;;;;;;;AAQA,SAAS,oBAAoB,OAAgC;CAC3D,OAAO;EACL,QAAQ,MAAM;EACd,QAAQ,MAAM;EACd,UAAU,MAAM,YAAY;EAC5B,GAAI,MAAM,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EAC/C,YAAY,MAAM;EAIlB,aAAa,MAAM,WAAW,KAAK,IAAI,KAAK,MAAM;EAClD,SAAS,MAAM;CACjB;AACF;;;;;;;AC1vCA,SAAgB,+BAA+B,SAAoD;CACjG,OAAO,QAAQ,aAAa,kBACtB,QAAQ,iBAAiB,qBAAqB;AACtD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-C8TwH34B.d.ts","names":[],"sources":["../src/contexts/process.ts"],"mappings":";;;iBAsIgB,oBAAA,CAAqB,MAAA,GAAS,WAAA,GAAc,gBAAgB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"sandbox-DLY6uah7.d.ts","names":[],"sources":["../src/contexts/sandbox.ts"],"mappings":";;;UAeiB,eAAA;EACf,IAAA;EACA,KAAA,GAAQ,MAAA,EAAQ,WAAA,KAAgB,OAAA;IAAU,EAAA;IAAY,GAAA;EAAA;EACtD,IAAA,GAAO,SAAA,UAAmB,OAAA,UAAiB,OAAA;IAAY,GAAA;IAAc,GAAA,GAAM,MAAA;IAAwB,OAAA;EAAA,MAAuB,OAAA,CAAQ,UAAA;EAClI,QAAA,GAAW,SAAA,UAAmB,IAAA,aAAiB,OAAA;EAC/C,SAAA,GAAY,SAAA,UAAmB,IAAA,UAAc,OAAA,aAAoB,OAAA;EACjE,SAAA,GAAY,SAAA,UAAmB,IAAA,UAAc,OAAA,GAAU,gBAAA,KAAqB,OAAA;EAC5E,OAAA,GAAU,SAAA,aAAsB,OAAA;EANhC;;;;;;;EAcA,UAAA,IACE,SAAA,UACA,OAAA,UACA,OAAA;IAAY,GAAA;IAAc,GAAA,GAAM,MAAA;IAAwB,MAAA,GAAS,WAAA;EAAA,MAC9D,OAAA,CAAQ,YAAA;AAAA;AAAA,iBAOC,oBAAA,CAAqB,QAAA,EAAU,eAAA,GAAkB,gBAAgB"}