dsh-agy 0.2.3 → 0.2.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.
package/README.md CHANGED
@@ -50,6 +50,8 @@ dsh web
50
50
  # Click "Login with Google", complete OAuth authorization, and start using the agy provider
51
51
  ```
52
52
 
53
+ Open **Settings → Plugins → Antigravity → Open Antigravity dashboard**; the button opens `/agy` in a new tab.
54
+
53
55
  ### Path B: Headless / Terminal Only (Standalone CLI)
54
56
 
55
57
  For Linux VPS, SSH remote servers, or headless CI environments:
@@ -81,6 +83,24 @@ dsh-agy logout # remove account
81
83
  | `dsh-agy health` | `--index <n...>` — check only these accounts (default: all enabled)<br>`--interval <ms>` — repeat on an interval instead of once | Batch health check (refresh + userinfo), re-enables accounts whose credentials are live again |
82
84
  | `dsh-agy logout` | `--index <n>` — account index (default: active)<br>`--email <email>` — account email | Remove an account |
83
85
 
86
+ ### Per-account proxy
87
+
88
+ Each account can have its own proxy; credentials are encrypted at rest and displayed masked as `protocol//host:port`.
89
+
90
+ ```sh
91
+ dsh-agy login --proxy socks5://user:pass@host:1080
92
+ dsh-agy import --proxy <url> file.json
93
+ dsh-agy proxy set --index 0 --proxy <url> # set / update
94
+ dsh-agy proxy clear --index 0 # clear (fall back to env)
95
+ dsh-agy proxy test --index 0 # TCP 2s fast-fail probe
96
+ dsh-agy proxy list # masked list
97
+ dsh-agy status # shows proxy column (masked host:port)
98
+ ```
99
+
100
+ Fallback: with no per-account proxy, requests use `EnvHttpProxyAgent` (`HTTP_PROXY`/`HTTPS_PROXY` with `NO_PROXY` honored). Per-account proxies ignore `NO_PROXY`, are fail-closed (unreachable proxy skips the account without cooldown and clears affinity), and loopback targets (`localhost`/`127.0.0.1`/`::1`) are always forced direct.
101
+
102
+ Web dashboard (`/agy`): each account card shows an inline Proxy row `[input] [Save][Clear][Test]` with masked `host:port`; writes via `POST /agy/api/proxy`, probes via `POST /agy/api/proxy/test`.
103
+
84
104
  ### Path C: Local Development & Link
85
105
 
