antigravity-auth 1.6.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 (256) hide show
  1. package/README.md +61 -0
  2. package/dist/antigravity/oauth.d.ts +30 -0
  3. package/dist/antigravity/oauth.js +170 -0
  4. package/dist/claude/login.d.ts +7 -0
  5. package/dist/claude/login.js +480 -0
  6. package/dist/claude/menu-helpers.d.ts +22 -0
  7. package/dist/claude/menu-helpers.js +281 -0
  8. package/dist/claude/proxy-manager.d.ts +11 -0
  9. package/dist/claude/proxy-manager.js +129 -0
  10. package/dist/claude/proxy.d.ts +1 -0
  11. package/dist/claude/proxy.js +733 -0
  12. package/dist/constants.d.ts +138 -0
  13. package/dist/constants.js +216 -0
  14. package/dist/hooks/auto-update-checker/cache.d.ts +2 -0
  15. package/dist/hooks/auto-update-checker/cache.js +70 -0
  16. package/dist/hooks/auto-update-checker/checker.d.ts +15 -0
  17. package/dist/hooks/auto-update-checker/checker.js +233 -0
  18. package/dist/hooks/auto-update-checker/constants.d.ts +8 -0
  19. package/dist/hooks/auto-update-checker/constants.js +22 -0
  20. package/dist/hooks/auto-update-checker/index.d.ts +33 -0
  21. package/dist/hooks/auto-update-checker/index.js +121 -0
  22. package/dist/hooks/auto-update-checker/logging.d.ts +2 -0
  23. package/dist/hooks/auto-update-checker/logging.js +8 -0
  24. package/dist/hooks/auto-update-checker/types.d.ts +24 -0
  25. package/dist/hooks/auto-update-checker/types.js +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.js +5 -0
  28. package/dist/opencode/hooks/auto-update-checker/cache.d.ts +2 -0
  29. package/dist/opencode/hooks/auto-update-checker/cache.js +70 -0
  30. package/dist/opencode/hooks/auto-update-checker/checker.d.ts +15 -0
  31. package/dist/opencode/hooks/auto-update-checker/checker.js +233 -0
  32. package/dist/opencode/hooks/auto-update-checker/constants.d.ts +8 -0
  33. package/dist/opencode/hooks/auto-update-checker/constants.js +22 -0
  34. package/dist/opencode/hooks/auto-update-checker/index.d.ts +33 -0
  35. package/dist/opencode/hooks/auto-update-checker/index.js +121 -0
  36. package/dist/opencode/hooks/auto-update-checker/logging.d.ts +2 -0
  37. package/dist/opencode/hooks/auto-update-checker/logging.js +8 -0
  38. package/dist/opencode/hooks/auto-update-checker/types.d.ts +24 -0
  39. package/dist/opencode/hooks/auto-update-checker/types.js +1 -0
  40. package/dist/opencode/plugin.d.ts +29 -0
  41. package/dist/opencode/plugin.js +2954 -0
  42. package/dist/plugin/accounts.d.ts +173 -0
  43. package/dist/plugin/accounts.js +966 -0
  44. package/dist/plugin/auth.d.ts +20 -0
  45. package/dist/plugin/auth.js +44 -0
  46. package/dist/plugin/cache/index.d.ts +4 -0
  47. package/dist/plugin/cache/index.js +4 -0
  48. package/dist/plugin/cache/signature-cache.d.ts +110 -0
  49. package/dist/plugin/cache/signature-cache.js +347 -0
  50. package/dist/plugin/cache.d.ts +43 -0
  51. package/dist/plugin/cache.js +180 -0
  52. package/dist/plugin/cli.d.ts +26 -0
  53. package/dist/plugin/cli.js +126 -0
  54. package/dist/plugin/config/index.d.ts +15 -0
  55. package/dist/plugin/config/index.js +15 -0
  56. package/dist/plugin/config/loader.d.ts +38 -0
  57. package/dist/plugin/config/loader.js +150 -0
  58. package/dist/plugin/config/models.d.ts +26 -0
  59. package/dist/plugin/config/models.js +95 -0
  60. package/dist/plugin/config/schema.d.ts +144 -0
  61. package/dist/plugin/config/schema.js +458 -0
  62. package/dist/plugin/config/updater.d.ts +76 -0
  63. package/dist/plugin/config/updater.js +205 -0
  64. package/dist/plugin/core/streaming/index.d.ts +2 -0
  65. package/dist/plugin/core/streaming/index.js +2 -0
  66. package/dist/plugin/core/streaming/transformer.d.ts +9 -0
  67. package/dist/plugin/core/streaming/transformer.js +301 -0
  68. package/dist/plugin/core/streaming/types.d.ts +28 -0
  69. package/dist/plugin/core/streaming/types.js +1 -0
  70. package/dist/plugin/debug.d.ts +93 -0
  71. package/dist/plugin/debug.js +375 -0
  72. package/dist/plugin/errors.d.ts +27 -0
  73. package/dist/plugin/errors.js +41 -0
  74. package/dist/plugin/fingerprint.d.ts +69 -0
  75. package/dist/plugin/fingerprint.js +137 -0
  76. package/dist/plugin/image-saver.d.ts +24 -0
  77. package/dist/plugin/image-saver.js +78 -0
  78. package/dist/plugin/logger.d.ts +35 -0
  79. package/dist/plugin/logger.js +67 -0
  80. package/dist/plugin/logging-utils.d.ts +22 -0
  81. package/dist/plugin/logging-utils.js +91 -0
  82. package/dist/plugin/project.d.ts +32 -0
  83. package/dist/plugin/project.js +229 -0
  84. package/dist/plugin/quota.d.ts +34 -0
  85. package/dist/plugin/quota.js +261 -0
  86. package/dist/plugin/recovery/constants.d.ts +21 -0
  87. package/dist/plugin/recovery/constants.js +42 -0
  88. package/dist/plugin/recovery/index.d.ts +11 -0
  89. package/dist/plugin/recovery/index.js +11 -0
  90. package/dist/plugin/recovery/storage.d.ts +23 -0
  91. package/dist/plugin/recovery/storage.js +340 -0
  92. package/dist/plugin/recovery/types.d.ts +115 -0
  93. package/dist/plugin/recovery/types.js +6 -0
  94. package/dist/plugin/recovery.d.ts +60 -0
  95. package/dist/plugin/recovery.js +360 -0
  96. package/dist/plugin/refresh-queue.d.ts +99 -0
  97. package/dist/plugin/refresh-queue.js +235 -0
  98. package/dist/plugin/request-helpers.d.ts +281 -0
  99. package/dist/plugin/request-helpers.js +2200 -0
  100. package/dist/plugin/request.d.ts +110 -0
  101. package/dist/plugin/request.js +1489 -0
  102. package/dist/plugin/rotation.d.ts +182 -0
  103. package/dist/plugin/rotation.js +364 -0
  104. package/dist/plugin/search.d.ts +31 -0
  105. package/dist/plugin/search.js +185 -0
  106. package/dist/plugin/server.d.ts +22 -0
  107. package/dist/plugin/server.js +306 -0
  108. package/dist/plugin/storage.d.ts +136 -0
  109. package/dist/plugin/storage.js +599 -0
  110. package/dist/plugin/stores/signature-store.d.ts +4 -0
  111. package/dist/plugin/stores/signature-store.js +24 -0
  112. package/dist/plugin/thinking-recovery.d.ts +89 -0
  113. package/dist/plugin/thinking-recovery.js +289 -0
  114. package/dist/plugin/token.d.ts +18 -0
  115. package/dist/plugin/token.js +127 -0
  116. package/dist/plugin/transform/claude.d.ts +79 -0
  117. package/dist/plugin/transform/claude.js +256 -0
  118. package/dist/plugin/transform/cross-model-sanitizer.d.ts +34 -0
  119. package/dist/plugin/transform/cross-model-sanitizer.js +224 -0
  120. package/dist/plugin/transform/gemini.d.ts +132 -0
  121. package/dist/plugin/transform/gemini.js +659 -0
  122. package/dist/plugin/transform/index.d.ts +14 -0
  123. package/dist/plugin/transform/index.js +9 -0
  124. package/dist/plugin/transform/model-resolver.d.ts +98 -0
  125. package/dist/plugin/transform/model-resolver.js +320 -0
  126. package/dist/plugin/transform/types.d.ts +110 -0
  127. package/dist/plugin/transform/types.js +1 -0
  128. package/dist/plugin/types.d.ts +95 -0
  129. package/dist/plugin/types.js +1 -0
  130. package/dist/plugin/ui/ansi.d.ts +31 -0
  131. package/dist/plugin/ui/ansi.js +45 -0
  132. package/dist/plugin/ui/auth-menu.d.ts +47 -0
  133. package/dist/plugin/ui/auth-menu.js +199 -0
  134. package/dist/plugin/ui/confirm.d.ts +1 -0
  135. package/dist/plugin/ui/confirm.js +14 -0
  136. package/dist/plugin/ui/select.d.ts +22 -0
  137. package/dist/plugin/ui/select.js +243 -0
  138. package/dist/plugin/version.d.ts +18 -0
  139. package/dist/plugin/version.js +79 -0
  140. package/dist/src/antigravity/oauth.d.ts +30 -0
  141. package/dist/src/antigravity/oauth.js +170 -0
  142. package/dist/src/constants.d.ts +138 -0
  143. package/dist/src/constants.js +216 -0
  144. package/dist/src/hooks/auto-update-checker/cache.d.ts +2 -0
  145. package/dist/src/hooks/auto-update-checker/cache.js +70 -0
  146. package/dist/src/hooks/auto-update-checker/checker.d.ts +15 -0
  147. package/dist/src/hooks/auto-update-checker/checker.js +233 -0
  148. package/dist/src/hooks/auto-update-checker/constants.d.ts +8 -0
  149. package/dist/src/hooks/auto-update-checker/constants.js +22 -0
  150. package/dist/src/hooks/auto-update-checker/index.d.ts +33 -0
  151. package/dist/src/hooks/auto-update-checker/index.js +121 -0
  152. package/dist/src/hooks/auto-update-checker/logging.d.ts +2 -0
  153. package/dist/src/hooks/auto-update-checker/logging.js +8 -0
  154. package/dist/src/hooks/auto-update-checker/types.d.ts +24 -0
  155. package/dist/src/hooks/auto-update-checker/types.js +1 -0
  156. package/dist/src/index.d.ts +6 -0
  157. package/dist/src/index.js +5 -0
  158. package/dist/src/plugin/accounts.d.ts +173 -0
  159. package/dist/src/plugin/accounts.js +966 -0
  160. package/dist/src/plugin/auth.d.ts +20 -0
  161. package/dist/src/plugin/auth.js +44 -0
  162. package/dist/src/plugin/cache/index.d.ts +4 -0
  163. package/dist/src/plugin/cache/index.js +4 -0
  164. package/dist/src/plugin/cache/signature-cache.d.ts +110 -0
  165. package/dist/src/plugin/cache/signature-cache.js +347 -0
  166. package/dist/src/plugin/cache.d.ts +43 -0
  167. package/dist/src/plugin/cache.js +180 -0
  168. package/dist/src/plugin/cli.d.ts +26 -0
  169. package/dist/src/plugin/cli.js +126 -0
  170. package/dist/src/plugin/config/index.d.ts +15 -0
  171. package/dist/src/plugin/config/index.js +15 -0
  172. package/dist/src/plugin/config/loader.d.ts +38 -0
  173. package/dist/src/plugin/config/loader.js +150 -0
  174. package/dist/src/plugin/config/models.d.ts +26 -0
  175. package/dist/src/plugin/config/models.js +95 -0
  176. package/dist/src/plugin/config/schema.d.ts +144 -0
  177. package/dist/src/plugin/config/schema.js +458 -0
  178. package/dist/src/plugin/config/updater.d.ts +76 -0
  179. package/dist/src/plugin/config/updater.js +205 -0
  180. package/dist/src/plugin/core/streaming/index.d.ts +2 -0
  181. package/dist/src/plugin/core/streaming/index.js +2 -0
  182. package/dist/src/plugin/core/streaming/transformer.d.ts +9 -0
  183. package/dist/src/plugin/core/streaming/transformer.js +301 -0
  184. package/dist/src/plugin/core/streaming/types.d.ts +28 -0
  185. package/dist/src/plugin/core/streaming/types.js +1 -0
  186. package/dist/src/plugin/debug.d.ts +93 -0
  187. package/dist/src/plugin/debug.js +375 -0
  188. package/dist/src/plugin/errors.d.ts +27 -0
  189. package/dist/src/plugin/errors.js +41 -0
  190. package/dist/src/plugin/fingerprint.d.ts +69 -0
  191. package/dist/src/plugin/fingerprint.js +137 -0
  192. package/dist/src/plugin/image-saver.d.ts +24 -0
  193. package/dist/src/plugin/image-saver.js +78 -0
  194. package/dist/src/plugin/logger.d.ts +35 -0
  195. package/dist/src/plugin/logger.js +67 -0
  196. package/dist/src/plugin/logging-utils.d.ts +22 -0
  197. package/dist/src/plugin/logging-utils.js +91 -0
  198. package/dist/src/plugin/project.d.ts +32 -0
  199. package/dist/src/plugin/project.js +229 -0
  200. package/dist/src/plugin/quota.d.ts +34 -0
  201. package/dist/src/plugin/quota.js +261 -0
  202. package/dist/src/plugin/recovery/constants.d.ts +21 -0
  203. package/dist/src/plugin/recovery/constants.js +42 -0
  204. package/dist/src/plugin/recovery/index.d.ts +11 -0
  205. package/dist/src/plugin/recovery/index.js +11 -0
  206. package/dist/src/plugin/recovery/storage.d.ts +23 -0
  207. package/dist/src/plugin/recovery/storage.js +340 -0
  208. package/dist/src/plugin/recovery/types.d.ts +115 -0
  209. package/dist/src/plugin/recovery/types.js +6 -0
  210. package/dist/src/plugin/recovery.d.ts +60 -0
  211. package/dist/src/plugin/recovery.js +360 -0
  212. package/dist/src/plugin/refresh-queue.d.ts +99 -0
  213. package/dist/src/plugin/refresh-queue.js +235 -0
  214. package/dist/src/plugin/request-helpers.d.ts +281 -0
  215. package/dist/src/plugin/request-helpers.js +2200 -0
  216. package/dist/src/plugin/request.d.ts +110 -0
  217. package/dist/src/plugin/request.js +1489 -0
  218. package/dist/src/plugin/rotation.d.ts +182 -0
  219. package/dist/src/plugin/rotation.js +364 -0
  220. package/dist/src/plugin/search.d.ts +31 -0
  221. package/dist/src/plugin/search.js +185 -0
  222. package/dist/src/plugin/server.d.ts +22 -0
  223. package/dist/src/plugin/server.js +306 -0
  224. package/dist/src/plugin/storage.d.ts +136 -0
  225. package/dist/src/plugin/storage.js +599 -0
  226. package/dist/src/plugin/stores/signature-store.d.ts +4 -0
  227. package/dist/src/plugin/stores/signature-store.js +24 -0
  228. package/dist/src/plugin/thinking-recovery.d.ts +89 -0
  229. package/dist/src/plugin/thinking-recovery.js +289 -0
  230. package/dist/src/plugin/token.d.ts +18 -0
  231. package/dist/src/plugin/token.js +127 -0
  232. package/dist/src/plugin/transform/claude.d.ts +79 -0
  233. package/dist/src/plugin/transform/claude.js +256 -0
  234. package/dist/src/plugin/transform/cross-model-sanitizer.d.ts +34 -0
  235. package/dist/src/plugin/transform/cross-model-sanitizer.js +224 -0
  236. package/dist/src/plugin/transform/gemini.d.ts +132 -0
  237. package/dist/src/plugin/transform/gemini.js +659 -0
  238. package/dist/src/plugin/transform/index.d.ts +14 -0
  239. package/dist/src/plugin/transform/index.js +9 -0
  240. package/dist/src/plugin/transform/model-resolver.d.ts +98 -0
  241. package/dist/src/plugin/transform/model-resolver.js +320 -0
  242. package/dist/src/plugin/transform/types.d.ts +110 -0
  243. package/dist/src/plugin/transform/types.js +1 -0
  244. package/dist/src/plugin/types.d.ts +95 -0
  245. package/dist/src/plugin/types.js +1 -0
  246. package/dist/src/plugin/ui/ansi.d.ts +31 -0
  247. package/dist/src/plugin/ui/ansi.js +45 -0
  248. package/dist/src/plugin/ui/auth-menu.d.ts +47 -0
  249. package/dist/src/plugin/ui/auth-menu.js +199 -0
  250. package/dist/src/plugin/ui/confirm.d.ts +1 -0
  251. package/dist/src/plugin/ui/confirm.js +14 -0
  252. package/dist/src/plugin/ui/select.d.ts +22 -0
  253. package/dist/src/plugin/ui/select.js +243 -0
  254. package/dist/src/plugin/version.d.ts +18 -0
  255. package/dist/src/plugin/version.js +79 -0
  256. package/package.json +54 -0
