dsh-agy 0.1.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.
@@ -0,0 +1,1221 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import lockfile from "proper-lockfile";
6
+ //#region src/oauth/constants.ts
7
+ var constants_exports = /* @__PURE__ */ __exportAll({
8
+ AGY_CLIENT_ID: () => AGY_CLIENT_ID,
9
+ AGY_CLIENT_SECRET: () => AGY_CLIENT_SECRET,
10
+ AGY_DEFAULT_REDIRECT_URI: () => AGY_DEFAULT_REDIRECT_URI,
11
+ AGY_ENDPOINT_AUTOPUSH: () => AGY_ENDPOINT_AUTOPUSH,
12
+ AGY_ENDPOINT_DAILY: () => AGY_ENDPOINT_DAILY,
13
+ AGY_ENDPOINT_DAILY_SANDBOX: () => AGY_ENDPOINT_DAILY_SANDBOX,
14
+ AGY_ENDPOINT_FALLBACKS: () => AGY_ENDPOINT_FALLBACKS,
15
+ AGY_ENDPOINT_PROD: () => AGY_ENDPOINT_PROD,
16
+ AGY_ENDPOINT_SKIP_STATUSES: () => AGY_ENDPOINT_SKIP_STATUSES,
17
+ AGY_SCOPES: () => AGY_SCOPES,
18
+ AGY_VERSION_FALLBACK: () => AGY_VERSION_FALLBACK,
19
+ OAUTH_AUTHORIZE_URL: () => OAUTH_AUTHORIZE_URL,
20
+ OAUTH_TOKEN_URL: () => OAUTH_TOKEN_URL,
21
+ OAUTH_USERINFO_URL: () => OAUTH_USERINFO_URL,
22
+ fetchAgyFirstOk: () => fetchAgyFirstOk,
23
+ getAgyBootstrapClientMetadata: () => getAgyBootstrapClientMetadata,
24
+ getAgyBootstrapUserAgent: () => getAgyBootstrapUserAgent
25
+ });
26
+ /**
27
+ * Antigravity (agy) OAuth and API constants.
28
+ *
29
+ * The client id/secret below are the public Google consumer-OAuth credentials
30
+ * shipped inside the Antigravity desktop product and its `agy` CLI; they are
31
+ * embedded in many public tools (see NOTICE.md). They are not secrets owned by
32
+ * this project.
33
+ */
34
+ const AGY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
35
+ const AGY_CLIENT_SECRET = "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";
36
+ /** Required scopes. `openid` must NOT be added: it routes Google into the hanging
37
+ * `firstparty/nativeapp` consent for this client (verified by OmniRoute). */
38
+ const AGY_SCOPES = [
39
+ "https://www.googleapis.com/auth/cloud-platform",
40
+ "https://www.googleapis.com/auth/userinfo.email",
41
+ "https://www.googleapis.com/auth/userinfo.profile",
42
+ "https://www.googleapis.com/auth/cclog",
43
+ "https://www.googleapis.com/auth/experimentsandconfigs"
44
+ ];
45
+ const OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth";
46
+ const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
47
+ const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
48
+ /** Default loopback callback used by the standalone CLI listener (fixed port, like opencode). */
49
+ const AGY_DEFAULT_REDIRECT_URI = "http://localhost:51121/oauth-callback";
50
+ /**
51
+ * Antigravity API endpoints. The daily runtime host (no .sandbox suffix) is the
52
+ * live endpoint for consumer OAuth accounts — cloudcode-pa.googleapis.com
53
+ * answers RESOURCE_EXHAUSTED for them (verified by live probe), while the
54
+ * daily host answers 200. Order matters: first reachable non-429/403 wins.
55
+ */
56
+ const AGY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.googleapis.com";
57
+ const AGY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com";
58
+ const AGY_ENDPOINT_DAILY_SANDBOX = "https://daily-cloudcode-pa.sandbox.googleapis.com";
59
+ const AGY_ENDPOINT_AUTOPUSH = "https://autopush-cloudcode-pa.sandbox.googleapis.com";
60
+ /** Runtime/bootstrap endpoint fallback order (daily first, mirroring OmniRoute). */
61
+ const AGY_ENDPOINT_FALLBACKS = [
62
+ AGY_ENDPOINT_DAILY,
63
+ AGY_ENDPOINT_PROD,
64
+ AGY_ENDPOINT_DAILY_SANDBOX,
65
+ AGY_ENDPOINT_AUTOPUSH
66
+ ];
67
+ /** Statuses that mean "this endpoint is not usable for this account"; skip to the next. */
68
+ const AGY_ENDPOINT_SKIP_STATUSES = /* @__PURE__ */ new Set([429, 403]);
69
+ /**
70
+ * Try each runtime endpoint in order, skipping unusable ones (429/403/network).
71
+ * Returns the first other response (2xx or a real error like 400/401); when
72
+ * every endpoint is unusable, returns the last skipped response so the caller's
73
+ * classifier can still produce a meaningful error.
74
+ */
75
+ async function fetchAgyFirstOk(urlPath, init, fetchImpl = fetch) {
76
+ let lastSkipped = null;
77
+ for (const baseEndpoint of AGY_ENDPOINT_FALLBACKS) try {
78
+ const response = await fetchImpl(`${baseEndpoint}${urlPath}`, init);
79
+ if (!AGY_ENDPOINT_SKIP_STATUSES.has(response.status)) return response;
80
+ lastSkipped = response;
81
+ } catch {}
82
+ if (lastSkipped) return lastSkipped;
83
+ throw new Error("all agy endpoints failed");
84
+ }
85
+ /** Default Antigravity client version used in User-Agent strings; overridden by the
86
+ * runtime version fetcher (see runtime/fingerprint.ts). */
87
+ const AGY_VERSION_FALLBACK = "1.18.3";
88
+ /** Electron-style UA used for bootstrap calls (loadCodeAssist/onboardUser). */
89
+ function getAgyBootstrapUserAgent(version = AGY_VERSION_FALLBACK) {
90
+ return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Antigravity/${version} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`;
91
+ }
92
+ /** Client-Metadata payload for bootstrap calls. */
93
+ function getAgyBootstrapClientMetadata(platform = process.platform) {
94
+ return `{"ideType":"ANTIGRAVITY","platform":"${platform === "win32" ? "WINDOWS" : "MACOS"}","pluginType":"GEMINI"}`;
95
+ }
96
+ //#endregion
97
+ //#region src/runtime/identity.ts
98
+ /**
99
+ * Antigravity request/session identity generation (mirrors the active
100
+ * OmniRoute implementation; the archived reference stopped maintaining these).
101
+ */
102
+ /** One request id per upstream call: `agent/<epoch>/<8 hex>`. */
103
+ function generateAntigravityRequestId() {
104
+ return `agent/${Date.now()}/${randomBytes(4).toString("hex")}`;
105
+ }
106
+ const FNV_OFFSET_I64 = -3750763044362895579n;
107
+ const FNV_PRIME_I64 = 1099511628211n;
108
+ /** 64-bit FNV-1a hash of a string (stable across processes). */
109
+ function fnv1a64(input) {
110
+ let hash = FNV_OFFSET_I64;
111
+ for (let i = 0; i < input.length; i++) {
112
+ hash ^= BigInt(input.charCodeAt(i));
113
+ hash = BigInt.asIntN(64, hash * FNV_PRIME_I64);
114
+ }
115
+ return hash;
116
+ }
117
+ /**
118
+ * Stable per-account session id: same account always derives the same id, so
119
+ * multi-turn context caching keys consistently; unknown accounts get a fresh
120
+ * random id.
121
+ */
122
+ function deriveAntigravitySessionId(accountKey) {
123
+ if (!accountKey || accountKey.trim().length === 0) return null;
124
+ const hash = fnv1a64(accountKey.trim());
125
+ return `-${((hash < 0n ? -hash : hash) % 9000000000000000000n).toString()}`;
126
+ }
127
+ //#endregion
128
+ //#region src/runtime/version.ts
129
+ /**
130
+ * Fresh Antigravity client version resolution: fetch from the product's own
131
+ * release feeds (6h TTL, single in-flight dedupe), falling back to the pinned
132
+ * pool. Keeps fingerprint User-Agent version strings current — a stale version
133
+ * is the most detectable fingerprint anomaly.
134
+ */
135
+ const VERSION_CACHE_TTL_MS = 216e5;
136
+ const VERSION_FETCH_TIMEOUT_MS = 5e3;
137
+ /** Source 1: Antigravity IDE auto-update release feed (array of {version}). */
138
+ const IDE_RELEASE_FEED_URL = "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases";
139
+ /** Source 2: agy CLI GitHub releases (object with tag_name). */
140
+ const CLI_RELEASE_URL = "https://api.github.com/repos/google-antigravity/antigravity-cli/releases/latest";
141
+ function compareSemver(a, b) {
142
+ const pa = a.split(".").map((n) => Number(n) || 0);
143
+ const pb = b.split(".").map((n) => Number(n) || 0);
144
+ for (let i = 0; i < 3; i++) {
145
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
146
+ if (diff !== 0) return diff;
147
+ }
148
+ return 0;
149
+ }
150
+ function pickNewestVersion(...versions) {
151
+ const valid = versions.filter((v) => typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v));
152
+ if (valid.length === 0) return null;
153
+ return valid.reduce((best, v) => !best || compareSemver(v, best) > 0 ? v : best, null);
154
+ }
155
+ function parseIdeReleaseFeed(payload) {
156
+ if (!Array.isArray(payload)) return null;
157
+ return pickNewestVersion(...payload.map((entry) => entry?.version));
158
+ }
159
+ function parseCliRelease(payload) {
160
+ if (!payload || typeof payload !== "object") return null;
161
+ const release = payload;
162
+ return pickNewestVersion(release.tag_name ?? release.name);
163
+ }
164
+ async function fetchJsonWithTimeout(fetchImpl, url) {
165
+ const controller = new AbortController();
166
+ const timeoutId = setTimeout(() => controller.abort(), VERSION_FETCH_TIMEOUT_MS);
167
+ try {
168
+ const response = await fetchImpl(url, {
169
+ headers: {
170
+ Accept: "application/json",
171
+ "User-Agent": "dsh-agy/0.1"
172
+ },
173
+ signal: controller.signal
174
+ });
175
+ if (!response.ok) throw new Error(`Version source ${url} returned ${response.status}`);
176
+ return response.json();
177
+ } finally {
178
+ clearTimeout(timeoutId);
179
+ }
180
+ }
181
+ async function resolveProductVersion(state, fallback, sourceUrl, parse, fetchImpl) {
182
+ const now = Date.now();
183
+ if (state.cache && now - state.cache.fetchedAt < VERSION_CACHE_TTL_MS) return pickNewestVersion(state.cache.version, fallback) ?? fallback;
184
+ if (state.inFlight) return state.inFlight;
185
+ state.inFlight = (async () => {
186
+ let resolved = null;
187
+ try {
188
+ resolved = parse(await fetchJsonWithTimeout(fetchImpl, sourceUrl));
189
+ } catch {
190
+ resolved = null;
191
+ }
192
+ const version = pickNewestVersion(resolved, state.cache?.version, fallback) ?? fallback;
193
+ if (resolved) state.cache = {
194
+ version,
195
+ fetchedAt: Date.now()
196
+ };
197
+ return version;
198
+ })();
199
+ try {
200
+ return await state.inFlight;
201
+ } finally {
202
+ state.inFlight = null;
203
+ }
204
+ }
205
+ /** Resolve the newest known Antigravity version from the IDE feed. */
206
+ function resolveAntigravityIdeVersion(fetchImpl = fetch) {
207
+ return resolveProductVersion(ideState, AGY_VERSION_FALLBACK, IDE_RELEASE_FEED_URL, parseIdeReleaseFeed, fetchImpl);
208
+ }
209
+ /** Resolve the newest known Antigravity version from the CLI releases. */
210
+ function resolveAntigravityCliVersion(fetchImpl = fetch) {
211
+ return resolveProductVersion(cliState, AGY_VERSION_FALLBACK, CLI_RELEASE_URL, parseCliRelease, fetchImpl);
212
+ }
213
+ /** Best available version: newest of both sources, cached 6h. */
214
+ async function resolveAntigravityVersion(fetchImpl = fetch) {
215
+ const [ide, cli] = await Promise.all([resolveAntigravityIdeVersion(fetchImpl), resolveAntigravityCliVersion(fetchImpl)]);
216
+ return pickNewestVersion(ide, cli) ?? "1.18.3";
217
+ }
218
+ /**
219
+ * Bounded resolve for fingerprint generation: never block the failure path on
220
+ * cold version feeds. Resolves undefined on timeout/error so callers fall
221
+ * back to the pinned version pool; the abandoned fetch keeps running and
222
+ * populates the 6h cache for later calls.
223
+ */
224
+ async function resolveAntigravityVersionBounded(timeoutMs = 750) {
225
+ let timer;
226
+ try {
227
+ return await Promise.race([resolveAntigravityVersion().catch(() => void 0), new Promise((resolve) => {
228
+ timer = setTimeout(() => resolve(void 0), timeoutMs);
229
+ })]);
230
+ } finally {
231
+ clearTimeout(timer);
232
+ }
233
+ }
234
+ /** Synchronously peek at a fresh cached version (no network). */
235
+ function peekCachedAntigravityVersion() {
236
+ const now = Date.now();
237
+ for (const state of [ideState, cliState]) if (state.cache && now - state.cache.fetchedAt < VERSION_CACHE_TTL_MS) return state.cache.version;
238
+ }
239
+ const ideState = { inFlight: null };
240
+ const cliState = { inFlight: null };
241
+ //#endregion
242
+ //#region src/oauth/auth.ts
243
+ const ACCESS_TOKEN_EXPIRY_BUFFER_MS = 6e4;
244
+ /** Split the packed refresh string into its constituent refresh token and project ids. */
245
+ function parseRefreshParts(refresh) {
246
+ const [refreshToken = "", projectId = "", managedProjectId = ""] = (refresh ?? "").split("|");
247
+ return {
248
+ refreshToken: refreshToken || void 0,
249
+ projectId: projectId || void 0,
250
+ managedProjectId: managedProjectId || void 0
251
+ };
252
+ }
253
+ /** Serialize refresh token parts into the stored string format. */
254
+ function formatRefreshParts(parts) {
255
+ const projectSegment = parts.projectId ?? "";
256
+ const base = `${parts.refreshToken ?? ""}|${projectSegment}`;
257
+ return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
258
+ }
259
+ /** Whether the access token is expired or missing, with a buffer for clock skew. */
260
+ function accessTokenExpired(auth) {
261
+ if (!auth.access || typeof auth.expires !== "number") return true;
262
+ return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;
263
+ }
264
+ /** Absolute expiry timestamp from a duration; malformed durations expire immediately. */
265
+ function calculateTokenExpiry(requestTimeMs, expiresInSeconds) {
266
+ const seconds = typeof expiresInSeconds === "number" ? expiresInSeconds : 3600;
267
+ if (Number.isNaN(seconds) || seconds <= 0) return requestTimeMs;
268
+ return requestTimeMs + seconds * 1e3;
269
+ }
270
+ //#endregion
271
+ //#region src/oauth/refresh.ts
272
+ var AgyTokenRefreshError = class extends Error {
273
+ code;
274
+ description;
275
+ status;
276
+ statusText;
277
+ constructor(options) {
278
+ super(options.message);
279
+ this.name = "AgyTokenRefreshError";
280
+ this.code = options.code;
281
+ this.description = options.description;
282
+ this.status = options.status;
283
+ this.statusText = options.statusText;
284
+ }
285
+ };
286
+ /** Parse Google token-endpoint error payloads, tolerating varied shapes. */
287
+ function parseOAuthErrorPayload(text) {
288
+ if (!text) return {};
289
+ try {
290
+ const payload = JSON.parse(text);
291
+ if (!payload || typeof payload !== "object") return { description: text };
292
+ let code;
293
+ if (typeof payload.error === "string") code = payload.error;
294
+ else if (payload.error && typeof payload.error === "object") {
295
+ code = payload.error.status ?? payload.error.code;
296
+ if (!payload.error_description && payload.error.message) return {
297
+ code,
298
+ description: payload.error.message
299
+ };
300
+ }
301
+ const description = payload.error_description;
302
+ if (description) return {
303
+ code,
304
+ description
305
+ };
306
+ if (payload.error && typeof payload.error === "object" && payload.error.message) return {
307
+ code,
308
+ description: payload.error.message
309
+ };
310
+ return { code };
311
+ } catch {
312
+ return { description: text };
313
+ }
314
+ }
315
+ /**
316
+ * Refresh the access token for an account. `revoked` means Google rejected the
317
+ * refresh token (`invalid_grant`) — the account must be re-authenticated.
318
+ */
319
+ async function refreshAccessToken(auth) {
320
+ const parts = parseRefreshParts(auth.refresh);
321
+ if (!parts.refreshToken) return {
322
+ type: "failed",
323
+ error: new AgyTokenRefreshError({
324
+ message: "Missing refresh token",
325
+ status: 400,
326
+ statusText: "Bad Request"
327
+ })
328
+ };
329
+ try {
330
+ const startTime = Date.now();
331
+ const response = await fetch(OAUTH_TOKEN_URL, {
332
+ method: "POST",
333
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
334
+ body: new URLSearchParams({
335
+ grant_type: "refresh_token",
336
+ refresh_token: parts.refreshToken,
337
+ client_id: AGY_CLIENT_ID,
338
+ client_secret: AGY_CLIENT_SECRET
339
+ })
340
+ });
341
+ if (!response.ok) {
342
+ const errorText = await response.text().catch(() => void 0);
343
+ const { code, description } = parseOAuthErrorPayload(errorText);
344
+ const details = [code, description ?? errorText].filter(Boolean).join(": ");
345
+ const baseMessage = `Agy token refresh failed (${response.status} ${response.statusText})`;
346
+ const message = details ? `${baseMessage} - ${details}` : baseMessage;
347
+ if (code === "invalid_grant") return { type: "revoked" };
348
+ return {
349
+ type: "failed",
350
+ error: new AgyTokenRefreshError({
351
+ message,
352
+ code,
353
+ description: description ?? errorText,
354
+ status: response.status,
355
+ statusText: response.statusText
356
+ })
357
+ };
358
+ }
359
+ const payload = await response.json();
360
+ const refreshedParts = {
361
+ refreshToken: payload.refresh_token ?? parts.refreshToken,
362
+ projectId: parts.projectId,
363
+ managedProjectId: parts.managedProjectId
364
+ };
365
+ return {
366
+ type: "success",
367
+ auth: {
368
+ access: payload.access_token,
369
+ expires: calculateTokenExpiry(startTime, payload.expires_in),
370
+ refresh: formatRefreshParts(refreshedParts)
371
+ }
372
+ };
373
+ } catch (error) {
374
+ if (error instanceof AgyTokenRefreshError) throw error;
375
+ return {
376
+ type: "failed",
377
+ error: new AgyTokenRefreshError({
378
+ message: error instanceof Error ? error.message : "Unknown refresh error",
379
+ status: 0,
380
+ statusText: ""
381
+ })
382
+ };
383
+ }
384
+ }
385
+ //#endregion
386
+ //#region src/store/keyring.ts
387
+ /**
388
+ * Master-key management and AES-256-GCM secret codec for the account store.
389
+ *
390
+ * The master key lives in the DSH credentials document (`~/.dsh/.credentials.yaml`,
391
+ * 0600) under `AGY_MASTER_KEY` so both the in-harness plugin (via
392
+ * `ctx.credentials`) and the standalone `dsh-agy` CLI (direct file read) can
393
+ * encrypt and decrypt the same account store.
394
+ */
395
+ const MASTER_KEY_REF = "AGY_MASTER_KEY";
396
+ const ENC_PREFIX$1 = "enc:v1:";
397
+ /** AES-256-GCM codec; ciphertext format `enc:v1:<iv-b64>:<tag-b64>:<data-b64>`. */
398
+ function createAesGcmCodec(key) {
399
+ if (key.length !== 32) throw new Error(`createAesGcmCodec: master key must be 32 bytes, got ${key.length}`);
400
+ return {
401
+ encrypt(plaintext) {
402
+ const iv = randomBytes(12);
403
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
404
+ const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
405
+ const tag = cipher.getAuthTag();
406
+ return `${ENC_PREFIX$1}${iv.toString("base64url")}:${tag.toString("base64url")}:${data.toString("base64url")}`;
407
+ },
408
+ decrypt(payload) {
409
+ if (!payload.startsWith(ENC_PREFIX$1)) throw new Error("decrypt: payload is not in encrypted format");
410
+ const [, , ivB64 = "", tagB64 = "", dataB64 = ""] = payload.split(":");
411
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivB64, "base64url"));
412
+ decipher.setAuthTag(Buffer.from(tagB64, "base64url"));
413
+ return Buffer.concat([decipher.update(Buffer.from(dataB64, "base64url")), decipher.final()]).toString("utf8");
414
+ }
415
+ };
416
+ }
417
+ /** Derive a 32-byte key from an arbitrary master-key string (SHA-256). */
418
+ function deriveKey(masterKey) {
419
+ return createHash("sha256").update(masterKey, "utf8").digest();
420
+ }
421
+ /** Default DSH home (`~/.dsh`), honoring `$DSH_HOME`. */
422
+ function resolveDshHome() {
423
+ return process.env.DSH_HOME ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh");
424
+ }
425
+ function homedir() {
426
+ return process.env.HOME ?? process.env.USERPROFILE ?? ".";
427
+ }
428
+ /**
429
+ * Minimal reader for the DSH credentials document: a flat YAML mapping of
430
+ * `KEY: "value"` / `KEY: value` lines. Values are unquoted with JSON-style
431
+ * escaping for double-quoted scalars; anything more complex fails loudly.
432
+ */
433
+ function readCredentialsDocument(file) {
434
+ if (!existsSync(file)) return /* @__PURE__ */ new Map();
435
+ const entries = /* @__PURE__ */ new Map();
436
+ const text = readFileSync(file, "utf8");
437
+ for (const line of text.split(/\r?\n/)) {
438
+ const trimmed = line.trim();
439
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("---")) continue;
440
+ const match = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/.exec(trimmed);
441
+ if (!match) continue;
442
+ const [, key = "", rawValue = ""] = match;
443
+ let value;
444
+ if (rawValue.startsWith("\"")) try {
445
+ value = JSON.parse(rawValue);
446
+ } catch {
447
+ throw new Error(`readCredentialsDocument: invalid quoted value for "${key}" in ${file}`);
448
+ }
449
+ else if (rawValue.startsWith("'")) {
450
+ if (!rawValue.endsWith("'") || rawValue.length < 2) throw new Error(`readCredentialsDocument: unterminated single-quoted value for "${key}" in ${file}`);
451
+ value = rawValue.slice(1, -1).replace(/''/g, "'");
452
+ } else value = rawValue;
453
+ if (value.length === 0) continue;
454
+ entries.set(key, value);
455
+ }
456
+ return entries;
457
+ }
458
+ /**
459
+ * POSIX owner-only enforcement. Windows mode bits never report 0600 (and
460
+ * chmod is a no-op there), so the check is skipped on win32; the encrypted
461
+ * account file and credentials document remain the only defense-in-depth
462
+ * layer on that platform.
463
+ */
464
+ function assertOwnerOnly(file) {
465
+ if (process.platform === "win32") return;
466
+ const mode = statSync(file).mode & 511;
467
+ if (mode !== 384) throw new Error(`dsh-agy: ${file} is readable beyond its owner (mode ${mode.toString(8)}); run "chmod 600" before starting again`);
468
+ }
469
+ /**
470
+ * Load the master key from the DSH credentials document. Returns undefined when
471
+ * the document or the reference is absent.
472
+ */
473
+ function loadMasterKey(dshHome) {
474
+ const file = join(dshHome, ".credentials.yaml");
475
+ if (!existsSync(file)) return void 0;
476
+ assertOwnerOnly(file);
477
+ return readCredentialsDocument(file).get(MASTER_KEY_REF);
478
+ }
479
+ /**
480
+ * Generate and persist a fresh master key (0600) in the DSH credentials document.
481
+ *
482
+ * Append-only + atomic rename: the DSH credentials service owns this file and
483
+ * preserves comments and the formatting of untouched entries (and may hold
484
+ * YAML constructs this minimal reader cannot parse). Rewriting the whole file
485
+ * from our parsed view would silently drop those; appending keeps them.
486
+ */
487
+ function persistMasterKey(dshHome, masterKey) {
488
+ const file = join(dshHome, ".credentials.yaml");
489
+ mkdirSync(dirname(file), { recursive: true });
490
+ const existingText = existsSync(file) ? readFileSync(file, "utf8") : "";
491
+ if (existingText.length > 0 && readCredentialsDocument(file).has("AGY_MASTER_KEY")) throw new Error(`persistMasterKey: ${MASTER_KEY_REF} already exists in ${file}`);
492
+ const entry = `${MASTER_KEY_REF}: ${JSON.stringify(masterKey)}`;
493
+ const next = existingText.length > 0 && !existingText.endsWith("\n") ? `${existingText}\n${entry}\n` : `${existingText}${entry}\n`;
494
+ const tmp = `${file}.tmp-masterkey`;
495
+ writeFileSync(tmp, next, { mode: 384 });
496
+ renameSync(tmp, file);
497
+ }
498
+ /**
499
+ * Resolve (load or create) the master key for a dsh home, then build the codec.
500
+ * Creating a key writes the credentials document; read-only setups should call
501
+ * {@link loadMasterKey} first and surface a friendly error instead.
502
+ */
503
+ function resolveMasterKeyCodec(dshHome) {
504
+ let masterKey = loadMasterKey(dshHome);
505
+ let created = false;
506
+ if (!masterKey) {
507
+ masterKey = randomBytes(32).toString("hex");
508
+ persistMasterKey(dshHome, masterKey);
509
+ created = true;
510
+ }
511
+ return {
512
+ codec: createAesGcmCodec(deriveKey(masterKey)),
513
+ created
514
+ };
515
+ }
516
+ const ENC_PREFIX = "enc:v1:";
517
+ const properFileLock = { async withLock(file, fn) {
518
+ const release = await lockfile.lock(file, {
519
+ stale: 3e4,
520
+ update: 1e4,
521
+ retries: {
522
+ retries: 10,
523
+ factor: 1.5,
524
+ minTimeout: 50,
525
+ maxTimeout: 2e3
526
+ }
527
+ });
528
+ try {
529
+ return await fn();
530
+ } finally {
531
+ await release();
532
+ }
533
+ } };
534
+ function isEncrypted(value) {
535
+ return value.startsWith(ENC_PREFIX);
536
+ }
537
+ /** Decrypt one account's refresh field when encrypted; plaintext passes through (legacy). */
538
+ function decryptAccount(account, codec) {
539
+ if (isEncrypted(account.refresh)) return {
540
+ ...account,
541
+ refresh: codec.decrypt(account.refresh)
542
+ };
543
+ return account;
544
+ }
545
+ /** Encrypt one account's refresh field (plaintext stays plaintext if no codec change). */
546
+ function encryptAccount(account, codec) {
547
+ if (!isEncrypted(account.refresh)) return {
548
+ ...account,
549
+ refresh: codec.encrypt(account.refresh)
550
+ };
551
+ return account;
552
+ }
553
+ function decryptStorage(storage, codec) {
554
+ return {
555
+ ...storage,
556
+ accounts: storage.accounts.map((a) => decryptAccount(a, codec))
557
+ };
558
+ }
559
+ function encryptStorage(storage, codec) {
560
+ return {
561
+ ...storage,
562
+ accounts: storage.accounts.map((a) => encryptAccount(a, codec))
563
+ };
564
+ }
565
+ function migrateV1ToV2(v1) {
566
+ return {
567
+ version: 2,
568
+ accounts: v1.accounts.map((acc) => ({
569
+ email: acc.email,
570
+ refreshToken: acc.refreshToken,
571
+ projectId: acc.projectId,
572
+ managedProjectId: acc.managedProjectId,
573
+ addedAt: acc.addedAt,
574
+ lastUsed: acc.lastUsed,
575
+ lastSwitchReason: acc.lastSwitchReason,
576
+ rateLimitResetTimes: acc.isRateLimited && acc.rateLimitResetTime ? { default: acc.rateLimitResetTime } : void 0
577
+ })),
578
+ activeIndex: v1.activeIndex
579
+ };
580
+ }
581
+ function migrateV2ToV3(v2) {
582
+ return {
583
+ version: 3,
584
+ accounts: v2.accounts.map((acc) => ({
585
+ email: acc.email,
586
+ refresh: `${acc.refreshToken}|${acc.projectId ?? ""}|${acc.managedProjectId ?? ""}`,
587
+ projectId: acc.projectId,
588
+ managedProjectId: acc.managedProjectId,
589
+ addedAt: acc.addedAt,
590
+ lastUsed: acc.lastUsed,
591
+ enabled: true,
592
+ lastSwitchReason: acc.lastSwitchReason,
593
+ rateLimitResetTimes: acc.rateLimitResetTimes
594
+ })),
595
+ activeIndex: v2.activeIndex
596
+ };
597
+ }
598
+ function migrateV3ToV4(v3) {
599
+ return {
600
+ version: 4,
601
+ accounts: v3.accounts,
602
+ activeIndex: v3.activeIndex
603
+ };
604
+ }
605
+ function migrateStorage(raw) {
606
+ switch (raw.version) {
607
+ case 1: return migrateStorage(migrateV1ToV2(raw));
608
+ case 2: return migrateStorage(migrateV2ToV3(raw));
609
+ case 3: return migrateStorage(migrateV3ToV4(raw));
610
+ case 4: return raw;
611
+ default: throw new Error(`migrateStorage: unsupported storage version ${raw.version}`);
612
+ }
613
+ }
614
+ var JsonAccountStore = class {
615
+ file;
616
+ codec;
617
+ lock;
618
+ constructor(options) {
619
+ this.file = options.file;
620
+ this.codec = options.codec;
621
+ this.lock = options.lock ?? properFileLock;
622
+ }
623
+ /**
624
+ * Ensure the store document exists before locking: proper-lockfile resolves
625
+ * the target's realpath and fails with ENOENT when it is absent.
626
+ */
627
+ ensureFile() {
628
+ if (existsSync(this.file)) return;
629
+ mkdirSync(dirname(this.file), { recursive: true });
630
+ const tmp = `${this.file}.tmp-init`;
631
+ writeFileSync(tmp, JSON.stringify({
632
+ version: 4,
633
+ accounts: [],
634
+ activeIndex: 0
635
+ }) + "\n", { mode: 384 });
636
+ renameSync(tmp, this.file);
637
+ }
638
+ async load() {
639
+ let text;
640
+ try {
641
+ text = readFileSync(this.file, "utf8");
642
+ } catch (error) {
643
+ if (error.code === "ENOENT") return {
644
+ version: 4,
645
+ accounts: [],
646
+ activeIndex: 0
647
+ };
648
+ throw error;
649
+ }
650
+ assertOwnerOnly(this.file);
651
+ return decryptStorage(migrateStorage(JSON.parse(text)), this.codec);
652
+ }
653
+ async save(storage) {
654
+ const encrypted = encryptStorage(storage, this.codec);
655
+ mkdirSync(dirname(this.file), { recursive: true });
656
+ const tmp = `${this.file}.tmp`;
657
+ writeFileSync(tmp, JSON.stringify(encrypted, null, 2) + "\n", { mode: 384 });
658
+ renameSync(tmp, this.file);
659
+ }
660
+ async mutate(fn) {
661
+ this.ensureFile();
662
+ return this.lock.withLock(this.file, async () => {
663
+ const storage = await this.load();
664
+ const result = await fn(storage);
665
+ await this.save(storage);
666
+ return result;
667
+ });
668
+ }
669
+ };
670
+ /** Pick the active account, falling back to the first enabled one. */
671
+ function resolveActiveAccount(storage) {
672
+ const accounts = storage.accounts;
673
+ if (accounts.length === 0) return void 0;
674
+ const active = accounts[storage.activeIndex];
675
+ if (active && active.enabled !== false) return {
676
+ account: active,
677
+ index: storage.activeIndex
678
+ };
679
+ const fallbackIndex = accounts.findIndex((a) => a.enabled !== false);
680
+ if (fallbackIndex === -1) return void 0;
681
+ return {
682
+ account: accounts[fallbackIndex],
683
+ index: fallbackIndex
684
+ };
685
+ }
686
+ //#endregion
687
+ //#region src/runtime/rotation.ts
688
+ const BACKOFF_TIERS_MS = [
689
+ 5e3,
690
+ 1e4,
691
+ 2e4,
692
+ 3e4,
693
+ 6e4
694
+ ];
695
+ function backoffFor(consecutiveFailures, maxJitterMs = 1e3) {
696
+ const index = Math.min(Math.max(consecutiveFailures, 0), BACKOFF_TIERS_MS.length - 1);
697
+ return (BACKOFF_TIERS_MS[index] ?? BACKOFF_TIERS_MS[BACKOFF_TIERS_MS.length - 1]) + Math.floor(Math.random() * maxJitterMs);
698
+ }
699
+ /** Whether the account is currently in a cooldown window. */
700
+ function isCoolingDown(account, now = Date.now()) {
701
+ return (account.coolingDownUntil ?? 0) > now;
702
+ }
703
+ /** Clear expired rate limits and cooldowns in place. */
704
+ function clearExpiredState(account, now = Date.now()) {
705
+ if (account.rateLimitResetTimes) {
706
+ const fresh = Object.fromEntries(Object.entries(account.rateLimitResetTimes).filter(([, reset]) => reset > now));
707
+ account.rateLimitResetTimes = Object.keys(fresh).length > 0 ? fresh : void 0;
708
+ }
709
+ if (account.coolingDownUntil && account.coolingDownUntil <= now) {
710
+ account.coolingDownUntil = void 0;
711
+ account.cooldownReason = void 0;
712
+ }
713
+ }
714
+ /** 24h cooldown for a fully exhausted daily quota (single-account: stop hitting the wall). */
715
+ const FULL_QUOTA_COOLDOWN_MS = 864e5;
716
+ /**
717
+ * Decide what to do after one failed attempt.
718
+ * @param kind - classified failure kind.
719
+ * @param category - 429 sub-category when kind is rate-limit.
720
+ * @param account - the account that failed (mutated with cooldown/rate-limit state).
721
+ * @param consecutiveFailures - consecutive failures on this account.
722
+ * @param retryAfterMs - server-provided retry delay when present.
723
+ */
724
+ function decideRotation(kind, account, consecutiveFailures, retryAfterMs, category = "unknown") {
725
+ const now = Date.now();
726
+ const backoffMs = backoffFor(consecutiveFailures);
727
+ switch (kind) {
728
+ case "rate-limit":
729
+ if (category === "soft_rate_limit") return {
730
+ action: "retry",
731
+ backoffMs: Math.min(retryAfterMs ?? backoffMs, 3e3)
732
+ };
733
+ if (category === "quota_exhausted") {
734
+ account.coolingDownUntil = now + FULL_QUOTA_COOLDOWN_MS;
735
+ account.cooldownReason = void 0;
736
+ return {
737
+ action: "cool",
738
+ backoffMs: FULL_QUOTA_COOLDOWN_MS
739
+ };
740
+ }
741
+ account.coolingDownUntil = now + (retryAfterMs ?? 3e5);
742
+ account.cooldownReason = void 0;
743
+ return {
744
+ action: "rotate",
745
+ backoffMs
746
+ };
747
+ case "auth-failure":
748
+ account.verificationRequired = true;
749
+ account.verificationRequiredAt = now;
750
+ account.verificationRequiredReason = "auth-failure";
751
+ account.enabled = false;
752
+ return { action: "revoke" };
753
+ case "network-error":
754
+ account.coolingDownUntil = now + backoffMs;
755
+ account.cooldownReason = "network-error";
756
+ return {
757
+ action: "rotate",
758
+ backoffMs
759
+ };
760
+ case "project-error":
761
+ account.coolingDownUntil = now + backoffMs;
762
+ account.cooldownReason = "project-error";
763
+ return {
764
+ action: "cool",
765
+ backoffMs
766
+ };
767
+ case "transient": return {
768
+ action: "retry",
769
+ backoffMs
770
+ };
771
+ }
772
+ }
773
+ /**
774
+ * Pick the next account index for rotation (round-robin across enabled,
775
+ * non-cooling accounts; falls back to the active one when all are cooling).
776
+ */
777
+ function pickNextAccountIndex(accounts, currentIndex, now = Date.now()) {
778
+ if (accounts.length <= 1) return currentIndex;
779
+ const enabled = accounts.map((a, i) => ({
780
+ account: a,
781
+ index: i
782
+ })).filter(({ account, index }) => index !== currentIndex && account.enabled !== false && !isCoolingDown(account, now));
783
+ if (enabled.length === 0) return currentIndex;
784
+ return (enabled.find((e) => e.index > currentIndex) ?? enabled[0]).index;
785
+ }
786
+ //#endregion
787
+ //#region src/runtime/fingerprint.ts
788
+ /**
789
+ * Device fingerprint generation for rate-limit mitigation (two layers):
790
+ *
791
+ * 1. Per-request randomized headers — platform/arch/SDK-client pools.
792
+ * 2. Per-account persistent fingerprint — deviceId/sessionToken/UA snapshot
793
+ * with bounded history (≤5 versions, restorable), regenerated when an
794
+ * account's capacity looks exhausted.
795
+ *
796
+ * All tunable pools live in fingerprint-data.json so they can be updated
797
+ * without a code release (the reference implementation stopped at an old
798
+ * version string once archived — that staleness is the detectable signal).
799
+ */
800
+ const DEFAULT_FINGERPRINT_DATA = {
801
+ comment: "Hot-updatable fingerprint data. Version pools and client strings must track the real Antigravity product; update this file (or ship an override) when a release ships. Kept out of code so stale versions never get compiled in.",
802
+ versionPool: [
803
+ "1.18.3",
804
+ "1.17.0",
805
+ "1.16.0"
806
+ ],
807
+ platforms: [
808
+ "windows/amd64",
809
+ "darwin/arm64",
810
+ "darwin/amd64"
811
+ ],
812
+ architectures: ["x64", "arm64"],
813
+ osVersions: {
814
+ "darwin": [
815
+ "10.15.7",
816
+ "11.6.8",
817
+ "12.6.3",
818
+ "13.5.2",
819
+ "14.2.1",
820
+ "14.5"
821
+ ],
822
+ "win32": [
823
+ "10.0.19041",
824
+ "10.0.19042",
825
+ "10.0.19043",
826
+ "10.0.22000",
827
+ "10.0.22621",
828
+ "10.0.22631"
829
+ ],
830
+ "linux": [
831
+ "5.15.0",
832
+ "5.19.0",
833
+ "6.1.0",
834
+ "6.2.0",
835
+ "6.5.0",
836
+ "6.6.0"
837
+ ]
838
+ },
839
+ sdkClients: [
840
+ "google-cloud-sdk vscode_cloudshelleditor/0.1",
841
+ "google-cloud-sdk vscode/1.86.0",
842
+ "google-cloud-sdk vscode/1.87.0",
843
+ "google-cloud-sdk vscode/1.96.0"
844
+ ],
845
+ ideTypes: ["ANTIGRAVITY"],
846
+ pluginTypes: ["GEMINI"]
847
+ };
848
+ const USER_OVERRIDE_FILE = "agy-fingerprint-data.json";
849
+ /**
850
+ * Effective fingerprint data: a user override at `$DSH_HOME/agy-fingerprint-data.json`
851
+ * wins when present and parseable (hot-updatable without a code release — the
852
+ * bundled copy is compiled in), otherwise the bundled defaults.
853
+ */
854
+ let cachedData = null;
855
+ function getFingerprintData() {
856
+ if (cachedData) return cachedData;
857
+ try {
858
+ const dshHome = process.env.DSH_HOME ? process.env.DSH_HOME : join(process.env.HOME ?? ".", ".dsh");
859
+ const overrideFile = join(dshHome, USER_OVERRIDE_FILE);
860
+ if (existsSync(overrideFile)) {
861
+ const parsed = JSON.parse(readFileSync(overrideFile, "utf8"));
862
+ if (parsed && Array.isArray(parsed.versionPool) && parsed.versionPool.length > 0) {
863
+ cachedData = parsed;
864
+ return cachedData;
865
+ }
866
+ }
867
+ } catch {}
868
+ cachedData = DEFAULT_FINGERPRINT_DATA;
869
+ return cachedData;
870
+ }
871
+ function randomFrom(arr) {
872
+ return arr[Math.floor(Math.random() * arr.length)];
873
+ }
874
+ function platformToDisplayName(platform) {
875
+ return platform.startsWith("windows") ? "WINDOWS" : "MACOS";
876
+ }
877
+ /** Generate a randomized device fingerprint representing one apparent device. */
878
+ function generateFingerprint(data = getFingerprintData(), version = randomFrom(data.versionPool)) {
879
+ const platform = randomFrom(data.platforms);
880
+ return {
881
+ deviceId: randomUUID(),
882
+ sessionToken: randomBytes(16).toString("hex"),
883
+ userAgent: `antigravity/${version} ${platform}`,
884
+ apiClient: randomFrom(data.sdkClients),
885
+ clientMetadata: {
886
+ ideType: randomFrom(data.ideTypes),
887
+ platform: platformToDisplayName(platform),
888
+ pluginType: randomFrom(data.pluginTypes)
889
+ },
890
+ createdAt: Date.now()
891
+ };
892
+ }
893
+ /** Per-request randomized headers (platform + SDK client pools). */
894
+ function getRandomizedHeaders(data = getFingerprintData(), version = randomFrom(data.versionPool)) {
895
+ const platform = randomFrom(data.platforms);
896
+ return {
897
+ "User-Agent": `antigravity/${version} ${platform}`,
898
+ "X-Goog-Api-Client": randomFrom(data.sdkClients),
899
+ "Client-Metadata": JSON.stringify({
900
+ ideType: randomFrom(data.ideTypes),
901
+ platform: platformToDisplayName(platform),
902
+ pluginType: randomFrom(data.pluginTypes)
903
+ })
904
+ };
905
+ }
906
+ /** Rewrite the version inside a fingerprint UA; reports whether it changed. */
907
+ function updateFingerprintVersion(fingerprint, version) {
908
+ const pattern = /^(antigravity\/)([\d.]+)/;
909
+ const match = fingerprint.userAgent.match(pattern);
910
+ if (!match || match[2] === version) return false;
911
+ fingerprint.userAgent = fingerprint.userAgent.replace(pattern, `$1${version}`);
912
+ return true;
913
+ }
914
+ /** Append a fingerprint to the account history (bounded), then use it as current. */
915
+ function recordFingerprintVersion(history, fingerprint, reason) {
916
+ return [...history ?? [], {
917
+ fingerprint,
918
+ timestamp: Date.now(),
919
+ reason
920
+ }].slice(-5);
921
+ }
922
+ //#endregion
923
+ //#region src/session.ts
924
+ /**
925
+ * Resolve the impersonation headers for one request from the account's
926
+ * persistent fingerprint (stable identity), falling back to full per-request
927
+ * randomization when the account has no fingerprint yet.
928
+ */
929
+ function impersonationHeadersFor(account) {
930
+ const fingerprint = account.fingerprint;
931
+ if (fingerprint) return {
932
+ "User-Agent": fingerprint.userAgent,
933
+ "X-Goog-Api-Client": fingerprint.apiClient,
934
+ "Client-Metadata": JSON.stringify(fingerprint.clientMetadata)
935
+ };
936
+ const randomized = getRandomizedHeaders(DEFAULT_FINGERPRINT_DATA);
937
+ return {
938
+ "User-Agent": randomized["User-Agent"],
939
+ "X-Goog-Api-Client": randomized["X-Goog-Api-Client"],
940
+ "Client-Metadata": randomized["Client-Metadata"]
941
+ };
942
+ }
943
+ var AgySessionManager = class {
944
+ store;
945
+ onRotate;
946
+ tokenCache = /* @__PURE__ */ new Map();
947
+ /** In-flight refresh promises keyed by account: concurrent requests share one refresh. */
948
+ refreshInFlight = /* @__PURE__ */ new Map();
949
+ failureCounts = /* @__PURE__ */ new Map();
950
+ /** Accounts whose request-time project discovery already failed (no retry per request). */
951
+ projectRetryFailed = /* @__PURE__ */ new Set();
952
+ constructor(options) {
953
+ this.store = options.store;
954
+ this.onRotate = options.onRotate;
955
+ }
956
+ accountKey(account) {
957
+ return account.email ?? `idx-${account.refresh}`;
958
+ }
959
+ /** Resolve a usable access token for the account, refreshing when expired. */
960
+ async accessTokenFor(account) {
961
+ const key = this.accountKey(account);
962
+ const cached = this.tokenCache.get(key);
963
+ if (cached && !accessTokenExpired({
964
+ access: cached.access,
965
+ expires: cached.expires,
966
+ refresh: account.refresh
967
+ })) return {
968
+ access: cached.access,
969
+ expires: cached.expires,
970
+ refresh: account.refresh
971
+ };
972
+ const inFlight = this.refreshInFlight.get(key);
973
+ if (inFlight) return inFlight;
974
+ const refreshing = (async () => {
975
+ const result = await refreshAccessToken({
976
+ access: cached?.access ?? "",
977
+ expires: cached?.expires ?? 0,
978
+ refresh: account.refresh
979
+ });
980
+ if (result.type === "success") {
981
+ this.tokenCache.set(key, {
982
+ access: result.auth.access,
983
+ expires: result.auth.expires
984
+ });
985
+ return result.auth;
986
+ }
987
+ if (result.type === "revoked") {
988
+ this.tokenCache.delete(key);
989
+ return;
990
+ }
991
+ })();
992
+ this.refreshInFlight.set(key, refreshing);
993
+ try {
994
+ return await refreshing;
995
+ } finally {
996
+ this.refreshInFlight.delete(key);
997
+ }
998
+ }
999
+ async pickAccount(storage) {
1000
+ const now = Date.now();
1001
+ for (const account of storage.accounts) clearExpiredState(account, now);
1002
+ let resolved = resolveActiveAccount(storage);
1003
+ if (!resolved) return void 0;
1004
+ if (isCoolingDown(resolved.account, now)) {
1005
+ const nextIndex = pickNextAccountIndex(storage.accounts, resolved.index, now);
1006
+ if (nextIndex !== resolved.index) resolved = {
1007
+ account: storage.accounts[nextIndex],
1008
+ index: nextIndex
1009
+ };
1010
+ }
1011
+ return resolved;
1012
+ }
1013
+ /**
1014
+ * Adapter hook: resolve the active session (refresh if needed), healing a
1015
+ * missing projectId at request time — the OAuth-time loadCodeAssist may have
1016
+ * transiently failed even when the Google account owns a Cloud Code project
1017
+ * (mirrors OmniRoute's ensureAntigravityProjectAssigned + persistence).
1018
+ */
1019
+ async getSession() {
1020
+ const storage = await this.store.load();
1021
+ const picked = await this.pickAccount(storage);
1022
+ if (!picked) return void 0;
1023
+ const auth = await this.accessTokenFor(picked.account);
1024
+ if (!auth) return void 0;
1025
+ const key = this.accountKey(picked.account);
1026
+ if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
1027
+ const { loadCodeAssist } = await import("./exchange-MhFUiBAm.mjs").then((n) => n.n);
1028
+ const { projectId } = await loadCodeAssist(auth.access);
1029
+ if (projectId) {
1030
+ await this.store.mutate((s) => {
1031
+ const account = s.accounts[picked.index];
1032
+ if (account) {
1033
+ account.projectId = projectId;
1034
+ const parts = parseRefreshParts(account.refresh);
1035
+ account.refresh = formatRefreshParts({
1036
+ refreshToken: parts.refreshToken,
1037
+ projectId,
1038
+ managedProjectId: parts.managedProjectId
1039
+ });
1040
+ }
1041
+ });
1042
+ picked.account.projectId = projectId;
1043
+ } else this.projectRetryFailed.add(key);
1044
+ } catch {
1045
+ this.projectRetryFailed.add(key);
1046
+ }
1047
+ return {
1048
+ auth,
1049
+ account: picked.account,
1050
+ index: picked.index,
1051
+ impersonation: impersonationHeadersFor(picked.account)
1052
+ };
1053
+ }
1054
+ /** Adapter hook: apply rotation decisions and fingerprint regeneration. */
1055
+ async reportFailure(kind, session, info) {
1056
+ const storage = await this.store.load();
1057
+ const account = storage.accounts[session.index];
1058
+ if (!account) return;
1059
+ const key = this.accountKey(account);
1060
+ const consecutive = (this.failureCounts.get(key) ?? 0) + 1;
1061
+ this.failureCounts.set(key, consecutive);
1062
+ const decision = decideRotation(kind, account, consecutive, info?.retryAfterMs, info?.rateLimitCategory);
1063
+ if (decision.action === "revoke") {
1064
+ this.tokenCache.delete(key);
1065
+ this.failureCounts.delete(key);
1066
+ await this.store.save(storage);
1067
+ return;
1068
+ }
1069
+ if (kind === "rate-limit") {
1070
+ if (!account.fingerprint) {
1071
+ account.fingerprint = generateFingerprint(void 0, await resolveAntigravityVersionBounded());
1072
+ account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, account.fingerprint, "initial");
1073
+ } else {
1074
+ const cached = peekCachedAntigravityVersion();
1075
+ if (cached) updateFingerprintVersion(account.fingerprint, cached);
1076
+ if (consecutive >= 2) {
1077
+ const fresh = generateFingerprint(void 0, await resolveAntigravityVersionBounded());
1078
+ account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, "regenerated");
1079
+ account.fingerprint = fresh;
1080
+ }
1081
+ }
1082
+ }
1083
+ if (decision.action === "rotate") {
1084
+ const nextIndex = pickNextAccountIndex(storage.accounts, session.index);
1085
+ if (nextIndex !== session.index) {
1086
+ storage.activeIndex = nextIndex;
1087
+ this.onRotate?.(session.index, nextIndex, kind);
1088
+ }
1089
+ }
1090
+ await this.store.save(storage);
1091
+ }
1092
+ /** Adapter hook: reset the failure counter after a clean completion. */
1093
+ async markSuccess(session) {
1094
+ const account = session.account;
1095
+ const key = this.accountKey(account);
1096
+ this.failureCounts.delete(key);
1097
+ }
1098
+ /**
1099
+ * Test call: one short streaming request against the live backend.
1100
+ * Returns the collected text or a structured error message.
1101
+ */
1102
+ async testCall(model, prompt = "Reply with exactly: OK", maxTokens = 1024) {
1103
+ const session = await this.getSession();
1104
+ if (!session) return {
1105
+ ok: false,
1106
+ error: "No agy account configured — run `dsh-agy login` first."
1107
+ };
1108
+ try {
1109
+ const { toAgyRequestBody } = await import("./translate-h2bjH7ub.mjs").then((n) => n.n);
1110
+ const { fetchAgyFirstOk } = await Promise.resolve().then(() => constants_exports);
1111
+ const { parseAgySse } = await import("./parse-CiEzD9d_.mjs").then((n) => n.n);
1112
+ const body = toAgyRequestBody({
1113
+ provider: "agy",
1114
+ model,
1115
+ messages: [{
1116
+ id: "test-1",
1117
+ role: "user",
1118
+ content: [{
1119
+ type: "text",
1120
+ text: prompt
1121
+ }]
1122
+ }],
1123
+ maxTokens
1124
+ }, {
1125
+ projectId: session.account.projectId,
1126
+ sessionId: deriveAntigravitySessionId(session.account.email) ?? void 0
1127
+ });
1128
+ const response = await fetchAgyFirstOk("/v1internal:streamGenerateContent?alt=sse", {
1129
+ method: "POST",
1130
+ headers: {
1131
+ authorization: `Bearer ${session.auth.access}`,
1132
+ "content-type": "application/json",
1133
+ accept: "text/event-stream",
1134
+ ...session.impersonation
1135
+ },
1136
+ body: JSON.stringify(body)
1137
+ });
1138
+ if (!response.ok) {
1139
+ const text = await response.text().catch(() => "");
1140
+ return {
1141
+ ok: false,
1142
+ error: `HTTP ${response.status}: ${text.slice(0, 300)}`
1143
+ };
1144
+ }
1145
+ if (!response.body) return {
1146
+ ok: false,
1147
+ error: "no response body"
1148
+ };
1149
+ const text = [];
1150
+ for await (const chunk of parseAgySse(response.body)) if (chunk.type === "text-delta") text.push(chunk.text);
1151
+ return {
1152
+ ok: text.length > 0,
1153
+ text: text.join(""),
1154
+ error: text.length > 0 ? void 0 : "empty response"
1155
+ };
1156
+ } catch (error) {
1157
+ return {
1158
+ ok: false,
1159
+ error: error instanceof Error ? error.message : String(error)
1160
+ };
1161
+ }
1162
+ }
1163
+ /** Export one account as a paste-credential blob (for migration to another host). */
1164
+ async exportBlob(index) {
1165
+ const account = (await this.store.load()).accounts[index];
1166
+ if (!account) return { error: "account not found" };
1167
+ const auth = await this.accessTokenFor(account);
1168
+ if (!auth) return { error: "refresh failed (revoked?)" };
1169
+ const { encodeCredentialBlob } = await import("./blob-D1e7_uT1.mjs").then((n) => n.t);
1170
+ const parts = parseRefreshParts(account.refresh);
1171
+ return { blob: encodeCredentialBlob("agy", {
1172
+ access_token: auth.access,
1173
+ refresh_token: parts.refreshToken,
1174
+ expires_in: Math.max(0, Math.round((auth.expires - Date.now()) / 1e3))
1175
+ }) };
1176
+ }
1177
+ /** CLI/web helper: verify an account's credentials (refresh + userinfo). */
1178
+ async verifyAccount(index) {
1179
+ const account = (await this.store.load()).accounts[index];
1180
+ if (!account) return {
1181
+ ok: false,
1182
+ error: "account not found"
1183
+ };
1184
+ const auth = await this.accessTokenFor(account);
1185
+ if (!auth) return {
1186
+ ok: false,
1187
+ error: "refresh failed (revoked?)"
1188
+ };
1189
+ try {
1190
+ const response = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { Authorization: `Bearer ${auth.access}` } });
1191
+ if (!response.ok) return {
1192
+ ok: false,
1193
+ error: `userinfo ${response.status}`
1194
+ };
1195
+ const info = await response.json();
1196
+ await this.store.mutate((s) => {
1197
+ const target = s.accounts[index];
1198
+ if (target) {
1199
+ target.enabled = true;
1200
+ target.verificationRequired = false;
1201
+ target.verificationRequiredAt = void 0;
1202
+ target.verificationRequiredReason = void 0;
1203
+ target.verificationUrl = void 0;
1204
+ }
1205
+ });
1206
+ return {
1207
+ ok: true,
1208
+ email: info.email
1209
+ };
1210
+ } catch (error) {
1211
+ return {
1212
+ ok: false,
1213
+ error: error instanceof Error ? error.message : String(error)
1214
+ };
1215
+ }
1216
+ }
1217
+ };
1218
+ //#endregion
1219
+ export { OAUTH_TOKEN_URL as C, getAgyBootstrapUserAgent as D, getAgyBootstrapClientMetadata as E, OAUTH_AUTHORIZE_URL as S, fetchAgyFirstOk as T, AGY_CLIENT_ID as _, MASTER_KEY_REF as a, AGY_ENDPOINT_FALLBACKS as b, loadMasterKey as c, resolveMasterKeyCodec as d, calculateTokenExpiry as f, generateAntigravityRequestId as g, deriveAntigravitySessionId as h, JsonAccountStore as i, persistMasterKey as l, resolveAntigravityVersionBounded as m, generateFingerprint as n, createAesGcmCodec as o, resolveAntigravityVersion as p, recordFingerprintVersion as r, deriveKey as s, AgySessionManager as t, resolveDshHome as u, AGY_CLIENT_SECRET as v, OAUTH_USERINFO_URL as w, AGY_SCOPES as x, AGY_DEFAULT_REDIRECT_URI as y };
1220
+
1221
+ //# sourceMappingURL=session-tawi2_uh.mjs.map