sidekick-shared 0.24.4 → 0.25.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +200 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +151 -0
  7. package/dist/accountManager.d.ts +13 -0
  8. package/dist/accountManager.js +60 -32
  9. package/dist/accountRegistry.d.ts +3 -1
  10. package/dist/accountRegistry.js +64 -41
  11. package/dist/accounts.js +40 -19
  12. package/dist/autoSwitch.d.ts +2 -1
  13. package/dist/autoSwitch.js +32 -7
  14. package/dist/browser.d.ts +10 -3
  15. package/dist/browser.js +17 -1
  16. package/dist/codexProfiles.d.ts +14 -0
  17. package/dist/codexProfiles.js +268 -116
  18. package/dist/codexQuotaWatcher.d.ts +6 -0
  19. package/dist/codexQuotaWatcher.js +101 -47
  20. package/dist/context/sessionContext.js +2 -4
  21. package/dist/ensureDefaultAccounts.js +4 -4
  22. package/dist/index.d.ts +32 -15
  23. package/dist/index.js +54 -9
  24. package/dist/modelAliases.d.ts +13 -0
  25. package/dist/modelAliases.js +58 -0
  26. package/dist/modelCatalog.d.ts +32 -0
  27. package/dist/modelCatalog.js +145 -0
  28. package/dist/modelContext.d.ts +36 -3
  29. package/dist/modelContext.js +109 -34
  30. package/dist/modelInfo.d.ts +30 -1
  31. package/dist/modelInfo.js +116 -26
  32. package/dist/multiProviderQuotaService.d.ts +8 -0
  33. package/dist/multiProviderQuotaService.js +64 -3
  34. package/dist/node.d.ts +8 -2
  35. package/dist/node.js +11 -1
  36. package/dist/observedContextWindows.d.ts +13 -0
  37. package/dist/observedContextWindows.js +13 -0
  38. package/dist/observedSessionCollector.d.ts +75 -4
  39. package/dist/observedSessionCollector.js +452 -42
  40. package/dist/parsers/codexHistory.d.ts +32 -0
  41. package/dist/parsers/codexHistory.js +125 -0
  42. package/dist/parsers/mcpToolName.d.ts +26 -0
  43. package/dist/parsers/mcpToolName.js +31 -0
  44. package/dist/parsers/sessionActivityDetector.d.ts +6 -0
  45. package/dist/parsers/sessionActivityDetector.js +17 -0
  46. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  47. package/dist/parsers/sessionPathResolver.js +11 -4
  48. package/dist/paths.d.ts +7 -3
  49. package/dist/paths.js +7 -3
  50. package/dist/providerIds.d.ts +11 -0
  51. package/dist/providerIds.js +9 -0
  52. package/dist/providerQuota.d.ts +3 -1
  53. package/dist/providerQuota.js +3 -0
  54. package/dist/providers/claudeCode.d.ts +11 -1
  55. package/dist/providers/claudeCode.js +222 -39
  56. package/dist/providers/codex.d.ts +39 -1
  57. package/dist/providers/codex.js +326 -39
  58. package/dist/providers/codexDatabase.d.ts +9 -0
  59. package/dist/providers/codexDatabase.js +107 -2
  60. package/dist/providers/diagnostics.d.ts +13 -0
  61. package/dist/providers/diagnostics.js +88 -0
  62. package/dist/providers/factory.d.ts +14 -0
  63. package/dist/providers/factory.js +57 -0
  64. package/dist/providers/openCode.d.ts +20 -1
  65. package/dist/providers/openCode.js +361 -11
  66. package/dist/providers/openCodeDatabase.d.ts +10 -0
  67. package/dist/providers/openCodeDatabase.js +69 -0
  68. package/dist/providers/types.d.ts +57 -2
  69. package/dist/providers/types.js +4 -0
  70. package/dist/quota.d.ts +4 -1
  71. package/dist/quota.js +3 -7
  72. package/dist/quotaHistory.d.ts +3 -2
  73. package/dist/quotaHistory.js +2 -2
  74. package/dist/quotaPoller.d.ts +13 -1
  75. package/dist/quotaPoller.js +65 -3
  76. package/dist/quotaSnapshots.js +8 -77
  77. package/dist/schemas/accountManager.d.ts +2 -2
  78. package/dist/schemas/accountManager.js +2 -4
  79. package/dist/schemas/accountStatus.d.ts +3 -0
  80. package/dist/schemas/accountStatus.js +1 -0
  81. package/dist/schemas/index.d.ts +1 -0
  82. package/dist/schemas/index.js +7 -2
  83. package/dist/schemas/observedSessionV1.d.ts +27 -26
  84. package/dist/schemas/observedSessionV1.js +6 -4
  85. package/dist/schemas/quota.d.ts +2 -2
  86. package/dist/schemas/quota.js +3 -6
  87. package/dist/schemas/quotaHistory.d.ts +2 -2
  88. package/dist/schemas/tokenEstimation.d.ts +1 -1
  89. package/dist/sessionMonitor.d.ts +15 -0
  90. package/dist/sessionMonitor.js +142 -0
  91. package/dist/sessionPreviews.d.ts +85 -0
  92. package/dist/sessionPreviews.js +549 -0
  93. package/dist/sessionTranscripts.js +10 -4
  94. package/dist/types/observedSessionV1.d.ts +6 -2
  95. package/dist/types/observedSessionV1.js +14 -8
  96. package/dist/writers/atomic.d.ts +20 -0
  97. package/dist/writers/atomic.js +190 -19
  98. package/package.json +22 -1