86
106
  ```sh
@@ -211,4 +231,4 @@ pnpm run e2e # real-account end-to-end (needs AGY_REFRESH_TOKE
211
231
  pnpm run debug:request # endpoint/header bisection probe
212
232
  pnpm run verify:tools # live two-turn tool-signature check
213
233
  npm pack --dry-run # verify the publishable artifact
214
- ```
234
+ ```
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { i as proxiedFetch, n as isProxyUnreachableError } from "./proxy-DQPIwUov.mjs";
2
3
  import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
3
- import { EnvHttpProxyAgent } from "undici";
4
4
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import lockfile from "proper-lockfile";
@@ -32,21 +32,6 @@ var AgyPoolBlockedError = class extends Error {
32
32
  }
33
33
  };
34
34
  //#endregion
35
- //#region src/proxy.ts
36
- /**
37
- * Proxy-aware fetch: respects the standard HTTP_PROXY / HTTPS_PROXY / NO_PROXY
38
- * environment variables (lowercase variants included) via undici's
39
- * EnvHttpProxyAgent. Applied per-request through the `dispatcher` option so the
40
- * plugin never mutates the host process's global dispatcher — DSH's own fetch
41
- * calls stay untouched.
42
- */
43
- const agent = new EnvHttpProxyAgent();
44
- /** fetch() that routes through the env-configured proxy when one is set. */
45
- const proxiedFetch = (input, init) => fetch(input, {
46
- ...init,
47
- dispatcher: agent
48
- });
49
- //#endregion
50
35
  //#region src/oauth/constants.ts
51
36
  /**
52
37
  * Antigravity (agy) OAuth and API constants.
@@ -144,7 +129,10 @@ async function fetchAgyFirstOk(urlPath, init, fetchImpl = proxiedFetch) {
144
129
  const response = await fetchImpl(`${baseEndpoint}${urlPath}`, init);
145
130
  if (!AGY_ENDPOINT_SKIP_STATUSES.has(response.status)) return response;
146
131
  lastSkipped = response;
147
- } catch {}
132
+ } catch (error) {
133
+ const { isProxyUnreachableError } = await import("./proxy-DQPIwUov.mjs").then((n) => n.o);
134
+ if (isProxyUnreachableError(error)) throw error;
135
+ }
148
136
  if (lastSkipped) return lastSkipped;
149
137
  throw new Error("all agy endpoints failed");
150
138
  }
@@ -410,7 +398,7 @@ async function refreshAccessToken(auth, options) {
410
398
  client_id: clientId,
411
399
  client_secret: clientSecret
412
400
  })
413
- });
401
+ }, options?.proxyUrl ? { proxyUrl: options.proxyUrl } : void 0);
414
402
  if (!response.ok) {
415
403
  const errorText = await response.text().catch(() => void 0);
416
404
  const { code, description } = parseOAuthErrorPayload(errorText);
@@ -449,13 +437,22 @@ async function refreshAccessToken(auth, options) {
449
437
  clientId
450
438
  };
451
439
  } catch (error) {
440
+ const raw = error instanceof Error ? error : new Error(String(error));
441
+ const code = error?.code;
442
+ const errorCode = error?.errorCode;
443
+ const wrapped = new AgyTokenRefreshError({
444
+ message: raw.message,
445
+ status: 0,
446
+ statusText: "Network Error",
447
+ code: code ?? errorCode,
448
+ description: raw.message
449
+ });
450
+ wrapped.cause = error;
451
+ if (code) wrapped.code = code;
452
+ if (errorCode) wrapped.errorCode = errorCode;
452
453
  transientFailure = {
453
454
  type: "failed",
454
- error: new AgyTokenRefreshError({
455
- message: error instanceof Error ? error.message : "Unknown refresh error",
456
- status: 0,
457
- statusText: "Network Error"
458
- })
455
+ error: wrapped
459
456
  };
460
457
  }
461
458
  if (transientFailure) return transientFailure;
@@ -584,6 +581,10 @@ function decideRotation(kind, account, consecutiveFailures, retryAfterMs, catego
584
581
  action: "retry",
585
582
  backoffMs
586
583
  };
584
+ case "proxy-unreachable": return {
585
+ action: "rotate",
586
+ backoffMs: Math.min(backoffMs, 1e3)
587
+ };
587
588
  }
588
589
  }
589
590
  /**
@@ -991,7 +992,10 @@ var AgySessionManager = class AgySessionManager {
991
992
  access: cached?.access ?? "",
992
993
  expires: cached?.expires ?? 0,
993
994
  refresh: account.refresh
994
- }, { clientId: account.clientId });
995
+ }, {
996
+ clientId: account.clientId,
997
+ proxyUrl: account.proxy
998
+ });
995
999
  if (result.type === "success") {
996
1000
  if (!account.clientId && result.clientId) {
997
1001
  await this.store.mutate((s) => {
@@ -1017,6 +1021,7 @@ var AgySessionManager = class AgySessionManager {
1017
1021
  expires: cached.expires,
1018
1022
  refresh: account.refresh
1019
1023
  };
1024
+ if (isProxyUnreachableError(result.error)) throw new AgyAuthError("transport", "proxy_unreachable", { cause: result.error });
1020
1025
  throw new AgyAuthError(result.error.status === 429 ? "rate-limit" : result.error.status === 0 || result.error.status === 408 || result.error.status >= 500 ? "transport" : "invalid-credential", result.error.message, { cause: result.error });
1021
1026
  }
1022
1027
  if (result.type === "revoked") {
@@ -1076,16 +1081,19 @@ var AgySessionManager = class AgySessionManager {
1076
1081
  try {
1077
1082
  const auth = await this.accessTokenFor(account);
1078
1083
  if (!auth) return null;
1079
- const { fetchAvailableModels } = await import("./models-I1UAEk9x.mjs").then((n) => n.r);
1080
- const boundedFetch = (input, init) => {
1084
+ const { fetchAvailableModels } = await import("./models-3VFg0nRK.mjs").then((n) => n.r);
1085
+ const quotas = ingestFamilyQuotas(await fetchAvailableModels(auth.access, account.projectId, (input, init) => {
1081
1086
  const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS);
1082
1087
  const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
1088
+ if (account.proxy) return proxiedFetch(input, {
1089
+ ...init,
1090
+ signal
1091
+ }, { proxyUrl: account.proxy });
1083
1092
  return fetch(input, {
1084
1093
  ...init,
1085
1094
  signal
1086
1095
  });
1087
- };
1088
- const quotas = ingestFamilyQuotas(await fetchAvailableModels(auth.access, account.projectId, boundedFetch));
1096
+ }));
1089
1097
  return {
1090
1098
  key,
1091
1099
  quotas,
@@ -1184,11 +1192,39 @@ var AgySessionManager = class AgySessionManager {
1184
1192
  async getSession(model) {
1185
1193
  let storage = await this.store.load();
1186
1194
  const maxAttempts = storage.accounts.filter((account) => account.enabled !== false).length;
1195
+ let proxyUnreachableCount = 0;
1187
1196
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1188
1197
  if (storage.accounts.filter((account) => account.enabled !== false).length > 1) await this.refreshQuotaCache(storage);
1189
1198
  const picked = await this.pickAccount(storage, model);
1190
1199
  if (!picked) return void 0;
1191
- const auth = await this.accessTokenFor(picked.account);
1200
+ let auth;
1201
+ try {
1202
+ auth = await this.accessTokenFor(picked.account);
1203
+ } catch (error) {
1204
+ if (error instanceof AgyAuthError && error.kind === "transport" && isProxyUnreachableError(error)) {
1205
+ proxyUnreachableCount++;
1206
+ this.lastUsed = null;
1207
+ const deadIndex = storage.accounts.findIndex((a) => this.accountKey(a) === this.accountKey(picked.account));
1208
+ if (deadIndex !== -1) {
1209
+ const next = pickNextAccountIndex(storage.accounts, deadIndex, Date.now());
1210
+ if (next !== storage.activeIndex) {
1211
+ storage.activeIndex = next;
1212
+ await this.store.mutate((s) => {
1213
+ s.activeIndex = next;
1214
+ }).catch(() => {});
1215
+ }
1216
+ }
1217
+ storage = await this.store.load();
1218
+ continue;
1219
+ }
1220
+ if (isProxyUnreachableError(error)) {
1221
+ proxyUnreachableCount++;
1222
+ this.lastUsed = null;
1223
+ storage = await this.store.load();
1224
+ continue;
1225
+ }
1226
+ throw error;
1227
+ }
1192
1228
  if (!auth) {
1193
1229
  this.lastUsed = null;
1194
1230
  storage = await this.store.load();
@@ -1196,7 +1232,7 @@ var AgySessionManager = class AgySessionManager {
1196
1232
  }
1197
1233
  const key = this.accountKey(picked.account);
1198
1234
  if (!picked.account.projectId && !this.projectRetryFailed.has(key)) try {
1199
- const { loadCodeAssist } = await import("./exchange-CteHnRzO.mjs").then((n) => n.n);
1235
+ const { loadCodeAssist } = await import("./exchange-C6y05v4v.mjs").then((n) => n.n);
1200
1236
  const { projectId } = await loadCodeAssist(auth.access);
1201
1237
  if (projectId) {
1202
1238
  await this.store.mutate((s) => {
@@ -1227,6 +1263,7 @@ var AgySessionManager = class AgySessionManager {
1227
1263
  impersonation: impersonationHeadersFor(picked.account)
1228
1264
  };
1229
1265
  }
1266
+ if (proxyUnreachableCount === maxAttempts && maxAttempts > 0) throw new AgyAuthError("transport", "proxy_unreachable");
1230
1267
  }
1231
1268
  /** Adapter hook: apply rotation decisions and fingerprint regeneration. */
1232
1269
  async reportFailure(kind, session, info) {
@@ -1290,7 +1327,7 @@ var AgySessionManager = class AgySessionManager {
1290
1327
  ok: false,
1291
1328
  error: "No agy account configured — run `dsh-agy login` first."
1292
1329
  };
1293
- const { toAgyRequestBody } = await import("./translate-C1SMdm5J.mjs").then((n) => n.n);
1330
+ const { toAgyRequestBody } = await import("./translate-Zf1mdBri.mjs").then((n) => n.n);
1294
1331
  const { fetchAgyFirstOk } = await Promise.resolve().then(() => constants_exports);
1295
1332
  const { parseAgySse } = await import("./parse-D8bV2yqa.mjs").then((n) => n.n);
1296
1333
  const body = toAgyRequestBody({
@@ -1370,7 +1407,7 @@ var AgySessionManager = class AgySessionManager {
1370
1407
  ok: false,
1371
1408
  error: "refresh failed (revoked?)"
1372
1409
  };
1373
- const response = await proxiedFetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { Authorization: `Bearer ${auth.access}` } });
1410
+ const response = await proxiedFetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", { headers: { Authorization: `Bearer ${auth.access}` } }, account.proxy ? { proxyUrl: account.proxy } : void 0);
1374
1411
  if (!response.ok) return {
1375
1412
  ok: false,
1376
1413
  error: `userinfo ${response.status}`
@@ -1589,21 +1626,42 @@ const properFileLock = { async withLock(file, fn) {
1589
1626
  function isEncrypted(value) {
1590
1627
  return value.startsWith(ENC_PREFIX);
1591
1628
  }
1592
- /** Decrypt one account's refresh field when encrypted; plaintext passes through (legacy). */
1629
+ /** Decrypt one account's refresh/proxy fields when encrypted; plaintext passes through (legacy). */
1593
1630
  function decryptAccount(account, codec) {
1594
- if (isEncrypted(account.refresh)) return {
1595
- ...account,
1596
- refresh: codec.decrypt(account.refresh)
1631
+ let out = account;
1632
+ if (isEncrypted(out.refresh)) out = {
1633
+ ...out,
1634
+ refresh: codec.decrypt(out.refresh)
1635
+ };
1636
+ if (out.proxy && isEncrypted(out.proxy)) out = {
1637
+ ...out,
1638
+ proxy: codec.decrypt(out.proxy)
1597
1639
  };
1598
- return account;
1640
+ return out;
1599
1641
  }
1600
- /** Encrypt one account's refresh field (plaintext stays plaintext if no codec change). */
1642
+ /** Encrypt one account's refresh/proxy fields (plaintext stays plaintext if no codec change). */
1601
1643
  function encryptAccount(account, codec) {
1602
- if (!isEncrypted(account.refresh)) return {
1603
- ...account,
1604
- refresh: codec.encrypt(account.refresh)
1644
+ let out = account;
1645
+ if (!isEncrypted(out.refresh)) out = {
1646
+ ...out,
1647
+ refresh: codec.encrypt(out.refresh)
1605
1648
  };
1606
- return account;
1649
+ if (out.proxy && !isEncrypted(out.proxy)) out = {
1650
+ ...out,
1651
+ proxy: codec.encrypt(out.proxy)
1652
+ };
1653
+ return out;
1654
+ }
1655
+ /** Mask a proxy URL for display/logs: protocol//host:port (no credentials, no query). */
1656
+ function maskProxyUrl(proxyUrl) {
1657
+ if (!proxyUrl) return null;
1658
+ try {
1659
+ const u = new URL(proxyUrl);
1660
+ const port = u.port || (u.protocol === "https:" ? "443" : u.protocol === "socks5:" || u.protocol === "socks5h:" ? "1080" : "8080");
1661
+ return `${u.protocol}//${u.hostname}:${port}`;
1662
+ } catch {
1663
+ return null;
1664
+ }
1607
1665
  }
