sidekick-shared 0.24.4 → 0.24.5

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 (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +86 -45
  3. package/dist/accountManager.d.ts +13 -0
  4. package/dist/accountManager.js +60 -32
  5. package/dist/accountRegistry.js +59 -41
  6. package/dist/accounts.js +40 -19
  7. package/dist/autoSwitch.d.ts +2 -1
  8. package/dist/autoSwitch.js +32 -7
  9. package/dist/browser.d.ts +2 -0
  10. package/dist/browser.js +3 -1
  11. package/dist/codexProfiles.d.ts +14 -0
  12. package/dist/codexProfiles.js +268 -116
  13. package/dist/context/sessionContext.js +2 -4
  14. package/dist/ensureDefaultAccounts.js +4 -4
  15. package/dist/index.d.ts +11 -4
  16. package/dist/index.js +28 -7
  17. package/dist/node.d.ts +6 -0
  18. package/dist/node.js +8 -1
  19. package/dist/observedContextWindows.d.ts +13 -0
  20. package/dist/observedContextWindows.js +13 -0
  21. package/dist/parsers/codexHistory.d.ts +30 -0
  22. package/dist/parsers/codexHistory.js +118 -0
  23. package/dist/parsers/mcpToolName.d.ts +26 -0
  24. package/dist/parsers/mcpToolName.js +31 -0
  25. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  26. package/dist/parsers/sessionPathResolver.js +11 -4
  27. package/dist/paths.d.ts +7 -3
  28. package/dist/paths.js +7 -3
  29. package/dist/providers/claudeCode.d.ts +2 -1
  30. package/dist/providers/claudeCode.js +18 -0
  31. package/dist/providers/codex.d.ts +22 -1
  32. package/dist/providers/codex.js +58 -0
  33. package/dist/providers/types.d.ts +5 -0
  34. package/dist/quotaHistory.d.ts +1 -1
  35. package/dist/quotaHistory.js +2 -2
  36. package/dist/quotaSnapshots.js +8 -77
  37. package/dist/sessionPreviews.d.ts +60 -0
  38. package/dist/sessionPreviews.js +234 -0
  39. package/dist/sessionTranscripts.js +10 -4
  40. package/dist/writers/atomic.d.ts +20 -0
  41. package/dist/writers/atomic.js +190 -19
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to sidekick-shared will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.24.5] - 2026-08-18
9
+
10
+ ### Added
11
+
12
+ - Session-preview index: `listSessionPreviews()` enumerates session files stat-first across providers, sorts by mtime, applies `since`/`limit`, and content-reads only the post-limit survivors — so cost tracks the size of the answer, not the size of the history. `readSessionPreview()` reads one file's bounded preview (label, first prompt, first timestamp, workspace path) and degrades to `null` fields instead of throwing. Both are exported from the root and `sidekick-shared/node`
13
+ - `readCodexHistory()` returns the newest entries of `~/.codex/history.jsonl` from a bounded tail read, dropping the leading partial line and tolerating multibyte characters straddling the boundary. `findCodexRolloutFile()` resolves a Codex session id to its `sessions/YYYY/MM/DD/rollout-*.jsonl` transcript by filename only, newest mtime winning across monitored Codex homes
14
+ - `parseMcpToolName()` is the canonical splitter for `mcp__<server>__<tool>` identifiers, exported browser-safe; the session-context projector's MCP-server inference delegates to it. The degenerate `mcp__server__` shape (empty tool part) now parses as `null` rather than yielding a server name
15
+ - Async account entry points: `getAccountLoginStatusAsync`, `finalizeAccountLoginAsync`, `switchAccountAsync`, `prepareCodexAccountAsync`, `finalizeCodexAccountAsync`, `switchToCodexAccountAsync`. Each runs the codex CLI probes through `execFile` so the caller's event loop stays free, and probes are resolved before any lock is taken — never while holding one. Sync entry points keep their exact signatures for CLI callers
16
+ - Synchronous locked writers: `withFileLockSync()`, `updateJsonStoreAtomicSync()`, and `atomicWriteFileSync()` share the async path's cross-process lock-file protocol (sync and async waiters exclude each other), with an absolute 15s wait ceiling because a sync waiter blocks its event loop
17
+ - `AutoSwitchController.switchAccount` accepts a Promise-returning callback and defaults to `switchAccountAsync`, so a threshold crossing no longer runs blocking codex probes on the caller's loop. Overlapping quota updates are skipped while a switch is in flight, and a switch that lands after `dispose()` records its cooldown state without emitting a stale transition
18
+ - `SessionProviderBase` gains an optional `listAllSessionFiles()` so providers can expose native session-file enumeration to the preview index
19
+
20
+ ### Fixed
21
+
22
+ - The store lock's timeout is measured from the last time the lock changed hands rather than from a waiter's first attempt, so a burst of concurrent writers no longer manufactures "Timed out waiting for store lock" once the queue outlives the flat budget — a wedged holder still trips it in 3s
23
+ - A lock whose heartbeat mtime has been frozen past two minutes is reclaimed even when its recorded PID probes alive: PID recycling made `process.kill(pid, 0)` an unreliable liveness signal, and a crashed owner with a recycled PID used to wedge the store until a human deleted the lock
24
+ - The quota-snapshot store drops its private copy of the lock — which still had the flat 15s-from-first-attempt bug — and delegates to the shared one, keeping the same lock path so mixed-version processes still exclude each other. Snapshot writes pick up the shared writer's fsync and directory sync
25
+ - Four account modules each carried their own lock-free atomic writer with a fixed `.tmp` suffix, so cross-process account writes were last-writer-wins and two processes could interleave the same temp file. All account writes go through the shared fsynced writers, registry read-modify-write cycles hold the registry lock, and live credential swaps hold a per-provider auth-swap lock so two switchers cannot interleave a stash/restore of a rotated Codex refresh token
26
+ - `switchToAccount()` and `applyActiveClaudeToLiveHome()` threw on a held auth-swap lock where the Codex twins return a failed `AccountManagerResult`; they now keep the non-throwing contract their callers rely on
27
+ - Codex login-status and `pgrep` probes no longer freeze the caller's event loop for up to 4s; `ensureDefaultAccounts` and `spawnAccountLogin` run on the async path
28
+ - `AutoSwitchController`'s fire-and-forget update handler could surface a throwing registry read, snapshot read, or consumer `onTransition` as an unhandled promise rejection — process-fatal on modern Node. Those errors are now caught and logged
29
+ - `readCodexHistory()` rejects entries whose `ts` is not a finite number — `JSON.parse` yields `Infinity` for out-of-range literals, which later RangeErrored out of `Date` serialization in consumers. `readSessionPreview()` likewise returns `null` for a file whose mtime cannot be serialized, per its degrade-don't-throw contract
30
+ - `listRecentSessions()` sorts before labeling, so per-file content reads no longer scale with total session count, and a corrupt session beyond the limit no longer poisons the whole call
31
+
8
32
  ## [0.24.4] - 2026-07-25
