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,218 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { ProviderError } from "../core/errors.js";
4
+ import { asArray, isFile, isObject, readJson, samePath } from "../core/files.js";
5
+ import { normalizeWindow, safeEmail, safePlan, safeText } from "../core/model.js";
6
+ import { accountId, DEFAULT_ACCOUNT_KEY, durationLabel, MINUTES, PROFILE_DIRS } from "../shared/catalog.js";
7
+ import { readClaudeKeychain } from "./claude-keychain.js";
8
+ import { getJson as defaultGetJson, isExpired, QUOTA_ENDPOINTS, readCredentialFile, USER_AGENT } from "./common.js";
9
+ import { defaultProfileDir, isRegularFile, profileAccountId, profileFolders, profileLabel, shortHash } from "./profiles.js";
10
+ // Required by the OAuth usage endpoint.
11
+ const OAUTH_BETA = 'oauth-2025-04-20';
12
+ // Fixed windows the usage endpoint reports at top level; Opus/Sonnet are per-model scopes.
13
+ const USAGE_WINDOWS = {
14
+ five_hour: { minutes: MINUTES.FIVE_HOURS, required: true },
15
+ seven_day: { minutes: MINUTES.WEEK, required: true },
16
+ seven_day_opus: { minutes: MINUTES.WEEK, scope: 'Opus' },
17
+ seven_day_sonnet: { minutes: MINUTES.WEEK, scope: 'Sonnet' }
18
+ };
19
+ // `limits[].kind` values that duplicate a top-level window when they carry no model scope.
20
+ const CANONICAL_LIMIT_KINDS = { session: 'five_hour', weekly_all: 'seven_day' };
21
+ const LIMIT_KIND_MINUTES = { session: MINUTES.FIVE_HOURS };
22
+ const WEEKLY_KIND_PREFIX = 'weekly';
23
+ function limitPeriod(kind) {
24
+ if (kind.startsWith(WEEKLY_KIND_PREFIX))
25
+ return durationLabel(MINUTES.WEEK);
26
+ const minutes = LIMIT_KIND_MINUTES[kind];
27
+ return minutes ? durationLabel(minutes) : { label: kind.replaceAll('_', ' ') };
28
+ }
29
+ // A limit's model scope: the model's display name or id, or a plain scope string.
30
+ function limitScope(record) {
31
+ if (typeof record.scope === 'string')
32
+ return { scope: safeText(record.scope), modelId: undefined };
33
+ const model = isObject(record.scope) && isObject(record.scope.model) ? record.scope.model : {};
34
+ return { scope: safeText(model.display_name) || safeText(model.id), modelId: model.id };
35
+ }
36
+ export function parseClaudeUsage(usage) {
37
+ if (!isObject(usage))
38
+ throw new ProviderError('invalid_response');
39
+ const windows = [];
40
+ const limits = asArray(usage.limits).filter(isObject);
41
+ const listed = new Set(limits.map(record => CANONICAL_LIMIT_KINDS[safeText(record.kind)]).filter(Boolean));
42
+ for (const [id, spec] of Object.entries(USAGE_WINDOWS)) {
43
+ const value = usage[id];
44
+ const reported = isObject(value) ? value : undefined;
45
+ // Reported as null and absent from the limits too: the plan has no such limit (no weekly one,
46
+ // say), which is not a limit of unknown size.
47
+ const none = value === null && limits.length > 0 && !listed.has(id);
48
+ if ((!spec.required && !reported) || none)
49
+ continue;
50
+ windows.push(normalizeWindow({
51
+ id,
52
+ ...durationLabel(spec.minutes),
53
+ usedPercent: reported?.utilization,
54
+ resetsAt: reported?.resets_at,
55
+ ...(spec.scope ? { scope: spec.scope } : {})
56
+ }));
57
+ }
58
+ for (const record of limits) {
59
+ const kind = safeText(record.kind, 'unknown');
60
+ const { scope, modelId } = limitScope(record);
61
+ const canonicalId = scope ? undefined : CANONICAL_LIMIT_KINDS[kind];
62
+ const identity = [record.id, modelId, scope, record.window, record.period].find((value) => (typeof value === 'string' || typeof value === 'number') && Boolean(value));
63
+ const window = normalizeWindow({
64
+ id: canonicalId ?? `limit:${kind}:${shortHash(String(identity ?? 'general'))}`,
65
+ ...limitPeriod(kind),
66
+ usedPercent: record.percent,
67
+ resetsAt: record.resets_at,
68
+ ...(scope ? { scope } : {}),
69
+ active: record.is_active
70
+ });
71
+ const canonical = canonicalId ? windows.find(w => w.id === canonicalId) : undefined;
72
+ if (!canonical)
73
+ windows.push(window);
74
+ else if (canonical.usedPercent === null && window.usedPercent !== null)
75
+ Object.assign(canonical, window);
76
+ else if (typeof record.is_active === 'boolean')
77
+ canonical.active = record.is_active;
78
+ }
79
+ return { windows };
80
+ }
81
+ const SECONDARY_PROFILE_DIR = `${PROFILE_DIRS.claude}-secondary`;
82
+ export const DEFAULT_ACCOUNT_ID = accountId('claude', DEFAULT_ACCOUNT_KEY);
83
+ const SECONDARY_ACCOUNT_ID = accountId('claude', 'secondary');
84
+ // Model name Claude Code records on messages it generated itself (API errors, limit notices).
85
+ export const SYNTHETIC_MODEL = '<synthetic>';
86
+ // What Claude Code keeps in a profile folder (and in a project's own `.claude` folder).
87
+ export const CLAUDE_FILES = {
88
+ credentials: '.credentials.json',
89
+ settings: 'settings.json',
90
+ localSettings: 'settings.local.json',
91
+ // The state file, which records the signed-in account.
92
+ state: '.claude.json',
93
+ // One folder of session transcripts per project.
94
+ projects: 'projects',
95
+ // The running processes, each listing itself (see claude-processes.ts).
96
+ sessions: 'sessions'
97
+ };
98
+ // The section of Claude Code's credentials (file or Keychain item) that holds its sign-in.
99
+ export const claudeOAuthSection = (credentials) => isObject(credentials) && isObject(credentials.claudeAiOauth) ? credentials.claudeAiOauth : null;
100
+ // The OAuth section of a credentials document, when it holds an access token.
101
+ function oauthCredentials(credentials) {
102
+ const oauth = claudeOAuthSection(credentials);
103
+ if (!oauth || typeof oauth.accessToken !== 'string' || !oauth.accessToken.trim())
104
+ return null;
105
+ return { accessToken: oauth.accessToken, expiresAt: oauth.expiresAt, subscriptionType: oauth.subscriptionType };
106
+ }
107
+ // The account a Claude profile folder holds. Any folder but the default one is explicit: Claude
108
+ // Code then runs with CLAUDE_CONFIG_DIR set, which selects a different Keychain item on macOS.
109
+ export function claudeProfile(configDir, home) {
110
+ const resolved = path.resolve(configDir);
111
+ const configDirExplicit = resolved !== path.resolve(defaultProfileDir('claude', home));
112
+ const isSecondary = path.basename(configDir) === SECONDARY_PROFILE_DIR;
113
+ const id = !configDirExplicit ? DEFAULT_ACCOUNT_ID : isSecondary ? SECONDARY_ACCOUNT_ID : profileAccountId('claude', resolved);
114
+ const naming = !configDirExplicit
115
+ ? { labelKey: 'accountA' }
116
+ : isSecondary
117
+ ? { labelKey: 'accountB' }
118
+ : { label: profileLabel('claude', resolved) };
119
+ return {
120
+ id,
121
+ providerId: 'claude',
122
+ ...naming,
123
+ configDir: resolved,
124
+ configDirExplicit,
125
+ primary: !configDirExplicit,
126
+ enabled: true
127
+ };
128
+ }
129
+ // A profile counts when it holds a signed-in credentials file. On macOS credentials live in
130
+ // the Keychain, so a configured profile (settings.json present) counts as well.
131
+ export async function isSignedInProfile(root, platform = process.platform) {
132
+ if (platform === 'darwin' && (await isFile(path.join(root, CLAUDE_FILES.settings))))
133
+ return true;
134
+ const file = path.join(root, CLAUDE_FILES.credentials);
135
+ if (!(await isRegularFile(file)))
136
+ return false;
137
+ return oauthCredentials(await readCredentialFile(file).catch(() => null)) !== null;
138
+ }
139
+ // Default first, then the secondary profile, then the rest by id.
140
+ const PROFILE_RANK = { [DEFAULT_ACCOUNT_ID]: 0, [SECONDARY_ACCOUNT_ID]: 1 };
141
+ const rank = (account) => PROFILE_RANK[account.id] ?? 2;
142
+ export async function discoverClaudeAccounts({ home = os.homedir(), platform = process.platform } = {}) {
143
+ const accounts = [];
144
+ for (const root of await profileFolders('claude', home)) {
145
+ if (await isSignedInProfile(root, platform))
146
+ accounts.push(claudeProfile(root, home));
147
+ }
148
+ // The default profile always exists on macOS (its Keychain entry may hold the sign-in),
149
+ // and is the fallback everywhere when nothing was found.
150
+ if (!accounts.some(a => a.id === DEFAULT_ACCOUNT_ID) && (platform === 'darwin' || !accounts.length))
151
+ accounts.push(claudeProfile(defaultProfileDir('claude', home), home));
152
+ accounts.sort((a, b) => rank(a) - rank(b) || a.id.localeCompare(b.id));
153
+ const [first] = accounts;
154
+ if (first && !accounts.some(a => a.primary))
155
+ first.primary = true;
156
+ return accounts;
157
+ }
158
+ export async function readClaudeCredentials(account, options) {
159
+ const { platform = process.platform, home = os.homedir() } = options;
160
+ const configDir = account.configDir ?? defaultProfileDir('claude', home);
161
+ if (platform === 'darwin') {
162
+ const configDirExplicit = account.configDirExplicit ?? account.id !== DEFAULT_ACCOUNT_ID;
163
+ const stored = await readClaudeKeychain({
164
+ configDir,
165
+ configDirExplicit,
166
+ platform,
167
+ ...(options.runProcess ? { runProcess: options.runProcess } : {})
168
+ }).catch(() => null);
169
+ // Without a usable token in the Keychain, the credentials file is the documented fallback.
170
+ if (stored && oauthCredentials(stored))
171
+ return stored;
172
+ }
173
+ try {
174
+ return await readCredentialFile(path.join(configDir, CLAUDE_FILES.credentials));
175
+ }
176
+ catch (error) {
177
+ if (platform !== 'darwin' || !(error instanceof ProviderError) || error.code !== 'auth_missing')
178
+ throw error;
179
+ const isDefaultDir = samePath(configDir, defaultProfileDir('claude', home));
180
+ throw new ProviderError(isDefaultDir ? 'keychain_unavailable' : 'unsupported_profile');
181
+ }
182
+ }
183
+ // The default profile keeps its state file in the home folder; any other in the profile folder.
184
+ export function claudeStateFile(account, home = os.homedir()) {
185
+ const explicit = account.configDirExplicit ?? account.id !== DEFAULT_ACCOUNT_ID;
186
+ return path.join(explicit && account.configDir ? account.configDir : home, CLAUDE_FILES.state);
187
+ }
188
+ async function readSignedInEmail(account, { home = os.homedir() }) {
189
+ const state = await readJson(claudeStateFile(account, home));
190
+ return safeEmail(isObject(state) && isObject(state.oauthAccount) ? state.oauthAccount.emailAddress : undefined);
191
+ }
192
+ export function createClaudeProvider(options = {}) {
193
+ const getJson = options.getJson ?? defaultGetJson;
194
+ const readCredentials = options.readCredentials ?? ((account) => readClaudeCredentials(account, options));
195
+ return {
196
+ id: 'claude',
197
+ discover: () => discoverClaudeAccounts(options),
198
+ async fetch(account) {
199
+ const oauth = oauthCredentials(await readCredentials(account));
200
+ if (!oauth)
201
+ throw new ProviderError('auth_missing');
202
+ if (isExpired(oauth.expiresAt, options.clock))
203
+ throw new ProviderError('auth_expired');
204
+ const usage = await getJson(QUOTA_ENDPOINTS.claude, {
205
+ headers: {
206
+ Authorization: `Bearer ${oauth.accessToken}`,
207
+ 'anthropic-beta': OAUTH_BETA,
208
+ 'User-Agent': USER_AGENT,
209
+ Accept: 'application/json'
210
+ }
211
+ });
212
+ const result = parseClaudeUsage(usage);
213
+ const plan = safePlan(oauth.subscriptionType);
214
+ const signedInAs = await readSignedInEmail(account, options);
215
+ return { ...result, ...(plan ? { plan } : {}), ...(signedInAs ? { signedInAs } : {}) };
216
+ }
217
+ };
218
+ }
@@ -0,0 +1,21 @@
1
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ import { type Environment } from '../core/environment.ts';
4
+ import { type BinaryLocation, type Clock } from './common.ts';
5
+ import type { DiscoveredAccount, ParsedQuota, ProviderAdapter } from './types.ts';
6
+ export declare const DEFAULT_ACCOUNT_ID: string;
7
+ export declare const CODEX_SESSIONS_DIR = "sessions";
8
+ export declare function parseCodexRateLimits(response: unknown): ParsedQuota;
9
+ type ServerProcess = Pick<ChildProcess, 'on' | 'once' | 'kill' | 'exitCode' | 'signalCode'> & {
10
+ stdin: Writable;
11
+ stdout: Readable;
12
+ };
13
+ export type SpawnAppServer = (binary: string, args: readonly string[], options: SpawnOptions) => ServerProcess;
14
+ export declare function codexProfile(configDir: string, home: string, env?: Environment): DiscoveredAccount;
15
+ export declare const isSignedInCodexHome: (root: string) => Promise<boolean>;
16
+ export interface CodexProviderOptions extends BinaryLocation {
17
+ clock?: Clock;
18
+ spawnProcess?: SpawnAppServer;
19
+ }
20
+ export declare function createCodexProvider(options?: CodexProviderOptions): ProviderAdapter;
21
+ export {};
@@ -0,0 +1,221 @@
1
+ import { spawn } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { cleanEnvironment } from "../core/environment.js";
5
+ import { ProviderError } from "../core/errors.js";
6
+ import { attemptSync, isObject } from "../core/files.js";
7
+ import { isoFromUnixSeconds, normalizeWindow, safeEmail, safePlan, safeText } from "../core/model.js";
8
+ import { accountId, APP_NAME, APP_SLUG, DEFAULT_ACCOUNT_KEY, durationLabel, PROFILE_ENV } from "../shared/catalog.js";
9
+ import { APP_VERSION, findBinary, isExpired, jwtClaims, MAX_RESPONSE_BYTES, readCredentialFile } from "./common.js";
10
+ import { defaultProfileDir, isRegularFile, profileAccountId, profileFolders, profileLabel } from "./profiles.js";
11
+ export const DEFAULT_ACCOUNT_ID = accountId('codex', DEFAULT_ACCOUNT_KEY);
12
+ const AUTH_FILE = 'auth.json';
13
+ // Where a Codex home keeps its session rollouts, by date.
14
+ export const CODEX_SESSIONS_DIR = 'sessions';
15
+ const RPC_TIMEOUT_MS = 10000;
16
+ const SHUTDOWN_GRACE_MS = 2000;
17
+ // The app server's requests in a read, one after the other (see probeAccount).
18
+ const PROBE_REQUESTS = 3;
19
+ // The default bucket is the account-wide limit; other buckets are model-specific scopes.
20
+ const DEFAULT_BUCKET = 'codex';
21
+ const WINDOW_SLOTS = ['primary', 'secondary'];
22
+ export function parseCodexRateLimits(response) {
23
+ if (!isObject(response) || !(isObject(response.rateLimitsByLimitId) || isObject(response.rateLimits)))
24
+ throw new ProviderError('invalid_response');
25
+ const buckets = isObject(response.rateLimitsByLimitId) ? response.rateLimitsByLimitId : { [DEFAULT_BUCKET]: response.rateLimits };
26
+ const windows = [];
27
+ for (const [bucketId, value] of Object.entries(buckets)) {
28
+ for (const slot of WINDOW_SLOTS) {
29
+ const raw = isObject(value) ? value[slot] : undefined;
30
+ if (!isObject(raw))
31
+ continue;
32
+ windows.push(normalizeWindow({
33
+ id: `${bucketId}:${slot}`,
34
+ ...durationLabel(raw.windowDurationMins),
35
+ usedPercent: raw.usedPercent,
36
+ resetsAt: isoFromUnixSeconds(raw.resetsAt),
37
+ ...(bucketId === DEFAULT_BUCKET ? {} : { scope: safeText(bucketId) })
38
+ }));
39
+ }
40
+ }
41
+ return windows.length ? { windows } : { windows, status: 'no_limits_reported' };
42
+ }
43
+ const spawnAppServer = (binary, args, options) => spawn(binary, args, { ...options, stdio: ['pipe', 'pipe', 'ignore'] });
44
+ function startAppServer(binary, configDir, spawnProcess) {
45
+ const child = spawnProcess(binary, ['app-server', '--listen', 'stdio://'], {
46
+ shell: false,
47
+ windowsHide: true,
48
+ env: cleanEnvironment({ [PROFILE_ENV.codex]: configDir }),
49
+ stdio: ['pipe', 'pipe', 'ignore']
50
+ });
51
+ const pending = new Map();
52
+ let buffer = '';
53
+ let closed = false;
54
+ let serial = 0;
55
+ function rejectAll() {
56
+ closed = true;
57
+ for (const item of pending.values())
58
+ item.reject(new ProviderError('server_closed'));
59
+ pending.clear();
60
+ }
61
+ function receive(line) {
62
+ let packet;
63
+ try {
64
+ packet = JSON.parse(line);
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ if (!isObject(packet) || typeof packet.id !== 'number')
70
+ return;
71
+ const item = pending.get(packet.id);
72
+ if (!item)
73
+ return;
74
+ pending.delete(packet.id);
75
+ if (packet.error)
76
+ item.reject(new ProviderError('rpc_error'));
77
+ else
78
+ item.resolve(packet.result);
79
+ }
80
+ child.on('error', rejectAll);
81
+ child.on('close', rejectAll);
82
+ // Writing to a server that already exited fails with EPIPE; `close` reports that instead.
83
+ child.stdin.on('error', () => undefined);
84
+ child.stdout.on('data', (chunk) => {
85
+ buffer += chunk.toString();
86
+ if (buffer.length > MAX_RESPONSE_BYTES) {
87
+ rejectAll();
88
+ child.kill();
89
+ return;
90
+ }
91
+ for (let newline = buffer.indexOf('\n'); newline !== -1; newline = buffer.indexOf('\n')) {
92
+ receive(buffer.slice(0, newline));
93
+ buffer = buffer.slice(newline + 1);
94
+ }
95
+ });
96
+ const send = (message) => {
97
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
98
+ };
99
+ return {
100
+ request: (method, params) => new Promise((resolve, reject) => {
101
+ if (closed) {
102
+ reject(new ProviderError('server_closed'));
103
+ return;
104
+ }
105
+ const id = ++serial;
106
+ const timeout = setTimeout(() => {
107
+ pending.delete(id);
108
+ reject(new ProviderError('rpc_timeout'));
109
+ }, RPC_TIMEOUT_MS);
110
+ pending.set(id, {
111
+ resolve: value => {
112
+ clearTimeout(timeout);
113
+ resolve(value);
114
+ },
115
+ reject: error => {
116
+ clearTimeout(timeout);
117
+ reject(error);
118
+ }
119
+ });
120
+ send({ id, method, ...(params ? { params } : {}) });
121
+ }),
122
+ notify: method => {
123
+ send({ method });
124
+ },
125
+ async stop() {
126
+ child.stdin.end();
127
+ child.kill();
128
+ if (child.exitCode === null && child.signalCode === null) {
129
+ await new Promise(resolve => {
130
+ const timer = setTimeout(resolve, SHUTDOWN_GRACE_MS);
131
+ child.once('close', () => {
132
+ clearTimeout(timer);
133
+ resolve();
134
+ });
135
+ });
136
+ }
137
+ rejectAll();
138
+ }
139
+ };
140
+ }
141
+ async function probeAccount(binary, configDir, spawnProcess) {
142
+ const server = startAppServer(binary, configDir, spawnProcess);
143
+ try {
144
+ await server.request('initialize', {
145
+ // The app server takes a client name as an identifier with underscores.
146
+ clientInfo: { name: APP_SLUG.replaceAll('-', '_'), title: APP_NAME, version: APP_VERSION },
147
+ capabilities: { experimentalApi: false }
148
+ });
149
+ server.notify('initialized');
150
+ const read = await server.request('account/read', { refreshToken: false });
151
+ const account = isObject(read) ? read.account : undefined;
152
+ if (!isObject(account))
153
+ throw new ProviderError('auth_missing');
154
+ const result = parseCodexRateLimits(await server.request('account/rateLimits/read'));
155
+ const plan = safePlan(account.planType);
156
+ return plan ? { ...result, plan } : result;
157
+ }
158
+ finally {
159
+ await server.stop();
160
+ }
161
+ }
162
+ // The signed-in e-mail from the local auth file, failing fast on a locally expired token. A
163
+ // missing auth file is left to the app server, which reports the signed-out state itself.
164
+ async function readLocalSignIn(configDir, clock) {
165
+ const auth = await readCredentialFile(path.join(configDir, AUTH_FILE)).catch((error) => {
166
+ if (error instanceof ProviderError && error.code === 'auth_missing')
167
+ return null;
168
+ throw error;
169
+ });
170
+ const tokens = isObject(auth?.tokens) ? auth.tokens : {};
171
+ if (typeof tokens.access_token === 'string') {
172
+ const { exp } = jwtClaims(tokens.access_token);
173
+ if (typeof exp === 'number' && isExpired(exp * 1000, clock))
174
+ throw new ProviderError('auth_expired');
175
+ }
176
+ return safeEmail(attemptSync(() => jwtClaims(tokens.id_token).email));
177
+ }
178
+ // The folder plain `codex` uses: CODEX_HOME when set, else `~/.codex`.
179
+ const codexDefaultDir = (home, env = process.env) => env[PROFILE_ENV.codex] ?? defaultProfileDir('codex', home);
180
+ // The account a Codex home folder holds; Codex keeps one sign-in per folder.
181
+ export function codexProfile(configDir, home, env = process.env) {
182
+ const resolved = path.resolve(configDir);
183
+ const isDefault = resolved === path.resolve(codexDefaultDir(home, env));
184
+ return {
185
+ id: isDefault ? DEFAULT_ACCOUNT_ID : profileAccountId('codex', resolved),
186
+ providerId: 'codex',
187
+ ...(isDefault ? { labelKey: 'defaultAccount' } : { label: profileLabel('codex', resolved) }),
188
+ configDir: resolved,
189
+ primary: isDefault,
190
+ enabled: true
191
+ };
192
+ }
193
+ // Whether a Codex home folder holds a sign-in.
194
+ export const isSignedInCodexHome = (root) => isRegularFile(path.join(root, AUTH_FILE));
195
+ // The default folder always counts: its app server reports a signed-out state itself. Other
196
+ // profile folders count once they hold a sign-in.
197
+ async function discoverCodexAccounts(home, env) {
198
+ const defaultDir = path.resolve(codexDefaultDir(home, env));
199
+ const others = [];
200
+ for (const root of await profileFolders('codex', home)) {
201
+ if (path.resolve(root) !== defaultDir && (await isSignedInCodexHome(root)))
202
+ others.push(codexProfile(root, home, env));
203
+ }
204
+ return [codexProfile(defaultDir, home, env), ...others.sort((a, b) => a.id.localeCompare(b.id))];
205
+ }
206
+ export function createCodexProvider(options = {}) {
207
+ const home = options.home ?? os.homedir();
208
+ const env = options.env ?? process.env;
209
+ const defaultConfigDir = codexDefaultDir(home, env);
210
+ return {
211
+ id: 'codex',
212
+ fetchTimeoutMs: PROBE_REQUESTS * RPC_TIMEOUT_MS + SHUTDOWN_GRACE_MS,
213
+ discover: () => discoverCodexAccounts(home, env),
214
+ async fetch(account) {
215
+ const configDir = account.configDir ?? defaultConfigDir;
216
+ const signedInAs = await readLocalSignIn(configDir, options.clock);
217
+ const result = await probeAccount(await findBinary('codex', options), configDir, options.spawnProcess ?? spawnAppServer);
218
+ return signedInAs ? { ...result, signedInAs } : result;
219
+ }
220
+ };
221
+ }
@@ -0,0 +1,63 @@
1
+ import type { ClientRequest, IncomingMessage } from 'node:http';
2
+ import https from 'node:https';
3
+ import { type Environment } from '../core/environment.ts';
4
+ import { ProviderError } from '../core/errors.ts';
5
+ import { type JsonObject } from '../core/files.ts';
6
+ import { type ProviderId } from '../shared/catalog.ts';
7
+ export type Clock = () => number;
8
+ export declare const APP_VERSION: string;
9
+ export declare const USER_AGENT: string;
10
+ export declare const MAX_RESPONSE_BYTES: number;
11
+ export declare const QUOTA_ENDPOINTS: {
12
+ readonly claude: "https://api.anthropic.com/api/oauth/usage";
13
+ readonly cursor: "https://cursor.com/api/usage-summary";
14
+ };
15
+ export declare const sha256Prefix: (value: string, length: number) => string;
16
+ export declare function isExpired(epochMs: unknown, clock?: Clock): boolean;
17
+ export declare function jwtClaims(token: unknown): JsonObject;
18
+ export declare function readCredentialFile(file: string): Promise<JsonObject>;
19
+ interface Destroyable {
20
+ destroy(): void;
21
+ }
22
+ export interface Settle<T> {
23
+ succeed(value: T): void;
24
+ fail(error: ProviderError): void;
25
+ attach<S extends Destroyable>(stream: S): S;
26
+ }
27
+ export declare function settleOnce<T>(resolve: (value: T) => void, reject: (error: ProviderError) => void, timeoutMs: number): Settle<T>;
28
+ export declare function readJsonResponse(response: IncomingMessage, settle: Settle<unknown>, maxBytes?: number): void;
29
+ export type QuotaEndpoint = (typeof QUOTA_ENDPOINTS)[keyof typeof QUOTA_ENDPOINTS];
30
+ type HttpsGet = (url: URL, options: https.RequestOptions, callback: (response: IncomingMessage) => void) => ClientRequest;
31
+ export interface GetJsonOptions {
32
+ headers?: Readonly<Record<string, string>>;
33
+ timeoutMs?: number;
34
+ requestImpl?: HttpsGet;
35
+ }
36
+ export type GetJson = (endpoint: QuotaEndpoint, options?: GetJsonOptions) => Promise<unknown>;
37
+ export declare const getJson: GetJson;
38
+ export declare function unpackedPath(file: string): string;
39
+ export declare function windowsPowerShell(env?: Environment): string;
40
+ export interface BinaryLocation {
41
+ env?: Environment;
42
+ home?: string;
43
+ platform?: NodeJS.Platform;
44
+ }
45
+ export interface DesktopApp {
46
+ mac: string;
47
+ windows: readonly string[];
48
+ }
49
+ export declare const TOOL_INSTALLS: Readonly<Record<ProviderId, {
50
+ binaries: readonly string[];
51
+ app?: DesktopApp;
52
+ }>>;
53
+ export declare const windowsAppPath: (app: DesktopApp, env: Environment, home: string) => string;
54
+ export declare function findBinary(name: string, { env, home, platform }?: BinaryLocation): Promise<string>;
55
+ export interface RunOptions {
56
+ env?: Record<string, string>;
57
+ platform?: NodeJS.Platform;
58
+ timeoutMs?: number;
59
+ maxBytes?: number;
60
+ }
61
+ export type RunProcess = (binary: string, args: readonly string[], options?: RunOptions) => Promise<string>;
62
+ export declare const runProcess: RunProcess;
63
+ export {};