@cortexkit/opencode-antigravity-auth 1.0.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.
Files changed (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +714 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +15175 -0
  6. package/dist/index.js.map +7 -0
  7. package/dist/src/antigravity/oauth.d.ts +2 -0
  8. package/dist/src/antigravity/oauth.d.ts.map +1 -0
  9. package/dist/src/antigravity/oauth.js +3 -0
  10. package/dist/src/antigravity/oauth.js.map +1 -0
  11. package/dist/src/constants.d.ts +2 -0
  12. package/dist/src/constants.d.ts.map +1 -0
  13. package/dist/src/constants.js +3 -0
  14. package/dist/src/constants.js.map +1 -0
  15. package/dist/src/hooks/auto-update-checker/cache.d.ts +3 -0
  16. package/dist/src/hooks/auto-update-checker/cache.d.ts.map +1 -0
  17. package/dist/src/hooks/auto-update-checker/cache.js +71 -0
  18. package/dist/src/hooks/auto-update-checker/cache.js.map +1 -0
  19. package/dist/src/hooks/auto-update-checker/checker.d.ts +16 -0
  20. package/dist/src/hooks/auto-update-checker/checker.d.ts.map +1 -0
  21. package/dist/src/hooks/auto-update-checker/checker.js +234 -0
  22. package/dist/src/hooks/auto-update-checker/checker.js.map +1 -0
  23. package/dist/src/hooks/auto-update-checker/constants.d.ts +9 -0
  24. package/dist/src/hooks/auto-update-checker/constants.d.ts.map +1 -0
  25. package/dist/src/hooks/auto-update-checker/constants.js +23 -0
  26. package/dist/src/hooks/auto-update-checker/constants.js.map +1 -0
  27. package/dist/src/hooks/auto-update-checker/index.d.ts +34 -0
  28. package/dist/src/hooks/auto-update-checker/index.d.ts.map +1 -0
  29. package/dist/src/hooks/auto-update-checker/index.js +122 -0
  30. package/dist/src/hooks/auto-update-checker/index.js.map +1 -0
  31. package/dist/src/hooks/auto-update-checker/logging.d.ts +3 -0
  32. package/dist/src/hooks/auto-update-checker/logging.d.ts.map +1 -0
  33. package/dist/src/hooks/auto-update-checker/logging.js +9 -0
  34. package/dist/src/hooks/auto-update-checker/logging.js.map +1 -0
  35. package/dist/src/hooks/auto-update-checker/types.d.ts +25 -0
  36. package/dist/src/hooks/auto-update-checker/types.d.ts.map +1 -0
  37. package/dist/src/hooks/auto-update-checker/types.js +1 -0
  38. package/dist/src/hooks/auto-update-checker/types.js.map +1 -0
  39. package/dist/src/plugin/accounts.d.ts +232 -0
  40. package/dist/src/plugin/accounts.d.ts.map +1 -0
  41. package/dist/src/plugin/accounts.js +1163 -0
  42. package/dist/src/plugin/accounts.js.map +1 -0
  43. package/dist/src/plugin/agy-transport.d.ts +2 -0
  44. package/dist/src/plugin/agy-transport.d.ts.map +1 -0
  45. package/dist/src/plugin/agy-transport.js +3 -0
  46. package/dist/src/plugin/agy-transport.js.map +1 -0
  47. package/dist/src/plugin/auth-doctor.d.ts +30 -0
  48. package/dist/src/plugin/auth-doctor.d.ts.map +1 -0
  49. package/dist/src/plugin/auth-doctor.js +144 -0
  50. package/dist/src/plugin/auth-doctor.js.map +1 -0
  51. package/dist/src/plugin/auth-drift.d.ts +13 -0
  52. package/dist/src/plugin/auth-drift.d.ts.map +1 -0
  53. package/dist/src/plugin/auth-drift.js +70 -0
  54. package/dist/src/plugin/auth-drift.js.map +1 -0
  55. package/dist/src/plugin/auth.d.ts +2 -0
  56. package/dist/src/plugin/auth.d.ts.map +1 -0
  57. package/dist/src/plugin/auth.js +3 -0
  58. package/dist/src/plugin/auth.js.map +1 -0
  59. package/dist/src/plugin/cache/index.d.ts +5 -0
  60. package/dist/src/plugin/cache/index.d.ts.map +1 -0
  61. package/dist/src/plugin/cache/index.js +5 -0
  62. package/dist/src/plugin/cache/index.js.map +1 -0
  63. package/dist/src/plugin/cache/signature-cache.d.ts +111 -0
  64. package/dist/src/plugin/cache/signature-cache.d.ts.map +1 -0
  65. package/dist/src/plugin/cache/signature-cache.js +375 -0
  66. package/dist/src/plugin/cache/signature-cache.js.map +1 -0
  67. package/dist/src/plugin/cache.d.ts +44 -0
  68. package/dist/src/plugin/cache.d.ts.map +1 -0
  69. package/dist/src/plugin/cache.js +247 -0
  70. package/dist/src/plugin/cache.js.map +1 -0
  71. package/dist/src/plugin/cli.d.ts +44 -0
  72. package/dist/src/plugin/cli.d.ts.map +1 -0
  73. package/dist/src/plugin/cli.js +153 -0
  74. package/dist/src/plugin/cli.js.map +1 -0
  75. package/dist/src/plugin/config/index.d.ts +16 -0
  76. package/dist/src/plugin/config/index.d.ts.map +1 -0
  77. package/dist/src/plugin/config/index.js +16 -0
  78. package/dist/src/plugin/config/index.js.map +1 -0
  79. package/dist/src/plugin/config/loader.d.ts +36 -0
  80. package/dist/src/plugin/config/loader.d.ts.map +1 -0
  81. package/dist/src/plugin/config/loader.js +140 -0
  82. package/dist/src/plugin/config/loader.js.map +1 -0
  83. package/dist/src/plugin/config/models.d.ts +3 -0
  84. package/dist/src/plugin/config/models.d.ts.map +1 -0
  85. package/dist/src/plugin/config/models.js +2 -0
  86. package/dist/src/plugin/config/models.js.map +1 -0
  87. package/dist/src/plugin/config/schema.d.ts +141 -0
  88. package/dist/src/plugin/config/schema.d.ts.map +1 -0
  89. package/dist/src/plugin/config/schema.js +504 -0
  90. package/dist/src/plugin/config/schema.js.map +1 -0
  91. package/dist/src/plugin/config/updater.d.ts +55 -0
  92. package/dist/src/plugin/config/updater.d.ts.map +1 -0
  93. package/dist/src/plugin/config/updater.js +127 -0
  94. package/dist/src/plugin/config/updater.js.map +1 -0
  95. package/dist/src/plugin/core/streaming/index.d.ts +3 -0
  96. package/dist/src/plugin/core/streaming/index.d.ts.map +1 -0
  97. package/dist/src/plugin/core/streaming/index.js +3 -0
  98. package/dist/src/plugin/core/streaming/index.js.map +1 -0
  99. package/dist/src/plugin/core/streaming/transformer.d.ts +12 -0
  100. package/dist/src/plugin/core/streaming/transformer.d.ts.map +1 -0
  101. package/dist/src/plugin/core/streaming/transformer.js +447 -0
  102. package/dist/src/plugin/core/streaming/transformer.js.map +1 -0
  103. package/dist/src/plugin/core/streaming/types.d.ts +34 -0
  104. package/dist/src/plugin/core/streaming/types.d.ts.map +1 -0
  105. package/dist/src/plugin/core/streaming/types.js +1 -0
  106. package/dist/src/plugin/core/streaming/types.js.map +1 -0
  107. package/dist/src/plugin/debug.d.ts +94 -0
  108. package/dist/src/plugin/debug.d.ts.map +1 -0
  109. package/dist/src/plugin/debug.js +383 -0
  110. package/dist/src/plugin/debug.js.map +1 -0
  111. package/dist/src/plugin/errors.d.ts +28 -0
  112. package/dist/src/plugin/errors.d.ts.map +1 -0
  113. package/dist/src/plugin/errors.js +42 -0
  114. package/dist/src/plugin/errors.js.map +1 -0
  115. package/dist/src/plugin/fingerprint.d.ts +2 -0
  116. package/dist/src/plugin/fingerprint.d.ts.map +1 -0
  117. package/dist/src/plugin/fingerprint.js +3 -0
  118. package/dist/src/plugin/fingerprint.js.map +1 -0
  119. package/dist/src/plugin/gemini-dump.d.ts +47 -0
  120. package/dist/src/plugin/gemini-dump.d.ts.map +1 -0
  121. package/dist/src/plugin/gemini-dump.js +238 -0
  122. package/dist/src/plugin/gemini-dump.js.map +1 -0
  123. package/dist/src/plugin/image-saver.d.ts +25 -0
  124. package/dist/src/plugin/image-saver.d.ts.map +1 -0
  125. package/dist/src/plugin/image-saver.js +86 -0
  126. package/dist/src/plugin/image-saver.js.map +1 -0
  127. package/dist/src/plugin/logger.d.ts +23 -0
  128. package/dist/src/plugin/logger.d.ts.map +1 -0
  129. package/dist/src/plugin/logger.js +80 -0
  130. package/dist/src/plugin/logger.js.map +1 -0
  131. package/dist/src/plugin/logging-utils.d.ts +23 -0
  132. package/dist/src/plugin/logging-utils.d.ts.map +1 -0
  133. package/dist/src/plugin/logging-utils.js +92 -0
  134. package/dist/src/plugin/logging-utils.js.map +1 -0
  135. package/dist/src/plugin/model-registry.d.ts +2 -0
  136. package/dist/src/plugin/model-registry.d.ts.map +1 -0
  137. package/dist/src/plugin/model-registry.js +3 -0
  138. package/dist/src/plugin/model-registry.js.map +1 -0
  139. package/dist/src/plugin/project.d.ts +2 -0
  140. package/dist/src/plugin/project.d.ts.map +1 -0
  141. package/dist/src/plugin/project.js +3 -0
  142. package/dist/src/plugin/project.js.map +1 -0
  143. package/dist/src/plugin/prompt-context.d.ts +18 -0
  144. package/dist/src/plugin/prompt-context.d.ts.map +1 -0
  145. package/dist/src/plugin/prompt-context.js +99 -0
  146. package/dist/src/plugin/prompt-context.js.map +1 -0
  147. package/dist/src/plugin/quota.d.ts +44 -0
  148. package/dist/src/plugin/quota.d.ts.map +1 -0
  149. package/dist/src/plugin/quota.js +336 -0
  150. package/dist/src/plugin/quota.js.map +1 -0
  151. package/dist/src/plugin/recovery/constants.d.ts +22 -0
  152. package/dist/src/plugin/recovery/constants.d.ts.map +1 -0
  153. package/dist/src/plugin/recovery/constants.js +43 -0
  154. package/dist/src/plugin/recovery/constants.js.map +1 -0
  155. package/dist/src/plugin/recovery/index.d.ts +12 -0
  156. package/dist/src/plugin/recovery/index.d.ts.map +1 -0
  157. package/dist/src/plugin/recovery/index.js +12 -0
  158. package/dist/src/plugin/recovery/index.js.map +1 -0
  159. package/dist/src/plugin/recovery/storage.d.ts +24 -0
  160. package/dist/src/plugin/recovery/storage.d.ts.map +1 -0
  161. package/dist/src/plugin/recovery/storage.js +354 -0
  162. package/dist/src/plugin/recovery/storage.js.map +1 -0
  163. package/dist/src/plugin/recovery/types.d.ts +116 -0
  164. package/dist/src/plugin/recovery/types.d.ts.map +1 -0
  165. package/dist/src/plugin/recovery/types.js +6 -0
  166. package/dist/src/plugin/recovery/types.js.map +1 -0
  167. package/dist/src/plugin/recovery.d.ts +61 -0
  168. package/dist/src/plugin/recovery.d.ts.map +1 -0
  169. package/dist/src/plugin/recovery.js +381 -0
  170. package/dist/src/plugin/recovery.js.map +1 -0
  171. package/dist/src/plugin/refresh-queue.d.ts +100 -0
  172. package/dist/src/plugin/refresh-queue.d.ts.map +1 -0
  173. package/dist/src/plugin/refresh-queue.js +247 -0
  174. package/dist/src/plugin/refresh-queue.js.map +1 -0
  175. package/dist/src/plugin/request-helpers.d.ts +278 -0
  176. package/dist/src/plugin/request-helpers.d.ts.map +1 -0
  177. package/dist/src/plugin/request-helpers.js +2323 -0
  178. package/dist/src/plugin/request-helpers.js.map +1 -0
  179. package/dist/src/plugin/request.d.ts +102 -0
  180. package/dist/src/plugin/request.d.ts.map +1 -0
  181. package/dist/src/plugin/request.js +1658 -0
  182. package/dist/src/plugin/request.js.map +1 -0
  183. package/dist/src/plugin/rotation.d.ts +169 -0
  184. package/dist/src/plugin/rotation.d.ts.map +1 -0
  185. package/dist/src/plugin/rotation.js +328 -0
  186. package/dist/src/plugin/rotation.js.map +1 -0
  187. package/dist/src/plugin/search.d.ts +25 -0
  188. package/dist/src/plugin/search.d.ts.map +1 -0
  189. package/dist/src/plugin/search.js +200 -0
  190. package/dist/src/plugin/search.js.map +1 -0
  191. package/dist/src/plugin/server.d.ts +23 -0
  192. package/dist/src/plugin/server.d.ts.map +1 -0
  193. package/dist/src/plugin/server.js +324 -0
  194. package/dist/src/plugin/server.js.map +1 -0
  195. package/dist/src/plugin/storage.d.ts +150 -0
  196. package/dist/src/plugin/storage.d.ts.map +1 -0
  197. package/dist/src/plugin/storage.js +628 -0
  198. package/dist/src/plugin/storage.js.map +1 -0
  199. package/dist/src/plugin/stores/signature-store.d.ts +5 -0
  200. package/dist/src/plugin/stores/signature-store.d.ts.map +1 -0
  201. package/dist/src/plugin/stores/signature-store.js +25 -0
  202. package/dist/src/plugin/stores/signature-store.js.map +1 -0
  203. package/dist/src/plugin/thinking-recovery.d.ts +90 -0
  204. package/dist/src/plugin/thinking-recovery.d.ts.map +1 -0
  205. package/dist/src/plugin/thinking-recovery.js +330 -0
  206. package/dist/src/plugin/thinking-recovery.js.map +1 -0
  207. package/dist/src/plugin/token.d.ts +19 -0
  208. package/dist/src/plugin/token.d.ts.map +1 -0
  209. package/dist/src/plugin/token.js +130 -0
  210. package/dist/src/plugin/token.js.map +1 -0
  211. package/dist/src/plugin/transform/claude.d.ts +92 -0
  212. package/dist/src/plugin/transform/claude.d.ts.map +1 -0
  213. package/dist/src/plugin/transform/claude.js +280 -0
  214. package/dist/src/plugin/transform/claude.js.map +1 -0
  215. package/dist/src/plugin/transform/cross-model-sanitizer.d.ts +2 -0
  216. package/dist/src/plugin/transform/cross-model-sanitizer.d.ts.map +1 -0
  217. package/dist/src/plugin/transform/cross-model-sanitizer.js +3 -0
  218. package/dist/src/plugin/transform/cross-model-sanitizer.js.map +1 -0
  219. package/dist/src/plugin/transform/gemini.d.ts +100 -0
  220. package/dist/src/plugin/transform/gemini.d.ts.map +1 -0
  221. package/dist/src/plugin/transform/gemini.js +446 -0
  222. package/dist/src/plugin/transform/gemini.js.map +1 -0
  223. package/dist/src/plugin/transform/index.d.ts +2 -0
  224. package/dist/src/plugin/transform/index.d.ts.map +1 -0
  225. package/dist/src/plugin/transform/index.js +3 -0
  226. package/dist/src/plugin/transform/index.js.map +1 -0
  227. package/dist/src/plugin/transform/model-resolver.d.ts +2 -0
  228. package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -0
  229. package/dist/src/plugin/transform/model-resolver.js +3 -0
  230. package/dist/src/plugin/transform/model-resolver.js.map +1 -0
  231. package/dist/src/plugin/transform/types.d.ts +2 -0
  232. package/dist/src/plugin/transform/types.d.ts.map +1 -0
  233. package/dist/src/plugin/transform/types.js +3 -0
  234. package/dist/src/plugin/transform/types.js.map +1 -0
  235. package/dist/src/plugin/types.d.ts +80 -0
  236. package/dist/src/plugin/types.d.ts.map +1 -0
  237. package/dist/src/plugin/types.js +1 -0
  238. package/dist/src/plugin/types.js.map +1 -0
  239. package/dist/src/plugin/ui/ansi.d.ts +32 -0
  240. package/dist/src/plugin/ui/ansi.d.ts.map +1 -0
  241. package/dist/src/plugin/ui/ansi.js +52 -0
  242. package/dist/src/plugin/ui/ansi.js.map +1 -0
  243. package/dist/src/plugin/ui/auth-menu.d.ts +59 -0
  244. package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -0
  245. package/dist/src/plugin/ui/auth-menu.js +362 -0
  246. package/dist/src/plugin/ui/auth-menu.js.map +1 -0
  247. package/dist/src/plugin/ui/confirm.d.ts +2 -0
  248. package/dist/src/plugin/ui/confirm.d.ts.map +1 -0
  249. package/dist/src/plugin/ui/confirm.js +15 -0
  250. package/dist/src/plugin/ui/confirm.js.map +1 -0
  251. package/dist/src/plugin/ui/model-status.d.ts +28 -0
  252. package/dist/src/plugin/ui/model-status.d.ts.map +1 -0
  253. package/dist/src/plugin/ui/model-status.js +80 -0
  254. package/dist/src/plugin/ui/model-status.js.map +1 -0
  255. package/dist/src/plugin/ui/quota-status.d.ts +82 -0
  256. package/dist/src/plugin/ui/quota-status.d.ts.map +1 -0
  257. package/dist/src/plugin/ui/quota-status.js +251 -0
  258. package/dist/src/plugin/ui/quota-status.js.map +1 -0
  259. package/dist/src/plugin/ui/select.d.ts +23 -0
  260. package/dist/src/plugin/ui/select.d.ts.map +1 -0
  261. package/dist/src/plugin/ui/select.js +254 -0
  262. package/dist/src/plugin/ui/select.js.map +1 -0
  263. package/dist/src/plugin/version.d.ts +2 -0
  264. package/dist/src/plugin/version.d.ts.map +1 -0
  265. package/dist/src/plugin/version.js +3 -0
  266. package/dist/src/plugin/version.js.map +1 -0
  267. package/dist/src/plugin.d.ts +32 -0
  268. package/dist/src/plugin.d.ts.map +1 -0
  269. package/dist/src/plugin.js +3125 -0
  270. package/dist/src/plugin.js.map +1 -0
  271. package/package.json +70 -0
@@ -0,0 +1,1163 @@
1
+ import { formatRefreshParts, parseRefreshParts } from "./auth";
2
+ import { loadAccounts, saveAccounts } from "./storage";
3
+ import { getHealthTracker, getTokenTracker, selectHybridAccount } from "./rotation";
4
+ import { generateFingerprint, updateFingerprintVersion, MAX_FINGERPRINT_HISTORY } from "./fingerprint";
5
+ import { getModelFamily } from "./transform/model-resolver";
6
+ import { debugLogToFile } from "./debug";
7
+ import { formatAccountLabel } from "./logging-utils";
8
+ const QUOTA_EXHAUSTED_BACKOFFS = [60_000, 300_000, 1_800_000, 7_200_000];
9
+ const RATE_LIMIT_EXCEEDED_BACKOFF = 30_000;
10
+ // Increased from 15s to 45s base + jitter to reduce retry pressure on capacity errors
11
+ const MODEL_CAPACITY_EXHAUSTED_BASE_BACKOFF = 45_000;
12
+ const MODEL_CAPACITY_EXHAUSTED_JITTER_MAX = 30_000; // ±15s jitter range
13
+ const SERVER_ERROR_BACKOFF = 20_000;
14
+ const UNKNOWN_BACKOFF = 60_000;
15
+ const MIN_BACKOFF_MS = 2_000;
16
+ function isStorageLockContention(error) {
17
+ const message = String(error);
18
+ return message.includes("Lock file is already being held") || message.includes("ELOCKED");
19
+ }
20
+ /**
21
+ * Generate a random jitter value for backoff timing.
22
+ * Helps prevent thundering herd problem when multiple clients retry simultaneously.
23
+ */
24
+ function generateJitter(maxJitterMs) {
25
+ return Math.random() * maxJitterMs - (maxJitterMs / 2);
26
+ }
27
+ export function parseRateLimitReason(reason, message, status) {
28
+ // 1. Status Code Checks (Rust parity)
29
+ // 529 = Site Overloaded, 503 = Service Unavailable -> Capacity issues
30
+ if (status === 529 || status === 503)
31
+ return "MODEL_CAPACITY_EXHAUSTED";
32
+ // 500 = Internal Server Error -> Treat as Server Error (soft wait)
33
+ if (status === 500)
34
+ return "SERVER_ERROR";
35
+ // 2. Explicit Reason String
36
+ if (reason) {
37
+ switch (reason.toUpperCase()) {
38
+ case "QUOTA_EXHAUSTED": return "QUOTA_EXHAUSTED";
39
+ case "RATE_LIMIT_EXCEEDED": return "RATE_LIMIT_EXCEEDED";
40
+ case "MODEL_CAPACITY_EXHAUSTED": return "MODEL_CAPACITY_EXHAUSTED";
41
+ }
42
+ }
43
+ // 3. Message Text Scanning (Rust Regex parity)
44
+ if (message) {
45
+ const lower = message.toLowerCase();
46
+ // Capacity / Overloaded (Transient) - Check FIRST before "exhausted"
47
+ if (lower.includes("capacity") || lower.includes("overloaded") || lower.includes("resource exhausted")) {
48
+ return "MODEL_CAPACITY_EXHAUSTED";
49
+ }
50
+ // RPM / TPM (Short Wait)
51
+ // "per minute", "rate limit", "too many requests"
52
+ // "presque" (French: almost) - retained for i18n parity with Rust reference
53
+ if (lower.includes("per minute") || lower.includes("rate limit") || lower.includes("too many requests") || lower.includes("presque")) {
54
+ return "RATE_LIMIT_EXCEEDED";
55
+ }
56
+ // Quota (Long Wait)
57
+ if (lower.includes("exhausted") || lower.includes("quota")) {
58
+ return "QUOTA_EXHAUSTED";
59
+ }
60
+ }
61
+ // Default fallback for 429 without clearer info
62
+ if (status === 429) {
63
+ return "UNKNOWN";
64
+ }
65
+ return "UNKNOWN";
66
+ }
67
+ export function calculateBackoffMs(reason, consecutiveFailures, retryAfterMs) {
68
+ // Respect explicit Retry-After header if reasonable
69
+ if (retryAfterMs && retryAfterMs > 0) {
70
+ // Rust uses 2s min buffer, we keep 2s
71
+ return Math.max(retryAfterMs, MIN_BACKOFF_MS);
72
+ }
73
+ switch (reason) {
74
+ case "QUOTA_EXHAUSTED": {
75
+ const index = Math.min(consecutiveFailures, QUOTA_EXHAUSTED_BACKOFFS.length - 1);
76
+ return QUOTA_EXHAUSTED_BACKOFFS[index] ?? UNKNOWN_BACKOFF;
77
+ }
78
+ case "RATE_LIMIT_EXCEEDED":
79
+ return RATE_LIMIT_EXCEEDED_BACKOFF; // 30s
80
+ case "MODEL_CAPACITY_EXHAUSTED":
81
+ // Apply jitter to prevent thundering herd on capacity errors
82
+ return MODEL_CAPACITY_EXHAUSTED_BASE_BACKOFF + generateJitter(MODEL_CAPACITY_EXHAUSTED_JITTER_MAX);
83
+ case "SERVER_ERROR":
84
+ return SERVER_ERROR_BACKOFF; // 20s
85
+ case "UNKNOWN":
86
+ default:
87
+ return UNKNOWN_BACKOFF; // 60s
88
+ }
89
+ }
90
+ function nowMs() {
91
+ return Date.now();
92
+ }
93
+ function clampNonNegativeInt(value, fallback) {
94
+ if (typeof value !== "number" || !Number.isFinite(value)) {
95
+ return fallback;
96
+ }
97
+ return value < 0 ? 0 : Math.floor(value);
98
+ }
99
+ function getQuotaKey(family, headerStyle, model) {
100
+ if (family === "claude") {
101
+ return "claude";
102
+ }
103
+ const base = headerStyle === "gemini-cli" ? "gemini-cli" : "gemini-antigravity";
104
+ if (model) {
105
+ return `${base}:${model}`;
106
+ }
107
+ return base;
108
+ }
109
+ function isRateLimitedForQuotaKey(account, key) {
110
+ const resetTime = account.rateLimitResetTimes[key];
111
+ return resetTime !== undefined && nowMs() < resetTime;
112
+ }
113
+ function isRateLimitedForFamily(account, family, model) {
114
+ if (family === "claude") {
115
+ return isRateLimitedForQuotaKey(account, "claude");
116
+ }
117
+ const antigravityIsLimited = isRateLimitedForHeaderStyle(account, family, "antigravity", model);
118
+ const cliIsLimited = isRateLimitedForHeaderStyle(account, family, "gemini-cli", model);
119
+ return antigravityIsLimited && cliIsLimited;
120
+ }
121
+ function isRateLimitedForHeaderStyle(account, family, headerStyle, model) {
122
+ clearExpiredRateLimits(account);
123
+ if (family === "claude") {
124
+ return isRateLimitedForQuotaKey(account, "claude");
125
+ }
126
+ // Check model-specific quota first if provided
127
+ if (model) {
128
+ const modelKey = getQuotaKey(family, headerStyle, model);
129
+ if (isRateLimitedForQuotaKey(account, modelKey)) {
130
+ return true;
131
+ }
132
+ }
133
+ // Then check base family quota
134
+ const baseKey = getQuotaKey(family, headerStyle);
135
+ return isRateLimitedForQuotaKey(account, baseKey);
136
+ }
137
+ function clearExpiredRateLimits(account) {
138
+ const now = nowMs();
139
+ const keys = Object.keys(account.rateLimitResetTimes);
140
+ for (const key of keys) {
141
+ const resetTime = account.rateLimitResetTimes[key];
142
+ if (resetTime !== undefined && now >= resetTime) {
143
+ delete account.rateLimitResetTimes[key];
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * Resolve the quota group for soft quota checks.
149
+ *
150
+ * When a model string is available, we can precisely determine the quota group.
151
+ * When model is null/undefined, we fall back based on family:
152
+ * - Claude → "claude" quota group
153
+ * - Gemini → "gemini-pro" (conservative fallback; may misclassify flash models)
154
+ *
155
+ * @param family - The model family ("claude" | "gemini")
156
+ * @param model - Optional model string for precise resolution
157
+ * @returns The QuotaGroup to use for soft quota checks
158
+ */
159
+ export function resolveQuotaGroup(family, model) {
160
+ if (model) {
161
+ return getModelFamily(model);
162
+ }
163
+ return family === "claude" ? "claude" : "gemini-pro";
164
+ }
165
+ function isOverSoftQuotaThreshold(account, family, thresholdPercent, cacheTtlMs, model) {
166
+ if (thresholdPercent >= 100)
167
+ return false;
168
+ if (!account.cachedQuota)
169
+ return false;
170
+ if (account.cachedQuotaUpdatedAt == null)
171
+ return false;
172
+ const age = nowMs() - account.cachedQuotaUpdatedAt;
173
+ if (age > cacheTtlMs)
174
+ return false;
175
+ const quotaGroup = resolveQuotaGroup(family, model);
176
+ const groupData = account.cachedQuota[quotaGroup];
177
+ if (groupData?.remainingFraction == null)
178
+ return false;
179
+ const remainingFraction = Math.max(0, Math.min(1, groupData.remainingFraction));
180
+ const usedPercent = (1 - remainingFraction) * 100;
181
+ const isOverThreshold = usedPercent >= thresholdPercent;
182
+ if (isOverThreshold) {
183
+ const accountLabel = formatAccountLabel(account.email, account.index);
184
+ const resetSuffix = groupData.resetTime ? ` (resets: ${groupData.resetTime})` : "";
185
+ const message = `[SoftQuota] Skipping ${accountLabel}: ${quotaGroup} usage ${usedPercent.toFixed(1)}% >= threshold ${thresholdPercent}%${resetSuffix}`;
186
+ debugLogToFile(message);
187
+ }
188
+ return isOverThreshold;
189
+ }
190
+ export function computeSoftQuotaCacheTtlMs(ttlConfig, refreshIntervalMinutes) {
191
+ if (ttlConfig === "auto") {
192
+ return Math.max(2 * refreshIntervalMinutes, 10) * 60 * 1000;
193
+ }
194
+ return ttlConfig * 60 * 1000;
195
+ }
196
+ /**
197
+ * In-memory multi-account manager with sticky account selection.
198
+ *
199
+ * Uses the same account until it hits a rate limit (429), then switches.
200
+ * Rate limits are tracked per-model-family (claude/gemini) so an account
201
+ * rate-limited for Claude can still be used for Gemini.
202
+ *
203
+ * Source of truth for the pool is `antigravity-accounts.json`.
204
+ */
205
+ export class AccountManager {
206
+ accounts = [];
207
+ cursorByFamily = { claude: 0, gemini: 0 };
208
+ currentAccountIndexByFamily = {
209
+ claude: -1,
210
+ gemini: -1,
211
+ };
212
+ sessionOffsetApplied = {
213
+ claude: false,
214
+ gemini: false,
215
+ };
216
+ lastToastAccountIndex = -1;
217
+ lastToastTime = 0;
218
+ savePending = false;
219
+ saveTimeout = null;
220
+ savePromiseResolvers = [];
221
+ sessionStartTime = Date.now();
222
+ sessionRequestCounts = new Map();
223
+ sessionUsedAccounts = new Set();
224
+ static async loadFromDisk(authFallback) {
225
+ const stored = await loadAccounts();
226
+ return new AccountManager(authFallback, stored);
227
+ }
228
+ constructor(authFallback, stored) {
229
+ const authParts = authFallback ? parseRefreshParts(authFallback.refresh) : null;
230
+ if (stored && stored.accounts.length === 0) {
231
+ this.accounts = [];
232
+ this.cursorByFamily = { claude: 0, gemini: 0 };
233
+ return;
234
+ }
235
+ if (stored && stored.accounts.length > 0) {
236
+ const baseNow = nowMs();
237
+ this.accounts = stored.accounts
238
+ .map((acc, index) => {
239
+ if (!acc.refreshToken || typeof acc.refreshToken !== "string") {
240
+ return null;
241
+ }
242
+ const matchesFallback = !!(authFallback &&
243
+ authParts &&
244
+ authParts.refreshToken &&
245
+ acc.refreshToken === authParts.refreshToken);
246
+ return {
247
+ index,
248
+ email: acc.email,
249
+ addedAt: clampNonNegativeInt(acc.addedAt, baseNow),
250
+ lastUsed: clampNonNegativeInt(acc.lastUsed, 0),
251
+ parts: {
252
+ refreshToken: acc.refreshToken,
253
+ projectId: acc.projectId,
254
+ managedProjectId: acc.managedProjectId,
255
+ },
256
+ access: matchesFallback ? authFallback?.access : undefined,
257
+ expires: matchesFallback ? authFallback?.expires : undefined,
258
+ enabled: acc.enabled !== false,
259
+ rateLimitResetTimes: acc.rateLimitResetTimes ?? {},
260
+ lastSwitchReason: acc.lastSwitchReason,
261
+ coolingDownUntil: acc.coolingDownUntil,
262
+ cooldownReason: acc.cooldownReason,
263
+ touchedForQuota: {},
264
+ fingerprint: acc.fingerprint ?? generateFingerprint(),
265
+ fingerprintHistory: acc.fingerprintHistory ?? [],
266
+ cachedQuota: acc.cachedQuota,
267
+ cachedQuotaUpdatedAt: acc.cachedQuotaUpdatedAt,
268
+ dailyRequestCounts: acc.dailyRequestCounts,
269
+ verificationRequired: acc.verificationRequired, verificationRequiredAt: acc.verificationRequiredAt,
270
+ verificationRequiredReason: acc.verificationRequiredReason,
271
+ verificationUrl: acc.verificationUrl,
272
+ };
273
+ })
274
+ .filter((a) => a !== null);
275
+ // Update fingerprint versions to match the current runtime version.
276
+ // Saved fingerprints may carry an older version string; this ensures
277
+ // they always reflect the latest fetched (or fallback) version.
278
+ let fingerprintVersionChanged = false;
279
+ for (const acc of this.accounts) {
280
+ if (acc.fingerprint && updateFingerprintVersion(acc.fingerprint)) {
281
+ fingerprintVersionChanged = true;
282
+ }
283
+ }
284
+ const legacyCursor = clampNonNegativeInt(stored.activeIndex, 0);
285
+ if (this.accounts.length > 0) {
286
+ const defaultIndex = legacyCursor % this.accounts.length;
287
+ this.currentAccountIndexByFamily.claude = clampNonNegativeInt(stored.activeIndexByFamily?.claude, defaultIndex) % this.accounts.length;
288
+ this.currentAccountIndexByFamily.gemini = clampNonNegativeInt(stored.activeIndexByFamily?.gemini, defaultIndex) % this.accounts.length;
289
+ this.cursorByFamily.claude = this.currentAccountIndexByFamily.claude;
290
+ this.cursorByFamily.gemini = this.currentAccountIndexByFamily.gemini;
291
+ }
292
+ // Persist updated fingerprint versions to disk
293
+ if (fingerprintVersionChanged) {
294
+ this.requestSaveToDisk();
295
+ }
296
+ // If current auth isn't in the loaded accounts, add it to the pool
297
+ if (authFallback && authParts && authParts.refreshToken) {
298
+ const hasMatching = this.accounts.some(acc => acc.parts.refreshToken === authParts.refreshToken);
299
+ if (!hasMatching) {
300
+ const now = nowMs();
301
+ const newAccount = {
302
+ index: this.accounts.length,
303
+ email: undefined,
304
+ addedAt: now,
305
+ lastUsed: 0,
306
+ parts: authParts,
307
+ access: authFallback.access,
308
+ expires: authFallback.expires,
309
+ enabled: true,
310
+ rateLimitResetTimes: {},
311
+ touchedForQuota: {},
312
+ fingerprint: generateFingerprint(),
313
+ fingerprintHistory: [],
314
+ };
315
+ this.accounts.push(newAccount);
316
+ }
317
+ }
318
+ return;
319
+ }
320
+ if (authFallback) {
321
+ const parts = parseRefreshParts(authFallback.refresh);
322
+ if (parts.refreshToken) {
323
+ const now = nowMs();
324
+ this.accounts = [
325
+ {
326
+ index: 0,
327
+ email: undefined,
328
+ addedAt: now,
329
+ lastUsed: 0,
330
+ parts,
331
+ access: authFallback.access,
332
+ expires: authFallback.expires,
333
+ enabled: true,
334
+ rateLimitResetTimes: {},
335
+ touchedForQuota: {},
336
+ },
337
+ ];
338
+ this.cursorByFamily = { claude: 0, gemini: 0 };
339
+ this.currentAccountIndexByFamily.claude = 0;
340
+ this.currentAccountIndexByFamily.gemini = 0;
341
+ }
342
+ }
343
+ }
344
+ getAccountCount() {
345
+ return this.getEnabledAccounts().length;
346
+ }
347
+ getTotalAccountCount() {
348
+ return this.accounts.length;
349
+ }
350
+ getEnabledAccounts() {
351
+ return this.accounts.filter((account) => account.enabled !== false);
352
+ }
353
+ getAccountsSnapshot() {
354
+ return this.accounts.map((a) => ({ ...a, parts: { ...a.parts }, rateLimitResetTimes: { ...a.rateLimitResetTimes } }));
355
+ }
356
+ getCurrentAccountForFamily(family) {
357
+ const currentIndex = this.currentAccountIndexByFamily[family];
358
+ if (currentIndex >= 0 && currentIndex < this.accounts.length) {
359
+ const account = this.accounts[currentIndex] ?? null;
360
+ // Only return account if it's enabled - disabled accounts should not be selected
361
+ if (account && account.enabled !== false) {
362
+ return account;
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+ markSwitched(account, reason, family) {
368
+ account.lastSwitchReason = reason;
369
+ this.currentAccountIndexByFamily[family] = account.index;
370
+ }
371
+ /**
372
+ * Check if we should show an account switch toast.
373
+ * Debounces repeated toasts for the same account.
374
+ */
375
+ shouldShowAccountToast(accountIndex, debounceMs = 30000) {
376
+ const now = nowMs();
377
+ if (accountIndex !== this.lastToastAccountIndex) {
378
+ return true;
379
+ }
380
+ return now - this.lastToastTime >= debounceMs;
381
+ }
382
+ markToastShown(accountIndex) {
383
+ this.lastToastAccountIndex = accountIndex;
384
+ this.lastToastTime = nowMs();
385
+ }
386
+ getCurrentOrNextForFamily(family, model, strategy = 'sticky', headerStyle = 'antigravity', pidOffsetEnabled = false, softQuotaThresholdPercent = 100, softQuotaCacheTtlMs = 10 * 60 * 1000) {
387
+ const quotaKey = getQuotaKey(family, headerStyle, model);
388
+ if (strategy === 'round-robin') {
389
+ const next = this.getNextForFamily(family, model, headerStyle, softQuotaThresholdPercent, softQuotaCacheTtlMs);
390
+ if (next) {
391
+ this.markTouchedForQuota(next, quotaKey);
392
+ this.currentAccountIndexByFamily[family] = next.index;
393
+ }
394
+ return next;
395
+ }
396
+ if (strategy === 'hybrid') {
397
+ const healthTracker = getHealthTracker();
398
+ const tokenTracker = getTokenTracker();
399
+ const accountsWithMetrics = this.accounts
400
+ .filter(acc => acc.enabled !== false)
401
+ .map(acc => {
402
+ clearExpiredRateLimits(acc);
403
+ return {
404
+ index: acc.index,
405
+ lastUsed: acc.lastUsed,
406
+ healthScore: healthTracker.getScore(acc.index),
407
+ isRateLimited: isRateLimitedForFamily(acc, family, model) ||
408
+ isOverSoftQuotaThreshold(acc, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model),
409
+ isCoolingDown: this.isAccountCoolingDown(acc),
410
+ };
411
+ });
412
+ // Get current account index for stickiness
413
+ const currentIndex = this.currentAccountIndexByFamily[family] ?? null;
414
+ const selectedIndex = selectHybridAccount(accountsWithMetrics, tokenTracker, currentIndex);
415
+ if (selectedIndex !== null) {
416
+ const selected = this.accounts[selectedIndex];
417
+ if (selected) {
418
+ selected.lastUsed = nowMs();
419
+ this.markTouchedForQuota(selected, quotaKey);
420
+ this.currentAccountIndexByFamily[family] = selected.index;
421
+ return selected;
422
+ }
423
+ }
424
+ }
425
+ // Fallback: sticky selection (used when hybrid finds no candidates)
426
+ // PID-based offset for multi-session distribution (opt-in)
427
+ // Different sessions (PIDs) will prefer different starting accounts
428
+ if (pidOffsetEnabled && !this.sessionOffsetApplied[family] && this.accounts.length > 1) {
429
+ const pidOffset = process.pid % this.accounts.length;
430
+ const baseIndex = this.currentAccountIndexByFamily[family] ?? 0;
431
+ const newIndex = (baseIndex + pidOffset) % this.accounts.length;
432
+ debugLogToFile(`[Account] Applying PID offset: pid=${process.pid} offset=${pidOffset} family=${family} index=${baseIndex}->${newIndex}`);
433
+ this.currentAccountIndexByFamily[family] = newIndex;
434
+ this.sessionOffsetApplied[family] = true;
435
+ }
436
+ const current = this.getCurrentAccountForFamily(family);
437
+ if (current) {
438
+ clearExpiredRateLimits(current);
439
+ const isLimitedForRequestedStyle = isRateLimitedForHeaderStyle(current, family, headerStyle, model);
440
+ const isOverThreshold = isOverSoftQuotaThreshold(current, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model);
441
+ if (!isLimitedForRequestedStyle && !isOverThreshold && !this.isAccountCoolingDown(current)) {
442
+ this.markTouchedForQuota(current, quotaKey);
443
+ return current;
444
+ }
445
+ }
446
+ const next = this.getNextForFamily(family, model, headerStyle, softQuotaThresholdPercent, softQuotaCacheTtlMs);
447
+ if (next) {
448
+ this.markTouchedForQuota(next, quotaKey);
449
+ this.currentAccountIndexByFamily[family] = next.index;
450
+ }
451
+ return next;
452
+ }
453
+ getNextForFamily(family, model, headerStyle = "antigravity", softQuotaThresholdPercent = 100, softQuotaCacheTtlMs = 10 * 60 * 1000) {
454
+ const available = this.accounts.filter((a) => {
455
+ clearExpiredRateLimits(a);
456
+ return a.enabled !== false &&
457
+ !isRateLimitedForHeaderStyle(a, family, headerStyle, model) &&
458
+ !isOverSoftQuotaThreshold(a, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model) &&
459
+ !this.isAccountCoolingDown(a);
460
+ });
461
+ if (available.length === 0) {
462
+ return null;
463
+ }
464
+ const sessionUsed = available.filter(a => this.sessionUsedAccounts.has(a.index));
465
+ const candidates = sessionUsed.length > 0 ? sessionUsed : available;
466
+ const cursor = this.cursorByFamily[family];
467
+ const account = candidates[cursor % candidates.length];
468
+ if (!account) {
469
+ return null;
470
+ }
471
+ this.cursorByFamily[family] = cursor + 1;
472
+ return account;
473
+ }
474
+ markRateLimited(account, retryAfterMs, family, headerStyle = "antigravity", model) {
475
+ const key = getQuotaKey(family, headerStyle, model);
476
+ account.rateLimitResetTimes[key] = nowMs() + retryAfterMs;
477
+ }
478
+ /**
479
+ * Mark an account as used after a successful API request.
480
+ * This updates the lastUsed timestamp for freshness calculations.
481
+ * Should be called AFTER request completion, not during account selection.
482
+ */
483
+ markAccountUsed(accountIndex) {
484
+ const account = this.accounts.find(a => a.index === accountIndex);
485
+ if (account) {
486
+ account.lastUsed = nowMs();
487
+ }
488
+ }
489
+ recordSessionUsage(accountIndex) {
490
+ this.sessionUsedAccounts.add(accountIndex);
491
+ }
492
+ wasUsedInSession(accountIndex) {
493
+ return this.sessionUsedAccounts.has(accountIndex);
494
+ }
495
+ shouldProactivelyRotate(family, model, thresholdPercent, cacheTtlMs) {
496
+ if (thresholdPercent <= 0)
497
+ return false;
498
+ const current = this.getCurrentAccountForFamily(family);
499
+ if (!current || !current.cachedQuota || current.cachedQuotaUpdatedAt == null)
500
+ return false;
501
+ const age = nowMs() - current.cachedQuotaUpdatedAt;
502
+ if (age > cacheTtlMs)
503
+ return false;
504
+ const quotaGroup = resolveQuotaGroup(family, model);
505
+ const groupData = current.cachedQuota[quotaGroup];
506
+ if (groupData?.remainingFraction == null)
507
+ return false;
508
+ const remainingPercent = Math.max(0, Math.min(100, groupData.remainingFraction * 100));
509
+ return remainingPercent < thresholdPercent;
510
+ }
511
+ proactivelyRotateForFamily(family, model, headerStyle, softQuotaThresholdPercent, softQuotaCacheTtlMs) {
512
+ const currentIndex = this.currentAccountIndexByFamily[family];
513
+ const candidates = this.accounts.filter(acc => {
514
+ if (acc.enabled === false)
515
+ return false;
516
+ if (acc.index === currentIndex)
517
+ return false;
518
+ clearExpiredRateLimits(acc);
519
+ if (isRateLimitedForHeaderStyle(acc, family, headerStyle, model))
520
+ return false;
521
+ if (isOverSoftQuotaThreshold(acc, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model))
522
+ return false;
523
+ if (this.isAccountCoolingDown(acc))
524
+ return false;
525
+ return true;
526
+ });
527
+ if (candidates.length === 0)
528
+ return null;
529
+ const warmCandidates = candidates.filter(a => this.sessionUsedAccounts.has(a.index));
530
+ const pool = warmCandidates.length > 0 ? warmCandidates : candidates;
531
+ const quotaGroup = resolveQuotaGroup(family, model);
532
+ pool.sort((a, b) => {
533
+ const aRemaining = a.cachedQuota?.[quotaGroup]?.remainingFraction ?? 0;
534
+ const bRemaining = b.cachedQuota?.[quotaGroup]?.remainingFraction ?? 0;
535
+ return bRemaining - aRemaining;
536
+ });
537
+ const selected = pool[0];
538
+ if (!selected)
539
+ return null;
540
+ const quotaKey = getQuotaKey(family, headerStyle, model);
541
+ this.markTouchedForQuota(selected, quotaKey);
542
+ this.currentAccountIndexByFamily[family] = selected.index;
543
+ return selected;
544
+ }
545
+ markRateLimitedWithReason(account, family, headerStyle, model, reason, retryAfterMs, failureTtlMs = 3600_000) {
546
+ const now = nowMs();
547
+ // TTL-based reset: if last failure was more than failureTtlMs ago, reset count
548
+ if (account.lastFailureTime !== undefined && (now - account.lastFailureTime) > failureTtlMs) {
549
+ account.consecutiveFailures = 0;
550
+ }
551
+ const failures = (account.consecutiveFailures ?? 0) + 1;
552
+ account.consecutiveFailures = failures;
553
+ account.lastFailureTime = now;
554
+ const backoffMs = calculateBackoffMs(reason, failures - 1, retryAfterMs);
555
+ const key = getQuotaKey(family, headerStyle, model);
556
+ account.rateLimitResetTimes[key] = now + backoffMs;
557
+ return backoffMs;
558
+ }
559
+ markRequestSuccess(account) {
560
+ if (account.consecutiveFailures) {
561
+ account.consecutiveFailures = 0;
562
+ }
563
+ }
564
+ clearAllRateLimitsForFamily(family, model) {
565
+ for (const account of this.accounts) {
566
+ if (family === "claude") {
567
+ delete account.rateLimitResetTimes.claude;
568
+ }
569
+ else {
570
+ const antigravityKey = getQuotaKey(family, "antigravity", model);
571
+ const cliKey = getQuotaKey(family, "gemini-cli", model);
572
+ delete account.rateLimitResetTimes[antigravityKey];
573
+ delete account.rateLimitResetTimes[cliKey];
574
+ }
575
+ account.consecutiveFailures = 0;
576
+ }
577
+ }
578
+ shouldTryOptimisticReset(family, model) {
579
+ const minWaitMs = this.getMinWaitTimeForFamily(family, model);
580
+ return minWaitMs > 0 && minWaitMs <= 2_000;
581
+ }
582
+ markAccountCoolingDown(account, cooldownMs, reason) {
583
+ account.coolingDownUntil = nowMs() + cooldownMs;
584
+ account.cooldownReason = reason;
585
+ }
586
+ isAccountCoolingDown(account) {
587
+ if (account.coolingDownUntil === undefined) {
588
+ return false;
589
+ }
590
+ if (nowMs() >= account.coolingDownUntil) {
591
+ this.clearAccountCooldown(account);
592
+ return false;
593
+ }
594
+ return true;
595
+ }
596
+ clearAccountCooldown(account) {
597
+ delete account.coolingDownUntil;
598
+ delete account.cooldownReason;
599
+ }
600
+ getAccountCooldownReason(account) {
601
+ return this.isAccountCoolingDown(account) ? account.cooldownReason : undefined;
602
+ }
603
+ markTouchedForQuota(account, quotaKey) {
604
+ account.touchedForQuota[quotaKey] = nowMs();
605
+ }
606
+ isFreshForQuota(account, quotaKey) {
607
+ const touchedAt = account.touchedForQuota[quotaKey];
608
+ if (!touchedAt)
609
+ return true;
610
+ const resetTime = account.rateLimitResetTimes[quotaKey];
611
+ if (resetTime && touchedAt < resetTime)
612
+ return true;
613
+ return false;
614
+ }
615
+ getFreshAccountsForQuota(quotaKey, family, model) {
616
+ return this.accounts.filter(acc => {
617
+ clearExpiredRateLimits(acc);
618
+ return acc.enabled !== false &&
619
+ this.isFreshForQuota(acc, quotaKey) &&
620
+ !isRateLimitedForFamily(acc, family, model) &&
621
+ !this.isAccountCoolingDown(acc);
622
+ });
623
+ }
624
+ isRateLimitedForHeaderStyle(account, family, headerStyle, model) {
625
+ return isRateLimitedForHeaderStyle(account, family, headerStyle, model);
626
+ }
627
+ getAvailableHeaderStyle(account, family, model) {
628
+ clearExpiredRateLimits(account);
629
+ if (family === "claude") {
630
+ return isRateLimitedForHeaderStyle(account, family, "antigravity") ? null : "antigravity";
631
+ }
632
+ if (!isRateLimitedForHeaderStyle(account, family, "antigravity", model)) {
633
+ return "antigravity";
634
+ }
635
+ if (!isRateLimitedForHeaderStyle(account, family, "gemini-cli", model)) {
636
+ return "gemini-cli";
637
+ }
638
+ return null;
639
+ }
640
+ /**
641
+ * Check if any OTHER account has antigravity quota available for the given family/model.
642
+ *
643
+ * Used to determine whether to switch accounts vs fall back to gemini-cli:
644
+ * - If true: Switch to another account (preserve antigravity priority)
645
+ * - If false: All accounts exhausted antigravity, safe to fall back to gemini-cli
646
+ *
647
+ * @param currentAccountIndex - Index of the current account (will be excluded from check)
648
+ * @param family - Model family ("gemini" or "claude")
649
+ * @param model - Optional model name for model-specific rate limits
650
+ * @returns true if any other enabled, non-cooling-down account has antigravity available
651
+ */
652
+ hasOtherAccountWithAntigravityAvailable(currentAccountIndex, family, model) {
653
+ // Claude has no gemini-cli fallback - always return false
654
+ // (This method is only relevant for Gemini's dual quota pools)
655
+ if (family === "claude") {
656
+ return false;
657
+ }
658
+ return this.accounts.some(acc => {
659
+ // Skip current account
660
+ if (acc.index === currentAccountIndex) {
661
+ return false;
662
+ }
663
+ // Skip disabled accounts
664
+ if (acc.enabled === false) {
665
+ return false;
666
+ }
667
+ // Skip cooling down accounts
668
+ if (this.isAccountCoolingDown(acc)) {
669
+ return false;
670
+ }
671
+ // Clear expired rate limits before checking
672
+ clearExpiredRateLimits(acc);
673
+ // Check if antigravity is available for this account
674
+ return !isRateLimitedForHeaderStyle(acc, family, "antigravity", model);
675
+ });
676
+ }
677
+ setAccountEnabled(accountIndex, enabled) {
678
+ const account = this.accounts[accountIndex];
679
+ if (!account) {
680
+ return false;
681
+ }
682
+ account.enabled = enabled;
683
+ if (!enabled) {
684
+ for (const family of Object.keys(this.currentAccountIndexByFamily)) {
685
+ if (this.currentAccountIndexByFamily[family] === accountIndex) {
686
+ const next = this.accounts.find((a, i) => i !== accountIndex && a.enabled !== false);
687
+ this.currentAccountIndexByFamily[family] = next?.index ?? -1;
688
+ }
689
+ }
690
+ }
691
+ this.requestSaveToDisk();
692
+ return true;
693
+ }
694
+ markAccountVerificationRequired(accountIndex, reason, verifyUrl) {
695
+ const account = this.accounts[accountIndex];
696
+ if (!account) {
697
+ return false;
698
+ }
699
+ account.verificationRequired = true;
700
+ account.verificationRequiredAt = nowMs();
701
+ account.verificationRequiredReason = reason?.trim() || undefined;
702
+ const normalizedVerifyUrl = verifyUrl?.trim();
703
+ if (normalizedVerifyUrl) {
704
+ account.verificationUrl = normalizedVerifyUrl;
705
+ }
706
+ if (account.enabled !== false) {
707
+ this.setAccountEnabled(accountIndex, false);
708
+ }
709
+ else {
710
+ this.requestSaveToDisk();
711
+ }
712
+ return true;
713
+ }
714
+ clearAccountVerificationRequired(accountIndex, enableAccount = false) {
715
+ const account = this.accounts[accountIndex];
716
+ if (!account) {
717
+ return false;
718
+ }
719
+ const wasVerificationRequired = account.verificationRequired === true;
720
+ const hadMetadata = (account.verificationRequiredAt !== undefined ||
721
+ account.verificationRequiredReason !== undefined ||
722
+ account.verificationUrl !== undefined);
723
+ account.verificationRequired = false;
724
+ account.verificationRequiredAt = undefined;
725
+ account.verificationRequiredReason = undefined;
726
+ account.verificationUrl = undefined;
727
+ if (enableAccount && wasVerificationRequired && account.enabled === false) {
728
+ this.setAccountEnabled(accountIndex, true);
729
+ }
730
+ else if (wasVerificationRequired || hadMetadata) {
731
+ this.requestSaveToDisk();
732
+ }
733
+ return true;
734
+ }
735
+ removeAccountByIndex(accountIndex) {
736
+ if (accountIndex < 0 || accountIndex >= this.accounts.length) {
737
+ return false;
738
+ }
739
+ const account = this.accounts[accountIndex];
740
+ if (!account) {
741
+ return false;
742
+ }
743
+ return this.removeAccount(account);
744
+ }
745
+ removeAccount(account) {
746
+ const idx = this.accounts.indexOf(account);
747
+ if (idx < 0) {
748
+ return false;
749
+ }
750
+ this.accounts.splice(idx, 1);
751
+ this.accounts.forEach((acc, index) => {
752
+ acc.index = index;
753
+ });
754
+ if (this.accounts.length === 0) {
755
+ this.cursorByFamily = { claude: 0, gemini: 0 };
756
+ this.currentAccountIndexByFamily.claude = -1;
757
+ this.currentAccountIndexByFamily.gemini = -1;
758
+ return true;
759
+ }
760
+ for (const family of ["claude", "gemini"]) {
761
+ if (this.cursorByFamily[family] > idx) {
762
+ this.cursorByFamily[family] -= 1;
763
+ }
764
+ this.cursorByFamily[family] = this.cursorByFamily[family] % this.accounts.length;
765
+ if (this.currentAccountIndexByFamily[family] > idx) {
766
+ this.currentAccountIndexByFamily[family] -= 1;
767
+ }
768
+ if (this.currentAccountIndexByFamily[family] >= this.accounts.length) {
769
+ this.currentAccountIndexByFamily[family] = -1;
770
+ }
771
+ }
772
+ return true;
773
+ }
774
+ updateFromAuth(account, auth) {
775
+ const parts = parseRefreshParts(auth.refresh);
776
+ // Preserve existing projectId/managedProjectId if not in the new parts
777
+ account.parts = {
778
+ ...parts,
779
+ projectId: parts.projectId ?? account.parts.projectId,
780
+ managedProjectId: parts.managedProjectId ?? account.parts.managedProjectId,
781
+ };
782
+ account.access = auth.access;
783
+ account.expires = auth.expires;
784
+ }
785
+ toAuthDetails(account) {
786
+ return {
787
+ type: "oauth",
788
+ refresh: formatRefreshParts(account.parts),
789
+ access: account.access,
790
+ expires: account.expires,
791
+ };
792
+ }
793
+ getMinWaitTimeForFamily(family, model, headerStyle, strict) {
794
+ const available = this.accounts.filter((a) => {
795
+ clearExpiredRateLimits(a);
796
+ return a.enabled !== false && (strict && headerStyle
797
+ ? !isRateLimitedForHeaderStyle(a, family, headerStyle, model)
798
+ : !isRateLimitedForFamily(a, family, model));
799
+ });
800
+ if (available.length > 0) {
801
+ return 0;
802
+ }
803
+ const waitTimes = [];
804
+ for (const a of this.accounts) {
805
+ if (family === "claude") {
806
+ const t = a.rateLimitResetTimes.claude;
807
+ if (t !== undefined)
808
+ waitTimes.push(Math.max(0, t - nowMs()));
809
+ }
810
+ else if (strict && headerStyle) {
811
+ const key = getQuotaKey(family, headerStyle, model);
812
+ const t = a.rateLimitResetTimes[key];
813
+ if (t !== undefined)
814
+ waitTimes.push(Math.max(0, t - nowMs()));
815
+ }
816
+ else {
817
+ // For Gemini, account becomes available when EITHER pool expires for this model/family
818
+ const antigravityKey = getQuotaKey(family, "antigravity", model);
819
+ const cliKey = getQuotaKey(family, "gemini-cli", model);
820
+ const t1 = a.rateLimitResetTimes[antigravityKey];
821
+ const t2 = a.rateLimitResetTimes[cliKey];
822
+ const accountWait = Math.min(t1 !== undefined ? Math.max(0, t1 - nowMs()) : Infinity, t2 !== undefined ? Math.max(0, t2 - nowMs()) : Infinity);
823
+ if (accountWait !== Infinity)
824
+ waitTimes.push(accountWait);
825
+ }
826
+ }
827
+ return waitTimes.length > 0 ? Math.min(...waitTimes) : 0;
828
+ }
829
+ getAccounts() {
830
+ return [...this.accounts];
831
+ }
832
+ async saveToDisk() {
833
+ const claudeIndex = Math.max(0, this.currentAccountIndexByFamily.claude);
834
+ const geminiIndex = Math.max(0, this.currentAccountIndexByFamily.gemini);
835
+ const storage = {
836
+ version: 4,
837
+ accounts: this.accounts.map((a) => ({
838
+ email: a.email,
839
+ refreshToken: a.parts.refreshToken,
840
+ projectId: a.parts.projectId,
841
+ managedProjectId: a.parts.managedProjectId,
842
+ addedAt: a.addedAt,
843
+ lastUsed: a.lastUsed,
844
+ enabled: a.enabled,
845
+ rateLimitResetTimes: Object.keys(a.rateLimitResetTimes).length > 0 ? a.rateLimitResetTimes : undefined,
846
+ fingerprint: a.fingerprint,
847
+ fingerprintHistory: a.fingerprintHistory?.length ? a.fingerprintHistory : undefined,
848
+ cachedQuota: a.cachedQuota && Object.keys(a.cachedQuota).length > 0 ? a.cachedQuota : undefined,
849
+ cachedQuotaUpdatedAt: a.cachedQuotaUpdatedAt,
850
+ dailyRequestCounts: a.dailyRequestCounts,
851
+ verificationRequired: a.verificationRequired,
852
+ verificationRequiredAt: a.verificationRequiredAt,
853
+ verificationRequiredReason: a.verificationRequiredReason,
854
+ verificationUrl: a.verificationUrl,
855
+ })), activeIndex: claudeIndex,
856
+ activeIndexByFamily: {
857
+ claude: claudeIndex,
858
+ gemini: geminiIndex,
859
+ },
860
+ };
861
+ await saveAccounts(storage);
862
+ }
863
+ requestSaveToDisk() {
864
+ if (this.savePending) {
865
+ return;
866
+ }
867
+ this.savePending = true;
868
+ this.saveTimeout = setTimeout(() => {
869
+ void this.executeSave();
870
+ }, 1000);
871
+ }
872
+ async flushSaveToDisk() {
873
+ if (!this.savePending) {
874
+ return;
875
+ }
876
+ return new Promise((resolve, reject) => {
877
+ this.savePromiseResolvers.push({ resolve, reject });
878
+ });
879
+ }
880
+ async executeSave() {
881
+ this.savePending = false;
882
+ this.saveTimeout = null;
883
+ const resolvers = this.savePromiseResolvers;
884
+ this.savePromiseResolvers = [];
885
+ try {
886
+ await this.saveToDisk();
887
+ for (const { resolve } of resolvers) {
888
+ resolve();
889
+ }
890
+ }
891
+ catch (error) {
892
+ if (isStorageLockContention(error)) {
893
+ debugLogToFile(`[Account] Skipped account-state persist because another plugin instance holds the storage lock: ${String(error)}`);
894
+ for (const { resolve } of resolvers) {
895
+ resolve();
896
+ }
897
+ return;
898
+ }
899
+ console.warn("[antigravity] Failed to persist account state to disk:", String(error));
900
+ for (const { reject } of resolvers) {
901
+ reject(error);
902
+ }
903
+ }
904
+ }
905
+ // ========== Fingerprint Management ==========
906
+ /**
907
+ * Regenerate fingerprint for an account, saving the old one to history.
908
+ * @param accountIndex - Index of the account to regenerate fingerprint for
909
+ * @returns The new fingerprint, or null if account not found
910
+ */
911
+ regenerateAccountFingerprint(accountIndex) {
912
+ const account = this.accounts[accountIndex];
913
+ if (!account)
914
+ return null;
915
+ // Save current fingerprint to history if it exists
916
+ if (account.fingerprint) {
917
+ const historyEntry = {
918
+ fingerprint: account.fingerprint,
919
+ timestamp: nowMs(),
920
+ reason: 'regenerated',
921
+ };
922
+ if (!account.fingerprintHistory) {
923
+ account.fingerprintHistory = [];
924
+ }
925
+ // Add to beginning of history (most recent first)
926
+ account.fingerprintHistory.unshift(historyEntry);
927
+ // Trim to max history size
928
+ if (account.fingerprintHistory.length > MAX_FINGERPRINT_HISTORY) {
929
+ account.fingerprintHistory = account.fingerprintHistory.slice(0, MAX_FINGERPRINT_HISTORY);
930
+ }
931
+ }
932
+ // Generate and assign new fingerprint
933
+ account.fingerprint = generateFingerprint();
934
+ this.requestSaveToDisk();
935
+ return account.fingerprint;
936
+ }
937
+ /**
938
+ * Restore a fingerprint from history for an account.
939
+ * @param accountIndex - Index of the account
940
+ * @param historyIndex - Index in the fingerprint history to restore from (0 = most recent)
941
+ * @returns The restored fingerprint, or null if account/history not found
942
+ */
943
+ restoreAccountFingerprint(accountIndex, historyIndex) {
944
+ const account = this.accounts[accountIndex];
945
+ if (!account)
946
+ return null;
947
+ const history = account.fingerprintHistory;
948
+ if (!history || historyIndex < 0 || historyIndex >= history.length) {
949
+ return null;
950
+ }
951
+ // Capture the fingerprint to restore BEFORE modifying history
952
+ const fingerprintToRestore = history[historyIndex].fingerprint;
953
+ // Save current fingerprint to history before restoring (if it exists)
954
+ if (account.fingerprint) {
955
+ const historyEntry = {
956
+ fingerprint: account.fingerprint,
957
+ timestamp: nowMs(),
958
+ reason: 'restored',
959
+ };
960
+ account.fingerprintHistory.unshift(historyEntry);
961
+ // Trim to max history size
962
+ if (account.fingerprintHistory.length > MAX_FINGERPRINT_HISTORY) {
963
+ account.fingerprintHistory = account.fingerprintHistory.slice(0, MAX_FINGERPRINT_HISTORY);
964
+ }
965
+ }
966
+ // Restore the fingerprint
967
+ account.fingerprint = { ...fingerprintToRestore, createdAt: nowMs() };
968
+ this.requestSaveToDisk();
969
+ return account.fingerprint;
970
+ }
971
+ /**
972
+ * Get fingerprint history for an account.
973
+ * @param accountIndex - Index of the account
974
+ * @returns Array of fingerprint versions, or empty array if not found
975
+ */
976
+ getAccountFingerprintHistory(accountIndex) {
977
+ const account = this.accounts[accountIndex];
978
+ if (!account || !account.fingerprintHistory) {
979
+ return [];
980
+ }
981
+ return [...account.fingerprintHistory];
982
+ }
983
+ updateQuotaCache(accountIndex, quotaGroups) {
984
+ const account = this.accounts[accountIndex];
985
+ if (account) {
986
+ account.cachedQuota = quotaGroups;
987
+ account.cachedQuotaUpdatedAt = nowMs();
988
+ }
989
+ }
990
+ /**
991
+ * Record a successful API request for an account.
992
+ * Tracks per model family with daily reset.
993
+ */
994
+ recordRequest(accountIndex, family) {
995
+ const account = this.accounts[accountIndex];
996
+ if (!account)
997
+ return;
998
+ const today = new Date().toISOString().slice(0, 10);
999
+ if (!account.dailyRequestCounts || account.dailyRequestCounts.date !== today) {
1000
+ account.dailyRequestCounts = { date: today, claude: 0, gemini: 0 };
1001
+ }
1002
+ account.dailyRequestCounts[family]++;
1003
+ account.lastUsed = nowMs();
1004
+ // Also track for session
1005
+ this.recordSessionRequest(accountIndex, family);
1006
+ }
1007
+ /**
1008
+ * Get request counts for an account for today.
1009
+ */
1010
+ getDailyRequestCounts(accountIndex) {
1011
+ const account = this.accounts[accountIndex];
1012
+ if (!account?.dailyRequestCounts)
1013
+ return null;
1014
+ const today = new Date().toISOString().slice(0, 10);
1015
+ if (account.dailyRequestCounts.date !== today)
1016
+ return null;
1017
+ return { ...account.dailyRequestCounts };
1018
+ }
1019
+ /**
1020
+ * Get total daily request counts across all accounts for a model family.
1021
+ */
1022
+ getTotalDailyRequests(family) {
1023
+ const today = new Date().toISOString().slice(0, 10);
1024
+ let total = 0;
1025
+ for (const account of this.accounts) {
1026
+ if (account.dailyRequestCounts?.date === today) {
1027
+ total += account.dailyRequestCounts[family];
1028
+ }
1029
+ }
1030
+ return total;
1031
+ }
1032
+ /**
1033
+ * Get a summary of daily request distribution across accounts.
1034
+ * Returns accounts sorted by request count (descending).
1035
+ */
1036
+ getDailyRequestSummary(family) {
1037
+ const today = new Date().toISOString().slice(0, 10);
1038
+ const result = [];
1039
+ for (const account of this.accounts) {
1040
+ const count = account.dailyRequestCounts?.date === today
1041
+ ? account.dailyRequestCounts[family]
1042
+ : 0;
1043
+ if (count > 0) {
1044
+ result.push({ index: account.index, email: account.email, count });
1045
+ }
1046
+ }
1047
+ return result.sort((a, b) => b.count - a.count);
1048
+ }
1049
+ /**
1050
+ * Record a request for the current session (in-memory only).
1051
+ */
1052
+ recordSessionRequest(accountIndex, family) {
1053
+ const key = String(accountIndex);
1054
+ const current = this.sessionRequestCounts.get(key) ?? { claude: 0, gemini: 0 };
1055
+ current[family]++;
1056
+ this.sessionRequestCounts.set(key, current);
1057
+ }
1058
+ /**
1059
+ * Get a summary of the current session's request usage.
1060
+ */
1061
+ getSessionSummary() {
1062
+ const durationMs = Date.now() - this.sessionStartTime;
1063
+ const durationMinutes = Math.round(durationMs / 60000);
1064
+ const durationHours = durationMs / 3600000;
1065
+ let totalClaude = 0;
1066
+ let totalGemini = 0;
1067
+ const perAccount = [];
1068
+ for (const [key, counts] of this.sessionRequestCounts) {
1069
+ const idx = Number(key);
1070
+ const account = this.accounts[idx];
1071
+ totalClaude += counts.claude;
1072
+ totalGemini += counts.gemini;
1073
+ if (counts.claude > 0 || counts.gemini > 0) {
1074
+ perAccount.push({ index: idx, email: account?.email, claude: counts.claude, gemini: counts.gemini });
1075
+ }
1076
+ }
1077
+ const totalRequests = totalClaude + totalGemini;
1078
+ const requestsPerHour = durationHours > 0 ? Math.round(totalRequests / durationHours) : 0;
1079
+ return {
1080
+ durationMinutes,
1081
+ totalClaude,
1082
+ totalGemini,
1083
+ requestsPerHour,
1084
+ accountsUsed: perAccount.length,
1085
+ perAccount: perAccount.sort((a, b) => (b.claude + b.gemini) - (a.claude + a.gemini)),
1086
+ };
1087
+ }
1088
+ isAccountOverSoftQuota(account, family, thresholdPercent, cacheTtlMs, model) {
1089
+ return isOverSoftQuotaThreshold(account, family, thresholdPercent, cacheTtlMs, model);
1090
+ }
1091
+ getAccountsForQuotaCheck() {
1092
+ return this.accounts.map((a) => ({
1093
+ email: a.email,
1094
+ refreshToken: a.parts.refreshToken,
1095
+ projectId: a.parts.projectId,
1096
+ managedProjectId: a.parts.managedProjectId,
1097
+ addedAt: a.addedAt,
1098
+ lastUsed: a.lastUsed,
1099
+ enabled: a.enabled,
1100
+ }));
1101
+ }
1102
+ getOldestQuotaCacheAge() {
1103
+ let oldest = null;
1104
+ for (const acc of this.accounts) {
1105
+ if (acc.enabled === false)
1106
+ continue;
1107
+ if (acc.cachedQuotaUpdatedAt == null)
1108
+ return null;
1109
+ const age = nowMs() - acc.cachedQuotaUpdatedAt;
1110
+ if (oldest === null || age > oldest)
1111
+ oldest = age;
1112
+ }
1113
+ return oldest;
1114
+ }
1115
+ areAllAccountsOverSoftQuota(family, thresholdPercent, cacheTtlMs, model) {
1116
+ if (thresholdPercent >= 100)
1117
+ return false;
1118
+ const enabled = this.accounts.filter(a => a.enabled !== false);
1119
+ if (enabled.length === 0)
1120
+ return false;
1121
+ return enabled.every(a => isOverSoftQuotaThreshold(a, family, thresholdPercent, cacheTtlMs, model));
1122
+ }
1123
+ /**
1124
+ * Get minimum wait time until any account's soft quota resets.
1125
+ * Returns 0 if any account is available (not over threshold).
1126
+ * Returns the minimum resetTime across all over-threshold accounts.
1127
+ * Returns null if no resetTime data is available.
1128
+ */
1129
+ getMinWaitTimeForSoftQuota(family, thresholdPercent, cacheTtlMs, model) {
1130
+ if (thresholdPercent >= 100)
1131
+ return 0;
1132
+ const enabled = this.accounts.filter(a => a.enabled !== false);
1133
+ if (enabled.length === 0)
1134
+ return null;
1135
+ // If any account is available (not over threshold), no wait needed
1136
+ const available = enabled.filter(a => !isOverSoftQuotaThreshold(a, family, thresholdPercent, cacheTtlMs, model));
1137
+ if (available.length > 0)
1138
+ return 0;
1139
+ // All accounts are over threshold - find earliest reset time
1140
+ // For gemini family, we MUST have the model to distinguish pro vs flash quotas.
1141
+ // Fail-open (return null = no wait info) if model is missing to avoid blocking on wrong quota.
1142
+ if (!model && family !== "claude")
1143
+ return null;
1144
+ const quotaGroup = resolveQuotaGroup(family, model);
1145
+ const now = nowMs();
1146
+ const waitTimes = [];
1147
+ for (const acc of enabled) {
1148
+ const groupData = acc.cachedQuota?.[quotaGroup];
1149
+ if (groupData?.resetTime) {
1150
+ const resetTimestamp = Date.parse(groupData.resetTime);
1151
+ if (Number.isFinite(resetTimestamp)) {
1152
+ waitTimes.push(Math.max(0, resetTimestamp - now));
1153
+ }
1154
+ }
1155
+ }
1156
+ if (waitTimes.length === 0)
1157
+ return null;
1158
+ const minWait = Math.min(...waitTimes);
1159
+ // Treat 0 as stale cache (resetTime in the past) → fail-open to avoid spin loop
1160
+ return minWait === 0 ? null : minWait;
1161
+ }
1162
+ }
1163
+ //# sourceMappingURL=accounts.js.map