@@ -0,0 +1,151 @@
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.onAccountsChanged = onAccountsChanged;
37
+ exports._notifyAccountsChanged = _notifyAccountsChanged;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const accountChangeSignal_1 = require("./accountChangeSignal");
42
+ const accountStatus_1 = require("./accountStatus");
43
+ const accountRegistry_1 = require("./accountRegistry");
44
+ const codexProfiles_1 = require("./codexProfiles");
45
+ const listeners = new Set();
46
+ let watchers = [];
47
+ let pollTimer;
48
+ let stopRawListener;
49
+ let lastFingerprint;
50
+ let debounceTimer;
51
+ let activePollIntervalMs = 30_000;
52
+ /**
53
+ * Observe login, logout, and active-account switches. Local Sidekick mutations
54
+ * signal immediately; filesystem watchers cover external CLIs; a low-frequency
55
+ * catch-up poll protects network mounts and watcher-hostile filesystems.
56
+ */
57
+ function onAccountsChanged(listener, options = {}) {
58
+ listeners.add(listener);
59
+ activePollIntervalMs = Math.max(1_000, options.pollIntervalMs ?? activePollIntervalMs);
60
+ ensureMonitoring();
61
+ if (options.emitCurrent)
62
+ emitIfChanged('poll', true, listener);
63
+ return {
64
+ dispose: () => {
65
+ listeners.delete(listener);
66
+ if (listeners.size === 0)
67
+ stopMonitoring();
68
+ },
69
+ };
70
+ }
71
+ function ensureMonitoring() {
72
+ if (stopRawListener)
73
+ return;
74
+ lastFingerprint = fingerprint((0, accountStatus_1.getActiveAccountStatus)());
75
+ stopRawListener = (0, accountChangeSignal_1._onRawAccountsChanged)((reason) => scheduleCheck(reason));
76
+ for (const target of watchTargets()) {
77
+ try {
78
+ const watcher = fs.watch(target, { persistent: false }, () => {
79
+ scheduleCheck('filesystem');
80
+ });
81
+ watcher.on('error', () => {
82
+ watchers = watchers.filter((candidate) => candidate !== watcher);
83
+ try {
84
+ watcher.close();
85
+ }
86
+ catch {
87
+ // The catch-up poll remains active.
88
+ }
89
+ });
90
+ watchers.push(watcher);
91
+ }
92
+ catch {
93
+ // Missing/unwatchable directories are covered by the catch-up poll.
94
+ }
95
+ }
96
+ pollTimer = setInterval(() => emitIfChanged('poll'), activePollIntervalMs);
97
+ pollTimer.unref?.();
98
+ }
99
+ function stopMonitoring() {
100
+ stopRawListener?.();
101
+ stopRawListener = undefined;
102
+ for (const watcher of watchers)
103
+ watcher.close();
104
+ watchers = [];
105
+ if (pollTimer)
106
+ clearInterval(pollTimer);
107
+ pollTimer = undefined;
108
+ if (debounceTimer)
109
+ clearTimeout(debounceTimer);
110
+ debounceTimer = undefined;
111
+ lastFingerprint = undefined;
112
+ }
113
+ function watchTargets() {
114
+ return Array.from(new Set([(0, accountRegistry_1.getAccountsDir)(), path.join(os.homedir(), '.claude'), (0, codexProfiles_1.getSystemCodexHome)()]));
115
+ }
116
+ function scheduleCheck(reason) {
117
+ if (debounceTimer)
118
+ clearTimeout(debounceTimer);
119
+ debounceTimer = setTimeout(() => {
120
+ debounceTimer = undefined;
121
+ // Never force: watch targets include ~/.claude and the Codex home, whose
122
+ // history files churn on every prompt. Only a real status change may wake
123
+ // subscribers, or dormant quota pollers would poll on unrelated writes.
124
+ emitIfChanged(reason);
125
+ }, 25);
126
+ debounceTimer.unref?.();
127
+ }
128
+ function emitIfChanged(reason, force = false, singleListener) {
129
+ const status = (0, accountStatus_1.getActiveAccountStatus)();
130
+ const nextFingerprint = fingerprint(status);
131
+ if (!force && nextFingerprint === lastFingerprint)
132
+ return;
133
+ lastFingerprint = nextFingerprint;
134
+ const event = { reason, status };
135
+ const targets = singleListener ? [singleListener] : [...listeners];
136
+ for (const listener of targets) {
137
+ try {
138
+ listener(event);
139
+ }
140
+ catch {
141
+ // A host callback cannot stop monitoring.
142
+ }
143
+ }
144
+ }
145
+ function fingerprint(status) {
146
+ return JSON.stringify(status);
147
+ }
148
+ /** Internal bridge for account mutations outside the registry module. */
149
+ function _notifyAccountsChanged() {
150
+ (0, accountChangeSignal_1._emitAccountsChanged)('local');
151
+ }
@@ -46,7 +46,20 @@ export declare function beginAccountLogin(provider: AccountProviderId, label: st
46
46
  loginCommand?: AccountLoginCommand;
47
47
  }): BeginAccountLoginResult;
