@herbertgao/pi-extensions 2026.9.3 → 2026.9.5

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.
Files changed (143) hide show
  1. package/README.md +2 -1
  2. package/THIRD_PARTY_NOTICES.md +25 -0
  3. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  4. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  6. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  11. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  15. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  16. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  17. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +88 -19
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
  26. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  35. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  37. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  38. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  39. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  40. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  41. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  42. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  43. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  44. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  45. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  46. package/node_modules/@tifan/pi-recap/package.json +3 -2
  47. package/node_modules/pi-antigravity/LICENSE +21 -0
  48. package/node_modules/pi-antigravity/README.md +194 -0
  49. package/node_modules/pi-antigravity/package.json +67 -0
  50. package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
  51. package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
  52. package/node_modules/pi-antigravity/src/client/client.ts +561 -0
  53. package/node_modules/pi-antigravity/src/client/index.ts +1 -0
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  55. package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
  56. package/node_modules/pi-antigravity/src/image/image.ts +336 -0
  57. package/node_modules/pi-antigravity/src/image/index.ts +1 -0
  58. package/node_modules/pi-antigravity/src/index.ts +280 -0
  59. package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
  60. package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
  61. package/node_modules/pi-antigravity/src/models/index.ts +3 -0
  62. package/node_modules/pi-antigravity/src/models/models.ts +500 -0
  63. package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
  64. package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
  65. package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
  66. package/node_modules/pi-antigravity/src/types/index.ts +2 -0
  67. package/node_modules/pi-antigravity/src/types/types.ts +292 -0
  68. package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
  69. package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
  70. package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
  71. package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
  72. package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
  73. package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
  74. package/node_modules/pi-antigravity/tsconfig.json +21 -0
  75. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  76. package/node_modules/pi-mcp-adapter/README.md +125 -19
  77. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  78. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  79. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  80. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  81. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  82. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  83. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  84. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  85. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  86. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  87. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  88. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  89. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  90. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  91. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  92. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  93. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  94. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  95. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  96. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  97. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  98. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  99. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  100. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  101. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  102. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  103. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  104. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  105. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  106. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  107. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  108. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  109. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  110. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  111. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  112. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  113. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  114. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  115. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  116. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  117. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  118. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  119. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  120. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  121. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  122. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  123. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  124. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  125. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  126. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  127. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  128. package/node_modules/pi-mcp-adapter/package.json +13 -3
  129. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  130. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  131. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  132. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  133. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  134. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  135. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  136. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  137. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  138. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  139. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  140. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  141. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  142. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  143. package/package.json +15 -15
