zidane 6.0.0 → 6.1.0

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 (165) hide show
  1. package/README.md +27 -0
  2. package/dist/{acp-Irby04Us.js → acp-cfR8T19u.js} +8 -8
  3. package/dist/{acp-Irby04Us.js.map → acp-cfR8T19u.js.map} +1 -1
  4. package/dist/acp-cli.js +7 -7
  5. package/dist/acp-cli.js.map +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-DkSmGkDM.js → agent-DZRA7wip.js} +377 -61
  9. package/dist/agent-DZRA7wip.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -1
  12. package/dist/{anthropic-CTHsdFG9.js → anthropic-DNJbRMUK.js} +7 -7
  13. package/dist/anthropic-DNJbRMUK.js.map +1 -0
  14. package/dist/atomic-write-B7zr1ddF.js.map +1 -1
  15. package/dist/{auth-BDSu_0t3.js → auth-B3cB5K6d.js} +7 -7
  16. package/dist/auth-B3cB5K6d.js.map +1 -0
  17. package/dist/cache-telemetry-BKAwsNxm.js.map +1 -1
  18. package/dist/chat/pure.d.ts +4 -4
  19. package/dist/chat/pure.js +3 -3
  20. package/dist/chat.d.ts +44 -21
  21. package/dist/chat.d.ts.map +1 -1
  22. package/dist/chat.js +7 -7
  23. package/dist/chat.js.map +1 -1
  24. package/dist/completion-core-CXYSVhZo.js.map +1 -1
  25. package/dist/context-breakdown-Dzo25N45.js.map +1 -1
  26. package/dist/contexts/daytona.d.ts +3 -1
  27. package/dist/contexts/daytona.d.ts.map +1 -1
  28. package/dist/contexts/daytona.js +2 -2
  29. package/dist/contexts/daytona.js.map +1 -1
  30. package/dist/contexts/docker.d.ts.map +1 -1
  31. package/dist/contexts/docker.js +2 -2
  32. package/dist/contexts/docker.js.map +1 -1
  33. package/dist/contexts/e2b.d.ts +3 -1
  34. package/dist/contexts/e2b.d.ts.map +1 -1
  35. package/dist/contexts/e2b.js +1 -1
  36. package/dist/contexts/e2b.js.map +1 -1
  37. package/dist/{contexts-Vone3qQQ.js → contexts-BIbcfBFH.js} +2 -2
  38. package/dist/contexts-BIbcfBFH.js.map +1 -0
  39. package/dist/contexts.js +1 -1
  40. package/dist/edit-utils-EGosADZq.js.map +1 -1
  41. package/dist/eval.d.ts +1 -1
  42. package/dist/eval.js +4 -3
  43. package/dist/eval.js.map +1 -1
  44. package/dist/exec-file-size-PXkr-MGA.js.map +1 -1
  45. package/dist/extensions.d.ts +1 -1
  46. package/dist/extensions.js +1 -1
  47. package/dist/{fetch-url-CWE8X5OD.js → fetch-url-_5WKWpbQ.js} +2 -2
  48. package/dist/{fetch-url-CWE8X5OD.js.map → fetch-url-_5WKWpbQ.js.map} +1 -1
  49. package/dist/{format-BNOXpl-1.js → format-BF745pLL.js} +9 -2
  50. package/dist/format-BF745pLL.js.map +1 -0
  51. package/dist/generate-data-WL_yGVL6.js.map +1 -1
  52. package/dist/hash-DJRzmcPI.js.map +1 -1
  53. package/dist/headless.d.ts +1 -1
  54. package/dist/headless.js +4 -4
  55. package/dist/headless.js.map +1 -1
  56. package/dist/{index-B8UtiZy7.d.ts → index-BGpM2YaU.d.ts} +378 -5
  57. package/dist/index-BGpM2YaU.d.ts.map +1 -0
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +11 -11
  60. package/dist/index.js.map +1 -1
  61. package/dist/logger-DBX9uYOw.js.map +1 -1
  62. package/dist/{login-DOF8pMVM.js → login-DZLG__2f.js} +4 -3
  63. package/dist/{login-DOF8pMVM.js.map → login-DZLG__2f.js.map} +1 -1
  64. package/dist/mcp-CcvuVXB9.js.map +1 -1
  65. package/dist/mcp.d.ts +1 -1
  66. package/dist/media-sniff-Bn76JxAu.js.map +1 -1
  67. package/dist/{messages-CwujiS74.js → messages-C5BDOjUG.js} +2 -2
  68. package/dist/{messages-CwujiS74.js.map → messages-C5BDOjUG.js.map} +1 -1
  69. package/dist/oauth-8LqbeI_Q.js.map +1 -1
  70. package/dist/{openai-compat-BvgYGSR1.js → openai-compat-B0kqvZqN.js} +2 -2
  71. package/dist/openai-compat-B0kqvZqN.js.map +1 -0
  72. package/dist/output/stream-json.d.ts +1 -1
  73. package/dist/output/terminal.d.ts +1 -1
  74. package/dist/output/terminal.js +1 -1
  75. package/dist/output/terminal.js.map +1 -1
  76. package/dist/output-cap-kBUHBlnq.js.map +1 -1
  77. package/dist/presets.d.ts +1 -1
  78. package/dist/presets.js +1 -1
  79. package/dist/providers/anthropic.d.ts +1 -1
  80. package/dist/providers/anthropic.js +1 -1
  81. package/dist/providers/arcee.d.ts +1 -1
  82. package/dist/providers/arcee.js +1 -1
  83. package/dist/providers/baseten.d.ts +1 -1
  84. package/dist/providers/baseten.js +2 -2
  85. package/dist/providers/baseten.js.map +1 -1
  86. package/dist/providers/cerebras.d.ts +1 -1
  87. package/dist/providers/cerebras.js +1 -1
  88. package/dist/providers/local.d.ts +1 -1
  89. package/dist/providers/local.js +1 -1
  90. package/dist/providers/openai-compat.d.ts +1 -1
  91. package/dist/providers/openai-compat.js +1 -1
  92. package/dist/providers/openai.d.ts +1 -1
  93. package/dist/providers/openai.js +8 -8
  94. package/dist/providers/openai.js.map +1 -1
  95. package/dist/providers/openrouter.d.ts +1 -1
  96. package/dist/providers/openrouter.js +1 -1
  97. package/dist/providers/xai.d.ts +1 -1
  98. package/dist/providers/xai.js +1 -1
  99. package/dist/{providers-z6LDMAr3.js → providers-BMlrTr6a.js} +3 -3
  100. package/dist/{providers-z6LDMAr3.js.map → providers-BMlrTr6a.js.map} +1 -1
  101. package/dist/providers.d.ts +1 -1
  102. package/dist/providers.js +4 -4
  103. package/dist/read-state-Dq_TXUX1.js.map +1 -1
  104. package/dist/{resolve-Cr8JSL_O.js → resolve-CC8Lmwcj.js} +3 -3
  105. package/dist/{resolve-Cr8JSL_O.js.map → resolve-CC8Lmwcj.js.map} +1 -1
  106. package/dist/restate.d.ts +1 -1
  107. package/dist/restate.d.ts.map +1 -1
  108. package/dist/restate.js.map +1 -1
  109. package/dist/session/sqlite.d.ts +1 -1
  110. package/dist/session/sqlite.d.ts.map +1 -1
  111. package/dist/session/sqlite.js +75 -20
  112. package/dist/session/sqlite.js.map +1 -1
  113. package/dist/{session-CsdDIPY8.js → session-CsKzK8-P.js} +3 -3
  114. package/dist/session-CsKzK8-P.js.map +1 -0
  115. package/dist/session.d.ts +2 -2
  116. package/dist/session.js +2 -2
  117. package/dist/{skills-Bsb0rvWI.js → skills-BjeXSRlQ.js} +2 -2
  118. package/dist/{skills-Bsb0rvWI.js.map → skills-BjeXSRlQ.js.map} +1 -1
  119. package/dist/skills.d.ts +1 -1
  120. package/dist/skills.js +2 -2
  121. package/dist/stdio-loader-Ce68wUmM.js.map +1 -1
  122. package/dist/{tool-formatters-DY0TDb1r.d.ts → tool-formatters-CMwjDvkp.d.ts} +2 -2
  123. package/dist/tool-formatters-CMwjDvkp.d.ts.map +1 -0
  124. package/dist/tools/fetch-url.d.ts +1 -1
  125. package/dist/tools/fetch-url.d.ts.map +1 -1
  126. package/dist/tools/fetch-url.js +1 -1
  127. package/dist/tools/web-search.d.ts +1 -1
  128. package/dist/tools/web-search.js +1 -1
  129. package/dist/tools/web-search.js.map +1 -1
  130. package/dist/tools.d.ts +2 -2
  131. package/dist/tools.js +2 -2
  132. package/dist/{transcript-anchors-DSRPWjiH.js → transcript-anchors-BTNe1OJi.js} +346 -89
  133. package/dist/transcript-anchors-BTNe1OJi.js.map +1 -0
  134. package/dist/{transcript-anchors-Cb-bDbNH.d.ts → transcript-anchors-D5wvdKQQ.d.ts} +37 -3
  135. package/dist/transcript-anchors-D5wvdKQQ.d.ts.map +1 -0
  136. package/dist/tui.d.ts +21 -6
  137. package/dist/tui.d.ts.map +1 -1
  138. package/dist/tui.js +497 -83
  139. package/dist/tui.js.map +1 -1
  140. package/dist/{turn-operations-Dq9Kx7m2.js → turn-operations-CEt_l3fy.js} +23 -8
  141. package/dist/turn-operations-CEt_l3fy.js.map +1 -0
  142. package/dist/{turn-operations-CgMROLsr.d.ts → turn-operations-JO5MYM9_.d.ts} +27 -9
  143. package/dist/turn-operations-JO5MYM9_.d.ts.map +1 -0
  144. package/dist/types-BIarq1qE.js.map +1 -1
  145. package/dist/types.d.ts +1 -1
  146. package/dist/{xai-B_e6TOA8.js → xai-C_aq671K.js} +3 -3
  147. package/dist/{xai-B_e6TOA8.js.map → xai-C_aq671K.js.map} +1 -1
  148. package/docs/ARCHITECTURE.md +4 -0
  149. package/docs/CHAT.md +12 -10
  150. package/docs/SKILL.md +1 -1
  151. package/docs/TUI.md +3 -3
  152. package/package.json +2 -2
  153. package/dist/agent-DkSmGkDM.js.map +0 -1
  154. package/dist/anthropic-CTHsdFG9.js.map +0 -1
  155. package/dist/auth-BDSu_0t3.js.map +0 -1
  156. package/dist/contexts-Vone3qQQ.js.map +0 -1
  157. package/dist/format-BNOXpl-1.js.map +0 -1
  158. package/dist/index-B8UtiZy7.d.ts.map +0 -1
  159. package/dist/openai-compat-BvgYGSR1.js.map +0 -1
  160. package/dist/session-CsdDIPY8.js.map +0 -1
  161. package/dist/tool-formatters-DY0TDb1r.d.ts.map +0 -1
  162. package/dist/transcript-anchors-Cb-bDbNH.d.ts.map +0 -1
  163. package/dist/transcript-anchors-DSRPWjiH.js.map +0 -1
  164. package/dist/turn-operations-CgMROLsr.d.ts.map +0 -1
  165. package/dist/turn-operations-Dq9Kx7m2.js.map +0 -1
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as AgentProviderError, c as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, f as matchesContextExceeded, i as AgentContextExceededError, l as ClassifiedError, n as AgentAbortedError, o as AgentToolNotAllowedError, r as AgentBudgetExceededError, u as ClassifiedErrorKind } from "./timeout-8vSIRjzl.js";
2
- import { $d as ToolResultImageRefContent, Ad as PromptVideoPart, Af as SessionStore, Bd as SessionMessage, Cd as ProgressNudgeKind, Cf as SkillResource, Dd as PromptImagePart, Df as Session, Dp as OpenRouterParams, Ed as PromptDocumentPart, Ef as CreateSessionOptions, Fd as RetryConfig, Fi as AgentOptions, Hd as SpawnHookContext, Id as RunHookMap, Jd as ToolResultAudioContent, Kd as ToolHookContext, Ld as SessionContentBlock, Md as RepeatGuardToolMatcher, Mi as Agent, Nd as ResolveContentRef, Ni as AgentHookMap, Od as PromptPart, Of as SessionData, Pd as ResolveContentRefBlock, Pi as AgentHooks, Qd as ToolResultImageContent, Qn as ValidationResult, Rd as SessionEndStatus, Sd as OAuthRefreshHookContext, Td as PromptAudioPart, Tf as SkillsConfig, Ud as StreamHookContext, Vd as SessionTurn, Wd as ThinkingLevel, Xd as ToolResultDocumentContent, Yd as ToolResultContent, Zd as ToolResultDocumentRefContent, Zp as OpenAIParams, _ as MutationValidationFailure, _p as ToolResult, ac as McpConnection, ad as AgentClock, af as toolOutputBudgetByteLength, b as MutationValidationResult, bd as McpToolHookContext, cf as ToolContext, dp as Provider, ef as ToolResultMetadata, ff as ReadStateEntry, fp as ProviderCapabilities, g as MutationValidationBatch, gp as ToolCall, h as statsByModel, hd as DEFAULT_AGENT_CLOCK, id as AgentBehavior, if as TurnUsage, jd as RepeatGuardConfig, kd as PromptTextPart, kf as SessionRun, lf as ToolDef, ls as SpawnToolOptions, m as flattenTurns, mm as AnthropicParams, mp as StreamOptions, nf as ToolResultVideoContent, od as AgentRunOptions, of as toolOutputByteLength, om as CerebrasParams, os as ChildAgent, p as ModelUsage, pf as ReadStateMap, pp as StreamCallbacks, qd as ToolOutcome, rf as TurnFinishReason, sd as AgentStats, sf as toolResultToText, t as Preset, tf as ToolResultTextContent, ud as ChildRunStats, uf as ToolMap, us as SpawnToolState, v as MutationValidationHooksOptions, vp as ToolSpec, vr as InteractionToolOptions, wd as ProgressNudgeMessage, x as SuccessfulMutationOutcome, xf as SkillConfig, y as MutationValidationMutation, yd as McpServerConfig, yp as TurnResult, zd as SessionHookContext, zf as RemoteStoreOptions } from "./index-B8UtiZy7.js";
2
+ import { $d as RunHookMap, Ad as DEFAULT_AGENT_CLOCK, Af as ReadStateMap, Ap as ProviderCapabilities, Bd as ProgressNudgeKind, Bf as SkillResource, Cf as toolOutputByteLength, Df as ToolMap, Ef as ToolDef, Fd as McpToolHookContext, Fi as AgentHookMap, Fp as ToolResult, Gd as PromptPart, Gf as SessionData, Gp as OpenRouterParams, Hd as PromptAudioPart, Hf as SkillsConfig, Ii as AgentHooks, Ip as ToolSpec, Jd as RepeatGuardConfig, Jf as SessionStore, Kd as PromptTextPart, Li as AgentOptions, Lp as TurnResult, Mm as AnthropicParams, Mp as StreamOptions, Pd as McpServerConfig, Pi as Agent, Pp as ToolCall, Qd as RetryConfig, Rf as SkillConfig, Sd as AgentStats, Sf as toolOutputBudgetByteLength, Td as ChildRunStats, Tf as ToolContext, Ud as PromptDocumentPart, Uf as CreateSessionOptions, Vd as ProgressNudgeMessage, Wd as PromptImagePart, Wf as Session, Xd as ResolveContentRef, Yd as RepeatGuardToolMatcher, Zd as ResolveContentRefBlock, _ as MutationValidationFailure, _f as ToolResultMetadata, af as SpawnHookContext, b as MutationValidationResult, bd as AgentClock, bf as TurnFinishReason, br as InteractionToolOptions, cs as ChildAgent, dc as McpConnection, df as ToolResultAudioContent, ds as SpawnToolOptions, ef as SessionContentBlock, er as ValidationResult, ff as ToolResultContent, fs as SpawnToolState, g as MutationValidationBatch, gf as ToolResultImageRefContent, h as statsByModel, hf as ToolResultImageContent, hm as OpenAIParams, if as SessionTurn, jp as StreamCallbacks, kf as ReadStateEntry, kp as Provider, lf as ToolHookContext, m as flattenTurns, mf as ToolResultDocumentRefContent, nf as SessionHookContext, of as StreamHookContext, p as ModelUsage, pf as ToolResultDocumentContent, qd as PromptVideoPart, qf as SessionRun, rf as SessionMessage, rp as RemoteStoreOptions, sf as ThinkingLevel, t as Preset, tf as SessionEndStatus, uf as ToolOutcome, v as MutationValidationHooksOptions, vf as ToolResultTextContent, wf as toolResultToText, wm as CerebrasParams, x as SuccessfulMutationOutcome, xd as AgentRunOptions, xf as TurnUsage, y as MutationValidationMutation, yd as AgentBehavior, yf as ToolResultVideoContent, zd as OAuthRefreshHookContext } from "./index-BGpM2YaU.js";
3
3
  import { _ as TaskStallInfo, a as ContextType, c as ExecutionContext, g as TaskHandle, h as TaskExitInfo, l as ExecutionHandle, m as TaskEntry, n as ContextCapabilities, o as DetachedTasksCapability, p as SpawnConfig, r as ContextHardening, s as ExecResult, t as BackgroundTaskStatus } from "./types-BDccavwj.js";
