zidane 6.4.4 → 6.5.1

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 (138) hide show
  1. package/README.md +1 -0
  2. package/dist/acp-cli.js +7 -6
  3. package/dist/acp-cli.js.map +1 -1
  4. package/dist/{acp-BLO58K3F.js → acp-o-LQAjFw.js} +4 -5
  5. package/dist/{acp-BLO58K3F.js.map → acp-o-LQAjFw.js.map} +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-8sUadp5M.js → agent-BVNkC1h4.js} +886 -113
  9. package/dist/agent-BVNkC1h4.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -2
  12. package/dist/{anthropic-D3lkw5mv.js → anthropic-Dov2mC5e.js} +3 -3
  13. package/dist/{anthropic-D3lkw5mv.js.map → anthropic-Dov2mC5e.js.map} +1 -1
  14. package/dist/{auth-BzYbbdN_.js → auth-B8ZuBcKC.js} +5 -5
  15. package/dist/{auth-BzYbbdN_.js.map → auth-B8ZuBcKC.js.map} +1 -1
  16. package/dist/chat/pure.d.ts +3 -3
  17. package/dist/chat/pure.js +2 -2
  18. package/dist/chat.d.ts +13 -44
  19. package/dist/chat.d.ts.map +1 -1
  20. package/dist/chat.js +8 -7
  21. package/dist/chat.js.map +1 -1
  22. package/dist/{edit-utils-EGosADZq.js → context-breakdown-BLF10tOO.js} +154 -2
  23. package/dist/context-breakdown-BLF10tOO.js.map +1 -0
  24. package/dist/{context-transport-D29WvHcm.js → context-transport-D-daWOmp.js} +3 -3
  25. package/dist/{context-transport-D29WvHcm.js.map → context-transport-D-daWOmp.js.map} +1 -1
  26. package/dist/contexts/daytona.d.ts +1 -1
  27. package/dist/contexts/e2b.d.ts +1 -1
  28. package/dist/eval.d.ts +1 -1
  29. package/dist/eval.js +1 -1
  30. package/dist/extensions.d.ts +2 -2
  31. package/dist/extensions.js +2 -2
  32. package/dist/headless.d.ts +1 -1
  33. package/dist/headless.js +3 -3
  34. package/dist/{index-C-Nd2ZbV.d.ts → index-CWBSrqI6.d.ts} +676 -3
  35. package/dist/index-CWBSrqI6.d.ts.map +1 -0
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +13 -13
  38. package/dist/{login-CMVVnAHS.js → login-CR3fDHXh.js} +3 -3
  39. package/dist/{login-CMVVnAHS.js.map → login-CR3fDHXh.js.map} +1 -1
  40. package/dist/{mcp-3GXUBwur.js → mcp-C6IkXNOS.js} +87 -6
  41. package/dist/mcp-C6IkXNOS.js.map +1 -0
  42. package/dist/mcp.d.ts +2 -2
  43. package/dist/mcp.js +3 -2
  44. package/dist/{messages-C01kKGj0.js → messages-C_H9soi5.js} +2 -3
  45. package/dist/{messages-C01kKGj0.js.map → messages-C_H9soi5.js.map} +1 -1
  46. package/dist/{openai-compat-B3c_hzwf.js → openai-compat-CnHeo4Lz.js} +216 -3
  47. package/dist/openai-compat-CnHeo4Lz.js.map +1 -0
  48. package/dist/origin-pinned-fetch-CqG8VZIq.js +159 -0
  49. package/dist/origin-pinned-fetch-CqG8VZIq.js.map +1 -0
  50. package/dist/output/stream-json.d.ts +1 -1
  51. package/dist/output/terminal.d.ts +1 -1
  52. package/dist/plugins-2PMTVcp-.js +1174 -0
  53. package/dist/plugins-2PMTVcp-.js.map +1 -0
  54. package/dist/plugins.d.ts +2 -0
  55. package/dist/plugins.js +2 -0
  56. package/dist/presets.d.ts +1 -1
  57. package/dist/presets.js +1 -1
  58. package/dist/providers/anthropic.d.ts +1 -1
  59. package/dist/providers/anthropic.js +1 -1
  60. package/dist/providers/arcee.d.ts +1 -1
  61. package/dist/providers/arcee.js +1 -1
  62. package/dist/providers/baseten.d.ts +1 -1
  63. package/dist/providers/baseten.js +1 -1
  64. package/dist/providers/cerebras.d.ts +1 -1
  65. package/dist/providers/cerebras.js +1 -1
  66. package/dist/providers/local.d.ts +1 -1
  67. package/dist/providers/local.js +1 -1
  68. package/dist/providers/openai-compat.d.ts +1 -1
  69. package/dist/providers/openai-compat.js +1 -1
  70. package/dist/providers/openai.d.ts +1 -1
  71. package/dist/providers/openai.js +2 -2
  72. package/dist/providers/openrouter.d.ts +1 -1
  73. package/dist/providers/openrouter.js +1 -1
  74. package/dist/providers/xai.d.ts +1 -1
  75. package/dist/providers/xai.js +1 -1
  76. package/dist/{providers-z1vU0b2Q.js → providers-B6mFe7OD.js} +3 -3
  77. package/dist/{providers-z1vU0b2Q.js.map → providers-B6mFe7OD.js.map} +1 -1
  78. package/dist/providers.d.ts +1 -1
  79. package/dist/providers.js +4 -4
  80. package/dist/restate.d.ts +178 -2
  81. package/dist/restate.d.ts.map +1 -1
  82. package/dist/restate.js +463 -7
  83. package/dist/restate.js.map +1 -1
  84. package/dist/session/sqlite.d.ts +1 -1
  85. package/dist/{session-cr9VdoEO.js → session-VpLrNaFQ.js} +2 -2
  86. package/dist/{session-cr9VdoEO.js.map → session-VpLrNaFQ.js.map} +1 -1
  87. package/dist/session.d.ts +1 -1
  88. package/dist/session.js +2 -2
  89. package/dist/{resolve-CZJxds85.js → skills-Cp9vnBwd.js} +1430 -1006
  90. package/dist/skills-Cp9vnBwd.js.map +1 -0
  91. package/dist/skills.d.ts +1 -1
  92. package/dist/skills.js +1 -2
  93. package/dist/{stdio-BvL8HGYW.js → stdio-DzUUkkhS.js} +2 -2
  94. package/dist/{stdio-BvL8HGYW.js.map → stdio-DzUUkkhS.js.map} +1 -1
  95. package/dist/{stdio-loader-CjfoqBle.js → stdio-loader-BILyUGmi.js} +2 -2
  96. package/dist/{stdio-loader-CjfoqBle.js.map → stdio-loader-BILyUGmi.js.map} +1 -1
  97. package/dist/{tool-formatters-DPQjso_C.d.ts → tool-formatters-gFqQbzPY.d.ts} +2 -2
  98. package/dist/{tool-formatters-DPQjso_C.d.ts.map → tool-formatters-gFqQbzPY.d.ts.map} +1 -1
  99. package/dist/tools/fetch-url.d.ts +1 -1
  100. package/dist/tools/web-search.d.ts +1 -1
  101. package/dist/tools.d.ts +1 -1
  102. package/dist/tools.js +1 -2
  103. package/dist/{transcript-anchors-CgDzYin1.js → transcript-anchors-DEIr0tJC.js} +44 -7
  104. package/dist/transcript-anchors-DEIr0tJC.js.map +1 -0
  105. package/dist/{transcript-anchors-BCUpbNxc.d.ts → transcript-anchors-qCs3wZpu.d.ts} +98 -92
  106. package/dist/transcript-anchors-qCs3wZpu.d.ts.map +1 -0
  107. package/dist/tui.d.ts +38 -4
  108. package/dist/tui.d.ts.map +1 -1
  109. package/dist/tui.js +843 -109
  110. package/dist/tui.js.map +1 -1
  111. package/dist/{turn-operations-CAvi8NYt.js → turn-operations-C3IijXMW.js} +2 -2
  112. package/dist/{turn-operations-CAvi8NYt.js.map → turn-operations-C3IijXMW.js.map} +1 -1
  113. package/dist/{turn-operations-Bu2Hkinw.d.ts → turn-operations-M9-jUyDd.d.ts} +3 -3
  114. package/dist/{turn-operations-Bu2Hkinw.d.ts.map → turn-operations-M9-jUyDd.d.ts.map} +1 -1
  115. package/dist/types-BIarq1qE.js.map +1 -1
  116. package/dist/types.d.ts +1 -1
  117. package/dist/{xai-BPwMQSuF.js → xai-Du6Wfnlb.js} +2 -2
  118. package/dist/{xai-BPwMQSuF.js.map → xai-Du6Wfnlb.js.map} +1 -1
  119. package/docs/PLUGINS.md +340 -0
  120. package/docs/RESTATE.md +168 -3
  121. package/docs/SKILL.md +24 -8
  122. package/package.json +6 -1
  123. package/dist/agent-8sUadp5M.js.map +0 -1
  124. package/dist/aliasing-lVFbm81n.js +0 -217
  125. package/dist/aliasing-lVFbm81n.js.map +0 -1
  126. package/dist/context-breakdown-Dzo25N45.js +0 -155
  127. package/dist/context-breakdown-Dzo25N45.js.map +0 -1
  128. package/dist/edit-utils-EGosADZq.js.map +0 -1
  129. package/dist/index-C-Nd2ZbV.d.ts.map +0 -1
  130. package/dist/mcp-3GXUBwur.js.map +0 -1
  131. package/dist/openai-compat-B3c_hzwf.js.map +0 -1
  132. package/dist/resolve-CZJxds85.js.map +0 -1
  133. package/dist/skills-Dd6XCi4s.js +0 -420
  134. package/dist/skills-Dd6XCi4s.js.map +0 -1
  135. package/dist/tool-state-DTs-3ZHH.js +0 -653
  136. package/dist/tool-state-DTs-3ZHH.js.map +0 -1
  137. package/dist/transcript-anchors-BCUpbNxc.d.ts.map +0 -1
  138. package/dist/transcript-anchors-CgDzYin1.js.map +0 -1
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { d as ClassifiedErrorKind, i as AgentContextExceededError, l as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, n as AgentAbortedError, o as AgentProviderError, p as matchesContextExceeded, r as AgentBudgetExceededError, s as AgentToolNotAllowedError, u as ClassifiedError } from "./timeout-DEaqslLO.js";
2
- import { $m as ToolDef, $p as DEFAULT_AGENT_CLOCK, Ah as SessionRun, As as SpawnToolOptions, Bm as ToolResultDocumentContent, C as SuccessfulMutationOutcome, Cm as RunHookMap, Dh as Session, Dm as SessionMessage, Em as SessionHookContext, Gi as AgentHookMap, Gm as ToolResultTextContent, Hm as ToolResultImageContent, Hp as AgentClock, Im as ToolHookContext, Jm as TurnUsage, Jp as ChildRunStats, Kf as OpenAIParams, Ki as AgentHooks, Km as ToolResultVideoContent, Lm as ToolOutcome, Mm as StreamHookContext, Oh as SessionData, Om as SessionTurn, Pm as ThinkingLevel, Qm as ToolContext, Rm as ToolResultAudioContent, Rr as InteractionToolOptions, S as MutationValidationResult, Sf as OpenRouterParams, Sh as SkillResource, Sm as RetryConfig, Th as CreateSessionOptions, Tm as SessionEndStatus, Ts as ChildAgent, Um as ToolResultImageRefContent, Up as AgentRunOptions, Vh as RemoteStoreOptions, Vm as ToolResultDocumentRefContent, Vp as AgentBehavior, Wi as Agent, Wm as ToolResultMetadata, Wp as AgentStats, Xm as toolOutputByteLength, Ym as toolOutputBudgetByteLength, Zm as toolResultToText, _ as statsByModel, _m as PromptVideoPart, af as StreamCallbacks, am as McpToolHookContext, b as MutationValidationHooksOptions, bg as McpConnection, bh as SkillConfig, bm as ResolveContentRef, cf as ToolCall, df as TurnResult, dh as StagedToolState, dm as ProgressNudgeMessage, eh as ToolMap, fh as ToolStateController, fm as PromptAudioPart, gm as PromptTextPart, h as flattenTurns, hh as ToolStateView, hm as PromptPart, if as ProviderCapabilities, im as McpServerConfig, ip as CerebrasParams, jh as SessionStore, js as SpawnToolState, km as SpawnHookContext, lf as ToolResult, lm as OAuthRefreshHookContext, mh as ToolStateStore, mm as PromptImagePart, nh as ReadStateEntry, of as StreamOptions, p as ModelUsage, ph as ToolStateOp, pm as PromptDocumentPart, pp as AnthropicParams, qi as AgentOptions, qm as TurnFinishReason, rf as Provider, rh as ReadStateMap, t as Preset, uf as ToolSpec, um as ProgressNudgeKind, v as MutationValidationBatch, vm as RepeatGuardConfig, vr as ValidationResult, wh as SkillsConfig, wm as SessionContentBlock, x as MutationValidationMutation, xm as ResolveContentRefBlock, y as MutationValidationFailure, ym as RepeatGuardToolMatcher, zm as ToolResultContent } from "./index-C-Nd2ZbV.js";
2
+ import { $h as SkillsConfig, $m as SessionContentBlock, Bf as TurnResult, Bh as StagedToolState, Bm as ProgressNudgeMessage, C as SuccessfulMutationOutcome, Ch as TurnUsage, Cm as ChildRunStats, Dh as ToolContext, Eh as toolResultToText, Ff as StreamOptions, Gi as AgentHookMap, Gm as PromptTextPart, Hh as ToolStateOp, Hm as PromptDocumentPart, Hp as AnthropicParams, Jm as RepeatGuardToolMatcher, Ki as AgentHooks, Km as PromptVideoPart, Lf as ToolCall, Mf as Provider, Mh as ReadStateMap, Nf as ProviderCapabilities, Nm as McpServerConfig, Np as CerebrasParams, Ns as SpawnToolOptions, Oh as ToolDef, Om as DEFAULT_AGENT_CLOCK, Os as ChildAgent, Pf as StreamCallbacks, Pm as McpToolHookContext, Ps as SpawnToolState, Qm as RunHookMap, Rf as ToolResult, Rm as OAuthRefreshHookContext, Rr as InteractionToolOptions, S as MutationValidationResult, Sh as TurnFinishReason, Th as toolOutputByteLength, Uh as ToolStateStore, Um as PromptImagePart, Vh as ToolStateController, Vm as PromptAudioPart, Wh as ToolStateView, Wi as Agent, Wm as PromptPart, Xm as ResolveContentRefBlock, Yg as McpConnection, Yh as SkillConfig, Ym as ResolveContentRef, Zf as OpenRouterParams, Zh as SkillResource, Zm as RetryConfig, _ as statsByModel, _h as ToolResultImageContent, _m as AgentClock, ag as SessionRun, b as MutationValidationHooksOptions, bh as ToolResultTextContent, dh as ToolHookContext, eg as CreateSessionOptions, eh as SessionEndStatus, fh as ToolOutcome, gg as RemoteStoreOptions, gh as ToolResultDocumentRefContent, gm as AgentBehavior, h as flattenTurns, hh as ToolResultDocumentContent, ih as SpawnHookContext, jh as ReadStateEntry, kh as ToolMap, lh as ThinkingLevel, mh as ToolResultContent, ng as Session, nh as SessionMessage, og as SessionStore, p as ModelUsage, ph as ToolResultAudioContent, qi as AgentOptions, qm as RepeatGuardConfig, rg as SessionData, rh as SessionTurn, sh as StreamHookContext, t as Preset, th as SessionHookContext, v as MutationValidationBatch, vh as ToolResultImageRefContent, vm as AgentRunOptions, vr as ValidationResult, wh as toolOutputBudgetByteLength, x as MutationValidationMutation, xh as ToolResultVideoContent, xp as OpenAIParams, y as MutationValidationFailure, yh as ToolResultMetadata, ym as AgentStats, zf as ToolSpec, zm as ProgressNudgeKind } from "./index-CWBSrqI6.js";
3
3
  import { _ as TaskHandle, a as ContextType, c as ExecutionContext, g as TaskExitInfo, h as TaskEntry, l as ExecutionHandle, n as ContextCapabilities, o as DetachedTasksCapability, p as SpawnConfig, r as ContextHardening, s as ExecResult, t as BackgroundTaskStatus, v as TaskStallInfo } from "./types-M2DauK2D.js";
