zelari-code 0.1.2 → 0.2.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.
@@ -10,276 +10,71 @@ var __export = (target, all) => {
10
10
  __defProp(target, name, { get: all[name], enumerable: true });
11
11
  };
12
12
 
13
- // src/cli/oauthCallbackServer.ts
14
- import { createServer } from "node:http";
15
- function startOAuthCallbackServer(options = {}) {
16
- const host = options.host ?? "127.0.0.1";
17
- const preferredPort = options.port ?? 14523;
18
- const timeoutMs = options.timeoutMs ?? 6e4;
19
- const expectedPath = options.expectedPath ?? "/oauth/callback";
20
- const successHtml = options.successHtml ?? DEFAULT_SUCCESS_HTML;
21
- return new Promise((resolveStart, rejectStart) => {
22
- let resolved = false;
23
- let resolveCode = null;
24
- let rejectCode = null;
25
- let timeoutHandle = null;
26
- const server = createServer((req, res) => {
27
- try {
28
- if (!req.url) {
29
- res.writeHead(400, { "Content-Type": "text/plain" });
30
- res.end("Bad request");
31
- return;
32
- }
33
- const url2 = new URL(req.url, `http://${host}:${preferredPort}`);
34
- if (url2.pathname !== expectedPath) {
35
- res.writeHead(404, { "Content-Type": "text/plain" });
36
- res.end("Not found");
37
- return;
38
- }
39
- const code = url2.searchParams.get("code");
40
- const state = url2.searchParams.get("state");
41
- const errorParam = url2.searchParams.get("error");
42
- const errorDescription = url2.searchParams.get("error_description");
43
- const extras = {};
44
- for (const [key, value] of url2.searchParams.entries()) {
45
- if (key !== "code" && key !== "state" && key !== "error" && key !== "error_description") {
46
- extras[key] = value;
47
- }
48
- }
49
- const params = {
50
- ...code ? { code } : {},
51
- ...state ? { state } : {},
52
- ...errorParam ? { error: errorParam } : {},
53
- ...errorDescription ? { errorDescription } : {},
54
- extras
55
- };
56
- if (errorParam) {
57
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
58
- res.end(`<h1>OAuth error: ${errorParam}</h1>`);
59
- if (rejectCode && !resolved) {
60
- resolved = true;
61
- if (timeoutHandle) clearTimeout(timeoutHandle);
62
- rejectCode(new OAuthCallbackError(`OAuth provider returned error: ${errorParam}`));
63
- }
64
- return;
65
- }
66
- if (!code) {
67
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
68
- res.end("<h1>Missing <code>code</code> query parameter</h1>");
69
- return;
70
- }
71
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
72
- res.end(successHtml);
73
- if (resolveCode && !resolved) {
74
- resolved = true;
75
- if (timeoutHandle) clearTimeout(timeoutHandle);
76
- resolveCode(params);
77
- }
78
- setTimeout(() => {
79
- try {
80
- server.close();
81
- } catch {
82
- }
83
- }, 100);
84
- } catch (err) {
85
- try {
86
- res.writeHead(500, { "Content-Type": "text/plain" });
87
- res.end("Internal error");
88
- } catch {
89
- }
90
- if (!resolved && rejectCode) {
91
- resolved = true;
92
- if (timeoutHandle) clearTimeout(timeoutHandle);
93
- rejectCode(err instanceof Error ? err : new Error(String(err)));
94
- }
95
- }
96
- });
97
- const onError = (err) => {
98
- if (!resolved) {
99
- resolved = true;
100
- if (timeoutHandle) clearTimeout(timeoutHandle);
101
- rejectStart(err);
102
- }
103
- };
104
- server.once("error", onError);
105
- server.listen(preferredPort, host, () => {
106
- const addr = server.address();
107
- const actualPort = addr ? addr.port : preferredPort;
108
- const waitForCode = () => {
109
- return new Promise((res, rej) => {
110
- if (resolved) {
111
- rej(new OAuthCallbackClosedError());
112
- return;
113
- }
114
- resolveCode = res;
115
- rejectCode = rej;
116
- timeoutHandle = setTimeout(() => {
117
- if (!resolved) {
118
- resolved = true;
119
- try {
120
- server.close();
121
- } catch {
122
- }
123
- rej(new OAuthCallbackTimeoutError(timeoutMs));
124
- }
125
- }, timeoutMs);
126
- if (typeof timeoutHandle?.unref === "function") timeoutHandle.unref();
127
- });
128
- };
129
- const close = () => {
130
- if (timeoutHandle) clearTimeout(timeoutHandle);
131
- try {
132
- server.close();
133
- } catch {
134
- }
135
- if (!resolved && rejectCode) {
136
- resolved = true;
137
- rejectCode(new OAuthCallbackClosedError());
138
- }
139
- };
140
- resolveStart({ port: actualPort, waitForCode, close });
141
- });
142
- });
143
- }
144
- var DEFAULT_SUCCESS_HTML, OAuthCallbackTimeoutError, OAuthCallbackClosedError, OAuthCallbackError;
145
- var init_oauthCallbackServer = __esm({
146
- "src/cli/oauthCallbackServer.ts"() {
147
- "use strict";
148
- DEFAULT_SUCCESS_HTML = `
149
- <!DOCTYPE html>
150
- <html lang="en">
151
- <head>
152
- <meta charset="UTF-8">
153
- <title>zelari-code OAuth</title>
154
- </head>
155
- <body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 40px; text-align: center; color: #1a1a1a;">
156
- <h1 style="color: #00aa33;">\u2713 Authentication complete</h1>
157
- <p>You can close this tab and return to zelari-code.</p>
158
- </body>
159
- </html>
160
- `.trim();
161
- OAuthCallbackTimeoutError = class extends Error {
162
- constructor(timeoutMs) {
163
- super(`OAuth callback not received within ${timeoutMs}ms`);
164
- this.name = "OAuthCallbackTimeoutError";
165
- }
166
- };
167
- OAuthCallbackClosedError = class extends Error {
168
- constructor() {
169
- super("OAuth callback server closed before receiving a callback");
170
- this.name = "OAuthCallbackClosedError";
171
- }
172
- };
173
- OAuthCallbackError = class extends Error {
174
- constructor(message) {
175
- super(message);
176
- this.name = "OAuthCallbackError";
177
- }
178
- };
179
- }
180
- });
181
-
182
13
  // src/cli/grokOAuth.ts