4
4
  import { t as SandboxProvider } from "./sandbox-CgjG2VvQ.js";
5
5
  export { type Agent, AgentAbortedError, type AgentBehavior, AgentBudgetExceededError, type AgentClock, AgentContextExceededError, type AgentHookMap, type AgentHooks, type AgentOptions, AgentProviderError, type AgentRunOptions, type AgentStats, AgentToolNotAllowedError, type AnthropicParams, type BackgroundTaskStatus, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, type CerebrasParams, type ChildAgent, type ChildRunStats, type ClassifiedError, type ClassifiedErrorKind, type ContextCapabilities, type ContextHardening, type ContextType, type CreateSessionOptions, DEFAULT_AGENT_CLOCK, type DetachedTasksCapability, type ExecResult, type ExecutionContext, type ExecutionHandle, type InteractionToolOptions, type McpConnection, type McpServerConfig, type McpToolHookContext, type ModelUsage, type MutationValidationBatch, type MutationValidationFailure, type MutationValidationHooksOptions, type MutationValidationMutation, type MutationValidationResult, type OAuthRefreshHookContext, type OpenAIParams, type OpenRouterParams, type Preset, type ProgressNudgeKind, type ProgressNudgeMessage, type PromptAudioPart, type PromptDocumentPart, type PromptImagePart, type PromptPart, type PromptTextPart, type PromptVideoPart, type Provider, type ProviderCapabilities, type ReadStateEntry, type ReadStateMap, type RemoteStoreOptions, type RepeatGuardConfig, type RepeatGuardToolMatcher, type ResolveContentRef, type ResolveContentRefBlock, type RetryConfig, type RunHookMap, type SandboxProvider, type Session, type SessionContentBlock, type SessionData, type SessionEndStatus, type SessionHookContext, type SessionMessage, type SessionRun, type SessionStore, type SessionTurn, type SkillConfig, type SkillResource, type SkillsConfig, type SpawnConfig, type SpawnHookContext, type SpawnToolOptions, type SpawnToolState, type StreamCallbacks, type StreamHookContext, type StreamOptions, type SuccessfulMutationOutcome, type TaskEntry, type TaskExitInfo, type TaskHandle, type TaskStallInfo, type ThinkingLevel, type ToolCall, type ToolContext, type ToolDef, type ToolHookContext, type ToolMap, type ToolOutcome, type ToolResult, type ToolResultAudioContent, type ToolResultContent, type ToolResultDocumentContent, type ToolResultDocumentRefContent, type ToolResultImageContent, type ToolResultImageRefContent, type ToolResultMetadata, type ToolResultTextContent, type ToolResultVideoContent, type ToolSpec, type TurnFinishReason, type TurnResult, type TurnUsage, type ValidationResult, flattenTurns, matchesContextExceeded, statsByModel, toolOutputBudgetByteLength, toolOutputByteLength, toolResultToText };
@@ -1,4 +1,4 @@
1
- import { m as openaiCompat } from "./openai-compat-BvgYGSR1.js";
1
+ import { m as openaiCompat } from "./openai-compat-B0kqvZqN.js";
2
2
  import { i as resolveOAuthApiKey, n as isOAuthAccessToken, t as extractRuntimeCredentials } from "./oauth-8LqbeI_Q.js";
3
3
  import { createServer } from "node:http";
4
4
  //#region src/chat/oauth-page/pkce.ts
@@ -17,7 +17,7 @@ function base64UrlEncode(bytes) {
17
17
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
18
18
  }
