sidekick-shared 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- if (registry.activeAccountUuid === uuid) {
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
- // Save current account's backup if we know who's active
216
- const currentActive = readActiveClaudeAccount();
217
- if (currentActive) {
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
- atomicWriteJson(path.join(getCredentialsDir(), `${currentActive.uuid}.credentials.json`), originalCreds);
335
+ (0, credentialIO_1.writeActiveCredentials)(originalCreds);
221
336
  }
222
- catch { /* best effort */ }
337
+ catch { /* rollback failed */ }
223
338
  }
224
339
  if (originalConfig) {
225
340
  try {
226
- const parsed = JSON.parse(originalConfig);
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 { /* best effort */ }
343
+ catch { /* rollback failed */ }
232
344
  }
345
+ return applied;
233
346
  }
234
- // 2. Read target account's backed-up credentials + config
235
- let targetCreds;
236
- let targetOauthAccount;
237
- try {
238
- targetCreds = JSON.parse(fs.readFileSync(path.join(getCredentialsDir(), `${uuid}.credentials.json`), 'utf8'));
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
- catch {
241
- return { success: false, error: `Backed-up credentials for ${target.email} not found.` };
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
- targetOauthAccount = JSON.parse(fs.readFileSync(path.join(getConfigsDir(), `${uuid}.config.json`), 'utf8'));
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
- // 3. Write target credentials (platform-aware: Keychain on macOS, file on Linux)
370
+ catch { /* no existing config to back up */ }
250
371
  try {
251
- (0, credentialIO_1.writeActiveCredentials)(targetCreds);
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
- let configObj = {};
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
+ }
@@ -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): CodexAccountManagerResult;
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;