48
48
  export declare function getAccountLoginStatus(provider: AccountProviderId, loginId: string): AccountLoginStatus;
49
+ /**
50
+ * {@link getAccountLoginStatus} without blocking the event loop on the codex
51
+ * CLI probe. The sync variant blocks up to 4s per call while a codex login is
52
+ * pending — ruinous for callers that poll it on an interval.
53
+ */
54
+ export declare function getAccountLoginStatusAsync(provider: AccountProviderId, loginId: string): Promise<AccountLoginStatus>;
49
55
  export declare function finalizeAccountLogin(provider: AccountProviderId, loginId: string, opts?: FinalizeAccountLoginOptions): AccountManagerResult;
56
+ /** {@link finalizeAccountLogin} without blocking the event loop on CLI probes. */
57
+ export declare function finalizeAccountLoginAsync(provider: AccountProviderId, loginId: string, opts?: FinalizeAccountLoginOptions): Promise<AccountManagerResult>;
50
58
  export declare function spawnAccountLogin(provider: AccountProviderId, label: string, opts?: SpawnAccountLoginOptions): Promise<AccountManagerResult>;
51
59
  export declare function switchAccount(provider: AccountProviderId, id: string): AccountManagerResult;
60
+ /**
61
+ * {@link switchAccount} without blocking the event loop on codex CLI probes.
62
+ * The Claude path still performs bounded synchronous keychain reads on macOS.
63
+ */
64
+ export declare function switchAccountAsync(provider: AccountProviderId, id: string): Promise<AccountManagerResult>;
52
65
  export declare function listAllAccounts(): ListAllAccountsResult;
@@ -36,15 +36,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.resolveClaudeLoginCommand = resolveClaudeLoginCommand;
37
37
  exports.beginAccountLogin = beginAccountLogin;
38
38
  exports.getAccountLoginStatus = getAccountLoginStatus;
39
+ exports.getAccountLoginStatusAsync = getAccountLoginStatusAsync;
39
40
  exports.finalizeAccountLogin = finalizeAccountLogin;
41
+ exports.finalizeAccountLoginAsync = finalizeAccountLoginAsync;
40
42
  exports.spawnAccountLogin = spawnAccountLogin;
41
43
  exports.switchAccount = switchAccount;
44
+ exports.switchAccountAsync = switchAccountAsync;
42
45
  exports.listAllAccounts = listAllAccounts;
43
46
  const child_process_1 = require("child_process");
44
47
  const crypto_1 = require("crypto");
45
48
  const fs = __importStar(require("fs"));
46
49
  const path = __importStar(require("path"));
47
50
  const accountRegistry_1 = require("./accountRegistry");
51
+ const atomic_1 = require("./writers/atomic");
48
52
  const accounts_1 = require("./accounts");
49
53
  const claudeProfiles_1 = require("./claudeProfiles");
50
54
  const credentialIO_1 = require("./credentialIO");