19
19
  async function generatePkce() {
20
- const verifierBytes = new Uint8Array(32);
20
+ const verifierBytes = /* @__PURE__ */ new Uint8Array(32);
21
21
  crypto.getRandomValues(verifierBytes);
22
22
  const verifier = base64UrlEncode(verifierBytes);
23
23
  const data = new TextEncoder().encode(verifier);
@@ -376,4 +376,4 @@ function xai(params) {
376
376
  //#endregion
377
377
  export { createXaiOAuthProvider as n, generatePkce as r, xai as t };
378
378
 
379
- //# sourceMappingURL=xai-B_e6TOA8.js.map
379
+ //# sourceMappingURL=xai-C_aq671K.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"xai-B_e6TOA8.js","names":[],"sources":["../src/chat/oauth-page/pkce.ts","../src/chat/oauth-page/xai.ts","../src/providers/xai.ts"],"sourcesContent":["/**\n * PKCE helper. Inlined here (rather than imported from pi-ai) because\n * `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want\n * to reach into `node_modules/.../utils/oauth/pkce.js` — that's an\n * implementation-detail path that breaks on minor upstream rearrangements.\n *\n * Web Crypto API only. Works under Bun + Node 22+ without any node:crypto\n * import (matches pi-ai's own behavior). Output is base64url per RFC 7636.\n */\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes)\n binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')\n}\n\nexport interface PkcePair {\n /** Random verifier used in the token-exchange step. */\n verifier: string\n /** SHA-256(verifier) sent on the authorize URL. */\n challenge: string\n}\n\nexport async function generatePkce(): Promise<PkcePair> {\n const verifierBytes = new Uint8Array(32)\n crypto.getRandomValues(verifierBytes)\n const verifier = base64UrlEncode(verifierBytes)\n\n const data = new TextEncoder().encode(verifier)\n const hashBuffer = await crypto.subtle.digest('SHA-256', data)\n const challenge = base64UrlEncode(new Uint8Array(hashBuffer))\n\n return { verifier, challenge }\n}\n","/**\n * xAI Grok OAuth flow (SuperGrok / X Premium+).\n *\n * xAI is **not** built into pi-ai, so — like Cursor — we implement the flow\n * here and register it with pi-ai's OAuth registry (`registerXaiOAuthProvider`)\n * so `getOAuthApiKey('xai-oauth', …)` resolves to it during lazy token refresh.\n *\n * Auth shape: OAuth 2.0 + PKCE with a loopback callback server, against xAI's\n * OIDC issuer (`https://auth.x.ai`). Differs from the Anthropic / Codex flows\n * (which use the shared `startCallbackServer`) on three points, so it gets its\n * own login impl rather than reusing that primitive:\n *\n * 1. The `redirect_uri` must be `http://127.0.0.1:56121/callback` verbatim\n * (xAI matches the registered loopback IP, not `localhost`).\n * 2. Endpoints come from OIDC discovery, not hard-coded constants.\n * 3. Token exchange + refresh are `application/x-www-form-urlencoded`\n * (the Anthropic flow posts JSON).\n *\n * The callback page still routes through {@link OAuthCallbackPageRenderer} so\n * the post-redirect HTML matches the rest of zidane's themed flows.\n *\n * Flow + constants adapted from the public reference implementations\n * (`stnly/pi-grok`, `BlockedPath/pi-xai-oauth`) and the Hermes Agent docs.\n * xAI's OAuth surface is unofficial/undocumented — re-verify the client id,\n * scopes, and callback port if login starts failing.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n OAuthProviderInterface,\n} from '@earendil-works/pi-ai/oauth'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { createServer } from 'node:http'\nimport { generatePkce } from './pkce'\n\n/** pi-ai / zidane OAuth provider id. Also the credentials-file key. */\nexport const XAI_OAUTH_PROVIDER_ID = 'xai-oauth'\n\nconst DEFAULT_BASE_URL = 'https://api.x.ai/v1'\nconst ISSUER = 'https://auth.x.ai'\nconst DISCOVERY_URL = `${ISSUER}/.well-known/openid-configuration`\n/** Public client id used by the Grok CLI OAuth surface. */\nconst DEFAULT_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828'\nconst DEFAULT_SCOPE = 'openid profile email offline_access grok-cli:access api:access'\n/** xAI matches the registered loopback IP exactly — `localhost` is rejected. */\nconst DEFAULT_CALLBACK_HOST = '127.0.0.1'\nconst DEFAULT_CALLBACK_PORT = 56121\nconst CALLBACK_PATH = '/callback'\nconst PROVIDER_NAME = 'xAI Grok'\n/** Refresh slightly early so requests don't race expiry. */\nconst REFRESH_SKEW_MS = 120_000\nconst CALLBACK_TIMEOUT_MS = 180_000\n\nfunction xaiOAuthClientId(): string {\n return process.env.PI_XAI_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID\n}\n\nfunction xaiOAuthScope(): string {\n return process.env.PI_XAI_OAUTH_SCOPE || DEFAULT_SCOPE\n}\n\nfunction xaiOAuthCallbackHost(): string {\n return process.env.PI_XAI_OAUTH_CALLBACK_HOST || DEFAULT_CALLBACK_HOST\n}\n\nfunction xaiOAuthCallbackPort(): number {\n const parsed = Number.parseInt(process.env.PI_XAI_OAUTH_CALLBACK_PORT || String(DEFAULT_CALLBACK_PORT), 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CALLBACK_PORT\n}\n\n/** Resolve the xAI API base URL (env override → default). */\nexport function xaiBaseUrl(): string {\n return (process.env.PI_XAI_BASE_URL || process.env.XAI_BASE_URL || DEFAULT_BASE_URL)\n .replace(/\\/+$/, '')\n}\n\ninterface XaiDiscovery {\n authorization_endpoint: string\n token_endpoint: string\n}\n\n/**\n * xAI OAuth credentials. Extends the base `{ access, refresh, expires }` with\n * the resolved `token_endpoint` so refresh doesn't need a second discovery\n * round-trip, plus the cached discovery doc for validation.\n */\nexport interface XaiOAuthCredentials extends OAuthCredentials {\n tokenEndpoint?: string\n discovery?: XaiDiscovery\n}\n\nfunction base64Url(bytes: Uint8Array): string {\n let binary = ''\n for (const b of bytes)\n binary += String.fromCharCode(b)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction randomToken(byteLength = 16): string {\n return base64Url(crypto.getRandomValues(new Uint8Array(byteLength)))\n}\n\n/**\n * Refuse any OIDC endpoint that isn't HTTPS on an xAI origin.\n *\n * The discovery doc is cached long-term in `credentials.json`. A single MITM\n * during initial login could otherwise pin a malicious `token_endpoint` that\n * receives every subsequent refresh token. Validating scheme + host keeps the\n * endpoint on `x.ai` / `*.x.ai`.\n */\nfunction validateEndpoint(value: string, field: string): string {\n let url: URL\n try {\n url = new URL(value)\n }\n catch {\n throw new Error(`xAI OAuth discovery returned an invalid ${field}: ${value}`)\n }\n if (url.protocol !== 'https:')\n throw new Error(`xAI OAuth ${field} must use HTTPS: ${value}`)\n const host = url.hostname.toLowerCase()\n if (host !== 'x.ai' && !host.endsWith('.x.ai'))\n throw new Error(`Refusing non-xAI OAuth ${field}: ${value}`)\n return url.toString()\n}\n\nasync function discover(): Promise<XaiDiscovery> {\n let response: Response\n try {\n response = await fetch(DISCOVERY_URL, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n })\n }\n catch (cause) {\n throw new Error(`xAI OIDC discovery failed: ${cause instanceof Error ? cause.message : String(cause)}`)\n }\n if (!response.ok)\n throw new Error(`xAI OIDC discovery returned ${response.status}`)\n\n const payload = await response.json() as Record<string, unknown>\n return {\n authorization_endpoint: validateEndpoint(String(payload.authorization_endpoint ?? ''), 'authorization_endpoint'),\n token_endpoint: validateEndpoint(String(payload.token_endpoint ?? ''), 'token_endpoint'),\n }\n}\n\ninterface CallbackResult {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\ninterface XaiCallbackServer {\n redirectUri: string\n waitForCallback: (timeoutMs: number) => Promise<CallbackResult>\n close: () => void\n}\n\n/**\n * Start the loopback callback server on `127.0.0.1:56121`. Falls back to an\n * OS-assigned port if 56121 is taken — xAI accepts any loopback port as long\n * as the `redirect_uri` we send on the authorize URL matches the listener.\n */\nasync function startXaiCallbackServer(renderPage: OAuthCallbackPageRenderer): Promise<XaiCallbackServer> {\n const callbackHost = xaiOAuthCallbackHost()\n const callbackPort = xaiOAuthCallbackPort()\n let settle: ((value: CallbackResult) => void) | undefined\n let settled = false\n const callbackPromise = new Promise<CallbackResult>((resolve) => {\n settle = (value) => {\n if (settled)\n return\n settled = true\n resolve(value)\n }\n })\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n try {\n // accounts.x.ai may issue a CORS preflight against the loopback listener.\n const origin = req.headers.origin\n if (origin === 'https://accounts.x.ai' || origin === 'https://auth.x.ai') {\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type')\n res.setHeader('Access-Control-Allow-Private-Network', 'true')\n res.setHeader('Vary', 'Origin')\n }\n if (req.method === 'OPTIONS') {\n res.statusCode = 204\n res.end()\n return\n }\n\n const url = new URL(req.url ?? '/', `http://${callbackHost}`)\n if (url.pathname !== CALLBACK_PATH) {\n res.statusCode = 404\n res.end('Not found')\n return\n }\n\n const result: CallbackResult = {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n error: url.searchParams.get('error') ?? undefined,\n errorDescription: url.searchParams.get('error_description') ?? undefined,\n }\n\n res.statusCode = result.error ? 400 : 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(renderPage(result.error\n ? {\n kind: 'error',\n provider: PROVIDER_NAME,\n message: `${PROVIDER_NAME} authentication did not complete.`,\n details: result.errorDescription ?? result.error,\n }\n : {\n kind: 'success',\n provider: PROVIDER_NAME,\n message: 'xAI authentication completed. You can close this window.',\n }))\n settle?.(result)\n }\n catch {\n res.statusCode = 500\n res.end('Internal error')\n }\n })\n\n const listen = (port: number) => new Promise<number>((resolve, reject) => {\n server.once('error', reject)\n server.listen(port, callbackHost, () => {\n server.removeListener('error', reject)\n const addr = server.address()\n resolve(typeof addr === 'object' && addr ? addr.port : port)\n })\n })\n\n let actualPort: number\n try {\n actualPort = await listen(callbackPort)\n }\n catch {\n actualPort = await listen(0)\n }\n\n return {\n redirectUri: `http://${callbackHost}:${actualPort}${CALLBACK_PATH}`,\n waitForCallback: timeoutMs => Promise.race([\n callbackPromise,\n new Promise<CallbackResult>(resolve =>\n setTimeout(\n resolve,\n timeoutMs,\n { error: 'timeout', errorDescription: 'Timed out waiting for the xAI OAuth callback.' },\n )),\n ]),\n close: () => {\n try { server.close() }\n catch { /* already closed */ }\n },\n }\n}\n\nfunction expiryFromPayload(payload: Record<string, unknown>): number {\n const expiresIn = typeof payload.expires_in === 'number'\n ? payload.expires_in\n : Number(payload.expires_in ?? 3600)\n return Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS\n}\n\nasync function exchangeCode(\n tokenEndpoint: string,\n code: string,\n redirectUri: string,\n verifier: string,\n discovery: XaiDiscovery,\n): Promise<XaiOAuthCredentials> {\n const clientId = xaiOAuthClientId()\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: clientId,\n code,\n redirect_uri: redirectUri,\n code_verifier: verifier,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token exchange failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n const refresh = String(payload.refresh_token ?? '')\n if (!access)\n throw new Error('xAI token exchange did not return an access_token.')\n if (!refresh)\n throw new Error('xAI token exchange did not return a refresh_token.')\n\n return { access, refresh, expires: expiryFromPayload(payload), tokenEndpoint, discovery }\n}\n\nexport async function loginXai(\n renderPage: OAuthCallbackPageRenderer,\n callbacks: OAuthLoginCallbacks,\n): Promise<XaiOAuthCredentials> {\n const discovery = await discover()\n const { verifier, challenge } = await generatePkce()\n const state = randomToken()\n const nonce = randomToken()\n const server = await startXaiCallbackServer(renderPage)\n\n try {\n const authUrl = new URL(discovery.authorization_endpoint)\n authUrl.searchParams.set('response_type', 'code')\n authUrl.searchParams.set('client_id', xaiOAuthClientId())\n authUrl.searchParams.set('redirect_uri', server.redirectUri)\n authUrl.searchParams.set('scope', xaiOAuthScope())\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('nonce', nonce)\n // Opt into xAI's generic OAuth plan tier (SuperGrok / X Premium+).\n authUrl.searchParams.set('plan', 'generic')\n authUrl.searchParams.set('referrer', 'zidane')\n\n callbacks.onAuth({\n url: authUrl.toString(),\n instructions: `Sign in to xAI and approve access. If the browser is on another machine, the callback listener is ${server.redirectUri}.`,\n })\n\n const result = await server.waitForCallback(CALLBACK_TIMEOUT_MS)\n if (result.error)\n throw new Error(result.errorDescription ?? result.error)\n if (result.state !== state)\n throw new Error('xAI OAuth state mismatch — possible CSRF. Please retry.')\n if (!result.code)\n throw new Error('xAI OAuth callback did not include an authorization code.')\n\n callbacks.onProgress?.('Exchanging authorization code for tokens...')\n return await exchangeCode(discovery.token_endpoint, result.code, server.redirectUri, verifier, discovery)\n }\n finally {\n server.close()\n }\n}\n\nexport async function refreshXaiToken(credentials: OAuthCredentials): Promise<XaiOAuthCredentials> {\n const xai = credentials as XaiOAuthCredentials\n const clientId = xaiOAuthClientId()\n if (!credentials.refresh)\n throw new Error('xAI credentials are missing a refresh token — re-login required.')\n\n const tokenEndpoint = xai.tokenEndpoint\n ?? xai.discovery?.token_endpoint\n ?? (await discover()).token_endpoint\n validateEndpoint(tokenEndpoint, 'token_endpoint')\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: credentials.refresh,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token refresh failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n if (!access)\n throw new Error('xAI token refresh did not return an access_token.')\n\n return {\n ...xai,\n access,\n // xAI may omit a rotated refresh token — keep the existing one.\n refresh: String(payload.refresh_token ?? credentials.refresh),\n expires: expiryFromPayload(payload),\n tokenEndpoint,\n }\n}\n\n/**\n * Build an xAI `OAuthProviderInterface`. Shape matches Anthropic / Codex /\n * Cursor so it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.\n *\n * `usesCallbackServer: true` — the loopback flow has a real callback server,\n * so the TUI doesn't need to prompt for a manual code paste.\n */\nexport function createXaiOAuthProvider(renderPage: OAuthCallbackPageRenderer): OAuthProviderInterface {\n return {\n id: XAI_OAUTH_PROVIDER_ID,\n name: 'xAI Grok (SuperGrok / X Premium+)',\n usesCallbackServer: true,\n login: callbacks => loginXai(renderPage, callbacks),\n refreshToken: refreshXaiToken,\n getApiKey: credentials => credentials.access,\n }\n}\n","import type { Provider, ProviderCapabilities, StreamCallbacks, StreamOptions, TurnResult } from '.'\nimport type { OAuthParams } from './oauth'\nimport { XAI_OAUTH_PROVIDER_ID, xaiBaseUrl } from '../chat/oauth-page/xai'\nimport { extractRuntimeCredentials, isOAuthAccessToken, resolveOAuthApiKey } from './oauth'\nimport { openaiCompat } from './openai-compat'\n\nconst DEFAULT_MODEL = 'grok-4.3'\n\n/**\n * xAI Grok provider — supports both a static API key (`XAI_API_KEY`) and the\n * SuperGrok / X Premium+ OAuth subscription.\n *\n * Both auth modes hit the same OpenAI-compatible endpoint (`https://api.x.ai/v1`),\n * so the wire handling is entirely {@link openaiCompat}. The only difference is\n * the bearer:\n *\n * - **API key**: `XAI_API_KEY` (or `params.apiKey`) → used verbatim.\n * - **OAuth**: no static key → {@link resolveOAuthApiKey} resolves + lazily\n * refreshes the stored `xai-oauth` token on every turn.\n *\n * `resolveOAuthApiKey` unifies the two: a real API key in `XAI_API_KEY` is\n * returned as-is, while an OAuth access token (or no env value at all) falls\n * through to the refreshable stored credentials. So this single factory serves\n * both paths without branching at construction time.\n */\nexport interface XaiParams extends OAuthParams {\n defaultModel?: string\n /**\n * Provider capability flags. Grok models are vision-capable; default\n * `{ vision: true, imageInToolResult: false }`. Override for text-only\n * deployments.\n */\n capabilities?: ProviderCapabilities\n /** Extra HTTP headers sent on every request. */\n extraHeaders?: Record<string, string>\n}\n\nexport function xai(params?: XaiParams): Provider {\n const defaultModel = params?.defaultModel || DEFAULT_MODEL\n const baseURL = xaiBaseUrl()\n const capabilities: ProviderCapabilities = params?.capabilities ?? {\n vision: true,\n imageInToolResult: false,\n }\n const baseCredentials = extractRuntimeCredentials(params)\n let runtimeCredentials = baseCredentials\n\n // Factory-time snapshot for the static `meta.isOAuth` display flag only (the\n // terminal header's `oauth`/`key` badge). The real bearer resolves per turn\n // in `stream`, so this is best-effort, mirroring anthropic's pattern. It's\n // OAuth unless a genuine static API key is present: a `xai-…` key → key mode;\n // runtime OAuth creds, a JWT access token, or no static key (stored creds\n // used) → oauth mode.\n const staticKey = params?.apiKey ?? process.env.XAI_API_KEY\n const isOAuth = baseCredentials !== undefined || !staticKey || isOAuthAccessToken(staticKey)\n\n /**\n * Build the openai-compat delegate for a resolved bearer. Cheap — the factory\n * only wires closures — so we re-create it per turn with the freshly resolved\n * (possibly refreshed) OAuth token rather than baking a stale key in once.\n */\n function delegateFor(apiKey: string): Provider {\n return openaiCompat({\n name: 'xai',\n apiKey,\n baseURL,\n defaultModel,\n capabilities,\n extraHeaders: params?.extraHeaders,\n })\n }\n\n // A delegate built with a placeholder key supplies the non-stream methods\n // (formatTools, userMessage, message builders, classifyError). Only `stream`\n // resolves the real bearer, so the placeholder never reaches the wire.\n const skeleton = delegateFor('placeholder-resolved-at-stream')\n\n return {\n ...skeleton,\n name: 'xai',\n // Reuse the delegate's normalized capability flags (openaiCompat fills the\n // audio/video/documents defaults) so the advertised shape matches the wire.\n meta: { ...skeleton.meta, defaultModel, isOAuth },\n\n async stream(options: StreamOptions, callbacks: StreamCallbacks): Promise<TurnResult> {\n const apiKey = await resolveOAuthApiKey(\n {\n provider: 'xai',\n providerId: XAI_OAUTH_PROVIDER_ID,\n params: runtimeCredentials ? { ...params, ...runtimeCredentials } : params,\n envKey: 'XAI_API_KEY',\n missingError: 'No xAI credentials found. Set XAI_API_KEY or run `bun run auth --xai` first.',\n refreshError: reason => `xAI OAuth token refresh failed. Run \\`bun run auth --xai\\` again. ${reason}`,\n },\n {\n ...callbacks,\n async onOAuthRefresh(ctx) {\n // Keep param-sourced credentials current so a long-lived provider\n // instance reuses the rotated token instead of re-refreshing.\n if (ctx.source === 'params') {\n runtimeCredentials = {\n access: ctx.credentials.access,\n refresh: ctx.credentials.refresh,\n expires: ctx.credentials.expires,\n }\n }\n await callbacks.onOAuthRefresh?.(ctx)\n },\n },\n )\n\n return delegateFor(apiKey).stream(options, callbacks)\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"xai-C_aq671K.js","names":[],"sources":["../src/chat/oauth-page/pkce.ts","../src/chat/oauth-page/xai.ts","../src/providers/xai.ts"],"sourcesContent":["/**\n * PKCE helper. Inlined here (rather than imported from pi-ai) because\n * `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want\n * to reach into `node_modules/.../utils/oauth/pkce.js` — that's an\n * implementation-detail path that breaks on minor upstream rearrangements.\n *\n * Web Crypto API only. Works under Bun + Node 22+ without any node:crypto\n * import (matches pi-ai's own behavior). Output is base64url per RFC 7636.\n */\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = ''\n for (const byte of bytes)\n binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '')\n}\n\nexport interface PkcePair {\n /** Random verifier used in the token-exchange step. */\n verifier: string\n /** SHA-256(verifier) sent on the authorize URL. */\n challenge: string\n}\n\nexport async function generatePkce(): Promise<PkcePair> {\n const verifierBytes = new Uint8Array(32)\n crypto.getRandomValues(verifierBytes)\n const verifier = base64UrlEncode(verifierBytes)\n\n const data = new TextEncoder().encode(verifier)\n const hashBuffer = await crypto.subtle.digest('SHA-256', data)\n const challenge = base64UrlEncode(new Uint8Array(hashBuffer))\n\n return { verifier, challenge }\n}\n","/**\n * xAI Grok OAuth flow (SuperGrok / X Premium+).\n *\n * xAI is **not** built into pi-ai, so — like Cursor — we implement the flow\n * here and register it with pi-ai's OAuth registry (`registerXaiOAuthProvider`)\n * so `getOAuthApiKey('xai-oauth', …)` resolves to it during lazy token refresh.\n *\n * Auth shape: OAuth 2.0 + PKCE with a loopback callback server, against xAI's\n * OIDC issuer (`https://auth.x.ai`). Differs from the Anthropic / Codex flows\n * (which use the shared `startCallbackServer`) on three points, so it gets its\n * own login impl rather than reusing that primitive:\n *\n * 1. The `redirect_uri` must be `http://127.0.0.1:56121/callback` verbatim\n * (xAI matches the registered loopback IP, not `localhost`).\n * 2. Endpoints come from OIDC discovery, not hard-coded constants.\n * 3. Token exchange + refresh are `application/x-www-form-urlencoded`\n * (the Anthropic flow posts JSON).\n *\n * The callback page still routes through {@link OAuthCallbackPageRenderer} so\n * the post-redirect HTML matches the rest of zidane's themed flows.\n *\n * Flow + constants adapted from the public reference implementations\n * (`stnly/pi-grok`, `BlockedPath/pi-xai-oauth`) and the Hermes Agent docs.\n * xAI's OAuth surface is unofficial/undocumented — re-verify the client id,\n * scopes, and callback port if login starts failing.\n */\n\nimport type {\n OAuthCredentials,\n OAuthLoginCallbacks,\n OAuthProviderInterface,\n} from '@earendil-works/pi-ai/oauth'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { OAuthCallbackPageRenderer } from './render'\nimport { createServer } from 'node:http'\nimport { generatePkce } from './pkce'\n\n/** pi-ai / zidane OAuth provider id. Also the credentials-file key. */\nexport const XAI_OAUTH_PROVIDER_ID = 'xai-oauth'\n\nconst DEFAULT_BASE_URL = 'https://api.x.ai/v1'\nconst ISSUER = 'https://auth.x.ai'\nconst DISCOVERY_URL = `${ISSUER}/.well-known/openid-configuration`\n/** Public client id used by the Grok CLI OAuth surface. */\nconst DEFAULT_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828'\nconst DEFAULT_SCOPE = 'openid profile email offline_access grok-cli:access api:access'\n/** xAI matches the registered loopback IP exactly — `localhost` is rejected. */\nconst DEFAULT_CALLBACK_HOST = '127.0.0.1'\nconst DEFAULT_CALLBACK_PORT = 56121\nconst CALLBACK_PATH = '/callback'\nconst PROVIDER_NAME = 'xAI Grok'\n/** Refresh slightly early so requests don't race expiry. */\nconst REFRESH_SKEW_MS = 120_000\nconst CALLBACK_TIMEOUT_MS = 180_000\n\nfunction xaiOAuthClientId(): string {\n return process.env.PI_XAI_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID\n}\n\nfunction xaiOAuthScope(): string {\n return process.env.PI_XAI_OAUTH_SCOPE || DEFAULT_SCOPE\n}\n\nfunction xaiOAuthCallbackHost(): string {\n return process.env.PI_XAI_OAUTH_CALLBACK_HOST || DEFAULT_CALLBACK_HOST\n}\n\nfunction xaiOAuthCallbackPort(): number {\n const parsed = Number.parseInt(process.env.PI_XAI_OAUTH_CALLBACK_PORT || String(DEFAULT_CALLBACK_PORT), 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CALLBACK_PORT\n}\n\n/** Resolve the xAI API base URL (env override → default). */\nexport function xaiBaseUrl(): string {\n return (process.env.PI_XAI_BASE_URL || process.env.XAI_BASE_URL || DEFAULT_BASE_URL)\n .replace(/\\/+$/, '')\n}\n\ninterface XaiDiscovery {\n authorization_endpoint: string\n token_endpoint: string\n}\n\n/**\n * xAI OAuth credentials. Extends the base `{ access, refresh, expires }` with\n * the resolved `token_endpoint` so refresh doesn't need a second discovery\n * round-trip, plus the cached discovery doc for validation.\n */\nexport interface XaiOAuthCredentials extends OAuthCredentials {\n tokenEndpoint?: string\n discovery?: XaiDiscovery\n}\n\nfunction base64Url(bytes: Uint8Array): string {\n let binary = ''\n for (const b of bytes)\n binary += String.fromCharCode(b)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction randomToken(byteLength = 16): string {\n return base64Url(crypto.getRandomValues(new Uint8Array(byteLength)))\n}\n\n/**\n * Refuse any OIDC endpoint that isn't HTTPS on an xAI origin.\n *\n * The discovery doc is cached long-term in `credentials.json`. A single MITM\n * during initial login could otherwise pin a malicious `token_endpoint` that\n * receives every subsequent refresh token. Validating scheme + host keeps the\n * endpoint on `x.ai` / `*.x.ai`.\n */\nfunction validateEndpoint(value: string, field: string): string {\n let url: URL\n try {\n url = new URL(value)\n }\n catch {\n throw new Error(`xAI OAuth discovery returned an invalid ${field}: ${value}`)\n }\n if (url.protocol !== 'https:')\n throw new Error(`xAI OAuth ${field} must use HTTPS: ${value}`)\n const host = url.hostname.toLowerCase()\n if (host !== 'x.ai' && !host.endsWith('.x.ai'))\n throw new Error(`Refusing non-xAI OAuth ${field}: ${value}`)\n return url.toString()\n}\n\nasync function discover(): Promise<XaiDiscovery> {\n let response: Response\n try {\n response = await fetch(DISCOVERY_URL, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n })\n }\n catch (cause) {\n throw new Error(`xAI OIDC discovery failed: ${cause instanceof Error ? cause.message : String(cause)}`)\n }\n if (!response.ok)\n throw new Error(`xAI OIDC discovery returned ${response.status}`)\n\n const payload = await response.json() as Record<string, unknown>\n return {\n authorization_endpoint: validateEndpoint(String(payload.authorization_endpoint ?? ''), 'authorization_endpoint'),\n token_endpoint: validateEndpoint(String(payload.token_endpoint ?? ''), 'token_endpoint'),\n }\n}\n\ninterface CallbackResult {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\ninterface XaiCallbackServer {\n redirectUri: string\n waitForCallback: (timeoutMs: number) => Promise<CallbackResult>\n close: () => void\n}\n\n/**\n * Start the loopback callback server on `127.0.0.1:56121`. Falls back to an\n * OS-assigned port if 56121 is taken — xAI accepts any loopback port as long\n * as the `redirect_uri` we send on the authorize URL matches the listener.\n */\nasync function startXaiCallbackServer(renderPage: OAuthCallbackPageRenderer): Promise<XaiCallbackServer> {\n const callbackHost = xaiOAuthCallbackHost()\n const callbackPort = xaiOAuthCallbackPort()\n let settle: ((value: CallbackResult) => void) | undefined\n let settled = false\n const callbackPromise = new Promise<CallbackResult>((resolve) => {\n settle = (value) => {\n if (settled)\n return\n settled = true\n resolve(value)\n }\n })\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n try {\n // accounts.x.ai may issue a CORS preflight against the loopback listener.\n const origin = req.headers.origin\n if (origin === 'https://accounts.x.ai' || origin === 'https://auth.x.ai') {\n res.setHeader('Access-Control-Allow-Origin', origin)\n res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type')\n res.setHeader('Access-Control-Allow-Private-Network', 'true')\n res.setHeader('Vary', 'Origin')\n }\n if (req.method === 'OPTIONS') {\n res.statusCode = 204\n res.end()\n return\n }\n\n const url = new URL(req.url ?? '/', `http://${callbackHost}`)\n if (url.pathname !== CALLBACK_PATH) {\n res.statusCode = 404\n res.end('Not found')\n return\n }\n\n const result: CallbackResult = {\n code: url.searchParams.get('code') ?? undefined,\n state: url.searchParams.get('state') ?? undefined,\n error: url.searchParams.get('error') ?? undefined,\n errorDescription: url.searchParams.get('error_description') ?? undefined,\n }\n\n res.statusCode = result.error ? 400 : 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(renderPage(result.error\n ? {\n kind: 'error',\n provider: PROVIDER_NAME,\n message: `${PROVIDER_NAME} authentication did not complete.`,\n details: result.errorDescription ?? result.error,\n }\n : {\n kind: 'success',\n provider: PROVIDER_NAME,\n message: 'xAI authentication completed. You can close this window.',\n }))\n settle?.(result)\n }\n catch {\n res.statusCode = 500\n res.end('Internal error')\n }\n })\n\n const listen = (port: number) => new Promise<number>((resolve, reject) => {\n server.once('error', reject)\n server.listen(port, callbackHost, () => {\n server.removeListener('error', reject)\n const addr = server.address()\n resolve(typeof addr === 'object' && addr ? addr.port : port)\n })\n })\n\n let actualPort: number\n try {\n actualPort = await listen(callbackPort)\n }\n catch {\n actualPort = await listen(0)\n }\n\n return {\n redirectUri: `http://${callbackHost}:${actualPort}${CALLBACK_PATH}`,\n waitForCallback: timeoutMs => Promise.race([\n callbackPromise,\n new Promise<CallbackResult>(resolve =>\n setTimeout(\n resolve,\n timeoutMs,\n { error: 'timeout', errorDescription: 'Timed out waiting for the xAI OAuth callback.' },\n )),\n ]),\n close: () => {\n try { server.close() }\n catch { /* already closed */ }\n },\n }\n}\n\nfunction expiryFromPayload(payload: Record<string, unknown>): number {\n const expiresIn = typeof payload.expires_in === 'number'\n ? payload.expires_in\n : Number(payload.expires_in ?? 3600)\n return Date.now() + expiresIn * 1000 - REFRESH_SKEW_MS\n}\n\nasync function exchangeCode(\n tokenEndpoint: string,\n code: string,\n redirectUri: string,\n verifier: string,\n discovery: XaiDiscovery,\n): Promise<XaiOAuthCredentials> {\n const clientId = xaiOAuthClientId()\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: clientId,\n code,\n redirect_uri: redirectUri,\n code_verifier: verifier,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token exchange failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n const refresh = String(payload.refresh_token ?? '')\n if (!access)\n throw new Error('xAI token exchange did not return an access_token.')\n if (!refresh)\n throw new Error('xAI token exchange did not return a refresh_token.')\n\n return { access, refresh, expires: expiryFromPayload(payload), tokenEndpoint, discovery }\n}\n\nexport async function loginXai(\n renderPage: OAuthCallbackPageRenderer,\n callbacks: OAuthLoginCallbacks,\n): Promise<XaiOAuthCredentials> {\n const discovery = await discover()\n const { verifier, challenge } = await generatePkce()\n const state = randomToken()\n const nonce = randomToken()\n const server = await startXaiCallbackServer(renderPage)\n\n try {\n const authUrl = new URL(discovery.authorization_endpoint)\n authUrl.searchParams.set('response_type', 'code')\n authUrl.searchParams.set('client_id', xaiOAuthClientId())\n authUrl.searchParams.set('redirect_uri', server.redirectUri)\n authUrl.searchParams.set('scope', xaiOAuthScope())\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('nonce', nonce)\n // Opt into xAI's generic OAuth plan tier (SuperGrok / X Premium+).\n authUrl.searchParams.set('plan', 'generic')\n authUrl.searchParams.set('referrer', 'zidane')\n\n callbacks.onAuth({\n url: authUrl.toString(),\n instructions: `Sign in to xAI and approve access. If the browser is on another machine, the callback listener is ${server.redirectUri}.`,\n })\n\n const result = await server.waitForCallback(CALLBACK_TIMEOUT_MS)\n if (result.error)\n throw new Error(result.errorDescription ?? result.error)\n if (result.state !== state)\n throw new Error('xAI OAuth state mismatch — possible CSRF. Please retry.')\n if (!result.code)\n throw new Error('xAI OAuth callback did not include an authorization code.')\n\n callbacks.onProgress?.('Exchanging authorization code for tokens...')\n return await exchangeCode(discovery.token_endpoint, result.code, server.redirectUri, verifier, discovery)\n }\n finally {\n server.close()\n }\n}\n\nexport async function refreshXaiToken(credentials: OAuthCredentials): Promise<XaiOAuthCredentials> {\n const xai = credentials as XaiOAuthCredentials\n const clientId = xaiOAuthClientId()\n if (!credentials.refresh)\n throw new Error('xAI credentials are missing a refresh token — re-login required.')\n\n const tokenEndpoint = xai.tokenEndpoint\n ?? xai.discovery?.token_endpoint\n ?? (await discover()).token_endpoint\n validateEndpoint(tokenEndpoint, 'token_endpoint')\n\n const response = await fetch(tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: clientId,\n refresh_token: credentials.refresh,\n }),\n signal: AbortSignal.timeout(30_000),\n })\n if (!response.ok)\n throw new Error(`xAI token refresh failed: ${response.status} ${await response.text().catch(() => '')}`)\n\n const payload = await response.json() as Record<string, unknown>\n const access = String(payload.access_token ?? '')\n if (!access)\n throw new Error('xAI token refresh did not return an access_token.')\n\n return {\n ...xai,\n access,\n // xAI may omit a rotated refresh token — keep the existing one.\n refresh: String(payload.refresh_token ?? credentials.refresh),\n expires: expiryFromPayload(payload),\n tokenEndpoint,\n }\n}\n\n/**\n * Build an xAI `OAuthProviderInterface`. Shape matches Anthropic / Codex /\n * Cursor so it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.\n *\n * `usesCallbackServer: true` — the loopback flow has a real callback server,\n * so the TUI doesn't need to prompt for a manual code paste.\n */\nexport function createXaiOAuthProvider(renderPage: OAuthCallbackPageRenderer): OAuthProviderInterface {\n return {\n id: XAI_OAUTH_PROVIDER_ID,\n name: 'xAI Grok (SuperGrok / X Premium+)',\n usesCallbackServer: true,\n login: callbacks => loginXai(renderPage, callbacks),\n refreshToken: refreshXaiToken,\n getApiKey: credentials => credentials.access,\n }\n}\n","import type { Provider, ProviderCapabilities, StreamCallbacks, StreamOptions, TurnResult } from '.'\nimport type { OAuthParams } from './oauth'\nimport { XAI_OAUTH_PROVIDER_ID, xaiBaseUrl } from '../chat/oauth-page/xai'\nimport { extractRuntimeCredentials, isOAuthAccessToken, resolveOAuthApiKey } from './oauth'\nimport { openaiCompat } from './openai-compat'\n\nconst DEFAULT_MODEL = 'grok-4.3'\n\n/**\n * xAI Grok provider — supports both a static API key (`XAI_API_KEY`) and the\n * SuperGrok / X Premium+ OAuth subscription.\n *\n * Both auth modes hit the same OpenAI-compatible endpoint (`https://api.x.ai/v1`),\n * so the wire handling is entirely {@link openaiCompat}. The only difference is\n * the bearer:\n *\n * - **API key**: `XAI_API_KEY` (or `params.apiKey`) → used verbatim.\n * - **OAuth**: no static key → {@link resolveOAuthApiKey} resolves + lazily\n * refreshes the stored `xai-oauth` token on every turn.\n *\n * `resolveOAuthApiKey` unifies the two: a real API key in `XAI_API_KEY` is\n * returned as-is, while an OAuth access token (or no env value at all) falls\n * through to the refreshable stored credentials. So this single factory serves\n * both paths without branching at construction time.\n */\nexport interface XaiParams extends OAuthParams {\n defaultModel?: string\n /**\n * Provider capability flags. Grok models are vision-capable; default\n * `{ vision: true, imageInToolResult: false }`. Override for text-only\n * deployments.\n */\n capabilities?: ProviderCapabilities\n /** Extra HTTP headers sent on every request. */\n extraHeaders?: Record<string, string>\n}\n\nexport function xai(params?: XaiParams): Provider {\n const defaultModel = params?.defaultModel || DEFAULT_MODEL\n const baseURL = xaiBaseUrl()\n const capabilities: ProviderCapabilities = params?.capabilities ?? {\n vision: true,\n imageInToolResult: false,\n }\n const baseCredentials = extractRuntimeCredentials(params)\n let runtimeCredentials = baseCredentials\n\n // Factory-time snapshot for the static `meta.isOAuth` display flag only (the\n // terminal header's `oauth`/`key` badge). The real bearer resolves per turn\n // in `stream`, so this is best-effort, mirroring anthropic's pattern. It's\n // OAuth unless a genuine static API key is present: a `xai-…` key → key mode;\n // runtime OAuth creds, a JWT access token, or no static key (stored creds\n // used) → oauth mode.\n const staticKey = params?.apiKey ?? process.env.XAI_API_KEY\n const isOAuth = baseCredentials !== undefined || !staticKey || isOAuthAccessToken(staticKey)\n\n /**\n * Build the openai-compat delegate for a resolved bearer. Cheap — the factory\n * only wires closures — so we re-create it per turn with the freshly resolved\n * (possibly refreshed) OAuth token rather than baking a stale key in once.\n */\n function delegateFor(apiKey: string): Provider {\n return openaiCompat({\n name: 'xai',\n apiKey,\n baseURL,\n defaultModel,\n capabilities,\n extraHeaders: params?.extraHeaders,\n })\n }\n\n // A delegate built with a placeholder key supplies the non-stream methods\n // (formatTools, userMessage, message builders, classifyError). Only `stream`\n // resolves the real bearer, so the placeholder never reaches the wire.\n const skeleton = delegateFor('placeholder-resolved-at-stream')\n\n return {\n ...skeleton,\n name: 'xai',\n // Reuse the delegate's normalized capability flags (openaiCompat fills the\n // audio/video/documents defaults) so the advertised shape matches the wire.\n meta: { ...skeleton.meta, defaultModel, isOAuth },\n\n async stream(options: StreamOptions, callbacks: StreamCallbacks): Promise<TurnResult> {\n const apiKey = await resolveOAuthApiKey(\n {\n provider: 'xai',\n providerId: XAI_OAUTH_PROVIDER_ID,\n params: runtimeCredentials ? { ...params, ...runtimeCredentials } : params,\n envKey: 'XAI_API_KEY',\n missingError: 'No xAI credentials found. Set XAI_API_KEY or run `bun run auth --xai` first.',\n refreshError: reason => `xAI OAuth token refresh failed. Run \\`bun run auth --xai\\` again. ${reason}`,\n },\n {\n ...callbacks,\n async onOAuthRefresh(ctx) {\n // Keep param-sourced credentials current so a long-lived provider\n // instance reuses the rotated token instead of re-refreshing.\n if (ctx.source === 'params') {\n runtimeCredentials = {\n access: ctx.credentials.access,\n refresh: ctx.credentials.refresh,\n expires: ctx.credentials.expires,\n }\n }\n await callbacks.onOAuthRefresh?.(ctx)\n },\n },\n )\n\n return delegateFor(apiKey).stream(options, callbacks)\n },\n }\n}\n"],"mappings":"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"}
@@ -515,6 +515,7 @@ turn:before ← turn starts
515
515
  stream:thinking (n) / stream:text (n) ← each streamed chunk
516
516
  stream:end ← text complete (if text present)
517
517
  stream:retry (n) ← retryable error caught; sleep then re-enter stream (only fires on intermediate failures)
518
+ model:fallback ← terminal failure advanced the run's provider+model fallback chain (run-sticky; only the LAST target's failure reaches stream:error)
518
519
  stream:error ← provider rejected terminally (omitted on user-initiated aborts and on retries that recover)
519
520
  turn:after ← always fires (incl. errors); SessionTurn + toolCounts
520
521
  tool:gate [mutable: block, reason, result?] ← refuse / substitute / run
@@ -621,12 +622,15 @@ child:turn:after
621
622
  **Subagent type registry** (`createSpawnTool({ subagents })`). When set, the spawn tool's input schema gains a `subagent_type` enum (the registered keys plus the always-available `'general-purpose'` fallback). The matching `SubagentDef` overlays the parent's resolved config for that one dispatch:
622
623
 
623
624
  - `system` — preset system prompt. Per-call `input.system` still wins (model specialization beats preset defaults).
625
+ - `model` — default model for this subagent type (e.g. an Explore preset pinned to a cheap model). Precedence: per-call `input.model` > this > `SpawnToolOptions.model` > inherit the parent run's model.
624
626
  - `tools: readonly string[]` — restrict the child to this canonical-name allow-list (strict subset of the parent's tools; unknown names silently dropped). Matched on `tool.spec.name`, not registry key.
625
627
  - `readonly: true` — convenience for `tools: ['read_file', 'grep', 'glob', 'list_files']`. `tools` wins when both are set.
626
628
  - `description` — short blurb rendered into the schema field so the model can pick a type without round-tripping through docs.
627
629
 
628
630
  Unknown `subagent_type` values are routed to `'general-purpose'` (no overlay) — Claude-Code-trained models emitting `subagent_type: 'Verification'` against hosts that didn't wire that type still run, just with the parent's defaults. Empty / unset registry omits the `subagent_type` schema field entirely (historical schema).
629
631
 
632
+ **Subagent model catalog** (`createSpawnTool({ models })` / `spawnTool.withModels(catalog)` for hosts whose provider list is only known at agent-build time). When set, the schema gains an optional `model` field so the parent can delegate to cheaper models per call. ≤ 8 entries embed id + pricing + context window in the field description; larger catalogs defer to a host-registered `model_search` tool (`createModelSearchTool(catalog)`). Each entry carries the `Provider.name` that serves it, and the child is routed to that provider (resolved via `ToolContext.providers`, the run's fallback-chain provider list). Unknown ids reject with closest matches so the model can self-correct; no catalog keeps the historical schema and children always inherit the parent's model.
633
+
630
634
  ## Dependency Graph
631
635
 
632
636
  ```mermaid
package/docs/CHAT.md CHANGED
@@ -115,7 +115,7 @@ The table below indexes every named export; sections further down dive into the
115
115
  | Module | Surface |
116
116
  |---|---|
117
117
  | `agents` | Multi-profile registry — `AgentProfile`, `AgentRegistry`, `AgentAccent`, `BUILD_AGENT`, `PLAN_AGENT`, `BUILTIN_AGENTS`, `DEFAULT_AGENT_ID`, `DEFAULT_PERSIST_EXCLUDE_TOOLS`, `resolveAgentId`, `singleAgentRegistry`, `accentColor`. See **Agents** below. |
118
- | `agent-prompt` | Composable system-prompt fragments — `IDENTITY_PREFIX`, `DOING_TASKS_DOCTRINE`, `ACTIONS_WITH_CARE_DOCTRINE`, `TOKEN_DISCIPLINE_DOCTRINE`, `COMMUNICATION_DOCTRINE`, `SUBAGENT_GUIDANCE`, `PLAN_MODE_DOCTRINE`, `PLAN_MODE_DOCTRINE_NO_PROMPTS`, `INTERACTION_GUIDANCE`, `INTERACTION_GUIDANCE_NO_PROMPTS`, `envSection`, `buildBuildSystem`, `buildPlanSystem`. Types: `BuildSystemOptions`, `EnvSectionOptions`. See **System prompt** below. |
118
+ | `agent-prompt` | Composable system-prompt fragments — `IDENTITY_PREFIX`, `DOING_TASKS_DOCTRINE`, `CONVENTIONS_DOCTRINE`, `GIT_DOCTRINE`, `ACTIONS_WITH_CARE_DOCTRINE`, `TOKEN_DISCIPLINE_DOCTRINE`, `TOKEN_DISCIPLINE_DOCTRINE_READ_ONLY`, `COMMUNICATION_DOCTRINE`, `SUBAGENT_GUIDANCE`, `PLAN_MODE_DOCTRINE`, `PLAN_MODE_DOCTRINE_NO_PROMPTS`, `INTERACTION_GUIDANCE`, `INTERACTION_GUIDANCE_NO_PROMPTS`, `envSection`, `buildBuildSystem`, `buildPlanSystem`. Types: `BuildSystemOptions`, `EnvSectionOptions`. See **System prompt** below. |
119
119
  | `agents-md` | Personal-instructions discovery — `discoverAgentsMd({ cwd, home, prefix, scope })` walks `~/.agents/AGENTS.md`, `~/.{prefix}/AGENTS.md`, `~/.claude/CLAUDE.md`, `<repo>/AGENTS.md`, `<repo>/.agents/AGENTS.md`, `<repo>/.{prefix}/AGENTS.md`, `<repo>/CLAUDE.md` and loads **every** file that exists (broadest → narrowest, deduped by absolute path; empty / whitespace-only files skipped). The `scope` option (`'none'` / `'user'` / `'project'` / `'both'`, default `'both'`) restricts which scopes contribute — `'none'` short-circuits before any filesystem access. Renders a `userInstructions` block consumed by `buildBuildSystem` / `buildPlanSystem`. Types: `AgentsMdFile`, `AgentsMdResult`, `AgentsMdScope`. |
120
120
  | `auth` | `detectAuth(dataDir, providers)` reports which providers have usable credentials and which auth method each accepts. `ProviderAuth.methods[].source` is `'apikey' \| 'oauth'`. |
121
121
  | `auto-compact` | `chatAutoCompactBehavior({ enabled, threshold, rawContextWindow })` — build the per-run `behavior.autoCompact` for loop-native compaction; `shouldAutoCompact(input)` — the pure predicate the loop uses. See **Auto-compaction**. |
@@ -131,8 +131,8 @@ The table below indexes every named export; sections further down dive into the
131
131
  | `edit-approval` | Pure helpers bridging the safe-mode gate to the host-side per-hunk flow — `resolveApprovalForPayload`, `maskToOutcomeKinds`, `buildEditOutcomesAnnotation`, `parseEditOutcomesFromResult`, `summarizeOutcomes`, `ResolvedApproval`. See **Per-edit approval**. |
132
132
  | `edit-diff` | Diff plumbing — `extractEditPayload`, `previewEditPayload`, `applyEditPayload`, `buildUnifiedDiff`, `buildContextualDiff`, `computeLineDiff`, `computeInlineDiff`, `splitLines`, `tokenize`, `filetypeFromPath`. See **Edit-diff rendering**. |
133
133
  | `enabled-toggle-set` | `useEnabledToggleSet({ catalog, keyOf, settingKey })` — generic state machine for `enabledSkills` / `enabledMcps` (undefined = all enabled, `[]` = off, `[names]` = allowlist). |
134
- | `files-discovery` | `listProjectFiles({ cwd, signal? })` → `FileEntry[]`, gitignored paths excluded. |
135
- | `format` | `fmtTokens`, `ageString`, `shortId`, `compactPath` — display-only helpers. |
134
+ | `files-discovery` | `listProjectFiles({ cwd, signal? })` → `FileEntry[]`, gitignored paths excluded. Includes directory entries (`kind: 'dir'`) derived from the file paths, so `@` mentions can reference dirs too. |
135
+ | `format` | `fmtTokens`, `fmtCost`, `ageString`, `shortId`, `compactPath` — display-only helpers. |
136
136
  | `generate-title` | `generateSessionTitle({ session, provider, model, signal? })` + `cleanTitle` + `shouldAutoGenerateTitle(turns, metadata?)` (true once a session has ≥2 turns and no explicit `metadata.title` — the TUI's automatic-titling gate). |
137
137
  | `hints` | Footer + prompt-hint plumbing — `Hint`, `hintsLength`, `clipHintsToWidth`, `truncateTrailing`. Plain-text math; rendering is the consumer's job. |
138
138
  | `interactions` | Plan + Q&A protocol (`present_plan`, `ask_user`). See **Interactions** and `docs/INTERACTIONS.md`. |
@@ -146,6 +146,7 @@ The table below indexes every named export; sections further down dive into the
146
146
  | `mcp-tools-cache` | Per-server tool catalog persistence at `<dataDir>/mcp-tools-cache.json`. `loadMcpToolsCache`, `saveMcpToolsCache`, `clearMcpToolsCache`, `mcpToolsCachePath`, `CachedMcpToolList`, `McpToolsCache`. `subscribeMcpToolsCache(agent.hooks, { dataDir, onChange? })` listens on `mcp:tools:list` and writes the file; `refreshMcpToolsCatalog({ servers, hooks, ... })` opens transient connections to repopulate without a full agent run. |
147
147
  | `mcps-discovery` | `discoverProjectMcps`, `parseMcpsFile`, `buildMcpServers`, `defaultMcpsConfigPaths`, `DiscoveredMcp`, `DiscoveryError`, `DiscoveryResult`. `buildMcpServers` now also accepts `disabledTools: Settings['disabledMcpTools']` and unions it into each config's `disabledTools` (the JSON-side deny-list still wins on collisions). See **Project MCP servers**. |
148
148
  | `model-catalog` | Cross-provider unified model picker assembly — `buildModelCatalog`, `filterModelCatalog`, `indexOfEntry`, `CatalogEntry`. |
149
+ | `model-chain` | Ordered model fallback chains (pure helpers behind the picker's multi-select + the host's run-option assembly) — `ModelChainEntry`, `toggleChainEntry`, `chainPositionOf`, `chainsEqual`, `filterChainFallbacks`, `restoreChainFallbacks`, `sanitizeModelChain`, `providerKeySetChanged`, `buildRunModel`. |
149
150
  | `oauth` | `runOAuthLogin(descriptor, { onUrl, onPrompt, onProgress, signal })` + `supportsOAuth(descriptor)` + `oauthUsesManualCodePaste(descriptor)` — AI-provider OAuth (distinct from MCP OAuth). |
150
151
  | `oauth-redirect` | `fetchOAuthRedirect(pastedUrl)` — SSH escape hatch. Validates `pasted` is a loopback URL, fires a GET so the in-process callback server (pi-ai's or `startOAuthCallback`'s) receives the request through its normal handler. Used by the TUI to accept a redirect-URL paste when the browser couldn't reach the callback server directly. |
151
152
  | `path-display` | `formatPathForCwd(projectRel, projectRoot, cwd)` — rewrites a project-root-relative path (as emitted by `listProjectFiles`) into the form the agent's CWD-resolving tools actually find. Wired into `createFilesCompletionProvider({ formatPath })` by the TUI shell. |
@@ -156,9 +157,9 @@ The table below indexes every named export; sections further down dive into the
156
157
  | `safe-mode` | Per-project safelist + matchers — `IMPLICITLY_SAFE_TOOLS`, `suggestSafelistEntries`, `addToSafelist`, `getSafelist`, `isOnSafelist`, `matchesSafelistEntry`, `readProjects`, `writeProjects`, `projectsFilePath`. |
157
158
  | `safe-mode-context` | `SafeModeProvider` owns a FIFO approval queue. `useSafeModeQueue()` returns the live array; `useSafeModeActions()` returns stable `{ requestApproval, resolveHead, denyAll }`. |
158
159
  | `session-export` | `renderSession`, `resolveSessionExportTarget`, `writeSessionExport`, `SessionExportFormat`, `SessionExportAnchor`, `SessionExportTarget`. |
159
- | `settings-context` | `SettingsProvider`, `useSettings`, `DEFAULT_SETTINGS`, `SETTINGS_TOGGLES`, `SETTINGS_CHOICES`, `SETTINGS_CATEGORIES`, `BooleanSettingKey`, `SettingsCategory`, `SettingsCategoryDescriptor`. See **Settings**. |
160
+ | `settings-context` | `SettingsProvider`, `useSettings`, `DEFAULT_SETTINGS`, `SETTINGS_TOGGLES`, `SETTINGS_CHOICES`, `SETTINGS_INPUTS`, `SETTINGS_CATEGORIES`, `BooleanSettingKey`, `SettingsCategory`, `SettingsCategoryDescriptor`. See **Settings**. |
160
161
  | `skills-discovery` | `discoverProjectSkills`, `buildSkillsConfig`, `defaultSkillScanPaths`. |
161
- | `store` | Session reconstruction + persisted UI state + transcript view rules — `eventsFromTurns`, `lastContextSizeFromTurns`, `listSessionMeta`, `deriveSessionTitle`, `titleFromTurns`, `selectableTurnIds`, `stripSpawnTokensLine`, `toolCallPreview`, `toolResultText`, `createStateStore`, `loadState`, `saveState`, `isVisible`, `marginTopFor`, `isEditErrorResult`, `isTurnHighlighted`, `sumRunCosts`, `turnSelectionOwnership`, `TuiState`, `StateStoreApi`. |
162
+ | `store` | Session reconstruction + persisted UI state + transcript view rules — `eventsFromTurns`, `lastContextSizeFromTurns`, `listSessionMeta`, `deriveSessionTitle`, `titleFromTurns`, `selectableTurnIds`, `stripSpawnTokensLine`, `toolCallPreview`, `toolResultText`, `createStateStore`, `loadState`, `saveState`, `isVisible`, `marginTopFor`, `isEditErrorResult`, `isTurnHighlighted`, `sumRunCosts`, `usageByModel`, `turnSelectionOwnership`, `TuiState`, `StateStoreApi`, `ModelUsageSummary`. |
162
163
  | `streaming` | `useStreamBuffer`, `finalizeStreamingMarkdown`, `finalizeStreamingMarkdownForOwner`, `turnContextSize`. Per-owner finalize handles concurrent parent + child streams. |
163
164
  | `theme` + `theme-context` | `Theme`, `BUILTIN_THEMES`, `resolveTheme`, `resolveChipColor`, `DEFAULT_THEME`, themes (`CATPPUCCIN_*`, `VAPORWAVE_THEME`), hooks (`useTheme`, `useColors`, `useSelectStyle`, `useSurfaces`, `useSyntaxStyles`). See **Theme**. |
164
165
  | `todos` | `todowrite` / `todoread` tool factory — `createTodoTools` (returns a `Preset`), `isTodoTool`, `getTodosForRun`, `setTodosForRun`, `pruneTodosByRun`, `TODOWRITE_TOOL`, `TODOREAD_TOOL`, `TODOS_METADATA_KEY`, `TODO_WRITE_COUNTS_METADATA_KEY`, `TodoItem`, `TodoStatus`, `TodosBag`, `CreateTodoToolsOptions`. Persistent task checkpointing — top-level runs share a session-scoped slot (continuity across prompts); subagent runs get their own slot (isolation). On by default: a run-scoped `block-after` dedup (`maxIdenticalWrites`) hard-blocks consecutive identical payloads (loop breaker). Optional blanket per-run budget via `behavior.toolBudgets`; a soft in-body identical dedup also short-circuits to "No change". Compose with `composePresets` or spread. See **Todos** below. |
@@ -319,6 +320,7 @@ interface TuiState {
319
320
  lastProvider?: ProviderKey
320
321
  lastSessionId?: string
321
322
  lastModelByProvider?: Partial<Record<ProviderKey, string>>
323
+ lastModelChain?: ModelChainEntry[] // entry 0 = primary, 1+ = fallbacks; restored only when entry 0 matches the resolved primary
322
324
  lastEffortByModel?: Record<string, ThinkingLevel>
323
325
  lastAgent?: string
324
326
  settings?: Partial<Settings>
@@ -395,8 +397,8 @@ Renderer-agnostic fragments designed to be byte-stable across sessions so the An
395
397
  buildBuildSystem({ cwd: projectDir, allowInteraction: true })
396
398
  // STATIC (cached):
397
399
  // IDENTITY_PREFIX
398
- // + DOING_TASKS_DOCTRINE + ACTIONS_WITH_CARE_DOCTRINE + TOKEN_DISCIPLINE_DOCTRINE
399
- // + SUBAGENT_GUIDANCE + COMMUNICATION_DOCTRINE
400
+ // + DOING_TASKS_DOCTRINE + CONVENTIONS_DOCTRINE + ACTIONS_WITH_CARE_DOCTRINE
401
+ // + GIT_DOCTRINE + TOKEN_DISCIPLINE_DOCTRINE + SUBAGENT_GUIDANCE + COMMUNICATION_DOCTRINE
400
402
  // + (allowInteraction ? INTERACTION_GUIDANCE : INTERACTION_GUIDANCE_NO_PROMPTS)
401
403
  // + opts.userInstructions
402
404
  // ── __ZIDANE_SYSTEM_PROMPT_BOUNDARY__ ──
@@ -407,7 +409,7 @@ buildPlanSystem({ cwd: projectDir, allowInteraction: true })
407
409
  // STATIC (cached):
408
410
  // IDENTITY_PREFIX
409
411
  // + (allowInteraction ? PLAN_MODE_DOCTRINE : PLAN_MODE_DOCTRINE_NO_PROMPTS)
410
- // + TOKEN_DISCIPLINE_DOCTRINE + COMMUNICATION_DOCTRINE
412
+ // + TOKEN_DISCIPLINE_DOCTRINE_READ_ONLY + COMMUNICATION_DOCTRINE
411
413
  // + (allowInteraction ? INTERACTION_GUIDANCE : INTERACTION_GUIDANCE_NO_PROMPTS)
412
414
  // + opts.userInstructions
413
415
  // ── __ZIDANE_SYSTEM_PROMPT_BOUNDARY__ ──
@@ -488,7 +490,7 @@ Four tables drive any settings UI (TUI tabs today, GUI panels tomorrow) — they
488
490
  - `SETTINGS_CATEGORIES: SettingsCategoryDescriptor[]` — display order + labels for the high-level buckets (`agent`, `ui`). Each toggle / choice / input declares which bucket it belongs to via its `category` field.
489
491
  - `SETTINGS_TOGGLES: SettingsToggle[]` — boolean rows. `key` is the narrowed `BooleanSettingKey`; `category: SettingsCategory` routes the row to the right tab.
490
492
  - `SETTINGS_CHOICES: SettingsChoice[]` — value-from-options rows. `category` field on every entry. Currently spans both buckets: `autoCompactThreshold`, `userInstructionsScope` (agent); `toolCallDisplay`, `editDiffDisplay`, `theme`, `uiMode`, `targetFps`, `ctrlCBehavior`, `terminalTitle` (ui). `terminalTitle: 'icon-session'` pairs with the `sessionIcon` input to render the tab title as `{icon} · {session}`.
491
- - `SETTINGS_INPUTS: SettingsInput[]` — free-text rows edited via a single-line input (`key` is the narrowed `StringSettingKey`). Currently `sessionIcon` (ui), shown only while `terminalTitle` is `'icon-session'`. The modal renders the value (or `placeholder` when empty) and opens a footer editor on `enter`.
493
+ - `SETTINGS_INPUTS: SettingsInput[]` — free-text rows edited via a single-line input (`key` is the narrowed `StringSettingKey`). Currently `sessionIcon` and `attentionIcon` (ui) — the latter swaps in while the agent waits on user input (tool approvals, question forms) — both shown only while `terminalTitle` is `'icon-session'`. The modal renders the value (or `placeholder` when empty) and opens a footer editor on `enter`.
492
494
 
493
495
  ## Theme
494
496
 
@@ -1541,7 +1543,7 @@ const text = turnAsText(turn)
1541
1543
 
1542
1544
  **Render chip pills on submitted prompts** — call `splitPromptSegments(text, refs)` and map each `'chip'` segment to your platform's pill (HTML `<span>`, React component, etc.). Pull colors from `theme.surfaces.chips` via `resolveChipColor`.
1543
1545
 
1544
- **Persist + restore the user's last picks** — `stateStore.save({ ...stateStore.load(), lastProvider, lastSessionId, lastModelByProvider, lastEffortByModel, lastAgent, settings })`. Resume is automatic via `resolveConfig`'s `resumeProvider` + `initialPicked`.
1546
+ **Persist + restore the user's last picks** — `stateStore.save({ ...stateStore.load(), lastProvider, lastSessionId, lastModelByProvider, lastModelChain, lastEffortByModel, lastAgent, settings })`. Resume is automatic via `resolveConfig`'s `resumeProvider` + `initialPicked` (which restores the fallback chain too, via `restoreChainFallbacks`).
1545
1547
 
1546
1548
  See `src/tui/app.tsx` for the canonical wiring across every pattern above.
1547
1549
 
package/docs/SKILL.md CHANGED
@@ -624,7 +624,7 @@ createSpawnTool({
624
624
  })
625
625
  ```
626
626
 
627
- Per-type overlay fields: `system` (preset system prompt — per-call `input.system` still wins), `tools: string[]` (canonical-name allow-list, strict subset of the parent's tools), `readonly: true` (convenience for `['read_file', 'grep', 'glob', 'list_files']`), `description` (rendered into the schema for model self-selection). Unknown type names degrade to `'general-purpose'` (no overlay, no error) so models emitting types the host didn't wire keep working. Empty / unset registry drops the schema field entirely.
627
+ Per-type overlay fields: `system` (preset system prompt — per-call `input.system` still wins), `model` (default model for the type — per-call `input.model` wins), `tools: string[]` (canonical-name allow-list, strict subset of the parent's tools), `readonly: true` (convenience for `['read_file', 'grep', 'glob', 'list_files']`), `description` (rendered into the schema for model self-selection). Unknown type names degrade to `'general-purpose'` (no overlay, no error) so models emitting types the host didn't wire keep working. Empty / unset registry drops the schema field entirely.
628
628
 
629
629
  ## Interaction Tool
630
630
 
package/docs/TUI.md CHANGED
@@ -97,7 +97,7 @@ import {
97
97
  | `AuthScreen` | First-run + re-config provider picker. Drives the OAuth / API-key wizard. |
98
98
  | `SessionsScreen` | Session list. Sorted by recency via the store. |
99
99
  | `Modal`, `ModalRoot`, `useModal`, `useModalAwareFocus` | Overlay layer. `Modal` owns its own esc-to-close handler. `useModalAwareFocus(preferred)` returns `false` while a modal is open so background inputs blur. **Single-slot:** `modal.open(node)` replaces the active node; modals do not stack. |
100
- | `ModelPickerModal` | Cross-provider searchable model picker. Returns `{ providerKey, modelId }` on commit; cross-provider picks trigger a provider swap + session re-activation in the host. |
100
+ | `ModelPickerModal` | Cross-provider searchable model picker. Two commit modes: **single pick** (`onPick`) — ↵ returns `{ providerKey, modelId }`, used by the compaction-model picker; **ordered multi-select** (`onApplyChain`) ↵ toggles the row in a numbered fallback chain (the number is the row's 1-based position; pick order = fallback order), esc applies. Entry 1 is the primary (provider swap, effort, options); entries 2+ become run-level fallback targets (`agent.run({ model: […] })`). Esc with an unchanged or emptied chain is a plain cancel. Cross-provider chains trigger a provider swap + session re-activation in the host; the footer model chip shows `+N` while fallbacks are configured. When a run actually falls back (`model:fallback`), the chip switches to the model NOW serving the run, warn-colored as the "temporary" cue, and a transcript info line records the switch; both snap back at the next run (fallback is run-sticky per run). |
101
101
  | `EffortPickerModal` | Reasoning-effort picker. Lists `'off' / 'minimal' / 'low' / 'medium' / 'high'` plus `'adaptive'` when `supportsAdaptive` is true (Anthropic only). Surfaced via `ctrl+l` when `modelSupportsReasoning(descriptor, model)`. |
102
102
  | `SettingsModal` | Tabbed: **Agent** (behaviour, persistence), **UI** (display, theming, animations), **Keybindings** (read-only catalog · `↵` opens `keybindings.json`), **Authentication** (current provider + detected providers · `↵` pops the AuthScreen), **Skills**, **MCP servers**. Category tab order is data-driven by `SETTINGS_CATEGORIES` from `zidane/chat`. Keybindings + Authentication tabs only appear when the host wires the `keybindings` / `authentication` data props; otherwise the legacy "open keybindings file" / "re-authenticate" rows render in the UI / Agent tabs as a fallback. One shared search input filters the active tab; `←/→` cycle tabs (preventDefault keeps the input cursor put), `↑/↓` navigate, `↵` toggle/cycle/pick/edit. MCP tab adds `ctrl+L` login, `ctrl+O` logout, and `esc` cancel-authorizing (Modal's esc-close still fires alongside). Sized at `maxWidth: 160, maxHeight: 50` — wider than the legacy modal so the Keybindings catalog + Authentication status block sit comfortably. |
103
103
  | `AgentPickerModal` | Multi-agent profile picker. Hidden when only one profile is registered. |
@@ -191,7 +191,7 @@ Every customizable shortcut routes through `<userDir>/keybindings.json` and is p
191
191
  | `esc` | — | modal open | close modal |
192
192
  | `ctrl+o` | `openSettings` | non-auth | open Settings modal |
193
193
  | `ctrl+shift+x` | `openSessionDetails` | chat (idle) or sessions list (focused row) | open session-details modal. Kept off `ctrl+x` (conventional "cut"); needs the kitty keyboard protocol to stay distinct from it — rebind on legacy terminals like Terminal.app |
194
- | `ctrl+m` | `openModelPicker` | chat (idle) | open cross-provider model picker |
194
+ | `ctrl+m` | `openModelPicker` | chat (idle) | open cross-provider model picker (↵ numbers models into a fallback chain · esc applies) |
195
195
  | `ctrl+l` | `openEffortPicker` | chat (idle, model has reasoning) | open reasoning-effort picker |
196
196
  | `ctrl+s` | `enterSelectTurnMode` | chat (idle) | enter select-turn mode (transcript navigation) |
197
197
  | `ctrl+t` | `openTodos` | chat (session attached) | open the active run's `todowrite` checkpoints (read-only) |
@@ -363,7 +363,7 @@ Both surfaces resolve "what list to show" via `useActiveTodos(session)`, which c
363
363
 
364
364
  ### Context-usage panel
365
365
 
366
- - **`ContextPanelModal`** (`src/tui/context-panel.tsx`) — opened via `ctrl+shift+g` (`openContextPanel`), closes on `esc`. Read-only, on-demand: the `App.tsx` handler calls `agent.getContextBreakdown({ model, effectiveWindow, autocompactBuffer, compactThreshold })` (see [Context-usage breakdown in CHAT.md](./CHAT.md#context-usage-breakdown)) and opens the modal with the result — guarded so a stale async resolve (session switched mid-fetch) doesn't pop a panel for the wrong agent. Renders a single full-width stacked bar (one fixed-hue segment per live category, the autocompact-buffer zone pinned at the tail from the threshold to the window end, free space as the remaining track) above per-category rows (swatch · label · token count · percent). Estimated rows carry a `~` prefix; the footnote `~ estimated — exact counts unavailable for this provider` shows only when ≥1 live category is heuristic. Below the categories: an "active skills" section (informational — an activated skill's body lives in the conversation, already counted there) and a "last turn" section with the exact cache-read / cache-write / fresh-input / output split from `TurnUsage`. Expandable rows (MCP tools, per-rule files) toggle with `↵`/`space`; `↑↓` move the cursor across expandable groups. The panel works before the first message via the agent's skills-only pre-run assembly. The bar palette is shared verbatim with the GUI popover so both surfaces read identically.
366
+ - **`ContextPanelModal`** (`src/tui/context-panel.tsx`) — opened via `ctrl+shift+g` (`openContextPanel`), closes on `esc`. Read-only, on-demand: the `App.tsx` handler calls `agent.getContextBreakdown({ model, effectiveWindow, autocompactBuffer, compactThreshold })` (see [Context-usage breakdown in CHAT.md](./CHAT.md#context-usage-breakdown)) and opens the modal with the result — guarded so a stale async resolve (session switched mid-fetch) doesn't pop a panel for the wrong agent. Renders a single full-width stacked bar (one fixed-hue segment per live category, the autocompact-buffer zone pinned at the tail from the threshold to the window end, free space as the remaining track) above per-category rows (swatch · label · token count · percent). Estimated rows carry a `~` prefix; the footnote `~ estimated — exact counts unavailable for this provider` shows only when ≥1 live category is heuristic. Below the categories: an "active skills" section (informational — an activated skill's body lives in the conversation, already counted there) a "last turn" section with the exact cache-read / cache-write / fresh-input / output split from `TurnUsage`, and a "usage by model" section (session-wide per-model turn/token/cost totals across parent + subagent runs, via `usageByModel` from `zidane/chat`; `≥` marks costs floored by turns with no known rate; hidden while the session has no runs). Expandable rows (MCP tools, per-rule files) toggle with `↵`/`space`; `↑↓` move the cursor across expandable groups. The panel works before the first message via the agent's skills-only pre-run assembly. The bar palette is shared verbatim with the GUI popover so both surfaces read identically.
367
367
 
368
368
  In addition, every `todowrite` tool call in the transcript renders a small "what's in progress" sub-list directly beneath the formatted call line. The sub-list reads the call's `input.todos` payload (the checkpoint the model just wrote), filters to `status === 'in_progress'`, and prints one indented `◐ <content>` row per item. Hidden when the payload has no in-progress items (e.g. all-completed close-out) so the transcript stays tight. This is purely declarative — it reads the event's own payload, not session state, so it's stable across re-renders and matches what the model intended at that checkpoint (not the latest list state, which the indicator + modal already surface).
369
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zidane",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "an agent that goes straight to the goal",
5
5
  "type": "module",
6
6
  "private": false,
@@ -260,7 +260,7 @@
260
260
  "typecheck": "tsc --noEmit"
261
261
  },
262
262
  "dependencies": {
263
- "@earendil-works/pi-ai": "0.79.10",
263
+ "@earendil-works/pi-ai": "0.80.3",
264
264
  "chalk": "^5.6.2",
265
265
  "fzf": "^0.5.2",
266
266
  "hookable": "^6.1.1",