183
- import nodeCrypto from "node:crypto";
184
- async function fetchGrokDiscovery(options = {}) {
14
+ async function requestDeviceCode(options) {
185
15
  const fetchImpl = options.fetchImpl ?? fetch;
186
- const discoveryUrl = options.discoveryUrl ?? `${options.issuer ?? DEFAULT_GROK_OAUTH_ISSUER}/.well-known/openid-configuration`;
187
- let response;
188
- try {
189
- response = await fetchImpl(discoveryUrl, { method: "GET", headers: { Accept: "application/json" } });
190
- } catch (err) {
191
- throw new GrokOAuthError(`OAuth discovery network error: ${err instanceof Error ? err.message : String(err)}`, "discovery_network_error");
192
- }
193
- if (!response.ok) {
194
- const text = await response.text().catch(() => "");
195
- throw new GrokOAuthError(
196
- `OAuth discovery HTTP ${response.status}: ${text.slice(0, 200)}`,
197
- `discovery_http_${response.status}`
198
- );
199
- }
200
- let body;
201
- try {
202
- body = await response.json();
203
- } catch (err) {
204
- throw new GrokOAuthError(`OAuth discovery returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
205
- }
206
- if (!body || typeof body !== "object") {
207
- throw new GrokOAuthError("OAuth discovery returned non-object body");
208
- }
209
- const obj = body;
210
- if (typeof obj.authorization_endpoint !== "string" || typeof obj.token_endpoint !== "string") {
211
- throw new GrokOAuthError("OAuth discovery missing authorization_endpoint or token_endpoint");
212
- }
213
- return {
214
- authorizationEndpoint: obj.authorization_endpoint,
215
- tokenEndpoint: obj.token_endpoint
216
- };
217
- }
218
- function generatePkce() {
219
- const random = getRandomValues(new Uint8Array(64));
220
- const verifier = Buffer.from(random).toString("base64url").slice(0, 128);
221
- const hash2 = createHash("sha256").update(verifier).digest();
222
- const challenge = hash2.toString("base64url").replace(/=+$/, "");
223
- return { verifier, challenge };
224
- }
225
- function buildGrokAuthorizeUrl(options) {
16
+ const endpoint = options.deviceCodeEndpoint ?? DEFAULT_DEVICE_CODE_ENDPOINT;
226
17
  const scopes = options.scopes ?? DEFAULT_GROK_OAUTH_SCOPES;
227
- const params = new URLSearchParams({
228
- client_id: options.clientId,
229
- redirect_uri: options.redirectUri,
230
- response_type: "code",
231
- scope: scopes.join(" "),
232
- state: options.state ?? crypto.randomUUID(),
233
- code_challenge: options.codeChallenge,
234
- code_challenge_method: "S256"
235
- });
236
- return `${options.authorizeEndpoint}?${params.toString()}`;
237
- }
238
- async function exchangeGrokCode(options) {
239
- const fetchImpl = options.fetchImpl ?? fetch;
240
18
  let response;
241
19
  try {
242
- response = await fetchImpl(options.tokenEndpoint, {
20
+ response = await fetchImpl(endpoint, {
243
21
  method: "POST",
244
22
  headers: {
245
23
  "Content-Type": "application/x-www-form-urlencoded",
246
24
  Accept: "application/json"
247
25
  },
248
26
  body: new URLSearchParams({
249
- grant_type: "authorization_code",
250
27
  client_id: options.clientId,
251
- code: options.code,
252
- redirect_uri: options.redirectUri,
253
- code_verifier: options.codeVerifier
28
+ scope: scopes.join(" ")
254
29
  }).toString()
255
30
  });
256
31
  } catch (err) {
257
- throw new GrokOAuthError(`Token exchange network error: ${err instanceof Error ? err.message : String(err)}`);
32
+ throw new GrokOAuthError(
33
+ `Device code request network error: ${err instanceof Error ? err.message : String(err)}`,
34
+ "device_code_network_error"
35
+ );
258
36
  }
259
37
  if (!response.ok) {
260
38
  const errText = await response.text().catch(() => "");
261
39
  throw new GrokOAuthError(
262
- `Token exchange HTTP ${response.status}: ${errText.slice(0, 200)}`,
263
- `http_${response.status}`
40
+ `Device code request HTTP ${response.status}: ${errText.slice(0, 200)}`,
41
+ `device_code_http_${response.status}`
264
42
  );
265
43
  }
266
44
  let body;
267
45
  try {
268
46
  body = await response.json();
269
47
  } catch (err) {
270
- throw new GrokOAuthError(`Token exchange returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
48
+ throw new GrokOAuthError(
49
+ `Device code response invalid JSON: ${err instanceof Error ? err.message : String(err)}`
50
+ );
271
51
  }
272
52
  if (!body || typeof body !== "object") {
273
- throw new GrokOAuthError("Token exchange returned non-object body");
53
+ throw new GrokOAuthError("Device code response is not an object");
274
54
  }
275
55
  const obj = body;
276
- const accessToken = obj.access_token;
277
- if (typeof accessToken !== "string" || accessToken.length === 0) {
278
- throw new GrokOAuthError("Token exchange response missing access_token", "no_access_token");
56
+ const deviceCode = obj.device_code;
57
+ const userCode = obj.user_code;
58
+ const verificationUri = obj.verification_uri;
59
+ if (typeof deviceCode !== "string" || deviceCode.length === 0) {
60
+ throw new GrokOAuthError("Device code response missing device_code", "no_device_code");
279
61
  }
280
- return parseTokenResponseBody(obj, accessToken, "Token exchange");
62
+ if (typeof userCode !== "string" || userCode.length === 0) {
63
+ throw new GrokOAuthError("Device code response missing user_code", "no_user_code");
64
+ }
65
+ if (typeof verificationUri !== "string" || verificationUri.length === 0) {
66
+ throw new GrokOAuthError("Device code response missing verification_uri", "no_verification_uri");
67
+ }
68
+ return {
69
+ deviceCode,
70
+ userCode,
71
+ verificationUri,
72
+ ...typeof obj.verification_uri_complete === "string" && obj.verification_uri_complete.length > 0 ? { verificationUriComplete: obj.verification_uri_complete } : {},
73
+ expiresIn: typeof obj.expires_in === "number" && Number.isFinite(obj.expires_in) ? obj.expires_in : 1800,
74
+ interval: typeof obj.interval === "number" && Number.isFinite(obj.interval) ? obj.interval : 5
75
+ };
281
76
  }
282
- function parseTokenResponseBody(obj, accessToken, label) {
77
+ function parseTokenResponseBody(obj, accessToken) {
283
78
  const result = { accessToken };
284
79
  if (typeof obj.expires_in === "number" && Number.isFinite(obj.expires_in)) {
285
80
  result.expiresAt = Date.now() + obj.expires_in * 1e3;
@@ -292,6 +87,116 @@ function parseTokenResponseBody(obj, accessToken, label) {
292
87
  }
293
88
  return result;
294
89
  }
90
+ async function pollForDeviceToken(options) {
91
+ const fetchImpl = options.fetchImpl ?? fetch;
92
+ const sleep = options.sleepImpl ?? defaultSleep;
93
+ const endpoint = options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT;
94
+ const deadline = Date.now() + options.timeoutMs;
95
+ let interval = Math.max(options.interval, 1);
96
+ while (true) {
97
+ if (Date.now() >= deadline) {
98
+ throw new GrokOAuthError("Device authorization timed out waiting for user approval", "timeout");
99
+ }
100
+ let response;
101
+ try {
102
+ response = await fetchImpl(endpoint, {
103
+ method: "POST",
104
+ headers: {
105
+ "Content-Type": "application/x-www-form-urlencoded",
106
+ Accept: "application/json"
107
+ },
108
+ body: new URLSearchParams({
109
+ grant_type: DEVICE_GRANT_TYPE,
110
+ client_id: options.clientId,
111
+ device_code: options.deviceCode
112
+ }).toString()
113
+ });
114
+ } catch (err) {
115
+ throw new GrokOAuthError(
116
+ `Token poll network error: ${err instanceof Error ? err.message : String(err)}`,
117
+ "poll_network_error"
118
+ );
119
+ }
120
+ if (response.ok) {
121
+ let body;
122
+ try {
123
+ body = await response.json();
124
+ } catch (err) {
125
+ throw new GrokOAuthError(
126
+ `Token response invalid JSON: ${err instanceof Error ? err.message : String(err)}`
127
+ );
128
+ }
129
+ if (!body || typeof body !== "object") {
130
+ throw new GrokOAuthError("Token response is not an object");
131
+ }
132
+ const obj = body;
133
+ const accessToken = obj.access_token;
134
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
135
+ throw new GrokOAuthError("Token response missing access_token", "no_access_token");
136
+ }
137
+ return parseTokenResponseBody(obj, accessToken);
138
+ }
139
+ let errBody = {};
140
+ try {
141
+ const parsed = await response.json();
142
+ if (parsed && typeof parsed === "object") errBody = parsed;
143
+ } catch {
144
+ }
145
+ const errorCode = typeof errBody.error === "string" ? errBody.error : `http_${response.status}`;
146
+ switch (errorCode) {
147
+ case "authorization_pending":
148
+ await sleep(interval * 1e3);
149
+ continue;
150
+ case "slow_down":
151
+ interval += 5;
152
+ await sleep(interval * 1e3);
153
+ continue;
154
+ case "expired_token":
155
+ case "expired":
156
+ throw new GrokOAuthError("Device code expired before user authorized", "expired");
157
+ case "access_denied":
158
+ case "denied":
159
+ throw new GrokOAuthError("User denied the authorization request", "denied");
160
+ case "invalid_grant":
161
+ throw new GrokOAuthError("Device code rejected (invalid_grant)", "invalid_grant");
162
+ default:
163
+ throw new GrokOAuthError(
164
+ `Token poll error: ${errorCode}${typeof errBody.error_description === "string" ? ` \u2014 ${errBody.error_description}` : ""}`,
165
+ errorCode
166
+ );
167
+ }
168
+ }
169
+ }
170
+ async function runGrokOAuthFlow(options = {}) {
171
+ const clientId = options.clientId || process.env.GROK_OAUTH_CLIENT_ID || DEFAULT_GROK_OAUTH_CLIENT_ID;
172
+ if (!clientId || clientId.trim().length === 0) {
173
+ throw new GrokOAuthError("Missing clientId", "no_client_id");
174
+ }
175
+ const deviceAuth = await requestDeviceCode({
176
+ clientId,
177
+ scopes: options.scopes,
178
+ deviceCodeEndpoint: options.deviceCodeEndpoint,
179
+ fetchImpl: options.fetchImpl
180
+ });
181
+ if (options.onUserCode) {
182
+ await options.onUserCode(deviceAuth);
183
+ }
184
+ try {
185
+ await openBrowser(deviceAuth.verificationUriComplete ?? deviceAuth.verificationUri);
186
+ } catch {
187
+ }
188
+ const timeoutMs = options.timeoutMs ?? DEFAULT_OAUTH_TIMEOUT_MS;
189
+ const effectiveTimeout = Math.min(timeoutMs, deviceAuth.expiresIn * 1e3);
190
+ return pollForDeviceToken({
191
+ clientId,
192
+ deviceCode: deviceAuth.deviceCode,
193
+ interval: deviceAuth.interval,
194
+ timeoutMs: effectiveTimeout,
195
+ tokenEndpoint: options.tokenEndpoint,
196
+ fetchImpl: options.fetchImpl,
197
+ sleepImpl: options.sleepImpl
198
+ });
199
+ }
295
200
  async function refreshGrokToken(options) {
296
201
  if (!options.clientId || options.clientId.trim().length === 0) {
297
202
  throw new GrokOAuthError("Missing clientId", "no_client_id");
@@ -299,7 +204,7 @@ async function refreshGrokToken(options) {
299
204
  if (!options.refreshToken || options.refreshToken.trim().length === 0) {
300
205
  throw new GrokOAuthError("Missing refreshToken", "no_refresh_token");
301
206
  }
302
- const endpoint = options.tokenEndpoint ?? "https://oauth.x.ai/token";
207
+ const endpoint = options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT;
303
208
  const fetchImpl = options.fetchImpl ?? fetch;
304
209
  let response;
305
210
  try {
@@ -340,77 +245,7 @@ async function refreshGrokToken(options) {
340
245
  if (typeof accessToken !== "string" || accessToken.length === 0) {
341
246
  throw new GrokOAuthError("Token refresh response missing access_token", "no_access_token");
342
247
  }
343
- return parseTokenResponseBody(obj, accessToken, "Token refresh");
344
- }
345
- async function runGrokOAuthFlow(options) {
346
- const clientId = options.clientId || process.env.GROK_OAUTH_CLIENT_ID || DEFAULT_GROK_OAUTH_CLIENT_ID;
347
- if (!clientId || clientId.trim().length === 0) {
348
- throw new GrokOAuthError("Missing clientId", "no_client_id");
349
- }
350
- const callbackPort = options.callbackPort ?? DEFAULT_GROK_OAUTH_REDIRECT_PORT;
351
- const callbackPath = options.callbackPath ?? DEFAULT_GROK_OAUTH_REDIRECT_PATH;
352
- const redirectUri = `http://127.0.0.1:${callbackPort}${callbackPath}`;
353
- const discovery = await fetchGrokDiscovery({
354
- issuer: options.issuer,
355
- discoveryUrl: options.discoveryUrl,
356
- fetchImpl: options.fetchImpl
357
- });
358
- const { verifier: codeVerifier, challenge: codeChallenge } = generatePkce();
359
- const state = crypto.randomUUID();
360
- const authorizeUrl = buildGrokAuthorizeUrl({
361
- clientId,
362
- redirectUri,
363
- scopes: options.scopes,
364
- state,
365
- codeChallenge,
366
- authorizeEndpoint: discovery.authorizationEndpoint
367
- });
368
- let handle;
369
- try {
370
- handle = await startOAuthCallbackServer({
371
- port: callbackPort,
372
- timeoutMs: options.callbackTimeoutMs ?? 12e4,
373
- expectedPath: callbackPath
374
- });
375
- } catch (err) {
376
- throw new GrokOAuthError(
377
- `Failed to start OAuth callback server on port ${callbackPort}: ${err instanceof Error ? err.message : String(err)}`,
378
- "callback_bind_failed"
379
- );
380
- }
381
- try {
382
- if (options.onBrowserOpen) {
383
- await options.onBrowserOpen(authorizeUrl);
384
- } else {
385
- await openBrowser(authorizeUrl);
386
- }
387
- const callbackResult = await handle.waitForCode();
388
- if (callbackResult.state !== state) {
389
- throw new GrokOAuthError(
390
- `OAuth state mismatch \u2014 possible CSRF. expected=${state} got=${callbackResult.state ?? "(none)"}`,
391
- "state_mismatch"
392
- );
393
- }
394
- if (callbackResult.error) {
395
- throw new GrokOAuthError(
396
- `OAuth provider returned error: ${callbackResult.error}${callbackResult.errorDescription ? ` \u2014 ${callbackResult.errorDescription}` : ""}`,
397
- callbackResult.error
398
- );
399
- }
400
- if (!callbackResult.code) {
401
- throw new GrokOAuthError("OAuth callback did not include authorization code", "no_code");
402
- }
403
- return await exchangeGrokCode({
404
- clientId,
405
- code: callbackResult.code,
406
- codeVerifier,
407
- redirectUri,
408
- tokenEndpoint: discovery.tokenEndpoint,
409
- fetchImpl: options.fetchImpl
410
- });
411
- } finally {
412
- handle.close();
413
- }
248
+ return parseTokenResponseBody(obj, accessToken);
414
249
  }
415
250
  async function openBrowser(url2) {
416
251
  const { spawn: spawn3 } = await import("node:child_process");
@@ -438,12 +273,10 @@ async function openBrowser(url2) {
438
273
  }
439
274
  });
440
275
  }
441
- var getRandomValues, createHash, GrokOAuthError, DEFAULT_GROK_OAUTH_CLIENT_ID, DEFAULT_GROK_OAUTH_ISSUER, DEFAULT_GROK_OAUTH_SCOPES, DEFAULT_GROK_OAUTH_REDIRECT_PORT, DEFAULT_GROK_OAUTH_REDIRECT_PATH;
276
+ var GrokOAuthError, DEFAULT_GROK_OAUTH_CLIENT_ID, DEFAULT_GROK_OAUTH_SCOPES, DEFAULT_DEVICE_CODE_ENDPOINT, DEFAULT_TOKEN_ENDPOINT, DEVICE_GRANT_TYPE, DEFAULT_OAUTH_TIMEOUT_MS, defaultSleep;
442
277
  var init_grokOAuth = __esm({
443
278
  "src/cli/grokOAuth.ts"() {
444
279
  "use strict";
445
- init_oauthCallbackServer();
446
- ({ getRandomValues, createHash } = nodeCrypto);
447
280
  GrokOAuthError = class extends Error {
448
281
  constructor(message, code) {
449
282
  super(message);
@@ -452,7 +285,6 @@ var init_grokOAuth = __esm({
452
285
  }
453
286
  };
454
287
  DEFAULT_GROK_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
455
- DEFAULT_GROK_OAUTH_ISSUER = "https://auth.x.ai";
456
288
  DEFAULT_GROK_OAUTH_SCOPES = [
457
289
  "openid",
458
290
  "profile",
@@ -461,8 +293,11 @@ var init_grokOAuth = __esm({
461
293
  "grok-cli:access",
462
294
  "api:access"
463
295
  ];
464
- DEFAULT_GROK_OAUTH_REDIRECT_PORT = 56121;
465
- DEFAULT_GROK_OAUTH_REDIRECT_PATH = "/callback";
296
+ DEFAULT_DEVICE_CODE_ENDPOINT = "https://auth.x.ai/oauth2/device/code";
297
+ DEFAULT_TOKEN_ENDPOINT = "https://auth.x.ai/oauth2/token";
298
+ DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
299
+ DEFAULT_OAUTH_TIMEOUT_MS = 3e5;
300
+ defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
466
301
  }
467
302
  });
468
303
 
@@ -688,11 +523,11 @@ __export(updater_exports, {
688
523
  });
689
524
  import { createRequire } from "node:module";
690
525
  import { spawn as spawn2 } from "node:child_process";
691
- import path15 from "node:path";
526
+ import path16 from "node:path";
692
527
  import { fileURLToPath } from "node:url";
693
528
  function getCurrentVersion() {
694
529
  try {
695
- const pkgPath = path15.resolve(__dirname2, "..", "..", "package.json");
530
+ const pkgPath = path16.resolve(__dirname2, "..", "..", "package.json");
696
531
  const pkg = require2(pkgPath);
697
532
  return pkg.version;
698
533
  } catch {
@@ -791,7 +626,7 @@ var init_updater = __esm({
791
626
  "src/cli/updater.ts"() {
792
627
  "use strict";
793
628
  require2 = createRequire(import.meta.url);
794
- __dirname2 = path15.dirname(fileURLToPath(import.meta.url));
629
+ __dirname2 = path16.dirname(fileURLToPath(import.meta.url));
795
630
  REGISTRY_URL = "https://registry.npmjs.org/zelari-code/latest";
796
631
  }
797
632
  });
@@ -2014,6 +1849,41 @@ var AgentHarness = class {
2014
1849
  });
2015
1850
  this.emit(initialMsgEnd);
2016
1851
  yield initialMsgEnd;
1852
+ const MAX_TOOL_LOOP_ITERATIONS = 12;
1853
+ let toolLoopTurns = 0;
1854
+ while (!this.cancelled && !hadError && toolLoopTurns < MAX_TOOL_LOOP_ITERATIONS && initialFinishRef.value === "tool_calls") {
1855
+ toolLoopTurns++;
1856
+ const turnMessageId = crypto.randomUUID();
1857
+ const msgStart = createBrainEvent(
1858
+ "message_start",
1859
+ this.sessionId,
1860
+ { messageId: turnMessageId, role: "assistant" }
1861
+ );
1862
+ this.emit(msgStart);
1863
+ yield msgStart;
1864
+ let turnLength = 0;
1865
+ const turnFinishRef = { value: "stop" };
1866
+ const turnUsageRef = { value: null };
1867
+ for await (const ev of this.runSingleTurn(turnMessageId, turnFinishRef, turnUsageRef)) {
1868
+ if (ev.type === "message_delta") {
1869
+ turnLength += ev.delta.length;
1870
+ } else if (ev.type === "error") {
1871
+ if (ev.severity !== "cancelled") hadError = true;
1872
+ }
1873
+ yield ev;
1874
+ }
1875
+ totalLength += turnLength;
1876
+ const msgEnd = createBrainEvent("message_end", this.sessionId, {
1877
+ messageId: turnMessageId,
1878
+ totalLength: turnLength,
1879
+ finishReason: turnFinishRef.value,
1880
+ ...turnUsageRef.value ? { usage: turnUsageRef.value } : {}
1881
+ });
1882
+ this.emit(msgEnd);
1883
+ yield msgEnd;
1884
+ initialFinishRef.value = turnFinishRef.value;
1885
+ if (hadError || this.cancelled) break;
1886
+ }
2017
1887
  let turns = 0;
2018
1888
  while (turns < this.maxQueuedIterations) {
2019
1889
  if (this.cancelled) break;
@@ -2084,6 +1954,8 @@ var AgentHarness = class {
2084
1954
  });
2085
1955
  let toolCallsThisTurn = 0;
2086
1956
  const maxToolCalls = this.config.maxToolCallsPerTurn;
1957
+ let turnText = "";
1958
+ const turnToolCalls = [];
2087
1959
  for await (const delta of stream) {
2088
1960
  if (this.cancelled) {
2089
1961
  const cancelEvent = createBrainEvent("error", this.sessionId, {
@@ -2096,6 +1968,7 @@ var AgentHarness = class {
2096
1968
  break;
2097
1969
  }
2098
1970
  if (delta.kind === "text") {
1971
+ turnText += delta.delta;
2099
1972
  const deltaEvent = createBrainEvent(
2100
1973
  "message_delta",
2101
1974
  this.sessionId,
@@ -2112,6 +1985,7 @@ var AgentHarness = class {
2112
1985
  yield thinkEvent;
2113
1986
  } else if (delta.kind === "tool_call") {
2114
1987
  toolCallsThisTurn++;
1988
+ turnToolCalls.push({ id: delta.toolCallId, name: delta.toolName, args: delta.args });
2115
1989
  const toolStartEvent = createBrainEvent("tool_execution_start", this.sessionId, {
2116
1990
  toolCallId: delta.toolCallId,
2117
1991
  toolName: delta.toolName,
@@ -2131,18 +2005,24 @@ var AgentHarness = class {
2131
2005
  signal: this.activeController?.signal
2132
2006
  }
2133
2007
  );
2008
+ const resultStr = result.ok ? String(result.value) : result.error;
2134
2009
  const endEvent = createBrainEvent(
2135
2010
  "tool_execution_end",
2136
2011
  this.sessionId,
2137
2012
  {
2138
2013
  toolCallId: delta.toolCallId,
2139
- result: result.ok ? String(result.value) : result.error,
2014
+ result: resultStr,
2140
2015
  isError: !result.ok,
2141
2016
  durationMs: Date.now() - startMs
2142
2017
  }
2143
2018
  );
2144
2019
  this.emit(endEvent);
2145
2020
  yield endEvent;
2021
+ this.config.messages.push({
2022
+ role: "tool",
2023
+ toolCallId: delta.toolCallId,
2024
+ content: resultStr
2025
+ });
2146
2026
  } else if (skipped) {
2147
2027
  const endEvent = createBrainEvent(
2148
2028
  "tool_execution_end",
@@ -2156,9 +2036,21 @@ var AgentHarness = class {
2156
2036
  );
2157
2037
  this.emit(endEvent);
2158
2038
  yield endEvent;
2039
+ this.config.messages.push({
2040
+ role: "tool",
2041
+ toolCallId: delta.toolCallId,
2042
+ content: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`
2043
+ });
2159
2044
  }
2160
2045
  } else if (delta.kind === "finish") {
2161
2046
  finishRef.value = delta.reason;
2047
+ if (turnToolCalls.length > 0 || turnText.length > 0) {
2048
+ this.config.messages.push({
2049
+ role: "assistant",
2050
+ content: turnText,
2051
+ ...turnToolCalls.length > 0 ? { toolCalls: turnToolCalls } : {}
2052
+ });
2053
+ }
2162
2054
  break;
2163
2055
  } else if (delta.kind === "error") {
2164
2056
  const errEvent = createBrainEvent("error", this.sessionId, {
@@ -2410,9 +2302,33 @@ function resolveBaseUrl(providerId) {
2410
2302
  }
2411
2303
  function openaiCompatibleProvider(config2) {
2412
2304
  return async function* (params) {
2305
+ const messages = params.messages.map((m) => {
2306
+ if (m.role === "tool") {
2307
+ return {
2308
+ role: "tool",
2309
+ tool_call_id: m.toolCallId,
2310
+ content: m.content
2311
+ };
2312
+ }
2313
+ if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
2314
+ return {
2315
+ role: "assistant",
2316
+ content: m.content ?? "",
2317
+ tool_calls: m.toolCalls.map((tc) => ({
2318
+ id: tc.id,
2319
+ type: "function",
2320
+ function: {
2321
+ name: tc.name,
2322
+ arguments: JSON.stringify(tc.args ?? {})
2323
+ }
2324
+ }))
2325
+ };
2326
+ }
2327
+ return { role: m.role, content: m.content };
2328
+ });
2413
2329
  const body = {
2414
2330
  model: config2.model,
2415
- messages: params.messages.filter((m) => m.role !== "tool").map((m) => ({ role: m.role, content: m.content })),
2331
+ messages,
2416
2332
  stream: true,
2417
2333
  temperature: 0.7,
2418
2334
  // Task G.4.2 — request the provider to send real token usage in
@@ -2422,6 +2338,17 @@ function openaiCompatibleProvider(config2) {
2422
2338
  // the harness will fall back to the ~4-char/token approximation.
2423
2339
  stream_options: { include_usage: true }
2424
2340
  };
2341
+ if (params.tools && params.tools.length > 0) {
2342
+ body.tools = params.tools.map((t) => ({
2343
+ type: "function",
2344
+ function: {
2345
+ name: t.name,
2346
+ description: t.description,
2347
+ parameters: t.parameters
2348
+ }
2349
+ }));
2350
+ body.tool_choice = "auto";
2351
+ }
2425
2352
  let response;
2426
2353
  try {
2427
2354
  response = await fetch(`${config2.baseUrl}/chat/completions`, {
@@ -2504,6 +2431,9 @@ function openaiCompatibleProvider(config2) {
2504
2431
  }
2505
2432
  }
2506
2433
  }
2434
+ if (choice?.finish_reason) {
2435
+ yield { kind: "finish", reason: choice.finish_reason };
2436
+ }
2507
2437
  } catch {
2508
2438
  }
2509
2439
  }
@@ -3005,11 +2935,11 @@ var TOOL_DEFINITIONS = [
3005
2935
  execute: (args, ctx) => {
3006
2936
  if (!ctx.addDocument) return "Knowledge vault tool not available.";
3007
2937
  const title = args["title"] || "New Document";
3008
- const path17 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
2938
+ const path18 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
3009
2939
  const content = args["content"] || "";
3010
2940
  const tags = args["tags"] || [];
3011
2941
  ctx.addDocument({
3012
- path: path17,
2942
+ path: path18,
3013
2943
  title,
3014
2944
  content,
3015
2945
  format: "markdown",
@@ -3018,7 +2948,7 @@ var TOOL_DEFINITIONS = [
3018
2948
  workspaceId: ctx.workspaceId
3019
2949
  });
3020
2950
  ctx.addActivity("vault", "created document", title);
3021
- return `Document "${title}" created at "${path17}".`;
2951
+ return `Document "${title}" created at "${path18}".`;
3022
2952
  }
3023
2953
  }
3024
2954
  ];
@@ -5285,10 +5215,10 @@ function mergeDefs(...defs) {
5285
5215
  function cloneDef(schema) {
5286
5216
  return mergeDefs(schema._zod.def);
5287
5217
  }
5288
- function getElementAtPath(obj, path17) {
5289
- if (!path17)
5218
+ function getElementAtPath(obj, path18) {
5219
+ if (!path18)
5290
5220
  return obj;
5291
- return path17.reduce((acc, key) => acc?.[key], obj);
5221
+ return path18.reduce((acc, key) => acc?.[key], obj);
5292
5222
  }
5293
5223
  function promiseAllObject(promisesObj) {
5294
5224
  const keys = Object.keys(promisesObj);
@@ -5697,11 +5627,11 @@ function explicitlyAborted(x, startIndex = 0) {
5697
5627
  }
5698
5628
  return false;
5699
5629
  }
5700
- function prefixIssues(path17, issues) {
5630
+ function prefixIssues(path18, issues) {
5701
5631
  return issues.map((iss) => {
5702
5632
  var _a3;
5703
5633
  (_a3 = iss).path ?? (_a3.path = []);
5704
- iss.path.unshift(path17);
5634
+ iss.path.unshift(path18);
5705
5635
  return iss;
5706
5636
  });
5707
5637
  }
@@ -5848,16 +5778,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
5848
5778
  }
5849
5779
  function formatError(error51, mapper = (issue2) => issue2.message) {
5850
5780
  const fieldErrors = { _errors: [] };
5851
- const processError = (error52, path17 = []) => {
5781
+ const processError = (error52, path18 = []) => {
5852
5782
  for (const issue2 of error52.issues) {
5853
5783
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5854
- issue2.errors.map((issues) => processError({ issues }, [...path17, ...issue2.path]));
5784
+ issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
5855
5785
  } else if (issue2.code === "invalid_key") {
5856
- processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
5786
+ processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
5857
5787
  } else if (issue2.code === "invalid_element") {
5858
- processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
5788
+ processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
5859
5789
  } else {
5860
- const fullpath = [...path17, ...issue2.path];
5790
+ const fullpath = [...path18, ...issue2.path];
5861
5791
  if (fullpath.length === 0) {
5862
5792
  fieldErrors._errors.push(mapper(issue2));
5863
5793
  } else {
@@ -5884,17 +5814,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
5884
5814
  }
5885
5815
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
5886
5816
  const result = { errors: [] };
5887
- const processError = (error52, path17 = []) => {
5817
+ const processError = (error52, path18 = []) => {
5888
5818
  var _a3, _b;
5889
5819
  for (const issue2 of error52.issues) {
5890
5820
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5891
- issue2.errors.map((issues) => processError({ issues }, [...path17, ...issue2.path]));
5821
+ issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
5892
5822
  } else if (issue2.code === "invalid_key") {
5893
- processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
5823
+ processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
5894
5824
  } else if (issue2.code === "invalid_element") {
5895
- processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
5825
+ processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
5896
5826
  } else {
5897
- const fullpath = [...path17, ...issue2.path];
5827
+ const fullpath = [...path18, ...issue2.path];
5898
5828
  if (fullpath.length === 0) {
5899
5829
  result.errors.push(mapper(issue2));
5900
5830
  continue;
@@ -5926,8 +5856,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
5926
5856
  }
5927
5857
  function toDotPath(_path) {
5928
5858
  const segs = [];
5929
- const path17 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5930
- for (const seg of path17) {
5859
+ const path18 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5860
+ for (const seg of path18) {
5931
5861
  if (typeof seg === "number")
5932
5862
  segs.push(`[${seg}]`);
5933
5863
  else if (typeof seg === "symbol")
@@ -18619,13 +18549,13 @@ function resolveRef(ref, ctx) {
18619
18549
  if (!ref.startsWith("#")) {
18620
18550
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
18621
18551
  }
18622
- const path17 = ref.slice(1).split("/").filter(Boolean);
18623
- if (path17.length === 0) {
18552
+ const path18 = ref.slice(1).split("/").filter(Boolean);
18553
+ if (path18.length === 0) {
18624
18554
  return ctx.rootSchema;
18625
18555
  }
18626
18556
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
18627
- if (path17[0] === defsKey) {
18628
- const key = path17[1];
18557
+ if (path18[0] === defsKey) {
18558
+ const key = path18[1];
18629
18559
  if (!key || !ctx.defs[key]) {
18630
18560
  throw new Error(`Reference not found: ${ref}`);
18631
18561
  }
@@ -19238,8 +19168,83 @@ var grepContentTool = {
19238
19168
  }
19239
19169
  };
19240
19170
 
19241
- // src/cli/safety/sandboxPath.ts
19171
+ // src/main/core/tools/builtin/listFiles.ts
19172
+ import { promises as fs11 } from "node:fs";
19242
19173
  import path10 from "node:path";
19174
+ var ListFilesArgsSchema = external_exports.object({
19175
+ /** Directory to list (relative to cwd or absolute). Defaults to cwd. */
19176
+ path: external_exports.string().optional(),
19177
+ /** Max traversal depth. 1 = immediate children only (default). */
19178
+ maxDepth: external_exports.number().int().positive().max(10).default(1),
19179
+ /** Glob-style patterns to exclude (matched against each entry name). */
19180
+ exclude: external_exports.array(external_exports.string()).default([
19181
+ "node_modules",
19182
+ ".git",
19183
+ "dist",
19184
+ "build",
19185
+ ".next",
19186
+ "__pycache__",
19187
+ ".cache"
19188
+ ])
19189
+ });
19190
+ function matchesAny(name, patterns) {
19191
+ return patterns.some((p3) => {
19192
+ if (p3.includes("*")) {
19193
+ const regex = new RegExp("^" + p3.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
19194
+ return regex.test(name);
19195
+ }
19196
+ return name === p3;
19197
+ });
19198
+ }
19199
+ async function walk(dir, baseRel, depth, maxDepth, exclude, entries, signal) {
19200
+ if (depth > maxDepth) return;
19201
+ let dirents;
19202
+ try {
19203
+ dirents = await fs11.readdir(dir, { withFileTypes: true });
19204
+ } catch {
19205
+ return;
19206
+ }
19207
+ if (signal?.aborted) return;
19208
+ for (const dirent of dirents) {
19209
+ if (matchesAny(dirent.name, exclude)) continue;
19210
+ const rel = baseRel ? `${baseRel}/${dirent.name}` : dirent.name;
19211
+ const isDir = dirent.isDirectory();
19212
+ entries.push({
19213
+ name: rel,
19214
+ type: isDir ? "directory" : dirent.isFile() ? "file" : "other"
19215
+ });
19216
+ if (isDir && depth < maxDepth) {
19217
+ await walk(path10.join(dir, dirent.name), rel, depth + 1, maxDepth, exclude, entries, signal);
19218
+ }
19219
+ }
19220
+ }
19221
+ var listFilesTool = {
19222
+ name: "list_files",
19223
+ description: "List files and directories in the given path (defaults to the working directory). Returns names with types (file/directory). Use this to discover the project structure before reading specific files. Supports a maxDepth for recursive listing and excludes common dependency/build directories by default.",
19224
+ permissions: ["read"],
19225
+ timeoutMs: 15e3,
19226
+ inputSchema: ListFilesArgsSchema,
19227
+ execute: async (args, ctx) => {
19228
+ try {
19229
+ const target = args.path ? path10.isAbsolute(args.path) ? args.path : path10.join(ctx.cwd, args.path) : ctx.cwd;
19230
+ const entries = [];
19231
+ await walk(target, "", 1, args.maxDepth, args.exclude, entries, ctx.signal);
19232
+ entries.sort((a, b) => {
19233
+ if (a.type === "directory" && b.type !== "directory") return -1;
19234
+ if (a.type !== "directory" && b.type === "directory") return 1;
19235
+ return a.name.localeCompare(b.name);
19236
+ });
19237
+ const MAX_ENTRIES = 500;
19238
+ const truncated = entries.length > MAX_ENTRIES;
19239
+ return typedOk({ dir: target, entries: truncated ? entries.slice(0, MAX_ENTRIES) : entries, truncated });
19240
+ } catch (err) {
19241
+ return typedErr(err instanceof Error ? err.message : String(err));
19242
+ }
19243
+ }
19244
+ };
19245
+
19246
+ // src/cli/safety/sandboxPath.ts
19247
+ import path11 from "node:path";
19243
19248
  var SandboxViolationError = class extends Error {
19244
19249
  constructor(message, attemptedPath, resolvedPath) {
19245
19250
  super(message);
@@ -19252,9 +19257,9 @@ function resolveSandboxedPath(userPath, options = {}) {
19252
19257
  if (typeof userPath !== "string" || userPath.length === 0) {
19253
19258
  throw new SandboxViolationError("Empty path", userPath, "");
19254
19259
  }
19255
- const root = path10.resolve(options.root ?? process.cwd());
19256
- const resolved = path10.isAbsolute(userPath) ? path10.resolve(userPath) : path10.resolve(root, userPath);
19257
- const rootWithSep = root.endsWith(path10.sep) ? root : root + path10.sep;
19260
+ const root = path11.resolve(options.root ?? process.cwd());
19261
+ const resolved = path11.isAbsolute(userPath) ? path11.resolve(userPath) : path11.resolve(root, userPath);
19262
+ const rootWithSep = root.endsWith(path11.sep) ? root : root + path11.sep;
19258
19263
  if (resolved !== root && !resolved.startsWith(rootWithSep)) {
19259
19264
  throw new SandboxViolationError(
19260
19265
  `Path escapes sandbox root: ${userPath} \u2192 ${resolved} (root: ${root})`,
@@ -19317,8 +19322,8 @@ function assertShellAllowed(command) {
19317
19322
  }
19318
19323
 
19319
19324
  // src/cli/safety/auditLogger.ts
19320
- import { promises as fs11 } from "node:fs";
19321
- import path11 from "node:path";
19325
+ import { promises as fs12 } from "node:fs";
19326
+ import path12 from "node:path";
19322
19327
  import os7 from "node:os";
19323
19328
  var AuditLogger = class {
19324
19329
  logPath;
@@ -19337,8 +19342,8 @@ var AuditLogger = class {
19337
19342
  async append(entry) {
19338
19343
  const line = JSON.stringify(entry) + "\n";
19339
19344
  this.writeQueue = this.writeQueue.then(async () => {
19340
- await fs11.mkdir(path11.dirname(this.logPath), { recursive: true });
19341
- await fs11.appendFile(this.logPath, line, "utf-8");
19345
+ await fs12.mkdir(path12.dirname(this.logPath), { recursive: true });
19346
+ await fs12.appendFile(this.logPath, line, "utf-8");
19342
19347
  });
19343
19348
  return this.writeQueue;
19344
19349
  }
@@ -19381,7 +19386,7 @@ var AuditLogger = class {
19381
19386
  function defaultAuditPath() {
19382
19387
  const override = process.env.ANATHEMA_AUDIT_LOG;
19383
19388
  if (override && override.trim().length > 0) return override;
19384
- return path11.join(os7.tmpdir(), "zelari-code", "audit.jsonl");
19389
+ return path12.join(os7.tmpdir(), "zelari-code", "audit.jsonl");
19385
19390
  }
19386
19391
  function redactArgs(args) {
19387
19392
  const redacted = {};
@@ -19413,6 +19418,7 @@ function createBuiltinToolRegistry(options = {}) {
19413
19418
  const safeWriteFile = wrapWithSandbox(writeFileTool, ["path"], root, audit, sessionId);
19414
19419
  const safeEditFile = wrapWithSandbox(editFileTool, ["path"], root, audit, sessionId);
19415
19420
  const safeGrepContent = wrapWithSandbox(grepContentTool, ["path"], root, audit, sessionId);
19421
+ const safeListFiles = wrapWithSandbox(listFilesTool, ["path"], root, audit, sessionId);
19416
19422
  const safeBash = wrapWithShellSafety(bashTool, audit, sessionId);
19417
19423
  const registry3 = new ToolRegistry();
19418
19424
  registry3.register(safeReadFile);
@@ -19420,12 +19426,14 @@ function createBuiltinToolRegistry(options = {}) {
19420
19426
  registry3.register(safeEditFile);
19421
19427
  registry3.register(safeBash);
19422
19428
  registry3.register(safeGrepContent);
19429
+ registry3.register(safeListFiles);
19423
19430
  const tools = [
19424
19431
  safeReadFile,
19425
19432
  safeWriteFile,
19426
19433
  safeEditFile,
19427
19434
  safeBash,
19428
- safeGrepContent
19435
+ safeGrepContent,
19436
+ safeListFiles
19429
19437
  ].map((t) => ({
19430
19438
  name: t.name,
19431
19439
  description: t.description,
@@ -19542,7 +19550,7 @@ function redactForAudit(args) {
19542
19550
  // src/cli/gitOps.ts
19543
19551
  import { execFile } from "node:child_process";
19544
19552
  import { promisify } from "node:util";
19545
- import path12 from "node:path";
19553
+ import path13 from "node:path";
19546
19554
  var execFileAsync = promisify(execFile);
19547
19555
  async function git(cwd, args) {
19548
19556
  try {
@@ -19588,7 +19596,7 @@ async function undoWorkingChanges(opts = {}) {
19588
19596
  };
19589
19597
  }
19590
19598
  function defaultProjectRoot() {
19591
- return path12.resolve(__dirname, "..", "..", "..");
19599
+ return path13.resolve(__dirname, "..", "..", "..");
19592
19600
  }
19593
19601
 
19594
19602
  // src/cli/compaction.ts
@@ -19629,16 +19637,16 @@ function formatCompactionSummary(result) {
19629
19637
  }
19630
19638
 
19631
19639
  // src/cli/metrics.ts
19632
- import { promises as fs12, existsSync as existsSync7, statSync as statSync3, renameSync, appendFileSync, mkdirSync as mkdirSync7 } from "node:fs";
19633
- import path13 from "node:path";
19640
+ import { promises as fs13, existsSync as existsSync7, statSync as statSync3, renameSync, appendFileSync, mkdirSync as mkdirSync7 } from "node:fs";
19641
+ import path14 from "node:path";
19634
19642
  import os8 from "node:os";
19635
19643
  var METRICS_ROTATE_BYTES = 10 * 1024 * 1024;
19636
19644
  var MetricsLogger = class {
19637
19645
  file;
19638
19646
  writeQueue = Promise.resolve();
19639
19647
  constructor(file2) {
19640
- this.file = file2 ?? process.env.ANATHEMA_METRICS_FILE ?? path13.join(os8.homedir(), ".tmp", "zelari-code", "metrics.jsonl");
19641
- mkdirSync7(path13.dirname(this.file), { recursive: true });
19648
+ this.file = file2 ?? process.env.ANATHEMA_METRICS_FILE ?? path14.join(os8.homedir(), ".tmp", "zelari-code", "metrics.jsonl");
19649
+ mkdirSync7(path14.dirname(this.file), { recursive: true });
19642
19650
  }
19643
19651
  /** Fire-and-forget record append. */
19644
19652
  record(rec) {
@@ -19689,8 +19697,8 @@ function getMetricsLogger() {
19689
19697
  }
19690
19698
 
19691
19699
  // src/cli/skillHistory.ts
19692
- import { promises as fs13, existsSync as existsSync8, statSync as statSync4, renameSync as renameSync2, appendFileSync as appendFileSync2, mkdirSync as mkdirSync8 } from "node:fs";
19693
- import path14 from "node:path";
19700
+ import { promises as fs14, existsSync as existsSync8, statSync as statSync4, renameSync as renameSync2, appendFileSync as appendFileSync2, mkdirSync as mkdirSync8 } from "node:fs";
19701
+ import path15 from "node:path";
19694
19702
  import os9 from "node:os";
19695
19703
  import { randomUUID as randomUUID2 } from "node:crypto";
19696
19704
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
@@ -19699,8 +19707,8 @@ var SkillHistoryLogger = class {
19699
19707
  writeQueue = Promise.resolve();
19700
19708
  inflight = /* @__PURE__ */ new Map();
19701
19709
  constructor(file2) {
19702
- this.file = file2 ?? process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path14.join(os9.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
19703
- mkdirSync8(path14.dirname(this.file), { recursive: true });
19710
+ this.file = file2 ?? process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path15.join(os9.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
19711
+ mkdirSync8(path15.dirname(this.file), { recursive: true });
19704
19712
  }
19705
19713
  /**
19706
19714
  * Mark the start of a skill invocation. Returns a unique invocationId
@@ -19778,7 +19786,7 @@ var SkillHistoryLogger = class {
19778
19786
  async function readSkillHistory(file2) {
19779
19787
  let raw = "";
19780
19788
  try {
19781
- raw = await fs13.readFile(file2, "utf-8");
19789
+ raw = await fs14.readFile(file2, "utf-8");
19782
19790
  } catch {
19783
19791
  return [];
19784
19792
  }
@@ -19814,7 +19822,7 @@ function getSkillStats(records, skillId, sinceTs) {
19814
19822
  }
19815
19823
 
19816
19824
  // src/cli/app.tsx
19817
- import path16 from "node:path";
19825
+ import path17 from "node:path";
19818
19826
  import os10 from "node:os";
19819
19827
  var MODEL = process.env.OPENAI_MODEL ?? "grok-4";
19820
19828
  var PROVIDER = "openai-compatible";
@@ -20052,13 +20060,38 @@ function App() {
20052
20060
  primary: baseProviderStream,
20053
20061
  fallback: failoverResolution.fallback
20054
20062
  });
20063
+ const cwd = process.cwd();
20064
+ const toolList = toolRegistry.toOpenAITools().map((t) => `- ${t.name}: ${t.description}`).join("\n");
20065
+ const systemPrompt = [
20066
+ "You are Zelari Code, an interactive AI coding agent operating directly in the user's terminal.",
20067
+ "",
20068
+ "You ARE connected to this machine and have real tools to read, modify, and explore the codebase.",
20069
+ "Never claim you lack filesystem or shell access \u2014 you have it. Use your tools instead of asking the user to paste file contents.",
20070
+ "",
20071
+ `# Working Directory`,
20072
+ `You are running in: ${cwd}`,
20073
+ "All relative file paths are resolved against this directory. Always work with real files here.",
20074
+ "",
20075
+ "# Available Tools",
20076
+ "You can call these tools. Use them to take action and gather information autonomously:",
20077
+ toolList,
20078
+ "",
20079
+ "# Guidelines",
20080
+ "- To understand a project, list files (bash: ls) and read key files (read_file), don't ask the user to paste them.",
20081
+ "- Be proactive: explore, read, and act. Prefer doing over asking.",
20082
+ "- When you finish a task, briefly summarize what you did."
20083
+ ].join("\n");
20055
20084
  const harness = new AgentHarness({
20056
20085
  model: envConfig.model,
20057
20086
  provider: PROVIDER,
20058
- messages: [{ role: "user", content: userText }],
20087
+ messages: [
20088
+ { role: "system", content: systemPrompt },
20089
+ { role: "user", content: userText }
20090
+ ],
20059
20091
  tools: toolRegistry.toOpenAITools(),
20060
20092
  toolRegistry,
20061
- providerStream
20093
+ providerStream,
20094
+ cwd
20062
20095
  });
20063
20096
  harnessRef.current = harness;
20064
20097
  setQueueCount(harness.queueLength);
@@ -20583,9 +20616,9 @@ ${lines.join("\n")}`;
20583
20616
  if (result.kind === "promote_member" && result.promoteMemberId) {
20584
20617
  try {
20585
20618
  const { skill, markdown } = promoteMember(result.promoteMemberId);
20586
- const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path16.join(os10.homedir(), ".tmp", "zelari-code", "skills");
20619
+ const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path17.join(os10.homedir(), ".tmp", "zelari-code", "skills");
20587
20620
  await fs.promises.mkdir(skillDir, { recursive: true });
20588
- const filePath = path16.join(skillDir, `${skill.id}.md`);
20621
+ const filePath = path17.join(skillDir, `${skill.id}.md`);
20589
20622
  await fs.promises.writeFile(filePath, markdown, "utf8");
20590
20623
  setMessages((prev) => [
20591
20624
  ...prev,
@@ -21219,14 +21252,27 @@ ${lines.join("\n")}`,
21219
21252
  {
21220
21253
  id: crypto.randomUUID(),
21221
21254
  role: "system",
21222
- content: "[login oauth] opening browser for SuperGrok authentication (PKCE)...",
21255
+ content: "[login oauth] requesting device code from xAI...",
21223
21256
  ts: Date.now()
21224
21257
  }
21225
21258
  ]);
21226
21259
  setBusy(true);
21227
21260
  try {
21228
21261
  const resultOAuth = await runGrokOAuthFlow({
21229
- callbackTimeoutMs: 12e4
21262
+ // Show the user_code + verification_uri as soon as xAI returns them.
21263
+ onUserCode: (info) => {
21264
+ setMessages((prev) => [
21265
+ ...prev,
21266
+ {
21267
+ id: crypto.randomUUID(),
21268
+ role: "system",
21269
+ content: `[login oauth] Open ${info.verificationUri} in your browser and enter the code:
21270
+ ${info.userCode}
21271
+ (Opening your browser automatically...)`,
21272
+ ts: Date.now()
21273
+ }
21274
+ ]);
21275
+ }
21230
21276
  });
21231
21277
  setOAuthToken("grok", {
21232
21278
  apiKey: resultOAuth.accessToken,
@@ -21302,7 +21348,7 @@ ${lines.join("\n")}`,
21302
21348
  return;
21303
21349
  }
21304
21350
  if (result.kind === "skill_stats") {
21305
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path16.join(os10.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
21351
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path17.join(os10.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
21306
21352
  try {
21307
21353
  const records = await readSkillHistory(historyFile);
21308
21354
  const stats = getSkillStats(records, result.skillStatsSkillId);
@@ -21331,7 +21377,7 @@ ${lines.join("\n")}`,
21331
21377
  setInput("");
21332
21378
  return;
21333
21379
  }
21334
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path16.join(os10.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
21380
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path17.join(os10.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
21335
21381
  try {
21336
21382
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
21337
21383
  setMessages((prev) => [