sidekick-shared 0.21.2 → 0.21.4

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 (77) hide show
  1. package/README.md +60 -45
  2. package/dist/accountManager.js +9 -7
  3. package/dist/accountRegistry.d.ts +17 -0
  4. package/dist/accountRegistry.js +18 -14
  5. package/dist/accountStatus.js +7 -7
  6. package/dist/accounts.d.ts +13 -0
  7. package/dist/accounts.js +110 -25
  8. package/dist/aggregation/EventAggregator.d.ts +14 -11
  9. package/dist/aggregation/EventAggregator.js +68 -31
  10. package/dist/aggregation/PatternExtractor.js +5 -4
  11. package/dist/autoSwitch.js +6 -12
  12. package/dist/claudeProfiles.js +3 -3
  13. package/dist/codexProfiles.d.ts +15 -1
  14. package/dist/codexProfiles.js +96 -22
  15. package/dist/codexQuota.js +44 -16
  16. package/dist/codexQuotaWatcher.js +19 -4
  17. package/dist/context/composer.js +8 -6
  18. package/dist/context/sessionContext.js +12 -9
  19. package/dist/credentialIO.js +10 -8
  20. package/dist/extractors/sessionAssets.js +9 -1
  21. package/dist/extractors/sources/claudeAssets.js +7 -1
  22. package/dist/extractors/sources/codexAssets.js +14 -3
  23. package/dist/extractors/toolCall.js +1 -4
  24. package/dist/formatters/eventHighlighter.js +148 -28
  25. package/dist/formatters/noiseClassifier.js +3 -11
  26. package/dist/formatters/sessionDump.js +22 -13
  27. package/dist/formatters/toolSummary.js +1 -3
  28. package/dist/index.d.ts +26 -26
  29. package/dist/index.js +10 -4
  30. package/dist/modelContext.js +10 -8
  31. package/dist/modelInfo.js +11 -6
  32. package/dist/multiProviderQuotaService.d.ts +2 -2
  33. package/dist/multiProviderQuotaService.js +14 -10
  34. package/dist/parsers/codexParser.js +170 -103
  35. package/dist/parsers/debugLogParser.js +5 -4
  36. package/dist/parsers/openCodeParser.js +66 -48
  37. package/dist/parsers/planExtractor.js +3 -3
  38. package/dist/parsers/sessionActivityDetector.js +10 -6
  39. package/dist/parsers/sessionPathResolver.js +29 -34
  40. package/dist/parsers/subagentScanner.js +21 -20
  41. package/dist/parsers/subagentTraceParser.js +7 -4
  42. package/dist/phrases.js +11 -11
  43. package/dist/providerStatus.js +3 -4
  44. package/dist/providers/claudeCode.js +3 -1
  45. package/dist/providers/codex.js +35 -14
  46. package/dist/providers/codexDatabase.js +14 -5
  47. package/dist/providers/detect.js +10 -6
  48. package/dist/providers/openCode.js +91 -63
  49. package/dist/providers/openCodeDatabase.js +44 -17
  50. package/dist/quota.d.ts +19 -0
  51. package/dist/quota.js +50 -8
  52. package/dist/quotaHistory.js +10 -3
  53. package/dist/quotaSnapshots.js +2 -2
  54. package/dist/readers/decisions.js +1 -1
  55. package/dist/readers/notes.js +5 -3
  56. package/dist/readers/plans.js +43 -23
  57. package/dist/readers/tasks.js +1 -1
  58. package/dist/report/htmlHelpers.js +135 -16
  59. package/dist/report/htmlReportGenerator.js +38 -12
  60. package/dist/report/transcriptParser.js +8 -2
  61. package/dist/schemas/index.d.ts +4 -4
  62. package/dist/schemas/quota.js +2 -10
  63. package/dist/schemas/sessionEvent.js +20 -10
  64. package/dist/search/advancedFilter.js +3 -3
  65. package/dist/search/sessionSearch.js +10 -4
  66. package/dist/terminalSync.js +4 -2
  67. package/dist/turns/assistantTurn.js +5 -1
  68. package/dist/watchers/eventBridge.js +56 -26
  69. package/dist/watchers/factory.js +5 -2
  70. package/dist/watchers/jsonlTail.js +3 -1
  71. package/dist/watchers/jsonlWatcher.js +128 -52
  72. package/dist/watchers/providerReaderWatcher.js +3 -1
  73. package/dist/watchers/sqliteWatcher.js +52 -22
  74. package/dist/zaiQuota.js +6 -10
  75. package/dist/zaiQuotaApi.js +13 -5
  76. package/dist/zaiQuotaWatcher.js +1 -1
  77. package/package.json +5 -2