@@ -0,0 +1,733 @@
1
+ import http from 'http';
2
+ import https from 'https';
3
+ import fs from 'fs';
4
+ import { fetchAvailableModels } from '../src/plugin/quota';
5
+ import { loadProxyConfig } from '../src/plugin/proxy-config';
6
+ import { loadHotPool, saveHotPool } from './proxy-manager';
7
+ import { resolveQuotaGroup } from '../src/plugin/accounts';
8
+ import { HttpsProxyAgent } from 'https-proxy-agent';
9
+ let lastNotifiedIndex = -1;
10
+ const pendingAlerts = [];
11
+ const PORT = 34567;
12
+ const HOST = '127.0.0.1';
13
+ function liveModelToQuotaModel(model) {
14
+ const normalized = model.replace(/-(minimal|low|medium|high)$/, '');
15
+ if (!normalized.startsWith('antigravity-')) {
16
+ return `antigravity-${normalized}`;
17
+ }
18
+ return normalized;
19
+ }
20
+ import { AccountManager } from '../src/plugin/accounts';
21
+ let accountManager;
22
+ let currentAccessToken = null;
23
+ let currentAccountEmail = null;
24
+ let liveModelsCache = [];
25
+ let liveModelsLastFetched = 0;
26
+ const thoughtSignatureMap = new Map();
27
+ const THOUGHT_SIG_TTL = 10 * 60 * 1000;
28
+ function storeThoughtSignature(toolId, signature) {
29
+ thoughtSignatureMap.set(toolId, { sig: signature, ts: Date.now() });
30
+ const cutoff = Date.now() - THOUGHT_SIG_TTL;
31
+ for (const [k, v] of thoughtSignatureMap) {
32
+ if (v.ts < cutoff)
33
+ thoughtSignatureMap.delete(k);
34
+ }
35
+ }
36
+ function recallThoughtSignature(toolId) {
37
+ const entry = thoughtSignatureMap.get(toolId);
38
+ return entry ? entry.sig : null;
39
+ }
40
+ async function getLiveModels(token, projectId) {
41
+ if (Date.now() - liveModelsLastFetched < 3600 * 1000 && liveModelsCache.length > 0) {
42
+ return liveModelsCache;
43
+ }
44
+ try {
45
+ const res = await fetchAvailableModels(token, projectId);
46
+ if (res.models) {
47
+ liveModelsCache = Object.keys(res.models);
48
+ liveModelsLastFetched = Date.now();
49
+ }
50
+ }
51
+ catch (e) {
52
+ console.error("Failed to fetch live models", e.message);
53
+ }
54
+ return liveModelsCache;
55
+ }
56
+ let tokenExpiresAt = 0;
57
+ function logDebug(msg) {
58
+ try {
59
+ fs.appendFileSync('C:\\\\Users\\\\finn\\\\.claude\\\\proxy-debug.log', `[${new Date().toISOString()}] ${msg}\\n`);
60
+ }
61
+ catch (e) { }
62
+ }
63
+ async function getAccessToken(requestedModel = null) {
64
+ if (!accountManager) {
65
+ accountManager = await AccountManager.loadFromDisk();
66
+ console.log("Loaded accounts from disk.");
67
+ setInterval(async () => {
68
+ try {
69
+ await accountManager.reloadFromDisk();
70
+ }
71
+ catch (e) {
72
+ logDebug(`Failed to reload from disk: ${e.message}`);
73
+ }
74
+ }, 10000);
75
+ }
76
+ if (currentAccessToken && Date.now() < tokenExpiresAt) {
77
+ return { token: currentAccessToken, email: currentAccountEmail, account: accountManager.accounts.find(a => a.email === currentAccountEmail), isFallback: false };
78
+ }
79
+ const quotaModel = requestedModel ? liveModelToQuotaModel(requestedModel) : null;
80
+ const isGeminiRequested = requestedModel && requestedModel.includes('gemini');
81
+ let account = null;
82
+ let isFallback = false;
83
+ if (isGeminiRequested) {
84
+ account = await accountManager.getCurrentOrNextForFamily('gemini', quotaModel, 'sequential');
85
+ isFallback = true; // Always true for gemini models so we mark quota correctly
86
+ }
87
+ else {
88
+ account = await accountManager.getCurrentOrNextForFamily('claude', quotaModel, 'sequential');
89
+ if (!account) {
90
+ account = await accountManager.getCurrentOrNextForFamily('gemini', quotaModel, 'sequential');
91
+ isFallback = true;
92
+ }
93
+ }
94
+ if (!account) {
95
+ throw new Error('All accounts (Claude and Gemini) are exhausted or rate limited!');
96
+ }
97
+ if (account && account.index !== lastNotifiedIndex) {
98
+ if (lastNotifiedIndex !== -1) {
99
+ pendingAlerts.push('?? Rotated to ' + (isFallback ? 'Gemini fallback: ' : '') + '`' + account.email + '`');
100
+ }
101
+ lastNotifiedIndex = account.index;
102
+ }
103
+ return new Promise((resolve, reject) => {
104
+ const postData = new URLSearchParams({
105
+ client_id: 'YOUR_GOOGLE_CLIENT_ID',
106
+ client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
107
+ refresh_token: account.parts.refreshToken,
108
+ grant_type: 'refresh_token',
109
+ }).toString();
110
+ const options = {
111
+ hostname: 'oauth2.googleapis.com',
112
+ path: '/token',
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/x-www-form-urlencoded',
116
+ 'Content-Length': Buffer.byteLength(postData),
117
+ },
118
+ };
119
+ const req = https.request(options, (res) => {
120
+ let body = '';
121
+ res.on('data', d => body += d);
122
+ res.on('end', () => {
123
+ if (res.statusCode >= 200 && res.statusCode < 300) {
124
+ try {
125
+ const parsed = JSON.parse(body);
126
+ currentAccessToken = parsed.access_token;
127
+ currentAccountEmail = account.email || 'Unknown';
128
+ tokenExpiresAt = Date.now() + (parsed.expires_in * 1000) - 60000;
129
+ resolve({ token: currentAccessToken, email: currentAccountEmail, account: account });
130
+ }
131
+ catch (e) {
132
+ reject(e);
133
+ }
134
+ }
135
+ else {
136
+ accountManager.markRateLimitedWithReason(account, isFallback ? "gemini" : "claude", "antigravity", null, "SERVER_ERROR");
137
+ pendingAlerts.push('?? Token refresh failed: `' + account.email + '`');
138
+ reject(new Error(`Failed to refresh token: ${res.statusCode} ${body}`));
139
+ }
140
+ });
141
+ });
142
+ req.on('error', reject);
143
+ req.write(postData);
144
+ req.end();
145
+ });
146
+ }
147
+ function translateAnthropicToGemini(anthropic) {
148
+ const gemini = { contents: [] };
149
+ if (anthropic.system) {
150
+ gemini.systemInstruction = {
151
+ parts: [{ text: typeof anthropic.system === 'string' ? anthropic.system : JSON.stringify(anthropic.system) }]
152
+ };
153
+ }
154
+ const toolSchemas = {};
155
+ const geminiToAnthropicName = new Map();
156
+ if (anthropic.tools) {
157
+ for (const t of anthropic.tools) {
158
+ const props = t.input_schema?.properties;
159
+ const normalizedName = t.name.replace(/^[^:]+:/, '').replace(/[^a-zA-Z0-9_-]/g, '_');
160
+ geminiToAnthropicName.set(normalizedName, t.name);
161
+ if (props)
162
+ toolSchemas[normalizedName] = new Set(Object.keys(props));
163
+ }
164
+ }
165
+ if (anthropic.tools) {
166
+ const sanitizeSchema = (schema) => {
167
+ if (!schema || typeof schema !== 'object')
168
+ return schema;
169
+ if (Array.isArray(schema))
170
+ return schema.map(sanitizeSchema);
171
+ const cleaned = {};
172
+ const allowedKeys = ['type', 'format', 'description', 'nullable', 'enum', 'properties', 'required', 'items'];
173
+ for (const key of Object.keys(schema)) {
174
+ if (allowedKeys.includes(key)) {
175
+ if (key === 'properties' && typeof schema[key] === 'object' && !Array.isArray(schema[key])) {
176
+ cleaned.properties = {};
177
+ for (const propName of Object.keys(schema.properties)) {
178
+ cleaned.properties[propName] = sanitizeSchema(schema.properties[propName]);
179
+ }
180
+ }
181
+ else if (typeof schema[key] === 'object') {
182
+ cleaned[key] = sanitizeSchema(schema[key]);
183
+ }
184
+ else {
185
+ cleaned[key] = schema[key];
186
+ }
187
+ }
188
+ }
189
+ if (cleaned.required && (!cleaned.properties || Object.keys(cleaned.properties).length === 0)) {
190
+ delete cleaned.required;
191
+ }
192
+ return cleaned;
193
+ };
194
+ gemini.tools = [{
195
+ functionDeclarations: anthropic.tools.map(t => {
196
+ const geminiName = t.name.replace(/^[^:]+:/, '').replace(/[^a-zA-Z0-9_-]/g, '_');
197
+ return {
198
+ name: geminiName,
199
+ description: t.description || '',
200
+ parameters: sanitizeSchema(t.input_schema)
201
+ };
202
+ })
203
+ }];
204
+ gemini.toolConfig = { functionCallingConfig: { mode: 'AUTO' } };
205
+ }
206
+ const toolIdToGeminiName = new Map();
207
+ for (const msg of anthropic.messages || []) {
208
+ const role = msg.role === 'assistant' ? 'model' : 'user';
209
+ const parts = [];
210
+ if (typeof msg.content === 'string') {
211
+ parts.push({ text: msg.content });
212
+ }
213
+ else if (Array.isArray(msg.content)) {
214
+ for (const c of msg.content) {
215
+ if (c.type === 'text')
216
+ parts.push({ text: c.text });
217
+ if (c.type === 'tool_use') {
218
+ const geminiName = c.name.replace(/^[^:]+:/, '').replace(/[^a-zA-Z0-9_-]/g, '_');
219
+ toolIdToGeminiName.set(c.id, geminiName);
220
+ const fc = { name: geminiName, args: c.input };
221
+ const sig = recallThoughtSignature(c.id);
222
+ if (sig)
223
+ fc.thoughtSignature = sig; // Gemini uses camelCase
224
+ parts.push({ functionCall: fc });
225
+ }
226
+ if (c.type === 'tool_result') {
227
+ const contentText = typeof c.content === 'string' ? c.content : JSON.stringify(c.content);
228
+ const mappedName = toolIdToGeminiName.get(c.tool_use_id) || c.tool_use_id;
229
+ parts.push({ functionResponse: { name: mappedName, response: { content: contentText } } });
230
+ }
231
+ }
232
+ }
233
+ gemini.contents.push({ role, parts });
234
+ }
235
+ return { gemini, toolSchemas, geminiToAnthropicName };
236
+ }
237
+ const server = http.createServer((req, res) => {
238
+ logDebug(`${req.method} ${req.url}`);
239
+ if (req.method === 'POST' && (req.url.startsWith('/v1/messages') || req.url.startsWith('/messages'))) {
240
+ let body = '';
241
+ req.on('data', chunk => body += chunk);
242
+ req.on('end', async () => {
243
+ const MAX_ATTEMPTS = accountManager && accountManager.accounts ? Math.max(4, accountManager.accounts.length + 1) : 10;
244
+ async function attemptRequest(attempt) {
245
+ try {
246
+ const anthropicPayload = JSON.parse(body);
247
+ let model = anthropicPayload.model || 'sonnet-4.6';
248
+ try {
249
+ const { token, email, account, isFallback } = await getAccessToken(model);
250
+ let actualProjectId = 'galvanized-spot-7zsgc'; // Fallback
251
+ try {
252
+ const loadData = JSON.stringify({
253
+ metadata: {
254
+ ideType: 'IDE_UNSPECIFIED',
255
+ platform: 'PLATFORM_UNSPECIFIED',
256
+ }
257
+ });
258
+ const loadOptions = {
259
+ hostname: 'daily-cloudcode-pa.sandbox.googleapis.com',
260
+ path: '/v1internal:loadCodeAssist',
261
+ method: 'POST',
262
+ headers: {
263
+ 'Authorization': "Bearer " + token,
264
+ 'Content-Type': 'application/json',
265
+ 'User-Agent': 'google-api-nodejs-client/9.15.1',
266
+ 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
267
+ 'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI'
268
+ }
269
+ };
270
+ const projectIdResult = await new Promise((resolve) => {
271
+ const r = https.request(loadOptions, (rRes) => {
272
+ let b = '';
273
+ rRes.on('data', d => b += d);
274
+ rRes.on('end', () => {
275
+ logDebug(`loadCodeAssist Status: ${rRes.statusCode}, Body: ${b}`);
276
+ try {
277
+ const p = JSON.parse(b);
278
+ if (p.cloudaicompanionProject) {
279
+ resolve(typeof p.cloudaicompanionProject === 'string' ? p.cloudaicompanionProject : p.cloudaicompanionProject.id);
280
+ }
281
+ else {
282
+ resolve(null);
283
+ }
284
+ }
285
+ catch (e) {
286
+ resolve(null);
287
+ }
288
+ });
289
+ });
290
+ r.on('error', () => resolve(null));
291
+ r.write(loadData);
292
+ r.end();
293
+ });
294
+ if (projectIdResult) {
295
+ actualProjectId = projectIdResult;
296
+ }
297
+ }
298
+ catch (e) {
299
+ logDebug(`loadCodeAssist error: ${e.message}`);
300
+ }
301
+ let availableModels = await getLiveModels(token, actualProjectId);
302
+ if (account && account.rateLimitResetTimes) {
303
+ const nowMs = Date.now();
304
+ availableModels = availableModels.filter(m => {
305
+ const quotaModel = liveModelToQuotaModel(m);
306
+ const base = m.includes('gemini') ? 'gemini-antigravity' : 'claude';
307
+ const modelKey = `${base}:${quotaModel}`;
308
+ if (account.rateLimitResetTimes[modelKey] > nowMs)
309
+ return false;
310
+ if (account.rateLimitResetTimes[base] > nowMs)
311
+ return false;
312
+ if (account.rateLimitResetTimes['claude'] > nowMs)
313
+ return false;
314
+ return true;
315
+ });
316
+ }
317
+ let requestedModel = anthropicPayload.model || 'sonnet';
318
+ if (isFallback) {
319
+ if (lastNotifiedIndex !== -1 && !pendingAlerts.some(a => a.includes('Auto-Fallback'))) {
320
+ pendingAlerts.push('?? Auto-Fallback: All Claude accounts rate-limited. Switched to Gemini.');
321
+ }
322
+ model = availableModels.find(m => m.includes('gemini-3.1-pro')) ||
323
+ availableModels.find(m => m.includes('gemini-3')) ||
324
+ availableModels.find(m => m.includes('gemini-1.5-pro')) ||
325
+ availableModels.find(m => m.includes('gemini')) ||
326
+ 'antigravity-gemini-3.1-pro';
327
+ }
328
+ else {
329
+ if (requestedModel.includes('opus')) {
330
+ model = availableModels.find(m => m.includes('opus')) || 'claude-opus-4-6-thinking';
331
+ }
332
+ else if (requestedModel.includes('haiku')) {
333
+ model = availableModels.find(m => m.includes('haiku')) ||
334
+ availableModels.find(m => m.includes('gemini-3.1-pro')) ||
335
+ availableModels.find(m => m.includes('gemini-3')) ||
336
+ availableModels.find(m => m.includes('gemini-1.5-pro')) ||
337
+ 'claude-sonnet-4-6';
338
+ }
339
+ else {
340
+ model = availableModels.find(m => m.includes('sonnet')) || 'claude-sonnet-4-6-thinking';
341
+ }
342
+ }
343
+ logDebug(`Using project: ${actualProjectId}, model: ${model}`);
344
+ pendingAlerts.push(`?? Using model: \`${model}\` (account: \`${email}\`)`);
345
+ const { gemini: geminiPayload, toolSchemas, geminiToAnthropicName } = translateAnthropicToGemini(anthropicPayload);
346
+ const wrappedBody = {
347
+ project: actualProjectId,
348
+ model: model,
349
+ request: geminiPayload,
350
+ requestType: 'agent',
351
+ userAgent: 'antigravity',
352
+ requestId: 'agent-' + Date.now()
353
+ };
354
+ const isGemini = model.includes('gemini');
355
+ const targetHostname = isGemini ? 'cloudcode-pa.googleapis.com' : 'daily-cloudcode-pa.sandbox.googleapis.com';
356
+ const options = {
357
+ hostname: targetHostname,
358
+ path: `/v1internal:streamGenerateContent?alt=sse`,
359
+ method: 'POST',
360
+ headers: {
361
+ 'Authorization': "Bearer " + token,
362
+ 'Content-Type': 'application/json',
363
+ 'User-Agent': 'antigravity/1.0.0 win32/x64',
364
+ }
365
+ };
366
+ let proxyUrl = null;
367
+ try {
368
+ const proxyConfig = loadProxyConfig();
369
+ if (proxyConfig.strategy === 'automatic') {
370
+ const hotPool = loadHotPool();
371
+ const proxy = hotPool.find(p => p.failures < 3) || hotPool[0];
372
+ if (proxy) {
373
+ proxyUrl = proxy.url;
374
+ options.agent = new HttpsProxyAgent(proxyUrl);
375
+ logDebug(`[Proxy] Using automatic hot pool proxy: ${proxyUrl}`);
376
+ }
377
+ }
378
+ }
379
+ catch (e) {
380
+ logDebug(`[Proxy] Error loading hot pool proxy: ${e.message}`);
381
+ }
382
+ const proxyReq = https.request(options, (proxyRes) => {
383
+ logDebug(`Google API Status: ${proxyRes.statusCode}`);
384
+ if (proxyRes.statusCode !== 200) {
385
+ let errorBody = '';
386
+ proxyRes.on('data', chunk => errorBody += chunk.toString());
387
+ proxyRes.on('end', () => {
388
+ logDebug(`Google API Error Body: ${errorBody}`);
389
+ let isRateLimited = proxyRes.statusCode === 429 || errorBody.includes('RESOURCE_EXHAUSTED');
390
+ let isCapacityExhausted = proxyRes.statusCode === 503 || errorBody.includes('MODEL_CAPACITY_EXHAUSTED') || errorBody.includes('UNAVAILABLE');
391
+ if (!isRateLimited && !isCapacityExhausted) {
392
+ try {
393
+ const errJson = JSON.parse(errorBody);
394
+ if (errJson.error) {
395
+ if (errJson.error.code === 429) {
396
+ isRateLimited = true;
397
+ }
398
+ else if (errJson.error.code === 503 || errJson.error.status === 'UNAVAILABLE') {
399
+ isCapacityExhausted = true;
400
+ }
401
+ }
402
+ }
403
+ catch (e) { }
404
+ }
405
+ if (isRateLimited || isCapacityExhausted) {
406
+ let isProxyIpBan = false;
407
+ if (isRateLimited) {
408
+ try {
409
+ const proxyConfig = loadProxyConfig();
410
+ if (proxyConfig.strategy === 'automatic' && account && account.cachedQuota) {
411
+ const family = isFallback ? 'gemini' : 'claude';
412
+ const quotaGroup = resolveQuotaGroup(family, model);
413
+ const groupData = account.cachedQuota[quotaGroup];
414
+ if (groupData && typeof groupData.remainingFraction === 'number') {
415
+ const remainingFraction = groupData.remainingFraction;
416
+ if (remainingFraction > 0.6) {
417
+ isProxyIpBan = true;
418
+ }
419
+ }
420
+ }
421
+ }
422
+ catch (e) {
423
+ logDebug(`Error evaluating quota heuristic: ${e.message}`);
424
+ }
425
+ }
426
+ if (isProxyIpBan) {
427
+ logDebug(`Suspected IP ban for model ${model} (remaining quota is > 60%). Discarding proxy and retrying request without changing account.`);
428
+ if (proxyUrl) {
429
+ try {
430
+ const pool = loadHotPool();
431
+ const filteredPool = pool.filter(p => p.url !== proxyUrl);
432
+ saveHotPool(filteredPool);
433
+ logDebug(`Discarded proxy ${proxyUrl} from hot pool.`);
434
+ }
435
+ catch (e) {
436
+ logDebug(`Error discarding proxy: ${e.message}`);
437
+ }
438
+ }
439
+ if (attempt < MAX_ATTEMPTS) {
440
+ logDebug(`Retrying request (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`);
441
+ return attemptRequest(attempt + 1);
442
+ }
443
+ }
444
+ const reason = isCapacityExhausted ? "MODEL_CAPACITY_EXHAUSTED" : "QUOTA_EXHAUSTED";
445
+ let cooldownMs = isCapacityExhausted ? 5 * 60 * 1000 : 30 * 60 * 1000; // 5min for capacity, 30min for quota
446
+ try {
447
+ const errJson = JSON.parse(errorBody);
448
+ const retryDetail = errJson.error?.details?.find((d) => d['@type']?.includes('RetryInfo'));
449
+ if (retryDetail?.retryDelay) {
450
+ const delaySec = parseInt(retryDetail.retryDelay.replace('s', ''), 10);
451
+ if (delaySec > 0)
452
+ cooldownMs = delaySec * 1000;
453
+ }
454
+ }
455
+ catch (e) { }
456
+ logDebug(`Account ${email} hit ${reason} on model ${model}, cooling down for ${Math.round(cooldownMs / 60000)} mins.`);
457
+ if (account) {
458
+ accountManager.markRateLimitedWithReason(account, isFallback ? "gemini" : "claude", "antigravity", liveModelToQuotaModel(model), reason, cooldownMs);
459
+ pendingAlerts.push('?? ' + reason + ' for model `' + model + '`: `' + account.email + '`');
460
+ }
461
+ currentAccessToken = null; // Force new account on next attempt
462
+ if (attempt < MAX_ATTEMPTS) {
463
+ logDebug(`Retrying with next account (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`);
464
+ return attemptRequest(attempt + 1);
465
+ }
466
+ }
467
+ res.writeHead(400, { 'Content-Type': 'application/json' });
468
+ res.end(JSON.stringify({
469
+ type: "error",
470
+ error: {
471
+ type: "invalid_request_error",
472
+ message: `Google API Error: ${errorBody}`
473
+ }
474
+ }));
475
+ });
476
+ return;
477
+ }
478
+ res.writeHead(200, {
479
+ 'Content-Type': 'text/event-stream',
480
+ 'Cache-Control': 'no-cache',
481
+ 'Connection': 'keep-alive',
482
+ 'Access-Control-Allow-Origin': '*'
483
+ });
484
+ const msgId = 'msg_' + Date.now();
485
+ const startEvent = `event: message_start\ndata: {"type": "message_start", "message": { "id": "${msgId}", "role": "assistant", "content": [], "model": "${model}", "stop_reason": null, "stop_sequence": null }}\n\n`;
486
+ res.write(startEvent);
487
+ let buffer = '';
488
+ let textBlockStarted = false;
489
+ let toolBlockStarted = false;
490
+ let blockIndex = 0;
491
+ let hadToolCall = false; // tracks if ANY tool was called in this response
492
+ let pendingThoughtSignature = null;
493
+ res.write(`event: content_block_start\ndata: {"type": "content_block_start", "index": ${blockIndex}, "content_block": {"type": "text", "text": ""}}\n\n`);
494
+ textBlockStarted = true;
495
+ let notification = `> ? **Antigravity Proxy**\n> ?? Account: \`${email}\``;
496
+ if (pendingAlerts.length > 0) {
497
+ notification += '\n>\n> **Alerts:**\n' + pendingAlerts.map(a => '> - ' + a).join('\n');
498
+ pendingAlerts.length = 0; // Clear the queue
499
+ }
500
+ if (anthropicPayload.model !== model) {
501
+ notification += `\n> ?? Mapped model \`${anthropicPayload.model}\` to \`${model}\``;
502
+ }
503
+ notification += `\n\n`;
504
+ res.write(`event: content_block_delta\ndata: {"type": "content_block_delta", "index": ${blockIndex}, "delta": {"type": "text_delta", "text": ${JSON.stringify(notification)}}}\n\n`);
505
+ proxyRes.on('data', chunk => {
506
+ const chunkStr = chunk.toString();
507
+ logDebug(`Raw Google chunk: ${chunkStr.substring(0, 500)}`);
508
+ buffer += chunkStr;
509
+ const lines = buffer.split('\n');
510
+ buffer = lines.pop(); // keep incomplete line
511
+ for (const line of lines) {
512
+ if (line.startsWith('data: ')) {
513
+ const dataStr = line.substring(6).trim();
514
+ if (dataStr === '[DONE]')
515
+ continue;
516
+ if (!dataStr)
517
+ continue;
518
+ try {
519
+ const eventData = JSON.parse(dataStr);
520
+ const payload = eventData.response || eventData;
521
+ const candidates = payload.candidates || [];
522
+ if (candidates.length > 0) {
523
+ const candidate = candidates[0];
524
+ const parts = candidate.content?.parts || [];
525
+ for (const part of parts) {
526
+ if (part.thoughtSignature) {
527
+ pendingThoughtSignature = part.thoughtSignature;
528
+ }
529
+ if (part.text) {
530
+ if (!textBlockStarted) {
531
+ res.write(`event: content_block_start\ndata: {"type": "content_block_start", "index": ${blockIndex}, "content_block": {"type": "text", "text": ""}}\n\n`);
532
+ textBlockStarted = true;
533
+ }
534
+ res.write(`event: content_block_delta\ndata: {"type": "content_block_delta", "index": ${blockIndex}, "delta": {"type": "text_delta", "text": ${JSON.stringify(part.text)}}}\n\n`);
535
+ }
536
+ if (part.functionCall) {
537
+ if (!toolBlockStarted) {
538
+ if (textBlockStarted) {
539
+ res.write(`event: content_block_stop\ndata: {"type": "content_block_stop", "index": ${blockIndex}}\n\n`);
540
+ blockIndex++;
541
+ textBlockStarted = false;
542
+ }
543
+ const toolId = `tool_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
544
+ const sig = part.functionCall.thoughtSignature ||
545
+ part.functionCall.thought_signature ||
546
+ pendingThoughtSignature;
547
+ if (sig) {
548
+ storeThoughtSignature(toolId, sig);
549
+ pendingThoughtSignature = null; // consumed
550
+ }
551
+ const anthropicToolName = geminiToAnthropicName.get(part.functionCall.name) || part.functionCall.name;
552
+ res.write(`event: content_block_start\ndata: {"type": "content_block_start", "index": ${blockIndex}, "content_block": {"type": "tool_use", "id": "${toolId}", "name": "${anthropicToolName}", "input": {}}}\n\n`);
553
+ toolBlockStarted = true;
554
+ hadToolCall = true;
555
+ }
556
+ // (e.g. Gemini adds "description" to Bash args, which Claude Code rejects)
557
+ let args = part.functionCall.args || {};
558
+ const allowedProps = toolSchemas[part.functionCall.name];
559
+ if (allowedProps && allowedProps.size > 0) {
560
+ args = Object.fromEntries(Object.entries(args).filter(([k]) => allowedProps.has(k)));
561
+ }
562
+ const argsJson = JSON.stringify(args);
563
+ res.write(`event: content_block_delta\ndata: {"type": "content_block_delta", "index": ${blockIndex}, "delta": {"type": "input_json_delta", "partial_json": ${JSON.stringify(argsJson)}}}\n\n`);
564
+ }
565
+ }
566
+ if (candidate.finishReason) {
567
+ if (textBlockStarted || toolBlockStarted) {
568
+ res.write(`event: content_block_stop\ndata: {"type": "content_block_stop", "index": ${blockIndex}}\n\n`);
569
+ }
570
+ // Claude Code needs "tool_use" to continue the agentic loop.
571
+ const stopReason = hadToolCall ? 'tool_use' :
572
+ (candidate.finishReason === 'STOP' ? 'end_turn' : 'tool_use');
573
+ res.write(`event: message_delta\ndata: {"type": "message_delta", "delta": {"stop_reason": "${stopReason}"}}\n\n`);
574
+ res.write(`event: message_stop\ndata: {"type": "message_stop"}\n\n`);
575
+ res.end();
576
+ return;
577
+ }
578
+ }
579
+ }
580
+ catch (e) {
581
+ }
582
+ }
583
+ }
584
+ });
585
+ proxyRes.on('end', () => {
586
+ if (!res.writableEnded) {
587
+ res.write(`event: message_stop\ndata: {"type": "message_stop"}\n\n`);
588
+ res.end();
589
+ }
590
+ });
591
+ proxyRes.on('error', (err) => {
592
+ logDebug(`Proxy response error: ${err.message}`);
593
+ if (!res.writableEnded) {
594
+ res.writeHead(500);
595
+ res.end(JSON.stringify({ error: err.message }));
596
+ }
597
+ });
598
+ });
599
+ proxyReq.on('error', (err) => {
600
+ logDebug(`Proxy request error: ${err.message}`);
601
+ res.writeHead(500, { 'Content-Type': 'application/json' });
602
+ res.end(JSON.stringify({ error: err.message }));
603
+ });
604
+ proxyReq.write(JSON.stringify(wrappedBody));
605
+ proxyReq.end();
606
+ }
607
+ catch (authError) {
608
+ logDebug(`Auth Error: ${authError.message}`);
609
+ if (authError.message.includes("exhausted or rate limited")) {
610
+ res.writeHead(400, { 'Content-Type': 'application/json' });
611
+ res.end(JSON.stringify({
612
+ type: "error",
613
+ error: {
614
+ type: "invalid_request_error",
615
+ message: "Antigravity Proxy: " + authError.message
616
+ }
617
+ }));
618
+ }
619
+ else {
620
+ res.writeHead(401, { 'Content-Type': 'application/json' });
621
+ res.end(JSON.stringify({ error: authError.message }));
622
+ }
623
+ }
624
+ }
625
+ catch (err) {
626
+ logDebug(`Error: ${err.message}`);
627
+ res.writeHead(500);
628
+ res.end(JSON.stringify({ error: err.message, stack: err.stack }));
629
+ }
630
+ } // end attemptRequest
631
+ attemptRequest(1);
632
+ });
633
+ }
634
+ else if (req.url.startsWith('/v1/models') || req.url.startsWith('/models')) {
635
+ res.writeHead(200, { 'Content-Type': 'application/json' });
636
+ res.end(JSON.stringify({
637
+ type: "model",
638
+ id: "claude-opus-4-7",
639
+ display_name: "Antigravity Model"
640
+ }));
641
+ }
642
+ else if (req.method === 'POST' && (req.url.startsWith('/v1/chat/completions') || req.url.startsWith('/v1/complete'))) {
643
+ logDebug(`Redirecting OpenAI-compatible request ${req.url} -> /v1/messages`);
644
+ req.url = '/v1/messages';
645
+ server.emit('request', req, res);
646
+ }
647
+ else if (req.url === '/' || req.url === '/health' || req.url === '/v1') {
648
+ res.writeHead(200, { 'Content-Type': 'application/json' });
649
+ res.end(JSON.stringify({ status: "ok", proxy: "antigravity", port: PORT }));
650
+ }
651
+ else {
652
+ logDebug(`Unhandled route: ${req.method} ${req.url} � returning 404`);
653
+ res.writeHead(404, { 'Content-Type': 'application/json' });
654
+ res.end(JSON.stringify({
655
+ type: "error",
656
+ error: {
657
+ type: "not_found",
658
+ message: `Route not found: ${req.method} ${req.url}. Supported: POST /v1/messages, GET /v1/models`
659
+ }
660
+ }));
661
+ }
662
+ });
663
+ server.listen(PORT, HOST, () => {
664
+ console.log(`Antigravity Auth Proxy running on http://${HOST}:${PORT}`);
665
+ });
666
+ // --- Auto-Shutdown Logic ---
667
+ import { exec } from 'child_process';
668
+ import os from 'os';
669
+ let activeRequests = 0;
670
+ const originalListeners = server.listeners('request');
671
+ server.removeAllListeners('request');
672
+ server.on('request', (req, res) => {
673
+ activeRequests++;
674
+ res.on('close', () => { activeRequests = Math.max(0, activeRequests - 1); });
675
+ for (const listener of originalListeners) {
676
+ listener.call(server, req, res);
677
+ }
678
+ });
679
+ if (os.platform() === 'win32') {
680
+ let emptyChecks = 0;
681
+ setInterval(() => {
682
+ if (activeRequests > 0) {
683
+ emptyChecks = 0;
684
+ return;
685
+ }
686
+ exec('tasklist /FO CSV /NH', { windowsHide: true, timeout: 10000 }, (err, stdout) => {
687
+ if (err)
688
+ return; // If tasklist fails, skip this check (don't increment)
689
+ const lower = stdout.toLowerCase();
690
+ const hasActiveInstance = lower.includes('opencode.exe') ||
691
+ lower.includes('claude.exe') ||
692
+ lower.includes('claude code');
693
+ if (!hasActiveInstance) {
694
+ emptyChecks++;
695
+ if (emptyChecks >= 6) { // 90 seconds of no activity (6 � 15s)
696
+ console.log("No active Claude/OpenCode instances detected. Shutting down background proxy.");
697
+ server.close(() => process.exit(0));
698
+ setTimeout(() => process.exit(0), 3000).unref();
699
+ }
700
+ }
701
+ else {
702
+ emptyChecks = 0;
703
+ }
704
+ });
705
+ }, 15000);
706
+ }
707
+ else if (os.platform() === 'linux' || os.platform() === 'darwin') {
708
+ let emptyChecks = 0;
709
+ setInterval(() => {
710
+ if (activeRequests > 0) {
711
+ emptyChecks = 0;
712
+ return;
713
+ }
714
+ exec('ps aux', { timeout: 5000 }, (err, stdout) => {
715
+ if (err)
716
+ return;
717
+ const lower = stdout.toLowerCase();
718
+ const hasActiveInstance = lower.includes('opencode') ||
719
+ lower.includes('claude');
720
+ if (!hasActiveInstance) {
721
+ emptyChecks++;
722
+ if (emptyChecks >= 6) {
723
+ console.log("No active Claude/OpenCode instances detected. Shutting down background proxy.");
724
+ server.close(() => process.exit(0));
725
+ setTimeout(() => process.exit(0), 3000).unref();
726
+ }
727
+ }
728
+ else {
729
+ emptyChecks = 0;
730
+ }
731
+ });
732
+ }, 15000);
733
+ }