pi-xai-oauth 1.0.13 → 1.0.14

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 CHANGED
@@ -58,7 +58,9 @@ Run:
58
58
  pi /login xai-oauth
59
59
  ```
60
60
 
61
- Then paste your xAI API key from https://console.x.ai
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
 
@@ -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 { readFileSync, existsSync } from "fs";
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
- function getGrokAuthToken(): string | null {
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
- const data = JSON.parse(readFileSync(authPath, "utf8"));
12
- return data.access_token || data.token || null;
254
+ server.close();
13
255
  } catch {
14
- return null;
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
- return null;
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
- // Open browser to xAI console for authorization
32
- callbacks.onAuth({
33
- url: "https://console.x.ai",
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
- const accessToken = await callbacks.onPrompt({
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
- "After authorizing in the browser, paste your xAI API key here (starts with xai-):",
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
- return {
42
- refresh: "",
43
- access: accessToken.trim(),
44
- expires: Date.now() + 1000 * 60 * 60 * 24 * 365,
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 response = await fetch("https://api.x.ai/oauth/token", {
52
- method: "POST",
53
- headers: { "Content-Type": "application/json" },
54
- body: JSON.stringify({
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 response.json();
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 (toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) => {
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 (toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) => {
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 (toolCallId: string, params: { query?: string }) => ({
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 (toolCallId: string, params: { query?: string }) => ({
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 (toolCallId: string, params: { code?: string }) => ({
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
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-xai-oauth",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "xAI (Grok) provider with OAuth support for pi",
5
5
  "keywords": [
6
6
  "pi-package"