package/README.md CHANGED
@@ -25,49 +25,49 @@ npm install sidekick-shared
25
25
 
26
26
  ## API Overview
27
27
 
28
- | Module | Description |
29
- |--------|-------------|
30
- | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
31
- | **Paths** | Config directory resolution, project data paths, workspace encoding |
32
- | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
33
- | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
34
- | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
35
- | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
36
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
37
- | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
38
- | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
39
- | **Session Context** | Provider-neutral context evidence snapshots (`buildSessionContextSnapshot()`, `calculateSessionContextPressure()`, `createSessionContextProjector()`, `readSessionContextSnapshot()`): layered evidence sources, low/medium/high context pressure, and observed capabilities (tools, MCP servers, permission mode, rate limits) |
40
- | **Assistant Turns** | Browser-safe timeline/process/answer projection for provider-normalized assistant turns (`segmentAssistantTurn()`, `assistantTurnEventsFromSessionEvents()`), including interleaved reasoning, compact tool groups, and Claude `Task` subagent refs without prompt leakage |
41
- | **Report** | Self-contained HTML session report generation |
42
- | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
43
- | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams, and authoritative z.ai Coding Plan quota fetching via `resolveZaiQuota()` / `fetchZaiQuotaFromApi()` (reads z.ai's `api/monitor/usage/quota/limit` endpoint with cached-snapshot fallback). The deprecated observed-traffic z.ai estimator helpers remain exported for backward compatibility only |
44
- | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
45
- | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, quota, account status, and quota history — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
46
- | **Extractors** | Pure functions for single-event processing: `extractTokenUsage()`, `extractToolCall()` (top-level `tool_use`), `extractToolCalls()` (assistant content blocks), plus Node-only session asset extraction (`gatherAssetsForCwd()`, `readClaudeAssets()`, `readCodexAssets()`) for URLs, file paths, commands, and plans |
47
- | **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5 / Opus 4.8 / Opus 4.7 / Sonnet 4.7 1M and GPT-5.x variants), pricing tables with optional LiteLLM hydration, null-aware cost (`calculateCost()`), provenance-preserving cost (`calculateCostWithProvenance()`, `mergeCostSources()`), and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
48
- | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
49
- | **Multi-Provider Quota** | `MultiProviderQuotaService` orchestrates Claude polling + peak-hours + account labels + Codex/z.ai quota updates behind one typed event stream. `CodexQuotaWatcher` watches the active Codex rollout for live rate limits with snapshot fallback; z.ai quota uses the API resolver with snapshot fallback |
50
- | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for first-run bootstrap of the active system Claude/Codex credentials as a "Default" saved account, and `getActiveAccountStatus()` for a single-pass active-account read across providers |
51
- | **Account Management 2.0** | Provider-neutral acquisition + switching: `beginAccountLogin()` / `getAccountLoginStatus()` / `finalizeAccountLogin()` / `spawnAccountLogin()` drive a TTY-less, profile-isolated login that doesn't disturb the active account until finalization; `listAllAccounts()` and `switchAccount()` expose a shared switcher across Claude and Codex. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
52
- | **Terminal sync & auto-switch** | Opt-in terminal profile pointers, shell-hook/launcher helpers, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) that moves to a healthier saved account when quota crosses a configured threshold |
53
- | **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — switching atomically swaps the profile's backed-up credentials into the system `~/.codex/auth.json`, with rotated-token staleness protection, one-time dual-home migration, and legacy multi-home session monitoring |
54
- | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
55
- | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
28
+ | Module | Description |
29
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
31
+ | **Paths** | Config directory resolution, project data paths, workspace encoding |
32
+ | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
33
+ | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
34
+ | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
35
+ | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
36
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
37
+ | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
38
+ | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
39
+ | **Session Context** | Provider-neutral context evidence snapshots (`buildSessionContextSnapshot()`, `calculateSessionContextPressure()`, `createSessionContextProjector()`, `readSessionContextSnapshot()`): layered evidence sources, low/medium/high context pressure, and observed capabilities (tools, MCP servers, permission mode, rate limits) |
40
+ | **Assistant Turns** | Browser-safe timeline/process/answer projection for provider-normalized assistant turns (`segmentAssistantTurn()`, `assistantTurnEventsFromSessionEvents()`), including interleaved reasoning, compact tool groups, and Claude `Task` subagent refs without prompt leakage |
41
+ | **Report** | Self-contained HTML session report generation |
42
+ | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
43
+ | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams, and authoritative z.ai Coding Plan quota fetching via `resolveZaiQuota()` / `fetchZaiQuotaFromApi()` (reads z.ai's `api/monitor/usage/quota/limit` endpoint with cached-snapshot fallback). The deprecated observed-traffic z.ai estimator helpers remain exported for backward compatibility only |
44
+ | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
45
+ | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, quota, account status, and quota history — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
46
+ | **Extractors** | Pure functions for single-event processing: `extractTokenUsage()`, `extractToolCall()` (top-level `tool_use`), `extractToolCalls()` (assistant content blocks), plus Node-only session asset extraction (`gatherAssetsForCwd()`, `readClaudeAssets()`, `readCodexAssets()`) for URLs, file paths, commands, and plans |
47
+ | **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5 / Opus 4.8 / Opus 4.7 / Sonnet 4.7 1M and GPT-5.x variants), pricing tables with optional LiteLLM hydration, null-aware cost (`calculateCost()`), provenance-preserving cost (`calculateCostWithProvenance()`, `mergeCostSources()`), and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
48
+ | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
49
+ | **Multi-Provider Quota** | `MultiProviderQuotaService` orchestrates Claude polling + peak-hours + account labels + Codex/z.ai quota updates behind one typed event stream. `CodexQuotaWatcher` watches the active Codex rollout for live rate limits with snapshot fallback; z.ai quota uses the API resolver with snapshot fallback |
50
+ | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for first-run bootstrap of the active system Claude/Codex credentials as a "Default" saved account, `getActiveAccountStatus()` for a single-pass active-account read across providers, and live-first `resolveActiveClaudeAccount()` / `resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
51
+ | **Account Management 2.0** | Provider-neutral acquisition + switching: `beginAccountLogin()` / `getAccountLoginStatus()` / `finalizeAccountLogin()` / `spawnAccountLogin()` drive a TTY-less, profile-isolated login that doesn't disturb the active account until finalization; `listAllAccounts()` and `switchAccount()` expose a shared switcher across Claude and Codex. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
52
+ | **Terminal sync & auto-switch** | Opt-in terminal profile pointers, shell-hook/launcher helpers, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) that moves to a healthier saved account when quota crosses a configured threshold |
53
+ | **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — switching atomically swaps the profile's backed-up credentials into the system `~/.codex/auth.json`, with rotated-token staleness protection, one-time dual-home migration, and legacy multi-home session monitoring |
54
+ | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
55
+ | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
56
56
 
57
57
  ## Supported import paths
58
58
 
59
59
  `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime.
