zidane 5.13.0 → 5.13.2

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 (96) hide show
  1. package/README.md +15 -0
  2. package/dist/acp-BqIU2mo-.js +1410 -0
  3. package/dist/acp-BqIU2mo-.js.map +1 -0
  4. package/dist/acp-cli.d.ts +1 -0
  5. package/dist/acp-cli.js +713 -0
  6. package/dist/acp-cli.js.map +1 -0
  7. package/dist/acp.d.ts +655 -0
  8. package/dist/acp.d.ts.map +1 -0
  9. package/dist/acp.js +2 -0
  10. package/dist/{agent-Db4ojCSV.d.ts → agent-D7ZL8B2X.d.ts} +2 -2
  11. package/dist/{agent-Db4ojCSV.d.ts.map → agent-D7ZL8B2X.d.ts.map} +1 -1
  12. package/dist/chat/pure.d.ts +3 -3
  13. package/dist/chat.d.ts +6 -6
  14. package/dist/chat.js +3 -2
  15. package/dist/chat.js.map +1 -1
  16. package/dist/contexts/daytona.d.ts +3 -3
  17. package/dist/contexts/docker.d.ts +1 -1
  18. package/dist/contexts/docker.d.ts.map +1 -1
  19. package/dist/contexts/docker.js +4 -1
  20. package/dist/contexts/docker.js.map +1 -1
  21. package/dist/contexts/e2b.d.ts +2 -2
  22. package/dist/{contexts-VhV4Af8x.js → contexts-DHi8LPCp.js} +25 -9
  23. package/dist/contexts-DHi8LPCp.js.map +1 -0
  24. package/dist/contexts.d.ts +3 -3
  25. package/dist/contexts.js +1 -1
  26. package/dist/eval.d.ts +1 -1
  27. package/dist/eval.js +3 -3
  28. package/dist/glob-DCWXy_tr.js +128 -0
  29. package/dist/glob-DCWXy_tr.js.map +1 -0
  30. package/dist/{headless-tVN-g6IR.js → headless-0O6HMNBQ.js} +6 -6
  31. package/dist/{headless-tVN-g6IR.js.map → headless-0O6HMNBQ.js.map} +1 -1
  32. package/dist/headless.d.ts +1 -1
  33. package/dist/headless.js +1 -1
  34. package/dist/{index-BEblm0Hu.d.ts → index-BsyPeCSL.d.ts} +3 -3
  35. package/dist/{index-BEblm0Hu.d.ts.map → index-BsyPeCSL.d.ts.map} +1 -1
  36. package/dist/{index-CJ-2g7bY.d.ts → index-CDcQW-2S.d.ts} +3 -3
  37. package/dist/index-CDcQW-2S.d.ts.map +1 -0
  38. package/dist/{index-CrMb8jCE.d.ts → index-CF15aqlk.d.ts} +3 -3
  39. package/dist/{index-CrMb8jCE.d.ts.map → index-CF15aqlk.d.ts.map} +1 -1
  40. package/dist/index.d.ts +7 -7
  41. package/dist/index.js +7 -7
  42. package/dist/lazy-DLOurOC_.js +20 -0
  43. package/dist/lazy-DLOurOC_.js.map +1 -0
  44. package/dist/{logger-Dcrj48qY.d.ts → logger-DItaCwPw.d.ts} +2 -2
  45. package/dist/{logger-Dcrj48qY.d.ts.map → logger-DItaCwPw.d.ts.map} +1 -1
  46. package/dist/mcp.d.ts +1 -1
  47. package/dist/{messages-CGazSyTL.js → messages-DEsLGBB9.js} +2 -2
  48. package/dist/{messages-CGazSyTL.js.map → messages-DEsLGBB9.js.map} +1 -1
  49. package/dist/output/stream-json.d.ts +2 -2
  50. package/dist/output/stream-json.js +1 -1
  51. package/dist/output/terminal.d.ts +2 -2
  52. package/dist/{presets-kPEMOCmE.js → presets-HDIxliiq.js} +2 -2
  53. package/dist/{presets-kPEMOCmE.js.map → presets-HDIxliiq.js.map} +1 -1
  54. package/dist/presets.d.ts +2 -2
  55. package/dist/presets.js +1 -1
  56. package/dist/{providers-Bo2biCyT.js → providers-Cz-RNYZO.js} +7 -13
  57. package/dist/providers-Cz-RNYZO.js.map +1 -0
  58. package/dist/providers.d.ts +1 -1
  59. package/dist/providers.js +2 -2
  60. package/dist/restate.d.ts +2 -2
  61. package/dist/session/sqlite.d.ts +1 -1
  62. package/dist/{session-B69BQSn1.js → session-BDWZZaYa.js} +2 -2
  63. package/dist/{session-B69BQSn1.js.map → session-BDWZZaYa.js.map} +1 -1
  64. package/dist/session.d.ts +1 -1
  65. package/dist/session.js +2 -2
  66. package/dist/skills.d.ts +2 -2
  67. package/dist/{tool-formatters-CkqBgPH4.d.ts → tool-formatters-CNSMadtp.d.ts} +2 -2
  68. package/dist/{tool-formatters-CkqBgPH4.d.ts.map → tool-formatters-CNSMadtp.d.ts.map} +1 -1
  69. package/dist/tools/fetch-url.d.ts +1 -1
  70. package/dist/tools/web-search.d.ts +1 -1
  71. package/dist/{tools-5Bnlq68O.js → tools-DhzKzB1y.js} +39 -56
  72. package/dist/tools-DhzKzB1y.js.map +1 -0
  73. package/dist/tools.d.ts +2 -2
  74. package/dist/tools.js +1 -1
  75. package/dist/{transcript-anchors-D4PwUMyO.js → transcript-anchors-Cq-8gx8u.js} +9 -1417
  76. package/dist/transcript-anchors-Cq-8gx8u.js.map +1 -0
  77. package/dist/{transcript-anchors-BnLZmASt.d.ts → transcript-anchors-EG-SmZRu.d.ts} +4 -4
  78. package/dist/{transcript-anchors-BnLZmASt.d.ts.map → transcript-anchors-EG-SmZRu.d.ts.map} +1 -1
  79. package/dist/tui.d.ts +3 -3
  80. package/dist/tui.js +7 -6
  81. package/dist/tui.js.map +1 -1
  82. package/dist/{turn-operations-B6FaQAZN.d.ts → turn-operations-DwtWRYr1.d.ts} +3 -3
  83. package/dist/{turn-operations-B6FaQAZN.d.ts.map → turn-operations-DwtWRYr1.d.ts.map} +1 -1
  84. package/dist/{types-B39tBba1.d.ts → types-Bs2oY7Ux.d.ts} +27 -4
  85. package/dist/types-Bs2oY7Ux.d.ts.map +1 -0
  86. package/dist/types.d.ts +4 -4
  87. package/dist/xdg-zlSeVBhQ.js +1417 -0
  88. package/dist/xdg-zlSeVBhQ.js.map +1 -0
  89. package/docs/ACP.md +221 -0
  90. package/package.json +11 -1
  91. package/dist/contexts-VhV4Af8x.js.map +0 -1
  92. package/dist/index-CJ-2g7bY.d.ts.map +0 -1
  93. package/dist/providers-Bo2biCyT.js.map +0 -1
  94. package/dist/tools-5Bnlq68O.js.map +0 -1
  95. package/dist/transcript-anchors-D4PwUMyO.js.map +0 -1
  96. package/dist/types-B39tBba1.d.ts.map +0 -1
