pi-xai-oauth 1.0.13 → 1.0.15
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 +3 -1
- package/extensions/xai-oauth.ts +420 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,9 @@ Run:
|
|
|
58
58
|
pi /login xai-oauth
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Pi displays the same xAI OAuth endpoint used by the official Grok CLI (`https://auth.x.ai/oauth2/authorize`) and listens on `127.0.0.1:56121/callback` for the redirect. Copy the shown URL into the browser/profile you want to use. After approval, it stores OAuth access/refresh tokens and refreshes them automatically.
|
|
62
|
+
|
|
63
|
+
If official Grok CLI credentials already exist in `~/.grok/auth.json`, Pi can reuse them. This is separate from creating an `xai-...` API key in the xAI API console.
|
|
62
64
|
|
|
63
65
|
## Updating the Package
|
|
64
66
|
|
package/extensions/xai-oauth.ts
CHANGED
|
@@ -1,20 +1,359 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { createServer, type Server } from "http";
|
|
4
6
|
import { homedir } from "os";
|
|
5
7
|
import { join } from "path";
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
const XAI_OAUTH_ISSUER = "https://auth.x.ai";
|
|
10
|
+
const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
|
|
11
|
+
const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
12
|
+
const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
13
|
+
const XAI_OAUTH_REDIRECT_HOST = "127.0.0.1";
|
|
14
|
+
const XAI_OAUTH_REDIRECT_PORT = 56121;
|
|
15
|
+
const XAI_OAUTH_REDIRECT_PATH = "/callback";
|
|
16
|
+
const XAI_OAUTH_REFRESH_SKEW_MS = 2 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
type XaiDiscovery = {
|
|
19
|
+
authorization_endpoint: string;
|
|
20
|
+
token_endpoint: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type XaiTokenPayload = {
|
|
24
|
+
access_token?: string;
|
|
25
|
+
refresh_token?: string;
|
|
26
|
+
id_token?: string;
|
|
27
|
+
expires_in?: number;
|
|
28
|
+
token_type?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CallbackResult = {
|
|
32
|
+
code?: string;
|
|
33
|
+
state?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
error_description?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const MODELS = [
|
|
39
|
+
{
|
|
40
|
+
id: "grok-4.3",
|
|
41
|
+
name: "Grok 4.3",
|
|
42
|
+
reasoning: true,
|
|
43
|
+
input: ["text", "image"],
|
|
44
|
+
cost: { input: 1.25, output: 2.5, cacheRead: 0.3125, cacheWrite: 0.625 },
|
|
45
|
+
contextWindow: 1_000_000,
|
|
46
|
+
maxTokens: 131_072,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "grok-4.20-0309-reasoning",
|
|
50
|
+
name: "Grok 4.2 Reasoning",
|
|
51
|
+
reasoning: true,
|
|
52
|
+
input: ["text", "image"],
|
|
53
|
+
cost: { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 2 },
|
|
54
|
+
contextWindow: 1_000_000,
|
|
55
|
+
maxTokens: 131_072,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "grok-4.20-0309-non-reasoning",
|
|
59
|
+
name: "Grok 4.2 Fast",
|
|
60
|
+
reasoning: false,
|
|
61
|
+
input: ["text", "image"],
|
|
62
|
+
cost: { input: 0.6, output: 1.2, cacheRead: 0.15, cacheWrite: 0.3 },
|
|
63
|
+
contextWindow: 1_000_000,
|
|
64
|
+
maxTokens: 131_072,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const XAI_GROK_CLI_AUTH_SCOPE_KEY = `${XAI_OAUTH_ISSUER}::${XAI_OAUTH_CLIENT_ID}`;
|
|
69
|
+
const XAI_GROK_CLI_LEGACY_AUTH_SCOPE_KEY = "https://accounts.x.ai/sign-in";
|
|
70
|
+
|
|
71
|
+
function parseExpiry(value: unknown): number | undefined {
|
|
72
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
73
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
74
|
+
|
|
75
|
+
const numeric = Number(value);
|
|
76
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
77
|
+
|
|
78
|
+
const parsed = Date.parse(value);
|
|
79
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getGrokAuthCredentials(): OAuthCredentials | null {
|
|
8
83
|
const authPath = join(homedir(), ".grok", "auth.json");
|
|
9
|
-
if (existsSync(authPath))
|
|
84
|
+
if (!existsSync(authPath)) return null;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(readFileSync(authPath, "utf8"));
|
|
88
|
+
|
|
89
|
+
// Official Grok CLI stores OAuth2 credentials under
|
|
90
|
+
// "https://auth.x.ai::<client_id>" as { key, refresh_token, expires_at }.
|
|
91
|
+
const oidc = data?.[XAI_GROK_CLI_AUTH_SCOPE_KEY];
|
|
92
|
+
if (oidc && typeof oidc === "object") {
|
|
93
|
+
const access = String(oidc.key || oidc.access_token || oidc.token || "");
|
|
94
|
+
if (access) {
|
|
95
|
+
const expires = parseExpiry(oidc.expires_at) || Date.now() + 6 * 60 * 60 * 1000;
|
|
96
|
+
return {
|
|
97
|
+
refresh: String(oidc.refresh_token || oidc.refresh || ""),
|
|
98
|
+
access,
|
|
99
|
+
expires: expires - XAI_OAUTH_REFRESH_SKEW_MS,
|
|
100
|
+
tokenEndpoint: `${XAI_OAUTH_ISSUER}/oauth2/token`,
|
|
101
|
+
tokenType: "Bearer",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Older Grok builds stored a bearer at the sign-in URL scope.
|
|
107
|
+
const legacy = data?.[XAI_GROK_CLI_LEGACY_AUTH_SCOPE_KEY];
|
|
108
|
+
const legacyAccess = legacy && typeof legacy === "object" ? legacy.key || legacy.access_token || legacy.token : "";
|
|
109
|
+
if (legacyAccess) {
|
|
110
|
+
return {
|
|
111
|
+
refresh: "",
|
|
112
|
+
access: String(legacyAccess),
|
|
113
|
+
expires: Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Back-compat with early pi-xai-oauth guesses.
|
|
118
|
+
const topLevelAccess = data?.access_token || data?.token;
|
|
119
|
+
if (topLevelAccess) {
|
|
120
|
+
return {
|
|
121
|
+
refresh: String(data.refresh_token || data.refresh || ""),
|
|
122
|
+
access: String(topLevelAccess),
|
|
123
|
+
expires: parseExpiry(data.expires_at || data.expires) || Date.now() + 30 * 24 * 60 * 60 * 1000,
|
|
124
|
+
tokenEndpoint: `${XAI_OAUTH_ISSUER}/oauth2/token`,
|
|
125
|
+
tokenType: String(data.token_type || "Bearer"),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pkcePair(): { verifier: string; challenge: string } {
|
|
136
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
137
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
138
|
+
return { verifier, challenge };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateXaiEndpoint(url: string): string {
|
|
142
|
+
const parsed = new URL(url);
|
|
143
|
+
const host = parsed.hostname.toLowerCase();
|
|
144
|
+
if (parsed.protocol !== "https:" || (host !== "x.ai" && !host.endsWith(".x.ai"))) {
|
|
145
|
+
throw new Error(`xAI OAuth discovery returned an unexpected endpoint: ${url}`);
|
|
146
|
+
}
|
|
147
|
+
return url;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function xaiDiscovery(): Promise<XaiDiscovery> {
|
|
151
|
+
const response = await fetch(XAI_OAUTH_DISCOVERY_URL, {
|
|
152
|
+
headers: { Accept: "application/json" },
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(`xAI OAuth discovery failed: ${response.status} ${await response.text()}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const data = (await response.json()) as Partial<XaiDiscovery>;
|
|
159
|
+
if (!data.authorization_endpoint || !data.token_endpoint) {
|
|
160
|
+
throw new Error("xAI OAuth discovery response did not include authorization/token endpoints");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
authorization_endpoint: validateXaiEndpoint(data.authorization_endpoint),
|
|
165
|
+
token_endpoint: validateXaiEndpoint(data.token_endpoint),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function callbackCorsOrigin(origin: string | undefined): string | undefined {
|
|
170
|
+
return origin === "https://accounts.x.ai" || origin === "https://auth.x.ai" ? origin : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function startCallbackServer(): Promise<{
|
|
174
|
+
redirectUri: string;
|
|
175
|
+
waitForCallback: (signal?: AbortSignal) => Promise<CallbackResult>;
|
|
176
|
+
resolveCallback: (result: CallbackResult) => void;
|
|
177
|
+
close: () => void;
|
|
178
|
+
}> {
|
|
179
|
+
let resolveCallback!: (result: CallbackResult) => void;
|
|
180
|
+
const callbackPromise = new Promise<CallbackResult>((resolve) => {
|
|
181
|
+
resolveCallback = resolve;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const makeServer = () =>
|
|
185
|
+
createServer((req, res) => {
|
|
186
|
+
const origin = callbackCorsOrigin(req.headers.origin);
|
|
187
|
+
const writeCors = () => {
|
|
188
|
+
if (!origin) return;
|
|
189
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
190
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
191
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
192
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
193
|
+
res.setHeader("Vary", "Origin");
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (req.method === "OPTIONS") {
|
|
197
|
+
writeCors();
|
|
198
|
+
res.writeHead(204);
|
|
199
|
+
res.end();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const url = new URL(req.url || "/", `http://${XAI_OAUTH_REDIRECT_HOST}`);
|
|
204
|
+
if (url.pathname !== XAI_OAUTH_REDIRECT_PATH) {
|
|
205
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
206
|
+
res.end("Not found");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result: CallbackResult = {
|
|
211
|
+
code: url.searchParams.get("code") || undefined,
|
|
212
|
+
state: url.searchParams.get("state") || undefined,
|
|
213
|
+
error: url.searchParams.get("error") || undefined,
|
|
214
|
+
error_description: url.searchParams.get("error_description") || undefined,
|
|
215
|
+
};
|
|
216
|
+
resolveCallback(result);
|
|
217
|
+
|
|
218
|
+
writeCors();
|
|
219
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
220
|
+
res.end(
|
|
221
|
+
result.error
|
|
222
|
+
? "<html><body><h1>xAI authorization failed.</h1>You can close this tab.</body></html>"
|
|
223
|
+
: "<html><body><h1>xAI authorization received.</h1>You can close this tab.</body></html>",
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const listen = (port: number): Promise<Server> =>
|
|
228
|
+
new Promise((resolve, reject) => {
|
|
229
|
+
const server = makeServer();
|
|
230
|
+
server.once("error", reject);
|
|
231
|
+
server.listen(port, XAI_OAUTH_REDIRECT_HOST, () => {
|
|
232
|
+
server.removeListener("error", reject);
|
|
233
|
+
resolve(server);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
let server: Server;
|
|
238
|
+
try {
|
|
239
|
+
server = await listen(XAI_OAUTH_REDIRECT_PORT);
|
|
240
|
+
} catch {
|
|
241
|
+
server = await listen(0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const address = server.address();
|
|
245
|
+
if (!address || typeof address === "string") {
|
|
246
|
+
server.close();
|
|
247
|
+
throw new Error("Could not determine xAI OAuth callback port");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const redirectUri = `http://${XAI_OAUTH_REDIRECT_HOST}:${address.port}${XAI_OAUTH_REDIRECT_PATH}`;
|
|
251
|
+
|
|
252
|
+
const close = () => {
|
|
10
253
|
try {
|
|
11
|
-
|
|
12
|
-
return data.access_token || data.token || null;
|
|
254
|
+
server.close();
|
|
13
255
|
} catch {
|
|
14
|
-
|
|
256
|
+
// ignore
|
|
15
257
|
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
redirectUri,
|
|
262
|
+
close,
|
|
263
|
+
resolveCallback,
|
|
264
|
+
waitForCallback: async (signal?: AbortSignal) => {
|
|
265
|
+
let timer: NodeJS.Timeout | undefined;
|
|
266
|
+
let abortHandler: (() => void) | undefined;
|
|
267
|
+
const timeout = new Promise<CallbackResult>((_, reject) => {
|
|
268
|
+
timer = setTimeout(() => reject(new Error("Timed out waiting for xAI OAuth callback")), 180_000);
|
|
269
|
+
abortHandler = () => {
|
|
270
|
+
if (timer) clearTimeout(timer);
|
|
271
|
+
reject(new Error("xAI OAuth login was cancelled"));
|
|
272
|
+
};
|
|
273
|
+
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
return await Promise.race([callbackPromise, timeout]);
|
|
278
|
+
} finally {
|
|
279
|
+
if (timer) clearTimeout(timer);
|
|
280
|
+
if (abortHandler) signal?.removeEventListener("abort", abortHandler);
|
|
281
|
+
close();
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildAuthorizeUrl(discovery: XaiDiscovery, redirectUri: string, challenge: string, state: string, nonce: string): string {
|
|
288
|
+
// Match the official Grok CLI authorize URL. Extra query params such as
|
|
289
|
+
// `plan=generic` can change xAI's routing/branding and send users toward
|
|
290
|
+
// the API-console SSO surface instead of the Grok OAuth consent surface.
|
|
291
|
+
const params = new URLSearchParams({
|
|
292
|
+
response_type: "code",
|
|
293
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
294
|
+
redirect_uri: redirectUri,
|
|
295
|
+
scope: XAI_OAUTH_SCOPE,
|
|
296
|
+
code_challenge: challenge,
|
|
297
|
+
code_challenge_method: "S256",
|
|
298
|
+
state,
|
|
299
|
+
nonce,
|
|
300
|
+
});
|
|
301
|
+
return `${discovery.authorization_endpoint}?${params.toString()}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function parseCallbackInput(input: string): CallbackResult | undefined {
|
|
305
|
+
const value = input.trim();
|
|
306
|
+
if (!value) return undefined;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const url = value.startsWith("http")
|
|
310
|
+
? new URL(value)
|
|
311
|
+
: new URL(`http://${XAI_OAUTH_REDIRECT_HOST}${XAI_OAUTH_REDIRECT_PATH}?${value.replace(/^\?/, "")}`);
|
|
312
|
+
return {
|
|
313
|
+
code: url.searchParams.get("code") || undefined,
|
|
314
|
+
state: url.searchParams.get("state") || undefined,
|
|
315
|
+
error: url.searchParams.get("error") || undefined,
|
|
316
|
+
error_description: url.searchParams.get("error_description") || undefined,
|
|
317
|
+
};
|
|
318
|
+
} catch {
|
|
319
|
+
if (/^[A-Za-z0-9_-]{20,}$/.test(value)) return { code: value };
|
|
320
|
+
return undefined;
|
|
16
321
|
}
|
|
17
|
-
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function exchangeXaiToken(tokenEndpoint: string, body: Record<string, string>): Promise<XaiTokenPayload> {
|
|
325
|
+
const response = await fetch(tokenEndpoint, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: {
|
|
328
|
+
Accept: "application/json",
|
|
329
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
330
|
+
},
|
|
331
|
+
body: new URLSearchParams(body).toString(),
|
|
332
|
+
});
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
throw new Error(`xAI token request failed: ${response.status} ${await response.text()}`);
|
|
335
|
+
}
|
|
336
|
+
return (await response.json()) as XaiTokenPayload;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function credentialsFromTokenPayload(data: XaiTokenPayload, tokenEndpoint: string, fallbackRefresh = ""): OAuthCredentials {
|
|
340
|
+
if (!data.access_token) {
|
|
341
|
+
throw new Error("xAI token response did not include an access token");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const refresh = data.refresh_token || fallbackRefresh;
|
|
345
|
+
if (!refresh) {
|
|
346
|
+
throw new Error("xAI token response did not include a refresh token");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
refresh,
|
|
351
|
+
access: data.access_token,
|
|
352
|
+
expires: Date.now() + (data.expires_in || 3600) * 1000 - XAI_OAUTH_REFRESH_SKEW_MS,
|
|
353
|
+
tokenEndpoint,
|
|
354
|
+
idToken: data.id_token || "",
|
|
355
|
+
tokenType: data.token_type || "Bearer",
|
|
356
|
+
};
|
|
18
357
|
}
|
|
19
358
|
|
|
20
359
|
export default function (pi: ExtensionAPI) {
|
|
@@ -22,55 +361,95 @@ export default function (pi: ExtensionAPI) {
|
|
|
22
361
|
name: "xAI (OAuth)",
|
|
23
362
|
baseUrl: "https://api.x.ai/v1",
|
|
24
363
|
api: "openai-responses",
|
|
364
|
+
models: MODELS as any,
|
|
25
365
|
authHeader: true,
|
|
26
366
|
|
|
27
367
|
oauth: {
|
|
368
|
+
usesCallbackServer: true,
|
|
28
369
|
name: "xAI (Grok)",
|
|
29
370
|
|
|
30
371
|
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
372
|
+
const existingCredentials = getGrokAuthCredentials();
|
|
373
|
+
if (existingCredentials) {
|
|
374
|
+
const useExisting = await callbacks.onPrompt({
|
|
375
|
+
message: "Found existing official Grok CLI credentials in ~/.grok/auth.json. Use them instead of opening a new xAI OAuth login? (y/n)",
|
|
376
|
+
});
|
|
377
|
+
if (useExisting.toLowerCase().startsWith("y")) {
|
|
378
|
+
return existingCredentials;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
35
381
|
|
|
36
|
-
|
|
382
|
+
callbacks.onProgress?.("Starting xAI SuperGrok OAuth login...");
|
|
383
|
+
const discovery = await xaiDiscovery();
|
|
384
|
+
const callbackServer = await startCallbackServer();
|
|
385
|
+
const { verifier, challenge } = pkcePair();
|
|
386
|
+
const state = randomUUID().replace(/-/g, "");
|
|
387
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
388
|
+
const authorizeUrl = buildAuthorizeUrl(discovery, callbackServer.redirectUri, challenge, state, nonce);
|
|
389
|
+
|
|
390
|
+
// Do not call callbacks.onAuth here: pi's login dialog always runs
|
|
391
|
+
// `open <url>`, which can launch the wrong browser/profile. Instead,
|
|
392
|
+
// show the exact OAuth URL and let the user paste it into the intended
|
|
393
|
+
// browser. The local callback server still completes automatically.
|
|
394
|
+
callbacks.onProgress?.(`Open this exact xAI OAuth URL in the browser/profile you want:\n${authorizeUrl}`);
|
|
395
|
+
callbacks.onProgress?.(`Waiting for callback on ${callbackServer.redirectUri}`);
|
|
396
|
+
|
|
397
|
+
callbacks.onPrompt({
|
|
37
398
|
message:
|
|
38
|
-
|
|
399
|
+
`Open this exact xAI OAuth URL in the browser/profile you want:\n${authorizeUrl}\n\n` +
|
|
400
|
+
"Paste the redirect URL/code here if the browser cannot reach localhost, or press Enter to keep waiting for the callback:",
|
|
401
|
+
allowEmpty: true,
|
|
402
|
+
}).then((input) => {
|
|
403
|
+
const manual = parseCallbackInput(input);
|
|
404
|
+
if (manual) callbackServer.resolveCallback(manual);
|
|
405
|
+
}).catch(() => {
|
|
406
|
+
// Cancellation is handled by callbacks.signal / the login dialog.
|
|
39
407
|
});
|
|
40
408
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
409
|
+
const callback = await callbackServer.waitForCallback(callbacks.signal);
|
|
410
|
+
if (callback.error) {
|
|
411
|
+
throw new Error(`xAI authorization failed: ${callback.error_description || callback.error}`);
|
|
412
|
+
}
|
|
413
|
+
if (callback.state && callback.state !== state) {
|
|
414
|
+
throw new Error("xAI authorization failed: state mismatch");
|
|
415
|
+
}
|
|
416
|
+
if (!callback.code) {
|
|
417
|
+
throw new Error("xAI authorization failed: no authorization code returned");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
callbacks.onProgress?.("Exchanging xAI authorization code...");
|
|
421
|
+
const data = await exchangeXaiToken(discovery.token_endpoint, {
|
|
422
|
+
grant_type: "authorization_code",
|
|
423
|
+
code: callback.code,
|
|
424
|
+
redirect_uri: callbackServer.redirectUri,
|
|
425
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
426
|
+
code_verifier: verifier,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return credentialsFromTokenPayload(data, discovery.token_endpoint);
|
|
46
430
|
},
|
|
47
431
|
|
|
48
432
|
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
49
433
|
if (!credentials.refresh) return credentials;
|
|
50
434
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
grant_type: "refresh_token",
|
|
56
|
-
refresh_token: credentials.refresh,
|
|
57
|
-
client_id: "pi-xai-oauth",
|
|
58
|
-
}),
|
|
59
|
-
});
|
|
435
|
+
const tokenEndpoint =
|
|
436
|
+
typeof credentials.tokenEndpoint === "string" && credentials.tokenEndpoint
|
|
437
|
+
? validateXaiEndpoint(credentials.tokenEndpoint)
|
|
438
|
+
: (await xaiDiscovery()).token_endpoint;
|
|
60
439
|
|
|
61
|
-
const data = await
|
|
440
|
+
const data = await exchangeXaiToken(tokenEndpoint, {
|
|
441
|
+
grant_type: "refresh_token",
|
|
442
|
+
refresh_token: credentials.refresh,
|
|
443
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
444
|
+
});
|
|
62
445
|
|
|
63
|
-
return
|
|
64
|
-
refresh: data.refresh_token || credentials.refresh,
|
|
65
|
-
access: data.access_token,
|
|
66
|
-
expires: Date.now() + (data.expires_in || 3600) * 1000,
|
|
67
|
-
};
|
|
446
|
+
return credentialsFromTokenPayload(data, tokenEndpoint, credentials.refresh);
|
|
68
447
|
},
|
|
69
448
|
|
|
70
449
|
getApiKey(credentials: OAuthCredentials): string {
|
|
71
450
|
return credentials.access;
|
|
72
451
|
},
|
|
73
|
-
},
|
|
452
|
+
} as any,
|
|
74
453
|
});
|
|
75
454
|
|
|
76
455
|
// ====================== CUSTOM TOOLS ======================
|
|
@@ -83,14 +462,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
462
|
type: "object",
|
|
84
463
|
properties: {
|
|
85
464
|
prompt: { type: "string", description: "The prompt or question" },
|
|
86
|
-
model: { type: "string", description: "Model to use", default: "grok-4" },
|
|
465
|
+
model: { type: "string", description: "Model to use", default: "grok-4.3" },
|
|
87
466
|
reasoning_effort: { type: "string", enum: ["low", "medium", "high"], default: "medium" },
|
|
88
467
|
response_format: { type: "string", description: "Set to 'json' for JSON output" },
|
|
89
468
|
previous_response_id: { type: "string", description: "Continue conversation" },
|
|
90
469
|
},
|
|
91
470
|
required: ["prompt"],
|
|
92
471
|
},
|
|
93
|
-
execute: async (
|
|
472
|
+
execute: async (_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) => {
|
|
94
473
|
const apiKey = ctx?.apiKey || process.env.XAI_API_KEY;
|
|
95
474
|
if (!apiKey) {
|
|
96
475
|
return {
|
|
@@ -100,7 +479,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
100
479
|
}
|
|
101
480
|
|
|
102
481
|
const body: any = {
|
|
103
|
-
model: params.model || "grok-4",
|
|
482
|
+
model: params.model || "grok-4.3",
|
|
104
483
|
input: params.prompt,
|
|
105
484
|
reasoning: { effort: params.reasoning_effort || "medium" },
|
|
106
485
|
};
|
|
@@ -147,7 +526,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
147
526
|
},
|
|
148
527
|
required: ["query"],
|
|
149
528
|
},
|
|
150
|
-
execute: async (
|
|
529
|
+
execute: async (_toolCallId: string, params: any, _signal: any, _onUpdate: any, ctx: any) => {
|
|
151
530
|
const apiKey = ctx?.apiKey || process.env.XAI_API_KEY;
|
|
152
531
|
if (!apiKey) {
|
|
153
532
|
return {
|
|
@@ -193,7 +572,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
193
572
|
properties: { query: { type: "string" } },
|
|
194
573
|
required: ["query"],
|
|
195
574
|
},
|
|
196
|
-
execute: async (
|
|
575
|
+
execute: async (_toolCallId: string, params: { query?: string }) => ({
|
|
197
576
|
content: [{ type: "text", text: `Web search results for: ${params.query}` }],
|
|
198
577
|
details: { query: params.query },
|
|
199
578
|
}),
|
|
@@ -208,7 +587,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
208
587
|
properties: { query: { type: "string" } },
|
|
209
588
|
required: ["query"],
|
|
210
589
|
},
|
|
211
|
-
execute: async (
|
|
590
|
+
execute: async (_toolCallId: string, params: { query?: string }) => ({
|
|
212
591
|
content: [{ type: "text", text: `X search results for: ${params.query}` }],
|
|
213
592
|
details: { query: params.query },
|
|
214
593
|
}),
|
|
@@ -223,7 +602,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
223
602
|
properties: { code: { type: "string" } },
|
|
224
603
|
required: ["code"],
|
|
225
604
|
},
|
|
226
|
-
execute: async (
|
|
605
|
+
execute: async (_toolCallId: string, params: { code?: string }) => ({
|
|
227
606
|
content: [{ type: "text", text: `Executed: ${String(params.code).substring(0, 80)}...` }],
|
|
228
607
|
details: { code: params.code },
|
|
229
608
|
}),
|