sidekick-shared 0.26.5 → 0.26.6

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 (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +56 -51
  3. package/dist/accountChanges.js +21 -2
  4. package/dist/accountHealth.d.ts +72 -0
  5. package/dist/accountHealth.js +286 -0
  6. package/dist/accountLaunch.d.ts +30 -0
  7. package/dist/accountLaunch.js +124 -0
  8. package/dist/accountManager.d.ts +33 -8
  9. package/dist/accountManager.js +183 -50
  10. package/dist/accountRegistry.d.ts +10 -0
  11. package/dist/accountRegistry.js +42 -22
  12. package/dist/accountSwitch.d.ts +43 -0
  13. package/dist/accountSwitch.js +115 -0
  14. package/dist/accountSync.d.ts +30 -0
  15. package/dist/accountSync.js +180 -0
  16. package/dist/accountSyncTypes.d.ts +25 -0
  17. package/dist/accountSyncTypes.js +7 -0
  18. package/dist/accounts.d.ts +58 -6
  19. package/dist/accounts.js +408 -189
  20. package/dist/claudeCredentials.d.ts +38 -0
  21. package/dist/claudeCredentials.js +74 -0
  22. package/dist/claudeProfiles.d.ts +40 -0
  23. package/dist/claudeProfiles.js +102 -10
  24. package/dist/codexAuth.d.ts +32 -0
  25. package/dist/codexAuth.js +127 -0
  26. package/dist/codexPaths.d.ts +21 -0
  27. package/dist/codexPaths.js +117 -0
  28. package/dist/codexProfiles.d.ts +58 -5
  29. package/dist/codexProfiles.js +574 -289
  30. package/dist/credentialIO.d.ts +12 -5
  31. package/dist/credentialIO.js +63 -29
  32. package/dist/credentials.d.ts +8 -0
  33. package/dist/credentials.js +28 -32
  34. package/dist/ensureDefaultAccounts.d.ts +8 -0
  35. package/dist/ensureDefaultAccounts.js +42 -82
  36. package/dist/index.d.ts +27 -10
  37. package/dist/index.js +61 -9
  38. package/dist/keepAlive.d.ts +44 -0
  39. package/dist/keepAlive.js +106 -0
  40. package/dist/processDetection.d.ts +32 -0
  41. package/dist/processDetection.js +210 -0
  42. package/dist/schemas/accountManager.d.ts +283 -1
  43. package/dist/schemas/accountManager.js +122 -1
  44. package/dist/schemas/assistantTurn.d.ts +2 -2
  45. package/dist/schemas/index.d.ts +6 -1
  46. package/dist/schemas/index.js +14 -1
  47. package/dist/schemas/observedSessionV1.d.ts +21 -21
  48. package/dist/schemas/quota.d.ts +18 -18
  49. package/dist/schemas/quotaHistory.d.ts +1 -1
  50. package/dist/schemas/stateFile.d.ts +5 -5
  51. package/dist/terminalSync.d.ts +5 -5
  52. package/dist/terminalSync.js +7 -112
  53. package/dist/writers/atomic.d.ts +2 -0
  54. package/dist/writers/atomic.js +49 -2
  55. package/package.json +1 -1
@@ -42,6 +42,8 @@ exports.finalizeAccountLoginAsync = finalizeAccountLoginAsync;
42
42
  exports.spawnAccountLogin = spawnAccountLogin;
43
43
  exports.switchAccount = switchAccount;
44
44
  exports.switchAccountAsync = switchAccountAsync;
45
+ exports.getLastSwitch = getLastSwitch;
46
+ exports.undoLastSwitch = undoLastSwitch;
45
47
  exports.listAllAccounts = listAllAccounts;
46
48
  const child_process_1 = require("child_process");
47
49
  const crypto_1 = require("crypto");
@@ -53,11 +55,9 @@ const accounts_1 = require("./accounts");
53
55
  const claudeProfiles_1 = require("./claudeProfiles");
54
56
  const credentialIO_1 = require("./credentialIO");
55
57
  const codexProfiles_1 = require("./codexProfiles");
56
- function getClaudeProfileDir(loginId) {
57
- return path.dirname((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
58
- }
58
+ const accountSwitch_1 = require("./accountSwitch");
59
59
  function getPendingClaudeProfilePath(loginId) {
60
- return path.join(getClaudeProfileDir(loginId), 'profile.json');
60
+ return path.join((0, claudeProfiles_1.getClaudeProfileDir)(loginId), 'profile.json');
61
61
  }
62
62
  function readPendingClaudeProfile(loginId) {
63
63
  try {
@@ -79,11 +79,21 @@ function removePendingClaudeProfile(loginId) {
79
79
  /* best effort */
80
80
  }
81
81
  }
82
- function getClaudeCredentialsBackupPath(uuid) {
83
- return path.join((0, accountRegistry_1.getAccountsDir)(), 'credentials', `${uuid}.credentials.json`);
84
- }
85
- function getClaudeConfigBackupPath(uuid) {
86
- return path.join((0, accountRegistry_1.getAccountsDir)(), 'configs', `${uuid}.config.json`);
82
+ /**
83
+ * A fresh `CLAUDE_CONFIG_DIR` puts the CLI through first-run onboarding before
84
+ * the login prompt. Seeding the onboarding flag keeps the isolated login on the
85
+ * sign-in flow only; `oauthAccount` is written by the CLI itself.
86
+ */
87
+ function seedClaudeLoginHome(home) {
88
+ const configPath = path.join(home, '.claude.json');
89
+ if (fs.existsSync(configPath))
90
+ return;
91
+ try {
92
+ (0, atomic_1.atomicWriteJsonSync)(configPath, { hasCompletedOnboarding: true });
93
+ }
94
+ catch {
95
+ /* the CLI creates the file itself */
96
+ }
87
97
  }
88
98
  function readClaudeOauthAccount(home) {
89
99
  try {
@@ -94,7 +104,7 @@ function readClaudeOauthAccount(home) {
94
104
  return null;
95
105
  }
96
106
  }
97
- function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials) {
107
+ function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials, oauthAccount) {
98
108
  const canonicalHome = (0, claudeProfiles_1.getClaudeProfileHome)(accountUuid);
99
109
  fs.mkdirSync(canonicalHome, { recursive: true, mode: 0o700 });
100
110
  if (path.resolve(sourceHome) !== path.resolve(canonicalHome)) {
@@ -105,27 +115,40 @@ function copyClaudeProfileToCanonicalHome(sourceHome, accountUuid, credentials)
105
115
  // The credential and identity files are re-written below when possible.
106
116
  }
107
117
  }
108
- const sourceConfig = path.join(sourceHome, '.claude.json');
109
- if (fs.existsSync(sourceConfig)) {
110
- fs.copyFileSync(sourceConfig, path.join(canonicalHome, '.claude.json'));
111
- }
112
- (0, credentialIO_1.writeActiveCredentials)(credentials, canonicalHome);
118
+ (0, accounts_1.storeClaudeProfileIdentity)(accountUuid, oauthAccount);
119
+ (0, accounts_1.storeClaudeProfileCredentials)(accountUuid, credentials, 'login');
113
120
  }
114
121
  function parseClaudeLoginArgs(raw) {
115
122
  const trimmed = raw?.trim();
116
123
  return trimmed ? trimmed.split(/\s+/) : null;
117
124
  }
125
+ /**
126
+ * `claude auth login` signs in without the interactive first-run flow the bare
127
+ * `claude /login` form triggers in a fresh config directory. Override with
128
+ * `SIDEKICK_CLAUDE_LOGIN_ARGS` (space separated) for older CLIs.
129
+ */
118
130
  function resolveClaudeLoginCommand(opts = {}) {
119
131
  if (opts.loginCommand)
120
132
  return opts.loginCommand;
121
133
  return {
122
134
  command: 'claude',
123
- args: parseClaudeLoginArgs(process.env.SIDEKICK_CLAUDE_LOGIN_ARGS) ?? ['/login'],
135
+ args: parseClaudeLoginArgs(process.env.SIDEKICK_CLAUDE_LOGIN_ARGS) ?? ['auth', 'login'],
124
136
  };
125
137
  }
126
138
  function beginAccountLogin(provider, label, opts = {}) {
127
139
  if (provider === 'codex') {
128
- const prepared = (0, codexProfiles_1.prepareCodexAccount)(label);
140
+ const existing = opts.existingAccountId
141
+ ? (0, codexProfiles_1.listCodexAccounts)().find((account) => account.id === opts.existingAccountId)
142
+ : undefined;
143
+ if (opts.existingAccountId && !existing) {
144
+ return { success: false, error: `Codex account ${opts.existingAccountId} not found.` };
145
+ }
146
+ // A re-login uses a throwaway label: finalize folds the fresh credentials
147
+ // into the existing profile by identity and keeps its label.
148
+ const effectiveLabel = existing
149
+ ? `${existing.label ?? existing.email ?? 'codex'} (re-login ${(0, crypto_1.randomUUID)().slice(0, 8)})`
150
+ : label.trim() || `Codex ${(0, codexProfiles_1.listCodexAccounts)().length + 1}`;
151
+ const prepared = (0, codexProfiles_1.prepareCodexAccount)(effectiveLabel);
129
152
  if (!prepared.success || !prepared.profileId || !prepared.codexHome) {
130
153
  return { success: false, error: prepared.error ?? 'Could not prepare Codex account login.' };
131
154
  }
@@ -135,6 +158,7 @@ function beginAccountLogin(provider, label, opts = {}) {
135
158
  loginId: prepared.profileId,
136
159
  alreadyComplete: true,
137
160
  configDir: prepared.codexHome,
161
+ existingAccountId: existing?.id,
138
162
  };
139
163
  }
140
164
  const loginCommand = opts.loginCommand ?? { command: 'codex', args: ['login'] };
@@ -145,18 +169,23 @@ function beginAccountLogin(provider, label, opts = {}) {
145
169
  args: loginCommand.args,
146
170
  env: { CODEX_HOME: prepared.codexHome },
147
171
  configDir: prepared.codexHome,
172
+ existingAccountId: existing?.id,
148
173
  };
149
174
  }
150
- const trimmedLabel = label.trim();
151
- if (!trimmedLabel) {
152
- return { success: false, error: 'Claude accounts require a non-empty label.' };
175
+ const existing = opts.existingAccountId
176
+ ? (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((account) => account.id === opts.existingAccountId)
177
+ : undefined;
178
+ if (opts.existingAccountId && !existing) {
179
+ return { success: false, error: `Claude account ${opts.existingAccountId} not found.` };
153
180
  }
154
181
  const loginId = (0, crypto_1.randomUUID)();
155
182
  const home = (0, claudeProfiles_1.getClaudeProfileHome)(loginId);
156
183
  writePendingClaudeProfile(loginId, {
157
- label: trimmedLabel,
158
- addedAt: new Date().toISOString(),
184
+ label: existing?.label ?? label.trim(),
185
+ addedAt: existing?.addedAt ?? new Date().toISOString(),
186
+ existingAccountId: existing?.id,
159
187
  });
188
+ seedClaudeLoginHome(home);
160
189
  const loginCommand = resolveClaudeLoginCommand(opts);
161
190
  return {
162
191
  success: true,
@@ -164,7 +193,9 @@ function beginAccountLogin(provider, label, opts = {}) {
164
193
  command: loginCommand.command,
165
194
  args: loginCommand.args,
166
195
  env: { CLAUDE_CONFIG_DIR: home },
196
+ envUnset: [claudeProfiles_1.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
167
197
  configDir: home,
198
+ existingAccountId: existing?.id,
168
199
  };
169
200
  }
170
201
  function getClaudeAccountLoginStatus(loginId) {
@@ -235,28 +266,51 @@ function finalizeClaudeAccountLogin(loginId, opts = {}) {
235
266
  return { success: false, error: 'Could not read Claude profile config.' };
236
267
  }
237
268
  const pending = readPendingClaudeProfile(loginId);
238
- (0, atomic_1.atomicWriteJsonSync)(getClaudeCredentialsBackupPath(identity.uuid), credentials);
239
- (0, atomic_1.atomicWriteJsonSync)(getClaudeConfigBackupPath(identity.uuid), oauthAccount);
240
- copyClaudeProfileToCanonicalHome(home, identity.uuid, credentials);
269
+ const existing = (0, accountRegistry_1.listSavedAccountProfiles)('claude-code').find((account) => (account.providerAccountId ?? account.id) === identity.uuid);
270
+ copyClaudeProfileToCanonicalHome(home, identity.uuid, credentials, oauthAccount);
271
+ const warnings = [];
272
+ if (pending?.existingAccountId && pending.existingAccountId !== identity.uuid) {
273
+ warnings.push(`You signed in as ${identity.email}, which is a different account from the one you chose to re-authenticate; it was saved separately.`);
274
+ }
241
275
  (0, accountRegistry_1.upsertSavedAccountProfile)({
242
- id: identity.uuid,
276
+ id: existing?.id ?? identity.uuid,
243
277
  providerId: 'claude-code',
244
278
  providerAccountId: identity.uuid,
245
279
  email: identity.email,
246
- label: pending?.label,
247
- addedAt: pending?.addedAt ?? new Date().toISOString(),
280
+ label: existing?.label ?? (pending?.label || identity.email),
281
+ addedAt: existing?.addedAt ?? pending?.addedAt ?? new Date().toISOString(),
248
282
  metadata: {
283
+ ...existing?.metadata,
249
284
  email: identity.email,
285
+ origin: 'login',
250
286
  },
251
287
  });
252
288
  removePendingClaudeProfile(loginId);
253
289
  if (loginId !== identity.uuid) {
254
290
  removePendingClaudeProfile(identity.uuid);
291
+ // The temporary home's keychain item holds live tokens; nothing references it now.
292
+ (0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
293
+ try {
294
+ fs.rmSync((0, claudeProfiles_1.getClaudeProfileDir)(loginId), { recursive: true, force: true });
295
+ }
296
+ catch {
297
+ /* the temporary login home is disposable */
298
+ }
255
299
  }
256
300
  if (opts.activate === false) {
257
- return { success: true };
301
+ return { success: true, warning: warnings.length ? warnings.join(' ') : undefined };
302
+ }
303
+ if ((0, accounts_1.readActiveClaudeAccount)()?.uuid === identity.uuid) {
304
+ // Re-login of the account that is already live: the new token replaces
305
+ // the live one (which may be the dead credential that prompted the login).
306
+ const applied = (0, accounts_1.applyClaudeProfileToLiveHome)(identity.uuid);
307
+ if (!applied.success)
308
+ return applied;
258
309
  }
259
- return (0, accounts_1.switchToAccount)(identity.uuid);
310
+ const switched = (0, accounts_1.switchToAccount)(identity.uuid);
311
+ return warnings.length
312
+ ? { ...switched, warning: [switched.warning, ...warnings].filter(Boolean).join(' ') }
313
+ : switched;
260
314
  }
261
315
  function emitStatus(opts, status) {
262
316
  opts.onStatus?.(status);
@@ -283,11 +337,48 @@ function waitForNextPoll(ms, signal) {
283
337
  signal?.addEventListener('abort', onAbort, { once: true });
284
338
  });
285
339
  }
340
+ /** Most recent modification inside an isolated login home (the CLI writes as the user progresses). */
341
+ function latestHomeActivity(home) {
342
+ if (!home)
343
+ return 0;
344
+ let latest = 0;
345
+ const candidates = [
346
+ home,
347
+ ...['.claude.json', '.credentials.json', 'auth.json', 'config.toml'].map((f) => path.join(home, f)),
348
+ ];
349
+ for (const candidate of candidates) {
350
+ try {
351
+ latest = Math.max(latest, fs.statSync(candidate).mtimeMs);
352
+ }
353
+ catch {
354
+ /* absent */
355
+ }
356
+ }
357
+ return latest;
358
+ }
359
+ function discardPendingLogin(provider, loginId) {
360
+ try {
361
+ const dir = provider === 'codex'
362
+ ? path.dirname((0, codexProfiles_1.getCodexProfileHome)(loginId))
363
+ : (0, claudeProfiles_1.getClaudeProfileDir)(loginId);
364
+ if (!dir.startsWith((0, accountRegistry_1.getAccountsDir)()))
365
+ return;
366
+ if (provider === 'claude-code')
367
+ (0, credentialIO_1.deleteStoredCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(loginId));
368
+ fs.rmSync(dir, { recursive: true, force: true });
369
+ }
370
+ catch {
371
+ /* best effort */
372
+ }
373
+ }
286
374
  async function spawnAccountLogin(provider, label, opts = {}) {
287
375
  if (opts.signal?.aborted) {
288
376
  return { success: false, error: 'Account login aborted.' };
289
377
  }
290
- const begin = beginAccountLogin(provider, label, { loginCommand: opts.loginCommand });
378
+ const begin = beginAccountLogin(provider, label, {
379
+ loginCommand: opts.loginCommand,
380
+ existingAccountId: opts.existingAccountId,
381
+ });
291
382
  if (!begin.success)
292
383
  return { success: false, error: begin.error };
293
384
  if (begin.alreadyComplete) {
@@ -299,16 +390,25 @@ async function spawnAccountLogin(provider, label, opts = {}) {
299
390
  let childExited = false;
300
391
  let childExitCode = null;
301
392
  const childState = {};
302
- const timeoutMs = opts.timeoutMs ?? 180_000;
303
- const deadline = Date.now() + timeoutMs;
393
+ const inactivityMs = opts.timeoutMs ?? 180_000;
394
+ const maxMs = opts.maxTimeoutMs ?? 900_000;
395
+ const startedAt = Date.now();
396
+ let lastActivity = startedAt;
397
+ let lastSeenMtime = latestHomeActivity(begin.configDir);
398
+ const env = provider === 'codex'
399
+ ? { ...process.env, ...(begin.env ?? {}) }
400
+ : (0, claudeProfiles_1.buildClaudeChildEnv)(begin.configDir ?? '', process.env);
401
+ for (const name of begin.envUnset ?? [])
402
+ delete env[name];
304
403
  let child;
305
404
  try {
306
405
  child = (0, child_process_1.spawn)(begin.command, begin.args ?? [], {
307
- env: { ...process.env, ...(begin.env ?? {}) },
406
+ env,
308
407
  stdio: opts.stdio ?? 'inherit',
309
408
  });
310
409
  }
311
410
  catch (err) {
411
+ discardPendingLogin(provider, begin.loginId);
312
412
  return { success: false, error: `Could not spawn account login: ${err}` };
313
413
  }
314
414
  child.on('exit', (code) => {
@@ -319,12 +419,15 @@ async function spawnAccountLogin(provider, label, opts = {}) {
319
419
  childExited = true;
320
420
  childState.spawnError = error;
321
421
  });
422
+ const abort = () => {
423
+ child.kill();
424
+ discardPendingLogin(provider, begin.loginId);
425
+ emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
426
+ return { success: false, error: 'Account login aborted.' };
427
+ };
322
428
  while (true) {
323
- if (opts.signal?.aborted) {
324
- child.kill();
325
- emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
326
- return { success: false, error: 'Account login aborted.' };
327
- }
429
+ if (opts.signal?.aborted)
430
+ return abort();
328
431
  const status = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
329
432
  if (status.state === 'authenticated') {
330
433
  return finalizeAccountLoginAsync(provider, begin.loginId, {
@@ -334,6 +437,7 @@ async function spawnAccountLogin(provider, label, opts = {}) {
334
437
  if (childExited) {
335
438
  if (childState.spawnError) {
336
439
  const error = `Could not spawn account login: ${childState.spawnError.message}`;
440
+ discardPendingLogin(provider, begin.loginId);
337
441
  emitStatus(opts, { state: 'failed', error });
338
442
  return { success: false, error };
339
443
  }
@@ -343,34 +447,63 @@ async function spawnAccountLogin(provider, label, opts = {}) {
343
447
  activate: opts.activate ?? true,
344
448
  });
345
449
  }
450
+ discardPendingLogin(provider, begin.loginId);
346
451
  emitStatus(opts, {
347
452
  state: 'failed',
348
453
  error: `Account login exited before authentication completed${childExitCode === null ? '.' : ` (exit ${childExitCode}).`}`,
349
454
  });
350
455
  return { success: false, error: 'Account login did not complete.' };
351
456
  }
352
- const remainingMs = deadline - Date.now();
457
+ const now = Date.now();
458
+ const mtime = latestHomeActivity(begin.configDir);
459
+ if (mtime > lastSeenMtime) {
460
+ lastSeenMtime = mtime;
461
+ lastActivity = now;
462
+ }
463
+ const remainingMs = Math.min(inactivityMs - (now - lastActivity), maxMs - (now - startedAt));
353
464
  if (remainingMs <= 0) {
465
+ child.kill();
466
+ discardPendingLogin(provider, begin.loginId);
354
467
  emitStatus(opts, { state: 'failed', error: 'Account login timed out.' });
355
468
  return { success: false, error: 'Account login timed out.' };
356
469
  }
357
470
  const shouldContinue = await waitForNextPoll(Math.min(2_000, remainingMs), opts.signal);
358
- if (!shouldContinue) {
359
- child.kill();
360
- emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
361
- return { success: false, error: 'Account login aborted.' };
362
- }
471
+ if (!shouldContinue)
472
+ return abort();
363
473
  }
364
474
  }
365
- function switchAccount(provider, id) {
366
- return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccount)(id) : (0, accounts_1.switchToAccount)(id);
475
+ function switchAccount(provider, id, options = {}) {
476
+ return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccount)(id, options) : (0, accounts_1.switchToAccount)(id, options);
367
477
  }
368
478
  /**
369
- * {@link switchAccount} without blocking the event loop on codex CLI probes.
479
+ * {@link switchAccount} without blocking the event loop on CLI/process probes.
370
480
  * The Claude path still performs bounded synchronous keychain reads on macOS.
371
481
  */
372
- async function switchAccountAsync(provider, id) {
373
- return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccountAsync)(id) : (0, accounts_1.switchToAccount)(id);
482
+ async function switchAccountAsync(provider, id, options = {}) {
483
+ return provider === 'codex'
484
+ ? (0, codexProfiles_1.switchToCodexAccountAsync)(id, options)
485
+ : (0, accounts_1.switchToAccountAsync)(id, options);
486
+ }
487
+ function getLastSwitch(provider) {
488
+ return (0, accountSwitch_1.readLastSwitch)(provider);
489
+ }
490
+ /**
491
+ * Switch back to the account that was live before the last recorded switch.
492
+ * With `token`, the record must match (a stale Undo button never reverts a
493
+ * newer switch).
494
+ */
495
+ async function undoLastSwitch(provider, token) {
496
+ const record = (0, accountSwitch_1.readLastSwitch)(provider);
497
+ if (!record) {
498
+ return (0, accountSwitch_1.switchFailure)(provider, '', 'There is no account switch to undo.');
499
+ }
500
+ if (token && token !== record.token) {
501
+ return (0, accountSwitch_1.switchFailure)(provider, record.from ?? '', 'A newer account switch has happened since; nothing was undone.');
502
+ }
503
+ if (!record.from) {
504
+ return (0, accountSwitch_1.switchFailure)(provider, '', 'The previous state had no saved account to return to.');
505
+ }
506
+ return switchAccountAsync(provider, record.from);
374
507
  }
375
508
  function listAllAccounts() {
376
509
  return {
@@ -1,11 +1,15 @@
1
1
  import type { AccountProviderId } from './providerIds';
2
2
  export { ACCOUNT_PROVIDER_IDS } from './providerIds';
3
3
  export type { AccountProviderId } from './providerIds';
4
+ /** How a saved profile came to exist. */
5
+ export type AccountProfileOrigin = 'login' | 'manual' | 'live-sync' | 'migration';
4
6
  export interface AccountIdentityMetadata {
5
7
  email?: string;
6
8
  workspaceId?: string;
7
9
  planType?: string;
8
10
  authMode?: 'chatgpt' | 'api-key' | 'unknown';
11
+ /** `live-sync` marks accounts sidekick registered from a login it observed. */
12
+ origin?: AccountProfileOrigin;
9
13
  }
10
14
  export interface SavedAccountProfile {
11
15
  id: string;
@@ -63,6 +67,10 @@ export declare function listSavedAccountProfiles(providerId?: AccountProviderId)
63
67
  export declare function getActiveSavedAccount(providerId: AccountProviderId): SavedAccountProfile | null;
64
68
  export declare function upsertSavedAccountProfile(profile: SavedAccountProfile): SavedAccountRegistry;
65
69
  export declare function setActiveSavedAccount(providerId: AccountProviderId, accountId: string | null, options?: RegistryWriteOptions): SavedAccountRegistry;
70
+ /** {@link upsertSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
71
+ export declare function upsertSavedAccountProfileUnlocked(profile: SavedAccountProfile): SavedAccountRegistry;
72
+ /** {@link setActiveSavedAccount} for callers already inside {@link withSavedAccountRegistryLock}. */
73
+ export declare function setActiveSavedAccountUnlocked(providerId: AccountProviderId, accountId: string | null, options?: RegistryWriteOptions): SavedAccountRegistry;
66
74
  export declare function replaceSavedAccountProfiles(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
67
75
  /**
68
76
  * Same as {@link replaceSavedAccountProfiles} without taking the registry
@@ -70,3 +78,5 @@ export declare function replaceSavedAccountProfiles(providerId: AccountProviderI
70
78
  */
71
79
  export declare function replaceSavedAccountProfilesUnlocked(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
72
80
  export declare function removeSavedAccountProfile(providerId: AccountProviderId, accountId: string): SavedAccountProfile | null;
81
+ /** {@link removeSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
82
+ export declare function removeSavedAccountProfileUnlocked(providerId: AccountProviderId, accountId: string): SavedAccountProfile | null;
@@ -42,9 +42,12 @@ exports.listSavedAccountProfiles = listSavedAccountProfiles;
42
42
  exports.getActiveSavedAccount = getActiveSavedAccount;
43
43
  exports.upsertSavedAccountProfile = upsertSavedAccountProfile;
44
44
  exports.setActiveSavedAccount = setActiveSavedAccount;
45
+ exports.upsertSavedAccountProfileUnlocked = upsertSavedAccountProfileUnlocked;
46
+ exports.setActiveSavedAccountUnlocked = setActiveSavedAccountUnlocked;
45
47
  exports.replaceSavedAccountProfiles = replaceSavedAccountProfiles;
46
48
  exports.replaceSavedAccountProfilesUnlocked = replaceSavedAccountProfilesUnlocked;
47
49
  exports.removeSavedAccountProfile = removeSavedAccountProfile;
50
+ exports.removeSavedAccountProfileUnlocked = removeSavedAccountProfileUnlocked;
48
51
  const fs = __importStar(require("fs"));
49
52
  const path = __importStar(require("path"));
50
53
  const paths_1 = require("./paths");
@@ -125,6 +128,7 @@ function normalizeRegistry(registry) {
125
128
  workspaceId: account.metadata.workspaceId || undefined,
126
129
  planType: account.metadata.planType || undefined,
127
130
  authMode: account.metadata.authMode || undefined,
131
+ origin: account.metadata.origin || undefined,
128
132
  }
129
133
  : undefined,
130
134
  }))
@@ -189,7 +193,19 @@ function getActiveSavedAccount(providerId) {
189
193
  return (registry.accounts.find((account) => account.providerId === providerId && account.id === activeId) ?? null);
190
194
  }
191
195
  function upsertSavedAccountProfile(profile) {
192
- return mutateSavedAccountRegistry((registry) => {
196
+ return mutateSavedAccountRegistry(upsertProfile(profile));
197
+ }
198
+ function setActiveSavedAccount(providerId, accountId, options = {}) {
199
+ return mutateSavedAccountRegistry(setActive(providerId, accountId), options);
200
+ }
201
+ function setActive(providerId, accountId) {
202
+ return (registry) => {
203
+ registry.activeByProvider[providerId] = accountId;
204
+ return registry;
205
+ };
206
+ }
207
+ function upsertProfile(profile) {
208
+ return (registry) => {
193
209
  const index = registry.accounts.findIndex((account) => account.providerId === profile.providerId && account.id === profile.id);
194
210
  if (index >= 0) {
195
211
  registry.accounts[index] = profile;
@@ -198,13 +214,15 @@ function upsertSavedAccountProfile(profile) {
198
214
  registry.accounts.push(profile);
199
215
  }
200
216
  return registry;
201
- });
217
+ };
202
218
  }
203
- function setActiveSavedAccount(providerId, accountId, options = {}) {
204
- return mutateSavedAccountRegistry((registry) => {
205
- registry.activeByProvider[providerId] = accountId;
206
- return registry;
207
- }, options);
219
+ /** {@link upsertSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
220
+ function upsertSavedAccountProfileUnlocked(profile) {
221
+ return mutateSavedAccountRegistryUnlocked(upsertProfile(profile));
222
+ }
223
+ /** {@link setActiveSavedAccount} for callers already inside {@link withSavedAccountRegistryLock}. */
224
+ function setActiveSavedAccountUnlocked(providerId, accountId, options = {}) {
225
+ return mutateSavedAccountRegistryUnlocked(setActive(providerId, accountId), options);
208
226
  }
209
227
  function replaceProfiles(providerId, accounts, activeId) {
210
228
  return (registry) => {
@@ -227,19 +245,21 @@ function replaceSavedAccountProfilesUnlocked(providerId, accounts, activeId) {
227
245
  }
228
246
  function removeSavedAccountProfile(providerId, accountId) {
229
247
  ensureAccountsDir();
230
- return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () => {
231
- const registry = readSavedAccountRegistry();
232
- if (!registry)
233
- return null;
234
- const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
235
- if (index === -1)
236
- return null;
237
- const [removed] = registry.accounts.splice(index, 1);
238
- if (registry.activeByProvider[providerId] === accountId) {
239
- registry.activeByProvider[providerId] =
240
- registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
241
- }
242
- writeRegistryUnlocked(registry);
243
- return removed;
244
- });
248
+ return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () => removeSavedAccountProfileUnlocked(providerId, accountId));
249
+ }
250
+ /** {@link removeSavedAccountProfile} for callers already inside {@link withSavedAccountRegistryLock}. */
251
+ function removeSavedAccountProfileUnlocked(providerId, accountId) {
252
+ const registry = readSavedAccountRegistry();
253
+ if (!registry)
254
+ return null;
255
+ const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
256
+ if (index === -1)
257
+ return null;
258
+ const [removed] = registry.accounts.splice(index, 1);
259
+ if (registry.activeByProvider[providerId] === accountId) {
260
+ registry.activeByProvider[providerId] =
261
+ registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
262
+ }
263
+ writeRegistryUnlocked(registry);
264
+ return removed;
245
265
  }
@@ -0,0 +1,43 @@
1
+ import { type AccountProviderId } from './accountRegistry';
2
+ import type { AccountHealth } from './accountHealth';
3
+ import type { AccountManagerResult } from './accounts';
4
+ import type { RunningAccountConsumer } from './processDetection';
5
+ export type SwitchVerification = 'store' | 'cli' | 'none' | 'failed';
6
+ export interface SwitchAccountResult extends AccountManagerResult {
7
+ provider: AccountProviderId;
8
+ accountId: string;
9
+ previousAccountId: string | null;
10
+ /** True when the live store was re-read after the write and matched the target. */
11
+ verified: boolean;
12
+ verification: SwitchVerification;
13
+ health?: AccountHealth;
14
+ warnings: string[];
15
+ hints: string[];
16
+ runningConsumers: RunningAccountConsumer[];
17
+ /** Present after a successful switch that changed the live login. */
18
+ undoToken?: string;
19
+ /** True when the target was already the live login and nothing was written. */
20
+ alreadyActive?: boolean;
21
+ /** Email of the account now live, when known. */
22
+ email?: string;
23
+ }
24
+ export interface SwitchAccountOptions {
25
+ /** Also confirm through the provider CLI after the store check (async paths only). */
26
+ verifyWithCli?: boolean;
27
+ /** Switch even when the stored credential is reported expired. */
28
+ force?: boolean;
29
+ }
30
+ export interface LastSwitchRecord {
31
+ token: string;
32
+ provider: AccountProviderId;
33
+ from: string | null;
34
+ to: string;
35
+ at: string;
36
+ }
37
+ export declare function getLastSwitchPath(provider: AccountProviderId): string;
38
+ export declare function readLastSwitch(provider: AccountProviderId): LastSwitchRecord | null;
39
+ export declare function writeLastSwitch(provider: AccountProviderId, from: string | null, to: string): LastSwitchRecord;
40
+ export declare function clearLastSwitch(provider: AccountProviderId): void;
41
+ export declare function switchFailure(provider: AccountProviderId, accountId: string, error: string, extra?: Partial<SwitchAccountResult>): SwitchAccountResult;
42
+ /** Fold per-consumer warnings into a result; keeps `warning` (legacy) in sync. */
43
+ export declare function finishSwitchResult(result: SwitchAccountResult): SwitchAccountResult;