@cad0p/pi-usage 0.0.0-stage → 0.11.2

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +509 -2
  3. package/bin/piquota.js +199 -0
  4. package/extensions/quota-panel.ts +963 -0
  5. package/package.json +59 -4
  6. package/src/auth/claude-code-auth.js +490 -0
  7. package/src/auth/jwt.js +84 -0
  8. package/src/auth/pi-auth.js +424 -0
  9. package/src/browser/cookies.js +557 -0
  10. package/src/browser/history.js +173 -0
  11. package/src/cache.js +224 -0
  12. package/src/cli/args.js +154 -0
  13. package/src/cli/commands/auth.js +216 -0
  14. package/src/cli/commands/gentle-pi.js +44 -0
  15. package/src/cli/commands/omarchy.js +75 -0
  16. package/src/cli/explain.js +99 -0
  17. package/src/cli/output.js +34 -0
  18. package/src/engine.js +404 -0
  19. package/src/exec.js +31 -0
  20. package/src/gentle-pi/rail-patch.js +335 -0
  21. package/src/http.js +182 -0
  22. package/src/model.js +332 -0
  23. package/src/omarchy/assets/antigravity.svg +3 -0
  24. package/src/omarchy/assets/claude.svg +1 -0
  25. package/src/omarchy/assets/codex.svg +1 -0
  26. package/src/omarchy/assets/github-actions.svg +3 -0
  27. package/src/omarchy/assets/opencode-go.svg +3 -0
  28. package/src/omarchy/publish.js +152 -0
  29. package/src/omarchy/record.js +462 -0
  30. package/src/opencode/dashboard.js +337 -0
  31. package/src/opencode/session.js +485 -0
  32. package/src/providers/antigravity-oauth.js +173 -0
  33. package/src/providers/antigravity.js +225 -0
  34. package/src/providers/backoff.js +146 -0
  35. package/src/providers/claude.js +194 -0
  36. package/src/providers/codex.js +151 -0
  37. package/src/providers/error-kind.js +128 -0
  38. package/src/providers/github-actions.js +379 -0
  39. package/src/providers/opencode-go.js +274 -0
  40. package/src/refresh.js +292 -0
  41. package/src/render/panel.js +411 -0
  42. package/src/render/theme.js +102 -0
  43. package/src/sticky.js +319 -0
