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
@@ -0,0 +1,286 @@
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.getHealthSidecarPath = getHealthSidecarPath;
37
+ exports.readHealthSidecar = readHealthSidecar;
38
+ exports.claudeHealthSidecarFromBlob = claudeHealthSidecarFromBlob;
39
+ exports.writeClaudeHealthSidecar = writeClaudeHealthSidecar;
40
+ exports.codexHealthSidecarFromAuth = codexHealthSidecarFromAuth;
41
+ exports.writeCodexHealthSidecar = writeCodexHealthSidecar;
42
+ exports.getAccountHealth = getAccountHealth;
43
+ exports.listAccountsWithHealth = listAccountsWithHealth;
44
+ exports.describeClaudeCredential = describeClaudeCredential;
45
+ /**
46
+ * Per-account credential health. Health is derived from a secret-free sidecar
47
+ * (`health.json`) next to each profile, written whenever sidekick stores a
48
+ * credential, so reading health never spawns `security` or the provider CLI.
49
+ */
50
+ const fs = __importStar(require("fs"));
51
+ const path = __importStar(require("path"));
52
+ const claudeProfiles_1 = require("./claudeProfiles");
53
+ const claudeCredentials_1 = require("./claudeCredentials");
54
+ const credentialIO_1 = require("./credentialIO");
55
+ const codexAuth_1 = require("./codexAuth");
56
+ const codexPaths_1 = require("./codexPaths");
57
+ const accountRegistry_1 = require("./accountRegistry");
58
+ const atomic_1 = require("./writers/atomic");
59
+ /** Refresh tokens that expire within this window count as `expiring`. */
60
+ const REFRESH_EXPIRY_WARNING_MS = 3 * 24 * 60 * 60 * 1000;
61
+ function getHealthSidecarPath(provider, accountId) {
62
+ const dir = provider === 'codex' ? (0, codexPaths_1.getCodexProfileDir)(accountId) : (0, claudeProfiles_1.getClaudeProfileDir)(accountId);
63
+ return path.join(dir, 'health.json');
64
+ }
65
+ function readHealthSidecar(provider, accountId) {
66
+ try {
67
+ const parsed = JSON.parse(fs.readFileSync(getHealthSidecarPath(provider, accountId), 'utf8'));
68
+ return parsed?.version === 1 && typeof parsed.capturedAt === 'number' ? parsed : null;
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ function writeSidecar(provider, accountId, sidecar) {
75
+ try {
76
+ (0, atomic_1.atomicWriteJsonSync)(getHealthSidecarPath(provider, accountId), sidecar);
77
+ }
78
+ catch {
79
+ // Health is advisory; a failed sidecar write must not fail the credential write.
80
+ }
81
+ }
82
+ /** Build the sidecar for a Claude credential blob. */
83
+ function claudeHealthSidecarFromBlob(blob, source, now = Date.now()) {
84
+ const credentials = (0, claudeCredentials_1.parseClaudeCredentialBlob)(blob);
85
+ if (!credentials)
86
+ return null;
87
+ const refresh = (0, claudeCredentials_1.resolveClaudeRefreshExpiry)(credentials);
88
+ return {
89
+ version: 1,
90
+ capturedAt: now,
91
+ accessExpiresAt: credentials.expiresAt,
92
+ refreshExpiresAt: refresh?.refreshExpiresAt,
93
+ refreshExpiryEstimated: refresh?.estimated,
94
+ hasRefreshToken: Boolean(credentials.refreshToken),
95
+ authMode: 'oauth',
96
+ source,
97
+ };
98
+ }
99
+ /** Record the health of a Claude profile from the credential just stored. */
100
+ function writeClaudeHealthSidecar(accountId, blob, source, now = Date.now()) {
101
+ const sidecar = claudeHealthSidecarFromBlob(blob, source, now);
102
+ if (sidecar)
103
+ writeSidecar('claude-code', accountId, sidecar);
104
+ return sidecar;
105
+ }
106
+ /** Build the sidecar for a raw Codex `auth.json`. */
107
+ function codexHealthSidecarFromAuth(authRaw, source, now = Date.now(), fallbackPath) {
108
+ const identity = (0, codexAuth_1.readAuthIdentityFromRaw)(authRaw);
109
+ if (!identity)
110
+ return null;
111
+ const lastRefreshAt = (0, codexAuth_1.readLastRefresh)(authRaw, fallbackPath) ?? undefined;
112
+ return {
113
+ version: 1,
114
+ capturedAt: now,
115
+ accessExpiresAt: (0, codexAuth_1.readAccessTokenExpiry)(authRaw) ?? undefined,
116
+ lastRefreshAt,
117
+ hasRefreshToken: identity.authMode === 'chatgpt',
118
+ authMode: identity.authMode,
119
+ source,
120
+ };
121
+ }
122
+ /** Record the health of a Codex profile from the auth file just stored. */
123
+ function writeCodexHealthSidecar(accountId, authRaw, source, now = Date.now()) {
124
+ const sidecar = codexHealthSidecarFromAuth(authRaw, source, now);
125
+ if (sidecar)
126
+ writeSidecar('codex', accountId, sidecar);
127
+ return sidecar;
128
+ }
129
+ function stateFromClaudeSidecar(sidecar, now) {
130
+ const accessKnown = sidecar.accessExpiresAt !== undefined;
131
+ const accessExpired = accessKnown && sidecar.accessExpiresAt <= now;
132
+ if (sidecar.refreshExpiresAt !== undefined) {
133
+ if (sidecar.refreshExpiresAt <= now) {
134
+ return {
135
+ state: 'expired',
136
+ reason: sidecar.refreshExpiryEstimated
137
+ ? 'Refresh token has most likely expired (estimated from the snapshot age).'
138
+ : 'Refresh token expired; sign in again.',
139
+ };
140
+ }
141
+ if (sidecar.refreshExpiresAt - now <= REFRESH_EXPIRY_WARNING_MS) {
142
+ return { state: 'expiring', reason: 'Refresh token expires within 3 days.' };
143
+ }
144
+ }
145
+ if (sidecar.hasRefreshToken === false && accessExpired) {
146
+ return { state: 'expired', reason: 'Access token expired and no refresh token is stored.' };
147
+ }
148
+ if (accessExpired) {
149
+ return { state: 'expiring', reason: 'Access token expired; the CLI refreshes it on next use.' };
150
+ }
151
+ if (!accessKnown && sidecar.refreshExpiresAt === undefined) {
152
+ return { state: 'unknown', reason: 'The stored credential carries no expiry.' };
153
+ }
154
+ return { state: 'fresh' };
155
+ }
156
+ function stateFromCodexSidecar(sidecar, now) {
157
+ if (sidecar.authMode === 'api-key') {
158
+ return { state: 'unknown', reason: 'API-key login; expiry is not observable locally.' };
159
+ }
160
+ if (sidecar.accessExpiresAt !== undefined && sidecar.accessExpiresAt > now) {
161
+ return { state: 'fresh' };
162
+ }
163
+ if (sidecar.lastRefreshAt !== undefined &&
164
+ now - sidecar.lastRefreshAt > codexAuth_1.STALE_AUTH_THRESHOLD_MS) {
165
+ const days = Math.floor((now - sidecar.lastRefreshAt) / (24 * 60 * 60 * 1000));
166
+ return {
167
+ state: 'expiring',
168
+ reason: `Not refreshed in ${days} days; codex may ask you to log in again.`,
169
+ };
170
+ }
171
+ if (sidecar.accessExpiresAt !== undefined) {
172
+ return { state: 'expiring', reason: 'Access token expired; codex refreshes it on next use.' };
173
+ }
174
+ return { state: 'unknown', reason: 'The stored credential carries no expiry.' };
175
+ }
176
+ function isLiveClaudeAccount(accountId) {
177
+ return (0, claudeProfiles_1.readLiveClaudeIdentity)()?.uuid === accountId;
178
+ }
179
+ function isLiveCodexAccount(profile) {
180
+ const live = (0, codexAuth_1.readAuthIdentityFromRaw)((0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.getSystemCodexHome)(), 'auth.json')));
181
+ if (!live)
182
+ return false;
183
+ return (0, codexAuth_1.identitiesMatch)(live, {
184
+ email: profile.email ?? profile.metadata?.email,
185
+ workspaceId: profile.metadata?.workspaceId,
186
+ });
187
+ }
188
+ function probeClaudeStore(accountId, now) {
189
+ const blob = (0, credentialIO_1.readActiveCredentials)((0, claudeProfiles_1.getClaudeProfileHome)(accountId));
190
+ return blob ? writeClaudeHealthSidecar(accountId, blob, 'switch', now) : null;
191
+ }
192
+ function probeCodexStore(accountId, now) {
193
+ const authPath = path.join((0, codexPaths_1.getCodexProfileHome)(accountId), 'auth.json');
194
+ const raw = (0, codexPaths_1.readFileOrNull)(authPath);
195
+ const sidecar = codexHealthSidecarFromAuth(raw, 'switch', now, authPath);
196
+ if (sidecar)
197
+ writeSidecar('codex', accountId, sidecar);
198
+ return sidecar;
199
+ }
200
+ function hasStoredClaudeCredential(accountId) {
201
+ const home = (0, claudeProfiles_1.getClaudeProfileHome)(accountId);
202
+ return (fs.existsSync(path.join(home, '.credentials.json')) ||
203
+ fs.existsSync(path.join((0, claudeProfiles_1.getClaudeProfileDir)(accountId), 'health.json')) ||
204
+ fs.existsSync(path.join((0, accountRegistry_1.getAccountsDir)(), 'credentials', `${accountId}.credentials.json`)) ||
205
+ (process.platform === 'darwin' && fs.existsSync(path.join(home, '.claude.json'))));
206
+ }
207
+ function hasStoredCodexCredential(accountId) {
208
+ const home = (0, codexPaths_1.getCodexProfileHome)(accountId);
209
+ return (fs.existsSync(path.join(home, 'auth.json')) ||
210
+ fs.existsSync(path.join(home, '.credentials.json')));
211
+ }
212
+ /**
213
+ * Health of one saved account. `probe: 'cache'` never spawns anything; a
214
+ * missing sidecar on a profile that has credentials is reported as `unknown`
215
+ * (never `expired`) until a store probe or the next sync rewrites it.
216
+ */
217
+ function getAccountHealth(provider, accountId, options = {}) {
218
+ const now = options.now ?? Date.now();
219
+ const probe = options.probe ?? 'cache';
220
+ const profile = (0, accountRegistry_1.listSavedAccountProfiles)(provider).find((p) => p.id === accountId);
221
+ const isLive = provider === 'codex'
222
+ ? profile !== undefined && isLiveCodexAccount(profile)
223
+ : isLiveClaudeAccount(accountId);
224
+ let sidecar = null;
225
+ if (probe === 'store') {
226
+ sidecar =
227
+ provider === 'codex' ? probeCodexStore(accountId, now) : probeClaudeStore(accountId, now);
228
+ }
229
+ sidecar ??= readHealthSidecar(provider, accountId);
230
+ if (!sidecar && provider === 'codex') {
231
+ // Codex sidecars are cheap to derive from the plaintext file.
232
+ sidecar = probeCodexStore(accountId, now);
233
+ }
234
+ if (!sidecar && probe === 'auto' && hasStoredClaudeCredential(accountId)) {
235
+ // First look at a profile that predates the health model: read its store
236
+ // once and record the sidecar so later reads are cache hits.
237
+ sidecar = probeClaudeStore(accountId, now);
238
+ }
239
+ const base = { providerId: provider, accountId, checkedAt: now, isLive };
240
+ if (!sidecar) {
241
+ const stored = provider === 'codex'
242
+ ? hasStoredCodexCredential(accountId)
243
+ : hasStoredClaudeCredential(accountId);
244
+ return stored
245
+ ? { ...base, state: 'unknown', reason: 'Credential health has not been recorded yet.' }
246
+ : { ...base, state: 'missing', reason: 'No stored credentials; sign in again.' };
247
+ }
248
+ const verdict = provider === 'codex'
249
+ ? stateFromCodexSidecar(sidecar, now)
250
+ : stateFromClaudeSidecar(sidecar, now);
251
+ return {
252
+ ...base,
253
+ ...verdict,
254
+ accessExpiresAt: sidecar.accessExpiresAt,
255
+ refreshExpiresAt: sidecar.refreshExpiresAt,
256
+ refreshExpiryEstimated: sidecar.refreshExpiryEstimated,
257
+ lastRefreshAt: sidecar.lastRefreshAt,
258
+ };
259
+ }
260
+ /** Saved accounts with their health, in registry order, grouped by provider. */
261
+ function listAccountsWithHealth(provider, options = {}) {
262
+ const resolved = { probe: 'auto', ...options };
263
+ const providers = provider ? [provider] : ['claude-code', 'codex'];
264
+ const views = [];
265
+ for (const providerId of providers) {
266
+ const active = (0, accountRegistry_1.getActiveSavedAccount)(providerId);
267
+ for (const profile of (0, accountRegistry_1.listSavedAccountProfiles)(providerId)) {
268
+ views.push({
269
+ id: profile.id,
270
+ providerId,
271
+ label: profile.label,
272
+ email: profile.email ?? profile.metadata?.email,
273
+ planType: profile.metadata?.planType,
274
+ isActive: active?.id === profile.id,
275
+ health: getAccountHealth(providerId, profile.id, resolved),
276
+ source: profile.metadata?.origin === 'live-sync' ? 'learned' : 'registered',
277
+ addedAt: profile.addedAt,
278
+ });
279
+ }
280
+ }
281
+ return views;
282
+ }
283
+ /** Sidecar helper shared by the credential chokepoints: extracts the Claude credential shape. */
284
+ function describeClaudeCredential(blob) {
285
+ return (0, claudeCredentials_1.parseClaudeCredentialBlob)(blob);
286
+ }
@@ -0,0 +1,30 @@
1
+ import type { AccountProviderId } from './accountRegistry';
2
+ import { type AccountHealth } from './accountHealth';
3
+ export interface AccountLaunchEnv {
4
+ provider: AccountProviderId;
5
+ accountId: string;
6
+ label?: string;
7
+ email?: string;
8
+ /** The isolated home the CLI will use. */
9
+ home: string;
10
+ /** Variables to set on the child. */
11
+ env: Record<string, string>;
12
+ /** Variables the host must remove from the child's environment. */
13
+ envUnset: string[];
14
+ command: 'claude' | 'codex';
15
+ health: AccountHealth;
16
+ warnings: string[];
17
+ /** Set when the account cannot be launched (expired or missing credentials). */
18
+ error?: string;
19
+ }
20
+ export interface AccountLaunchOptions {
21
+ /**
22
+ * Copy the live `settings.json` / `config.toml` into the profile home once
23
+ * so the isolated session behaves like the live one (default true). Never
24
+ * touches credentials or `oauthAccount`.
25
+ */
26
+ materialize?: boolean;
27
+ /** Launch even when the stored credential is reported expired. */
28
+ force?: boolean;
29
+ }
30
+ export declare function getAccountLaunchEnv(provider: AccountProviderId, accountId: string, options?: AccountLaunchOptions): AccountLaunchEnv;
@@ -0,0 +1,124 @@
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.getAccountLaunchEnv = getAccountLaunchEnv;
37
+ /**
38
+ * Environment for running a provider CLI against a saved profile without
39
+ * touching the live home: two accounts can run side by side, each with its
40
+ * own refresh-token chain.
41
+ */
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const accountRegistry_1 = require("./accountRegistry");
45
+ const accountHealth_1 = require("./accountHealth");
46
+ const claudeProfiles_1 = require("./claudeProfiles");
47
+ const codexPaths_1 = require("./codexPaths");
48
+ const atomic_1 = require("./writers/atomic");
49
+ function materializeClaudeHome(home) {
50
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
51
+ const liveHome = (0, claudeProfiles_1.getLiveClaudeHome)();
52
+ const settingsTarget = path.join(home, 'settings.json');
53
+ if (!fs.existsSync(settingsTarget)) {
54
+ const settings = (0, codexPaths_1.readFileOrNull)(path.join(liveHome, 'settings.json'));
55
+ if (settings)
56
+ (0, atomic_1.atomicWriteFileSync)(settingsTarget, settings);
57
+ }
58
+ const configPath = path.join(home, '.claude.json');
59
+ let config = {};
60
+ try {
61
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
62
+ }
63
+ catch {
64
+ /* fresh */
65
+ }
66
+ if (config.hasCompletedOnboarding !== true) {
67
+ (0, atomic_1.atomicWriteJsonSync)(configPath, { ...config, hasCompletedOnboarding: true });
68
+ }
69
+ }
70
+ function materializeCodexHome(home) {
71
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
72
+ const configTarget = path.join(home, 'config.toml');
73
+ if (!fs.existsSync(configTarget)) {
74
+ const source = (0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.getSystemCodexHome)(), 'config.toml')) ?? '';
75
+ (0, atomic_1.atomicWriteFileSync)(configTarget, (0, codexPaths_1.forceFileCredentialStore)(source));
76
+ }
77
+ }
78
+ function getAccountLaunchEnv(provider, accountId, options = {}) {
79
+ const profile = (0, accountRegistry_1.listSavedAccountProfiles)(provider).find((p) => p.id === accountId);
80
+ const command = provider === 'codex' ? 'codex' : 'claude';
81
+ const home = provider === 'codex'
82
+ ? (0, codexPaths_1.getCodexProfileHome)(accountId)
83
+ : (0, claudeProfiles_1.getClaudeProfileHome)(profile?.providerAccountId ?? accountId);
84
+ const health = (0, accountHealth_1.getAccountHealth)(provider, accountId);
85
+ const warnings = [];
86
+ const base = {
87
+ provider,
88
+ accountId,
89
+ label: profile?.label,
90
+ email: profile?.email ?? profile?.metadata?.email,
91
+ home,
92
+ env: provider === 'codex' ? { CODEX_HOME: home } : { CLAUDE_CONFIG_DIR: home },
93
+ envUnset: provider === 'codex' ? [] : [claudeProfiles_1.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
94
+ command,
95
+ health,
96
+ warnings,
97
+ };
98
+ if (!profile) {
99
+ return { ...base, error: `${provider} account ${accountId} not found.` };
100
+ }
101
+ if ((health.state === 'expired' || health.state === 'missing') && !options.force) {
102
+ return {
103
+ ...base,
104
+ error: `Stored credentials for "${profile.label ?? profile.email ?? accountId}" have ${health.state === 'missing' ? 'not been saved' : 'expired'}; sign in again before launching.`,
105
+ };
106
+ }
107
+ if (health.isLive) {
108
+ warnings.push('This account is also the live login. Two sessions refreshing the same login can invalidate each other; prefer switching instead.');
109
+ }
110
+ if (health.state === 'expiring' && health.reason)
111
+ warnings.push(health.reason);
112
+ if (options.materialize !== false) {
113
+ try {
114
+ if (provider === 'codex')
115
+ materializeCodexHome(home);
116
+ else
117
+ materializeClaudeHome(home);
118
+ }
119
+ catch (err) {
120
+ warnings.push(`Could not prepare the isolated home: ${err}`);
121
+ }
122
+ }
123
+ return base;
124
+ }
@@ -1,5 +1,6 @@
1
1
  import { type AccountProviderId, type SavedAccountProfile } from './accountRegistry';
2
2
  import { type AccountEntry, type AccountManagerResult } from './accounts';
3
+ import { type LastSwitchRecord, type SwitchAccountOptions, type SwitchAccountResult } from './accountSwitch';
3
4
  export interface BeginAccountLoginSuccess {
4
5
  success: true;
5
6
  loginId: string;
@@ -7,7 +8,11 @@ export interface BeginAccountLoginSuccess {
7
8
  command?: string;
8
9
  args?: string[];
9
10
  env?: Record<string, string>;
11
+ /** Variables the host must remove from the child's environment. */
12
+ envUnset?: string[];
10
13
  configDir?: string;
14
+ /** Set when the login re-authenticates an existing saved profile. */
15
+ existingAccountId?: string;
11
16
  }
12
17
  export interface BeginAccountLoginFailure {
13
18
  success: false;
@@ -27,24 +32,37 @@ export interface AccountLoginCommand {
27
32
  command: string;
28
33
  args: string[];
29
34
  }
30
- export interface SpawnAccountLoginOptions extends FinalizeAccountLoginOptions {
35
+ export interface BeginAccountLoginOptions {
36
+ loginCommand?: AccountLoginCommand;
37
+ /** Sign in again to this saved profile instead of creating a new one. */
38
+ existingAccountId?: string;
39
+ }
40
+ export interface SpawnAccountLoginOptions extends FinalizeAccountLoginOptions, BeginAccountLoginOptions {
31
41
  onStatus?: (status: AccountLoginStatus) => void;
32
42
  signal?: AbortSignal;
43
+ /**
44
+ * Inactivity budget: the login fails when the isolated home has not changed
45
+ * for this long. Default 180 s.
46
+ */
33
47
  timeoutMs?: number;
48
+ /** Hard ceiling while the child is alive. Default 900 s. */
49
+ maxTimeoutMs?: number;
34
50
  stdio?: 'inherit' | 'pipe';
35
- loginCommand?: AccountLoginCommand;
36
51
  }
37
52
  export interface ListAllAccountsResult {
38
53
  claude: AccountEntry[];
39
54
  codex: SavedAccountProfile[];
40
55
  activeByProvider: Record<AccountProviderId, string | null>;
41
56
  }
57
+ /**
58
+ * `claude auth login` signs in without the interactive first-run flow the bare
59
+ * `claude /login` form triggers in a fresh config directory. Override with
60
+ * `SIDEKICK_CLAUDE_LOGIN_ARGS` (space separated) for older CLIs.
61
+ */
42
62
  export declare function resolveClaudeLoginCommand(opts?: {
43
63
  loginCommand?: AccountLoginCommand;
44
64
  }): AccountLoginCommand;
45
- export declare function beginAccountLogin(provider: AccountProviderId, label: string, opts?: {
46
- loginCommand?: AccountLoginCommand;
47
- }): BeginAccountLoginResult;
65
+ export declare function beginAccountLogin(provider: AccountProviderId, label: string, opts?: BeginAccountLoginOptions): BeginAccountLoginResult;
48
66
  export declare function getAccountLoginStatus(provider: AccountProviderId, loginId: string): AccountLoginStatus;
49
67
  /**
50
68
  * {@link getAccountLoginStatus} without blocking the event loop on the codex
@@ -56,10 +74,17 @@ export declare function finalizeAccountLogin(provider: AccountProviderId, loginI
56
74
  /** {@link finalizeAccountLogin} without blocking the event loop on CLI probes. */
57
75
  export declare function finalizeAccountLoginAsync(provider: AccountProviderId, loginId: string, opts?: FinalizeAccountLoginOptions): Promise<AccountManagerResult>;
58
76
  export declare function spawnAccountLogin(provider: AccountProviderId, label: string, opts?: SpawnAccountLoginOptions): Promise<AccountManagerResult>;
59
- export declare function switchAccount(provider: AccountProviderId, id: string): AccountManagerResult;
77
+ export declare function switchAccount(provider: AccountProviderId, id: string, options?: SwitchAccountOptions): SwitchAccountResult;
60
78
  /**
61
- * {@link switchAccount} without blocking the event loop on codex CLI probes.
79
+ * {@link switchAccount} without blocking the event loop on CLI/process probes.
62
80
  * The Claude path still performs bounded synchronous keychain reads on macOS.
63
81
  */
64
- export declare function switchAccountAsync(provider: AccountProviderId, id: string): Promise<AccountManagerResult>;
82
+ export declare function switchAccountAsync(provider: AccountProviderId, id: string, options?: SwitchAccountOptions): Promise<SwitchAccountResult>;
83
+ export declare function getLastSwitch(provider: AccountProviderId): LastSwitchRecord | null;
84
+ /**
85
+ * Switch back to the account that was live before the last recorded switch.
86
+ * With `token`, the record must match (a stale Undo button never reverts a
87
+ * newer switch).
88
+ */
89
+ export declare function undoLastSwitch(provider: AccountProviderId, token?: string): Promise<SwitchAccountResult>;
65
90
  export declare function listAllAccounts(): ListAllAccountsResult;