vibe-usage-cli 0.0.0-stage → 0.1.1

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 (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -2
  3. package/build/cli.js +197 -0
  4. package/build/core/claude-processes.d.ts +51 -0
  5. package/build/core/claude-processes.js +288 -0
  6. package/build/core/claude-profile.d.ts +2 -0
  7. package/build/core/claude-profile.js +146 -0
  8. package/build/core/claude-renew.d.ts +12 -0
  9. package/build/core/claude-renew.js +79 -0
  10. package/build/core/continuity.d.ts +90 -0
  11. package/build/core/continuity.js +1244 -0
  12. package/build/core/diagnostics.d.ts +25 -0
  13. package/build/core/diagnostics.js +229 -0
  14. package/build/core/environment.d.ts +12 -0
  15. package/build/core/environment.js +25 -0
  16. package/build/core/errors.d.ts +23 -0
  17. package/build/core/errors.js +48 -0
  18. package/build/core/files.d.ts +20 -0
  19. package/build/core/files.js +136 -0
  20. package/build/core/glance.d.ts +25 -0
  21. package/build/core/glance.js +107 -0
  22. package/build/core/handover.d.ts +61 -0
  23. package/build/core/handover.js +333 -0
  24. package/build/core/history.d.ts +19 -0
  25. package/build/core/history.js +134 -0
  26. package/build/core/launch.d.ts +78 -0
  27. package/build/core/launch.js +256 -0
  28. package/build/core/measurements.d.ts +11 -0
  29. package/build/core/measurements.js +75 -0
  30. package/build/core/model-families.d.ts +12 -0
  31. package/build/core/model-families.js +61 -0
  32. package/build/core/model.d.ts +32 -0
  33. package/build/core/model.js +86 -0
  34. package/build/core/models.d.ts +25 -0
  35. package/build/core/models.js +164 -0
  36. package/build/core/redact.d.ts +2 -0
  37. package/build/core/redact.js +64 -0
  38. package/build/core/selection.d.ts +65 -0
  39. package/build/core/selection.js +173 -0
  40. package/build/core/service.d.ts +35 -0
  41. package/build/core/service.js +484 -0
  42. package/build/core/sessions.d.ts +87 -0
  43. package/build/core/sessions.js +868 -0
  44. package/build/core/settings.d.ts +16 -0
  45. package/build/core/settings.js +377 -0
  46. package/build/core/tray-icon.d.ts +5 -0
  47. package/build/core/tray-icon.js +91 -0
  48. package/build/core/updater.d.ts +29 -0
  49. package/build/core/updater.js +88 -0
  50. package/build/core/updates.d.ts +5 -0
  51. package/build/core/updates.js +29 -0
  52. package/build/launchers/discover-antigravity.ps1 +68 -0
  53. package/build/launchers/terminal.ps1 +78 -0
  54. package/build/main/main.js +587 -0
  55. package/build/providers/antigravity.d.ts +48 -0
  56. package/build/providers/antigravity.js +463 -0
  57. package/build/providers/claude-keychain.d.ts +13 -0
  58. package/build/providers/claude-keychain.js +42 -0
  59. package/build/providers/claude.d.ts +33 -0
  60. package/build/providers/claude.js +218 -0
  61. package/build/providers/codex.d.ts +21 -0
  62. package/build/providers/codex.js +221 -0
  63. package/build/providers/common.d.ts +63 -0
  64. package/build/providers/common.js +294 -0
  65. package/build/providers/cursor.d.ts +14 -0
  66. package/build/providers/cursor.js +141 -0
  67. package/build/providers/profiles.d.ts +10 -0
  68. package/build/providers/profiles.js +69 -0
  69. package/build/providers/tls-trust.d.ts +7 -0
  70. package/build/providers/tls-trust.js +29 -0
  71. package/build/providers/types.d.ts +19 -0
  72. package/build/providers/types.js +1 -0
  73. package/build/shared/account-state.d.ts +19 -0
  74. package/build/shared/account-state.js +75 -0
  75. package/build/shared/catalog.d.ts +85 -0
  76. package/build/shared/catalog.js +125 -0
  77. package/build/shared/format.d.ts +40 -0
  78. package/build/shared/format.js +134 -0
  79. package/build/shared/i18n/index.d.ts +12 -0
  80. package/build/shared/i18n/index.js +40 -0
  81. package/build/shared/i18n/locales/de.d.ts +2 -0
  82. package/build/shared/i18n/locales/de.js +328 -0
  83. package/build/shared/i18n/locales/en.d.ts +328 -0
  84. package/build/shared/i18n/locales/en.js +330 -0
  85. package/build/shared/i18n/locales/es.d.ts +2 -0
  86. package/build/shared/i18n/locales/es.js +328 -0
  87. package/build/shared/i18n/locales/fr.d.ts +2 -0
  88. package/build/shared/i18n/locales/fr.js +328 -0
  89. package/build/shared/i18n/locales/it.d.ts +2 -0
  90. package/build/shared/i18n/locales/it.js +327 -0
  91. package/build/shared/i18n/locales/ja.d.ts +2 -0
  92. package/build/shared/i18n/locales/ja.js +322 -0
  93. package/build/shared/i18n/locales/ko.d.ts +2 -0
  94. package/build/shared/i18n/locales/ko.js +322 -0
  95. package/build/shared/i18n/locales/nl.d.ts +2 -0
  96. package/build/shared/i18n/locales/nl.js +327 -0
  97. package/build/shared/i18n/locales/pl.d.ts +2 -0
  98. package/build/shared/i18n/locales/pl.js +332 -0
  99. package/build/shared/i18n/locales/pt.d.ts +2 -0
  100. package/build/shared/i18n/locales/pt.js +328 -0
  101. package/build/shared/i18n/locales/ru.d.ts +2 -0
  102. package/build/shared/i18n/locales/ru.js +331 -0
  103. package/build/shared/i18n/locales/tr.d.ts +2 -0
  104. package/build/shared/i18n/locales/tr.js +322 -0
  105. package/build/shared/i18n/locales/zh.d.ts +2 -0
  106. package/build/shared/i18n/locales/zh.js +322 -0
  107. package/build/shared/i18n/messages.d.ts +10 -0
  108. package/build/shared/i18n/messages.js +1 -0
  109. package/build/shared/ipc.d.ts +138 -0
  110. package/build/shared/ipc.js +2 -0
  111. package/build/shared/tsconfig.tsbuildinfo +1 -0
  112. package/build/shared/types.d.ts +205 -0
  113. package/build/shared/types.js +1 -0
  114. package/package.json +144 -4
@@ -0,0 +1,463 @@
1
+ import https from 'node:https';
2
+ import path from 'node:path';
3
+ import { ProviderError } from "../core/errors.js";
4
+ import { field, isObject, parseJson } from "../core/files.js";
5
+ import { hasControlChars, isoFromUnixSeconds, normalizeEmail, normalizeWindow, safeText } from "../core/model.js";
6
+ import { accountId, DURATION_MS, durationLabel, isOneOf, MAX_LABEL_LENGTH, MINUTES } from "../shared/catalog.js";
7
+ import { MAX_RESPONSE_BYTES, readJsonResponse, runProcess as defaultRunProcess, settleOnce, sha256Prefix, unpackedPath, windowsPowerShell } from "./common.js";
8
+ const MAX_SERVERS = 4;
9
+ const MAX_PORTS_PER_SERVER = 4;
10
+ const LOCAL_REQUEST_TIMEOUT_MS = 3000;
11
+ const MIN_REQUEST_TIMEOUT_MS = 100;
12
+ const DISCOVERY_BUDGET_MS = 6000;
13
+ const FETCH_BUDGET_MS = 12000;
14
+ const WINDOWS_DISCOVERY_TIMEOUT_MS = 12000;
15
+ const MAC_PROCESS_TIMEOUT_MS = 4000;
16
+ const ID_HASH_LENGTH = 24;
17
+ const ACCOUNT_HASH_LENGTH = 16;
18
+ // Two readings whose resets differ by less than this describe the same quota period.
19
+ const SAME_PERIOD_TOLERANCE_MS = DURATION_MS.MINUTE;
20
+ const RPC_METHODS = { quota: 'RetrieveUserQuotaSummary', status: 'GetUserStatus' };
21
+ const RPC_METHOD_NAMES = new Set(Object.values(RPC_METHODS));
22
+ const RPC_PATH_PREFIX = '/exa.language_server_pb.LanguageServerService/';
23
+ // Connect error codes (gRPC numbering) the language server returns in the body.
24
+ const CONNECT_CODES = { UNAUTHENTICATED: 16, RESOURCE_EXHAUSTED: 8 };
25
+ const SUCCESS_CODES = [undefined, null, 0, '0', 'ok', 'success'];
26
+ const FIVE_HOUR_CADENCE = /^(5h|session|5-hour|five hour|five-hour)$/;
27
+ const WEEKLY_CADENCE = 'weekly';
28
+ // What discover-antigravity.ps1 reports as its `outcome`; the script writes the same values.
29
+ export const DISCOVERY_OUTCOMES = ['success', 'app_closed', 'local_service_unavailable'];
30
+ const DISCOVERY_SCRIPT = unpackedPath(path.resolve(import.meta.dirname, '../launchers/discover-antigravity.ps1'));
31
+ function checkedCode(value) {
32
+ if (SUCCESS_CODES.includes(typeof value === 'string' ? value.toLowerCase() : value))
33
+ return;
34
+ if (value === CONNECT_CODES.UNAUTHENTICATED || value === 'UNAUTHENTICATED')
35
+ throw new ProviderError('auth_rejected');
36
+ if (value === CONNECT_CODES.RESOURCE_EXHAUSTED || value === 'RESOURCE_EXHAUSTED')
37
+ throw new ProviderError('rate_limited');
38
+ throw new ProviderError('local_service_unavailable');
39
+ }
40
+ // Reset times arrive as an ISO string or as epoch seconds in a decimal string.
41
+ function rawReset(value) {
42
+ return typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value) ? isoFromUnixSeconds(Number(value)) : value;
43
+ }
44
+ const fractionPercent = (value) => (typeof value === 'number' && Number.isFinite(value) ? value * 100 : null);
45
+ // The first non-blank string among the values, as a stable identity component.
46
+ function sourceKey(...values) {
47
+ const value = values.find((v) => typeof v === 'string' && v.trim() !== '');
48
+ return value?.trim().normalize('NFC') ?? '';
49
+ }
50
+ function stableWindowId(kind, ...parts) {
51
+ return `${kind}:${sha256Prefix(JSON.stringify(parts), ID_HASH_LENGTH)}`;
52
+ }
53
+ // The language server has reported the signed-in e-mail under several shapes.
54
+ function extractEmail(raw) {
55
+ const email = [
56
+ field(field(raw, 'userStatus'), 'email'),
57
+ field(field(field(raw, 'response'), 'userStatus'), 'email'),
58
+ field(raw, 'userEmail'),
59
+ field(raw, 'email'),
60
+ field(field(raw, 'user'), 'email')
61
+ ].find(value => typeof value === 'string' && value.trim());
62
+ return typeof email === 'string' ? normalizeEmail(email) : null;
63
+ }
64
+ function cadenceLabel(cadence, fallbackName) {
65
+ if (FIVE_HOUR_CADENCE.test(cadence))
66
+ return durationLabel(MINUTES.FIVE_HOURS);
67
+ if (cadence === WEEKLY_CADENCE)
68
+ return durationLabel(MINUTES.WEEK);
69
+ return { label: fallbackName };
70
+ }
71
+ // Bucket remaining fraction, in any of the encodings servers have used.
72
+ function bucketRemaining(bucket) {
73
+ const { remaining } = bucket;
74
+ const nested = isObject(remaining)
75
+ ? (remaining.remainingFraction ?? (remaining.case === 'remainingFraction' ? remaining.value : null))
76
+ : null;
77
+ return bucket.remainingFraction ?? nested;
78
+ }
79
+ // Uniquely identified windows: a repeated identity means the response is not understood.
80
+ function addUnique(ids, id) {
81
+ if (ids.has(id))
82
+ throw new ProviderError('invalid_response');
83
+ ids.add(id);
84
+ return id;
85
+ }
86
+ export function parseAntigravitySummary(raw) {
87
+ checkedCode(field(raw, 'code'));
88
+ const groups = field(field(raw, 'response'), 'groups') ?? field(field(raw, 'summary'), 'groups') ?? field(raw, 'groups');
89
+ if (!Array.isArray(groups))
90
+ throw new ProviderError('invalid_response');
91
+ const windows = [];
92
+ const ids = new Set();
93
+ for (const group of groups.filter(isObject)) {
94
+ if (!Array.isArray(group.buckets))
95
+ continue;
96
+ const groupKey = sourceKey(group.groupId, group.id, group.displayName, group.name);
97
+ for (const bucket of group.buckets.filter(isObject)) {
98
+ const bucketKey = sourceKey(bucket.bucketId, bucket.id, bucket.window, bucket.displayName, bucket.name);
99
+ if (!bucketKey)
100
+ throw new ProviderError('invalid_response');
101
+ const name = sourceKey(bucket.displayName, bucket.name) || bucketKey;
102
+ const cadence = safeText(sourceKey(bucket.window, bucket.displayName, bucket.name) || bucketKey).toLowerCase();
103
+ windows.push(normalizeWindow({
104
+ id: addUnique(ids, stableWindowId('quota', groupKey, bucketKey, sourceKey(bucket.window))),
105
+ ...cadenceLabel(cadence, safeText(name)),
106
+ scope: safeText(sourceKey(group.displayName, group.name)),
107
+ detailKey: 'antigravitySourceNote',
108
+ remainingPercent: fractionPercent(bucketRemaining(bucket)),
109
+ resetsAt: rawReset(bucket.resetTime),
110
+ active: typeof bucket.disabled === 'boolean' ? !bucket.disabled : undefined
111
+ }));
112
+ }
113
+ }
114
+ if (!windows.length)
115
+ throw new ProviderError('invalid_response');
116
+ return { windows, email: extractEmail(raw) };
117
+ }
118
+ export function parseAntigravityUserStatus(raw) {
119
+ checkedCode(field(raw, 'code'));
120
+ const models = field(field(field(raw, 'userStatus'), 'cascadeModelConfigData'), 'clientModelConfigs') ?? field(raw, 'clientModelConfigs');
121
+ if (!Array.isArray(models))
122
+ throw new ProviderError('invalid_response');
123
+ const ids = new Set();
124
+ const windows = models.filter(isObject).flatMap(model => {
125
+ const quota = model.quotaInfo;
126
+ if (!isObject(quota))
127
+ return [];
128
+ const modelKey = sourceKey(field(model.modelOrAlias, 'model'), model.modelId, model.label, model.displayName);
129
+ if (!modelKey)
130
+ throw new ProviderError('invalid_response');
131
+ const label = safeText(sourceKey(model.label, model.displayName) || modelKey);
132
+ return [
133
+ normalizeWindow({
134
+ id: addUnique(ids, stableWindowId('model', modelKey)),
135
+ label,
136
+ scope: label,
137
+ detailKey: 'modelQuotaNoPeriod',
138
+ remainingPercent: fractionPercent(quota.remainingFraction),
139
+ resetsAt: rawReset(quota.resetTime)
140
+ })
141
+ ];
142
+ });
143
+ if (!windows.length)
144
+ throw new ProviderError('invalid_response');
145
+ return { windows, email: extractEmail(raw) };
146
+ }
147
+ const MAX_TOKEN_LENGTH = 512;
148
+ const MAX_LSOF_BYTES = 64 * 1024;
149
+ const MAX_PS_BYTES = 4 * 1024 * 1024;
150
+ const MAX_TCP_PORT = 65535;
151
+ const isPositiveInteger = (value) => typeof value === 'number' && Number.isInteger(value) && value > 0;
152
+ const isPort = (value) => isPositiveInteger(value) && value <= MAX_TCP_PORT;
153
+ const isPid = isPositiveInteger;
154
+ // The CSRF token is sent as a header; it must be a single visible token.
155
+ const isSafeToken = (value) => typeof value === 'string' && value.length > 0 && value.length <= MAX_TOKEN_LENGTH && !hasControlChars(value) && !value.includes(' ');
156
+ function validateServers(value) {
157
+ if (!Array.isArray(value))
158
+ throw new ProviderError('local_service_unavailable');
159
+ return value
160
+ .slice(0, MAX_SERVERS)
161
+ .filter(isObject)
162
+ .flatMap(server => {
163
+ const { pid, csrfToken, ports } = server;
164
+ if (!isPid(pid) || !isSafeToken(csrfToken) || !Array.isArray(ports))
165
+ return [];
166
+ const owned = [...new Set(ports.filter(isPort))].slice(0, MAX_PORTS_PER_SERVER);
167
+ return owned.length ? [{ pid, csrfToken, ports: owned }] : [];
168
+ });
169
+ }
170
+ // Antigravity's language server is recognized by its executable name, the app bundle it runs
171
+ // from and its app-data flag; only ports that the same PID listens on are trusted.
172
+ const LANGUAGE_SERVER = /(?:^|[/\\])language(?:_|-)server(?:[_-][a-z0-9]+)*(?:\.exe)?(?:["\s]|$)/;
173
+ const ANTIGRAVITY_BUNDLE = /\/(?:antigravity|antigravity ide|gemini)\.app\//;
174
+ const APP_DATA_DIR_FLAG = /(?:^|\s)--app_data_dir(?:=|\s)/;
175
+ const CSRF_FLAG = /(?:^|\s)--csrf_token(?:=|\s+)(?:"([^"\r\n]+)"|([^\s]+))/;
176
+ const PS_LINE = /^\s*(\d+)\s+(\d+)\s+(.+)$/;
177
+ export function parseMacProcessList(output, currentUid) {
178
+ if (typeof currentUid !== 'number' || !Number.isInteger(currentUid) || currentUid < 0)
179
+ return [];
180
+ const servers = [];
181
+ for (const line of output.split(/\r?\n/)) {
182
+ const [, pid, uid, command = ''] = PS_LINE.exec(line) ?? [];
183
+ if (Number(uid) !== currentUid)
184
+ continue;
185
+ const lower = command.toLowerCase();
186
+ const ownedByAntigravity = ANTIGRAVITY_BUNDLE.test(lower) || lower.includes('/antigravity/') || (APP_DATA_DIR_FLAG.test(lower) && lower.includes('antigravity'));
187
+ // Only the running app's servers carry a CSRF token; tokenless CLI processes are skipped.
188
+ if (!LANGUAGE_SERVER.test(lower) || !ownedByAntigravity)
189
+ continue;
190
+ const csrf = CSRF_FLAG.exec(command);
191
+ const csrfToken = csrf?.[1] ?? csrf?.[2];
192
+ if (isSafeToken(csrfToken))
193
+ servers.push({ pid: Number(pid), csrfToken });
194
+ if (servers.length >= MAX_SERVERS)
195
+ break;
196
+ }
197
+ return servers;
198
+ }
199
+ const LSOF_OWNER = /^\S+\s+(\d+)\s+\S+\s+\S+\s+IPv4\s+/;
200
+ // Requests go to exactly 127.0.0.1. A LAN address, another 127/8 address or an IPv6-only
201
+ // socket does not prove ownership of that target.
202
+ const LSOF_LISTENER = /\bTCP\s+(?:127\.0\.0\.1|0\.0\.0\.0|\*):(\d+)\s+\(LISTEN\)\s*$/;
203
+ export function parseListeningPorts(output, expectedPid) {
204
+ if (!isPid(expectedPid))
205
+ return [];
206
+ const ports = new Set();
207
+ for (const line of output.split(/\r?\n/)) {
208
+ const owner = LSOF_OWNER.exec(line.trim());
209
+ if (Number(owner?.[1]) !== expectedPid)
210
+ continue;
211
+ const port = Number(LSOF_LISTENER.exec(line)?.[1]);
212
+ if (isPort(port))
213
+ ports.add(port);
214
+ }
215
+ return [...ports].sort((a, b) => a - b);
216
+ }
217
+ async function discoverWindowsServers(run, env) {
218
+ let output;
219
+ try {
220
+ const stdout = await run(windowsPowerShell(env), ['-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', DISCOVERY_SCRIPT], { timeoutMs: WINDOWS_DISCOVERY_TIMEOUT_MS, maxBytes: MAX_LSOF_BYTES });
221
+ output = parseJson(stdout);
222
+ }
223
+ catch (error) {
224
+ throw error instanceof ProviderError ? error : new ProviderError('local_service_unavailable', { cause: error });
225
+ }
226
+ const outcome = field(output, 'outcome');
227
+ if (!isOneOf(DISCOVERY_OUTCOMES, outcome))
228
+ throw new ProviderError('local_service_unavailable');
229
+ // The other outcomes are the provider errors of the same name.
230
+ if (outcome !== 'success')
231
+ throw new ProviderError(outcome);
232
+ return field(output, 'servers');
233
+ }
234
+ async function discoverMacServers(run, currentUid) {
235
+ if (currentUid === undefined)
236
+ throw new ProviderError('local_service_unavailable');
237
+ const processes = await run('/bin/ps', ['-ax', '-o', 'pid=,uid=,command='], {
238
+ timeoutMs: MAC_PROCESS_TIMEOUT_MS,
239
+ maxBytes: MAX_PS_BYTES
240
+ });
241
+ const entries = parseMacProcessList(processes, currentUid);
242
+ if (!entries.length)
243
+ throw new ProviderError('app_closed');
244
+ // A server whose ports cannot be listed is dropped: there is no fallback to unowned ports.
245
+ const listed = await Promise.all(entries.map(entry => run('/usr/sbin/lsof', ['-nP', '-i4TCP', '-sTCP:LISTEN', '-a', '-p', String(entry.pid)], {
246
+ timeoutMs: MAC_PROCESS_TIMEOUT_MS,
247
+ maxBytes: MAX_LSOF_BYTES
248
+ }).then(output => ({ ...entry, ports: parseListeningPorts(output, entry.pid) }), () => null)));
249
+ return listed.filter(Boolean);
250
+ }
251
+ export async function discoverAntigravityServers({ runProcess = defaultRunProcess, platform = process.platform, env = process.env, currentUid = process.geteuid?.() } = {}) {
252
+ let raw;
253
+ if (platform === 'win32')
254
+ raw = await discoverWindowsServers(runProcess, env);
255
+ else if (platform === 'darwin')
256
+ raw = await discoverMacServers(runProcess, currentUid);
257
+ else
258
+ throw new ProviderError('unsupported_platform');
259
+ const servers = validateServers(raw);
260
+ if (!servers.length)
261
+ throw new ProviderError('local_service_unavailable');
262
+ return servers;
263
+ }
264
+ function isAllowedTarget(server, port, method) {
265
+ return isPid(server.pid) && isPort(port) && server.ports.includes(port) && isSafeToken(server.csrfToken) && RPC_METHOD_NAMES.has(method);
266
+ }
267
+ function requestBody(method) {
268
+ return JSON.stringify(method === RPC_METHODS.quota
269
+ ? { forceRefresh: true }
270
+ : { metadata: { ideName: 'antigravity', extensionName: 'antigravity', locale: 'en', ideVersion: 'unknown' } });
271
+ }
272
+ // The local language server listens on 127.0.0.1 with a self-signed certificate; access is
273
+ // scoped by the per-process CSRF token and the PID-owned port, not by TLS identity.
274
+ export const requestLocalJson = (server, port, method, options = {}) => new Promise((resolve, reject) => {
275
+ if (!isAllowedTarget(server, port, method)) {
276
+ reject(new ProviderError('endpoint_not_allowed'));
277
+ return;
278
+ }
279
+ const timeoutMs = Math.max(1, Math.min(options.timeoutMs ?? LOCAL_REQUEST_TIMEOUT_MS, LOCAL_REQUEST_TIMEOUT_MS));
280
+ const maxBytes = Math.min(options.maxBytes ?? MAX_RESPONSE_BYTES, MAX_RESPONSE_BYTES);
281
+ const body = requestBody(method);
282
+ const settle = settleOnce(resolve, reject, timeoutMs);
283
+ try {
284
+ const request = settle.attach((options.request ?? https.request)({
285
+ hostname: '127.0.0.1',
286
+ port,
287
+ path: `${RPC_PATH_PREFIX}${method}`,
288
+ method: 'POST',
289
+ agent: false,
290
+ rejectUnauthorized: false,
291
+ timeout: timeoutMs,
292
+ headers: {
293
+ 'Content-Type': 'application/json',
294
+ 'Content-Length': Buffer.byteLength(body),
295
+ 'Connect-Protocol-Version': '1',
296
+ 'X-Codeium-Csrf-Token': server.csrfToken
297
+ }
298
+ }, response => {
299
+ readJsonResponse(settle.attach(response), settle, maxBytes);
300
+ }));
301
+ request.on('error', () => {
302
+ settle.fail(new ProviderError('transport_error'));
303
+ });
304
+ request.on('timeout', () => {
305
+ settle.fail(new ProviderError('request_timeout'));
306
+ });
307
+ request.end(body);
308
+ }
309
+ catch {
310
+ settle.fail(new ProviderError('transport_error'));
311
+ }
312
+ });
313
+ export function antigravityAccountId(email) {
314
+ return accountId('antigravity', sha256Prefix(normalizeEmail(email), ACCOUNT_HASH_LENGTH));
315
+ }
316
+ const timeLeft = (deadline) => Math.min(LOCAL_REQUEST_TIMEOUT_MS, Math.max(MIN_REQUEST_TIMEOUT_MS, deadline - Date.now()));
317
+ const asProviderError = (error) => error instanceof ProviderError ? error : new ProviderError('local_service_unavailable', { cause: error });
318
+ // Rate limiting is account-wide: retrying another port would only extend the cooldown.
319
+ function rethrowIfRateLimited(error) {
320
+ if (error instanceof ProviderError && error.code === 'rate_limited')
321
+ throw error;
322
+ }
323
+ async function fetchServerWindows(server, read, deadline, needsEmail) {
324
+ let lastError = null;
325
+ const request = (port, method) => read(server, port, method, { timeoutMs: timeLeft(deadline) });
326
+ for (const port of server.ports) {
327
+ if (Date.now() >= deadline)
328
+ throw new ProviderError('request_timeout');
329
+ let email = null;
330
+ let status = null;
331
+ if (needsEmail) {
332
+ // Identity first: with several servers, windows are only usable once tied to an account.
333
+ try {
334
+ status = await request(port, RPC_METHODS.status);
335
+ email = extractEmail(status);
336
+ }
337
+ catch (error) {
338
+ rethrowIfRateLimited(error);
339
+ lastError = asProviderError(error);
340
+ }
341
+ if (!email)
342
+ continue;
343
+ }
344
+ try {
345
+ const reading = parseAntigravitySummary(await request(port, RPC_METHODS.quota));
346
+ return { windows: reading.windows, email: email ?? reading.email };
347
+ }
348
+ catch (error) {
349
+ rethrowIfRateLimited(error);
350
+ // Older servers lack the quota summary; per-model quota from the status call is the fallback.
351
+ try {
352
+ const reading = parseAntigravityUserStatus(status ?? (await request(port, RPC_METHODS.status)));
353
+ return { windows: reading.windows, email: email ?? reading.email };
354
+ }
355
+ catch {
356
+ // The fallback failed as well; the original error is the one worth reporting.
357
+ }
358
+ lastError = asProviderError(error);
359
+ }
360
+ }
361
+ throw lastError ?? new ProviderError('local_service_unavailable');
362
+ }
363
+ // When the app and the IDE both report a window: a later reset wins (it is the newer period);
364
+ // within the same period the lower remaining value wins, so quota is never overstated.
365
+ function preferIncoming(existing, incoming) {
366
+ const resetExisting = Date.parse(existing.resetsAt ?? '');
367
+ const resetIncoming = Date.parse(incoming.resetsAt ?? '');
368
+ const bothReset = Number.isFinite(resetIncoming) && Number.isFinite(resetExisting);
369
+ if (bothReset && Math.abs(resetIncoming - resetExisting) > SAME_PERIOD_TOLERANCE_MS)
370
+ return resetIncoming > resetExisting;
371
+ if (Number.isFinite(resetIncoming) !== Number.isFinite(resetExisting))
372
+ return Number.isFinite(resetIncoming);
373
+ if (incoming.remainingPercent === null)
374
+ return false;
375
+ return existing.remainingPercent === null || incoming.remainingPercent < existing.remainingPercent;
376
+ }
377
+ function mergeWindows(readings) {
378
+ const merged = new Map();
379
+ for (const window of readings.flatMap(reading => reading.windows)) {
380
+ const existing = merged.get(window.id);
381
+ if (!existing || preferIncoming(existing, window))
382
+ merged.set(window.id, window);
383
+ }
384
+ return [...merged.values()];
385
+ }
386
+ async function resolveServerEmails(servers, read) {
387
+ const deadline = Date.now() + DISCOVERY_BUDGET_MS;
388
+ return Promise.allSettled(servers.map(async (server) => {
389
+ let lastError = null;
390
+ for (const port of server.ports) {
391
+ try {
392
+ const email = extractEmail(await read(server, port, RPC_METHODS.status, { timeoutMs: timeLeft(deadline) }));
393
+ if (email)
394
+ return email;
395
+ }
396
+ catch (error) {
397
+ lastError = asProviderError(error);
398
+ }
399
+ }
400
+ if (lastError)
401
+ throw lastError;
402
+ return null;
403
+ }));
404
+ }
405
+ const fulfilled = (results) => results.flatMap(result => (result.status === 'fulfilled' ? [result.value] : []));
406
+ export function createAntigravityProvider(options = {}) {
407
+ const discoverServers = () => (options.discoverAntigravityServers ?? discoverAntigravityServers)(options);
408
+ const read = options.requestLocalJson ?? requestLocalJson;
409
+ return {
410
+ id: 'antigravity',
411
+ // Finding the servers (PowerShell on Windows), then reading them.
412
+ fetchTimeoutMs: Math.max(WINDOWS_DISCOVERY_TIMEOUT_MS, DISCOVERY_BUDGET_MS) + FETCH_BUDGET_MS,
413
+ async discover() {
414
+ const servers = await discoverServers();
415
+ if (!servers.length)
416
+ throw new ProviderError('app_closed');
417
+ const queries = await resolveServerEmails(servers, read);
418
+ const emails = fulfilled(queries);
419
+ // Fail closed: an unidentified server could belong to any account.
420
+ if (emails.length !== servers.length || emails.some(email => !email))
421
+ throw new ProviderError('local_service_unavailable');
422
+ return [...new Set(emails.filter(email => email !== null))].map((email, index) => ({
423
+ id: antigravityAccountId(email),
424
+ providerId: 'antigravity',
425
+ label: email.slice(0, MAX_LABEL_LENGTH),
426
+ email,
427
+ primary: index === 0,
428
+ enabled: true
429
+ }));
430
+ },
431
+ async fetch(account) {
432
+ if (account.configDir)
433
+ throw new ProviderError('unsupported_profile');
434
+ const servers = await discoverServers();
435
+ if (!servers.length)
436
+ throw new ProviderError('app_closed');
437
+ const deadline = Date.now() + FETCH_BUDGET_MS;
438
+ const needsEmail = Boolean(account.email) || servers.length > 1;
439
+ const queries = await Promise.allSettled(servers.map(server => fetchServerWindows(server, read, deadline, needsEmail)));
440
+ let readings = fulfilled(queries);
441
+ if (!readings.length) {
442
+ const rejected = queries.find(query => query.status === 'rejected');
443
+ throw rejected?.reason instanceof ProviderError ? rejected.reason : new ProviderError('local_service_unavailable');
444
+ }
445
+ // Several servers can only be merged when every one of them is tied to an account.
446
+ if (servers.length > 1 && (readings.length !== servers.length || readings.some(reading => !reading.email)))
447
+ throw new ProviderError('local_service_unavailable');
448
+ const accountEmail = account.email === undefined ? undefined : normalizeEmail(account.email);
449
+ if (accountEmail) {
450
+ readings = readings.filter(reading => reading.email === accountEmail);
451
+ if (!readings.length)
452
+ throw new ProviderError('auth_missing');
453
+ }
454
+ else if (new Set(readings.map(reading => reading.email).filter(Boolean)).size > 1) {
455
+ throw new ProviderError('unsupported_profile');
456
+ }
457
+ const windows = mergeWindows(readings);
458
+ if (!windows.length)
459
+ throw new ProviderError('invalid_response');
460
+ return { windows };
461
+ }
462
+ };
463
+ }
@@ -0,0 +1,13 @@
1
+ import { type JsonObject } from '../core/files.ts';
2
+ import { type RunProcess } from './common.ts';
3
+ export interface KeychainSelector {
4
+ configDir?: string;
5
+ configDirExplicit?: boolean;
6
+ secureStorageConfigDir?: string;
7
+ }
8
+ export declare function resolveClaudeKeychainService({ configDir, configDirExplicit, secureStorageConfigDir }?: KeychainSelector): string;
9
+ export interface KeychainOptions extends KeychainSelector {
10
+ platform?: NodeJS.Platform;
11
+ runProcess?: RunProcess;
12
+ }
13
+ export declare function readClaudeKeychain({ platform, runProcess, ...selector }?: KeychainOptions): Promise<JsonObject>;
@@ -0,0 +1,42 @@
1
+ import { ProviderError } from "../core/errors.js";
2
+ import { isObject } from "../core/files.js";
3
+ import { hasControlChars } from "../core/model.js";
4
+ import { MAX_RESPONSE_BYTES, runProcess as defaultRunProcess, sha256Prefix } from "./common.js";
5
+ const DEFAULT_SERVICE = 'Claude Code-credentials';
6
+ const SELECTOR_MAX_LENGTH = 1024;
7
+ const SELECTOR_HASH_LENGTH = 8;
8
+ const KEYCHAIN_TIMEOUT_MS = 8000;
9
+ // The Keychain item Claude Code uses for a profile. The selector is the raw configured
10
+ // directory, not a resolved path: an explicitly configured default directory is a different
11
+ // item than the unset default. Verified against quota-axi (src/lib/claude-profile.ts,
12
+ // suffixedKeychainService) and anthropics/claude-code#92252, #79223.
13
+ export function resolveClaudeKeychainService({ configDir, configDirExplicit = false, secureStorageConfigDir } = {}) {
14
+ const selected = secureStorageConfigDir || (configDirExplicit ? configDir : undefined);
15
+ if (!selected)
16
+ return DEFAULT_SERVICE;
17
+ if (selected.length > SELECTOR_MAX_LENGTH || hasControlChars(selected))
18
+ throw new ProviderError('unsupported_profile');
19
+ const suffix = sha256Prefix(selected.normalize('NFC'), SELECTOR_HASH_LENGTH);
20
+ return `${DEFAULT_SERVICE}-${suffix}`;
21
+ }
22
+ // The parsed credentials document stored for the profile; the caller checks its contents.
23
+ // Read only: the item is never refreshed or written.
24
+ export async function readClaudeKeychain({ platform = process.platform, runProcess = defaultRunProcess, ...selector } = {}) {
25
+ if (platform !== 'darwin')
26
+ throw new ProviderError('unsupported_platform');
27
+ const service = resolveClaudeKeychainService(selector);
28
+ let credentials;
29
+ try {
30
+ const output = await runProcess('/usr/bin/security', ['find-generic-password', '-s', service, '-w'], {
31
+ timeoutMs: KEYCHAIN_TIMEOUT_MS,
32
+ maxBytes: MAX_RESPONSE_BYTES
33
+ });
34
+ credentials = JSON.parse(output);
35
+ }
36
+ catch (error) {
37
+ throw new ProviderError('keychain_unavailable', { cause: error });
38
+ }
39
+ if (!isObject(credentials))
40
+ throw new ProviderError('keychain_unavailable');
41
+ return credentials;
42
+ }
@@ -0,0 +1,33 @@
1
+ import { type JsonObject } from '../core/files.ts';
2
+ import type { Account } from '../shared/types.ts';
3
+ import { type Clock, type GetJson, type RunProcess } from './common.ts';
4
+ import type { DiscoveredAccount, ParsedQuota, ProviderAdapter } from './types.ts';
5
+ export declare function parseClaudeUsage(usage: unknown): ParsedQuota;
6
+ export declare const DEFAULT_ACCOUNT_ID: string;
7
+ export declare const SYNTHETIC_MODEL = "<synthetic>";
8
+ export declare const CLAUDE_FILES: {
9
+ readonly credentials: ".credentials.json";
10
+ readonly settings: "settings.json";
11
+ readonly localSettings: "settings.local.json";
12
+ readonly state: ".claude.json";
13
+ readonly projects: "projects";
14
+ readonly sessions: "sessions";
15
+ };
16
+ export declare const claudeOAuthSection: (credentials: unknown) => JsonObject | null;
17
+ export declare function claudeProfile(configDir: string, home: string): DiscoveredAccount;
18
+ export declare function isSignedInProfile(root: string, platform?: NodeJS.Platform): Promise<boolean>;
19
+ export declare function discoverClaudeAccounts({ home, platform }?: {
20
+ home?: string;
21
+ platform?: NodeJS.Platform;
22
+ }): Promise<DiscoveredAccount[]>;
23
+ export interface ClaudeProviderOptions {
24
+ home?: string;
25
+ platform?: NodeJS.Platform;
26
+ clock?: Clock;
27
+ getJson?: GetJson;
28
+ runProcess?: RunProcess;
29
+ readCredentials?: (account: Account) => Promise<unknown>;
30
+ }
31
+ export declare function readClaudeCredentials(account: Account, options: ClaudeProviderOptions): Promise<JsonObject>;
32
+ export declare function claudeStateFile(account: Account, home?: string): string;
33
+ export declare function createClaudeProvider(options?: ClaudeProviderOptions): ProviderAdapter;