@@ -0,0 +1,485 @@
1
+ /**
2
+ * OpenCode Go session resolution (cookie + workspace) and dashboard fetch.
3
+ *
4
+ * Independent implementation: nothing here calls another tool's code. The
5
+ * session cookie is the only credential Pi cannot provide, so it is resolved
6
+ * from, in order:
7
+ * 1. `OPENCODE_GO_AUTH_COOKIE` in the environment;
8
+ * 2. `~/.config/pi-quota/opencode-cookie` (0600, written only by an explicit
9
+ * `piquota auth opencode --paste`);
10
+ * 3. a local browser's cookie store, read read-only (see src/browser/cookies.js).
11
+ */
12
+
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join } from "node:path";
16
+
17
+ import { discoverCookieStores, findCookie } from "../browser/cookies.js";
18
+ import { findRecentWorkspaceIds } from "../browser/history.js";
19
+ import { requestJson } from "../http.js";
20
+ import { parseGoDashboard, parseGoMeters, findWorkspaceIds } from "./dashboard.js";
21
+
22
+ export const OPENCODE_ORIGIN = "https://opencode.ai";
23
+ export const OPENCODE_COOKIE_HOST = "opencode.ai";
24
+ export const OPENCODE_COOKIE_NAME = "auth";
25
+ export const OPENCODE_COOKIE_NAMES = ["__Host-console_session", OPENCODE_COOKIE_NAME];
26
+
27
+ const USER_AGENT =
28
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36";
29
+
30
+ /**
31
+ * @param {{ env?: Record<string, string | undefined>, home?: string }} [options]
32
+ * @returns {{ path: string, workspacePath: string }}
33
+ */
34
+ export function configPaths(options = {}) {
35
+ const env = options.env ?? process.env;
36
+ const base = env.XDG_CONFIG_HOME || join(options.home ?? homedir(), ".config", "pi-quota");
37
+ return {
38
+ path: join(base, "opencode-cookie"),
39
+ workspacePath: join(base, "opencode-workspace"),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * @param {string} path
45
+ * @returns {string | null}
46
+ */
47
+ function readTrimmed(path) {
48
+ try {
49
+ const value = readFileSync(path, "utf-8").trim();
50
+ return value === "" ? null : value;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * @param {string} path
58
+ * @param {string} value
59
+ * @returns {{ ok: boolean, error?: string }}
60
+ */
61
+ export function writeSecretFile(path, value) {
62
+ try {
63
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
64
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
65
+ writeFileSync(temporary, value.trim() + "\n", { encoding: "utf-8", mode: 0o600 });
66
+ try {
67
+ chmodSync(temporary, 0o600);
68
+ } catch {
69
+ // Best-effort in environments without chmod.
70
+ }
71
+ renameSync(temporary, path);
72
+ return { ok: true };
73
+ } catch (error) {
74
+ return { ok: false, error: /** @type {{ message?: string }} */ (error)?.message ?? String(error) };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Resolve the session cookie without ever writing it anywhere new.
80
+ *
81
+ * @param {{
82
+ * env?: Record<string, string | undefined>,
83
+ * home?: string,
84
+ * stores?: import("../browser/cookies.js").CookieStore[],
85
+ * allowBrowser?: boolean,
86
+ * }} [options]
87
+ * @returns {{
88
+ * found: boolean,
89
+ * value?: string,
90
+ * origin: "env" | "config" | "browser" | null,
91
+ * detail: string,
92
+ * encryptedOnly: boolean,
93
+ * storeCount: number,
94
+ * name?: string,
95
+ * }}
96
+ */
97
+ export function resolveCookie(options = {}) {
98
+ const env = options.env ?? process.env;
99
+ const paths = configPaths(options);
100
+
101
+ const fromEnv = env.OPENCODE_GO_AUTH_COOKIE?.trim();
102
+ if (fromEnv) return { found: true, value: fromEnv, origin: "env", detail: "OPENCODE_GO_AUTH_COOKIE", encryptedOnly: false, storeCount: 0 };
103
+
104
+ const fromConfig = readTrimmed(paths.path);
105
+ if (fromConfig) return { found: true, value: fromConfig, origin: "config", detail: paths.path, encryptedOnly: false, storeCount: 0 };
106
+
107
+ if (options.allowBrowser === false) {
108
+ return { found: false, origin: null, detail: "browser lookup disabled", encryptedOnly: false, storeCount: 0 };
109
+ }
110
+
111
+ const stores = options.stores ?? discoverCookieStores(options);
112
+ let lastHit = null;
113
+ let partialMatch = null;
114
+ for (const store of stores) {
115
+ if (store.readability === "encrypted") {
116
+ lastHit = findCookie({ host: OPENCODE_COOKIE_HOST, name: OPENCODE_COOKIE_NAME, stores: [store], home: options.home, secretTool: options.secretTool });
117
+ continue;
118
+ }
119
+ const hits = {};
120
+ for (const name of OPENCODE_COOKIE_NAMES) {
121
+ const hit = findCookie({ host: OPENCODE_COOKIE_HOST, name, stores: [store], home: options.home, secretTool: options.secretTool });
122
+ if (hit.found && hit.value) {
123
+ hits[name] = hit.value;
124
+ }
125
+ lastHit = hit;
126
+ }
127
+ if (hits[OPENCODE_COOKIE_NAME] && hits["__Host-console_session"]) {
128
+ return {
129
+ found: true,
130
+ value: `__Host-console_session=${hits["__Host-console_session"]}; ${OPENCODE_COOKIE_NAME}=${hits[OPENCODE_COOKIE_NAME]}`,
131
+ origin: "browser",
132
+ detail: `${store.browser} ${store.profile}`,
133
+ encryptedOnly: false,
134
+ storeCount: stores.length,
135
+ };
136
+ }
137
+ if (!partialMatch && (hits[OPENCODE_COOKIE_NAME] || hits["__Host-console_session"])) {
138
+ const cookieName = hits[OPENCODE_COOKIE_NAME] ? OPENCODE_COOKIE_NAME : "__Host-console_session";
139
+ partialMatch = {
140
+ found: true,
141
+ value: `${cookieName}=${hits[cookieName]}`,
142
+ origin: "browser",
143
+ detail: `${store.browser} ${store.profile}`,
144
+ encryptedOnly: false,
145
+ storeCount: stores.length,
146
+ name: cookieName,
147
+ };
148
+ }
149
+ }
150
+ if (partialMatch) return partialMatch;
151
+
152
+ const hit = lastHit ?? { encryptedOnly: false, candidates: 0 };
153
+ return {
154
+ found: false,
155
+ origin: null,
156
+ detail:
157
+ stores.length === 0
158
+ ? "no browser cookie store found"
159
+ : hit.encryptedOnly
160
+ ? "only encrypted Chromium stores found (DPAPI is unavailable from WSL)"
161
+ : `no "${OPENCODE_COOKIE_NAMES.join("\" or \"")}" cookie for ${OPENCODE_COOKIE_HOST} in ${hit.candidates} readable store(s)`,
162
+ encryptedOnly: hit.encryptedOnly,
163
+ storeCount: stores.length,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * @param {{ env?: Record<string, string | undefined>, home?: string }} [options]
169
+ * @returns {string | null}
170
+ */
171
+ export function resolveWorkspaceIdFromConfig(options = {}) {
172
+ const env = options.env ?? process.env;
173
+ const fromEnv = env.OPENCODE_GO_WORKSPACE_ID?.trim();
174
+ if (fromEnv) return fromEnv;
175
+ return readTrimmed(configPaths(options).workspacePath);
176
+ }
177
+
178
+ /**
179
+ * Fetch a page with the session cookie.
180
+ *
181
+ * @param {string} url
182
+ * @param {{ cookie: string, fetchFn?: typeof fetch, timeoutMs?: number }} options
183
+ * @returns {Promise<{ ok: boolean, status: number, html: string, redirected: boolean, error?: string }>}
184
+ */
185
+ async function fetchPage(url, options) {
186
+ const fetchFn = options.fetchFn ?? fetch;
187
+ const controller = new AbortController();
188
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 15000);
189
+ try {
190
+ const response = await fetchFn(url, {
191
+ method: "GET",
192
+ redirect: "manual",
193
+ headers: {
194
+ Cookie: `${OPENCODE_COOKIE_NAME}=${options.cookie}`,
195
+ Accept: "text/html,application/xhtml+xml",
196
+ "User-Agent": USER_AGENT,
197
+ },
198
+ signal: controller.signal,
199
+ });
200
+ const location = response.headers?.get?.("location") ?? null;
201
+ const redirected = response.status >= 300 && response.status < 400;
202
+ if (redirected) {
203
+ return {
204
+ ok: false,
205
+ status: response.status,
206
+ html: "",
207
+ redirected: true,
208
+ error: location ? `redirected to ${location.replace(/\?.*$/, "")}` : `HTTP ${response.status}`,
209
+ };
210
+ }
211
+ if (!response.ok) {
212
+ return { ok: false, status: response.status, html: "", redirected: false, error: `HTTP ${response.status}` };
213
+ }
214
+ return { ok: true, status: response.status, html: await response.text(), redirected: false };
215
+ } catch (error) {
216
+ const name = /** @type {{ name?: string }} */ (error)?.name;
217
+ const message = /** @type {{ message?: string, cause?: { code?: string } }} */ (error)?.message ?? String(error);
218
+ const code = /** @type {{ cause?: { code?: string } }} */ (error)?.cause?.code;
219
+ const detail = code ? `${message} (${code})` : message;
220
+ return {
221
+ ok: false,
222
+ status: 0,
223
+ html: "",
224
+ redirected: false,
225
+ error: name === "AbortError" ? "request timed out" : `request failed: ${detail}`,
226
+ };
227
+ } finally {
228
+ clearTimeout(timeout);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Discover the workspace id.
234
+ *
235
+ * opencode.ai serves the workspace app only at `/workspace/<id>/...`: there is
236
+ * no listing route (`/workspace` answers 404 even authenticated), so the id is
237
+ * observed rather than requested. Order:
238
+ * 1. the cached id;
239
+ * 2. Firefox history, because the OAuth flow lands on `/workspace/<id>`;
240
+ * 3. any id referenced by the authenticated console page.
241
+ *
242
+ * @param {{ cookie: string, fetchFn?: typeof fetch, timeoutMs?: number, home?: string, env?: Record<string, string | undefined>, history?: { ids: string[], profile: string | null } }} options
243
+ * @returns {Promise<{ workspaceId: string | null, origin?: string, error?: string }>}
244
+ */
245
+ export async function discoverWorkspaceId(options) {
246
+ const known = resolveWorkspaceIdFromConfig(options);
247
+ if (known) return { workspaceId: known, origin: "config" };
248
+
249
+ const history = options.history ?? findRecentWorkspaceIds(options);
250
+ if (history.ids.length > 0) {
251
+ const paths = configPaths(options);
252
+ writeSecretFile(paths.workspacePath, history.ids[0]);
253
+ return { workspaceId: history.ids[0], origin: `firefox history (${history.profile ?? "unknown"})` };
254
+ }
255
+
256
+ for (const candidate of [`${OPENCODE_ORIGIN}/console/`, `${OPENCODE_ORIGIN}/`]) {
257
+ const page = await fetchPage(candidate, options);
258
+ if (!page.ok) continue;
259
+ const ids = findWorkspaceIds(page.html);
260
+ if (ids.length > 0) {
261
+ const paths = configPaths(options);
262
+ writeSecretFile(paths.workspacePath, ids[0]);
263
+ return { workspaceId: ids[0], origin: `page ${candidate}` };
264
+ }
265
+ }
266
+
267
+ return {
268
+ workspaceId: null,
269
+ error:
270
+ "no workspace id found; finish the login so the browser lands on /workspace/<id>, " +
271
+ "then re-run, or set OPENCODE_GO_WORKSPACE_ID",
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Format a supplied cookie value for an OpenCode request.
277
+ *
278
+ * @param {string} cookie
279
+ * @param {string} [cookieName]
280
+ * @returns {string}
281
+ */
282
+ export function toCookieHeader(cookie, cookieName) {
283
+ const value = cookie.trim();
284
+ if (value.includes("=")) return value;
285
+ if (cookieName) return `${cookieName}=${value}`;
286
+ return `__Host-console_session=${value}; auth=${value}`;
287
+ }
288
+
289
+ /**
290
+ * Fetch Go quota meters from the Console API.
291
+ *
292
+ * @param {{
293
+ * env?: Record<string, string | undefined>,
294
+ * home?: string,
295
+ * fetchFn?: typeof fetch,
296
+ * timeoutMs?: number,
297
+ * now?: number,
298
+ * cookie?: string,
299
+ * cookieName?: string,
300
+ * workspaceId?: string,
301
+ * stores?: import("../browser/cookies.js").CookieStore[],
302
+ * allowBrowser?: boolean,
303
+ * }} [options]
304
+ * @returns {Promise<{
305
+ * ok: boolean,
306
+ * windows: import("../model.js").QuotaWindow[],
307
+ * workspaceId: string | null,
308
+ * planHint: string | null,
309
+ * strategies: string[],
310
+ * cookieOrigin: string,
311
+ * error: string | null,
312
+ * }>}
313
+ */
314
+ export async function fetchGoStatusApi(options = {}) {
315
+ const cookie = options.cookie !== undefined
316
+ ? { found: Boolean(options.cookie), value: options.cookie, origin: "provided", name: options.cookieName }
317
+ : resolveCookie(options);
318
+ if (!cookie.found || !cookie.value) {
319
+ return {
320
+ ok: false, windows: [], workspaceId: null, planHint: null, strategies: [], cookieOrigin: "none",
321
+ error: `no OpenCode session cookie: ${cookie.detail ?? "not found"}`,
322
+ };
323
+ }
324
+
325
+ const headers = {
326
+ Cookie: toCookieHeader(cookie.value, cookie.name),
327
+ Accept: "application/json",
328
+ "User-Agent": USER_AGENT,
329
+ };
330
+ const ids = [];
331
+ for (const id of [options.workspaceId, resolveWorkspaceIdFromConfig(options)]) {
332
+ if (id && !ids.includes(id)) ids.push(id);
333
+ }
334
+
335
+ const errors = [];
336
+ const tryStatus = async (orgId) => {
337
+ const response = await requestJson(`${OPENCODE_ORIGIN}/console/api/go/status`, {
338
+ fetchFn: options.fetchFn,
339
+ timeoutMs: options.timeoutMs,
340
+ headers: { ...headers, "x-org-id": orgId },
341
+ });
342
+ if (!response.ok) {
343
+ errors.push(response.error ?? `HTTP ${response.status}`);
344
+ return null;
345
+ }
346
+ const body = response.body && typeof response.body === "object" ? response.body : null;
347
+ const access = body && "access" in body && body.access && typeof body.access === "object" ? body.access : null;
348
+ const meters = access && "meters" in access ? access.meters : null;
349
+ const windows = parseGoMeters(meters, { now: options.now });
350
+ if (windows.length === 0) {
351
+ errors.push("Console API returned no Go usage meters");
352
+ return null;
353
+ }
354
+ writeSecretFile(configPaths(options).workspacePath, orgId);
355
+ return {
356
+ ok: true, windows, workspaceId: orgId, planHint: "Go subscription", strategies: ["console-api"],
357
+ cookieOrigin: String(cookie.origin), error: null,
358
+ };
359
+ };
360
+
361
+ for (const orgId of ids) {
362
+ const result = await tryStatus(orgId);
363
+ if (result) return result;
364
+ }
365
+
366
+ const orgs = await requestJson(`${OPENCODE_ORIGIN}/console/api/orgs`, {
367
+ fetchFn: options.fetchFn,
368
+ timeoutMs: options.timeoutMs,
369
+ headers,
370
+ });
371
+ if (orgs.ok && Array.isArray(orgs.body)) {
372
+ for (const org of orgs.body) {
373
+ const orgId = org && typeof org === "object" && typeof org.id === "string" ? org.id : null;
374
+ if (!orgId || ids.includes(orgId)) continue;
375
+ const result = await tryStatus(orgId);
376
+ if (result) return result;
377
+ }
378
+ } else {
379
+ errors.push(orgs.error ?? `HTTP ${orgs.status}`);
380
+ }
381
+
382
+ return {
383
+ ok: false, windows: [], workspaceId: null, planHint: null, strategies: [],
384
+ cookieOrigin: String(cookie.origin), error: errors[0] ?? "Console API returned no organizations",
385
+ };
386
+ }
387
+
388
+ /**
389
+ * Read the Go plan windows for one account.
390
+ *
391
+ * @param {{
392
+ * env?: Record<string, string | undefined>,
393
+ * home?: string,
394
+ * fetchFn?: typeof fetch,
395
+ * timeoutMs?: number,
396
+ * now?: number,
397
+ * cookie?: string,
398
+ * workspaceId?: string,
399
+ * }} [options]
400
+ * @returns {Promise<{
401
+ * ok: boolean,
402
+ * windows: import("../model.js").QuotaWindow[],
403
+ * workspaceId: string | null,
404
+ * planHint: string | null,
405
+ * strategies: string[],
406
+ * cookieOrigin: string,
407
+ * error: string | null,
408
+ * }>}
409
+ */
410
+ export async function readGoPlan(options = {}) {
411
+ const apiResult = await fetchGoStatusApi(options);
412
+ if (apiResult.ok) return apiResult;
413
+
414
+ const now = options.now ?? Date.now();
415
+ const cookie = options.cookie !== undefined
416
+ ? { found: Boolean(options.cookie), value: options.cookie, origin: "provided", detail: "provided", encryptedOnly: false, storeCount: 0 }
417
+ : resolveCookie(options);
418
+ if (!cookie.found || !cookie.value) {
419
+ return {
420
+ ok: false,
421
+ windows: [],
422
+ workspaceId: null,
423
+ planHint: null,
424
+ strategies: [],
425
+ cookieOrigin: "none",
426
+ error: `no ${OPENCODE_COOKIE_NAME} cookie: ${cookie.detail}`,
427
+ };
428
+ }
429
+
430
+ const workspace = options.workspaceId
431
+ ? { workspaceId: options.workspaceId }
432
+ : await discoverWorkspaceId({ ...options, cookie: cookie.value });
433
+ if (!workspace.workspaceId) {
434
+ return {
435
+ ok: false,
436
+ windows: [],
437
+ workspaceId: null,
438
+ planHint: null,
439
+ strategies: [],
440
+ cookieOrigin: String(cookie.origin),
441
+ error: `session found (${cookie.detail}) but no workspace id: ${workspace.error ?? "unknown"}`,
442
+ };
443
+ }
444
+
445
+ const url = `${OPENCODE_ORIGIN}/workspace/${encodeURIComponent(workspace.workspaceId)}/go`;
446
+ const page = await fetchPage(url, { ...options, cookie: cookie.value });
447
+ if (!page.ok) {
448
+ return {
449
+ ok: false,
450
+ windows: [],
451
+ workspaceId: workspace.workspaceId,
452
+ planHint: null,
453
+ strategies: [],
454
+ cookieOrigin: String(cookie.origin),
455
+ error: page.redirected
456
+ ? `session rejected (${page.error}); the cookie is missing or expired`
457
+ : `dashboard request failed (${page.error})`,
458
+ };
459
+ }
460
+
461
+ const parsed = parseGoDashboard(page.html, { now });
462
+ if (parsed.windows.length === 0) {
463
+ return {
464
+ ok: false,
465
+ windows: [],
466
+ workspaceId: workspace.workspaceId,
467
+ planHint: parsed.planHint,
468
+ strategies: parsed.strategies,
469
+ cookieOrigin: String(cookie.origin),
470
+ error: "dashboard HTML fetched but no usage windows matched (page layout may have changed)",
471
+ };
472
+ }
473
+
474
+ return {
475
+ ok: true,
476
+ windows: parsed.windows,
477
+ workspaceId: workspace.workspaceId,
478
+ planHint: parsed.planHint,
479
+ strategies: parsed.strategies,
480
+ cookieOrigin: String(cookie.origin),
481
+ error: null,
482
+ };
483
+ }
484
+
485
+ export { fetchPage as fetchOpenCodePage, requestJson };
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Antigravity token refresh, in memory only.
3
+ *
4
+ * Why this is safe here and nowhere else: Google does not rotate the refresh
5
+ * token for this client, so refreshing cannot invalidate the copy Pi stores. The
6
+ * refreshed access token is kept in memory for the quota request and is never
7
+ * written back to any credential file.
8
+ *
9
+ * The OAuth client is Google's public Antigravity desktop client, the same one
10
+ * Pi ships in its `pi-antigravity` package. Resolution order:
11
+ * 1. `ANTIGRAVITY_CLIENT_ID` / `ANTIGRAVITY_CLIENT_SECRET`;
12
+ * 2. the literal embedded in the installed `pi-antigravity` package, so the
13
+ * value self-heals if Google rotates the client;
14
+ * 3. the recorded fallback below.
15
+ */
16
+
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+
21
+ import { requestJson } from "../http.js";
22
+
23
+ export const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
24
+
25
+ // Obfuscated identical to pi-antigravity to avoid triggering public secret scanners
26
+ // on Google's public client desktop credentials.
27
+ const FALLBACK_CLIENT = {
28
+ clientId: Buffer.from(
29
+ "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlc" +
30
+ "C5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
31
+ "base64",
32
+ ).toString("utf-8"),
33
+ clientSecret: Buffer.from(
34
+ "R09DU1BYLUs1OEZXUjQ" + "4NkxkTEoxbUxCOHNYQzR6NnFEQWY=",
35
+ "base64",
36
+ ).toString("utf-8"),
37
+ };
38
+
39
+ const ANTIGRAVITY_OAUTH_SOURCES = [
40
+ join(homedir(), ".pi", "agent", "npm", "node_modules", "pi-antigravity", "src", "auth", "oauth.ts"),
41
+ join(homedir(), ".pi", "agent", "npm", "node_modules", "pi-antigravity", "dist", "auth", "oauth.js"),
42
+ ];
43
+
44
+ /**
45
+ * Recover the two `atob("..." + "...")` literals from the installed package.
46
+ *
47
+ * @param {string} [sourcePath]
48
+ * @returns {{ clientId: string, clientSecret: string } | null}
49
+ */
50
+ export function extractClientFromPiAntigravity(sourcePath) {
51
+ const candidates = sourcePath ? [sourcePath] : ANTIGRAVITY_OAUTH_SOURCES;
52
+ for (const path of candidates) {
53
+ if (!existsSync(path)) continue;
54
+ let text;
55
+ try {
56
+ text = readFileSync(path, "utf-8");
57
+ } catch {
58
+ continue;
59
+ }
60
+ /** @type {string[]} */
61
+ const decoded = [];
62
+ for (const match of text.matchAll(/atob\(\s*((?:"[^"]*"\s*\+?\s*)+)\)/g)) {
63
+ const parts = [...match[1].matchAll(/"([^"]*)"/g)].map((part) => part[1]);
64
+ try {
65
+ decoded.push(Buffer.from(parts.join(""), "base64").toString("utf-8"));
66
+ } catch {
67
+ // Ignore malformed literals and keep looking.
68
+ }
69
+ }
70
+ if (decoded.length >= 2 && decoded[0].includes(".") && decoded[1].length > 10) {
71
+ return { clientId: decoded[0], clientSecret: decoded[1] };
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * @param {{ env?: Record<string, string | undefined>, sourcePath?: string }} [options]
79
+ * @returns {{ clientId: string, clientSecret: string, source: string }}
80
+ */
81
+ export function resolveOAuthClient(options = {}) {
82
+ const env = options.env ?? process.env;
83
+ if (env.ANTIGRAVITY_CLIENT_ID && env.ANTIGRAVITY_CLIENT_SECRET) {
84
+ return { clientId: env.ANTIGRAVITY_CLIENT_ID, clientSecret: env.ANTIGRAVITY_CLIENT_SECRET, source: "env" };
85
+ }
86
+ const extracted = extractClientFromPiAntigravity(options.sourcePath);
87
+ if (extracted) return { ...extracted, source: "pi-antigravity" };
88
+ return { ...FALLBACK_CLIENT, source: "builtin" };
89
+ }
90
+
91
+ /**
92
+ * @param {{ client?: { clientId: string, clientSecret: string }, fetchFn?: typeof fetch, timeoutMs?: number }} [options]
93
+ * @returns {string}
94
+ */
95
+ export function describeOAuthClient(options = {}) {
96
+ const client = options.client ?? resolveOAuthClient();
97
+ return client.source === "env"
98
+ ? "client from ANTIGRAVITY_CLIENT_ID/SECRET"
99
+ : client.source === "pi-antigravity"
100
+ ? "client read from the installed pi-antigravity package"
101
+ : "builtin public Google Antigravity client";
102
+ }
103
+
104
+ /**
105
+ * @param {string} refreshToken
106
+ * @param {{
107
+ * client?: { clientId: string, clientSecret: string },
108
+ * fetchFn?: typeof fetch,
109
+ * timeoutMs?: number,
110
+ * now?: number,
111
+ * }} [options]
112
+ * @returns {Promise<{ access: string, refresh: string, expires: number } | { error: string }>}
113
+ */
114
+ export async function refreshAccessToken(refreshToken, options = {}) {
115
+ if (!refreshToken) return { error: "no refresh token available" };
116
+ const client = options.client ?? resolveOAuthClient();
117
+
118
+ const response = await requestJson(GOOGLE_TOKEN_URL, {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
121
+ body: new URLSearchParams({
122
+ client_id: client.clientId,
123
+ client_secret: client.clientSecret,
124
+ refresh_token: refreshToken,
125
+ grant_type: "refresh_token",
126
+ }).toString(),
127
+ fetchFn: options.fetchFn,
128
+ timeoutMs: options.timeoutMs,
129
+ });
130
+
131
+ if (!response.ok) return { error: `token refresh failed: ${response.error}` };
132
+ const body = /** @type {Record<string, unknown> | null} */ (response.body);
133
+ const access = typeof body?.access_token === "string" ? body.access_token : null;
134
+ if (!access) return { error: "token refresh returned no access_token" };
135
+ const expiresIn = typeof body?.expires_in === "number" ? body.expires_in : 3600;
136
+ return {
137
+ access,
138
+ refresh: typeof body?.refresh_token === "string" && body.refresh_token ? body.refresh_token : refreshToken,
139
+ expires: (options.now ?? Date.now()) + expiresIn * 1000,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Refresh an account in memory when its access token is expired or about to be.
145
+ * Nothing is persisted.
146
+ *
147
+ * @param {{ access?: string, refresh?: string, expiresAtMs?: number | null }} account
148
+ * @param {{
149
+ * client?: { clientId: string, clientSecret: string },
150
+ * fetchFn?: typeof fetch,
151
+ * timeoutMs?: number,
152
+ * now?: number,
153
+ * bufferMs?: number,
154
+ * enabled?: boolean,
155
+ * }} [options]
156
+ * @returns {Promise<{ ok: boolean, refreshed: boolean, error?: string }>}
157
+ */
158
+ export async function ensureFreshAccessToken(account, options = {}) {
159
+ const now = options.now ?? Date.now();
160
+ const bufferMs = options.bufferMs ?? 5 * 60 * 1000;
161
+ const expiring = !account.expiresAtMs || account.expiresAtMs - bufferMs <= now;
162
+ if (!expiring) return { ok: true, refreshed: false };
163
+ if (options.enabled === false) return { ok: Boolean(account.access), refreshed: false, error: "refresh disabled" };
164
+ if (!account.refresh) return { ok: Boolean(account.access), refreshed: false, error: "no refresh token stored" };
165
+
166
+ const result = await refreshAccessToken(account.refresh, { ...options, now });
167
+ if ("error" in result) return { ok: Boolean(account.access), refreshed: false, error: result.error };
168
+
169
+ account.access = result.access;
170
+ account.refresh = result.refresh;
171
+ account.expiresAtMs = result.expires;
172
+ return { ok: true, refreshed: true };
173
+ }