9
33
 
10
34
  ### Fixed
package/README.md CHANGED
@@ -9,6 +9,8 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
+ - **Native session-file helpers** — `encodeClaudeWorkspacePath()` / `getClaudeSessionDirectory()` locate Claude Code's `~/.claude/projects/` directories with the real on-disk encoding, `findCodexRolloutFile()` resolves a Codex session id to its rollout path, `readCodexHistory()` tail-reads `~/.codex/history.jsonl`, `listSessionPreviews()` / `readSessionPreview()` build a cheap stat-first recent-sessions index across providers, and `parseMcpToolName()` splits `mcp__<server>__<tool>` identifiers (browser-safe).
13
+ - **Non-blocking account APIs & locked sync writers** — every login/switch entry point gains an async variant (`switchAccountAsync()`, `getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `prepareCodexAccountAsync()`, `finalizeCodexAccountAsync()`, `switchToCodexAccountAsync()`) that keeps `codex` CLI probes off the event loop, and synchronous callers get the same cross-process store lock via `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()`, with progress-based timeouts and abandoned-lock reclaim.
12
14
  - **Canonical usage and pricing** — provider-specific counters normalize into disjoint input/cache/output/reasoning categories with `normalizeProviderUsage()`; `calculateNormalizedUsageCost()` prevents cache/reasoning double counting and reports pricing provenance.
13
15
  - **Transcripts and resilient collection** — `projectSessionTranscript()` is browser-safe; Node consumers can use `listRecentSessions()`, `readSessionTranscript()`, and `ObservedSessionCollector` across Claude Code, Codex, and OpenCode readers.
14
16
  - **Shared token estimation** — `estimateTextTokens()` / `estimateSerializedTokens()` provide an injectable exact-counter path and the stable tokenizer-free `sidekick-fallback-v1` heuristic.
@@ -76,56 +78,56 @@ returns the platform default that both overrides bypass.
76
78
 
77
79
  ## API Overview
78
80
 
79
- | Module | Description |
80
- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
- | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
82
- | **Paths** | Config directory resolution, project data paths, workspace encoding |
83
- | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
84
- | **Writers** | Atomic persistence writers — `atomicWriteJson()` / `updateJsonStoreAtomic()` plus lock-coordinated capture helpers `addTask()`, `completeTask()`, `addDecision()`, `addNote()` that merge into the shared stores without clobbering concurrent writers |
85
- | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
86
- | **Observed Sessions (V1)** | Versioned provider-neutral observation contracts — `createProviderSessionAdapterV1()` / `derivePendingUserRequestV1()` with `ObservedAgentSessionV1`, `ProviderCapabilitiesV1`, `PendingUserRequestV1`, `SessionEvidenceRefV1`, and provenance/confidence wrapper types (Zod schemas in `sidekick-shared/schemas`) |
87
- | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
88
- | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
89
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
90
- | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
91
- | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
92
- | **Analytics** | Session-quality scoring (beta) with factor contributions and weekly trends (`scoreSessionQuality()`, `calculateQualityTrend()`), code-impact metrics including cost per changed line and per-model churn/cost (`calculateCodeImpact()`), and reported-vs-heuristic compaction accounting (`calculateCompactionLedger()`, `formatCompactionLedger()`) |
93
- | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
94
- | **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) |
95
- | **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 |
96
- | **Report** | Self-contained HTML session report generation |
97
- | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
98
- | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams (plus Codex reset credits via `fetchCodexResetCreditsFromApi()`, attached to quota state by `fetchCodexQuotaFromApi()`), 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 observed-traffic z.ai estimator was removed from the package root exports in 0.23.0 and every remaining export of it is `@deprecated` — `resolveZaiQuota()` is the single supported z.ai quota API |
99
- | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
100
- | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` exposes the per-provider session probe |
101
- | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, 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 |
102
- | **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 |
103
- | **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, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
104
- | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
105
- | **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 |
106
- | **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 |
107
- | **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 |
108
- | **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 |
109
- | **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 |
110
- | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
111
- | **Statusline** | Cache-only one-line account/quota/burn footer — `formatStatusline()`, `selectStatuslineAccount()`, `BurnRateCalculator`, and `estimateTimeToQuota()`, also published via the [`sidekick-shared/statusline`](#supported-import-paths) subpath for fast per-prompt rendering |
112
- | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
81
+ | Module | Description |
82
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
+ | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
84
+ | **Paths** | Config directory resolution, project data paths, workspace encoding |
85
+ | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
86
+ | **Writers** | Atomic persistence writers — `atomicWriteJson()` / `updateJsonStoreAtomic()` and their synchronous twins `atomicWriteJsonSync()` / `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()` (same cross-process lock file, progress-based timeouts, abandoned-lock reclaim), plus lock-coordinated capture helpers `addTask()`, `completeTask()`, `addDecision()`, `addNote()` that merge into the shared stores without clobbering concurrent writers |
87
+ | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
88
+ | **Observed Sessions (V1)** | Versioned provider-neutral observation contracts — `createProviderSessionAdapterV1()` / `derivePendingUserRequestV1()` with `ObservedAgentSessionV1`, `ProviderCapabilitiesV1`, `PendingUserRequestV1`, `SessionEvidenceRefV1`, and provenance/confidence wrapper types (Zod schemas in `sidekick-shared/schemas`) |
89
+ | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
90
+ | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
91
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
92
+ | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
93
+ | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
94
+ | **Analytics** | Session-quality scoring (beta) with factor contributions and weekly trends (`scoreSessionQuality()`, `calculateQualityTrend()`), code-impact metrics including cost per changed line and per-model churn/cost (`calculateCodeImpact()`), and reported-vs-heuristic compaction accounting (`calculateCompactionLedger()`, `formatCompactionLedger()`) |
95
+ | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
96
+ | **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) |
97
+ | **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 |
98
+ | **Report** | Self-contained HTML session report generation |
99
+ | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
100
+ | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams (plus Codex reset credits via `fetchCodexResetCreditsFromApi()`, attached to quota state by `fetchCodexQuotaFromApi()`), 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 observed-traffic z.ai estimator was removed from the package root exports in 0.23.0 and every remaining export of it is `@deprecated` — `resolveZaiQuota()` is the single supported z.ai quota API |
101
+ | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
102
+ | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` exposes the per-provider session probe |
103
+ | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, 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 |
104
+ | **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 |
105
+ | **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, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
106
+ | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
107
+ | **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 |
108
+ | **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 |
109
+ | **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. Every status/finalize/switch entry point also has an `-Async` variant (`getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `switchAccountAsync()`, …) that keeps `codex` CLI probes off the event loop — prefer those from hosts with a UI loop. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
110
+ | **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; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
111
+ | **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 |
112
+ | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
113
+ | **Statusline** | Cache-only one-line account/quota/burn footer — `formatStatusline()`, `selectStatuslineAccount()`, `BurnRateCalculator`, and `estimateTimeToQuota()`, also published via the [`sidekick-shared/statusline`](#supported-import-paths) subpath for fast per-prompt rendering |
114
+ | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
113
115
 
114
116
  ## Supported import paths
115
117
 
116
118
  `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime.
117
119
 
118
- | Path | Runtime | What it exposes |
119
- | ------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
121
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math. |
122
- | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, observed context-window persistence, recent-session/transcript reads, and resilient observed-session collection (`fs` + providers). |
123
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
124
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
125
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
126
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
127
- | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, quota history) — fs-free, no Node builtins. |
128
- | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
120
+ | Path | Runtime | What it exposes |
121
+ | ------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
123
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
124
+ | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, observed context-window persistence, recent-session/transcript reads, session previews (`listSessionPreviews()` / `readSessionPreview()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
125
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
126
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
127
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
128
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
129
+ | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, quota history) — fs-free, no Node builtins. |
130
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
129
131
 