4
4
  import { t as SandboxProvider } from "./sandbox-DLY6uah7.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 StagedToolState, 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 ToolStateController, type ToolStateOp, type ToolStateStore, type ToolStateView, 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-B3c_hzwf.js";
1
+ import { m as openaiCompat } from "./openai-compat-CnHeo4Lz.js";
2
2
  import { i as isOAuthAccessToken, r as extractRuntimeCredentials, s as resolveOAuthApiKey } from "./oauth-bkEOybqm.js";
3
3
  import { createServer } from "node:http";
4
4
  //#region src/chat/oauth-page/pkce.ts
@@ -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-BPwMQSuF.js.map
379
+ //# sourceMappingURL=xai-Du6Wfnlb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"xai-BPwMQSuF.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} from '@earendil-works/pi-ai/oauth'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\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-Du6Wfnlb.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} from '@earendil-works/pi-ai/oauth'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { OAuthProviderInterface } from '../../providers/oauth-registry'\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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"}
@@ -0,0 +1,340 @@
1
+ # Agent Plugins
2
+
3
+ Support for [Agent Plugins](https://agent-plugins.org) 1.0.0 — an open, vendor-neutral package format for reusable agent components. A plugin is a directory with a manifest and its components at fixed locations; any conformant client can load it.
4
+
5
+ ```
6
+ my-plugin/
7
+ ├── plugin.json manifest (required)
8
+ ├── skills/ one directory per Agent Skill
9
+ │ └── deploy/
10
+ │ └── SKILL.md
11
+ └── mcp.json MCP server configuration
12
+ ```
13
+
14
+ ```ts
15
+ import { loadAgentPlugin } from 'zidane/plugins'
16
+
17
+ const plugin = await loadAgentPlugin('./my-plugin')
18
+ if (plugin.ok) {
19
+ console.log(plugin.manifest.name, plugin.skills.length, plugin.mcpServers.length)
20
+ }
21
+ ```
22
+
23
+ Agent Plugins deliberately does **not** redefine either component format — skills are Agent Skills, MCP config is MCP config. So `zidane/plugins` owns only the packaging contract and hands back plain `SkillConfig[]` / `McpServerConfig[]` that every other zidane surface already consumes.
24
+
25
+ ## Why a separate loader
26
+
27
+ zidane already reads both component types, but both existing readers are deliberately *lenient*: `discoverSkills` recurses into nested category directories, and `normalizeMcpServers` infers a transport from the presence of `command` / `url` and accepts `httpUrl` / `sseUrl` / `type: "http"` aliases. That leniency is correct for a file the user wrote, and wrong for a third-party package — guessing on behalf of a stranger's `mcp.json` is how you spawn a command the author never wrote.
28
+
29
+ `zidane/plugins` is the strict layer: closed schemas, immediate-children-only discovery, and a filesystem boundary. It never loosens the existing readers; it wraps them.
30
+
31
+ ## Discovery
32
+
33
+ Plugins are found in four locations, in precedence order — the same project/user pair every other zidane discovery surface uses:
34
+
35
+ 1. `{project}/.agents/plugins/`
36
+ 2. `{project}/.zidane/plugins/`
37
+ 3. `~/.agents/plugins/`
38
+ 4. `~/.zidane/plugins/`
39
+
40
+ Each **immediate child directory** is one plugin. Nothing is searched below that, and a child with no `plugin.json` is silently ignored — dropping a README next to your plugins is not an error.
41
+
42
+ `.agents/plugins/` is the portable location: Agent Plugins is vendor-neutral, so there is no foreign-agent or Ruler import variant the way there is for skills and MCP servers.
43
+
44
+ Identity is the **manifest** name, not the directory name. Two directories called `tools/` shipping different plugins are two plugins; the same plugin vendored at two scopes is one, and the project copy wins.
45
+
46
+ Discovered plugins are enabled by default and gated by `enabledPlugins` in settings. A plugin that fails to load still appears — as a read-only row explaining why — rather than vanishing.
47
+
48
+ ### Extra roots for development
49
+
50
+ `ZIDANE_PLUGIN_DIRS` adds scan roots, mirroring `ZIDANE_EXTENSION_DIRS`:
51
+
52
+ ```sh
53
+ ZIDANE_PLUGIN_DIRS=/repo/plugins zidane
54
+ ```
55
+
56
+ Each entry is a **root** whose immediate children are plugins (`<root>/<name>/plugin.json`), not a single plugin directory. Multiple roots use the platform path delimiter.
57
+
58
+ This exists because all four default roots are dot-directories and `.zidane/` is gitignored — so a repo has nowhere *tracked* to keep example or first-party plugins. Extra roots are appended **after** the four defaults and scoped `project`, so a real project or user plugin always wins a name collision against a dev root.
59
+
60
+ This repo uses it to dogfood both load paths in `npm run tui`:
61
+
62
+ | Plugin | Route | Origin shown |
63
+ | --- | --- | --- |
64
+ | `plugins/hello-plugin` | disk, via `ZIDANE_PLUGIN_DIRS=../plugins` | `[project: plugins/hello-plugin]` |
65
+ | `extensions/plugin-host/vendor/vendored-plugin` | `ctx.loadAgentPlugin` | `[via extension: plugin-host]` |
66
+
67
+ Their skills (`plugin-smoke-test`, `vendored-smoke-test`) double as the manual verification scripts for plugin loading.
68
+
69
+ ### Project-scope trust
70
+
71
+ A project plugin's stdio MCP servers run, the same as those in a project `.zidane/mcps.json` beside them. There is no extra gate, deliberately: holding plugins to a stricter rule than the MCP config sitting next to them would be inconsistent rather than safer, since both are things you or your repo placed on purpose.
72
+
73
+ Stated plainly, the exposure that comes with that: a cloned repo carrying `.agents/plugins/evil/mcp.json` gets its stdio command spawned on the first agent bootstrap. That is today's behavior for project `mcps.json` too. If you don't want it, disable the plugin in settings, or set `enabledPlugins` to an allowlist that omits it.
74
+
75
+ (The `dropStdio` carve-out in `chat/mcps-discovery.ts` is narrower than it looks — it applies to **Ruler** files specifically, because `importRulerConfigs` defaults on and would otherwise execute a repo-committed foreign config unprompted.)
76
+
77
+ ## Loading a plugin
78
+
79
+ ### From an extension
80
+
81
+ The primary entry point. One call registers everything the plugin ships:
82
+
83
+ ```ts
84
+ import { defineExtension } from 'zidane'
85
+
86
+ export default defineExtension({
87
+ name: 'my-toolkit',
88
+ capabilities: ['plugins'],
89
+ async setup(ctx) {
90
+ const plugin = await ctx.loadAgentPlugin('./vendor/deploy-tools')
91
+ if (!plugin.ok)
92
+ ctx.logger.warn('deploy-tools failed to load')
93
+ },
94
+ })
95
+ ```
96
+
97
+ `dir` may be absolute or relative to the extension's own directory, so an extension can vendor plugins inside itself. Inline extensions (no directory) resolve against `ctx.paths.cwd`.
98
+
99
+ Options, when you need them:
100
+
101
+ ```ts
102
+ await ctx.loadAgentPlugin({
103
+ dir: './vendor/deploy-tools',
104
+ skills: true, // register skills/ (default true)
105
+ mcp: true, // register mcp.json servers (default true)
106
+ namePrefix: 'acme', // rename to `acme-<name>` (default none)
107
+ })
108
+ ```
109
+
110
+ Three things worth knowing:
111
+
112
+ - **It never throws for plugin-level problems.** A malformed manifest, an escaping symlink or an invalid skill comes back as `ok: false` / diagnostics and is logged to the extension's logger. One bad package cannot fail your `setup()`.
113
+ - **Await it inside `setup()`.** Registration closes when `setup()` resolves. A load still in flight at that point is dropped with a warning rather than mutating an already-published registry.
114
+ - **Components inherit the extension's scope.** A project-scoped extension's plugin is project-scoped, exactly as `ctx.registerSkill` behaves.
115
+
116
+ ### From the SDK
117
+
118
+ ```ts
119
+ import { loadAgentPlugin } from 'zidane/plugins'
120
+
121
+ const plugin = await loadAgentPlugin(dir, {
122
+ source: 'user', // scope stamped on discovered skills (default 'project')
123
+ skills: true, // discover skills/ (default true)
124
+ mcp: true, // discover mcp.json (default true)
125
+ namePrefix: 'acme', // rename to `acme-<name>` (default none)
126
+ dataRoot: '/custom/parent', // ${PLUGIN_DATA} parent; name is appended
127
+ dataDir: '/exact/dir', // or pin one plugin's directory outright
128
+ })
129
+ ```
130
+
131
+ ## `plugin.json`
132
+
133
+ The manifest is a **closed** schema, validated locally — the spec forbids retrieving a schema while loading. `$schema` and `name` are required; everything else is optional metadata.
134
+
135
+ ```json
136
+ {
137
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
138
+ "name": "deploy-tools",
139
+ "version": "1.2.0",
140
+ "description": "Deployment skills and a release MCP server",
141
+ "author": { "name": "Acme", "email": "dev@acme.example" },
142
+ "homepage": "https://acme.example",
143
+ "repository": "https://github.com/acme/deploy-tools",
144
+ "license": "MIT",
145
+ "keywords": ["deploy", "release"],
146
+ "extensions": { "dev.example": { "anything": true } }
147
+ }
148
+ ```
149
+
150
+ `name` is 1–64 characters: lowercase letters, digits, `-` and `.`, alphanumeric at both ends, no `--` or `..`.
151
+
152
+ Metadata strings are type-checked only. A `license` that isn't valid SPDX, or a `homepage` that isn't a valid URL, does **not** reject the plugin — the spec keeps identity portable and leaves semantic policing to registries.
153
+
154
+ Exactly two violations are non-fatal, per spec:
155
+
156
+ | Violation | Behavior |
157
+ |---|---|
158
+ | Unknown top-level field | Reported, ignored, loading continues |
159
+ | `extensions` is not an object | Reported, ignored, loading continues |
160
+
161
+ Every other schema violation **rejects the whole plugin**, and rejection happens *before* component discovery — a plugin with a broken manifest never has its skills read or its servers spawned.
162
+
163
+ `extensions` member *values* are never validated. The spec says to ignore namespaces the client does not implement without inspecting them, so a client validates only its own namespace at the point of use.
164
+
165
+ ## `skills/`
166
+
167
+ Each immediate child directory containing a `SKILL.md` that resolves to a regular file is one skill. The format is plain [Agent Skills](./SKILL.md) — same parser as `~/.agents/skills`.
168
+
169
+ Nested descendants are **not** searched. `skills/category/nested/SKILL.md` is invisible, even though zidane's own skill discovery would find it. Bundling a non-skill directory under `skills/` is legal and silently ignored.
170
+
171
+ Every discovered skill is stamped with `metadata['zidane.plugin']` naming the plugin that shipped it.
172
+
173
+ ## `mcp.json`
174
+
175
+ ```json
176
+ {
177
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
178
+ "mcpServers": {
179
+ "release": {
180
+ "type": "stdio",
181
+ "command": "uvx",
182
+ "args": ["release-mcp", "--root", "${PLUGIN_ROOT}"],
183
+ "env": { "CACHE_DIR": "${PLUGIN_DATA}/cache" },
184
+ "cwd": "${PLUGIN_ROOT}"
185
+ },
186
+ "registry": {
187
+ "type": "streamable-http",
188
+ "url": "https://mcp.acme.example/v1",
189
+ "headers": { "X-Client": "zidane" }
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ `$schema` must declare the **same** Agent Plugins version as `plugin.json`.
196
+
197
+ `type` is required and never inferred. Each transport has a closed key set:
198
+
199
+ | `type` | Permitted keys |
200
+ |---|---|
201
+ | `stdio` | `type`, `command`, `args`, `env`, `cwd` |
202
+ | `streamable-http` | `type`, `url`, `headers` |
203
+ | `sse` | `type`, `url`, `headers` |
204
+
205
+ An unknown field, an unknown transport, or a missing required field invalidates **that one entry**; its siblings still load. A malformed document, a version mismatch, or an unknown top-level field disables the MCP component for that plugin only — its skills are unaffected.
206
+
207
+ ### Plugin variables
208
+
209
+ Two placeholders, and only two:
210
+
211
+ | Variable | Value |
212
+ |---|---|
213
+ | `${PLUGIN_ROOT}` | Symlink-resolved plugin root |
214
+ | `${PLUGIN_DATA}` | Dedicated writable directory, persistent across plugin updates |
215
+
216
+ They expand in `args`, `env` values and `cwd` — **nowhere else**. In `command`, `url` and `headers` the text stays literal, so a `${PLUGIN_ROOT}` in `command` is rejected rather than handed to the OS verbatim.
217
+
218
+ Expansion is single-pass and non-recursive: an expanded value containing `${PLUGIN_DATA}` does not expand again. Unknown `${...}` text is left alone — zidane's wider `${VAR}` / `${VAR:-default}` / `${env:VAR}` handling does **not** apply to plugin configs (they carry `preExpanded`, which suppresses it). That is deliberate: it keeps an ambient `PLUGIN_ROOT` environment variable from reaching fields the spec says must stay literal.
219
+
220
+ Both variables are always present in a stdio server's `env`, written last, and a plugin that tries to set either itself has that entry rejected.
221
+
222
+ `${PLUGIN_DATA}` defaults to `<dataDir>/plugin-data/<plugin-name>/` from `resolveStorageDirs` (honouring `ZIDANE_STORAGE_DIR` and XDG). It sits **outside** the plugin root so a plugin update — which replaces the package tree — cannot wipe it. Extension-loaded plugins get it inside the loading extension's storage root instead, so two extensions loading the same plugin don't share state.
223
+
224
+ The directory is created at **spawn** time, not load time. Loading is not consent: a host has to load every plugin it finds just to learn its name (identity lives in the manifest, so it can't filter first), and `enabledPlugins` only applies afterwards — creating on load left a directory behind for every plugin the user had switched off, on every discovery pass. A `cwd` inside `${PLUGIN_DATA}` is created along with it, since spawning into a non-existent directory fails with a bare `ENOENT`; a `cwd` under `${PLUGIN_ROOT}` is not, because that path is package content the author was supposed to ship.
225
+
226
+ Note the honest consequence: `ZIDANE_PREFIX` is *not* read by `resolveStorageDirs` — callers thread `prefix` explicitly, which disk discovery does.
227
+
228
+ ### stdio commands
229
+
230
+ `command` is exactly one executable token — no shell, no argv splitting:
231
+
232
+ - **Bare name** (`uvx`, `node`) → PATH lookup by the OS.
233
+ - **`./`-relative** → resolved inside the plugin root, and it must exist.
234
+ - **Absolute** → rejected. It is neither a PATH lookup nor a package file.
235
+
236
+ `cwd` defaults to the plugin root and must stay inside the root or the data directory, checked both lexically and after symlink resolution.
237
+
238
+ ### Remote servers
239
+
240
+ - HTTPS required unless the host is loopback.
241
+ - No userinfo credentials, no fragment.
242
+ - Configured headers are pinned to the URL's origin: redirects are followed manually and credentials are dropped the moment a hop leaves that origin. `authorization` is always protected, including the header the SDK's OAuth provider injects rather than reading from config.
243
+ - A redirect to non-loopback cleartext is refused outright, not merely stripped.
244
+
245
+ ## The package boundary
246
+
247
+ Every package file must stay inside the symlink-resolved plugin root. Lexical checks are not enough — a `skills` symlink pointing at `~/.ssh` passes every one of them — so each path is resolved through `realpath` and re-tested, and required filesystem kinds are asserted.
248
+
249
+ Failures are isolated at the narrowest level that still holds:
250
+
251
+ | Problem | Consequence |
252
+ |---|---|
253
+ | Invalid manifest, or manifest outside the root | Whole plugin rejected, no components discovered |
254
+ | `skills/` or `mcp.json` invalid, wrong kind, or outside the root | That component type disabled; the other still loads |
255
+ | One skill invalid, unreadable, or outside the root | That skill skipped; siblings load |
256
+ | One `mcp.json` entry invalid | That server skipped; siblings load |
257
+ | Any other escaping path | Access denied for that path |
258
+
259
+ A **missing** fixed location is not an error. `components.skills === 'absent'` means the plugin ships no skills; `'invalid'` means they were rejected.
260
+
261
+ ## Results and diagnostics
262
+
263
+ ```ts
264
+ interface LoadedPlugin {
265
+ ok: boolean // false ⇒ plugin rejected, components never discovered
266
+ dir: string // root as requested, made absolute
267
+ root: string // symlink-resolved root — the boundary
268
+ manifest?: PluginManifest // absent iff !ok
269
+ skills: SkillConfig[]
270
+ mcpServers: McpServerConfig[]
271
+ components: {
272
+ skills: 'absent' | 'loaded' | 'invalid' | 'unsupported'
273
+ mcp: 'absent' | 'loaded' | 'invalid' | 'unsupported'
274
+ }
275
+ diagnostics: PluginDiagnostic[]
276
+ }
277
+ ```
278
+
279
+ Each diagnostic carries a `severity`, a stable machine-readable `code` (`manifest-unknown-field`, `skill-escapes-root`, `mcp-entry-transport-invalid`, …), a message, and optionally the `field` or `path` involved. `severity: 'error'` on a plugin that still returned `ok: true` means a narrow boundary fired, not that the plugin failed.
280
+
281
+ ## Naming and collisions
282
+
283
+ zidane's skill catalog and MCP server names are single flat namespaces, so two plugins each shipping a `deploy` skill or a `github` server would collide. The spec leaves the policy to the client; zidane's default is first-found-wins with a collision diagnostic, plus opt-in `namePrefix` for the cases where you know you're loading several.
284
+
285
+ Server names are validated in the form they actually take in the tool catalog — `mcp_<server>_<tool>` — which means *after* `namePrefix` is applied, since a bad prefix can invalidate a name the plugin got right.
286
+
287
+ Collisions are resolved at the merge, not at load, so a plugin's settings row counts what the **package ships** rather than what ended up live. A plugin server whose name matches one in `mcps.json` is dropped (first-wins, `mcps.json` first), and a plugin skill whose name matches an extension's is shadowed. The MCPs and Skills tabs are the authority on what actually loaded; skill collisions emit `name-collision-shadowed`, server collisions are logged under `ZIDANE_DEBUG`. Pass `namePrefix` when loading plugins that might collide.
288
+
289
+ ### The same plugin from both routes
290
+
291
+ A plugin present *both* on disk and vendored inside an extension is **one** plugin: the disk copy wins, and the extension's copy contributes nothing. One settings row, one checkbox, one set of skills and servers.
292
+
293
+ This has to hold on the contribution side, not just the display side. If both copies stayed live, that single checkbox would govern two of them, and the skills are the sharp end: both sets reach the catalog, where one shadows the other and earns a `name-collision-shadowed` warning about a collision the user never created.
294
+
295
+ The MCP half is the same shape but milder — both configs reach the merge, where a first-wins dedupe by name collapses them, so nothing is spawned twice. What's wrong there is that one checkbox nominally governs two records, and which copy survives is decided by merge order rather than by anything the user chose.
296
+
297
+ Disk wins even when the disk copy is *rejected*, so a broken installed copy doesn't quietly fall back to a vendored one while the row reads `rejected`.
298
+
299
+ ## Settings
300
+
301
+ Plugins live in the **Extensions & Plugins** tab, interleaved alphabetically with extensions rather than in a separate section. They aren't parallel categories — a plugin can be loaded *by* an extension — so each row carries a kind badge and an origin instead:
302
+
303
+ ```
304
+ [✓] git-flow v2.1.0 ext
305
+ Git helpers · [project: .zidane/extensions/git-flow]
306
+ [✓] deploy-tools v1.2.0 plugin
307
+ Deploy skills · [project: .agents/plugins/deploy-tools] · 3 skills, 1 server
308
+ [✓] registry-mcp v0.4.0 plugin
309
+ Registry access · [via extension: my-toolkit] · 1 server
310
+ ✗ broken-thing plugin
311
+ rejected: manifest-name-invalid · [user: ~/.agents/plugins/broken-thing]
312
+ ```
313
+
314
+ The kind badge is a filled pill trailing the title line, so names stay left-aligned instead of being pushed right by a fixed-width column. Extensions get the neutral `border` tier because they're the majority — a saturated pill on nearly every row is noise — and plugins get the `model` accent so they stand out of that field.
315
+
316
+ Both colors derive from `ThemeColors` / `ThemeSurfaces` members every theme must define, rather than from `surfaces.chips` entries: that map is keyed by completion-provider id, and a third-party theme that never opted in would fall back to `chips.default` for both kinds, collapsing the very distinction the badge exists to draw.
317
+
318
+ Errored extensions and rejected plugins lead the list. A rejected plugin has no checkbox — there is nothing to enable — matching how an errored extension renders.
319
+
320
+ Searching matches a plugin's name, description and origin, plus the literal word `plugin`: type `plugin` to narrow to plugins, or an extension's name to find the plugins it loaded.
321
+
322
+ `ctrl+r` on this tab re-scans the plugin directories, so a package you just dropped into `.agents/plugins/` appears without a restart. It refreshes **plugins only** — an extension's `setup()` runs once per process, so extensions genuinely cannot be re-scanned in place, and calling it a generic refresh would over-promise. Extension-loaded plugins likewise only reload with their extension.
323
+
324
+ Toggling writes `enabledPlugins`, which gates the plugin's skills **and** its MCP servers together — for disk plugins and for extension-loaded ones alike. Extension-loaded plugins need a second filter pass to achieve that (`filterRegistryPlugins`), because their contributions are fanned into the loading extension's own sinks and `enabledExtensions` can only express whole-extension on/off.
325
+
326
+ Skills from an *enabled* plugin are force-included into `enabledSkills` when you have an allowlist there — enabling the plugin is the consent, and `enabledPlugins` is where you withhold it.
327
+
328
+ The gate is plugin-level only. A plugin's skills **are** listed in the Skills tab, badged `plugin:<name>`, but they carry no checkbox: the agent build force-includes every enabled plugin's skills, so a per-skill "off" would be persisted, flip the box, and then be overridden on the next build. Rather than ship a control whose off state doesn't hold, the row is read-only and the badge points at the plugin row that owns the switch. Turning off one skill of a plugin means disabling the plugin.
329
+
330
+ Extension-registered skills work the same way, badged `ext:<name>` and gated by `enabledExtensions`.
331
+
332
+ Real per-skill control would mean dropping that force-include and seeding contributed skill names into `enabledSkills` at enable time — a persistence change, not a rendering one.
333
+
334
+ ## Conformance
335
+
336
+ zidane implements Agent Plugins 1.0.0: manifest validation, both component types, both required MCP transports plus legacy HTTP+SSE, plugin variables, and the failure-isolation ladder. Supported versions are listed in `SUPPORTED_AGENT_PLUGINS_VERSIONS`; a plugin targeting anything else is rejected with a diagnostic rather than loaded on a guess.
337
+
338
+ `test/plugins/conformance.test.ts` maps 1:1 to the [published checklist](https://agent-plugins.org/client-implementers/conformance) — one test per line, same order, same headings. Behavioral depth lives in the sibling suites; that file exists so conformance can be verified item by item instead of inferred from three test files. A checklist line with no test there is a claim nobody checked.
339
+
340
+ Not yet implemented: installing a plugin from a git/npm spec (`/plugin install`). Drop a package into one of the discovery directories, load it from an extension, or call `loadAgentPlugin` directly.