1608
1666
  function ensureAccountIds(storage) {
1609
1667
  let mutated = false;
@@ -1761,6 +1819,6 @@ var JsonAccountStore = class {
1761
1819
  }
1762
1820
  };
1763
1821
  //#endregion
1764
- export { AgyPoolBlockedError as A, OAUTH_USERINFO_URL as C, resolveAgyClientCredentials as D, getAgyBootstrapUserAgent as E, proxiedFetch as O, OAUTH_TOKEN_URL as S, getAgyBootstrapClientMetadata as T, generateAntigravityRequestId as _, loadMasterKey as a, AGY_SCOPES as b, resolveMasterKeyCodec as c, generateFingerprint as d, recordFingerprintVersion as f, deriveAntigravitySessionId as g, resolveAntigravityVersionBounded as h, deriveKey as i, AgyAuthError as k, AgySessionManager as l, resolveAntigravityVersion as m, MASTER_KEY_REF as n, persistMasterKey as o, calculateTokenExpiry as p, createAesGcmCodec as r, resolveDshHome as s, JsonAccountStore as t, isAgyDisabled as u, AGY_DEFAULT_REDIRECT_URI as v, fetchAgyFirstOk as w, OAUTH_AUTHORIZE_URL as x, AGY_ENDPOINT_FALLBACKS as y };
1822
+ export { AgyPoolBlockedError as A, OAUTH_TOKEN_URL as C, getAgyBootstrapUserAgent as D, getAgyBootstrapClientMetadata as E, resolveAgyClientCredentials as O, OAUTH_AUTHORIZE_URL as S, fetchAgyFirstOk as T, deriveAntigravitySessionId as _, deriveKey as a, AGY_ENDPOINT_FALLBACKS as b, resolveDshHome as c, isAgyDisabled as d, generateFingerprint as f, resolveAntigravityVersionBounded as g, resolveAntigravityVersion as h, createAesGcmCodec as i, AgyAuthError as k, resolveMasterKeyCodec as l, calculateTokenExpiry as m, maskProxyUrl as n, loadMasterKey as o, recordFingerprintVersion as p, MASTER_KEY_REF as r, persistMasterKey as s, JsonAccountStore as t, AgySessionManager as u, generateAntigravityRequestId as v, OAUTH_USERINFO_URL as w, AGY_SCOPES as x, AGY_DEFAULT_REDIRECT_URI as y };
1765
1823
 
1766
- //# sourceMappingURL=accounts-BvX5zmPa.mjs.map
1824
+ //# sourceMappingURL=accounts-Blqno5Te.mjs.map