sidekick-shared 0.20.0 → 0.21.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.
- package/README.md +14 -2
- package/dist/accountManager.d.ts +52 -0
- package/dist/accountManager.js +344 -0
- package/dist/accounts.d.ts +3 -0
- package/dist/accounts.js +180 -40
- package/dist/autoSwitch.d.ts +70 -0
- package/dist/autoSwitch.js +160 -0
- package/dist/claudeProfiles.d.ts +12 -0
- package/dist/claudeProfiles.js +96 -0
- package/dist/codexProfiles.d.ts +6 -2
- package/dist/codexProfiles.js +6 -1
- package/dist/credentialIO.d.ts +3 -2
- package/dist/credentialIO.js +11 -9
- package/dist/ensureDefaultAccounts.js +6 -0
- package/dist/index.d.ts +16 -3
- package/dist/index.js +60 -5
- package/dist/multiProviderQuotaService.d.ts +10 -0
- package/dist/multiProviderQuotaService.js +34 -0
- package/dist/providerQuota.d.ts +2 -1
- package/dist/providers/detect.js +3 -13
- package/dist/providers/openCode.d.ts +31 -0
- package/dist/providers/openCode.js +108 -2
- package/dist/providers/openCodeDatabase.d.ts +19 -0
- package/dist/providers/openCodeDatabase.js +28 -0
- package/dist/quota.d.ts +1 -1
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +14 -2
- package/dist/quotaSnapshots.d.ts +8 -2
- package/dist/schemas/accountManager.d.ts +104 -0
- package/dist/schemas/accountManager.js +84 -0
- package/dist/schemas/assistantTurn.d.ts +5 -5
- package/dist/schemas/index.d.ts +5 -1
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +185 -20
- package/dist/schemas/quota.js +8 -1
- package/dist/schemas/quotaHistory.d.ts +3 -1
- package/dist/schemas/quotaHistory.js +1 -0
- package/dist/terminalSync.d.ts +7 -0
- package/dist/terminalSync.js +202 -0
- package/dist/zaiQuota.d.ts +209 -0
- package/dist/zaiQuota.js +290 -0
- package/dist/zaiQuotaWatcher.d.ts +97 -0
- package/dist/zaiQuotaWatcher.js +259 -0
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -44,6 +44,9 @@ exports.writeAccountRegistry = writeAccountRegistry;
|
|
|
44
44
|
exports.readActiveClaudeAccount = readActiveClaudeAccount;
|
|
45
45
|
exports.addCurrentAccount = addCurrentAccount;
|
|
46
46
|
exports.switchToAccount = switchToAccount;
|
|
47
|
+
exports.resolveActiveClaudeHome = resolveActiveClaudeHome;
|
|
48
|
+
exports.applyActiveClaudeToLiveHome = applyActiveClaudeToLiveHome;
|
|
49
|
+
exports.reconcileClaudeAuthState = reconcileClaudeAuthState;
|
|
47
50
|
exports.removeAccount = removeAccount;
|
|
48
51
|
exports.listAccounts = listAccounts;
|
|
49
52
|
exports.getActiveAccount = getActiveAccount;
|
|
@@ -53,6 +56,7 @@ const path = __importStar(require("path"));
|
|
|
53
56
|
const os = __importStar(require("os"));
|
|
54
57
|
const credentialIO_1 = require("./credentialIO");
|
|
55
58
|
const accountRegistry_1 = require("./accountRegistry");
|
|
59
|
+
const claudeProfiles_1 = require("./claudeProfiles");
|
|
56
60
|
// ── Paths ────────────────────────────────────────────────────────────────
|
|
57
61
|
function getClaudeDir() {
|
|
58
62
|
return path.join(os.homedir(), '.claude');
|
|
@@ -75,6 +79,9 @@ function getCredentialsDir() {
|
|
|
75
79
|
function getConfigsDir() {
|
|
76
80
|
return path.join((0, accountRegistry_1.getAccountsDir)(), 'configs');
|
|
77
81
|
}
|
|
82
|
+
function getClaudeMigrationMarkerPath() {
|
|
83
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'claude', '.profiles-migrated-v1');
|
|
84
|
+
}
|
|
78
85
|
// ── Directory bootstrap ──────────────────────────────────────────────────
|
|
79
86
|
function ensureDirs() {
|
|
80
87
|
for (const dir of [(0, accountRegistry_1.getAccountsDir)(), getCredentialsDir(), getConfigsDir()]) {
|
|
@@ -83,6 +90,7 @@ function ensureDirs() {
|
|
|
83
90
|
}
|
|
84
91
|
// ── Atomic file write ────────────────────────────────────────────────────
|
|
85
92
|
function atomicWriteJson(filePath, data, mode = 0o600) {
|
|
93
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
86
94
|
const tmp = filePath + '.tmp';
|
|
87
95
|
const json = JSON.stringify(data, null, 2);
|
|
88
96
|
// Validate we can re-parse before writing
|
|
@@ -90,6 +98,95 @@ function atomicWriteJson(filePath, data, mode = 0o600) {
|
|
|
90
98
|
fs.writeFileSync(tmp, json, { encoding: 'utf8', mode });
|
|
91
99
|
fs.renameSync(tmp, filePath);
|
|
92
100
|
}
|
|
101
|
+
function readJsonOrNull(filePath) {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function readOauthAccountFromConfigPath(filePath) {
|
|
110
|
+
const parsed = readJsonOrNull(filePath);
|
|
111
|
+
return parsed?.oauthAccount ?? null;
|
|
112
|
+
}
|
|
113
|
+
function writeClaudeProfileConfig(uuid, oauthAccount) {
|
|
114
|
+
atomicWriteJson(path.join((0, claudeProfiles_1.getClaudeProfileHome)(uuid), '.claude.json'), { oauthAccount });
|
|
115
|
+
}
|
|
116
|
+
function writeClaudeProfileCredentials(uuid, credentials) {
|
|
117
|
+
(0, credentialIO_1.writeActiveCredentials)(credentials, (0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
118
|
+
}
|
|
119
|
+
function writeClaudeProfileMirror(uuid, credentials, oauthAccount) {
|
|
120
|
+
writeClaudeProfileConfig(uuid, oauthAccount);
|
|
121
|
+
writeClaudeProfileCredentials(uuid, credentials);
|
|
122
|
+
}
|
|
123
|
+
function readFlatClaudeCredentials(uuid) {
|
|
124
|
+
return readJsonOrNull(path.join(getCredentialsDir(), `${uuid}.credentials.json`));
|
|
125
|
+
}
|
|
126
|
+
function readFlatClaudeOauthAccount(uuid) {
|
|
127
|
+
return readJsonOrNull(path.join(getConfigsDir(), `${uuid}.config.json`));
|
|
128
|
+
}
|
|
129
|
+
function readProfileClaudeOauthAccount(uuid) {
|
|
130
|
+
return readOauthAccountFromConfigPath(path.join((0, claudeProfiles_1.getClaudeProfileHome)(uuid), '.claude.json'));
|
|
131
|
+
}
|
|
132
|
+
function readStoredClaudeAccount(uuid) {
|
|
133
|
+
const profileCredentials = (0, credentialIO_1.readActiveCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(uuid));
|
|
134
|
+
const profileOauthAccount = readProfileClaudeOauthAccount(uuid);
|
|
135
|
+
if (profileCredentials && profileOauthAccount) {
|
|
136
|
+
return { credentials: profileCredentials, oauthAccount: profileOauthAccount };
|
|
137
|
+
}
|
|
138
|
+
const flatCredentials = readFlatClaudeCredentials(uuid);
|
|
139
|
+
const flatOauthAccount = readFlatClaudeOauthAccount(uuid);
|
|
140
|
+
if (flatCredentials && flatOauthAccount) {
|
|
141
|
+
try {
|
|
142
|
+
writeClaudeProfileMirror(uuid, flatCredentials, flatOauthAccount);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Flat backups remain the fallback when profile materialization fails.
|
|
146
|
+
}
|
|
147
|
+
return { credentials: flatCredentials, oauthAccount: flatOauthAccount };
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function readCurrentLiveOauthAccount() {
|
|
152
|
+
return readOauthAccountFromConfigPath(getClaudeConfigPath());
|
|
153
|
+
}
|
|
154
|
+
function backupCurrentClaudeLiveHome() {
|
|
155
|
+
const currentActive = readActiveClaudeAccount();
|
|
156
|
+
if (!currentActive)
|
|
157
|
+
return;
|
|
158
|
+
const originalCreds = (0, credentialIO_1.readActiveCredentials)();
|
|
159
|
+
const originalOauthAccount = readCurrentLiveOauthAccount();
|
|
160
|
+
if (originalCreds) {
|
|
161
|
+
try {
|
|
162
|
+
atomicWriteJson(path.join(getCredentialsDir(), `${currentActive.uuid}.credentials.json`), originalCreds);
|
|
163
|
+
writeClaudeProfileCredentials(currentActive.uuid, originalCreds);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Best effort only.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (originalOauthAccount) {
|
|
170
|
+
try {
|
|
171
|
+
atomicWriteJson(path.join(getConfigsDir(), `${currentActive.uuid}.config.json`), originalOauthAccount);
|
|
172
|
+
writeClaudeProfileConfig(currentActive.uuid, originalOauthAccount);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Best effort only.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function mergeOauthAccountIntoLiveConfig(oauthAccount) {
|
|
180
|
+
let configObj = {};
|
|
181
|
+
try {
|
|
182
|
+
configObj = JSON.parse(fs.readFileSync(getClaudeConfigPath(), 'utf8'));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// File may not exist yet.
|
|
186
|
+
}
|
|
187
|
+
configObj.oauthAccount = oauthAccount;
|
|
188
|
+
atomicWriteJson(getClaudeConfigPath(), configObj);
|
|
189
|
+
}
|
|
93
190
|
// ── Registry operations ──────────────────────────────────────────────────
|
|
94
191
|
function readAccountRegistry() {
|
|
95
192
|
const registry = (0, accountRegistry_1.readSavedAccountRegistry)();
|
|
@@ -173,6 +270,10 @@ function addCurrentAccount(label) {
|
|
|
173
270
|
existing.label = label;
|
|
174
271
|
atomicWriteJson(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
175
272
|
atomicWriteJson(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
273
|
+
try {
|
|
274
|
+
writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
|
|
275
|
+
}
|
|
276
|
+
catch { /* flat backups remain available */ }
|
|
176
277
|
registry.activeAccountUuid = active.uuid;
|
|
177
278
|
writeAccountRegistry(registry);
|
|
178
279
|
return { success: true };
|
|
@@ -180,6 +281,10 @@ function addCurrentAccount(label) {
|
|
|
180
281
|
// 6. New account — back up credentials
|
|
181
282
|
atomicWriteJson(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
182
283
|
atomicWriteJson(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
284
|
+
try {
|
|
285
|
+
writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
|
|
286
|
+
}
|
|
287
|
+
catch { /* flat backups remain available */ }
|
|
183
288
|
// 7. Add entry
|
|
184
289
|
registry.accounts.push({
|
|
185
290
|
uuid: active.uuid,
|
|
@@ -201,10 +306,7 @@ function switchToAccount(uuid) {
|
|
|
201
306
|
if (!target) {
|
|
202
307
|
return { success: false, error: `Account ${uuid} not found in registry.` };
|
|
203
308
|
}
|
|
204
|
-
|
|
205
|
-
return { success: true }; // Already active
|
|
206
|
-
}
|
|
207
|
-
// 1. Back up current credentials + config (for the currently-active account)
|
|
309
|
+
// 1. Back up current credentials + config (for whichever Claude account is live).
|
|
208
310
|
let originalCreds = null;
|
|
209
311
|
let originalConfig = null;
|
|
210
312
|
originalCreds = (0, credentialIO_1.readActiveCredentials)();
|
|
@@ -212,46 +314,64 @@ function switchToAccount(uuid) {
|
|
|
212
314
|
originalConfig = fs.readFileSync(getClaudeConfigPath(), 'utf8');
|
|
213
315
|
}
|
|
214
316
|
catch { /* no existing config to back up */ }
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
317
|
+
try {
|
|
318
|
+
backupCurrentClaudeLiveHome();
|
|
319
|
+
}
|
|
320
|
+
catch { /* best effort */ }
|
|
321
|
+
try {
|
|
322
|
+
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', uuid);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return { success: false, error: `Failed to update account registry: ${err}` };
|
|
326
|
+
}
|
|
327
|
+
const applied = applyActiveClaudeToLiveHome();
|
|
328
|
+
if (!applied.success) {
|
|
329
|
+
try {
|
|
330
|
+
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', registry.activeAccountUuid);
|
|
331
|
+
}
|
|
332
|
+
catch { /* rollback failed */ }
|
|
218
333
|
if (originalCreds) {
|
|
219
334
|
try {
|
|
220
|
-
|
|
335
|
+
(0, credentialIO_1.writeActiveCredentials)(originalCreds);
|
|
221
336
|
}
|
|
222
|
-
catch { /*
|
|
337
|
+
catch { /* rollback failed */ }
|
|
223
338
|
}
|
|
224
339
|
if (originalConfig) {
|
|
225
340
|
try {
|
|
226
|
-
|
|
227
|
-
if (parsed.oauthAccount) {
|
|
228
|
-
atomicWriteJson(path.join(getConfigsDir(), `${currentActive.uuid}.config.json`), parsed.oauthAccount);
|
|
229
|
-
}
|
|
341
|
+
fs.writeFileSync(getClaudeConfigPath(), originalConfig);
|
|
230
342
|
}
|
|
231
|
-
catch { /*
|
|
343
|
+
catch { /* rollback failed */ }
|
|
232
344
|
}
|
|
345
|
+
return applied;
|
|
233
346
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
347
|
+
return applied;
|
|
348
|
+
}
|
|
349
|
+
function resolveActiveClaudeHome() {
|
|
350
|
+
const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
|
|
351
|
+
if (!active)
|
|
352
|
+
return getClaudeDir();
|
|
353
|
+
return (0, claudeProfiles_1.getClaudeProfileHome)(active.providerAccountId ?? active.id);
|
|
354
|
+
}
|
|
355
|
+
function applyActiveClaudeToLiveHome() {
|
|
356
|
+
const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
|
|
357
|
+
if (!active) {
|
|
358
|
+
return { success: false, error: 'No active Claude account found.' };
|
|
239
359
|
}
|
|
240
|
-
|
|
241
|
-
|
|
360
|
+
const uuid = active.providerAccountId ?? active.id;
|
|
361
|
+
const stored = readStoredClaudeAccount(uuid);
|
|
362
|
+
if (!stored) {
|
|
363
|
+
return { success: false, error: `Stored credentials for ${active.email ?? uuid} not found.` };
|
|
242
364
|
}
|
|
365
|
+
const originalCreds = (0, credentialIO_1.readActiveCredentials)();
|
|
366
|
+
let originalConfig = null;
|
|
243
367
|
try {
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return { success: false, error: `Backed-up config for ${target.email} not found.` };
|
|
368
|
+
originalConfig = fs.readFileSync(getClaudeConfigPath(), 'utf8');
|
|
248
369
|
}
|
|
249
|
-
|
|
370
|
+
catch { /* no existing config to back up */ }
|
|
250
371
|
try {
|
|
251
|
-
(0, credentialIO_1.writeActiveCredentials)(
|
|
372
|
+
(0, credentialIO_1.writeActiveCredentials)(stored.credentials);
|
|
252
373
|
}
|
|
253
374
|
catch (err) {
|
|
254
|
-
// Rollback: restore original credentials
|
|
255
375
|
if (originalCreds) {
|
|
256
376
|
try {
|
|
257
377
|
(0, credentialIO_1.writeActiveCredentials)(originalCreds);
|
|
@@ -260,18 +380,10 @@ function switchToAccount(uuid) {
|
|
|
260
380
|
}
|
|
261
381
|
return { success: false, error: `Failed to write credentials: ${err}` };
|
|
262
382
|
}
|
|
263
|
-
// 4. Merge target oauthAccount into current .claude.json
|
|
264
383
|
try {
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
configObj = JSON.parse(fs.readFileSync(getClaudeConfigPath(), 'utf8'));
|
|
268
|
-
}
|
|
269
|
-
catch { /* file may not exist yet */ }
|
|
270
|
-
configObj.oauthAccount = targetOauthAccount;
|
|
271
|
-
atomicWriteJson(getClaudeConfigPath(), configObj);
|
|
384
|
+
mergeOauthAccountIntoLiveConfig(stored.oauthAccount);
|
|
272
385
|
}
|
|
273
386
|
catch (err) {
|
|
274
|
-
// Rollback: restore original credentials and config
|
|
275
387
|
if (originalCreds) {
|
|
276
388
|
try {
|
|
277
389
|
(0, credentialIO_1.writeActiveCredentials)(originalCreds);
|
|
@@ -286,11 +398,39 @@ function switchToAccount(uuid) {
|
|
|
286
398
|
}
|
|
287
399
|
return { success: false, error: `Failed to write config: ${err}` };
|
|
288
400
|
}
|
|
289
|
-
// 5. Update registry
|
|
290
|
-
registry.activeAccountUuid = uuid;
|
|
291
|
-
writeAccountRegistry(registry);
|
|
292
401
|
return { success: true };
|
|
293
402
|
}
|
|
403
|
+
function reconcileClaudeAuthState() {
|
|
404
|
+
try {
|
|
405
|
+
const markerPath = getClaudeMigrationMarkerPath();
|
|
406
|
+
if (fs.existsSync(markerPath))
|
|
407
|
+
return;
|
|
408
|
+
const registry = (0, accountRegistry_1.readSavedAccountRegistry)();
|
|
409
|
+
if (registry) {
|
|
410
|
+
for (const profile of registry.accounts.filter(account => account.providerId === 'claude-code')) {
|
|
411
|
+
try {
|
|
412
|
+
const uuid = profile.providerAccountId ?? profile.id;
|
|
413
|
+
if ((0, credentialIO_1.readActiveCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(uuid)) && readProfileClaudeOauthAccount(uuid)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const credentials = readFlatClaudeCredentials(uuid);
|
|
417
|
+
const oauthAccount = readFlatClaudeOauthAccount(uuid);
|
|
418
|
+
if (!credentials || !oauthAccount)
|
|
419
|
+
continue;
|
|
420
|
+
writeClaudeProfileMirror(uuid, credentials, oauthAccount);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Migration is best-effort per account.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true, mode: 0o700 });
|
|
428
|
+
fs.writeFileSync(markerPath, new Date().toISOString() + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// Reconciliation must never break startup.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
294
434
|
// ── Remove account ───────────────────────────────────────────────────────
|
|
295
435
|
function removeAccount(uuid) {
|
|
296
436
|
const registry = readAccountRegistry();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type AccountEntry, type AccountManagerResult } from './accounts';
|
|
2
|
+
import type { AccountProviderId, SavedAccountProfile } from './accountRegistry';
|
|
3
|
+
import type { ProviderQuotaMap } from './providerQuota';
|
|
4
|
+
import type { Disposable } from './quotaPoller';
|
|
5
|
+
import type { QuotaState } from './quota';
|
|
6
|
+
export interface AutoSwitchConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
thresholdPct: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const DEFAULT_AUTO_SWITCH_CONFIG: AutoSwitchConfig;
|
|
11
|
+
export interface AutoSwitchCandidate {
|
|
12
|
+
accountId: string;
|
|
13
|
+
quota: QuotaState | null;
|
|
14
|
+
switchable?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface AutoSwitchDecision {
|
|
17
|
+
switchTo: string;
|
|
18
|
+
}
|
|
19
|
+
export interface AutoSwitchActiveAccount {
|
|
20
|
+
accountId: string;
|
|
21
|
+
quota: QuotaState | null;
|
|
22
|
+
}
|
|
23
|
+
export interface AutoSwitchTransitionEvent {
|
|
24
|
+
provider: AccountProviderId;
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
}
|
|
28
|
+
interface QuotaServiceLike {
|
|
29
|
+
onUpdate(cb: (state: ProviderQuotaMap) => void): Disposable;
|
|
30
|
+
}
|
|
31
|
+
export interface AutoSwitchControllerOptions {
|
|
32
|
+
quotaService: QuotaServiceLike;
|
|
33
|
+
config?: AutoSwitchConfig;
|
|
34
|
+
getClaudeAccounts?: () => AccountEntry[];
|
|
35
|
+
getActiveClaudeAccount?: () => AccountEntry | null;
|
|
36
|
+
getCodexAccounts?: () => SavedAccountProfile[];
|
|
37
|
+
getActiveCodexAccount?: () => SavedAccountProfile | null;
|
|
38
|
+
readSnapshot?: (providerId: AccountProviderId, accountId: string) => QuotaState | null;
|
|
39
|
+
switchAccount?: (providerId: AccountProviderId, accountId: string) => AccountManagerResult;
|
|
40
|
+
onTransition?: (event: AutoSwitchTransitionEvent) => void;
|
|
41
|
+
log?: (message: string, error?: unknown) => void;
|
|
42
|
+
cooldownMs?: number;
|
|
43
|
+
now?: () => number;
|
|
44
|
+
}
|
|
45
|
+
export declare function decideAutoSwitch(_provider: AccountProviderId, active: AutoSwitchActiveAccount, candidates: AutoSwitchCandidate[], config?: AutoSwitchConfig): AutoSwitchDecision | null;
|
|
46
|
+
export declare class AutoSwitchController implements Disposable {
|
|
47
|
+
private readonly quotaService;
|
|
48
|
+
private readonly getClaudeAccounts;
|
|
49
|
+
private readonly getActiveClaudeAccount;
|
|
50
|
+
private readonly getCodexAccounts;
|
|
51
|
+
private readonly getActiveCodexAccount;
|
|
52
|
+
private readonly readSnapshot;
|
|
53
|
+
private readonly switchAccount;
|
|
54
|
+
private readonly onTransition?;
|
|
55
|
+
private readonly log?;
|
|
56
|
+
private readonly cooldownMs;
|
|
57
|
+
private readonly now;
|
|
58
|
+
private config;
|
|
59
|
+
private subscription;
|
|
60
|
+
private readonly switchedDuringCrossing;
|
|
61
|
+
private readonly lastSwitchAt;
|
|
62
|
+
constructor(options: AutoSwitchControllerOptions);
|
|
63
|
+
start(): void;
|
|
64
|
+
stop(): void;
|
|
65
|
+
dispose(): void;
|
|
66
|
+
setConfig(config: AutoSwitchConfig): void;
|
|
67
|
+
private handleUpdate;
|
|
68
|
+
private handleProviderUpdate;
|
|
69
|
+
}
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AutoSwitchController = exports.DEFAULT_AUTO_SWITCH_CONFIG = void 0;
|
|
4
|
+
exports.decideAutoSwitch = decideAutoSwitch;
|
|
5
|
+
const accounts_1 = require("./accounts");
|
|
6
|
+
const accountManager_1 = require("./accountManager");
|
|
7
|
+
const codexProfiles_1 = require("./codexProfiles");
|
|
8
|
+
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
9
|
+
exports.DEFAULT_AUTO_SWITCH_CONFIG = {
|
|
10
|
+
enabled: false,
|
|
11
|
+
thresholdPct: 90,
|
|
12
|
+
};
|
|
13
|
+
const MATERIAL_REMAINING_IMPROVEMENT_PCT = 5;
|
|
14
|
+
function mergedConfig(config) {
|
|
15
|
+
return {
|
|
16
|
+
...exports.DEFAULT_AUTO_SWITCH_CONFIG,
|
|
17
|
+
...config,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function quotaUtilization(quota) {
|
|
21
|
+
if (!quota?.available)
|
|
22
|
+
return 0;
|
|
23
|
+
return Math.max(quota.fiveHour.utilization, quota.sevenDay.utilization);
|
|
24
|
+
}
|
|
25
|
+
function quotaRemaining(quota) {
|
|
26
|
+
if (!quota?.available)
|
|
27
|
+
return 0;
|
|
28
|
+
return Math.max(0, 100 - quotaUtilization(quota));
|
|
29
|
+
}
|
|
30
|
+
function decideAutoSwitch(_provider, active, candidates, config) {
|
|
31
|
+
const cfg = mergedConfig(config);
|
|
32
|
+
if (!cfg.enabled)
|
|
33
|
+
return null;
|
|
34
|
+
if (!active.quota?.available)
|
|
35
|
+
return null;
|
|
36
|
+
const activeUtilization = quotaUtilization(active.quota);
|
|
37
|
+
if (activeUtilization < cfg.thresholdPct)
|
|
38
|
+
return null;
|
|
39
|
+
const activeRemaining = quotaRemaining(active.quota);
|
|
40
|
+
const best = candidates
|
|
41
|
+
.filter(candidate => candidate.accountId !== active.accountId &&
|
|
42
|
+
candidate.switchable !== false &&
|
|
43
|
+
candidate.quota?.available)
|
|
44
|
+
.sort((a, b) => quotaRemaining(b.quota) - quotaRemaining(a.quota))[0];
|
|
45
|
+
if (!best)
|
|
46
|
+
return null;
|
|
47
|
+
if (quotaRemaining(best.quota) < activeRemaining + MATERIAL_REMAINING_IMPROVEMENT_PCT)
|
|
48
|
+
return null;
|
|
49
|
+
return { switchTo: best.accountId };
|
|
50
|
+
}
|
|
51
|
+
function runtimeStateToProvider(provider) {
|
|
52
|
+
return provider === 'claude' ? 'claude-code' : 'codex';
|
|
53
|
+
}
|
|
54
|
+
function accountIdFor(provider, account) {
|
|
55
|
+
return provider === 'claude-code'
|
|
56
|
+
? account.uuid
|
|
57
|
+
: account.id;
|
|
58
|
+
}
|
|
59
|
+
class AutoSwitchController {
|
|
60
|
+
quotaService;
|
|
61
|
+
getClaudeAccounts;
|
|
62
|
+
getActiveClaudeAccount;
|
|
63
|
+
getCodexAccounts;
|
|
64
|
+
getActiveCodexAccount;
|
|
65
|
+
readSnapshot;
|
|
66
|
+
switchAccount;
|
|
67
|
+
onTransition;
|
|
68
|
+
log;
|
|
69
|
+
cooldownMs;
|
|
70
|
+
now;
|
|
71
|
+
config;
|
|
72
|
+
subscription = null;
|
|
73
|
+
switchedDuringCrossing = new Set();
|
|
74
|
+
lastSwitchAt = {};
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.quotaService = options.quotaService;
|
|
77
|
+
this.config = mergedConfig(options.config);
|
|
78
|
+
this.getClaudeAccounts = options.getClaudeAccounts ?? accounts_1.listAccounts;
|
|
79
|
+
this.getActiveClaudeAccount = options.getActiveClaudeAccount ?? accounts_1.getActiveAccount;
|
|
80
|
+
this.getCodexAccounts = options.getCodexAccounts ?? codexProfiles_1.listCodexAccounts;
|
|
81
|
+
this.getActiveCodexAccount = options.getActiveCodexAccount ?? codexProfiles_1.getActiveCodexAccount;
|
|
82
|
+
this.readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
|
|
83
|
+
this.switchAccount = options.switchAccount ?? accountManager_1.switchAccount;
|
|
84
|
+
this.onTransition = options.onTransition;
|
|
85
|
+
this.log = options.log;
|
|
86
|
+
this.cooldownMs = options.cooldownMs ?? 0;
|
|
87
|
+
this.now = options.now ?? Date.now;
|
|
88
|
+
}
|
|
89
|
+
start() {
|
|
90
|
+
if (this.subscription)
|
|
91
|
+
return;
|
|
92
|
+
this.subscription = this.quotaService.onUpdate(state => this.handleUpdate(state));
|
|
93
|
+
}
|
|
94
|
+
stop() {
|
|
95
|
+
this.subscription?.dispose();
|
|
96
|
+
this.subscription = null;
|
|
97
|
+
}
|
|
98
|
+
dispose() {
|
|
99
|
+
this.stop();
|
|
100
|
+
}
|
|
101
|
+
setConfig(config) {
|
|
102
|
+
this.config = mergedConfig(config);
|
|
103
|
+
if (!this.config.enabled) {
|
|
104
|
+
this.switchedDuringCrossing.clear();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
handleUpdate(state) {
|
|
108
|
+
if (state.claude)
|
|
109
|
+
this.handleProviderUpdate('claude', state.claude);
|
|
110
|
+
if (state.codex)
|
|
111
|
+
this.handleProviderUpdate('codex', state.codex);
|
|
112
|
+
}
|
|
113
|
+
handleProviderUpdate(runtimeProvider, activeQuota) {
|
|
114
|
+
const providerId = runtimeStateToProvider(runtimeProvider);
|
|
115
|
+
const activeAccount = providerId === 'claude-code'
|
|
116
|
+
? this.getActiveClaudeAccount()
|
|
117
|
+
: this.getActiveCodexAccount();
|
|
118
|
+
if (!activeAccount)
|
|
119
|
+
return;
|
|
120
|
+
const activeAccountId = accountIdFor(providerId, activeAccount);
|
|
121
|
+
if (quotaUtilization(activeQuota) < this.config.thresholdPct) {
|
|
122
|
+
this.switchedDuringCrossing.delete(providerId);
|
|
123
|
+
}
|
|
124
|
+
const accounts = providerId === 'claude-code'
|
|
125
|
+
? this.getClaudeAccounts()
|
|
126
|
+
: this.getCodexAccounts();
|
|
127
|
+
if (accounts.length <= 1)
|
|
128
|
+
return;
|
|
129
|
+
const candidates = accounts.map(account => {
|
|
130
|
+
const accountId = accountIdFor(providerId, account);
|
|
131
|
+
return {
|
|
132
|
+
accountId,
|
|
133
|
+
quota: accountId === activeAccountId
|
|
134
|
+
? activeQuota
|
|
135
|
+
: this.readSnapshot(providerId, accountId),
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
const decision = decideAutoSwitch(providerId, { accountId: activeAccountId, quota: activeQuota }, candidates, this.config);
|
|
139
|
+
if (!decision)
|
|
140
|
+
return;
|
|
141
|
+
if (this.switchedDuringCrossing.has(providerId))
|
|
142
|
+
return;
|
|
143
|
+
const previousSwitchAt = this.lastSwitchAt[providerId] ?? 0;
|
|
144
|
+
if (this.cooldownMs > 0 && this.now() - previousSwitchAt < this.cooldownMs)
|
|
145
|
+
return;
|
|
146
|
+
const result = this.switchAccount(providerId, decision.switchTo);
|
|
147
|
+
if (!result.success) {
|
|
148
|
+
this.log?.(`[AutoSwitch] Could not switch ${providerId} to ${decision.switchTo}.`, result.error);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.lastSwitchAt[providerId] = this.now();
|
|
152
|
+
this.switchedDuringCrossing.add(providerId);
|
|
153
|
+
this.onTransition?.({
|
|
154
|
+
provider: providerId,
|
|
155
|
+
from: activeAccountId,
|
|
156
|
+
to: decision.switchTo,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.AutoSwitchController = AutoSwitchController;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ClaudeProfileIdentity {
|
|
2
|
+
email: string;
|
|
3
|
+
uuid: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function getClaudeProfilesDir(): string;
|
|
6
|
+
export declare function getClaudeProfileHome(uuid: string): string;
|
|
7
|
+
export declare function ensureClaudeProfileDirs(uuid: string): void;
|
|
8
|
+
export declare function claudeKeychainSuffix(configDir: string): string;
|
|
9
|
+
export declare function claudeKeychainService(configDir?: string): string;
|
|
10
|
+
export declare function readClaudeProfileIdentity(home: string): ClaudeProfileIdentity | null;
|
|
11
|
+
export declare function keychainServiceExists(service: string): boolean;
|
|
12
|
+
export declare function isClaudeProfileAuthenticated(home: string): boolean;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getClaudeProfilesDir = getClaudeProfilesDir;
|
|
37
|
+
exports.getClaudeProfileHome = getClaudeProfileHome;
|
|
38
|
+
exports.ensureClaudeProfileDirs = ensureClaudeProfileDirs;
|
|
39
|
+
exports.claudeKeychainSuffix = claudeKeychainSuffix;
|
|
40
|
+
exports.claudeKeychainService = claudeKeychainService;
|
|
41
|
+
exports.readClaudeProfileIdentity = readClaudeProfileIdentity;
|
|
42
|
+
exports.keychainServiceExists = keychainServiceExists;
|
|
43
|
+
exports.isClaudeProfileAuthenticated = isClaudeProfileAuthenticated;
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const crypto_1 = require("crypto");
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const accountRegistry_1 = require("./accountRegistry");
|
|
49
|
+
const DEFAULT_CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
50
|
+
function getClaudeProfilesDir() {
|
|
51
|
+
return path.join((0, accountRegistry_1.getAccountsDir)(), 'claude', 'profiles');
|
|
52
|
+
}
|
|
53
|
+
function getClaudeProfileHome(uuid) {
|
|
54
|
+
return path.join(getClaudeProfilesDir(), uuid, 'home');
|
|
55
|
+
}
|
|
56
|
+
function ensureClaudeProfileDirs(uuid) {
|
|
57
|
+
fs.mkdirSync(getClaudeProfileHome(uuid), { recursive: true, mode: 0o700 });
|
|
58
|
+
}
|
|
59
|
+
function claudeKeychainSuffix(configDir) {
|
|
60
|
+
return (0, crypto_1.createHash)('sha256').update(configDir).digest('hex').slice(0, 8);
|
|
61
|
+
}
|
|
62
|
+
function claudeKeychainService(configDir) {
|
|
63
|
+
return configDir
|
|
64
|
+
? `${DEFAULT_CLAUDE_KEYCHAIN_SERVICE}-${claudeKeychainSuffix(configDir)}`
|
|
65
|
+
: DEFAULT_CLAUDE_KEYCHAIN_SERVICE;
|
|
66
|
+
}
|
|
67
|
+
function readClaudeProfileIdentity(home) {
|
|
68
|
+
try {
|
|
69
|
+
const raw = JSON.parse(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'));
|
|
70
|
+
const email = raw?.oauthAccount?.emailAddress;
|
|
71
|
+
const uuid = raw?.oauthAccount?.accountUuid;
|
|
72
|
+
return typeof email === 'string' && typeof uuid === 'string'
|
|
73
|
+
? { email, uuid }
|
|
74
|
+
: null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function keychainServiceExists(service) {
|
|
81
|
+
try {
|
|
82
|
+
(0, child_process_1.execFileSync)('security', ['find-generic-password', '-s', service], {
|
|
83
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isClaudeProfileAuthenticated(home) {
|
|
92
|
+
const hasCredentials = process.platform === 'darwin'
|
|
93
|
+
? keychainServiceExists(claudeKeychainService(home))
|
|
94
|
+
: fs.existsSync(path.join(home, '.credentials.json'));
|
|
95
|
+
return hasCredentials && readClaudeProfileIdentity(home) !== null;
|
|
96
|
+
}
|
package/dist/codexProfiles.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SavedAccountProfile } from './accountRegistry';
|
|
1
|
+
import type { AccountIdentityMetadata, SavedAccountProfile } from './accountRegistry';
|
|
2
2
|
import type { AccountManagerResult } from './accounts';
|
|
3
3
|
export interface CodexAccountManagerResult extends AccountManagerResult {
|
|
4
4
|
needsLogin?: boolean;
|
|
@@ -9,12 +9,16 @@ export declare function getSystemCodexHome(): string;
|
|
|
9
9
|
export declare function getCodexMonitoringHomes(): string[];
|
|
10
10
|
export declare function getCodexProfilesDir(): string;
|
|
11
11
|
export declare function getCodexProfileHome(profileId: string): string;
|
|
12
|
+
export declare function readCodexAccountMetadata(codexHome: string): AccountIdentityMetadata;
|
|
13
|
+
export declare function isCodexProfileAuthenticated(codexHome: string): boolean;
|
|
12
14
|
export declare function listCodexAccounts(): SavedAccountProfile[];
|
|
13
15
|
export declare function getActiveCodexAccount(): SavedAccountProfile | null;
|
|
14
16
|
export declare function resolveSidekickCodexHome(): string;
|
|
15
17
|
export declare function getCodexExecutionEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
16
18
|
export declare function prepareCodexAccount(label: string): CodexAccountManagerResult;
|
|
17
|
-
export declare function finalizeCodexAccount(profileId: string
|
|
19
|
+
export declare function finalizeCodexAccount(profileId: string, opts?: {
|
|
20
|
+
activate?: boolean;
|
|
21
|
+
}): CodexAccountManagerResult;
|
|
18
22
|
export declare function switchToCodexAccount(profileId: string): CodexAccountManagerResult;
|
|
19
23
|
export declare function reconcileCodexAuthState(): void;
|
|
20
24
|
export declare function removeCodexAccount(profileId: string): AccountManagerResult;
|