@oh-my-pi/pi-ai 6.7.67 → 6.8.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 +31 -31
- package/package.json +2 -1
- package/src/cli.ts +114 -52
- package/src/providers/anthropic.ts +215 -166
- package/src/providers/google-gemini-cli.ts +4 -20
- package/src/providers/openai-codex/response-handler.ts +4 -43
- package/src/providers/openai-codex-responses.ts +2 -2
- package/src/storage.ts +185 -0
- package/src/utils/event-stream.ts +3 -3
- package/src/utils/oauth/anthropic.ts +70 -88
- package/src/utils/oauth/callback-server.ts +245 -0
- package/src/utils/oauth/cursor.ts +1 -5
- package/src/utils/oauth/github-copilot.ts +1 -23
- package/src/utils/oauth/google-antigravity.ts +73 -263
- package/src/utils/oauth/google-gemini-cli.ts +73 -281
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/openai-codex.ts +131 -318
- package/src/utils/oauth/pkce.ts +1 -1
- package/src/utils/oauth/types.ts +8 -0
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
|
|
3
3
|
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
|
|
4
|
-
*
|
|
5
|
-
* NOTE: This module uses Node.js http.createServer for the OAuth callback.
|
|
6
|
-
* It is only intended for CLI use, not browser environments.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
|
-
import
|
|
6
|
+
import { OAuthCallbackFlow } from "./callback-server";
|
|
10
7
|
import { generatePKCE } from "./pkce";
|
|
11
|
-
import type { OAuthCredentials } from "./types";
|
|
8
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
12
9
|
|
|
13
|
-
// Antigravity OAuth credentials (different from Gemini CLI)
|
|
14
10
|
const decode = (s: string) => atob(s);
|
|
15
11
|
const CLIENT_ID = decode(
|
|
16
12
|
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
|
|
17
13
|
);
|
|
18
14
|
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
|
19
|
-
const
|
|
15
|
+
const CALLBACK_PORT = 51121;
|
|
16
|
+
const CALLBACK_PATH = "/oauth-callback";
|
|
20
17
|
|
|
21
|
-
// Antigravity requires additional scopes
|
|
22
18
|
const SCOPES = [
|
|
23
19
|
"https://www.googleapis.com/auth/cloud-platform",
|
|
24
20
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
@@ -29,110 +25,14 @@ const SCOPES = [
|
|
|
29
25
|
|
|
30
26
|
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
31
27
|
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
32
|
-
|
|
33
|
-
// Fallback project ID when discovery fails
|
|
34
28
|
const DEFAULT_PROJECT_ID = "rising-fact-p41fc";
|
|
35
29
|
|
|
36
|
-
type CallbackServerInfo = {
|
|
37
|
-
server: Server;
|
|
38
|
-
cancelWait: () => void;
|
|
39
|
-
waitForCode: () => Promise<{ code: string; state: string } | null>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Start a local HTTP server to receive the OAuth callback
|
|
44
|
-
*/
|
|
45
|
-
async function startCallbackServer(): Promise<CallbackServerInfo> {
|
|
46
|
-
const { createServer } = await import("http");
|
|
47
|
-
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
let result: { code: string; state: string } | null = null;
|
|
50
|
-
let cancelled = false;
|
|
51
|
-
|
|
52
|
-
const server = createServer((req, res) => {
|
|
53
|
-
const url = new URL(req.url || "", `http://localhost:51121`);
|
|
54
|
-
|
|
55
|
-
if (url.pathname === "/oauth-callback") {
|
|
56
|
-
const code = url.searchParams.get("code");
|
|
57
|
-
const state = url.searchParams.get("state");
|
|
58
|
-
const error = url.searchParams.get("error");
|
|
59
|
-
|
|
60
|
-
if (error) {
|
|
61
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
62
|
-
res.end(
|
|
63
|
-
`<html><body><h1>Authentication Failed</h1><p>Error: ${error}</p><p>You can close this window.</p></body></html>`,
|
|
64
|
-
);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (code && state) {
|
|
69
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
70
|
-
res.end(
|
|
71
|
-
`<html><body><h1>Authentication Successful</h1><p>You can close this window and return to the terminal.</p></body></html>`,
|
|
72
|
-
);
|
|
73
|
-
result = { code, state };
|
|
74
|
-
} else {
|
|
75
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
76
|
-
res.end(
|
|
77
|
-
`<html><body><h1>Authentication Failed</h1><p>Missing code or state parameter.</p></body></html>`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
} else {
|
|
81
|
-
res.writeHead(404);
|
|
82
|
-
res.end();
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
server.on("error", (err) => {
|
|
87
|
-
reject(err);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
server.listen(51121, "127.0.0.1", () => {
|
|
91
|
-
resolve({
|
|
92
|
-
server,
|
|
93
|
-
cancelWait: () => {
|
|
94
|
-
cancelled = true;
|
|
95
|
-
},
|
|
96
|
-
waitForCode: async () => {
|
|
97
|
-
const sleep = () => new Promise((r) => setTimeout(r, 100));
|
|
98
|
-
while (!result && !cancelled) {
|
|
99
|
-
await sleep();
|
|
100
|
-
}
|
|
101
|
-
return result;
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Parse redirect URL to extract code and state
|
|
110
|
-
*/
|
|
111
|
-
function parseRedirectUrl(input: string): { code?: string; state?: string } {
|
|
112
|
-
const value = input.trim();
|
|
113
|
-
if (!value) return {};
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const url = new URL(value);
|
|
117
|
-
return {
|
|
118
|
-
code: url.searchParams.get("code") ?? undefined,
|
|
119
|
-
state: url.searchParams.get("state") ?? undefined,
|
|
120
|
-
};
|
|
121
|
-
} catch {
|
|
122
|
-
// Not a URL, return empty
|
|
123
|
-
return {};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
30
|
interface LoadCodeAssistPayload {
|
|
128
31
|
cloudaicompanionProject?: string | { id?: string };
|
|
129
32
|
currentTier?: { id?: string };
|
|
130
33
|
allowedTiers?: Array<{ id?: string; isDefault?: boolean }>;
|
|
131
34
|
}
|
|
132
35
|
|
|
133
|
-
/**
|
|
134
|
-
* Discover or provision a project for the user
|
|
135
|
-
*/
|
|
136
36
|
async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise<string> {
|
|
137
37
|
const headers = {
|
|
138
38
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -146,7 +46,6 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
146
46
|
}),
|
|
147
47
|
};
|
|
148
48
|
|
|
149
|
-
// Try endpoints in order: prod first, then sandbox
|
|
150
49
|
const endpoints = ["https://cloudcode-pa.googleapis.com", "https://daily-cloudcode-pa.sandbox.googleapis.com"];
|
|
151
50
|
|
|
152
51
|
onProgress?.("Checking for existing project...");
|
|
@@ -168,7 +67,6 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
168
67
|
if (loadResponse.ok) {
|
|
169
68
|
const data = (await loadResponse.json()) as LoadCodeAssistPayload;
|
|
170
69
|
|
|
171
|
-
// Handle both string and object formats
|
|
172
70
|
if (typeof data.cloudaicompanionProject === "string" && data.cloudaicompanionProject) {
|
|
173
71
|
return data.cloudaicompanionProject;
|
|
174
72
|
}
|
|
@@ -185,20 +83,14 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
|
|
|
185
83
|
}
|
|
186
84
|
}
|
|
187
85
|
|
|
188
|
-
// Use fallback project ID
|
|
189
86
|
onProgress?.("Using default project...");
|
|
190
87
|
return DEFAULT_PROJECT_ID;
|
|
191
88
|
}
|
|
192
89
|
|
|
193
|
-
/**
|
|
194
|
-
* Get user email from the access token
|
|
195
|
-
*/
|
|
196
90
|
async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
|
197
91
|
try {
|
|
198
92
|
const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
199
|
-
headers: {
|
|
200
|
-
Authorization: `Bearer ${accessToken}`,
|
|
201
|
-
},
|
|
93
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
202
94
|
});
|
|
203
95
|
|
|
204
96
|
if (response.ok) {
|
|
@@ -211,165 +103,51 @@ async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
|
|
211
103
|
return undefined;
|
|
212
104
|
}
|
|
213
105
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
export async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
|
|
218
|
-
const response = await fetch(TOKEN_URL, {
|
|
219
|
-
method: "POST",
|
|
220
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
221
|
-
body: new URLSearchParams({
|
|
222
|
-
client_id: CLIENT_ID,
|
|
223
|
-
client_secret: CLIENT_SECRET,
|
|
224
|
-
refresh_token: refreshToken,
|
|
225
|
-
grant_type: "refresh_token",
|
|
226
|
-
}),
|
|
227
|
-
});
|
|
106
|
+
class AntigravityOAuthFlow extends OAuthCallbackFlow {
|
|
107
|
+
private verifier: string = "";
|
|
108
|
+
private challenge: string = "";
|
|
228
109
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
throw new Error(`Antigravity token refresh failed: ${error}`);
|
|
110
|
+
constructor(ctrl: OAuthController) {
|
|
111
|
+
super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
|
|
232
112
|
}
|
|
233
113
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
refresh: data.refresh_token || refreshToken,
|
|
242
|
-
access: data.access_token,
|
|
243
|
-
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
244
|
-
projectId,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
114
|
+
protected async generateAuthUrl(
|
|
115
|
+
state: string,
|
|
116
|
+
redirectUri: string,
|
|
117
|
+
): Promise<{ url: string; instructions?: string }> {
|
|
118
|
+
const pkce = await generatePKCE();
|
|
119
|
+
this.verifier = pkce.verifier;
|
|
120
|
+
this.challenge = pkce.challenge;
|
|
247
121
|
|
|
248
|
-
/**
|
|
249
|
-
* Login with Antigravity OAuth
|
|
250
|
-
*
|
|
251
|
-
* @param onAuth - Callback with URL and optional instructions
|
|
252
|
-
* @param onProgress - Optional progress callback
|
|
253
|
-
* @param onManualCodeInput - Optional promise that resolves with user-pasted redirect URL.
|
|
254
|
-
* Races with browser callback - whichever completes first wins.
|
|
255
|
-
*/
|
|
256
|
-
export async function loginAntigravity(
|
|
257
|
-
onAuth: (info: { url: string; instructions?: string }) => void,
|
|
258
|
-
onProgress?: (message: string) => void,
|
|
259
|
-
onManualCodeInput?: () => Promise<string>,
|
|
260
|
-
): Promise<OAuthCredentials> {
|
|
261
|
-
const { verifier, challenge } = await generatePKCE();
|
|
262
|
-
|
|
263
|
-
// Start local server for callback
|
|
264
|
-
onProgress?.("Starting local server for OAuth callback...");
|
|
265
|
-
const server = await startCallbackServer();
|
|
266
|
-
|
|
267
|
-
let code: string | undefined;
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
// Build authorization URL
|
|
271
122
|
const authParams = new URLSearchParams({
|
|
272
123
|
client_id: CLIENT_ID,
|
|
273
124
|
response_type: "code",
|
|
274
|
-
redirect_uri:
|
|
125
|
+
redirect_uri: redirectUri,
|
|
275
126
|
scope: SCOPES.join(" "),
|
|
276
|
-
code_challenge: challenge,
|
|
127
|
+
code_challenge: this.challenge,
|
|
277
128
|
code_challenge_method: "S256",
|
|
278
|
-
state
|
|
129
|
+
state,
|
|
279
130
|
access_type: "offline",
|
|
280
131
|
prompt: "consent",
|
|
281
132
|
});
|
|
282
133
|
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
onAuth({
|
|
287
|
-
url: authUrl,
|
|
288
|
-
instructions: "Complete the sign-in in your browser.",
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// Wait for the callback, racing with manual input if provided
|
|
292
|
-
onProgress?.("Waiting for OAuth callback...");
|
|
293
|
-
|
|
294
|
-
if (onManualCodeInput) {
|
|
295
|
-
// Race between browser callback and manual input
|
|
296
|
-
let manualInput: string | undefined;
|
|
297
|
-
let manualError: Error | undefined;
|
|
298
|
-
const manualPromise = onManualCodeInput()
|
|
299
|
-
.then((input) => {
|
|
300
|
-
manualInput = input;
|
|
301
|
-
server.cancelWait();
|
|
302
|
-
})
|
|
303
|
-
.catch((err) => {
|
|
304
|
-
manualError = err instanceof Error ? err : new Error(String(err));
|
|
305
|
-
server.cancelWait();
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const result = await server.waitForCode();
|
|
309
|
-
|
|
310
|
-
// If manual input was cancelled, throw that error
|
|
311
|
-
if (manualError) {
|
|
312
|
-
throw manualError;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (result?.code) {
|
|
316
|
-
// Browser callback won - verify state
|
|
317
|
-
if (result.state !== verifier) {
|
|
318
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
319
|
-
}
|
|
320
|
-
code = result.code;
|
|
321
|
-
} else if (manualInput) {
|
|
322
|
-
// Manual input won
|
|
323
|
-
const parsed = parseRedirectUrl(manualInput);
|
|
324
|
-
if (parsed.state && parsed.state !== verifier) {
|
|
325
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
326
|
-
}
|
|
327
|
-
code = parsed.code;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// If still no code, wait for manual promise and try that
|
|
331
|
-
if (!code) {
|
|
332
|
-
await manualPromise;
|
|
333
|
-
if (manualError) {
|
|
334
|
-
throw manualError;
|
|
335
|
-
}
|
|
336
|
-
if (manualInput) {
|
|
337
|
-
const parsed = parseRedirectUrl(manualInput);
|
|
338
|
-
if (parsed.state && parsed.state !== verifier) {
|
|
339
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
340
|
-
}
|
|
341
|
-
code = parsed.code;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
// Original flow: just wait for callback
|
|
346
|
-
const result = await server.waitForCode();
|
|
347
|
-
if (result?.code) {
|
|
348
|
-
if (result.state !== verifier) {
|
|
349
|
-
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
350
|
-
}
|
|
351
|
-
code = result.code;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
134
|
+
const url = `${AUTH_URL}?${authParams.toString()}`;
|
|
135
|
+
return { url, instructions: "Complete the sign-in in your browser." };
|
|
136
|
+
}
|
|
354
137
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
138
|
+
protected async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
|
|
139
|
+
this.ctrl.onProgress?.("Exchanging authorization code for tokens...");
|
|
358
140
|
|
|
359
|
-
// Exchange code for tokens
|
|
360
|
-
onProgress?.("Exchanging authorization code for tokens...");
|
|
361
141
|
const tokenResponse = await fetch(TOKEN_URL, {
|
|
362
142
|
method: "POST",
|
|
363
|
-
headers: {
|
|
364
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
365
|
-
},
|
|
143
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
366
144
|
body: new URLSearchParams({
|
|
367
145
|
client_id: CLIENT_ID,
|
|
368
146
|
client_secret: CLIENT_SECRET,
|
|
369
147
|
code,
|
|
370
148
|
grant_type: "authorization_code",
|
|
371
|
-
redirect_uri:
|
|
372
|
-
code_verifier: verifier,
|
|
149
|
+
redirect_uri: redirectUri,
|
|
150
|
+
code_verifier: this.verifier,
|
|
373
151
|
}),
|
|
374
152
|
});
|
|
375
153
|
|
|
@@ -388,26 +166,58 @@ export async function loginAntigravity(
|
|
|
388
166
|
throw new Error("No refresh token received. Please try again.");
|
|
389
167
|
}
|
|
390
168
|
|
|
391
|
-
|
|
392
|
-
onProgress?.("Getting user info...");
|
|
169
|
+
this.ctrl.onProgress?.("Getting user info...");
|
|
393
170
|
const email = await getUserEmail(tokenData.access_token);
|
|
171
|
+
const projectId = await discoverProject(tokenData.access_token, this.ctrl.onProgress);
|
|
394
172
|
|
|
395
|
-
|
|
396
|
-
const projectId = await discoverProject(tokenData.access_token, onProgress);
|
|
397
|
-
|
|
398
|
-
// Calculate expiry time (current time + expires_in seconds - 5 min buffer)
|
|
399
|
-
const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;
|
|
400
|
-
|
|
401
|
-
const credentials: OAuthCredentials = {
|
|
173
|
+
return {
|
|
402
174
|
refresh: tokenData.refresh_token,
|
|
403
175
|
access: tokenData.access_token,
|
|
404
|
-
expires:
|
|
176
|
+
expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
|
|
405
177
|
projectId,
|
|
406
178
|
email,
|
|
407
179
|
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Login with Antigravity OAuth
|
|
185
|
+
*/
|
|
186
|
+
export async function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
187
|
+
const flow = new AntigravityOAuthFlow(ctrl);
|
|
188
|
+
return flow.login();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Refresh Antigravity token
|
|
193
|
+
*/
|
|
194
|
+
export async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
|
|
195
|
+
const response = await fetch(TOKEN_URL, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
198
|
+
body: new URLSearchParams({
|
|
199
|
+
client_id: CLIENT_ID,
|
|
200
|
+
client_secret: CLIENT_SECRET,
|
|
201
|
+
refresh_token: refreshToken,
|
|
202
|
+
grant_type: "refresh_token",
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
408
205
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
const error = await response.text();
|
|
208
|
+
throw new Error(`Antigravity token refresh failed: ${error}`);
|
|
412
209
|
}
|
|
210
|
+
|
|
211
|
+
const data = (await response.json()) as {
|
|
212
|
+
access_token: string;
|
|
213
|
+
expires_in: number;
|
|
214
|
+
refresh_token?: string;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
refresh: data.refresh_token || refreshToken,
|
|
219
|
+
access: data.access_token,
|
|
220
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
221
|
+
projectId,
|
|
222
|
+
};
|
|
413
223
|
}
|