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.
- package/README.md +4 -3
- package/dist/cli/app.js +44 -4
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/grokOAuth.js +215 -216
- package/dist/cli/grokOAuth.js.map +1 -1
- package/dist/cli/main.bundled.js +422 -376
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/provider/openai-compatible.js +51 -3
- package/dist/cli/provider/openai-compatible.js.map +1 -1
- package/dist/cli/toolRegistry.js +4 -0
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/main/core/AgentHarness.js +80 -1
- package/dist/main/core/AgentHarness.js.map +1 -1
- package/dist/main/core/tools/builtin/listFiles.js +86 -0
- package/dist/main/core/tools/builtin/listFiles.js.map +1 -0
- package/package.json +1 -1
package/dist/cli/main.bundled.js
CHANGED
|
@@ -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
|
-
|
|
184
|
-
async function fetchGrokDiscovery(options = {}) {
|
|
14
|
+
async function requestDeviceCode(options) {
|
|
185
15
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
186
|
-
const
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
`
|
|
263
|
-
`
|
|
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(
|
|
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("
|
|
53
|
+
throw new GrokOAuthError("Device code response is not an object");
|
|
274
54
|
}
|
|
275
55
|
const obj = body;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
-
|
|
465
|
-
|
|
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
|
|
526
|
+
import path16 from "node:path";
|
|
692
527
|
import { fileURLToPath } from "node:url";
|
|
693
528
|
function getCurrentVersion() {
|
|
694
529
|
try {
|
|
695
|
-
const pkgPath =
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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 "${
|
|
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,
|
|
5289
|
-
if (!
|
|
5218
|
+
function getElementAtPath(obj, path18) {
|
|
5219
|
+
if (!path18)
|
|
5290
5220
|
return obj;
|
|
5291
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 }, [...
|
|
5784
|
+
issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
|
|
5855
5785
|
} else if (issue2.code === "invalid_key") {
|
|
5856
|
-
processError({ issues: issue2.issues }, [...
|
|
5786
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5857
5787
|
} else if (issue2.code === "invalid_element") {
|
|
5858
|
-
processError({ issues: issue2.issues }, [...
|
|
5788
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5859
5789
|
} else {
|
|
5860
|
-
const fullpath = [...
|
|
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,
|
|
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 }, [...
|
|
5821
|
+
issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
|
|
5892
5822
|
} else if (issue2.code === "invalid_key") {
|
|
5893
|
-
processError({ issues: issue2.issues }, [...
|
|
5823
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5894
5824
|
} else if (issue2.code === "invalid_element") {
|
|
5895
|
-
processError({ issues: issue2.issues }, [...
|
|
5825
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5896
5826
|
} else {
|
|
5897
|
-
const fullpath = [...
|
|
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
|
|
5930
|
-
for (const seg of
|
|
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
|
|
18623
|
-
if (
|
|
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 (
|
|
18628
|
-
const key =
|
|
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/
|
|
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 =
|
|
19256
|
-
const resolved =
|
|
19257
|
-
const rootWithSep = root.endsWith(
|
|
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
|
|
19321
|
-
import
|
|
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
|
|
19341
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
19633
|
-
import
|
|
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 ??
|
|
19641
|
-
mkdirSync7(
|
|
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
|
|
19693
|
-
import
|
|
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 ??
|
|
19703
|
-
mkdirSync8(
|
|
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
|
|
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
|
|
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: [
|
|
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 ??
|
|
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 =
|
|
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]
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
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) => [
|