zidane 5.13.26 → 5.14.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.
- package/dist/{acp-CjAoILwj.js → acp-DgKEJH3P.js} +7 -7
- package/dist/{acp-CjAoILwj.js.map → acp-DgKEJH3P.js.map} +1 -1
- package/dist/acp-cli.js +11 -6
- package/dist/acp-cli.js.map +1 -1
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +1 -1
- package/dist/{tools-B32lZXLE.js → agent-B-cKanSC.js} +8035 -9459
- package/dist/agent-B-cKanSC.js.map +1 -0
- package/dist/{agent-DQIL5BFk.d.ts → agent-BAgOgS37.d.ts} +367 -208
- package/dist/agent-BAgOgS37.d.ts.map +1 -0
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +5 -0
- package/dist/anthropic-RGqsJlC8.js +807 -0
- package/dist/anthropic-RGqsJlC8.js.map +1 -0
- package/dist/{atomic-write-Bgtr5JPu.js → atomic-write-B7zr1ddF.js} +2 -2
- package/dist/{atomic-write-Bgtr5JPu.js.map → atomic-write-B7zr1ddF.js.map} +1 -1
- package/dist/{auth-DPrSP1YV.js → auth-C-ms5N2x.js} +33 -5
- package/dist/{auth-DPrSP1YV.js.map → auth-C-ms5N2x.js.map} +1 -1
- package/dist/cache-telemetry-BKAwsNxm.js +169 -0
- package/dist/cache-telemetry-BKAwsNxm.js.map +1 -0
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat/pure.js +1 -1
- package/dist/chat.d.ts +21 -7
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +30 -5
- package/dist/chat.js.map +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/docker.js +0 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/errors-CkTvg97v.d.ts +203 -0
- package/dist/errors-CkTvg97v.d.ts.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +3 -0
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +2 -2
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +406 -1
- package/dist/headless.js.map +1 -0
- package/dist/{index-YY3vp2F2.d.ts → index-CLuLFFLa.d.ts} +88 -418
- package/dist/index-CLuLFFLa.d.ts.map +1 -0
- package/dist/{index-C5qw3pj7.d.ts → index-CgBIl2mX.d.ts} +2 -2
- package/dist/{index-C5qw3pj7.d.ts.map → index-CgBIl2mX.d.ts.map} +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +113 -167
- package/dist/index.js.map +1 -1
- package/dist/{logger-C2ngE8CY.d.ts → logger-CJ-lpKPy.d.ts} +2 -2
- package/dist/{logger-C2ngE8CY.d.ts.map → logger-CJ-lpKPy.d.ts.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/messages-YZQLKaz2.js +1157 -0
- package/dist/messages-YZQLKaz2.js.map +1 -0
- package/dist/oauth-8LqbeI_Q.js +292 -0
- package/dist/oauth-8LqbeI_Q.js.map +1 -0
- package/dist/{messages-Iy_vEycX.js → openai-compat-0Y7jcncn.js} +27 -1379
- package/dist/openai-compat-0Y7jcncn.js.map +1 -0
- package/dist/output/stream-json.d.ts +2 -2
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +2 -2
- package/dist/{presets-B9nDwmaz.js → presets-Pr02ZNcy.js} +3 -2
- package/dist/{presets-B9nDwmaz.js.map → presets-Pr02ZNcy.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +210 -0
- package/dist/prompt.js.map +1 -0
- package/dist/providers/anthropic.d.ts +2 -0
- package/dist/providers/anthropic.js +2 -0
- package/dist/providers/arcee.d.ts +2 -0
- package/dist/providers/arcee.js +42 -0
- package/dist/providers/arcee.js.map +1 -0
- package/dist/providers/baseten.d.ts +2 -0
- package/dist/providers/baseten.js +66 -0
- package/dist/providers/baseten.js.map +1 -0
- package/dist/providers/cerebras.d.ts +2 -0
- package/dist/providers/cerebras.js +31 -0
- package/dist/providers/cerebras.js.map +1 -0
- package/dist/providers/local.d.ts +2 -0
- package/dist/providers/local.js +44 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/providers/openai-compat.d.ts +2 -0
- package/dist/providers/openai-compat.js +2 -0
- package/dist/providers/openai.d.ts +2 -0
- package/dist/providers/openai.js +504 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +2 -0
- package/dist/providers/openrouter.js +52 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/xai.d.ts +2 -0
- package/dist/providers/xai.js +2 -0
- package/dist/providers-DgmdYGKm.js +168 -0
- package/dist/providers-DgmdYGKm.js.map +1 -0
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +10 -2
- package/dist/restate.d.ts +1 -1
- package/dist/run-summary-DAnBZnrt.d.ts +249 -0
- package/dist/run-summary-DAnBZnrt.d.ts.map +1 -0
- package/dist/run-summary-nD3IQ4ZQ.js +253 -0
- package/dist/run-summary-nD3IQ4ZQ.js.map +1 -0
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-CiA1_8SB.js → session-CqrAFAKJ.js} +2 -2
- package/dist/{session-CiA1_8SB.js.map → session-CqrAFAKJ.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/system-prompt-BlsdjcUi.d.ts +139 -0
- package/dist/system-prompt-BlsdjcUi.d.ts.map +1 -0
- package/dist/timeout-CpFm0jJ6.d.ts +10 -0
- package/dist/timeout-CpFm0jJ6.d.ts.map +1 -0
- package/dist/timeout-kGGSXagK.js +84 -0
- package/dist/timeout-kGGSXagK.js.map +1 -0
- package/dist/{tool-formatters-w5oaoCS4.d.ts → tool-formatters-Ch8oRpYR.d.ts} +2 -2
- package/dist/{tool-formatters-w5oaoCS4.d.ts.map → tool-formatters-Ch8oRpYR.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools-00_Nqazv.js +1689 -0
- package/dist/tools-00_Nqazv.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +2 -1
- package/dist/{transcript-anchors-BUtOd-qa.js → transcript-anchors-BUmvPnis.js} +10 -9
- package/dist/{transcript-anchors-BUtOd-qa.js.map → transcript-anchors-BUmvPnis.js.map} +1 -1
- package/dist/{transcript-anchors-BJ-ESWnm.d.ts → transcript-anchors-CL9UM_M4.d.ts} +4 -4
- package/dist/{transcript-anchors-BJ-ESWnm.d.ts.map → transcript-anchors-CL9UM_M4.d.ts.map} +1 -1
- package/dist/tui.d.ts +3 -3
- package/dist/tui.js +38 -13
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-BSSQHNq1.d.ts → turn-operations-Cj6v_ypW.d.ts} +3 -3
- package/dist/{turn-operations-BSSQHNq1.d.ts.map → turn-operations-Cj6v_ypW.d.ts.map} +1 -1
- package/dist/{turn-operations-B_XeuNp-.js → turn-operations-tG0vuBNc.js} +2 -2
- package/dist/{turn-operations-B_XeuNp-.js.map → turn-operations-tG0vuBNc.js.map} +1 -1
- package/dist/types-CyVGdbia.js.map +1 -1
- package/dist/types.d.ts +4 -3
- package/dist/xai-tPdbAGkq.js +379 -0
- package/dist/xai-tPdbAGkq.js.map +1 -0
- package/docs/ARCHITECTURE.md +4 -2
- package/docs/CHAT.md +4 -0
- package/docs/SKILL.md +31 -5
- package/package.json +63 -1
- package/dist/agent-DQIL5BFk.d.ts.map +0 -1
- package/dist/headless-2SH7dvoW.js +0 -656
- package/dist/headless-2SH7dvoW.js.map +0 -1
- package/dist/index-YY3vp2F2.d.ts.map +0 -1
- package/dist/messages-Iy_vEycX.js.map +0 -1
- package/dist/providers-BMw1D_lj.js +0 -2332
- package/dist/providers-BMw1D_lj.js.map +0 -1
- package/dist/tools-B32lZXLE.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { $n as
|
|
1
|
+
import { $n as PromptDocumentPart, Ar as TurnUsage, At as TurnResult, Cr as ToolResultDocumentContent, Ct as ProviderCapabilities, Dr as ToolResultTextContent, Dt as ToolCall, E as ToolMap, Er as ToolResultImageRefContent, Fn as AgentStats, G as SessionRun, H as CreateSessionOptions, K as SessionStore, Kn as McpServerConfig, L as SkillConfig, Mn as AgentBehavior, Mr as toolOutputByteLength, Nn as AgentClock, Nr as toolResultToText, O as ReadStateEntry, Or as ToolResultVideoContent, Ot as ToolResult, Pn as AgentRunOptions, Qn as PromptAudioPart, Rn as ChildRunStats, Rt as OpenRouterParams, Sr as ToolResultContent, St as Provider, T as ToolDef, Tn as AnthropicParams, Tr as ToolResultImageContent, Tt as StreamOptions, U as Session, Un as DEFAULT_AGENT_CLOCK, V as SkillsConfig, W as SessionData, X as RemoteStoreOptions, Xn as ProgressNudgeKind, Yn as OAuthRefreshHookContext, Zn as ProgressNudgeMessage, _n as CerebrasParams, _r as ThinkingLevel, ar as RepeatGuardToolMatcher, br as ToolOutcome, cn as OpenAIParams, cr as RetryConfig, dr as SessionEndStatus, er as PromptImagePart, fr as SessionHookContext, g as McpConnection, gr as StreamHookContext, hr as SpawnHookContext, i as AgentOptions, ir as RepeatGuardConfig, jr as toolOutputBudgetByteLength, k as ReadStateMap, kr as TurnFinishReason, kt as ToolSpec, lr as RunHookMap, mr as SessionTurn, n as AgentHookMap, nr as PromptTextPart, or as ResolveContentRef, pr as SessionMessage, qn as McpToolHookContext, r as AgentHooks, rr as PromptVideoPart, sr as ResolveContentRefBlock, t as Agent, tr as PromptPart, ur as SessionContentBlock, w as ToolContext, wr as ToolResultDocumentRefContent, wt as StreamCallbacks, xr as ToolResultAudioContent, yr as ToolHookContext, z as SkillResource } from "./agent-BAgOgS37.js";
|
|
2
2
|
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-Bs2oY7Ux.js";
|
|
3
3
|
import { t as SandboxProvider } from "./sandbox-B-bMq3K6.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { a as AgentToolNotAllowedError, c as ClassifiedError, d as matchesContextExceeded, i as AgentProviderError, l as ClassifiedErrorKind, n as AgentBudgetExceededError, r as AgentContextExceededError, s as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, t as AgentAbortedError } from "./errors-CkTvg97v.js";
|
|
5
|
+
import { $n as SpawnToolState, Qn as SpawnToolOptions, S as SuccessfulMutationOutcome, Wn as ValidationResult, Zn as ChildAgent, _ as MutationValidationBatch, b as MutationValidationMutation, g as statsByModel, gr as InteractionToolOptions, h as flattenTurns, m as ModelUsage, t as Preset, v as MutationValidationFailure, x as MutationValidationResult, y as MutationValidationHooksOptions } from "./index-CLuLFFLa.js";
|
|
6
|
+
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 ToolResultTextContent, type ToolResultVideoContent, type ToolSpec, type TurnFinishReason, type TurnResult, type TurnUsage, type ValidationResult, flattenTurns, matchesContextExceeded, statsByModel, toolOutputBudgetByteLength, toolOutputByteLength, toolResultToText };
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { m as openaiCompat } from "./openai-compat-0Y7jcncn.js";
|
|
2
|
+
import { i as resolveOAuthApiKey, n as isOAuthAccessToken, t as extractRuntimeCredentials } from "./oauth-8LqbeI_Q.js";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
//#region src/chat/oauth-page/pkce.ts
|
|
5
|
+
/**
|
|
6
|
+
* PKCE helper. Inlined here (rather than imported from pi-ai) because
|
|
7
|
+
* `@earendil-works/pi-ai/oauth` does not re-export it, and we don't want
|
|
8
|
+
* to reach into `node_modules/.../utils/oauth/pkce.js` — that's an
|
|
9
|
+
* implementation-detail path that breaks on minor upstream rearrangements.
|
|
10
|
+
*
|
|
11
|
+
* Web Crypto API only. Works under Bun + Node 22+ without any node:crypto
|
|
12
|
+
* import (matches pi-ai's own behavior). Output is base64url per RFC 7636.
|
|
13
|
+
*/
|
|
14
|
+
function base64UrlEncode(bytes) {
|
|
15
|
+
let binary = "";
|
|
16
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
17
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
18
|
+
}
|
|
19
|
+
async function generatePkce() {
|
|
20
|
+
const verifierBytes = new Uint8Array(32);
|
|
21
|
+
crypto.getRandomValues(verifierBytes);
|
|
22
|
+
const verifier = base64UrlEncode(verifierBytes);
|
|
23
|
+
const data = new TextEncoder().encode(verifier);
|
|
24
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
25
|
+
return {
|
|
26
|
+
verifier,
|
|
27
|
+
challenge: base64UrlEncode(new Uint8Array(hashBuffer))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/chat/oauth-page/xai.ts
|
|
32
|
+
/** pi-ai / zidane OAuth provider id. Also the credentials-file key. */
|
|
33
|
+
const XAI_OAUTH_PROVIDER_ID = "xai-oauth";
|
|
34
|
+
const DEFAULT_BASE_URL = "https://api.x.ai/v1";
|
|
35
|
+
const DISCOVERY_URL = `https://auth.x.ai/.well-known/openid-configuration`;
|
|
36
|
+
/** Public client id used by the Grok CLI OAuth surface. */
|
|
37
|
+
const DEFAULT_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
38
|
+
const DEFAULT_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
39
|
+
/** xAI matches the registered loopback IP exactly — `localhost` is rejected. */
|
|
40
|
+
const DEFAULT_CALLBACK_HOST = "127.0.0.1";
|
|
41
|
+
const DEFAULT_CALLBACK_PORT = 56121;
|
|
42
|
+
const CALLBACK_PATH = "/callback";
|
|
43
|
+
const PROVIDER_NAME = "xAI Grok";
|
|
44
|
+
/** Refresh slightly early so requests don't race expiry. */
|
|
45
|
+
const REFRESH_SKEW_MS = 12e4;
|
|
46
|
+
const CALLBACK_TIMEOUT_MS = 18e4;
|
|
47
|
+
function xaiOAuthClientId() {
|
|
48
|
+
return process.env.PI_XAI_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID;
|
|
49
|
+
}
|
|
50
|
+
function xaiOAuthScope() {
|
|
51
|
+
return process.env.PI_XAI_OAUTH_SCOPE || DEFAULT_SCOPE;
|
|
52
|
+
}
|
|
53
|
+
function xaiOAuthCallbackHost() {
|
|
54
|
+
return process.env.PI_XAI_OAUTH_CALLBACK_HOST || DEFAULT_CALLBACK_HOST;
|
|
55
|
+
}
|
|
56
|
+
function xaiOAuthCallbackPort() {
|
|
57
|
+
const parsed = Number.parseInt(process.env.PI_XAI_OAUTH_CALLBACK_PORT || String(DEFAULT_CALLBACK_PORT), 10);
|
|
58
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CALLBACK_PORT;
|
|
59
|
+
}
|
|
60
|
+
/** Resolve the xAI API base URL (env override → default). */
|
|
61
|
+
function xaiBaseUrl() {
|
|
62
|
+
return (process.env.PI_XAI_BASE_URL || process.env.XAI_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
63
|
+
}
|
|
64
|
+
function base64Url(bytes) {
|
|
65
|
+
let binary = "";
|
|
66
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
67
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
68
|
+
}
|
|
69
|
+
function randomToken(byteLength = 16) {
|
|
70
|
+
return base64Url(crypto.getRandomValues(new Uint8Array(byteLength)));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Refuse any OIDC endpoint that isn't HTTPS on an xAI origin.
|
|
74
|
+
*
|
|
75
|
+
* The discovery doc is cached long-term in `credentials.json`. A single MITM
|
|
76
|
+
* during initial login could otherwise pin a malicious `token_endpoint` that
|
|
77
|
+
* receives every subsequent refresh token. Validating scheme + host keeps the
|
|
78
|
+
* endpoint on `x.ai` / `*.x.ai`.
|
|
79
|
+
*/
|
|
80
|
+
function validateEndpoint(value, field) {
|
|
81
|
+
let url;
|
|
82
|
+
try {
|
|
83
|
+
url = new URL(value);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error(`xAI OAuth discovery returned an invalid ${field}: ${value}`);
|
|
86
|
+
}
|
|
87
|
+
if (url.protocol !== "https:") throw new Error(`xAI OAuth ${field} must use HTTPS: ${value}`);
|
|
88
|
+
const host = url.hostname.toLowerCase();
|
|
89
|
+
if (host !== "x.ai" && !host.endsWith(".x.ai")) throw new Error(`Refusing non-xAI OAuth ${field}: ${value}`);
|
|
90
|
+
return url.toString();
|
|
91
|
+
}
|
|
92
|
+
async function discover() {
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await fetch(DISCOVERY_URL, {
|
|
96
|
+
headers: { Accept: "application/json" },
|
|
97
|
+
signal: AbortSignal.timeout(15e3)
|
|
98
|
+
});
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
throw new Error(`xAI OIDC discovery failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
101
|
+
}
|
|
102
|
+
if (!response.ok) throw new Error(`xAI OIDC discovery returned ${response.status}`);
|
|
103
|
+
const payload = await response.json();
|
|
104
|
+
return {
|
|
105
|
+
authorization_endpoint: validateEndpoint(String(payload.authorization_endpoint ?? ""), "authorization_endpoint"),
|
|
106
|
+
token_endpoint: validateEndpoint(String(payload.token_endpoint ?? ""), "token_endpoint")
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Start the loopback callback server on `127.0.0.1:56121`. Falls back to an
|
|
111
|
+
* OS-assigned port if 56121 is taken — xAI accepts any loopback port as long
|
|
112
|
+
* as the `redirect_uri` we send on the authorize URL matches the listener.
|
|
113
|
+
*/
|
|
114
|
+
async function startXaiCallbackServer(renderPage) {
|
|
115
|
+
const callbackHost = xaiOAuthCallbackHost();
|
|
116
|
+
const callbackPort = xaiOAuthCallbackPort();
|
|
117
|
+
let settle;
|
|
118
|
+
let settled = false;
|
|
119
|
+
const callbackPromise = new Promise((resolve) => {
|
|
120
|
+
settle = (value) => {
|
|
121
|
+
if (settled) return;
|
|
122
|
+
settled = true;
|
|
123
|
+
resolve(value);
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const server = createServer((req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
const origin = req.headers.origin;
|
|
129
|
+
if (origin === "https://accounts.x.ai" || origin === "https://auth.x.ai") {
|
|
130
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
131
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
132
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
133
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
134
|
+
res.setHeader("Vary", "Origin");
|
|
135
|
+
}
|
|
136
|
+
if (req.method === "OPTIONS") {
|
|
137
|
+
res.statusCode = 204;
|
|
138
|
+
res.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const url = new URL(req.url ?? "/", `http://${callbackHost}`);
|
|
142
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
143
|
+
res.statusCode = 404;
|
|
144
|
+
res.end("Not found");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const result = {
|
|
148
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
149
|
+
state: url.searchParams.get("state") ?? void 0,
|
|
150
|
+
error: url.searchParams.get("error") ?? void 0,
|
|
151
|
+
errorDescription: url.searchParams.get("error_description") ?? void 0
|
|
152
|
+
};
|
|
153
|
+
res.statusCode = result.error ? 400 : 200;
|
|
154
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
155
|
+
res.end(renderPage(result.error ? {
|
|
156
|
+
kind: "error",
|
|
157
|
+
provider: PROVIDER_NAME,
|
|
158
|
+
message: `${PROVIDER_NAME} authentication did not complete.`,
|
|
159
|
+
details: result.errorDescription ?? result.error
|
|
160
|
+
} : {
|
|
161
|
+
kind: "success",
|
|
162
|
+
provider: PROVIDER_NAME,
|
|
163
|
+
message: "xAI authentication completed. You can close this window."
|
|
164
|
+
}));
|
|
165
|
+
settle?.(result);
|
|
166
|
+
} catch {
|
|
167
|
+
res.statusCode = 500;
|
|
168
|
+
res.end("Internal error");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const listen = (port) => new Promise((resolve, reject) => {
|
|
172
|
+
server.once("error", reject);
|
|
173
|
+
server.listen(port, callbackHost, () => {
|
|
174
|
+
server.removeListener("error", reject);
|
|
175
|
+
const addr = server.address();
|
|
176
|
+
resolve(typeof addr === "object" && addr ? addr.port : port);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
let actualPort;
|
|
180
|
+
try {
|
|
181
|
+
actualPort = await listen(callbackPort);
|
|
182
|
+
} catch {
|
|
183
|
+
actualPort = await listen(0);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
redirectUri: `http://${callbackHost}:${actualPort}${CALLBACK_PATH}`,
|
|
187
|
+
waitForCallback: (timeoutMs) => Promise.race([callbackPromise, new Promise((resolve) => setTimeout(resolve, timeoutMs, {
|
|
188
|
+
error: "timeout",
|
|
189
|
+
errorDescription: "Timed out waiting for the xAI OAuth callback."
|
|
190
|
+
}))]),
|
|
191
|
+
close: () => {
|
|
192
|
+
try {
|
|
193
|
+
server.close();
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function expiryFromPayload(payload) {
|
|
199
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : Number(payload.expires_in ?? 3600);
|
|
200
|
+
return Date.now() + expiresIn * 1e3 - REFRESH_SKEW_MS;
|
|
201
|
+
}
|
|
202
|
+
async function exchangeCode(tokenEndpoint, code, redirectUri, verifier, discovery) {
|
|
203
|
+
const clientId = xaiOAuthClientId();
|
|
204
|
+
const response = await fetch(tokenEndpoint, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
208
|
+
"Accept": "application/json"
|
|
209
|
+
},
|
|
210
|
+
body: new URLSearchParams({
|
|
211
|
+
grant_type: "authorization_code",
|
|
212
|
+
client_id: clientId,
|
|
213
|
+
code,
|
|
214
|
+
redirect_uri: redirectUri,
|
|
215
|
+
code_verifier: verifier
|
|
216
|
+
}),
|
|
217
|
+
signal: AbortSignal.timeout(3e4)
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok) throw new Error(`xAI token exchange failed: ${response.status} ${await response.text().catch(() => "")}`);
|
|
220
|
+
const payload = await response.json();
|
|
221
|
+
const access = String(payload.access_token ?? "");
|
|
222
|
+
const refresh = String(payload.refresh_token ?? "");
|
|
223
|
+
if (!access) throw new Error("xAI token exchange did not return an access_token.");
|
|
224
|
+
if (!refresh) throw new Error("xAI token exchange did not return a refresh_token.");
|
|
225
|
+
return {
|
|
226
|
+
access,
|
|
227
|
+
refresh,
|
|
228
|
+
expires: expiryFromPayload(payload),
|
|
229
|
+
tokenEndpoint,
|
|
230
|
+
discovery
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async function loginXai(renderPage, callbacks) {
|
|
234
|
+
const discovery = await discover();
|
|
235
|
+
const { verifier, challenge } = await generatePkce();
|
|
236
|
+
const state = randomToken();
|
|
237
|
+
const nonce = randomToken();
|
|
238
|
+
const server = await startXaiCallbackServer(renderPage);
|
|
239
|
+
try {
|
|
240
|
+
const authUrl = new URL(discovery.authorization_endpoint);
|
|
241
|
+
authUrl.searchParams.set("response_type", "code");
|
|
242
|
+
authUrl.searchParams.set("client_id", xaiOAuthClientId());
|
|
243
|
+
authUrl.searchParams.set("redirect_uri", server.redirectUri);
|
|
244
|
+
authUrl.searchParams.set("scope", xaiOAuthScope());
|
|
245
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
246
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
247
|
+
authUrl.searchParams.set("state", state);
|
|
248
|
+
authUrl.searchParams.set("nonce", nonce);
|
|
249
|
+
authUrl.searchParams.set("plan", "generic");
|
|
250
|
+
authUrl.searchParams.set("referrer", "zidane");
|
|
251
|
+
callbacks.onAuth({
|
|
252
|
+
url: authUrl.toString(),
|
|
253
|
+
instructions: `Sign in to xAI and approve access. If the browser is on another machine, the callback listener is ${server.redirectUri}.`
|
|
254
|
+
});
|
|
255
|
+
const result = await server.waitForCallback(CALLBACK_TIMEOUT_MS);
|
|
256
|
+
if (result.error) throw new Error(result.errorDescription ?? result.error);
|
|
257
|
+
if (result.state !== state) throw new Error("xAI OAuth state mismatch — possible CSRF. Please retry.");
|
|
258
|
+
if (!result.code) throw new Error("xAI OAuth callback did not include an authorization code.");
|
|
259
|
+
callbacks.onProgress?.("Exchanging authorization code for tokens...");
|
|
260
|
+
return await exchangeCode(discovery.token_endpoint, result.code, server.redirectUri, verifier, discovery);
|
|
261
|
+
} finally {
|
|
262
|
+
server.close();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function refreshXaiToken(credentials) {
|
|
266
|
+
const xai = credentials;
|
|
267
|
+
const clientId = xaiOAuthClientId();
|
|
268
|
+
if (!credentials.refresh) throw new Error("xAI credentials are missing a refresh token — re-login required.");
|
|
269
|
+
const tokenEndpoint = xai.tokenEndpoint ?? xai.discovery?.token_endpoint ?? (await discover()).token_endpoint;
|
|
270
|
+
validateEndpoint(tokenEndpoint, "token_endpoint");
|
|
271
|
+
const response = await fetch(tokenEndpoint, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
275
|
+
"Accept": "application/json"
|
|
276
|
+
},
|
|
277
|
+
body: new URLSearchParams({
|
|
278
|
+
grant_type: "refresh_token",
|
|
279
|
+
client_id: clientId,
|
|
280
|
+
refresh_token: credentials.refresh
|
|
281
|
+
}),
|
|
282
|
+
signal: AbortSignal.timeout(3e4)
|
|
283
|
+
});
|
|
284
|
+
if (!response.ok) throw new Error(`xAI token refresh failed: ${response.status} ${await response.text().catch(() => "")}`);
|
|
285
|
+
const payload = await response.json();
|
|
286
|
+
const access = String(payload.access_token ?? "");
|
|
287
|
+
if (!access) throw new Error("xAI token refresh did not return an access_token.");
|
|
288
|
+
return {
|
|
289
|
+
...xai,
|
|
290
|
+
access,
|
|
291
|
+
refresh: String(payload.refresh_token ?? credentials.refresh),
|
|
292
|
+
expires: expiryFromPayload(payload),
|
|
293
|
+
tokenEndpoint
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Build an xAI `OAuthProviderInterface`. Shape matches Anthropic / Codex /
|
|
298
|
+
* Cursor so it drops into `BUILTIN_PROVIDERS` and `src/auth.ts` unchanged.
|
|
299
|
+
*
|
|
300
|
+
* `usesCallbackServer: true` — the loopback flow has a real callback server,
|
|
301
|
+
* so the TUI doesn't need to prompt for a manual code paste.
|
|
302
|
+
*/
|
|
303
|
+
function createXaiOAuthProvider(renderPage) {
|
|
304
|
+
return {
|
|
305
|
+
id: XAI_OAUTH_PROVIDER_ID,
|
|
306
|
+
name: "xAI Grok (SuperGrok / X Premium+)",
|
|
307
|
+
usesCallbackServer: true,
|
|
308
|
+
login: (callbacks) => loginXai(renderPage, callbacks),
|
|
309
|
+
refreshToken: refreshXaiToken,
|
|
310
|
+
getApiKey: (credentials) => credentials.access
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/providers/xai.ts
|
|
315
|
+
const DEFAULT_MODEL = "grok-4.3";
|
|
316
|
+
function xai(params) {
|
|
317
|
+
const defaultModel = params?.defaultModel || DEFAULT_MODEL;
|
|
318
|
+
const baseURL = xaiBaseUrl();
|
|
319
|
+
const capabilities = params?.capabilities ?? {
|
|
320
|
+
vision: true,
|
|
321
|
+
imageInToolResult: false
|
|
322
|
+
};
|
|
323
|
+
const baseCredentials = extractRuntimeCredentials(params);
|
|
324
|
+
let runtimeCredentials = baseCredentials;
|
|
325
|
+
const staticKey = params?.apiKey ?? process.env.XAI_API_KEY;
|
|
326
|
+
const isOAuth = baseCredentials !== void 0 || !staticKey || isOAuthAccessToken(staticKey);
|
|
327
|
+
/**
|
|
328
|
+
* Build the openai-compat delegate for a resolved bearer. Cheap — the factory
|
|
329
|
+
* only wires closures — so we re-create it per turn with the freshly resolved
|
|
330
|
+
* (possibly refreshed) OAuth token rather than baking a stale key in once.
|
|
331
|
+
*/
|
|
332
|
+
function delegateFor(apiKey) {
|
|
333
|
+
return openaiCompat({
|
|
334
|
+
name: "xai",
|
|
335
|
+
apiKey,
|
|
336
|
+
baseURL,
|
|
337
|
+
defaultModel,
|
|
338
|
+
capabilities,
|
|
339
|
+
extraHeaders: params?.extraHeaders
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const skeleton = delegateFor("placeholder-resolved-at-stream");
|
|
343
|
+
return {
|
|
344
|
+
...skeleton,
|
|
345
|
+
name: "xai",
|
|
346
|
+
meta: {
|
|
347
|
+
...skeleton.meta,
|
|
348
|
+
defaultModel,
|
|
349
|
+
isOAuth
|
|
350
|
+
},
|
|
351
|
+
async stream(options, callbacks) {
|
|
352
|
+
return delegateFor(await resolveOAuthApiKey({
|
|
353
|
+
provider: "xai",
|
|
354
|
+
providerId: XAI_OAUTH_PROVIDER_ID,
|
|
355
|
+
params: runtimeCredentials ? {
|
|
356
|
+
...params,
|
|
357
|
+
...runtimeCredentials
|
|
358
|
+
} : params,
|
|
359
|
+
envKey: "XAI_API_KEY",
|
|
360
|
+
missingError: "No xAI credentials found. Set XAI_API_KEY or run `bun run auth --xai` first.",
|
|
361
|
+
refreshError: (reason) => `xAI OAuth token refresh failed. Run \`bun run auth --xai\` again. ${reason}`
|
|
362
|
+
}, {
|
|
363
|
+
...callbacks,
|
|
364
|
+
async onOAuthRefresh(ctx) {
|
|
365
|
+
if (ctx.source === "params") runtimeCredentials = {
|
|
366
|
+
access: ctx.credentials.access,
|
|
367
|
+
refresh: ctx.credentials.refresh,
|
|
368
|
+
expires: ctx.credentials.expires
|
|
369
|
+
};
|
|
370
|
+
await callbacks.onOAuthRefresh?.(ctx);
|
|
371
|
+
}
|
|
372
|
+
})).stream(options, callbacks);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
export { createXaiOAuthProvider as n, generatePkce as r, xai as t };
|
|
378
|
+
|
|
379
|
+
//# sourceMappingURL=xai-tPdbAGkq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xai-tPdbAGkq.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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"}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -712,7 +712,7 @@ Merge rules:
|
|
|
712
712
|
|
|
713
713
|
`run.behavior` > `agent.behavior` > defaults (field-by-field merge).
|
|
714
714
|
|
|
715
|
-
Defaults: unlimited turns (set `maxTurns` as a safety net), `maxTokens: 16384`, level-based thinking; `cache: true`, `dedupReads: true`, `requireReadBeforeEdit: false`, `maxConcurrentTools: 10`, `compactStrategy: 'off'`, `compactThreshold: 128 KiB`, `compactKeepTurns: 4`, `toolDisclosure: 'eager'`, `readLineNumbers: true`, `elideStaleReads: false`, `surfaceMcpInstructions: true`, `maxConsecutivePauseTurns: 5`, `persistTurns: true`, `providerStreamStartTimeoutMs: 300000`, `providerStreamTimeoutMs: 1800000`, and `mcpConnectTimeoutMs: 120000` for custom connectors. `persistTurns: true` means incremental per-turn store sync; set `false` to persist once at run end instead — trades mid-run crash granularity for O(1) writes per run on stores whose `appendTurns` re-serializes the session. All other knobs (`toolOutputBudget`, `dedupTools`, `toolBudgets`, `repeatGuard`, `thinkingDecay`, `toolSearch`, `mediaKeepTurns`, `autoCompact`, `persistThreshold`, `persistDir`, `persistExcludeTools`, `persistMaxBytes`, `maxCostUsd`, `maxTotalTokens`, `retry`, `toolBatchTimeoutMs`) are `undefined` (`retry` defaults to 3 attempts with 1s → 30s exponential backoff when absent). The chat-layer profiles (`BUILD_AGENT` / `PLAN_AGENT`) override several of these (including turning `requireReadBeforeEdit` **on**, enabling `repeatGuard`, and setting `mediaKeepTurns: 8`) — see CHAT.md → Agents.
|
|
715
|
+
Defaults: unlimited turns (set `maxTurns` as a safety net), `maxTokens: 16384`, level-based thinking; `cache: true`, `dedupReads: true`, `requireReadBeforeEdit: false`, `maxConcurrentTools: 10`, `compactStrategy: 'off'`, `compactThreshold: 128 KiB`, `compactKeepTurns: 4`, `toolDisclosure: 'eager'`, `readLineNumbers: true`, `elideStaleReads: false`, `surfaceMcpInstructions: true`, `maxConsecutivePauseTurns: 5`, `persistTurns: true`, `providerStreamStartTimeoutMs: 300000`, `providerStreamTimeoutMs: 1800000`, and `mcpConnectTimeoutMs: 120000` for custom connectors. `persistTurns: true` means incremental per-turn store sync; set `false` to persist once at run end instead — trades mid-run crash granularity for O(1) writes per run on stores whose `appendTurns` re-serializes the session. All other knobs (`toolOutputBudget`, `dedupTools`, `toolBudgets`, `repeatGuard`, `thinkingDecay`, `thinkingOverrunPolicy`, `readFileDefaults`, `writeFileExistingPolicy`, `textToolCallRescue`, `unknownToolRecovery`, `progressNudges`, `maxTurnsWarningMessage`, `toolSearch`, `mediaKeepTurns`, `autoCompact`, `persistThreshold`, `persistDir`, `persistExcludeTools`, `persistMaxBytes`, `maxCostUsd`, `maxTotalTokens`, `retry`, `toolBatchTimeoutMs`) are `undefined` (`retry` defaults to 3 attempts with 1s → 30s exponential backoff when absent). The chat-layer profiles (`BUILD_AGENT` / `PLAN_AGENT`) override several of these (including turning `requireReadBeforeEdit` **on**, enabling `repeatGuard`, and setting `mediaKeepTurns: 8`) — see CHAT.md → Agents.
|
|
716
716
|
|
|
717
717
|
**`toolOutputBudget`** (off by default). The loop sums every tool result's `outputBytes` after `tool:transform`, so consumer truncation counts. On overshoot, a synthetic user message is appended:
|
|
718
718
|
|
|
@@ -740,7 +740,9 @@ Tools with side effects or non-deterministic outputs MUST NOT be listed (no safe
|
|
|
740
740
|
|
|
741
741
|
**`thinkingDecay`** tapers `thinkingBudget` per run-relative turn. `applyThinkingDecay(base, decay, turn)` runs at `runTurn` start. Struct form `{ afterTurn, factor, floor }` does geometric decay; function form `(turn, base) => number` accepts arbitrary curves. Result clamped to `[0, base]` so buggy curves can't exceed the caller's opt-in.
|
|
742
742
|
|
|
743
|
-
**
|
|
743
|
+
**Reliability levers** are opt-in and live in `src/behaviors/`. `writeFileExistingPolicy` blocks full-file rewrites of existing files and points the model at `read_file` + `edit` / `multi_edit`; `readFileDefaults` sets omitted `read_file` slice defaults without overriding explicit tool-call args; `thinkingOverrunPolicy` queues a non-interrupting next-turn nudge when streamed thinking crosses a threshold; `textToolCallRescue` steers after a model writes a tool call as text; `unknownToolRecovery` queues closest currently-advertised tool-name suggestions without changing the unknown-tool error result; `progressNudges` emits bounded non-interrupting pivot nudges for broad stalls that exact repeat guards do not cover; `maxTurnsWarningMessage` customizes the existing max-turn proximity nudge.
|
|
744
|
+
|
|
745
|
+
**Gate precedence**. Consumer hooks register first (agent-lifetime then per-run), framework gates install after — `allowed-tools → writeFileExistingPolicy → toolBudgets → repeatGuard → dedupTools → lazy-disclosure`. Each framework gate short-circuits on existing `ctx.block` or `ctx.result`, so the firing order is consumer → framework with the framework gates acting as an early-exit chain. The net effect: a policy gate always beats a consumer cache substitute, budgets enforce before dedup replays a cached call against an exhausted cap, repeat-guard can still escalate budget-blocked loops when configured to count blocked calls, and lazy-disclosure runs last so skill refusal wins over "load schema first" and dedup substitutes only hit on previously-recorded successful calls (which already passed lazy-disclosure once).
|
|
744
746
|
|
|
745
747
|
**Tool-result persistence** (`behavior.persistThreshold` + `persistDir` + `persistExcludeTools`). When set, the loop replaces oversize `tool_result` outputs with a `<persisted-output tool="…" bytes="…" path="…">` stub carrying a 2 KiB preview. Substitution happens just after `tool:transform`; the stub flows into `session.turns` directly so the prompt-cache prefix stays stable across turns. `persistDir` is required to enable the feature — without a target dir the framework silently skips persistence. `persistExcludeTools` (default empty at the SDK; the chat layer ships `DEFAULT_PERSIST_EXCLUDE_TOOLS` — see CHAT.md) lists canonical tool names that bypass regardless of size. `persistMaxBytes` caps the directory's cumulative `*.txt` size; after each successful write the helper evicts oldest-first by mtime (LRU) until total ≤ cap. The just-written blob is never evicted in the same sweep, so a single oversize result still lands. Off by default.
|
|
746
748
|
|
package/docs/CHAT.md
CHANGED
|
@@ -381,6 +381,8 @@ Both share `SHARED_BEHAVIOR`:
|
|
|
381
381
|
|
|
382
382
|
The loop-prevention knobs are the one place the chat profiles diverge from the SDK core's conservative defaults. `requireReadBeforeEdit` is the only one that can *reject* a previously-succeeding call — but the refusal is a recoverable `tool_result` (`Edit error: <path> has not been read in this session. Call read_file first …`), not a hard error, so a model self-corrects in one turn. `repeatGuard` defaults to strict chain mode: it blocks the 4th back-to-back identical `shell` call (6th for `read_file`) and aborts at the 8th (12th for `read_file`) via the agent's `AbortController`, preceded by a `repeat-guard:exceeded` event. Consumers can opt into `mode: 'window' | 'both'`, custom `steeringMessage`, or `thresholdAction: 'steer'` to append guidance to the tool result instead of blocking at the soft threshold. `dedupTools` replay and `thinkingDecay` are free / inert until triggered.
|
|
383
383
|
|
|
384
|
+
Other reliability levers stay off unless a profile opts in: `writeFileExistingPolicy: 'block-with-edit-recipe'` converts existing-file `write_file` rewrites into a recoverable `tool:gate` refusal; `readFileDefaults` sets omitted `read_file` slice defaults without changing explicit calls; `thinkingOverrunPolicy` queues a non-interrupting next-turn nudge when streamed thinking crosses a threshold; `textToolCallRescue` steers after a model writes JSON/XML/Liquid-style tool-call markup as plain text; `unknownToolRecovery` queues closest currently-advertised tool-name suggestions after an unknown native tool call; `progressNudges` emits bounded pivot nudges for repeated read-only turns, failed/no-op mutations, validation rejects, or tool errors; `maxTurnsWarningMessage` customizes the built-in turn-cap warning.
|
|
385
|
+
|
|
384
386
|
`resolveAgentId(registry, requestedId, defaultId)` picks the next active profile with fallbacks: requested id > defaultId > first registry key > `null`. `singleAgentRegistry(preset)` wraps a legacy `preset` into a one-entry registry — used internally when the host passed `preset` without `agents`.
|
|
385
387
|
|
|
386
388
|
`accentColor(profile.accent, COLOR)` resolves an `AgentAccent` token (`'brand' | 'accent' | 'warn' | 'model'`, defaulting to `'accent'`) to a hex string from the four-role color subset the caller passes. Renderer-agnostic — the TUI's footer + picker call it with `useColors()`, a GUI can hand it a CSS-variable lookup with the same shape.
|
|
@@ -620,6 +622,7 @@ Public API (re-exported from `zidane/chat`):
|
|
|
620
622
|
- `selectActiveTodos(session) → ActiveTodosState` — pure selector, identical contract, no React.
|
|
621
623
|
- `pickActiveRunId(session) → runId | null` — "the run UI surfaces should reflect right now" (latest-running, falling back to most-recently-appended).
|
|
622
624
|
- `TODO_STATUS_GLYPHS: Record<TodoStatus, string>` — single source of truth for status icons across surfaces.
|
|
625
|
+
- **Prompt anchor helper** — `createActiveTodoAnchorHooks(options?)` returns an opt-in `system:transform` hook that appends a compact live-todo block. It reads only the live `todos` list, never `archive`, so an auto-cleared completed plan is not reintroduced. `renderActiveTodoAnchor(todos, options?)` is the pure renderer behind it.
|
|
623
626
|
- **Types** — `TodoItem`, `TodoStatus` (`'pending' | 'in_progress' | 'completed' | 'cancelled'`), `TodosBag` (`{ session?, byRun?, archive? }`), `TodoTally`, `ActiveTodosState` (carries both `todos` and `archive` so UI surfaces can render the close-out batch after auto-clear), `CreateTodoToolsOptions`.
|
|
624
627
|
|
|
625
628
|
`CreateTodoToolsOptions`:
|
|
@@ -679,6 +682,7 @@ The chat layer ships two renderer-agnostic primitives any host can build a UI on
|
|
|
679
682
|
|
|
680
683
|
- **Inline indicator.** Single-line "▸ <in-progress content>" badge to render near the prompt input. Show iff `selectActiveTodos(session).inProgress` is non-null. Gated on `Settings.showTodoIndicator` (default `true`). When the active run has no `in_progress` item the indicator returns null — never a placeholder, never visual noise.
|
|
681
684
|
- **Todos modal.** Read-only viewer for the active run's slot. Renders the per-status tally header + a scrollable list of rows (`TODO_STATUS_GLYPHS[status]` + content). No interactive controls — the model is the only writer. The host wires it behind the `openTodos` keybinding action (default `ctrl+t` — see **Keybindings** below); the modal closes on `esc`. Resolves the active run via `pickActiveRunId` (latest running, fallback most-recently-appended) so a child subagent's slot surfaces when it's the live run.
|
|
685
|
+
- **Active todo prompt anchor.** Optional. Add `createActiveTodoAnchorHooks()` to a chat/profile's hooks when a host wants the current live todos repeated in the dynamic system prompt. Leave it off for minimal prompts; it never changes `todowrite` storage or UI behavior.
|
|
682
686
|
|
|
683
687
|
The hook recomputes on every parent re-render (cheap O(runs + todos) selector) so a `setTodosForRun` write — whether to the session slot or a subagent's — lands in the indicator and modal on the next paint without any memoization-cache invalidation gymnastics.
|
|
684
688
|
|
package/docs/SKILL.md
CHANGED
|
@@ -35,6 +35,15 @@ const agent = createAgent({
|
|
|
35
35
|
dedupReads: true, // re-read dedup
|
|
36
36
|
dedupTools: { run_query: i => typeof i.query === 'string' ? i.query.trim() : undefined },
|
|
37
37
|
requireReadBeforeEdit: false,
|
|
38
|
+
|
|
39
|
+
// optional reliability levers; omit unless the profile wants this behavior
|
|
40
|
+
readFileDefaults: { limit: 1000, maxBytes: 128 * 1024 }, // defaults when read_file omits limits
|
|
41
|
+
writeFileExistingPolicy: 'block-with-edit-recipe', // steer full rewrites toward read_file + edit/multi_edit
|
|
42
|
+
thinkingOverrunPolicy: { mode: 'steer-next-turn', thresholdTokens: 8192 },
|
|
43
|
+
textToolCallRescue: { mode: 'steer' },
|
|
44
|
+
unknownToolRecovery: { mode: 'steer' },
|
|
45
|
+
progressNudges: { mode: 'steer', readOnlyTurns: 4, failedMutations: 3 },
|
|
46
|
+
maxTurnsWarningMessage: ({ remaining }) => `Wrap up soon; ${remaining} turns left.`,
|
|
38
47
|
toolBudgets: { run_query: { max: 20, onExceed: 'steer' } },
|
|
39
48
|
compactStrategy: 'off', // 'off' | 'tail' (non-Anthropic compaction)
|
|
40
49
|
compactThreshold: 131_072, // 128 KiB
|
|
@@ -62,6 +71,14 @@ const stats = await agent.run({
|
|
|
62
71
|
// agent.run({}) // promptless resume when session has the user turn
|
|
63
72
|
```
|
|
64
73
|
|
|
74
|
+
For brittle/local OpenAI-compatible endpoints, use the opt-in overlay and override it like any other `AgentBehavior` object:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { localReliabilityBehavior } from 'zidane'
|
|
78
|
+
|
|
79
|
+
const behavior = localReliabilityBehavior({ maxConcurrentTools: 1 })
|
|
80
|
+
```
|
|
81
|
+
|
|
65
82
|
### Multimodal prompts
|
|
66
83
|
|
|
67
84
|
`prompt` accepts a string or a `PromptPart[]`:
|
|
@@ -122,6 +139,8 @@ Provider params types: `AnthropicParams`, `OpenAIParams`, `OpenRouterParams`, `C
|
|
|
122
139
|
|
|
123
140
|
`openrouter`, `cerebras`, `arcee`, and `local` are thin wrappers over `openaiCompat` with vendor/runtime defaults pinned. For new OAI-compatible backends (Baseten, Fireworks, Groq), use `openaiCompat` directly.
|
|
124
141
|
|
|
142
|
+
Each factory also has a per-provider subpath (`zidane/providers/anthropic`, `zidane/providers/openai`, `zidane/providers/openai-compat`, `zidane/providers/{openrouter,xai,cerebras,baseten,arcee,local}`) so a bundle pulls only the provider it uses, not the whole `zidane/providers` barrel. Cost estimation loads pi-ai's price registry through a guarded dynamic import, so edge bundles may externalize `@earendil-works/pi-ai`: providers not built on it (anthropic, openrouter, the openai-compat family) then drop the multi-MB registry and `fillEstimatedCost` falls back to the local rate table. `zidane/providers/openai` keeps pi-ai since its client is built on it.
|
|
143
|
+
|
|
125
144
|
### Vision + image tool results
|
|
126
145
|
|
|
127
146
|
`Provider.meta.capabilities` exposes `{ vision?, imageInToolResult? }`:
|
|
@@ -294,7 +313,9 @@ const { valid, coercedInput, coercions } = validateToolArgs(
|
|
|
294
313
|
|
|
295
314
|
## Hooks
|
|
296
315
|
|
|
297
|
-
Hooks fire at every lifecycle point via [hookable](https://github.com/unjs/hookable). Awaited in registration order; ctx is shared per firing (last-writer wins). Hooks marked **mutable** influence downstream behavior; others are observational. Framework gates (allowed-tools
|
|
316
|
+
Hooks fire at every lifecycle point via [hookable](https://github.com/unjs/hookable). Awaited in registration order; ctx is shared per firing (last-writer wins). Hooks marked **mutable** influence downstream behavior; others are observational. Framework gates (`allowed-tools`, `writeFileExistingPolicy`, `toolBudgets`, `repeatGuard`, `dedupTools`, lazy disclosure) short-circuit on existing `block` / `result`, so a policy gate always beats a consumer cache substitute regardless of registration order.
|
|
317
|
+
|
|
318
|
+
For post-edit validation, use `createMutationValidationHooks()` from `zidane`. It observes successful `tool:after.outcome` values (`created`, `updated`, `edited`) and batches them at `tool-results:after`; hosts still own the validator command and can call `agent.followUp(failure.message)` from `onFailure`. It does not wrap or replace `write_file`, `edit`, or `multi_edit`.
|
|
298
319
|
|
|
299
320
|
### Hook reference
|
|
300
321
|
|
|
@@ -336,7 +357,7 @@ Hooks fire at every lifecycle point via [hookable](https://github.com/unjs/hooka
|
|
|
336
357
|
| `mcp:tool:after` | MCP complete | — | `+ result: string \| ToolResultContent[], outputBytes: number` |
|
|
337
358
|
| `mcp:tool:error` | MCP error | — | `+ error` |
|
|
338
359
|
| `tool:transform` | After tool result | **yes** (`result`, `isError`) | `+ result: string \| ToolResultContent[], isError, outputBytes: number` |
|
|
339
|
-
| `tool:after` | Tool complete | — | `+ result: string \| ToolResultContent[], outputBytes: number, runToolCounts
|
|
360
|
+
| `tool:after` | Tool complete | — | `+ result: string \| ToolResultContent[], outputBytes: number, runToolCounts, outcome?` |
|
|
340
361
|
| `tool:error` | Tool error | **yes** (`result?`) | `+ error` — mutate `result` to substitute the payload sent back to the model |
|
|
341
362
|
| `tool:cancelled` | Per-call cancel via `agent.cancelTool(callId, reason?)` (typically a TUI "cancel this tool" affordance) | — | `+ reason: string, runToolCounts` — wire result is the canonical `TOOL_USE_CANCELLED_MESSAGE` with `isError: true`; `tool:transform` + `tool:after` do **not** fire for the cancelled call |
|
|
342
363
|
| `background:start` | Background task spawned via `shell({ run_in_background: true })` | — | `taskId, pid, command, cwd, outputPath, startedAt` — observational; subscribe for telemetry / "task started" toasts |
|
|
@@ -881,20 +902,23 @@ createSandboxContext(myProvider) // implement SandboxProvider (E2B, Rivet, …)
|
|
|
881
902
|
```
|
|
882
903
|
src/
|
|
883
904
|
agent.ts createAgent, orchestration, hooks
|
|
905
|
+
behaviors/ AgentBehavior modules + barrel (dedup, budgets, guards, nudges)
|
|
884
906
|
loop.ts Turn loop, tool execution
|
|
885
907
|
loop-persistence.ts Disk persistence of oversize tool results (`<persisted-output>` stubs)
|
|
886
908
|
aliasing.ts Tool-alias lookup + rewrite helpers
|
|
887
909
|
atomic-write.ts tmp + rename helper (shared by sessions, credentials, state)
|
|
888
|
-
dedup-tools.ts behavior.dedupTools gate
|
|
889
|
-
tool-budgets.ts behavior.toolBudgets gate
|
|
890
910
|
errors.ts Typed errors + toTypedError dispatcher
|
|
911
|
+
errors-entry.ts Narrow `zidane/errors` entry (errors.ts + OperationTimeoutError)
|
|
891
912
|
prompt.ts PromptPart canonicalizer + default builder
|
|
913
|
+
system-prompt.ts System-prompt section helpers (the `zidane/prompt` entry)
|
|
914
|
+
mutation-validation.ts Host-side post-mutation validation hook helper
|
|
892
915
|
tracing.ts createTracingHooks
|
|
893
916
|
stats.ts flattenTurns, statsByModel
|
|
894
917
|
types.ts Shared types
|
|
895
918
|
types-export.ts Public type surface for `zidane/types`
|
|
896
919
|
xml.ts Minimal escapers
|
|
897
|
-
index.ts Public entry
|
|
920
|
+
index.ts Public entry (root barrel)
|
|
921
|
+
agent-entry.ts Narrow `zidane/agent` entry (core + run-summary + cache-telemetry + read-state)
|
|
898
922
|
zod.ts zodToJsonSchema (v4)
|
|
899
923
|
auth.ts OAuth login CLI
|
|
900
924
|
start.ts CLI entrypoint
|
|
@@ -912,6 +936,8 @@ src/
|
|
|
912
936
|
|
|
913
937
|
Types: `import type { Agent, SessionTurn, TurnUsage, Provider, ToolDef } from 'zidane/types'`
|
|
914
938
|
|
|
939
|
+
Narrow runtime subpaths for edge / Worker bundles — each re-exports root-public symbols so you avoid the root barrel's eval / headless / provider / context graph: `zidane/agent` (agent core + run-summary + cache-telemetry + read-state), `zidane/errors` (typed errors + `OperationTimeoutError`), `zidane/prompt` (system-prompt section helpers). The package sets `"sideEffects": false`, so bundlers also tree-shake unused root re-exports.
|
|
940
|
+
|
|
915
941
|
## Related
|
|
916
942
|
|
|
917
943
|
- `docs/ARCHITECTURE.md` — execution-flow diagrams, hook firing order, gate precedence.
|