60
60
 
61
- | Path | Runtime | What it exposes |
62
- |-----------------------------------|-----------------------------|--------------------------------------------------------------------|
63
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
64
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: context-window lookup, model parsing, cost math, assistant turn projection. |
65
- | `sidekick-shared/node` | Node only | LiteLLM pricing catalog hydration (`fs` + `path`). |
66
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
67
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
68
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
69
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
70
- | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, quota, account status, quota history) — fs-free, no Node builtins. |
61
+ | Path | Runtime | What it exposes |
62
+ | ------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
63
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
64
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: context-window lookup, model parsing, cost math, assistant turn projection. |
65
+ | `sidekick-shared/node` | Node only | LiteLLM pricing catalog hydration (`fs` + `path`). |
66
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
67
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
68
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
69
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
70
+ | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, quota, account status, quota history) — fs-free, no Node builtins. |
71
71
 
72
72
  ### Browser / webview runtimes
73
73
 
@@ -191,9 +191,9 @@ console.log(formatCost(cost)); // "$0.0045"
191
191
  ```typescript
192
192
  import { extractTokenUsage, extractToolCall, extractToolCalls } from 'sidekick-shared';
193
193
 
194
- const usage = extractTokenUsage(event); // TokenUsage | null
195
- const tools = extractToolCalls(event); // ToolCall[] — assistant content blocks
196
- const toolFromEvent = extractToolCall(event); // ToolCall | null — top-level `tool_use` events
194
+ const usage = extractTokenUsage(event); // TokenUsage | null
195
+ const tools = extractToolCalls(event); // ToolCall[] — assistant content blocks
196
+ const toolFromEvent = extractToolCall(event); // ToolCall | null — top-level `tool_use` events
197
197
  ```