@@ -55,25 +59,6 @@ function getClaudeProfileDir(loginId) {
55
59
  function getPendingClaudeProfilePath(loginId) {
56
60
  return path.join(getClaudeProfileDir(loginId), 'profile.json');
57
61
  }
58
- function atomicWriteJson(filePath, data, mode = 0o600) {
59
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
60
- const tmp = `${filePath}.tmp`;
61
- const json = JSON.stringify(data, null, 2);
62
- JSON.parse(json);
63
- try {
64
- fs.writeFileSync(tmp, json, { encoding: 'utf8', mode });
65
- fs.renameSync(tmp, filePath);
66
- }
67
- catch (err) {
68
- try {
69
- fs.unlinkSync(tmp);
70
- }
71
- catch {
72
- /* nothing to clean up */
73
- }
74
- throw err;
75
- }
76
- }
77
62
  function readPendingClaudeProfile(loginId) {
78
63
  try {
79
64
  return JSON.parse(fs.readFileSync(getPendingClaudeProfilePath(loginId), 'utf8'));
@@ -84,7 +69,7 @@ function readPendingClaudeProfile(loginId) {
84
69
  }
85
70
  function writePendingClaudeProfile(loginId, pending) {
86
71
  (0, claudeProfiles_1.ensureClaudeProfileDirs)(loginId);
87
- atomicWriteJson(getPendingClaudeProfilePath(loginId), pending);
72
+ (0, atomic_1.atomicWriteJsonSync)(getPendingClaudeProfilePath(loginId), pending);
88
73
  }
89
74
  function removePendingClaudeProfile(loginId) {
90
75
  try {
@@ -182,6 +167,13 @@ function beginAccountLogin(provider, label, opts = {}) {
182
167
  configDir: home,
183
168
  };
184
169
  }
170
+ function getClaudeAccountLoginStatus(loginId) {
171
+ const home = (0, claudeProfiles_1.getClaudeProfileHome)(loginId);
172
+ const identity = (0, claudeProfiles_1.readClaudeProfileIdentity)(home);
173
+ return (0, claudeProfiles_1.isClaudeProfileAuthenticated)(home)
174
+ ? { state: 'authenticated', email: identity?.email }
175
+ : { state: 'pending' };
176
+ }
185
177
  function getAccountLoginStatus(provider, loginId) {
186
178
  if (provider === 'codex') {
187
179
  const codexHome = (0, codexProfiles_1.getCodexProfileHome)(loginId);
@@ -194,16 +186,41 @@ function getAccountLoginStatus(provider, loginId) {
194
186
  email: metadata.email,
195
187
  };
196
188
  }
197
- const home = (0, claudeProfiles_1.getClaudeProfileHome)(loginId);
198
- const identity = (0, claudeProfiles_1.readClaudeProfileIdentity)(home);
199
- return (0, claudeProfiles_1.isClaudeProfileAuthenticated)(home)
200
- ? { state: 'authenticated', email: identity?.email }
201
- : { state: 'pending' };
189
+ return getClaudeAccountLoginStatus(loginId);
190
+ }
191
+ /**
192
+ * {@link getAccountLoginStatus} without blocking the event loop on the codex
193
+ * CLI probe. The sync variant blocks up to 4s per call while a codex login is
194
+ * pending — ruinous for callers that poll it on an interval.
195
+ */
196
+ async function getAccountLoginStatusAsync(provider, loginId) {
197
+ if (provider === 'codex') {
198
+ const codexHome = (0, codexProfiles_1.getCodexProfileHome)(loginId);
199
+ if (!(await (0, codexProfiles_1.isCodexProfileAuthenticatedAsync)(codexHome))) {
200
+ return { state: 'pending' };
201
+ }
202
+ const metadata = await (0, codexProfiles_1.readCodexAccountMetadataAsync)(codexHome);
203
+ return {
204
+ state: 'authenticated',
205
+ email: metadata.email,
206
+ };
207
+ }
208
+ return getClaudeAccountLoginStatus(loginId);
202
209
  }
203
210
  function finalizeAccountLogin(provider, loginId, opts = {}) {
204
211
  if (provider === 'codex') {
205
212
  return (0, codexProfiles_1.finalizeCodexAccount)(loginId, opts);
206
213
  }
214
+ return finalizeClaudeAccountLogin(loginId, opts);
215
+ }
216
+ /** {@link finalizeAccountLogin} without blocking the event loop on CLI probes. */
217
+ async function finalizeAccountLoginAsync(provider, loginId, opts = {}) {
218
+ if (provider === 'codex') {
219
+ return (0, codexProfiles_1.finalizeCodexAccountAsync)(loginId, opts);
220
+ }
221
+ return finalizeClaudeAccountLogin(loginId, opts);
222
+ }
223
+ function finalizeClaudeAccountLogin(loginId, opts = {}) {
207
224
  const home = (0, claudeProfiles_1.getClaudeProfileHome)(loginId);
208
225
  const identity = (0, claudeProfiles_1.readClaudeProfileIdentity)(home);
209
226
  if (!identity) {
@@ -218,8 +235,8 @@ function finalizeAccountLogin(provider, loginId, opts = {}) {
218
235
  return { success: false, error: 'Could not read Claude profile config.' };
219
236
  }
220
237
  const pending = readPendingClaudeProfile(loginId);
221
- atomicWriteJson(getClaudeCredentialsBackupPath(identity.uuid), credentials);
222
- atomicWriteJson(getClaudeConfigBackupPath(identity.uuid), oauthAccount);
238
+ (0, atomic_1.atomicWriteJsonSync)(getClaudeCredentialsBackupPath(identity.uuid), credentials);
239
+ (0, atomic_1.atomicWriteJsonSync)(getClaudeConfigBackupPath(identity.uuid), oauthAccount);
223
240
  copyClaudeProfileToCanonicalHome(home, identity.uuid, credentials);
224
241
  (0, accountRegistry_1.upsertSavedAccountProfile)({
225
242
  id: identity.uuid,
@@ -274,7 +291,7 @@ async function spawnAccountLogin(provider, label, opts = {}) {
274
291
  if (!begin.success)
275
292
  return { success: false, error: begin.error };
276
293
  if (begin.alreadyComplete) {
277
- return finalizeAccountLogin(provider, begin.loginId, { activate: opts.activate ?? true });
294
+ return finalizeAccountLoginAsync(provider, begin.loginId, { activate: opts.activate ?? true });
278
295
  }
279
296
  if (!begin.command) {
280
297
  return { success: false, error: 'Account login command was not prepared.' };
@@ -308,9 +325,11 @@ async function spawnAccountLogin(provider, label, opts = {}) {
308
325
  emitStatus(opts, { state: 'failed', error: 'Account login aborted.' });
309
326
  return { success: false, error: 'Account login aborted.' };
310
327
  }
311
- const status = emitStatus(opts, getAccountLoginStatus(provider, begin.loginId));
328
+ const status = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
312
329
  if (status.state === 'authenticated') {
313
- return finalizeAccountLogin(provider, begin.loginId, { activate: opts.activate ?? true });
330
+ return finalizeAccountLoginAsync(provider, begin.loginId, {
331
+ activate: opts.activate ?? true,
332
+ });
314
333
  }
315
334
  if (childExited) {
316
335
  if (childState.spawnError) {
@@ -318,9 +337,11 @@ async function spawnAccountLogin(provider, label, opts = {}) {
318
337
  emitStatus(opts, { state: 'failed', error });
319
338
  return { success: false, error };
320
339
  }
321
- const finalStatus = emitStatus(opts, getAccountLoginStatus(provider, begin.loginId));
340
+ const finalStatus = emitStatus(opts, await getAccountLoginStatusAsync(provider, begin.loginId));
322
341
  if (finalStatus.state === 'authenticated') {
323
- return finalizeAccountLogin(provider, begin.loginId, { activate: opts.activate ?? true });
342
+ return finalizeAccountLoginAsync(provider, begin.loginId, {
343
+ activate: opts.activate ?? true,
344
+ });
324
345
  }
325
346
  emitStatus(opts, {
326
347
  state: 'failed',
@@ -344,6 +365,13 @@ async function spawnAccountLogin(provider, label, opts = {}) {
344
365
  function switchAccount(provider, id) {
345
366
  return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccount)(id) : (0, accounts_1.switchToAccount)(id);
346
367
  }
368
+ /**
369
+ * {@link switchAccount} without blocking the event loop on codex CLI probes.
370
+ * The Claude path still performs bounded synchronous keychain reads on macOS.
371
+ */
372
+ async function switchAccountAsync(provider, id) {
373
+ return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccountAsync)(id) : (0, accounts_1.switchToAccount)(id);
374
+ }
347
375
  function listAllAccounts() {
348
376
  return {
349
377
  claude: (0, accounts_1.listAccounts)(),
@@ -1,4 +1,6 @@
1
- export type AccountProviderId = 'claude-code' | 'codex';
1
+ import type { AccountProviderId } from './providerIds';
2
+ export { ACCOUNT_PROVIDER_IDS } from './providerIds';
3
+ export type { AccountProviderId } from './providerIds';
2
4
  export interface AccountIdentityMetadata {
3
5
  email?: string;
4
6
  workspaceId?: string;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ACCOUNT_PROVIDER_IDS = void 0;
36
37
  exports.getAccountsDir = getAccountsDir;
37
38
  exports.readSavedAccountRegistry = readSavedAccountRegistry;
38
39
  exports.writeSavedAccountRegistry = writeSavedAccountRegistry;
@@ -45,21 +46,40 @@ exports.removeSavedAccountProfile = removeSavedAccountProfile;
45
46
  const fs = __importStar(require("fs"));
46
47
  const path = __importStar(require("path"));
47
48
  const paths_1 = require("./paths");
49
+ const atomic_1 = require("./writers/atomic");
50
+ const accountChangeSignal_1 = require("./accountChangeSignal");
51
+ var providerIds_1 = require("./providerIds");
52
+ Object.defineProperty(exports, "ACCOUNT_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.ACCOUNT_PROVIDER_IDS; } });
48
53
  function getAccountsDir() {
49
54
  return path.join((0, paths_1.getConfigDir)(), 'accounts');
50
55
  }
51
56
  function getRegistryPath() {
52
57
  return path.join(getAccountsDir(), 'accounts.json');
53
58
  }
59
+ function getRegistryLockPath() {
60
+ return getRegistryPath() + '.lock';
61
+ }
54
62
  function ensureAccountsDir() {
55
63
  fs.mkdirSync(getAccountsDir(), { recursive: true, mode: 0o700 });
56
64
  }
57
- function atomicWriteJson(filePath, data, mode = 0o600) {
58
- const tmp = filePath + '.tmp';
59
- const json = JSON.stringify(data, null, 2);
60
- JSON.parse(json);
61
- fs.writeFileSync(tmp, json, { encoding: 'utf8', mode });
62
- fs.renameSync(tmp, filePath);
65
+ function writeRegistryUnlocked(registry) {
66
+ (0, atomic_1.atomicWriteJsonSync)(getRegistryPath(), normalizeRegistry(registry));
67
+ (0, accountChangeSignal_1._emitAccountsChanged)('local');
68
+ }
69
+ /**
70
+ * Serializes registry read-modify-write cycles across processes. Without the
71
+ * lock, two concurrent mutators (CLI, editor extension, external consumers)
72
+ * read the same base state and the second write silently drops the first
73
+ * mutation. Lock ordering: callers that also hold a provider auth-swap lock
74
+ * take that lock first and this one inside it, never the reverse.
75
+ */
76
+ function mutateSavedAccountRegistry(mutator) {
77
+ ensureAccountsDir();
78
+ return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () => {
79
+ const next = mutator(readSavedAccountRegistry() ?? createEmptyRegistry());
80
+ writeRegistryUnlocked(next);
81
+ return next;
82
+ });
63
83
  }
64
84
  function createEmptyRegistry() {
65
85
  return {
@@ -136,7 +156,7 @@ function readSavedAccountRegistry() {
136
156
  }
137
157
  function writeSavedAccountRegistry(registry) {
138
158
  ensureAccountsDir();
139
- atomicWriteJson(getRegistryPath(), normalizeRegistry(registry));
159
+ (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () => writeRegistryUnlocked(registry));
140
160
  }
141
161
  function listSavedAccountProfiles(providerId) {
142
162
  const registry = readSavedAccountRegistry();
@@ -156,44 +176,47 @@ function getActiveSavedAccount(providerId) {
156
176
  return (registry.accounts.find((account) => account.providerId === providerId && account.id === activeId) ?? null);
157
177
  }
158
178
  function upsertSavedAccountProfile(profile) {
159
- const registry = readSavedAccountRegistry() ?? createEmptyRegistry();
160
- const index = registry.accounts.findIndex((account) => account.providerId === profile.providerId && account.id === profile.id);
161
- if (index >= 0) {
162
- registry.accounts[index] = profile;
163
- }
164
- else {
165
- registry.accounts.push(profile);
166
- }
167
- writeSavedAccountRegistry(registry);
168
- return registry;
179
+ return mutateSavedAccountRegistry((registry) => {
180
+ const index = registry.accounts.findIndex((account) => account.providerId === profile.providerId && account.id === profile.id);
181
+ if (index >= 0) {
182
+ registry.accounts[index] = profile;
183
+ }
184
+ else {
185
+ registry.accounts.push(profile);
186
+ }
187
+ return registry;
188
+ });
169
189
  }
170
190
  function setActiveSavedAccount(providerId, accountId) {
171
- const registry = readSavedAccountRegistry() ?? createEmptyRegistry();
172
- registry.activeByProvider[providerId] = accountId;
173
- writeSavedAccountRegistry(registry);
174
- return registry;
191
+ return mutateSavedAccountRegistry((registry) => {
192
+ registry.activeByProvider[providerId] = accountId;
193
+ return registry;
194
+ });
175
195
  }
176
196
  function replaceSavedAccountProfiles(providerId, accounts, activeId) {
177
- const registry = readSavedAccountRegistry() ?? createEmptyRegistry();
178
- registry.accounts = registry.accounts
179
- .filter((account) => account.providerId !== providerId)
180
- .concat(accounts);
181
- registry.activeByProvider[providerId] = activeId;
182
- writeSavedAccountRegistry(registry);
183
- return registry;
197
+ return mutateSavedAccountRegistry((registry) => {
198
+ registry.accounts = registry.accounts
199
+ .filter((account) => account.providerId !== providerId)
200
+ .concat(accounts);
201
+ registry.activeByProvider[providerId] = activeId;
202
+ return registry;
203
+ });
184
204
  }
185
205
  function removeSavedAccountProfile(providerId, accountId) {
186
- const registry = readSavedAccountRegistry();
187
- if (!registry)
188
- return null;
189
- const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
190
- if (index === -1)
191
- return null;
192
- const [removed] = registry.accounts.splice(index, 1);
193
- if (registry.activeByProvider[providerId] === accountId) {
194
- registry.activeByProvider[providerId] =
195
- registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
196
- }
197
- writeSavedAccountRegistry(registry);
198
- return removed;
206
+ ensureAccountsDir();
207
+ return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () => {
208
+ const registry = readSavedAccountRegistry();
209
+ if (!registry)
210
+ return null;
211
+ const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
212
+ if (index === -1)
213
+ return null;
214
+ const [removed] = registry.accounts.splice(index, 1);
215
+ if (registry.activeByProvider[providerId] === accountId) {
216
+ registry.activeByProvider[providerId] =
217
+ registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
218
+ }
219
+ writeRegistryUnlocked(registry);
220
+ return removed;
221
+ });
199
222
  }
package/dist/accounts.js CHANGED
@@ -56,6 +56,7 @@ const fs = __importStar(require("fs"));
56
56
  const path = __importStar(require("path"));
57
57
  const os = __importStar(require("os"));
58
58
  const credentialIO_1 = require("./credentialIO");
59
+ const atomic_1 = require("./writers/atomic");
59
60
  const accountRegistry_1 = require("./accountRegistry");
60
61
  const claudeProfiles_1 = require("./claudeProfiles");
61
62
  // ── Paths ────────────────────────────────────────────────────────────────
@@ -91,16 +92,6 @@ function ensureDirs() {
91
92
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
92
93
  }
93
94
  }
94
- // ── Atomic file write ────────────────────────────────────────────────────
95
- function atomicWriteJson(filePath, data, mode = 0o600) {
96
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
97
- const tmp = filePath + '.tmp';
98
- const json = JSON.stringify(data, null, 2);
99
- // Validate we can re-parse before writing
100
- JSON.parse(json);
101
- fs.writeFileSync(tmp, json, { encoding: 'utf8', mode });
102
- fs.renameSync(tmp, filePath);
103
- }
104
95
  function readJsonOrNull(filePath) {
105
96
  try {
106
97
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -114,7 +105,7 @@ function readOauthAccountFromConfigPath(filePath) {
114
105
  return parsed?.oauthAccount ?? null;
115
106
  }
116
107
  function writeClaudeProfileConfig(uuid, oauthAccount) {
117
- atomicWriteJson(path.join((0, claudeProfiles_1.getClaudeProfileHome)(uuid), '.claude.json'), { oauthAccount });
108
+ (0, atomic_1.atomicWriteJsonSync)(path.join((0, claudeProfiles_1.getClaudeProfileHome)(uuid), '.claude.json'), { oauthAccount });
118
109
  }
119
110
  function writeClaudeProfileCredentials(uuid, credentials) {
120
111
  (0, credentialIO_1.writeActiveCredentials)(credentials, (0, claudeProfiles_1.getClaudeProfileHome)(uuid));
@@ -162,7 +153,7 @@ function backupCurrentClaudeLiveHome() {
162
153
  const originalOauthAccount = readCurrentLiveOauthAccount();
163
154
  if (originalCreds) {
164
155
  try {
165
- atomicWriteJson(path.join(getCredentialsDir(), `${currentActive.uuid}.credentials.json`), originalCreds);
156
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${currentActive.uuid}.credentials.json`), originalCreds);
166
157
  writeClaudeProfileCredentials(currentActive.uuid, originalCreds);
167
158
  }
168
159
  catch {
@@ -171,7 +162,7 @@ function backupCurrentClaudeLiveHome() {
171
162
  }
172
163
  if (originalOauthAccount) {
173
164
  try {
174
- atomicWriteJson(path.join(getConfigsDir(), `${currentActive.uuid}.config.json`), originalOauthAccount);
165
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${currentActive.uuid}.config.json`), originalOauthAccount);
175
166
  writeClaudeProfileConfig(currentActive.uuid, originalOauthAccount);
176
167
  }
177
168
  catch {
@@ -188,7 +179,7 @@ function mergeOauthAccountIntoLiveConfig(oauthAccount) {
188
179
  // File may not exist yet.
189
180
  }
190
181
  configObj.oauthAccount = oauthAccount;
191
- atomicWriteJson(getClaudeConfigPath(), configObj);
182
+ (0, atomic_1.atomicWriteJsonSync)(getClaudeConfigPath(), configObj);
192
183
  }
193
184
  // ── Registry operations ──────────────────────────────────────────────────
194
185
  function readAccountRegistry() {
@@ -274,8 +265,8 @@ function addCurrentAccount(label) {
274
265
  if (existing) {
275
266
  if (label !== undefined)
276
267
  existing.label = label;
277
- atomicWriteJson(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
278
- atomicWriteJson(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
268
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
269
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
279
270
  try {
280
271
  writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
281
272
  }
@@ -287,8 +278,8 @@ function addCurrentAccount(label) {
287
278
  return { success: true };
288
279
  }
289
280
  // 6. New account — back up credentials
290
- atomicWriteJson(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
291
- atomicWriteJson(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
281
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
282
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
292
283
  try {
293
284
  writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
294
285
  }
@@ -307,7 +298,28 @@ function addCurrentAccount(label) {
307
298
  return { success: true };
308
299
  }
309
300
  // ── Switch to account ────────────────────────────────────────────────────
301
+ /**
302
+ * Serializes live Claude credential swaps across processes so two concurrent
303
+ * switchers cannot interleave their backup/write/rollback sequences. It cannot
304
+ * exclude Claude Code's own writes to the live home — only our switchers. Lock
305
+ * ordering: this lock first, the registry lock (inside setActiveSavedAccount)
306
+ * inside it — never the reverse.
307
+ */
308
+ function withClaudeAuthSwapLock(operation) {
309
+ const lockDir = path.join((0, accountRegistry_1.getAccountsDir)(), 'claude');
310
+ fs.mkdirSync(lockDir, { recursive: true, mode: 0o700 });
311
+ return (0, atomic_1.withFileLockSync)(path.join(lockDir, 'auth-swap.lock'), operation);
312
+ }
310
313
  function switchToAccount(uuid) {
314
+ try {
315
+ // Keep the non-throwing result contract when the lock cannot be acquired.
316
+ return withClaudeAuthSwapLock(() => switchToAccountUnlocked(uuid));
317
+ }
318
+ catch (err) {
319
+ return { success: false, error: `Could not acquire the account-switch lock: ${err}` };
320
+ }
321
+ }
322
+ function switchToAccountUnlocked(uuid) {
311
323
  const registry = readAccountRegistry();
312
324
  if (!registry) {
313
325
  return { success: false, error: 'No account registry found. Save an account first.' };
@@ -338,7 +350,7 @@ function switchToAccount(uuid) {
338
350
  catch (err) {
339
351
  return { success: false, error: `Failed to update account registry: ${err}` };
340
352
  }
341
- const applied = applyActiveClaudeToLiveHome();
353
+ const applied = applyActiveClaudeToLiveHomeUnlocked();
342
354
  if (!applied.success) {
343
355
  try {
344
356
  (0, accountRegistry_1.setActiveSavedAccount)('claude-code', registry.activeAccountUuid);
@@ -373,6 +385,15 @@ function resolveActiveClaudeHome() {
373
385
  return (0, claudeProfiles_1.getClaudeProfileHome)(active.providerAccountId ?? active.id);
374
386
  }
375
387
  function applyActiveClaudeToLiveHome() {
388
+ try {
389
+ // Keep the non-throwing result contract when the lock cannot be acquired.
390
+ return withClaudeAuthSwapLock(applyActiveClaudeToLiveHomeUnlocked);
391
+ }
392
+ catch (err) {
393
+ return { success: false, error: `Could not acquire the account-switch lock: ${err}` };
394
+ }
395
+ }
396
+ function applyActiveClaudeToLiveHomeUnlocked() {
376
397
  const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
377
398
  if (!active) {
378
399
  return { success: false, error: 'No active Claude account found.' };
@@ -36,7 +36,7 @@ export interface AutoSwitchControllerOptions {
36
36
  getCodexAccounts?: () => SavedAccountProfile[];
37
37
  getActiveCodexAccount?: () => SavedAccountProfile | null;
38
38
  readSnapshot?: (providerId: AccountProviderId, accountId: string) => QuotaState | null;
39
- switchAccount?: (providerId: AccountProviderId, accountId: string) => AccountManagerResult;
39
+ switchAccount?: (providerId: AccountProviderId, accountId: string) => AccountManagerResult | Promise<AccountManagerResult>;
40
40
  onTransition?: (event: AutoSwitchTransitionEvent) => void;
41
41
  log?: (message: string, error?: unknown) => void;
42
42
  cooldownMs?: number;
@@ -59,6 +59,7 @@ export declare class AutoSwitchController implements Disposable {
59
59
  private subscription;
60
60
  private readonly switchedDuringCrossing;
61
61
  private readonly lastSwitchAt;
62
+ private readonly switchInFlight;
62
63
  constructor(options: AutoSwitchControllerOptions);
63
64
  start(): void;
64
65
  stop(): void;