@@ -0,0 +1,1417 @@
1
+ import { t as writeFileAtomic } from "./atomic-write-Bgtr5JPu.js";
2
+ import { a as local, c as generatePkce, f as arcee, g as FAST_MODE_OPTIONS, h as ANTHROPIC_EXTRA_MODELS, i as openai, l as cerebras, n as createXaiOAuthProvider, p as anthropic, r as openrouter, s as createCursorOAuthProvider, t as xai, u as baseten } from "./providers-Cz-RNYZO.js";
3
+ import { dirname, resolve } from "node:path";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { randomBytes } from "node:crypto";
6
+ import { getModel, getModels } from "@earendil-works/pi-ai";
7
+ import { homedir } from "node:os";
8
+ import { createServer } from "node:http";
9
+ import { refreshAnthropicToken, refreshOpenAICodexToken, registerOAuthProvider } from "@earendil-works/pi-ai/oauth";
10
+ //#region src/chat/oauth-page/server.ts
11
+ const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1";
12
+ function buildRequestHandler(opts, pending) {
13
+ return (req, res) => {
14
+ try {
15
+ const url = new URL(req.url || "", "http://localhost");
16
+ if (url.pathname !== opts.path) {
17
+ respondError(res, 404, opts, "Callback route not found.");
18
+ return;
19
+ }
20
+ const error = url.searchParams.get("error");
21
+ if (error) {
22
+ respondError(res, 400, opts, `${opts.providerName} authentication did not complete.`, `Error: ${error}`);
23
+ return;
24
+ }
25
+ const code = url.searchParams.get("code");
26
+ const state = url.searchParams.get("state");
27
+ if (!code || !state) {
28
+ respondError(res, 400, opts, "Missing code or state parameter.");
29
+ return;
30
+ }
31
+ if (state !== opts.expectedState) {
32
+ respondError(res, 400, opts, "State mismatch.");
33
+ return;
34
+ }
35
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
36
+ res.end(opts.renderPage({
37
+ kind: "success",
38
+ provider: opts.providerName,
39
+ message: opts.successMessage
40
+ }));
41
+ pending.settle({
42
+ code,
43
+ state
44
+ });
45
+ } catch {
46
+ respondError(res, 500, opts, "Internal error while processing the callback.");
47
+ }
48
+ };
49
+ }
50
+ function respondError(res, status, opts, message, details) {
51
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
52
+ res.end(opts.renderPage({
53
+ kind: "error",
54
+ provider: opts.providerName,
55
+ message,
56
+ details
57
+ }));
58
+ }
59
+ function buildStubHandle(redirectUri, server) {
60
+ return {
61
+ redirectUri,
62
+ waitForCode: async () => null,
63
+ cancelWait: () => {},
64
+ close: () => {
65
+ try {
66
+ server?.close();
67
+ } catch {}
68
+ }
69
+ };
70
+ }
71
+ /**
72
+ * Start the loopback HTTP callback server. The returned handle is the
73
+ * caller's contract — they `await waitForCode()`, race it against manual
74
+ * paste, then `close()` in a `finally`.
75
+ */
76
+ async function startCallbackServer(opts) {
77
+ const onListenError = opts.onListenError ?? "reject";
78
+ const redirectUri = `http://localhost:${opts.port}${opts.path}`;
79
+ return new Promise((resolve, reject) => {
80
+ let settled = false;
81
+ const pending = { settle: () => {} };
82
+ const waitPromise = new Promise((resolveWait) => {
83
+ pending.settle = (value) => {
84
+ if (settled) return;
85
+ settled = true;
86
+ resolveWait(value);
87
+ };
88
+ });
89
+ const server = createServer(buildRequestHandler(opts, pending));
90
+ server.on("error", (err) => {
91
+ if (onListenError === "resolveWithStub") {
92
+ pending.settle(null);
93
+ resolve(buildStubHandle(redirectUri, server));
94
+ return;
95
+ }
96
+ reject(err);
97
+ });
98
+ server.listen(opts.port, CALLBACK_HOST, () => {
99
+ resolve({
100
+ redirectUri,
101
+ waitForCode: () => waitPromise,
102
+ cancelWait: () => pending.settle(null),
103
+ close: () => {
104
+ try {
105
+ server.close();
106
+ } catch {}
107
+ }
108
+ });
109
+ });
110
+ });
111
+ }
112
+ //#endregion
113
+ //#region src/chat/oauth-page/anthropic.ts
114
+ const CLIENT_ID$1 = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
115
+ const AUTHORIZE_URL$1 = "https://claude.ai/oauth/authorize";
116
+ const TOKEN_URL$1 = "https://platform.claude.com/v1/oauth/token";
117
+ const CALLBACK_PORT$1 = 53692;
118
+ const CALLBACK_PATH$1 = "/callback";
119
+ const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
120
+ const PROVIDER_NAME$1 = "Anthropic";
121
+ /**
122
+ * Parse what the user pasted into the manual-code prompt. Accepts:
123
+ * - the bare authorization code
124
+ * - the full `redirect_uri?code=...&state=...` URL
125
+ * - the `code#state` shorthand Anthropic surfaces on the page
126
+ * - a raw query string with `code=...&state=...`
127
+ *
128
+ * Matches pi-ai's parse table so an existing user's muscle memory still works.
129
+ */
130
+ function parseAuthorizationInput$1(input) {
131
+ const value = input.trim();
132
+ if (!value) return {};
133
+ try {
134
+ const url = new URL(value);
135
+ return {
136
+ code: url.searchParams.get("code") ?? void 0,
137
+ state: url.searchParams.get("state") ?? void 0
138
+ };
139
+ } catch {}
140
+ if (value.includes("#")) {
141
+ const [code, state] = value.split("#", 2);
142
+ return {
143
+ code,
144
+ state
145
+ };
146
+ }
147
+ if (value.includes("code=")) {
148
+ const params = new URLSearchParams(value);
149
+ return {
150
+ code: params.get("code") ?? void 0,
151
+ state: params.get("state") ?? void 0
152
+ };
153
+ }
154
+ return { code: value };
155
+ }
156
+ async function postJson(url, body) {
157
+ const response = await fetch(url, {
158
+ method: "POST",
159
+ headers: {
160
+ "Content-Type": "application/json",
161
+ "Accept": "application/json"
162
+ },
163
+ body: JSON.stringify(body),
164
+ signal: AbortSignal.timeout(3e4)
165
+ });
166
+ const responseBody = await response.text();
167
+ if (!response.ok) throw new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);
168
+ return responseBody;
169
+ }
170
+ async function exchangeAuthorizationCode$1(code, state, verifier, redirectUri) {
171
+ const responseBody = await postJson(TOKEN_URL$1, {
172
+ grant_type: "authorization_code",
173
+ client_id: CLIENT_ID$1,
174
+ code,
175
+ state,
176
+ redirect_uri: redirectUri,
177
+ code_verifier: verifier
178
+ });
179
+ const tokenData = JSON.parse(responseBody);
180
+ return {
181
+ refresh: tokenData.refresh_token,
182
+ access: tokenData.access_token,
183
+ expires: Date.now() + tokenData.expires_in * 1e3 - 300 * 1e3
184
+ };
185
+ }
186
+ async function loginAnthropicWithCustomPage(options) {
187
+ const { verifier, challenge } = await generatePkce();
188
+ const server = await startCallbackServer({
189
+ port: CALLBACK_PORT$1,
190
+ path: CALLBACK_PATH$1,
191
+ expectedState: verifier,
192
+ providerName: PROVIDER_NAME$1,
193
+ renderPage: options.renderPage,
194
+ successMessage: "Anthropic authentication completed. You can close this window.",
195
+ onListenError: "reject"
196
+ });
197
+ let code;
198
+ let state;
199
+ try {
200
+ const authParams = new URLSearchParams({
201
+ code: "true",
202
+ client_id: CLIENT_ID$1,
203
+ response_type: "code",
204
+ redirect_uri: server.redirectUri,
205
+ scope: SCOPES,
206
+ code_challenge: challenge,
207
+ code_challenge_method: "S256",
208
+ state: verifier
209
+ });
210
+ options.onAuth({
211
+ url: `${AUTHORIZE_URL$1}?${authParams.toString()}`,
212
+ instructions: "Complete login in your browser. If the browser is on another machine, paste the final redirect URL here."
213
+ });
214
+ if (options.onManualCodeInput) {
215
+ let manualInput;
216
+ let manualError;
217
+ const manualPromise = options.onManualCodeInput().then((input) => {
218
+ manualInput = input;
219
+ server.cancelWait();
220
+ }).catch((err) => {
221
+ manualError = err instanceof Error ? err : new Error(String(err));
222
+ server.cancelWait();
223
+ });
224
+ const result = await server.waitForCode();
225
+ if (manualError) throw manualError;
226
+ if (result?.code) {
227
+ code = result.code;
228
+ state = result.state;
229
+ } else if (manualInput) {
230
+ const parsed = parseAuthorizationInput$1(manualInput);
231
+ if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
232
+ code = parsed.code;
233
+ state = parsed.state ?? verifier;
234
+ }
235
+ if (!code) {
236
+ await manualPromise;
237
+ if (manualError) throw manualError;
238
+ if (manualInput) {
239
+ const parsed = parseAuthorizationInput$1(manualInput);
240
+ if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
241
+ code = parsed.code;
242
+ state = parsed.state ?? verifier;
243
+ }
244
+ }
245
+ } else {
246
+ const result = await server.waitForCode();
247
+ if (result?.code) {
248
+ code = result.code;
249
+ state = result.state;
250
+ }
251
+ }
252
+ if (!code) {
253
+ const parsed = parseAuthorizationInput$1(await options.onPrompt({
254
+ message: "Paste the authorization code or full redirect URL:",
255
+ placeholder: server.redirectUri
256
+ }));
257
+ if (parsed.state && parsed.state !== verifier) throw new Error("OAuth state mismatch");
258
+ code = parsed.code;
259
+ state = parsed.state ?? verifier;
260
+ }
261
+ if (!code) throw new Error("Missing authorization code");
262
+ if (!state) throw new Error("Missing OAuth state");
263
+ options.onProgress?.("Exchanging authorization code for tokens...");
264
+ return await exchangeAuthorizationCode$1(code, state, verifier, server.redirectUri);
265
+ } finally {
266
+ server.close();
267
+ }
268
+ }
269
+ /**
270
+ * Build an `OAuthProviderInterface` that behaves identically to pi-ai's
271
+ * `anthropicOAuthProvider` except for the callback page HTML. Drop this
272
+ * onto `ProviderDescriptor.oauthProvider` to override.
273
+ */
274
+ function createAnthropicOAuthProviderWithCustomPage(renderPage) {
275
+ return {
276
+ id: "anthropic",
277
+ name: "Anthropic (Claude Pro/Max)",
278
+ usesCallbackServer: true,
279
+ async login(callbacks) {
280
+ return loginAnthropicWithCustomPage({
281
+ renderPage,
282
+ onAuth: callbacks.onAuth,
283
+ onPrompt: callbacks.onPrompt,
284
+ onProgress: callbacks.onProgress,
285
+ onManualCodeInput: callbacks.onManualCodeInput
286
+ });
287
+ },
288
+ async refreshToken(credentials) {
289
+ return refreshAnthropicToken(credentials.refresh);
290
+ },
291
+ getApiKey(credentials) {
292
+ return credentials.access;
293
+ }
294
+ };
295
+ }
296
+ //#endregion
297
+ //#region src/chat/oauth-page/openai-codex.ts
298
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
299
+ const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
300
+ const TOKEN_URL = "https://auth.openai.com/oauth/token";
301
+ const CALLBACK_PORT = 1455;
302
+ const CALLBACK_PATH = "/auth/callback";
303
+ const SCOPE = "openid profile email offline_access";
304
+ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
305
+ const PROVIDER_NAME = "OpenAI";
306
+ function createState() {
307
+ return randomBytes(16).toString("hex");
308
+ }
309
+ function parseAuthorizationInput(input) {
310
+ const value = input.trim();
311
+ if (!value) return {};
312
+ try {
313
+ const url = new URL(value);
314
+ return {
315
+ code: url.searchParams.get("code") ?? void 0,
316
+ state: url.searchParams.get("state") ?? void 0
317
+ };
318
+ } catch {}
319
+ if (value.includes("#")) {
320
+ const [code, state] = value.split("#", 2);
321
+ return {
322
+ code,
323
+ state
324
+ };
325
+ }
326
+ if (value.includes("code=")) {
327
+ const params = new URLSearchParams(value);
328
+ return {
329
+ code: params.get("code") ?? void 0,
330
+ state: params.get("state") ?? void 0
331
+ };
332
+ }
333
+ return { code: value };
334
+ }
335
+ function decodeJwt(token) {
336
+ try {
337
+ const parts = token.split(".");
338
+ if (parts.length !== 3) return null;
339
+ const payload = parts[1] ?? "";
340
+ const decoded = atob(payload);
341
+ return JSON.parse(decoded);
342
+ } catch {
343
+ return null;
344
+ }
345
+ }
346
+ function getAccountId(accessToken) {
347
+ const accountId = (decodeJwt(accessToken)?.[JWT_CLAIM_PATH])?.chatgpt_account_id;
348
+ return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
349
+ }
350
+ async function exchangeAuthorizationCode(code, verifier, redirectUri) {
351
+ const response = await fetch(TOKEN_URL, {
352
+ method: "POST",
353
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
354
+ body: new URLSearchParams({
355
+ grant_type: "authorization_code",
356
+ client_id: CLIENT_ID,
357
+ code,
358
+ code_verifier: verifier,
359
+ redirect_uri: redirectUri
360
+ })
361
+ });
362
+ if (!response.ok) {
363
+ const text = await response.text().catch(() => "");
364
+ return {
365
+ type: "failed",
366
+ message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`
367
+ };
368
+ }
369
+ const json = await response.json();
370
+ if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") return {
371
+ type: "failed",
372
+ message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`
373
+ };
374
+ return {
375
+ type: "success",
376
+ access: json.access_token,
377
+ refresh: json.refresh_token,
378
+ expires: Date.now() + json.expires_in * 1e3
379
+ };
380
+ }
381
+ async function loginOpenAICodexWithCustomPage(options) {
382
+ const { verifier, challenge } = await generatePkce();
383
+ const state = createState();
384
+ const originator = options.originator ?? "pi";
385
+ const server = await startCallbackServer({
386
+ port: CALLBACK_PORT,
387
+ path: CALLBACK_PATH,
388
+ expectedState: state,
389
+ providerName: PROVIDER_NAME,
390
+ renderPage: options.renderPage,
391
+ successMessage: "OpenAI authentication completed. You can close this window.",
392
+ onListenError: "resolveWithStub"
393
+ });
394
+ const authUrl = new URL(AUTHORIZE_URL);
395
+ authUrl.searchParams.set("response_type", "code");
396
+ authUrl.searchParams.set("client_id", CLIENT_ID);
397
+ authUrl.searchParams.set("redirect_uri", server.redirectUri);
398
+ authUrl.searchParams.set("scope", SCOPE);
399
+ authUrl.searchParams.set("code_challenge", challenge);
400
+ authUrl.searchParams.set("code_challenge_method", "S256");
401
+ authUrl.searchParams.set("state", state);
402
+ authUrl.searchParams.set("id_token_add_organizations", "true");
403
+ authUrl.searchParams.set("codex_cli_simplified_flow", "true");
404
+ authUrl.searchParams.set("originator", originator);
405
+ options.onAuth({
406
+ url: authUrl.toString(),
407
+ instructions: "A browser window should open. Complete login to finish."
408
+ });
409
+ let code;
410
+ try {
411
+ if (options.onManualCodeInput) {
412
+ let manualCode;
413
+ let manualError;
414
+ const manualPromise = options.onManualCodeInput().then((input) => {
415
+ manualCode = input;
416
+ server.cancelWait();
417
+ }).catch((err) => {
418
+ manualError = err instanceof Error ? err : new Error(String(err));
419
+ server.cancelWait();
420
+ });
421
+ const result = await server.waitForCode();
422
+ if (manualError) throw manualError;
423
+ if (result?.code) code = result.code;
424
+ else if (manualCode) {
425
+ const parsed = parseAuthorizationInput(manualCode);
426
+ if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
427
+ code = parsed.code;
428
+ }
429
+ if (!code) {
430
+ await manualPromise;
431
+ if (manualError) throw manualError;
432
+ if (manualCode) {
433
+ const parsed = parseAuthorizationInput(manualCode);
434
+ if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
435
+ code = parsed.code;
436
+ }
437
+ }
438
+ } else {
439
+ const result = await server.waitForCode();
440
+ if (result?.code) code = result.code;
441
+ }
442
+ if (!code) {
443
+ const parsed = parseAuthorizationInput(await options.onPrompt({ message: "Paste the authorization code (or full redirect URL):" }));
444
+ if (parsed.state && parsed.state !== state) throw new Error("State mismatch");
445
+ code = parsed.code;
446
+ }
447
+ if (!code) throw new Error("Missing authorization code");
448
+ const tokenResult = await exchangeAuthorizationCode(code, verifier, server.redirectUri);
449
+ if (tokenResult.type !== "success") throw new Error(tokenResult.message);
450
+ const accountId = getAccountId(tokenResult.access);
451
+ if (!accountId) throw new Error("Failed to extract accountId from token");
452
+ return {
453
+ access: tokenResult.access,
454
+ refresh: tokenResult.refresh,
455
+ expires: tokenResult.expires,
456
+ accountId
457
+ };
458
+ } finally {
459
+ server.close();
460
+ }
461
+ }
462
+ /**
463
+ * Build an `OAuthProviderInterface` that behaves identically to pi-ai's
464
+ * `openaiCodexOAuthProvider` except for the callback page HTML.
465
+ */
466
+ function createOpenAICodexOAuthProviderWithCustomPage(renderPage) {
467
+ return {
468
+ id: "openai-codex",
469
+ name: "ChatGPT Plus/Pro (Codex Subscription)",
470
+ usesCallbackServer: true,
471
+ async login(callbacks) {
472
+ return loginOpenAICodexWithCustomPage({
473
+ renderPage,
474
+ onAuth: callbacks.onAuth,
475
+ onPrompt: callbacks.onPrompt,
476
+ onProgress: callbacks.onProgress,
477
+ onManualCodeInput: callbacks.onManualCodeInput
478
+ });
479
+ },
480
+ async refreshToken(credentials) {
481
+ return refreshOpenAICodexToken(credentials.refresh);
482
+ },
483
+ getApiKey(credentials) {
484
+ return credentials.access;
485
+ }
486
+ };
487
+ }
488
+ //#endregion
489
+ //#region src/chat/oauth-page/render.ts
490
+ function escapeHtml(value) {
491
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
492
+ }
493
+ /**
494
+ * Default zidane-themed page. Visually neutral but distinguishable from
495
+ * pi-ai's stock page — dark background, mono headings, no logo (host can
496
+ * pass a custom renderer to add one).
497
+ */
498
+ const renderDefaultCallbackPage = (page) => {
499
+ const heading = escapeHtml(page.kind === "success" ? `Signed in to ${page.provider}` : `Could not sign in to ${page.provider}`);
500
+ const message = escapeHtml(page.message);
501
+ const details = page.details ? escapeHtml(page.details) : void 0;
502
+ return `<!doctype html>
503
+ <html lang="en">
504
+ <head>
505
+ <meta charset="utf-8" />
506
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
507
+ <title>${heading}</title>
508
+ <style>
509
+ :root {
510
+ --text: #f4f4f5;
511
+ --text-dim: #a1a1aa;
512
+ --accent: ${page.kind === "success" ? "#22d3ee" : "#f87171"};
513
+ --page-bg: #0a0a0a;
514
+ --panel-bg: #131316;
515
+ --border: #27272a;
516
+ --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
517
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
518
+ }
519
+ * { box-sizing: border-box; }
520
+ html { color-scheme: dark; }
521
+ body {
522
+ margin: 0;
523
+ min-height: 100vh;
524
+ display: flex;
525
+ align-items: center;
526
+ justify-content: center;
527
+ padding: 24px;
528
+ background: var(--page-bg);
529
+ color: var(--text);
530
+ font-family: var(--font-sans);
531
+ }
532
+ main {
533
+ width: 100%;
534
+ max-width: 520px;
535
+ padding: 32px;
536
+ background: var(--panel-bg);
537
+ border: 1px solid var(--border);
538
+ border-radius: 12px;
539
+ }
540
+ .label {
541
+ font-family: var(--font-mono);
542
+ font-size: 12px;
543
+ letter-spacing: 0.08em;
544
+ text-transform: uppercase;
545
+ color: var(--accent);
546
+ margin-bottom: 12px;
547
+ }
548
+ h1 {
549
+ margin: 0 0 12px;
550
+ font-size: 22px;
551
+ font-weight: 600;
552
+ letter-spacing: -0.01em;
553
+ color: var(--text);
554
+ }
555
+ p {
556
+ margin: 0;
557
+ line-height: 1.6;
558
+ color: var(--text-dim);
559
+ font-size: 14px;
560
+ }
561
+ .details {
562
+ margin-top: 18px;
563
+ padding: 12px 14px;
564
+ background: var(--page-bg);
565
+ border: 1px solid var(--border);
566
+ border-radius: 8px;
567
+ font-family: var(--font-mono);
568
+ font-size: 12px;
569
+ color: var(--text-dim);
570
+ white-space: pre-wrap;
571
+ word-break: break-word;
572
+ }
573
+ </style>
574
+ </head>
575
+ <body>
576
+ <main>
577
+ <div class="label">zidane · OAuth · ${escapeHtml(page.provider)}</div>
578
+ <h1>${heading}</h1>
579
+ <p>${message}</p>
580
+ ${details ? `<div class="details">${details}</div>` : ""}
581
+ </main>
582
+ </body>
583
+ </html>`;
584
+ };
585
+ //#endregion
586
+ //#region src/chat/oauth-page/index.ts
587
+ /**
588
+ * Bundle helper — returns both Anthropic + OpenAI Codex providers wired
589
+ * with the same renderer. Hosts that want different renderers per provider
590
+ * should call the individual `create…WithCustomPage` factories instead.
591
+ */
592
+ function createCustomCallbackOAuthProviders(renderPage) {
593
+ return {
594
+ anthropic: createAnthropicOAuthProviderWithCustomPage(renderPage),
595
+ openaiCodex: createOpenAICodexOAuthProviderWithCustomPage(renderPage)
596
+ };
597
+ }
598
+ let cursorRegistered = false;
599
+ /**
600
+ * Register Cursor with pi-ai's OAuth registry.
601
+ *
602
+ * Unlike Anthropic / Codex, Cursor is **not** built into pi-ai, so
603
+ * `getOAuthApiKey('cursor', …)` (used by `resolveOAuthApiKey` for lazy
604
+ * token refresh) would throw "Unknown OAuth provider" until we register it.
605
+ * Call once at startup from both the CLI auth path and the TUI. Idempotent.
606
+ */
607
+ function registerCursorOAuthProvider() {
608
+ const provider = createCursorOAuthProvider();
609
+ if (!cursorRegistered) {
610
+ registerOAuthProvider(provider);
611
+ cursorRegistered = true;
612
+ }
613
+ return provider;
614
+ }
615
+ let xaiRegistered = false;
616
+ /**
617
+ * Register xAI Grok with pi-ai's OAuth registry.
618
+ *
619
+ * pi-ai ships an xAI *API-key* provider, but no OAuth one — so
620
+ * `getOAuthApiKey('xai-oauth', …)` (lazy refresh in `resolveOAuthApiKey`)
621
+ * would throw "Unknown OAuth provider" until we register our flow. The
622
+ * callback page routes through the supplied renderer so the post-redirect
623
+ * HTML matches zidane's theme. Call once at startup; idempotent.
624
+ */
625
+ function registerXaiOAuthProvider(renderPage) {
626
+ const provider = createXaiOAuthProvider(renderPage);
627
+ if (!xaiRegistered) {
628
+ registerOAuthProvider(provider);
629
+ xaiRegistered = true;
630
+ }
631
+ return provider;
632
+ }
633
+ //#endregion
634
+ //#region src/chat/providers.ts
635
+ /**
636
+ * pi-ai's stock Anthropic + Codex OAuth providers bake their own callback
637
+ * HTML; we route both through `renderDefaultCallbackPage` so the post-redirect
638
+ * page matches zidane's theme. The override lives in `src/chat/oauth-page/`
639
+ * — see that folder's `index.ts` for the deletion path once pi-ai exposes
640
+ * a `renderCallbackPage` hook upstream.
641
+ */
642
+ const { anthropic: anthropicOAuthProvider, openaiCodex: openaiCodexOAuthProvider } = createCustomCallbackOAuthProviders(renderDefaultCallbackPage);
643
+ registerCursorOAuthProvider();
644
+ /**
645
+ * xAI Grok isn't built into pi-ai's OAuth registry (only its API-key provider
646
+ * is), so register our loopback-PKCE flow — themed via `renderDefaultCallbackPage`
647
+ * — so lazy token refresh resolves through it.
648
+ */
649
+ const xaiOAuthProvider = registerXaiOAuthProvider(renderDefaultCallbackPage);
650
+ /** Convenience accessor — returns `credentialFileKey ?? key`. */
651
+ function credKeyOf(desc) {
652
+ return desc.credentialFileKey ?? desc.key;
653
+ }
654
+ /** Convenience accessor — returns `piProviderId ?? key`. */
655
+ function piIdOf(desc) {
656
+ return desc.piProviderId ?? desc.key;
657
+ }
658
+ const anthropicDescriptor = {
659
+ key: "anthropic",
660
+ label: "Anthropic",
661
+ factory: anthropic,
662
+ defaultModel: "claude-opus-4-8",
663
+ envKey: "ANTHROPIC_API_KEY",
664
+ apiKeyPlaceholder: "sk-ant-…",
665
+ oauthProvider: anthropicOAuthProvider,
666
+ oauthHint: "Claude Pro/Max subscription",
667
+ extraModels: ANTHROPIC_EXTRA_MODELS,
668
+ optionsFor: (id) => FAST_MODE_OPTIONS[id] ? [FAST_MODE_OPTIONS[id]] : void 0
669
+ };
670
+ const openaiDescriptor = {
671
+ key: "openai",
672
+ label: "OpenAI",
673
+ factory: openai,
674
+ defaultModel: "gpt-5.5",
675
+ envKey: "OPENAI_CODEX_API_KEY",
676
+ credentialFileKey: "openai-codex",
677
+ piProviderId: "openai-codex",
678
+ apiKeyPlaceholder: "sk-… or eyJ… (Codex)",
679
+ oauthProvider: openaiCodexOAuthProvider
680
+ };
681
+ const openrouterDescriptor = {
682
+ key: "openrouter",
683
+ label: "OpenRouter",
684
+ factory: openrouter,
685
+ defaultModel: "anthropic/claude-sonnet-4-6",
686
+ envKey: "OPENROUTER_API_KEY",
687
+ apiKeyPlaceholder: "sk-or-…"
688
+ };
689
+ const cerebrasDescriptor = {
690
+ key: "cerebras",
691
+ label: "Cerebras",
692
+ factory: cerebras,
693
+ defaultModel: "zai-glm-4.7",
694
+ envKey: "CEREBRAS_API_KEY",
695
+ apiKeyPlaceholder: "csk-…"
696
+ };
697
+ /**
698
+ * xAI Grok. Supports BOTH a static API key (`XAI_API_KEY`) and the
699
+ * SuperGrok / X Premium+ OAuth subscription, so the wizard offers both methods.
700
+ * Models come from pi-ai's `xai` registry (OpenAI-compatible endpoint), shared
701
+ * by both auth modes.
702
+ */
703
+ const xaiDescriptor = {
704
+ key: "xai",
705
+ label: "xAI Grok",
706
+ factory: xai,
707
+ defaultModel: "grok-4.3",
708
+ envKey: "XAI_API_KEY",
709
+ apiKeyPlaceholder: "xai-…",
710
+ oauthProvider: xaiOAuthProvider,
711
+ oauthHint: "SuperGrok / X Premium+ subscription",
712
+ credentialFileKey: "xai-oauth",
713
+ piProviderId: "xai"
714
+ };
715
+ const arceeDescriptor = {
716
+ key: "arcee",
717
+ label: "Arcee",
718
+ factory: arcee,
719
+ defaultModel: "trinity-large-thinking",
720
+ envKey: "ARCEE_API_KEY",
721
+ apiKeyPlaceholder: "arcee-…",
722
+ models: [
723
+ {
724
+ id: "trinity-large-thinking",
725
+ name: "Trinity Large (Thinking)",
726
+ contextWindow: 256e3,
727
+ reasoning: true,
728
+ input: ["text"],
729
+ cost: {
730
+ input: .25,
731
+ output: .8
732
+ }
733
+ },
734
+ {
735
+ id: "trinity-large-preview",
736
+ name: "Trinity Large (Preview)",
737
+ contextWindow: 128e3,
738
+ input: ["text"],
739
+ cost: {
740
+ input: .45,
741
+ output: .15
742
+ }
743
+ },
744
+ {
745
+ id: "trinity-mini",
746
+ name: "Trinity Mini",
747
+ contextWindow: 128e3,
748
+ input: ["text"],
749
+ cost: {
750
+ input: .045,
751
+ output: .15
752
+ }
753
+ }
754
+ ]
755
+ };
756
+ const basetenDescriptor = {
757
+ key: "baseten",
758
+ label: "Baseten",
759
+ factory: baseten,
760
+ defaultModel: "zai-org/GLM-5.1",
761
+ envKey: "BASETEN_API_KEY",
762
+ apiKeyPlaceholder: "baseten-api-key…",
763
+ models: [
764
+ {
765
+ id: "zai-org/GLM-5.2",
766
+ name: "GLM 5.2",
767
+ contextWindow: 1e6,
768
+ reasoning: true,
769
+ input: ["text"]
770
+ },
771
+ {
772
+ id: "zai-org/GLM-5.1",
773
+ name: "GLM 5.1",
774
+ contextWindow: 2e5,
775
+ reasoning: true,
776
+ input: ["text"]
777
+ },
778
+ {
779
+ id: "zai-org/GLM-5",
780
+ name: "GLM 5",
781
+ contextWindow: 2e5,
782
+ reasoning: true,
783
+ input: ["text"],
784
+ cost: {
785
+ input: .95,
786
+ output: 3.15,
787
+ cacheRead: .2
788
+ }
789
+ },
790
+ {
791
+ id: "zai-org/GLM-4.7",
792
+ name: "GLM 4.7",
793
+ contextWindow: 2e5,
794
+ reasoning: true,
795
+ input: ["text"],
796
+ cost: {
797
+ input: .6,
798
+ output: 2.2,
799
+ cacheRead: .12
800
+ }
801
+ },
802
+ {
803
+ id: "moonshotai/Kimi-K2.6",
804
+ name: "Kimi K2.6",
805
+ contextWindow: 256e3,
806
+ reasoning: true,
807
+ input: ["text", "image"],
808
+ cost: {
809
+ input: 1,
810
+ output: 3.9,
811
+ cacheRead: .2
812
+ }
813
+ },
814
+ {
815
+ id: "moonshotai/Kimi-K2.5",
816
+ name: "Kimi K2.5",
817
+ contextWindow: 256e3,
818
+ reasoning: true,
819
+ input: ["text", "image"],
820
+ cost: {
821
+ input: .6,
822
+ output: 3,
823
+ cacheRead: .12
824
+ }
825
+ },
826
+ {
827
+ id: "deepseek-ai/DeepSeek-V4-Pro",
828
+ name: "DeepSeek V4 Pro",
829
+ contextWindow: 131e3,
830
+ reasoning: true,
831
+ input: ["text"],
832
+ cost: {
833
+ input: 1.74,
834
+ output: 3.48,
835
+ cacheRead: .145
836
+ }
837
+ },
838
+ {
839
+ id: "nvidia/Nemotron-120B-A12B",
840
+ name: "Nemotron Super",
841
+ contextWindow: 2e5,
842
+ reasoning: true,
843
+ input: ["text"],
844
+ cost: {
845
+ input: .3,
846
+ output: .75,
847
+ cacheRead: .06
848
+ }
849
+ },
850
+ {
851
+ id: "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B",
852
+ name: "Nemotron Ultra",
853
+ contextWindow: 2e5,
854
+ reasoning: true,
855
+ input: ["text"]
856
+ },
857
+ {
858
+ id: "openai/gpt-oss-120b",
859
+ name: "GPT-OSS 120B",
860
+ contextWindow: 128e3,
861
+ reasoning: true,
862
+ input: ["text"],
863
+ cost: {
864
+ input: .1,
865
+ output: .5
866
+ }
867
+ }
868
+ ]
869
+ };
870
+ /**
871
+ * Conservative context-window assumption for a user-configured local model.
872
+ * Local runtimes expose no reliable per-model metadata over the OpenAI-compat
873
+ * `/v1/models` endpoint (it returns ids, not context sizes), so we pick a
874
+ * floor that fits the smallest mainstream OSS models (Llama 3.x 8B, Qwen2.5)
875
+ * without over-promising. The footer's context indicator and auto-compaction
876
+ * lean on this — under-estimating is the safe direction (compact early rather
877
+ * than overflow the server's real window).
878
+ *
879
+ * Users running larger-context models (e.g. a 128K Qwen) can raise it via the
880
+ * `LOCAL_LLM_CONTEXT_WINDOW` env var / customField, which supports a single
881
+ * value or a per-model map (resolved by {@link resolveContextWindow}).
882
+ */
883
+ const LOCAL_DEFAULT_CONTEXT_WINDOW = 8192;
884
+ /**
885
+ * Local OpenAI-compatible LLM (Ollama, vLLM, LM Studio, Lemonade, llama.cpp).
886
+ *
887
+ * No fixed base URL or model catalogue — both come from the user via
888
+ * `customFields`. No `envKey`, so the wizard skips the API-key prompt and
889
+ * treats the customFields-only credential as the auth signal (see
890
+ * `applyApiKeyEnv` + `detectAuth`).
891
+ *
892
+ * Vision / cache / reasoning are off by default in the factory — flip them
893
+ * by passing a custom descriptor that calls `local({ capabilities })`.
894
+ *
895
+ * `models` is a getter, not a static list: there's no fixed catalogue to ship,
896
+ * but once the user has configured a default model (mirrored into
897
+ * `LOCAL_LLM_DEFAULT_MODEL` by `applyApiKeyEnv`), we surface it as a
898
+ * single-entry list so the model picker + footer aren't empty. Resolved at
899
+ * access time because the env var is populated at TUI launch, after this
900
+ * module loads. Empty list when unconfigured — the picker hides, the user
901
+ * types a slug at the prompt as before.
902
+ */
903
+ const localDescriptor = {
904
+ key: "local",
905
+ label: "Local LLM",
906
+ factory: local,
907
+ get models() {
908
+ const configured = process.env.LOCAL_LLM_DEFAULT_MODEL?.trim();
909
+ if (!configured) return [];
910
+ return [{
911
+ id: configured,
912
+ name: configured,
913
+ contextWindow: resolveContextWindow(process.env.LOCAL_LLM_CONTEXT_WINDOW, configured) ?? LOCAL_DEFAULT_CONTEXT_WINDOW,
914
+ input: ["text"]
915
+ }];
916
+ },
917
+ customFields: [
918
+ {
919
+ key: "baseURL",
920
+ label: "Base URL",
921
+ envVar: "LOCAL_LLM_BASE_URL",
922
+ placeholder: "http://localhost:11434/v1",
923
+ hint: "Where your local LLM server is reachable. Examples: Ollama → http://localhost:11434/v1, vLLM → http://localhost:8000/v1, LM Studio → http://localhost:1234/v1.",
924
+ required: true
925
+ },
926
+ {
927
+ key: "apiKey",
928
+ label: "API key",
929
+ envVar: "LOCAL_LLM_API_KEY",
930
+ placeholder: "leave blank if your server is unauthenticated"
931
+ },
932
+ {
933
+ key: "model",
934
+ label: "Default model",
935
+ envVar: "LOCAL_LLM_DEFAULT_MODEL",
936
+ placeholder: "e.g. llama3.1:8b, qwen2.5-coder, mistral-small",
937
+ hint: "Model id your server exposes. Leave blank to pick later from the model picker."
938
+ },
939
+ {
940
+ key: "contextWindow",
941
+ label: "Context window (tokens)",
942
+ envVar: "LOCAL_LLM_CONTEXT_WINDOW",
943
+ placeholder: "e.g. 32768 · or per-model: llama3.1:8b=32768, qwen2.5-coder=128k, 64k",
944
+ hint: "Context length(s) for your local model(s) — local runtimes don't advertise this, so set it here to enable the context indicator and auto-compaction. Use a single value (32768, 128k) for all models, or a comma-separated map of `model=window` with an optional bare value as the fallback. Press"
945
+ }
946
+ ],
947
+ contextWindowEnvVar: "LOCAL_LLM_CONTEXT_WINDOW"
948
+ };
949
+ /**
950
+ * Default provider registry. Passed verbatim when `runTui` is invoked without
951
+ * an explicit `providers` option. Hosts that want to override per-provider
952
+ * metadata can spread this and replace specific entries:
953
+ *
954
+ * ```ts
955
+ * runTui({ providers: { ...BUILTIN_PROVIDERS, anthropic: myOwnAnthropicDescriptor } })
956
+ * ```
957
+ *
958
+ * `cursor` is intentionally NOT registered here: OAuth works, but inference
959
+ * isn't wired (`cursor.stream()` throws — Cursor speaks a protobuf/HTTP-2
960
+ * agent protocol, not OpenAI-compat). Shipping it in the picker only lets users
961
+ * select a model that errors every turn. The descriptor stays exported so a
962
+ * host can opt in (`{ ...BUILTIN_PROVIDERS, cursor: cursorDescriptor }`) once
963
+ * the transport lands — re-add it here at that point.
964
+ */
965
+ const BUILTIN_PROVIDERS = {
966
+ anthropic: anthropicDescriptor,
967
+ openai: openaiDescriptor,
968
+ openrouter: openrouterDescriptor,
969
+ cerebras: cerebrasDescriptor,
970
+ xai: xaiDescriptor,
971
+ arcee: arceeDescriptor,
972
+ baseten: basetenDescriptor,
973
+ local: localDescriptor
974
+ };
975
+ /**
976
+ * Resolve the model list for a given provider. Honors `descriptor.models`
977
+ * when set; otherwise queries pi-ai via `descriptor.piProviderId`. Returns
978
+ * `[]` for descriptors with no known mapping (custom providers without a
979
+ * model list) — callers should hide the model picker in that case.
980
+ */
981
+ function modelsForDescriptor(descriptor) {
982
+ if (descriptor.models) return descriptor.models;
983
+ let bundled;
984
+ try {
985
+ bundled = getModels(piIdOf(descriptor));
986
+ } catch {
987
+ bundled = [];
988
+ }
989
+ if (descriptor.extraModels?.length) {
990
+ const bundledIds = new Set(bundled.map((m) => m.id));
991
+ bundled = [...descriptor.extraModels.filter((m) => !bundledIds.has(m.id)), ...bundled];
992
+ }
993
+ return descriptor.optionsFor ? bundled.map((m) => decorateOptions(m, descriptor)) : bundled;
994
+ }
995
+ /**
996
+ * Attach descriptor-resolved {@link ModelOption}s to a model that doesn't
997
+ * already declare its own. Pure / non-mutating — returns the input untouched
998
+ * when the model has options already or the descriptor resolves none.
999
+ */
1000
+ function decorateOptions(model, descriptor) {
1001
+ if (model.options?.length || !descriptor.optionsFor) return model;
1002
+ const options = descriptor.optionsFor(model.id);
1003
+ return options?.length ? {
1004
+ ...model,
1005
+ options
1006
+ } : model;
1007
+ }
1008
+ /**
1009
+ * Resolve a single model's metadata via the descriptor's model source.
1010
+ * Mirrors {@link modelsForDescriptor} routing: descriptor's own list wins,
1011
+ * pi-ai's registry is the fallback. Returns `null` when the model isn't
1012
+ * known.
1013
+ */
1014
+ function getModelInfo(descriptor, modelId) {
1015
+ if (descriptor.models) return descriptor.models.find((m) => m.id === modelId) ?? null;
1016
+ try {
1017
+ const bundled = getModel(piIdOf(descriptor), modelId);
1018
+ if (bundled) return decorateOptions(bundled, descriptor);
1019
+ } catch {}
1020
+ const extra = descriptor.extraModels?.find((m) => m.id === modelId);
1021
+ return extra ? decorateOptions(extra, descriptor) : null;
1022
+ }
1023
+ /**
1024
+ * Parse a user-supplied context-window string into a token count.
1025
+ *
1026
+ * Accepts a plain integer (`"32768"`), or a number with a `k`/`m` suffix
1027
+ * (×1_000 / ×1_000_000, case-insensitive, optional space) — so `"128k"`
1028
+ * yields `128_000`, matching how the footer renders windows (`fmtTokens`).
1029
+ * Fractional values are floored (`"1.5k"` → `1500`). Returns `null` for
1030
+ * empty, malformed, zero, or negative input so callers fall through to
1031
+ * "window unknown" rather than a bogus ceiling.
1032
+ */
1033
+ function parseContextWindow(raw) {
1034
+ if (raw == null) return null;
1035
+ const trimmed = raw.trim();
1036
+ if (trimmed.length === 0) return null;
1037
+ const match = /^(\d+(?:\.\d+)?)\s*([km])?$/i.exec(trimmed);
1038
+ if (!match) return null;
1039
+ const suffix = match[2]?.toLowerCase();
1040
+ const mult = suffix === "k" ? 1e3 : suffix === "m" ? 1e6 : 1;
1041
+ const value = Math.floor(Number.parseFloat(match[1]) * mult);
1042
+ return value >= 1 ? value : null;
1043
+ }
1044
+ /**
1045
+ * Resolve a per-model context window from a user-supplied spec string.
1046
+ *
1047
+ * A spec is a comma-separated list of entries. An entry is either:
1048
+ * - `model=window` — a per-model override (split on the **first** `=`, so
1049
+ * model ids containing `:` or `/` survive), or
1050
+ * - a bare `window` — the fallback applied to any model not named above.
1051
+ *
1052
+ * Windows are parsed by {@link parseContextWindow} (plain int or k/m suffix).
1053
+ * Whitespace around entries and the `=` is ignored; malformed entries are
1054
+ * skipped rather than poisoning the whole spec; a later duplicate key wins.
1055
+ *
1056
+ * Lookup order for `modelId`: exact map entry → bare fallback → `null`. A
1057
+ * lone bare value (`"32768"`) therefore behaves as a single global window,
1058
+ * keeping the simple single-model config working.
1059
+ *
1060
+ * @example resolveContextWindow('llama3.1:8b=32768, qwen=128k, 64k', 'qwen') // 128000
1061
+ */
1062
+ function resolveContextWindow(spec, modelId) {
1063
+ if (spec == null) return null;
1064
+ const overrides = /* @__PURE__ */ new Map();
1065
+ let fallback = null;
1066
+ for (const entry of spec.split(",")) {
1067
+ const eq = entry.indexOf("=");
1068
+ if (eq === -1) {
1069
+ const bare = parseContextWindow(entry);
1070
+ if (bare != null) fallback = bare;
1071
+ continue;
1072
+ }
1073
+ const key = entry.slice(0, eq).trim();
1074
+ const window = parseContextWindow(entry.slice(eq + 1));
1075
+ if (key.length > 0 && window != null) overrides.set(key, window);
1076
+ }
1077
+ return overrides.get(modelId) ?? fallback;
1078
+ }
1079
+ /**
1080
+ * Look up the model's max context window via the descriptor's model source.
1081
+ * Falls back to {@link ProviderDescriptor.contextWindowEnvVar} (parsed via
1082
+ * {@link resolveContextWindow}, which supports a per-model map) when the
1083
+ * registry has nothing — this is how local LLMs, whose runtimes don't
1084
+ * advertise a window, get one. Returns `null` when neither source resolves a
1085
+ * value (custom slugs, providers without a registry or a configured window);
1086
+ * callers should hide the context indicator and skip auto-compaction then.
1087
+ */
1088
+ function getContextWindow(descriptor, modelId) {
1089
+ const fromRegistry = getModelInfo(descriptor, modelId)?.contextWindow;
1090
+ if (fromRegistry != null) return fromRegistry;
1091
+ if (descriptor.contextWindowEnvVar) return resolveContextWindow(process.env[descriptor.contextWindowEnvVar], modelId);
1092
+ return null;
1093
+ }
1094
+ /**
1095
+ * Whether the given model exposes a reasoning / extended-thinking knob.
1096
+ * Drives the TUI's effort picker visibility — the `ctrl+n` shortcut and
1097
+ * the bottom-bar `effortName` segment only surface when this is `true`.
1098
+ * Returns `false` for unknown models (no registry entry → no advertised
1099
+ * capability).
1100
+ */
1101
+ function modelSupportsReasoning(descriptor, modelId) {
1102
+ return getModelInfo(descriptor, modelId)?.reasoning === true;
1103
+ }
1104
+ /**
1105
+ * Custom {@link ModelOption}s the given model supports (e.g. fast mode), or `[]`
1106
+ * when none. Drives the TUI/GUI options picker visibility — surfaces only when
1107
+ * non-empty.
1108
+ */
1109
+ function modelOptionsFor(descriptor, modelId) {
1110
+ return getModelInfo(descriptor, modelId)?.options ?? [];
1111
+ }
1112
+ /**
1113
+ * Normalize a model-options blob to an enabled-only `{ id: true }` map.
1114
+ *
1115
+ * Single source of truth shared by every surface that reads or persists model
1116
+ * options — the TUI run path, the GUI engine reader, and the GUI IPC handlers.
1117
+ * Tolerant of arbitrary input (persisted JSON metadata may be anything): a
1118
+ * non-object → `{}`, and only entries strictly equal to `true` survive. This
1119
+ * keeps the persisted shape minimal (no `{ fast: false }` noise) and means
1120
+ * callers never forward a disabled option to `agent.run`.
1121
+ */
1122
+ function enabledModelOptions(raw) {
1123
+ if (!raw || typeof raw !== "object") return {};
1124
+ const out = {};
1125
+ for (const [id, on] of Object.entries(raw)) if (on === true) out[id] = true;
1126
+ return out;
1127
+ }
1128
+ /**
1129
+ * Resolve a model's remembered options for a (re)pick: keep only options the
1130
+ * model still declares AND that are enabled, dropping any that no longer apply
1131
+ * (e.g. switching away from a model that supported `fast`). Returns `undefined`
1132
+ * when nothing applies so callers can omit the field entirely.
1133
+ *
1134
+ * Shared by the TUI pick path and the launch-time resume path so "which options
1135
+ * survive a model switch / relaunch" is decided in exactly one place.
1136
+ */
1137
+ function restoreModelOptions(descriptor, modelId, remembered) {
1138
+ const validIds = new Set(modelOptionsFor(descriptor, modelId).map((o) => o.id));
1139
+ if (validIds.size === 0) return void 0;
1140
+ const enabled = enabledModelOptions(remembered?.[modelId]);
1141
+ const filtered = {};
1142
+ for (const id of Object.keys(enabled)) if (validIds.has(id)) filtered[id] = true;
1143
+ return Object.keys(filtered).length > 0 ? filtered : void 0;
1144
+ }
1145
+ //#endregion
1146
+ //#region src/chat/credentials.ts
1147
+ /** POSIX mode for the credentials file. Ignored on Windows. */
1148
+ const FILE_MODE = 384;
1149
+ /**
1150
+ * Resolve the credentials file path given the resolved TUI data directory
1151
+ * (typically `~/.zidane`, i.e. `config.paths.dir`).
1152
+ *
1153
+ * Matches the convention used elsewhere in the TUI (sessions.db, state.json)
1154
+ * so a single `ZIDANE_STORAGE_DIR` override moves the entire data root.
1155
+ */
1156
+ function credentialsPath(dataDir) {
1157
+ return resolve(dataDir, "credentials.json");
1158
+ }
1159
+ /**
1160
+ * Read credentials from disk.
1161
+ *
1162
+ * Returns `{}` when the file is missing or corrupt (last-ditch tolerance —
1163
+ * a hand-edit gone wrong shouldn't lock the user out of re-authing). On first
1164
+ * call with no file present, attempts a migration from `cwd/.credentials.json`
1165
+ * (the legacy location used by `bun run auth`).
1166
+ */
1167
+ function readCredentials(dataDir) {
1168
+ const path = credentialsPath(dataDir);
1169
+ if (!existsSync(path)) {
1170
+ const migrated = migrateLegacyFile(path);
1171
+ if (migrated) return migrated;
1172
+ return {};
1173
+ }
1174
+ try {
1175
+ const raw = readFileSync(path, "utf-8");
1176
+ const parsed = JSON.parse(raw);
1177
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
1178
+ return parsed;
1179
+ } catch {
1180
+ return {};
1181
+ }
1182
+ }
1183
+ /** Read a single provider's credential (translating via the descriptor). */
1184
+ function readProviderCredential(dataDir, descriptor) {
1185
+ return readCredentials(dataDir)[credKeyOf(descriptor)];
1186
+ }
1187
+ /**
1188
+ * Write credentials atomically (write-then-rename) with mode 0o600.
1189
+ *
1190
+ * Atomic on the same filesystem — readers either see the previous file or the
1191
+ * new one, never a half-written intermediate. Creates the parent dir if needed
1192
+ * (first launch on a fresh machine: `~/.zidane/` may not exist yet).
1193
+ */
1194
+ function writeCredentials(dataDir, creds) {
1195
+ writeFileAtomic(credentialsPath(dataDir), `${JSON.stringify(creds, null, 2)}\n`, {
1196
+ ensureDir: true,
1197
+ mode: FILE_MODE
1198
+ });
1199
+ }
1200
+ function setProviderCredential(dataDir, descriptor, cred) {
1201
+ const all = readCredentials(dataDir);
1202
+ all[credKeyOf(descriptor)] = cred;
1203
+ writeCredentials(dataDir, all);
1204
+ }
1205
+ function removeProviderCredential(dataDir, descriptor) {
1206
+ const all = readCredentials(dataDir);
1207
+ const fileKey = credKeyOf(descriptor);
1208
+ if (!(fileKey in all)) return;
1209
+ delete all[fileKey];
1210
+ writeCredentials(dataDir, all);
1211
+ }
1212
+ /**
1213
+ * Reconcile `process.env` with the stored credentials so the harness providers
1214
+ * pick up the wizard's output via their existing env-var resolution. Called
1215
+ * once at TUI launch after the credentials file has been resolved.
1216
+ *
1217
+ * **Precedence: stored credential > ambient env.** When the user has run the
1218
+ * auth wizard, that's an explicit, recent action — it MUST win over whatever
1219
+ * ambient value is already in `process.env`. Otherwise an upgrade reliably
1220
+ * breaks for anyone whose cwd happens to contain a stale `.env`: Bun
1221
+ * auto-loads `.env` from cwd into `process.env` before our code runs, the
1222
+ * legacy `bun run auth` used to upsert tokens into `cwd/.env`, and even a
1223
+ * one-off `export ANTHROPIC_API_KEY=…` from months ago in the user's shell rc
1224
+ * can outlive its usefulness. None of those should override a key the user
1225
+ * just typed into the wizard.
1226
+ *
1227
+ * Three cases per registered provider with an `envKey`:
1228
+ *
1229
+ * 1. Stored `kind: 'apikey'` → overwrite env with the stored value. Wizard
1230
+ * wins. A debug log fires when this clobbers a different ambient value.
1231
+ * 2. Stored `kind: 'oauth'` → DELETE env, so {@link resolveOAuthApiKey}
1232
+ * falls through to the file-read + refresh path. A stale OAuth token
1233
+ * in env would otherwise short-circuit refresh and get rejected.
1234
+ * 3. No stored entry → leave env alone. Env-only setups (no wizard run yet,
1235
+ * hosts driving zidane purely from secrets management) keep working.
1236
+ *
1237
+ * To opt out and force ambient env to win again, the user signs the provider
1238
+ * out via the wizard (which deletes the stored entry → case 3 above).
1239
+ *
1240
+ * Descriptors without an `envKey` (OAuth-only providers, custom providers
1241
+ * that bypass env-var resolution) are skipped silently.
1242
+ */
1243
+ function applyApiKeyEnv(dataDir, registry) {
1244
+ const creds = readCredentials(dataDir);
1245
+ for (const descriptor of Object.values(registry)) {
1246
+ const cred = creds[credKeyOf(descriptor)];
1247
+ if (cred?.kind === "apikey" && descriptor.customFields) {
1248
+ const stored = cred.customFields ?? {};
1249
+ for (const field of descriptor.customFields) {
1250
+ const value = stored[field.key];
1251
+ if (typeof value === "string" && value.length > 0) {
1252
+ const ambient = process.env[field.envVar];
1253
+ if (ambient && ambient !== value && process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] applyApiKeyEnv: overriding ambient \`${field.envVar}\` with stored value from credentials.json (provider=${descriptor.key}, field=${field.key}).\n`);
1254
+ process.env[field.envVar] = value;
1255
+ }
1256
+ }
1257
+ }
1258
+ if (!descriptor.envKey) continue;
1259
+ const envKey = descriptor.envKey;
1260
+ const ambient = process.env[envKey];
1261
+ if (cred?.kind === "apikey" && cred.value) {
1262
+ if (ambient && ambient !== cred.value && process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] applyApiKeyEnv: overriding ambient \`${envKey}\` with stored API key from credentials.json (provider=${descriptor.key}). Sign out via the auth wizard if the ambient value was intended to win.\n`);
1263
+ process.env[envKey] = cred.value;
1264
+ continue;
1265
+ }
1266
+ if (cred?.kind === "oauth" && cred.access) {
1267
+ if (ambient !== void 0 && process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/chat] applyApiKeyEnv: clearing ambient \`${envKey}\` because credentials.json has stored OAuth for ${descriptor.key} — refresh path needs the file.\n`);
1268
+ delete process.env[envKey];
1269
+ continue;
1270
+ }
1271
+ }
1272
+ }
1273
+ /**
1274
+ * `bun run auth` (pre-TUI) wrote `cwd/.credentials.json` with an entry per
1275
+ * provider mapping directly to an OAuthCredentials payload, e.g.:
1276
+ *
1277
+ * {
1278
+ * "anthropic": { "access": "...", "refresh": "...", "expires": 123 },
1279
+ * "openai-codex": { "access": "...", "refresh": "...", "expires": 123, "accountId": "..." }
1280
+ * }
1281
+ *
1282
+ * We don't delete the legacy file — it might still be used by a host that
1283
+ * imports the harness directly. We just copy its contents into the new
1284
+ * location under the kind-tagged shape so the TUI picks them up.
1285
+ *
1286
+ * Migration is provider-agnostic: any top-level entry with an `access` field
1287
+ * is preserved verbatim (extras included), under the same key. The TUI's
1288
+ * detection then looks them up via the matching descriptor's `credentialFileKey`.
1289
+ *
1290
+ * Returns the migrated credentials when the migration ran, or `null` when
1291
+ * there's no legacy file to migrate.
1292
+ */
1293
+ function migrateLegacyFile(targetPath) {
1294
+ const legacyPath = resolve(process.cwd(), ".credentials.json");
1295
+ const real = (p) => {
1296
+ try {
1297
+ return realpathSync(p);
1298
+ } catch {
1299
+ return p;
1300
+ }
1301
+ };
1302
+ const legacyDir = real(dirname(legacyPath));
1303
+ if (legacyDir !== real(homedir()) && legacyDir !== real(dirname(resolve(targetPath)))) return null;
1304
+ if (!existsSync(legacyPath)) return null;
1305
+ let legacy;
1306
+ try {
1307
+ legacy = JSON.parse(readFileSync(legacyPath, "utf-8"));
1308
+ } catch {
1309
+ return null;
1310
+ }
1311
+ if (!legacy || typeof legacy !== "object" || Array.isArray(legacy)) return null;
1312
+ const migrated = {};
1313
+ for (const [fileKey, value] of Object.entries(legacy)) {
1314
+ if (!isOAuthLegacy(value)) continue;
1315
+ const { access, refresh, expires, ...extras } = value;
1316
+ migrated[fileKey] = {
1317
+ kind: "oauth",
1318
+ access,
1319
+ ...typeof refresh === "string" ? { refresh } : {},
1320
+ ...typeof expires === "number" ? { expires } : {},
1321
+ ...extras
1322
+ };
1323
+ }
1324
+ if (Object.keys(migrated).length === 0) return null;
1325
+ writeFileAtomic(targetPath, `${JSON.stringify(migrated, null, 2)}\n`, {
1326
+ ensureDir: true,
1327
+ mode: FILE_MODE
1328
+ });
1329
+ return migrated;
1330
+ }
1331
+ function isOAuthLegacy(value) {
1332
+ return typeof value === "object" && value !== null && "access" in value && typeof value.access === "string";
1333
+ }
1334
+ //#endregion
1335
+ //#region src/chat/xdg.ts
1336
+ /**
1337
+ * XDG Base Directory resolution for zidane.
1338
+ *
1339
+ * The user's storage spreads across four logical slots:
1340
+ *
1341
+ * - **config** — credentials, mcp-credentials, keybindings, config.json,
1342
+ * mcps.json, skills (user-editable surface).
1343
+ * - **data** — sessions.db (long-lived chat history).
1344
+ * - **state** — state.json (per-machine UI bookkeeping: last provider,
1345
+ * last model, last agent, settings).
1346
+ * - **cache** — persisted tool results, background-task logs,
1347
+ * auto-update registry cache (regenerable, safe to wipe).
1348
+ *
1349
+ * Historically all four collapsed to `~/.zidane/`. This module adds
1350
+ * XDG-Base-Directory-aware defaults so a Linux user with no existing
1351
+ * `~/.zidane/` lands in the conventional `~/.config/zidane/`,
1352
+ * `~/.local/share/zidane/`, `~/.local/state/zidane/`, `~/.cache/zidane/`
1353
+ * trio. Existing setups (anyone with a `~/.zidane/` directory or
1354
+ * `ZIDANE_STORAGE_DIR` set) keep the single-dir layout unchanged.
1355
+ *
1356
+ * Resolution flow — applied in this order:
1357
+ *
1358
+ * 1. Caller passed an explicit `storageDir` (`ChatOptions.storageDir`)
1359
+ * OR `ZIDANE_STORAGE_DIR` is set → **legacy-explicit** single-dir
1360
+ * layout under `<storageDir>/<prefix>`. The four slots collapse to
1361
+ * the same directory.
1362
+ * 2. `~/<prefix>/` already exists on disk → **legacy-existing**
1363
+ * single-dir layout under that directory. Upgrades never silently
1364
+ * shift files around.
1365
+ * 3. Any `XDG_*_HOME` env var is set OR `process.platform === 'linux'`
1366
+ * → **XDG split**. Each slot lands in its own dir. Per-slot
1367
+ * `ZIDANE_{CONFIG,DATA,CACHE,STATE}_DIR` overrides beat the XDG
1368
+ * vars for surgical tweaks.
1369
+ * 4. Otherwise (macOS/Windows, no XDG signal, no existing dir) →
1370
+ * **default** single-dir layout at `~/<prefix>/`. Day-one behavior
1371
+ * on these platforms is unchanged.
1372
+ *
1373
+ * The `userDir` legacy alias always equals `configDir` so existing code
1374
+ * that still references `paths.userDir` keeps reading credentials, mcps,
1375
+ * etc. from the right place.
1376
+ */
1377
+ /**
1378
+ * Resolve the four storage slots according to the precedence above.
1379
+ *
1380
+ * Pure aside from one filesystem probe (`existsSync(home/<prefix>)`)
1381
+ * — the same probe `resolveStoragePaths` would do anyway when
1382
+ * computing `paths.userDir`. No directories are created here; the
1383
+ * relevant write paths (state store, credentials writer, persist
1384
+ * dir) handle their own lazy `mkdirSync` already.
1385
+ */
1386
+ function resolveStorageDirs(opts = {}) {
1387
+ const env = opts.env ?? process.env;
1388
+ const home = opts.home ?? homedir();
1389
+ const platform = opts.platform ?? process.platform;
1390
+ const rawPrefix = opts.prefix ?? ".zidane";
1391
+ const prefixBare = rawPrefix.replace(/^\./, "");
1392
+ const explicitStorageDir = opts.storageDir ?? env.ZIDANE_STORAGE_DIR;
1393
+ if (explicitStorageDir) return single(resolve(explicitStorageDir, rawPrefix), "legacy-explicit");
1394
+ const legacyHomeDir = resolve(home, rawPrefix);
1395
+ if (existsSync(legacyHomeDir)) return single(legacyHomeDir, "legacy-existing");
1396
+ if (platform === "linux" || !!env.XDG_CONFIG_HOME || !!env.XDG_DATA_HOME || !!env.XDG_CACHE_HOME || !!env.XDG_STATE_HOME || !!env.ZIDANE_CONFIG_DIR || !!env.ZIDANE_DATA_DIR || !!env.ZIDANE_STATE_DIR || !!env.ZIDANE_CACHE_DIR) return {
1397
+ configDir: env.ZIDANE_CONFIG_DIR ?? resolve(env.XDG_CONFIG_HOME || resolve(home, ".config"), prefixBare),
1398
+ dataDir: env.ZIDANE_DATA_DIR ?? resolve(env.XDG_DATA_HOME || resolve(home, ".local", "share"), prefixBare),
1399
+ stateDir: env.ZIDANE_STATE_DIR ?? resolve(env.XDG_STATE_HOME || resolve(home, ".local", "state"), prefixBare),
1400
+ cacheDir: env.ZIDANE_CACHE_DIR ?? resolve(env.XDG_CACHE_HOME || resolve(home, ".cache"), prefixBare),
1401
+ mode: "xdg"
1402
+ };
1403
+ return single(legacyHomeDir, "default");
1404
+ }
1405
+ function single(base, mode) {
1406
+ return {
1407
+ configDir: base,
1408
+ dataDir: base,
1409
+ stateDir: base,
1410
+ cacheDir: base,
1411
+ mode
1412
+ };
1413
+ }
1414
+ //#endregion
1415
+ export { restoreModelOptions as C, piIdOf as S, modelOptionsFor as _, readProviderCredential as a, openaiDescriptor as b, writeCredentials as c, cerebrasDescriptor as d, credKeyOf as f, localDescriptor as g, getModelInfo as h, readCredentials as i, BUILTIN_PROVIDERS as l, getContextWindow as m, applyApiKeyEnv as n, removeProviderCredential as o, enabledModelOptions as p, credentialsPath as r, setProviderCredential as s, resolveStorageDirs as t, anthropicDescriptor as u, modelSupportsReasoning as v, openrouterDescriptor as x, modelsForDescriptor as y };
1416
+
1417
+ //# sourceMappingURL=xdg-zlSeVBhQ.js.map