198
198
 
199
199
  ### Extract actionable assets from recent sessions
@@ -234,7 +234,7 @@ const provider = await detectProvider('/path/to/project');
234
234
  if (provider) {
235
235
  const snapshot = readSessionContextSnapshot(provider, '/path/to/session.jsonl');
236
236
 
237
- console.log(snapshot.pressure); // 'low' | 'medium' | 'high'
237
+ console.log(snapshot.pressure); // 'low' | 'medium' | 'high'
238
238
  console.log(snapshot.contextTokens, '/', snapshot.contextWindow);
239
239
  console.log(snapshot.capabilities.tools, snapshot.capabilities.mcpServers);
240
240
  console.log(snapshot.sources.length, 'evidence sources');
@@ -295,9 +295,9 @@ import { createJsonlTail, sessionEventSchema } from 'sidekick-shared';
295
295
  const tail = createJsonlTail({
296
296
  path: '/path/to/session.jsonl',
297
297
  schema: sessionEventSchema,
298
- onEvent: event => aggregator.processEvent(event),
298
+ onEvent: (event) => aggregator.processEvent(event),
299
299
  onBatchComplete: () => renderMetrics(aggregator.getMetrics()),
300
- onError: error => console.warn(error.message),
300
+ onError: (error) => console.warn(error.message),
301
301
  });
302
302
 
303
303
  tail.start();
@@ -329,7 +329,7 @@ const service = new MultiProviderQuotaService({
329
329
 
330
330
  service.onUpdate(({ claude, codex }) => {
331
331
  if (claude) console.log('Claude:', claude.fiveHour.utilization, claude.peakHours?.label);
332
- if (codex) console.log('Codex:', codex.fiveHour.utilization, codex.accountLabel);
332
+ if (codex) console.log('Codex:', codex.fiveHour.utilization, codex.accountLabel);
333
333
  });
334
334
 
335
335
  service.startPolling();
@@ -359,6 +359,21 @@ console.log(status.claude.present, status.claude.email);
359
359
  console.log(status.codex.present, status.codex.label);
360
360
  ```
361
361
 
362
+ For display surfaces that must reflect the **currently logged-in** account — even after a native `claude /login` or `codex login` outside Sidekick — use the live-first resolvers. They prefer the live provider auth (`~/.claude/.claude.json` oauthAccount; the `~/.codex/auth.json` id_token JWT) over the saved `activeByProvider` pointer, fall back to the registry, and self-heal the saved pointer on an unambiguous match (best-effort, never creating or deleting profiles):
363
+
364
+ ```typescript
365
+ import { resolveActiveClaudeAccount, resolveActiveCodexAccount } from 'sidekick-shared';
366
+ import type { ResolvedActiveAccount } from 'sidekick-shared';
367
+
368
+ const claude: ResolvedActiveAccount = resolveActiveClaudeAccount();
369
+ // source: 'live' → identity came from the live provider auth (label filled when it matches a saved profile)
370
+ // source: 'registry' → no usable live identity; fell back to the saved active pointer
371
+ // source: 'none' → neither a live identity nor a saved active account
372
+ console.log(claude.email, claude.label, claude.source);
373
+
374
+ const codex = resolveActiveCodexAccount();
375
+ ```
376
+
362
377
  ### Track cost provenance for honest UI rollups
363
378
 
364
379
  ```typescript
@@ -68,7 +68,9 @@ function atomicWriteJson(filePath, data, mode = 0o600) {
68
68
  try {
69
69
  fs.unlinkSync(tmp);
70
70
  }
71
- catch { /* nothing to clean up */ }
71
+ catch {
72
+ /* nothing to clean up */
73
+ }
72
74
  throw err;
73
75
  }
74
76
  }
@@ -88,7 +90,9 @@ function removePendingClaudeProfile(loginId) {
88
90
  try {
89
91
  fs.rmSync(getPendingClaudeProfilePath(loginId), { force: true });
90
92
  }
91
- catch { /* best effort */ }
93
+ catch {
94
+ /* best effort */
95
+ }
92
96
  }
93
97
  function getClaudeCredentialsBackupPath(uuid) {
94
98
  return path.join((0, accountRegistry_1.getAccountsDir)(), 'credentials', `${uuid}.credentials.json`);
@@ -246,7 +250,7 @@ function waitForNextPoll(ms, signal) {
246
250
  return Promise.resolve(true);
247
251
  if (signal?.aborted)
248
252
  return Promise.resolve(false);
249
- return new Promise(resolve => {
253
+ return new Promise((resolve) => {
250
254
  const timer = setTimeout(() => {
251
255
  cleanup();
252
256
  resolve(true);
@@ -289,7 +293,7 @@ async function spawnAccountLogin(provider, label, opts = {}) {
289
293
  catch (err) {
290
294
  return { success: false, error: `Could not spawn account login: ${err}` };
291
295
  }
292
- child.on('exit', code => {
296
+ child.on('exit', (code) => {
293
297
  childExited = true;
294
298
  childExitCode = code;
295
299
  });
@@ -328,9 +332,7 @@ async function spawnAccountLogin(provider, label, opts = {}) {
328
332
  }
329
333
  }
330
334
  function switchAccount(provider, id) {
331
- return provider === 'codex'
332
- ? (0, codexProfiles_1.switchToCodexAccount)(id)
333
- : (0, accounts_1.switchToAccount)(id);
335
+ return provider === 'codex' ? (0, codexProfiles_1.switchToCodexAccount)(id) : (0, accounts_1.switchToAccount)(id);
334
336
  }
335
337
  function listAllAccounts() {
336
338
  return {
@@ -19,6 +19,23 @@ export interface SavedAccountRegistry {
19
19
  activeByProvider: Record<AccountProviderId, string | null>;
20
20
  accounts: SavedAccountProfile[];
21
21
  }
22
+ /**
23
+ * The currently active account resolved from the *live* provider auth, with the
24
+ * saved registry used only to enrich the display label.
25
+ *
26
+ * - `source: 'live'` — identity came from the provider's live auth file. This
27
+ * is the source of truth; `label` is filled in only when the live identity
28
+ * matches a saved profile.
29
+ * - `source: 'registry'` — no usable live identity (logged out, api-key auth,
30
+ * unparseable token); fell back to the saved active pointer.
31
+ * - `source: 'none'` — neither a live identity nor a saved active account.
32
+ */
33
+ export interface ResolvedActiveAccount {
34
+ email?: string;
35
+ label?: string;
36
+ providerAccountId?: string;
37
+ source: 'live' | 'registry' | 'none';
38
+ }
22
39
  export declare function getAccountsDir(): string;
23
40
  export declare function readSavedAccountRegistry(): SavedAccountRegistry | null;
24
41
  export declare function writeSavedAccountRegistry(registry: SavedAccountRegistry): void;
@@ -81,17 +81,19 @@ function normalizeRegistry(registry) {
81
81
  (account.providerId === 'claude-code' || account.providerId === 'codex') &&
82
82
  typeof account.id === 'string' &&
83
83
  typeof account.addedAt === 'string')
84
- .map(account => ({
84
+ .map((account) => ({
85
85
  ...account,
86
86
  label: account.label || undefined,
87
87
  email: account.email || undefined,
88
88
  providerAccountId: account.providerAccountId || undefined,
89
- metadata: account.metadata ? {
90
- email: account.metadata.email || undefined,
91
- workspaceId: account.metadata.workspaceId || undefined,
92
- planType: account.metadata.planType || undefined,
93
- authMode: account.metadata.authMode || undefined,
94
- } : undefined,
89
+ metadata: account.metadata
90
+ ? {
91
+ email: account.metadata.email || undefined,
92
+ workspaceId: account.metadata.workspaceId || undefined,
93
+ planType: account.metadata.planType || undefined,
94
+ authMode: account.metadata.authMode || undefined,
95
+ }
96
+ : undefined,
95
97
  }))
96
98
  : [];
97
99
  return normalized;
@@ -103,7 +105,7 @@ function migrateLegacyRegistry(legacy) {
103
105
  'claude-code': legacy.activeAccountUuid,
104
106
  codex: null,
105
107
  },
106
- accounts: legacy.accounts.map(account => ({
108
+ accounts: legacy.accounts.map((account) => ({
107
109
  id: account.uuid,
108
110
  providerId: 'claude-code',
109
111
  providerAccountId: account.uuid,
@@ -141,7 +143,7 @@ function listSavedAccountProfiles(providerId) {
141
143
  if (!registry)
142
144
  return [];
143
145
  return providerId
144
- ? registry.accounts.filter(account => account.providerId === providerId)
146
+ ? registry.accounts.filter((account) => account.providerId === providerId)
145
147
  : registry.accounts;
146
148
  }
147
149
  function getActiveSavedAccount(providerId) {
@@ -151,11 +153,11 @@ function getActiveSavedAccount(providerId) {
151
153
  const activeId = registry.activeByProvider[providerId];
152
154
  if (!activeId)
153
155
  return null;
154
- return registry.accounts.find(account => account.providerId === providerId && account.id === activeId) ?? null;
156
+ return (registry.accounts.find((account) => account.providerId === providerId && account.id === activeId) ?? null);
155
157
  }
156
158
  function upsertSavedAccountProfile(profile) {
157
159
  const registry = readSavedAccountRegistry() ?? createEmptyRegistry();
158
- const index = registry.accounts.findIndex(account => account.providerId === profile.providerId && account.id === profile.id);
160
+ const index = registry.accounts.findIndex((account) => account.providerId === profile.providerId && account.id === profile.id);
159
161
  if (index >= 0) {
160
162
  registry.accounts[index] = profile;
161
163
  }
@@ -173,7 +175,9 @@ function setActiveSavedAccount(providerId, accountId) {
173
175
  }
174
176
  function replaceSavedAccountProfiles(providerId, accounts, activeId) {
175
177
  const registry = readSavedAccountRegistry() ?? createEmptyRegistry();
176
- registry.accounts = registry.accounts.filter(account => account.providerId !== providerId).concat(accounts);
178
+ registry.accounts = registry.accounts
179
+ .filter((account) => account.providerId !== providerId)
180
+ .concat(accounts);
177
181
  registry.activeByProvider[providerId] = activeId;
178
182
  writeSavedAccountRegistry(registry);
179
183
  return registry;
@@ -182,13 +186,13 @@ function removeSavedAccountProfile(providerId, accountId) {
182
186
  const registry = readSavedAccountRegistry();
183
187
  if (!registry)
184
188
  return null;
185
- const index = registry.accounts.findIndex(account => account.providerId === providerId && account.id === accountId);
189
+ const index = registry.accounts.findIndex((account) => account.providerId === providerId && account.id === accountId);
186
190
  if (index === -1)
187
191
  return null;
188
192
  const [removed] = registry.accounts.splice(index, 1);
189
193
  if (registry.activeByProvider[providerId] === accountId) {
190
194
  registry.activeByProvider[providerId] =
191
- registry.accounts.find(account => account.providerId === providerId)?.id ?? null;
195
+ registry.accounts.find((account) => account.providerId === providerId)?.id ?? null;
192
196
  }
193
197
  writeSavedAccountRegistry(registry);
194
198
  return removed;
@@ -14,20 +14,20 @@ function errorMessage(error) {
14
14
  */
15
15
  function getActiveAccountStatus(error) {
16
16
  try {
17
- const claudeAccount = (0, accounts_1.readActiveClaudeAccount)();
18
- const codexAccount = (0, codexProfiles_1.getActiveCodexAccount)();
19
- const claude = claudeAccount
17
+ const claudeAccount = (0, accounts_1.resolveActiveClaudeAccount)();
18
+ const codexAccount = (0, codexProfiles_1.resolveActiveCodexAccount)();
19
+ const claude = claudeAccount.source !== 'none'
20
20
  ? {
21
21
  present: true,
22
22
  email: claudeAccount.email,
23
- label: claudeAccount.email,
23
+ label: claudeAccount.label ?? claudeAccount.email,
24
24
  }
25
25
  : { present: false };
26
- const codex = codexAccount
26
+ const codex = codexAccount.source !== 'none'
27
27
  ? {
28
28
  present: true,
29
- email: codexAccount.email ?? codexAccount.metadata?.email,
30
- label: codexAccount.label ?? codexAccount.email ?? codexAccount.metadata?.email ?? codexAccount.id,
29
+ email: codexAccount.email,
30
+ label: codexAccount.label ?? codexAccount.email ?? codexAccount.providerAccountId,
31
31
  }
32
32
  : { present: false };
33
33
  return {
@@ -4,6 +4,7 @@
4
4
  * Handles saving, listing, switching, and removing Claude accounts.
5
5
  * Both the VS Code extension and CLI consume this module.
6
6
  */
7
+ import { type ResolvedActiveAccount } from './accountRegistry';
7
8
  export interface AccountEntry {
8
9
  uuid: string;
9
10
  email: string;
@@ -38,4 +39,16 @@ export declare function reconcileClaudeAuthState(): void;
38
39
  export declare function removeAccount(uuid: string): AccountManagerResult;
39
40
  export declare function listAccounts(): AccountEntry[];
40
41
  export declare function getActiveAccount(): AccountEntry | null;
42
+ /**
43
+ * Resolves the *currently logged-in* Claude account for display, preferring the
44
+ * live `~/.claude/.claude.json` identity over the saved registry pointer (which
45
+ * only sidekick's own switch flow updates and therefore goes stale after a native
46
+ * `claude /login`).
47
+ *
48
+ * Safe self-heal: when the live account matches a saved profile (by account UUID,
49
+ * email as fallback) that isn't the current active pointer, the pointer is
50
+ * re-pointed so registry-keyed data tracks reality too. Never creates or deletes
51
+ * profiles; an unknown live account is shown as-is with no label and no write.
52
+ */
53
+ export declare function resolveActiveClaudeAccount(): ResolvedActiveAccount;
41
54
  export declare function isMultiAccountEnabled(): boolean;