sidekick-shared 0.24.5 → 0.26.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 (157) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +205 -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 +170 -0
  7. package/dist/accountRegistry.d.ts +25 -2
  8. package/dist/accountRegistry.js +41 -13
  9. package/dist/accountStatus.d.ts +9 -1
  10. package/dist/accountStatus.js +3 -3
  11. package/dist/accounts.d.ts +3 -1
  12. package/dist/accounts.js +84 -64
  13. package/dist/aggregation/EventAggregator.d.ts +15 -2
  14. package/dist/aggregation/EventAggregator.js +54 -9
  15. package/dist/aggregation/costProvenance.d.ts +14 -0
  16. package/dist/aggregation/costProvenance.js +35 -0
  17. package/dist/aggregation/snapshot.d.ts +13 -2
  18. package/dist/aggregation/snapshot.js +57 -6
  19. package/dist/aggregation/types.d.ts +30 -1
  20. package/dist/analytics/compactionLedger.js +15 -5
  21. package/dist/analytics/qualityScore.js +2 -1
  22. package/dist/browser.d.ts +19 -4
  23. package/dist/browser.js +43 -1
  24. package/dist/codexProfiles.d.ts +3 -1
  25. package/dist/codexProfiles.js +4 -2
  26. package/dist/codexQuota.d.ts +2 -0
  27. package/dist/codexQuota.js +11 -66
  28. package/dist/codexQuotaWatcher.d.ts +6 -0
  29. package/dist/codexQuotaWatcher.js +101 -47
  30. package/dist/failingTools.d.ts +37 -0
  31. package/dist/failingTools.js +70 -0
  32. package/dist/formatters/sessionDump.js +13 -7
  33. package/dist/formatting.d.ts +13 -0
  34. package/dist/formatting.js +35 -0
  35. package/dist/historicalStore.d.ts +47 -0
  36. package/dist/historicalStore.js +312 -0
  37. package/dist/index.d.ts +67 -23
  38. package/dist/index.js +138 -26
  39. package/dist/modelAliases.d.ts +13 -0
  40. package/dist/modelAliases.js +58 -0
  41. package/dist/modelCatalog.d.ts +32 -0
  42. package/dist/modelCatalog.js +145 -0
  43. package/dist/modelContext.d.ts +36 -3
  44. package/dist/modelContext.js +109 -34
  45. package/dist/modelInfo.d.ts +30 -1
  46. package/dist/modelInfo.js +116 -26
  47. package/dist/multiProviderQuotaService.d.ts +8 -0
  48. package/dist/multiProviderQuotaService.js +64 -3
  49. package/dist/node.d.ts +23 -4
  50. package/dist/node.js +50 -1
  51. package/dist/observedContextWindows.d.ts +7 -0
  52. package/dist/observedContextWindows.js +17 -2
  53. package/dist/observedSessionCollector.d.ts +72 -6
  54. package/dist/observedSessionCollector.js +440 -47
  55. package/dist/parsers/codexHistory.d.ts +4 -2
  56. package/dist/parsers/codexHistory.js +9 -2
  57. package/dist/parsers/sessionActivityDetector.d.ts +28 -0
  58. package/dist/parsers/sessionActivityDetector.js +110 -15
  59. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  60. package/dist/parsers/sessionPathResolver.js +13 -10
  61. package/dist/peakHours.d.ts +34 -3
  62. package/dist/peakHours.js +92 -7
  63. package/dist/pricingCatalog.d.ts +7 -0
  64. package/dist/pricingCatalog.js +8 -5
  65. package/dist/providerIds.d.ts +11 -0
  66. package/dist/providerIds.js +9 -0
  67. package/dist/providerQuota.d.ts +3 -1
  68. package/dist/providerQuota.js +3 -0
  69. package/dist/providers/claudeCode.d.ts +10 -1
  70. package/dist/providers/claudeCode.js +211 -131
  71. package/dist/providers/codex.d.ts +20 -1
  72. package/dist/providers/codex.js +308 -244
  73. package/dist/providers/codexDatabase.d.ts +9 -0
  74. package/dist/providers/codexDatabase.js +107 -2
  75. package/dist/providers/detect.d.ts +2 -0
  76. package/dist/providers/detect.js +29 -24
  77. package/dist/providers/diagnostics.d.ts +13 -0
  78. package/dist/providers/diagnostics.js +88 -0
  79. package/dist/providers/factory.d.ts +20 -0
  80. package/dist/providers/factory.js +57 -0
  81. package/dist/providers/openCode.d.ts +22 -1
  82. package/dist/providers/openCode.js +460 -126
  83. package/dist/providers/openCodeDatabase.d.ts +10 -0
  84. package/dist/providers/openCodeDatabase.js +69 -0
  85. package/dist/providers/rolloutWalker.d.ts +53 -0
  86. package/dist/providers/rolloutWalker.js +209 -0
  87. package/dist/providers/types.d.ts +82 -2
  88. package/dist/providers/types.js +4 -0
  89. package/dist/quota.d.ts +34 -4
  90. package/dist/quota.js +37 -9
  91. package/dist/quotaHistory.d.ts +3 -2
  92. package/dist/quotaHistory.js +36 -35
  93. package/dist/quotaPoller.d.ts +13 -1
  94. package/dist/quotaPoller.js +65 -3
  95. package/dist/quotaResolver.d.ts +81 -0
  96. package/dist/quotaResolver.js +213 -0
  97. package/dist/quotaSnapshots.d.ts +15 -2
  98. package/dist/quotaSnapshots.js +25 -3
  99. package/dist/quotaThresholds.d.ts +60 -0
  100. package/dist/quotaThresholds.js +90 -0
  101. package/dist/report/htmlReportGenerator.js +10 -4
  102. package/dist/report/sessionReportInputs.d.ts +23 -0
  103. package/dist/report/sessionReportInputs.js +34 -0
  104. package/dist/schemas/accountManager.d.ts +2 -2
  105. package/dist/schemas/accountManager.js +2 -4
  106. package/dist/schemas/accountStatus.d.ts +3 -0
  107. package/dist/schemas/accountStatus.js +1 -0
  108. package/dist/schemas/index.d.ts +3 -0
  109. package/dist/schemas/index.js +16 -2
  110. package/dist/schemas/observedSessionV1.d.ts +27 -26
  111. package/dist/schemas/observedSessionV1.js +6 -4
  112. package/dist/schemas/quota.d.ts +141 -2
  113. package/dist/schemas/quota.js +20 -8
  114. package/dist/schemas/quotaHistory.d.ts +3 -2
  115. package/dist/schemas/quotaHistory.js +1 -1
  116. package/dist/schemas/sessionEvent.d.ts +6 -0
  117. package/dist/schemas/sessionEvent.js +1 -0
  118. package/dist/schemas/stateFile.d.ts +198 -0
  119. package/dist/schemas/stateFile.js +77 -0
  120. package/dist/schemas/tokenEstimation.d.ts +1 -1
  121. package/dist/schemas/transcript.js +2 -0
  122. package/dist/sessionFingerprint.d.ts +25 -0
  123. package/dist/sessionFingerprint.js +46 -0
  124. package/dist/sessionMonitor.d.ts +15 -0
  125. package/dist/sessionMonitor.js +142 -0
  126. package/dist/sessionPreviews.d.ts +33 -2
  127. package/dist/sessionPreviews.js +333 -6
  128. package/dist/sessionStats.d.ts +51 -0
  129. package/dist/sessionStats.js +166 -0
  130. package/dist/stateFile.d.ts +113 -0
  131. package/dist/stateFile.js +160 -0
  132. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  133. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  134. package/dist/statusline/formatter.d.ts +13 -0
  135. package/dist/statusline/formatter.js +48 -6
  136. package/dist/statusline/index.d.ts +10 -2
  137. package/dist/statusline/index.js +19 -1
  138. package/dist/tokenSummary.d.ts +62 -0
  139. package/dist/tokenSummary.js +72 -0
  140. package/dist/transcript.d.ts +5 -0
  141. package/dist/transcript.js +2 -0
  142. package/dist/types/observedSessionV1.d.ts +8 -2
  143. package/dist/types/observedSessionV1.js +88 -25
  144. package/dist/types/sessionEvent.d.ts +2 -0
  145. package/dist/usage/billingBlocks.d.ts +85 -0
  146. package/dist/usage/billingBlocks.js +153 -0
  147. package/dist/usage/importSessionHistory.d.ts +53 -0
  148. package/dist/usage/importSessionHistory.js +88 -0
  149. package/dist/usage/usageEvents.d.ts +92 -0
  150. package/dist/usage/usageEvents.js +322 -0
  151. package/dist/usage/usageReports.d.ts +76 -0
  152. package/dist/usage/usageReports.js +208 -0
  153. package/dist/watchers/factory.d.ts +6 -0
  154. package/dist/watchers/factory.js +30 -25
  155. package/dist/zaiQuotaApi.d.ts +2 -0
  156. package/dist/zaiQuotaApi.js +2 -0
  157. package/package.json +22 -1
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._onRawAccountsChanged = _onRawAccountsChanged;
4
+ exports._emitAccountsChanged = _emitAccountsChanged;
5
+ const listeners = new Set();
6
+ function _onRawAccountsChanged(listener) {
7
+ listeners.add(listener);
8
+ return () => listeners.delete(listener);
9
+ }
10
+ function _emitAccountsChanged(reason) {
11
+ for (const listener of listeners) {
12
+ try {
13
+ listener(reason);
14
+ }
15
+ catch {
16
+ // Account notifications are observational and cannot break mutations.
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import { type AccountChangeReason } from './accountChangeSignal';
2
+ import { type ActiveAccountStatus } from './accountStatus';
3
+ import type { Disposable } from './quotaPoller';
4
+ export interface AccountsChangedEvent {
5
+ reason: AccountChangeReason;
6
+ status: ActiveAccountStatus;
7
+ }
8
+ export interface OnAccountsChangedOptions {
9
+ /** Emit the current status immediately after subscribing. */
10
+ emitCurrent?: boolean;
11
+ /** Catch-up poll for filesystems where watch delivery is unreliable. Default: 30s. */
12
+ pollIntervalMs?: number;
13
+ }
14
+ /**
15
+ * Observe login, logout, and active-account switches. Local Sidekick mutations
16
+ * signal immediately; filesystem watchers cover external CLIs; a low-frequency
17
+ * catch-up poll protects network mounts and watcher-hostile filesystems.
18
+ */
19
+ export declare function onAccountsChanged(listener: (event: AccountsChangedEvent) => void, options?: OnAccountsChangedOptions): Disposable;
20
+ /** Internal bridge for account mutations outside the registry module. */
21
+ export declare function _notifyAccountsChanged(): void;
@@ -0,0 +1,170 @@
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
+ const DEFAULT_POLL_INTERVAL_MS = 30_000;
52
+ let activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
53
+ /**
54
+ * Observe login, logout, and active-account switches. Local Sidekick mutations
55
+ * signal immediately; filesystem watchers cover external CLIs; a low-frequency
56
+ * catch-up poll protects network mounts and watcher-hostile filesystems.
57
+ */
58
+ function onAccountsChanged(listener, options = {}) {
59
+ listeners.add(listener);
60
+ // The fastest interval any live subscriber asked for wins; a later, faster
61
+ // subscriber restarts the timer rather than being silently ignored.
62
+ const requestedPollMs = Math.max(1_000, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
63
+ if (!stopRawListener) {
64
+ activePollIntervalMs = requestedPollMs;
65
+ ensureMonitoring();
66
+ }
67
+ else if (requestedPollMs < activePollIntervalMs) {
68
+ activePollIntervalMs = requestedPollMs;
69
+ restartPollTimer();
70
+ }
71
+ if (options.emitCurrent)
72
+ emitIfChanged('poll', true, listener);
73
+ return {
74
+ dispose: () => {
75
+ listeners.delete(listener);
76
+ if (listeners.size === 0)
77
+ stopMonitoring();
78
+ },
79
+ };
80
+ }
81
+ function ensureMonitoring() {
82
+ if (stopRawListener)
83
+ return;
84
+ lastFingerprint = fingerprint((0, accountStatus_1.getActiveAccountStatus)());
85
+ stopRawListener = (0, accountChangeSignal_1._onRawAccountsChanged)((reason) => scheduleCheck(reason));
86
+ for (const target of watchTargets()) {
87
+ try {
88
+ const watcher = fs.watch(target, { persistent: false }, () => {
89
+ scheduleCheck('filesystem');
90
+ });
91
+ watcher.on('error', () => {
92
+ watchers = watchers.filter((candidate) => candidate !== watcher);
93
+ try {
94
+ watcher.close();
95
+ }
96
+ catch {
97
+ // The catch-up poll remains active.
98
+ }
99
+ });
100
+ watchers.push(watcher);
101
+ }
102
+ catch {
103
+ // Missing/unwatchable directories are covered by the catch-up poll.
104
+ }
105
+ }
106
+ startPollTimer();
107
+ }
108
+ function startPollTimer() {
109
+ pollTimer = setInterval(() => emitIfChanged('poll'), activePollIntervalMs);
110
+ pollTimer.unref?.();
111
+ }
112
+ function restartPollTimer() {
113
+ if (pollTimer)
114
+ clearInterval(pollTimer);
115
+ startPollTimer();
116
+ }
117
+ function stopMonitoring() {
118
+ stopRawListener?.();
119
+ stopRawListener = undefined;
120
+ for (const watcher of watchers)
121
+ watcher.close();
122
+ watchers = [];
123
+ if (pollTimer)
124
+ clearInterval(pollTimer);
125
+ pollTimer = undefined;
126
+ if (debounceTimer)
127
+ clearTimeout(debounceTimer);
128
+ debounceTimer = undefined;
129
+ lastFingerprint = undefined;
130
+ activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
131
+ }
132
+ function watchTargets() {
133
+ return Array.from(new Set([(0, accountRegistry_1.getAccountsDir)(), path.join(os.homedir(), '.claude'), (0, codexProfiles_1.getSystemCodexHome)()]));
134
+ }
135
+ function scheduleCheck(reason) {
136
+ if (debounceTimer)
137
+ clearTimeout(debounceTimer);
138
+ debounceTimer = setTimeout(() => {
139
+ debounceTimer = undefined;
140
+ // Never force: watch targets include ~/.claude and the Codex home, whose
141
+ // history files churn on every prompt. Only a real status change may wake
142
+ // subscribers, or dormant quota pollers would poll on unrelated writes.
143
+ emitIfChanged(reason);
144
+ }, 25);
145
+ debounceTimer.unref?.();
146
+ }
147
+ function emitIfChanged(reason, force = false, singleListener) {
148
+ const status = (0, accountStatus_1.getActiveAccountStatus)();
149
+ const nextFingerprint = fingerprint(status);
150
+ if (!force && nextFingerprint === lastFingerprint)
151
+ return;
152
+ lastFingerprint = nextFingerprint;
153
+ const event = { reason, status };
154
+ const targets = singleListener ? [singleListener] : [...listeners];
155
+ for (const listener of targets) {
156
+ try {
157
+ listener(event);
158
+ }
159
+ catch {
160
+ // A host callback cannot stop monitoring.
161
+ }
162
+ }
163
+ }
164
+ function fingerprint(status) {
165
+ return JSON.stringify(status);
166
+ }
167
+ /** Internal bridge for account mutations outside the registry module. */
168
+ function _notifyAccountsChanged() {
169
+ (0, accountChangeSignal_1._emitAccountsChanged)('local');
170
+ }
@@ -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;
@@ -39,11 +41,32 @@ export interface ResolvedActiveAccount {
39
41
  source: 'live' | 'registry' | 'none';
40
42
  }
41
43
  export declare function getAccountsDir(): string;
44
+ interface RegistryWriteOptions {
45
+ /**
46
+ * Skip the local change notification. Only for writes that change no
47
+ * observable account state, such as re-pointing the active pointer at the
48
+ * profile that already matches the live login (self-heal): a read path
49
+ * must not wake every subscriber.
50
+ */
51
+ silent?: boolean;
52
+ }
53
+ /**
54
+ * Run `operation` while holding the registry lock. For callers that need a
55
+ * read-modify-write across several steps (the legacy Claude facade's
56
+ * add/remove); inside it use the `*Unlocked` writers, never the locking ones:
57
+ * the lock is not re-entrant.
58
+ */
59
+ export declare function withSavedAccountRegistryLock<T>(operation: () => T): T;
42
60
  export declare function readSavedAccountRegistry(): SavedAccountRegistry | null;
43
61
  export declare function writeSavedAccountRegistry(registry: SavedAccountRegistry): void;
44
62
  export declare function listSavedAccountProfiles(providerId?: AccountProviderId): SavedAccountProfile[];
45
63
  export declare function getActiveSavedAccount(providerId: AccountProviderId): SavedAccountProfile | null;
46
64
  export declare function upsertSavedAccountProfile(profile: SavedAccountProfile): SavedAccountRegistry;
47
- export declare function setActiveSavedAccount(providerId: AccountProviderId, accountId: string | null): SavedAccountRegistry;
65
+ export declare function setActiveSavedAccount(providerId: AccountProviderId, accountId: string | null, options?: RegistryWriteOptions): SavedAccountRegistry;
48
66
  export declare function replaceSavedAccountProfiles(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
67
+ /**
68
+ * Same as {@link replaceSavedAccountProfiles} without taking the registry
69
+ * lock: for callers already inside {@link withSavedAccountRegistryLock}.
70
+ */
71
+ export declare function replaceSavedAccountProfilesUnlocked(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
49
72
  export declare function removeSavedAccountProfile(providerId: AccountProviderId, accountId: string): SavedAccountProfile | null;
@@ -33,7 +33,9 @@ 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;
38
+ exports.withSavedAccountRegistryLock = withSavedAccountRegistryLock;
37
39
  exports.readSavedAccountRegistry = readSavedAccountRegistry;
38
40
  exports.writeSavedAccountRegistry = writeSavedAccountRegistry;
39
41
  exports.listSavedAccountProfiles = listSavedAccountProfiles;
@@ -41,11 +43,15 @@ exports.getActiveSavedAccount = getActiveSavedAccount;
41
43
  exports.upsertSavedAccountProfile = upsertSavedAccountProfile;
42
44
  exports.setActiveSavedAccount = setActiveSavedAccount;
43
45
  exports.replaceSavedAccountProfiles = replaceSavedAccountProfiles;
46
+ exports.replaceSavedAccountProfilesUnlocked = replaceSavedAccountProfilesUnlocked;
44
47
  exports.removeSavedAccountProfile = removeSavedAccountProfile;
45
48
  const fs = __importStar(require("fs"));
46
49
  const path = __importStar(require("path"));
47
50
  const paths_1 = require("./paths");
48
51
  const atomic_1 = require("./writers/atomic");
52
+ const accountChangeSignal_1 = require("./accountChangeSignal");
53
+ var providerIds_1 = require("./providerIds");
54
+ Object.defineProperty(exports, "ACCOUNT_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.ACCOUNT_PROVIDER_IDS; } });
49
55
  function getAccountsDir() {
50
56
  return path.join((0, paths_1.getConfigDir)(), 'accounts');
51
57
  }
@@ -58,8 +64,20 @@ function getRegistryLockPath() {
58
64
  function ensureAccountsDir() {
59
65
  fs.mkdirSync(getAccountsDir(), { recursive: true, mode: 0o700 });
60
66
  }
61
- function writeRegistryUnlocked(registry) {
67
+ function writeRegistryUnlocked(registry, options = {}) {
62
68
  (0, atomic_1.atomicWriteJsonSync)(getRegistryPath(), normalizeRegistry(registry));
69
+ if (!options.silent)
70
+ (0, accountChangeSignal_1._emitAccountsChanged)('local');
71
+ }
72
+ /**
73
+ * Run `operation` while holding the registry lock. For callers that need a
74
+ * read-modify-write across several steps (the legacy Claude facade's
75
+ * add/remove); inside it use the `*Unlocked` writers, never the locking ones:
76
+ * the lock is not re-entrant.
77
+ */
78
+ function withSavedAccountRegistryLock(operation) {
79
+ ensureAccountsDir();
80
+ return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), operation);
63
81
  }
64
82
  /**
65
83
  * Serializes registry read-modify-write cycles across processes. Without the
@@ -68,13 +86,13 @@ function writeRegistryUnlocked(registry) {
68
86
  * mutation. Lock ordering: callers that also hold a provider auth-swap lock
69
87
  * take that lock first and this one inside it, never the reverse.
70
88
  */
71
- function mutateSavedAccountRegistry(mutator) {
72
- ensureAccountsDir();
73
- return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), () => {
74
- const next = mutator(readSavedAccountRegistry() ?? createEmptyRegistry());
75
- writeRegistryUnlocked(next);
76
- return next;
77
- });
89
+ function mutateSavedAccountRegistry(mutator, options = {}) {
90
+ return withSavedAccountRegistryLock(() => mutateSavedAccountRegistryUnlocked(mutator, options));
91
+ }
92
+ function mutateSavedAccountRegistryUnlocked(mutator, options = {}) {
93
+ const next = mutator(readSavedAccountRegistry() ?? createEmptyRegistry());
94
+ writeRegistryUnlocked(next, options);
95
+ return next;
78
96
  }
79
97
  function createEmptyRegistry() {
80
98
  return {
@@ -182,20 +200,30 @@ function upsertSavedAccountProfile(profile) {
182
200
  return registry;
183
201
  });
184
202
  }
185
- function setActiveSavedAccount(providerId, accountId) {
203
+ function setActiveSavedAccount(providerId, accountId, options = {}) {
186
204
  return mutateSavedAccountRegistry((registry) => {
187
205
  registry.activeByProvider[providerId] = accountId;
188
206
  return registry;
189
- });
207
+ }, options);
190
208
  }
191
- function replaceSavedAccountProfiles(providerId, accounts, activeId) {
192
- return mutateSavedAccountRegistry((registry) => {
209
+ function replaceProfiles(providerId, accounts, activeId) {
210
+ return (registry) => {
193
211
  registry.accounts = registry.accounts
194
212
  .filter((account) => account.providerId !== providerId)
195
213
  .concat(accounts);
196
214
  registry.activeByProvider[providerId] = activeId;
197
215
  return registry;
198
- });
216
+ };
217
+ }
218
+ function replaceSavedAccountProfiles(providerId, accounts, activeId) {
219
+ return mutateSavedAccountRegistry(replaceProfiles(providerId, accounts, activeId));
220
+ }
221
+ /**
222
+ * Same as {@link replaceSavedAccountProfiles} without taking the registry
223
+ * lock: for callers already inside {@link withSavedAccountRegistryLock}.
224
+ */
225
+ function replaceSavedAccountProfilesUnlocked(providerId, accounts, activeId) {
226
+ return mutateSavedAccountRegistryUnlocked(replaceProfiles(providerId, accounts, activeId));
199
227
  }
200
228
  function removeSavedAccountProfile(providerId, accountId) {
201
229
  ensureAccountsDir();
@@ -10,10 +10,18 @@ export interface ActiveAccountStatus {
10
10
  codex: ActiveProviderAccountStatus;
11
11
  error?: string;
12
12
  }
13
+ export interface ActiveAccountStatusOptions {
14
+ /**
15
+ * Re-point a stale saved-account pointer at the live identity when they
16
+ * disagree (default true). Hot paths such as the status line pass `false`
17
+ * so they never write; an ordinary command repairs the pointer later.
18
+ */
19
+ selfHeal?: boolean;
20
+ }
13
21
  /**
14
22
  * Reads active Claude Code and Codex account status in one filesystem pass.
15
23
  *
16
24
  * If reading either provider throws, the result still has provider-shaped
17
25
  * fields so startup flows can render a consistent "not configured" state.
18
26
  */
19
- export declare function getActiveAccountStatus(error?: string): ActiveAccountStatus;
27
+ export declare function getActiveAccountStatus(error?: string, options?: ActiveAccountStatusOptions): ActiveAccountStatus;
@@ -12,10 +12,10 @@ function errorMessage(error) {
12
12
  * If reading either provider throws, the result still has provider-shaped
13
13
  * fields so startup flows can render a consistent "not configured" state.
14
14
  */
15
- function getActiveAccountStatus(error) {
15
+ function getActiveAccountStatus(error, options = {}) {
16
16
  try {
17
- const claudeAccount = (0, accounts_1.resolveActiveClaudeAccount)();
18
- const codexAccount = (0, codexProfiles_1.resolveActiveCodexAccount)();
17
+ const claudeAccount = (0, accounts_1.resolveActiveClaudeAccount)(options);
18
+ const codexAccount = (0, codexProfiles_1.resolveActiveCodexAccount)(options);
19
19
  const claude = claudeAccount.source !== 'none'
20
20
  ? {
21
21
  present: true,
@@ -50,5 +50,7 @@ export declare function getActiveAccount(): AccountEntry | null;
50
50
  * re-pointed so registry-keyed data tracks reality too. Never creates or deletes
51
51
  * profiles; an unknown live account is shown as-is with no label and no write.
52
52
  */
53
- export declare function resolveActiveClaudeAccount(): ResolvedActiveAccount;
53
+ export declare function resolveActiveClaudeAccount(options?: {
54
+ selfHeal?: boolean;
55
+ }): ResolvedActiveAccount;
54
56
  export declare function isMultiAccountEnabled(): boolean;
package/dist/accounts.js CHANGED
@@ -201,7 +201,14 @@ function readAccountRegistry() {
201
201
  };
202
202
  }
203
203
  function writeAccountRegistry(registry) {
204
- const mappedClaudeProfiles = registry.accounts.map((account) => ({
204
+ (0, accountRegistry_1.replaceSavedAccountProfiles)('claude-code', mapClaudeProfiles(registry), registry.activeAccountUuid);
205
+ }
206
+ /** For callers already holding the registry lock (see withSavedAccountRegistryLock). */
207
+ function writeAccountRegistryUnlocked(registry) {
208
+ (0, accountRegistry_1.replaceSavedAccountProfilesUnlocked)('claude-code', mapClaudeProfiles(registry), registry.activeAccountUuid);
209
+ }
210
+ function mapClaudeProfiles(registry) {
211
+ return registry.accounts.map((account) => ({
205
212
  id: account.uuid,
206
213
  providerId: 'claude-code',
207
214
  providerAccountId: account.uuid,
@@ -212,7 +219,6 @@ function writeAccountRegistry(registry) {
212
219
  email: account.email,
213
220
  },
214
221
  }));
215
- (0, accountRegistry_1.replaceSavedAccountProfiles)('claude-code', mappedClaudeProfiles, registry.activeAccountUuid);
216
222
  }
217
223
  // ── Read active Claude account ───────────────────────────────────────────
218
224
  function readActiveClaudeAccount() {
@@ -254,17 +260,34 @@ function addCurrentAccount(label) {
254
260
  return { success: false, error: 'Could not read Claude config file.' };
255
261
  }
256
262
  ensureDirs();
257
- // 4. Load or create registry
258
- const registry = readAccountRegistry() ?? {
259
- version: 1,
260
- activeAccountUuid: null,
261
- accounts: [],
262
- };
263
- // 5. Check if account already exists — update credentials + label
264
- const existing = registry.accounts.find((a) => a.uuid === active.uuid);
265
- if (existing) {
266
- if (label !== undefined)
267
- existing.label = label;
263
+ // 4–7 are one read-modify-write of the registry: hold the registry lock
264
+ // across them so a concurrent add from another process is not dropped
265
+ // when this write replaces the whole Claude profile set.
266
+ return (0, accountRegistry_1.withSavedAccountRegistryLock)(() => {
267
+ // 4. Load or create registry
268
+ const registry = readAccountRegistry() ?? {
269
+ version: 1,
270
+ activeAccountUuid: null,
271
+ accounts: [],
272
+ };
273
+ // 5. Check if account already exists — update credentials + label
274
+ const existing = registry.accounts.find((a) => a.uuid === active.uuid);
275
+ if (existing) {
276
+ if (label !== undefined)
277
+ existing.label = label;
278
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
279
+ (0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
280
+ try {
281
+ writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
282
+ }
283
+ catch {
284
+ /* flat backups remain available */
285
+ }
286
+ registry.activeAccountUuid = active.uuid;
287
+ writeAccountRegistryUnlocked(registry);
288
+ return { success: true };
289
+ }
290
+ // 6. New account — back up credentials
268
291
  (0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
269
292
  (0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
270
293
  try {
@@ -273,29 +296,17 @@ function addCurrentAccount(label) {
273
296
  catch {
274
297
  /* flat backups remain available */
275
298
  }
299
+ // 7. Add entry
300
+ registry.accounts.push({
301
+ uuid: active.uuid,
302
+ email: active.email,
303
+ label,
304
+ addedAt: new Date().toISOString(),
305
+ });
276
306
  registry.activeAccountUuid = active.uuid;
277
- writeAccountRegistry(registry);
307
+ writeAccountRegistryUnlocked(registry);
278
308
  return { success: true };
279
- }
280
- // 6. New account — back up credentials
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);
283
- try {
284
- writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
285
- }
286
- catch {
287
- /* flat backups remain available */
288
- }
289
- // 7. Add entry
290
- registry.accounts.push({
291
- uuid: active.uuid,
292
- email: active.email,
293
- label,
294
- addedAt: new Date().toISOString(),
295
309
  });
296
- registry.activeAccountUuid = active.uuid;
297
- writeAccountRegistry(registry);
298
- return { success: true };
299
310
  }
300
311
  // ── Switch to account ────────────────────────────────────────────────────
301
312
  /**
@@ -486,34 +497,37 @@ function reconcileClaudeAuthState() {
486
497
  }
487
498
  // ── Remove account ───────────────────────────────────────────────────────
488
499
  function removeAccount(uuid) {
489
- const registry = readAccountRegistry();
490
- if (!registry) {
491
- return { success: false, error: 'No account registry found.' };
492
- }
493
- const idx = registry.accounts.findIndex((a) => a.uuid === uuid);
494
- if (idx === -1) {
495
- return { success: false, error: `Account ${uuid} not found.` };
496
- }
497
- // Remove backed-up files
498
- try {
499
- fs.unlinkSync(path.join(getCredentialsDir(), `${uuid}.credentials.json`));
500
- }
501
- catch {
502
- /* ok */
503
- }
504
- try {
505
- fs.unlinkSync(path.join(getConfigsDir(), `${uuid}.config.json`));
506
- }
507
- catch {
508
- /* ok */
509
- }
510
- // Remove from registry
511
- registry.accounts.splice(idx, 1);
512
- if (registry.activeAccountUuid === uuid) {
513
- registry.activeAccountUuid = registry.accounts[0]?.uuid ?? null;
514
- }
515
- writeAccountRegistry(registry);
516
- return { success: true };
500
+ // One read-modify-write under the registry lock (see addCurrentAccount).
501
+ return (0, accountRegistry_1.withSavedAccountRegistryLock)(() => {
502
+ const registry = readAccountRegistry();
503
+ if (!registry) {
504
+ return { success: false, error: 'No account registry found.' };
505
+ }
506
+ const idx = registry.accounts.findIndex((a) => a.uuid === uuid);
507
+ if (idx === -1) {
508
+ return { success: false, error: `Account ${uuid} not found.` };
509
+ }
510
+ // Remove backed-up files
511
+ try {
512
+ fs.unlinkSync(path.join(getCredentialsDir(), `${uuid}.credentials.json`));
513
+ }
514
+ catch {
515
+ /* ok */
516
+ }
517
+ try {
518
+ fs.unlinkSync(path.join(getConfigsDir(), `${uuid}.config.json`));
519
+ }
520
+ catch {
521
+ /* ok */
522
+ }
523
+ // Remove from registry
524
+ registry.accounts.splice(idx, 1);
525
+ if (registry.activeAccountUuid === uuid) {
526
+ registry.activeAccountUuid = registry.accounts[0]?.uuid ?? null;
527
+ }
528
+ writeAccountRegistryUnlocked(registry);
529
+ return { success: true };
530
+ });
517
531
  }
518
532
  // ── Query helpers ────────────────────────────────────────────────────────
519
533
  function listAccounts() {
@@ -546,7 +560,8 @@ function getActiveAccount() {
546
560
  * re-pointed so registry-keyed data tracks reality too. Never creates or deletes
547
561
  * profiles; an unknown live account is shown as-is with no label and no write.
548
562
  */
549
- function resolveActiveClaudeAccount() {
563
+ function resolveActiveClaudeAccount(options = {}) {
564
+ const selfHeal = options.selfHeal ?? true;
550
565
  const live = readActiveClaudeAccount();
551
566
  if (live) {
552
567
  const profiles = (0, accountRegistry_1.listSavedAccountProfiles)('claude-code');
@@ -554,12 +569,17 @@ function resolveActiveClaudeAccount() {
554
569
  profiles.find((p) => (p.email ?? p.metadata?.email) === live.email);
555
570
  if (match) {
556
571
  const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
557
- if (!active || active.id !== match.id) {
572
+ // Hot paths (the status line runs on every prompt) pass selfHeal: false
573
+ // so a persistently failing registry write never becomes a write per
574
+ // prompt; the next ordinary command repairs the pointer instead.
575
+ if (selfHeal && (!active || active.id !== match.id)) {
558
576
  // Self-heal is best-effort: a registry write failure (read-only/full
559
577
  // disk) must never break display or extension activation. We still
560
578
  // return the correct live identity below.
579
+ // Silent: this is a read path repairing a pointer to the profile that
580
+ // already matches the live login, so subscribers have nothing new to see.
561
581
  try {
562
- (0, accountRegistry_1.setActiveSavedAccount)('claude-code', match.id);
582
+ (0, accountRegistry_1.setActiveSavedAccount)('claude-code', match.id, { silent: true });
563
583
  }
564
584
  catch {
565
585
  /* keep going with the live identity */
@@ -14,7 +14,7 @@ import type { SerializedFrequencyState } from './FrequencyTracker';
14
14
  import type { SerializedHeatmapState } from './HeatmapTracker';
15
15
  import type { SerializedPatternState } from './PatternExtractor';
16
16
  /** Schema version for serialized snapshots. */
17
- export declare const SNAPSHOT_SCHEMA_VERSION = 4;
17
+ export declare const SNAPSHOT_SCHEMA_VERSION = 5;
18
18
  /**
19
19
  * JSON-serializable snapshot of EventAggregator state.
20
20
  * Used by session snapshot sidecar files for fast resume.
@@ -28,7 +28,14 @@ export interface SerializedAggregatorState {
28
28
  cacheRead: number;
29
29
  reasoning: number;
30
30
  total: number;
31
+ /** @deprecated Mirrors `costUsd`; kept so older readers of the field keep working. */
31
32
  reportedCost: number;
33
+ costUsd: number;
34
+ reportedCostUsd: number;
35
+ estimatedCostUsd: number;
36
+ reportedCalls: number;
37
+ estimatedCalls: number;
38
+ unpricedCalls: number;
32
39
  };
33
40
  modelUsage: Array<[
34
41
  string,
@@ -100,7 +107,13 @@ export declare class EventAggregator {
100
107
  private cacheReadTokens;
101
108
  private reasoningTokens;
102
109
  private totalTokens;
103
- private reportedCost;
110
+ /** Running cost in USD (reported + estimated). */
111
+ private costUsd;
112
+ private reportedCostUsd;
113
+ private estimatedCostUsd;
114
+ private reportedCalls;
115
+ private estimatedCalls;
116
+ private unpricedCalls;
104
117
  private modelUsage;
105
118
  private currentContextSize;
106
119
  private previousContextSize;