130
132
  ### Browser / webview runtimes
131
133
 
@@ -235,6 +237,45 @@ const humanCliPrompts = transcript.messages.filter(
235
237
 
236
238
  For an already-open incremental Codex rollout, keep one `CodexRolloutParser` per stream and pass each parsed `SessionEvent[]` batch to `projectSessionTranscript()` (or append the events to an existing projection). This preserves parser state for paired tool calls, model context, and cumulative token deltas; consumers should not convert raw `response_item` rows themselves.
237
239
 
240
+ ### Locate provider-native session files
241
+
242
+ The library exports the same primitives its providers use to find session files on disk, so consumers never need to hand-roll path encoding or directory walks:
243
+
244
+ ```typescript
245
+ import {
246
+ getClaudeSessionDirectory, // ~/.claude/projects/<encoded workspace>
247
+ encodeClaudeWorkspacePath, // the encoding itself, if you need the raw string
248
+ discoverSessionDirectory, // mismatch-tolerant lookup — prefer this for reads
249
+ findAllClaudeSessions,
250
+ findCodexRolloutFile, // Codex session id -> rollout file path
251
+ readCodexHistory, // bounded tail of ~/.codex/history.jsonl, newest first
252
+ } from 'sidekick-shared';
253
+
254
+ const sessions = findAllClaudeSessions('/home/user/code/my-project');
255
+
256
+ const [latest] = readCodexHistory({ limit: 1 });
257
+ const rolloutPath = latest ? findCodexRolloutFile(latest.sessionId) : null;
258
+ ```
259
+
260
+ > **Naming trap:** the top-level `encodeWorkspacePath` export is Sidekick's own config-store slug (it keeps dots) and does **not** match Claude Code's directory scheme. For `~/.claude/projects/` paths always use `encodeClaudeWorkspacePath` — or better, `getClaudeSessionDirectory` / `discoverSessionDirectory`, which tolerate encoding drift.
261
+
262
+ For a cross-provider "recent sessions" surface, `listSessionPreviews()` enumerates stat-only, sorts by mtime, applies `since`/`limit`, and only then does bounded content reads for the survivors — so cost tracks the size of the answer, not the size of the history:
263
+
264
+ ```typescript
265
+ import { ClaudeCodeProvider, CodexProvider, listSessionPreviews } from 'sidekick-shared';
266
+
267
+ const previews = listSessionPreviews([new ClaudeCodeProvider(), new CodexProvider()], {
268
+ limit: 10,
269
+ since: lastRefreshTime, // optional incremental refresh
270
+ });
271
+ // -> [{ provider, sessionId, filePath, modifiedAt, sizeBytes,
272
+ // firstUserPrompt, firstTimestamp, workspacePath }, ...]
273
+ ```
274
+
275
+ `readSessionPreview(provider, filePath)` reads the same bounded preview for one known session file — `null` when the file is missing or unreadable, `null` fields when its content is malformed.
276
+
277
+ MCP tool identifiers (`mcp__<server>__<tool>`) split with `parseMcpToolName(name)` — first-double-underscore rule, `null` for malformed names, safe in browser bundles.
278
+
238
279
  ### Collect observed sessions with isolation and retry
239
280
 
240
281
  ```typescript
@@ -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)(),