@@ -0,0 +1,442 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer, type Server } from "node:http";
3
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
4
+ import { defaultProjectId, loadCodeAssist } from "../client/client.js";
5
+ import { escapeHtml, antigravityEnv } from "../utils/util.js";
6
+ import { resolveCallbackHost, redactSecrets } from "../utils/security.js";
7
+ import type { AntigravityOAuthCredentials, CallbackServer } from "../types/types.js";
8
+
9
+ export const REDIRECT_URI = "http://localhost:51121/oauth-callback";
10
+ export const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
11
+ export const TOKEN_URL = "https://oauth2.googleapis.com/token";
12
+ export const OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
13
+ export const SCOPES = [
14
+ "https://www.googleapis.com/auth/aicode",
15
+ "https://www.googleapis.com/auth/cloud-platform",
16
+ "https://www.googleapis.com/auth/userinfo.email",
17
+ "https://www.googleapis.com/auth/userinfo.profile",
18
+ "https://www.googleapis.com/auth/cclog",
19
+ "https://www.googleapis.com/auth/experimentsandconfigs",
20
+ ];
21
+
22
+ /**
23
+ * Default OAuth client is Google's public Antigravity desktop client (not a private app secret).
24
+ * Prefer ANTIGRAVITY_CLIENT_ID / ANTIGRAVITY_CLIENT_SECRET when you manage your own OAuth app.
25
+ */
26
+ export const CLIENT_ID =
27
+ antigravityEnv("CLIENT_ID") ||
28
+ atob(
29
+ "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlc" +
30
+ "C5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
31
+ );
32
+ export const CLIENT_SECRET =
33
+ antigravityEnv("CLIENT_SECRET") || atob("R09DU1BYLUs1OEZXUjQ" + "4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
34
+
35
+ export const CALLBACK_HOST = resolveCallbackHost();
36
+
37
+ function oauthCallbackHeaders(contentType = "text/html; charset=utf-8"): Record<string, string> {
38
+ return {
39
+ "Content-Type": contentType,
40
+ "Cache-Control": "no-store",
41
+ "X-Content-Type-Options": "nosniff",
42
+ "Content-Security-Policy":
43
+ "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'",
44
+ "Referrer-Policy": "no-referrer",
45
+ };
46
+ }
47
+
48
+ function sanitizeOAuthProviderError(text: string): string {
49
+ const redacted = redactSecrets(text).trim();
50
+ try {
51
+ const parsed = JSON.parse(redacted) as {
52
+ error?: string;
53
+ error_description?: string;
54
+ };
55
+ const parts = [parsed.error, parsed.error_description].filter(
56
+ (part): part is string => typeof part === "string" && part.length > 0,
57
+ );
58
+ if (parts.length) return parts.join(": ").slice(0, 300);
59
+ } catch {
60
+ // not JSON
61
+ }
62
+ return redacted.slice(0, 300) || "unknown OAuth provider error";
63
+ }
64
+
65
+ function base64Url(buffer: Buffer): string {
66
+ return buffer.toString("base64url");
67
+ }
68
+
69
+ function generatePKCE(): { verifier: string; challenge: string } {
70
+ const verifier = base64Url(randomBytes(32));
71
+ const challenge = base64Url(createHash("sha256").update(verifier).digest());
72
+ return { verifier, challenge };
73
+ }
74
+
75
+ async function getUserEmail(token: string): Promise<string | undefined> {
76
+ try {
77
+ const res = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
78
+ headers: { Authorization: `Bearer ${token}` },
79
+ });
80
+ if (!res.ok) return undefined;
81
+ const data = (await res.json()) as { email?: string };
82
+ return data.email;
83
+ } catch {
84
+ return undefined;
85
+ }
86
+ }
87
+
88
+ function closeServerGracefully(server: Server): void {
89
+ if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
90
+ server.closeAllConnections();
91
+ }
92
+ server.close();
93
+ }
94
+
95
+ function startCallbackServer(expectedState: string): Promise<CallbackServer> {
96
+ return new Promise((resolve, reject) => {
97
+ let settled = false;
98
+ let timeout: ReturnType<typeof setTimeout> | undefined;
99
+ let resolveCode!: (value: { code: string; state: string }) => void;
100
+ let rejectCode!: (error: Error) => void;
101
+ const codePromise = new Promise<{ code: string; state: string }>((res, rej) => {
102
+ resolveCode = res;
103
+ rejectCode = rej;
104
+ });
105
+
106
+ const finish = (fn: () => void) => {
107
+ if (settled) return;
108
+ settled = true;
109
+ if (timeout) clearTimeout(timeout);
110
+ fn();
111
+ };
112
+
113
+ const server = createServer((req, res) => {
114
+ if (req.method !== "GET" && req.method !== "HEAD") {
115
+ res.writeHead(405, oauthCallbackHeaders("text/plain; charset=utf-8"));
116
+ res.end("Method Not Allowed");
117
+ return;
118
+ }
119
+
120
+ const url = new URL(req.url || "", REDIRECT_URI);
121
+ if (url.pathname !== "/oauth-callback") {
122
+ res.writeHead(404, oauthCallbackHeaders());
123
+ res.end("Antigravity OAuth callback route not found.");
124
+ return;
125
+ }
126
+
127
+ const error = url.searchParams.get("error");
128
+ const code = url.searchParams.get("code");
129
+ const state = url.searchParams.get("state");
130
+ if (error) {
131
+ const safe = escapeHtml(error.slice(0, 200));
132
+ res.writeHead(400, oauthCallbackHeaders());
133
+ res.end(`Antigravity authentication failed: ${safe}`);
134
+ finish(() => rejectCode(new Error(`OAuth error: ${error.slice(0, 200)}`)));
135
+ return;
136
+ }
137
+ if (!code || !state) {
138
+ res.writeHead(400, oauthCallbackHeaders());
139
+ res.end("Antigravity authentication failed: missing code or state.");
140
+ finish(() => rejectCode(new Error("Missing code or state in OAuth callback")));
141
+ return;
142
+ }
143
+ if (state !== expectedState) {
144
+ res.writeHead(400, oauthCallbackHeaders());
145
+ res.end("Antigravity authentication failed: invalid state.");
146
+ finish(() => rejectCode(new Error("OAuth state mismatch")));
147
+ return;
148
+ }
149
+
150
+ res.writeHead(200, oauthCallbackHeaders());
151
+ res.end("Antigravity authentication complete. You can close this window and return to Pi.");
152
+ finish(() => resolveCode({ code, state }));
153
+ });
154
+
155
+ server.on("error", (err: NodeJS.ErrnoException) => {
156
+ if (err.code === "EADDRINUSE") {
157
+ reject(
158
+ new Error(
159
+ "Port 51121 is already in use by another process. Please close the process using port 51121 and retry /login antigravity.",
160
+ ),
161
+ );
162
+ } else {
163
+ reject(err);
164
+ }
165
+ });
166
+
167
+ let closed = false;
168
+ const close = () => {
169
+ if (closed) return;
170
+ closed = true;
171
+ closeServerGracefully(server);
172
+ };
173
+ const cleanup = () => {
174
+ finish(() => rejectCode(new Error("OAuth callback cancelled")));
175
+ close();
176
+ };
177
+
178
+ server.listen(51121, CALLBACK_HOST, () => {
179
+ timeout = setTimeout(() => {
180
+ finish(() => rejectCode(new Error("OAuth callback timed out waiting for browser login")));
181
+ close();
182
+ }, OAUTH_CALLBACK_TIMEOUT_MS);
183
+ resolve({ server, waitForCode: () => codePromise, cleanup });
184
+ });
185
+ });
186
+ }
187
+
188
+ function credentialProjectId(credentials: OAuthCredentials): string | undefined {
189
+ const projectId = (credentials as AntigravityOAuthCredentials).projectId;
190
+ return typeof projectId === "string" ? projectId : undefined;
191
+ }
192
+
193
+ function credentialEmail(credentials: OAuthCredentials): string | undefined {
194
+ const email = (credentials as AntigravityOAuthCredentials).email;
195
+ return typeof email === "string" ? email : undefined;
196
+ }
197
+
198
+ /**
199
+ * Parse a callback URL (or bare query string) pasted by the user from a
200
+ * remote/headless browser that could not reach the local callback server.
201
+ * Returns the validated {code, state} or throws a descriptive error. The checks
202
+ * mirror the local callback server so a pasted URL is accepted under the exact
203
+ * same rules as a same-machine browser redirect.
204
+ */
205
+ function parsePastedCallback(raw: string, expectedState: string): { code: string; state: string } {
206
+ const text = (raw ?? "").trim();
207
+ if (!text) {
208
+ throw new Error(
209
+ "No callback pasted. Paste the full URL from your browser's address bar (http://localhost:51121/oauth-callback?…).",
210
+ );
211
+ }
212
+ let url: URL;
213
+ try {
214
+ url = new URL(text);
215
+ } catch {
216
+ // Bare query string: "state=…&code=…" or "?state=…&code=…"
217
+ const qs = text.startsWith("?") ? text.slice(1) : text;
218
+ url = new URL(`http://localhost:51121/oauth-callback?${qs}`);
219
+ }
220
+ const error = url.searchParams.get("error");
221
+ if (error) throw new Error(`OAuth error from browser: ${error.slice(0, 200)}`);
222
+ const code = url.searchParams.get("code");
223
+ const state = url.searchParams.get("state");
224
+ if (!code || !state) {
225
+ throw new Error(
226
+ "Pasted text is missing 'code' or 'state'. Paste the FULL callback URL from your browser's address bar (http://localhost:51121/oauth-callback?…).",
227
+ );
228
+ }
229
+ if (state !== expectedState) {
230
+ throw new Error(
231
+ "State mismatch: that callback is from a different sign-in. Re-run /login antigravity, sign in again, and paste the new URL.",
232
+ );
233
+ }
234
+ return { code, state };
235
+ }
236
+
237
+ /**
238
+ * Resolve the authorization code from either the local callback server (browser
239
+ * on the same machine) or a pasted callback URL (browser on a remote/headless
240
+ * machine that cannot reach localhost:51121). Whichever provides a valid code
241
+ * first wins; the losing path is cancelled. Honors callbacks.signal for cancel.
242
+ */
243
+ async function acquireAuthCode(
244
+ expectedState: string,
245
+ opts: {
246
+ waitForCode: () => Promise<{ code: string; state: string }>;
247
+ callbacks: OAuthLoginCallbacks;
248
+ },
249
+ ): Promise<{ code: string; state: string }> {
250
+ const { waitForCode, callbacks } = opts;
251
+ // Signals the losing candidate(s) to stop once the race settles.
252
+ const settle = new AbortController();
253
+ const candidates: Promise<{ code: string; state: string }>[] = [waitForCode()];
254
+
255
+ // Manual paste path: offered whenever the callback surface can prompt.
256
+ if (typeof callbacks.onPrompt === "function") {
257
+ const promptLoop = async (): Promise<{ code: string; state: string }> => {
258
+ let promptMessage =
259
+ "Remote/headless machine (your browser can't reach localhost:51121)? " +
260
+ "After signing in, paste the full callback URL shown in your browser's address bar.";
261
+ while (true) {
262
+ if (callbacks.signal?.aborted || settle.signal.aborted) {
263
+ throw new Error("Login cancelled");
264
+ }
265
+ let text: string;
266
+ try {
267
+ text = await callbacks.onPrompt({
268
+ message: promptMessage,
269
+ placeholder: "http://localhost:51121/oauth-callback?state=…&code=…",
270
+ });
271
+ } catch (err: unknown) {
272
+ // Prompt/transport failure — propagate, do not retry.
273
+ throw err instanceof Error ? err : new Error(String(err));
274
+ }
275
+ if (callbacks.signal?.aborted || settle.signal.aborted) {
276
+ throw new Error("Login cancelled");
277
+ }
278
+ try {
279
+ return parsePastedCallback(text, expectedState);
280
+ } catch (err: unknown) {
281
+ if (callbacks.signal?.aborted || settle.signal.aborted) {
282
+ throw new Error("Login cancelled", { cause: err });
283
+ }
284
+ const errorMessage = err instanceof Error ? err.message : String(err);
285
+ promptMessage = `Invalid callback (${errorMessage}). Please paste the full URL:`;
286
+ }
287
+ }
288
+ };
289
+ candidates.push(promptLoop());
290
+ }
291
+
292
+ // Flow cancellation (escape) aborts the whole login when a signal is provided.
293
+ let removeAbortListener: (() => void) | undefined;
294
+ if (callbacks.signal) {
295
+ candidates.push(
296
+ new Promise<never>((_resolve, reject) => {
297
+ const signal = callbacks.signal as AbortSignal;
298
+ if (signal.aborted) return reject(new Error("Login cancelled"));
299
+ const onAbort = () => reject(new Error("Login cancelled"));
300
+ signal.addEventListener("abort", onAbort, { once: true });
301
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
302
+ }),
303
+ );
304
+ }
305
+
306
+ // Suppress unhandled rejections from losing candidates after the race settles.
307
+ for (const candidate of candidates) {
308
+ candidate.catch(() => {});
309
+ }
310
+
311
+ try {
312
+ return await Promise.race(candidates);
313
+ } finally {
314
+ settle.abort();
315
+ removeAbortListener?.();
316
+ }
317
+ }
318
+
319
+ export async function loginAntigravity(
320
+ callbacks: OAuthLoginCallbacks,
321
+ ): Promise<AntigravityOAuthCredentials> {
322
+ const { verifier, challenge } = generatePKCE();
323
+ // State must be independent of the PKCE verifier so a leaked callback URL
324
+ // cannot also disclose the code_verifier needed to mint tokens.
325
+ const state = base64Url(randomBytes(32));
326
+ const { waitForCode, cleanup } = await startCallbackServer(state);
327
+ try {
328
+ const authParams = new URLSearchParams({
329
+ client_id: CLIENT_ID,
330
+ response_type: "code",
331
+ redirect_uri: REDIRECT_URI,
332
+ scope: SCOPES.join(" "),
333
+ code_challenge: challenge,
334
+ code_challenge_method: "S256",
335
+ state,
336
+ access_type: "offline",
337
+ prompt: "consent",
338
+ });
339
+ callbacks.onAuth({
340
+ url: `${AUTH_URL}?${authParams.toString()}`,
341
+ instructions:
342
+ "Complete Google sign-in. Pi captures the local callback automatically — or, on a remote/headless machine, paste the callback URL when prompted.",
343
+ });
344
+
345
+ const { code, state: returnedState } = await acquireAuthCode(state, {
346
+ waitForCode,
347
+ callbacks,
348
+ });
349
+ if (returnedState !== state) throw new Error("OAuth state mismatch");
350
+
351
+ const tokenResponse = await fetch(TOKEN_URL, {
352
+ method: "POST",
353
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
354
+ body: new URLSearchParams({
355
+ client_id: CLIENT_ID,
356
+ client_secret: CLIENT_SECRET,
357
+ code,
358
+ grant_type: "authorization_code",
359
+ redirect_uri: REDIRECT_URI,
360
+ code_verifier: verifier,
361
+ }).toString(),
362
+ });
363
+ if (!tokenResponse.ok) {
364
+ throw new Error(
365
+ `Token exchange failed: ${sanitizeOAuthProviderError(await tokenResponse.text())}`,
366
+ );
367
+ }
368
+ const tokenData = (await tokenResponse.json()) as {
369
+ access_token: string;
370
+ refresh_token?: string;
371
+ expires_in: number;
372
+ };
373
+ if (!tokenData.refresh_token) {
374
+ throw new Error(
375
+ "No refresh token received. Re-run /login antigravity and allow offline access.",
376
+ );
377
+ }
378
+
379
+ const [email, discoveredProject] = await Promise.all([
380
+ getUserEmail(tokenData.access_token),
381
+ loadCodeAssist(tokenData.access_token),
382
+ ]);
383
+ return {
384
+ refresh: tokenData.refresh_token,
385
+ access: tokenData.access_token,
386
+ expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
387
+ projectId: discoveredProject || defaultProjectId(email || "antigravity-default"),
388
+ email,
389
+ };
390
+ } finally {
391
+ cleanup();
392
+ }
393
+ }
394
+
395
+ export async function refreshAntigravityToken(
396
+ credentials: OAuthCredentials,
397
+ ): Promise<AntigravityOAuthCredentials> {
398
+ const response = await fetch(TOKEN_URL, {
399
+ method: "POST",
400
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
401
+ body: new URLSearchParams({
402
+ client_id: CLIENT_ID,
403
+ client_secret: CLIENT_SECRET,
404
+ refresh_token: credentials.refresh,
405
+ grant_type: "refresh_token",
406
+ }).toString(),
407
+ });
408
+ if (!response.ok) {
409
+ throw new Error(
410
+ `Antigravity token refresh failed: ${sanitizeOAuthProviderError(await response.text())}`,
411
+ );
412
+ }
413
+ const data = (await response.json()) as {
414
+ access_token: string;
415
+ expires_in: number;
416
+ refresh_token?: string;
417
+ };
418
+ // Every refresh mints a brand-new access token, so loadCodeAssist's token-keyed cache
419
+ // can never hit here — skip the extra round-trip entirely when we already know the
420
+ // project id (the normal case; it's set at login and doesn't change token to token).
421
+ const existingProjectId = credentialProjectId(credentials);
422
+ const discoveredProject = existingProjectId ? undefined : await loadCodeAssist(data.access_token);
423
+ const email = credentialEmail(credentials);
424
+ return {
425
+ ...credentials,
426
+ refresh: data.refresh_token || credentials.refresh,
427
+ access: data.access_token,
428
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
429
+ projectId:
430
+ existingProjectId || discoveredProject || defaultProjectId(email || "antigravity-default"),
431
+ };
432
+ }
433
+
434
+ export function getApiKey(credentials: OAuthCredentials): string {
435
+ const email = credentialEmail(credentials);
436
+ return JSON.stringify({
437
+ token: credentials.access,
438
+ projectId: credentialProjectId(credentials) || defaultProjectId(email || "antigravity-default"),
439
+ });
440
+ }
441
+
442
+ export type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";