sidekick-shared 0.24.3 → 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 (52) hide show
  1. package/CHANGELOG.md +436 -0
  2. package/LICENSE +21 -0
  3. package/README.md +172 -60
  4. package/dist/accountManager.d.ts +13 -0
  5. package/dist/accountManager.js +60 -32
  6. package/dist/accountRegistry.js +59 -41
  7. package/dist/accounts.js +40 -19
  8. package/dist/autoSwitch.d.ts +2 -1
  9. package/dist/autoSwitch.js +32 -7
  10. package/dist/browser.d.ts +2 -0
  11. package/dist/browser.js +3 -1
  12. package/dist/codexProfiles.d.ts +14 -0
  13. package/dist/codexProfiles.js +268 -116
  14. package/dist/context/sessionContext.js +2 -4
  15. package/dist/ensureDefaultAccounts.js +4 -4
  16. package/dist/index.d.ts +14 -7
  17. package/dist/index.js +33 -7
  18. package/dist/modelInfo.js +11 -2
  19. package/dist/node.d.ts +6 -0
  20. package/dist/node.js +8 -1
  21. package/dist/observedContextWindows.d.ts +20 -4
  22. package/dist/observedContextWindows.js +47 -7
  23. package/dist/parsers/codexHistory.d.ts +30 -0
  24. package/dist/parsers/codexHistory.js +118 -0
  25. package/dist/parsers/mcpToolName.d.ts +26 -0
  26. package/dist/parsers/mcpToolName.js +31 -0
  27. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  28. package/dist/parsers/sessionPathResolver.js +11 -4
  29. package/dist/paths.d.ts +43 -4
  30. package/dist/paths.js +66 -6
  31. package/dist/peakHours.d.ts +16 -2
  32. package/dist/peakHours.js +18 -3
  33. package/dist/pricingCatalog.d.ts +7 -3
  34. package/dist/pricingCatalog.js +3 -2
  35. package/dist/providers/claudeCode.d.ts +2 -1
  36. package/dist/providers/claudeCode.js +18 -0
  37. package/dist/providers/codex.d.ts +22 -1
  38. package/dist/providers/codex.js +58 -0
  39. package/dist/providers/types.d.ts +5 -0
  40. package/dist/quotaHistory.d.ts +1 -1
  41. package/dist/quotaHistory.js +2 -2
  42. package/dist/quotaSnapshots.js +8 -77
  43. package/dist/sessionPreviews.d.ts +60 -0
  44. package/dist/sessionPreviews.js +234 -0
  45. package/dist/sessionTranscripts.js +10 -4
  46. package/dist/writers/atomic.d.ts +20 -0
  47. package/dist/writers/atomic.js +190 -19
  48. package/dist/zaiQuota.d.ts +87 -0
  49. package/dist/zaiQuota.js +49 -0
  50. package/dist/zaiQuotaWatcher.d.ts +7 -0
  51. package/dist/zaiQuotaWatcher.js +3 -0
  52. package/package.json +60 -2
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.
@@ -28,58 +30,104 @@ See the [full changelog](https://github.com/cesarandreslopez/sidekick-agent-hub/
28
30
  npm install sidekick-shared
29
31
  ```
30
32
 
33
+ Requires Node 20 or newer. The only npm dependency is `zod` (v4) — if your app
34
+ also uses zod, keep it on `^4` so schemas from this package validate against
35
+ your instance.
36
+
37
+ ### External runtime requirement: `sqlite3`
38
+
39
+ `OpenCodeProvider` and the database helpers behind it read their session store by
40
+ shelling out to an executable **`sqlite3` on `PATH`**. There is no bundled
41
+ driver. `CodexProvider` shells out to the same binary, but only for an optional
42
+ index.
43
+
44
+ Without it `OpenCodeProvider` returns **empty results rather than an error**,
45
+ which is indistinguishable from an empty workspace. Call `getRuntimeStatus()`,
46
+ implemented by `OpenCodeProvider` only, to tell the two apart:
47
+
48
+ ```typescript
49
+ const status = provider.getRuntimeStatus?.();
50
+ // status.kind: 'available' | 'db_missing' | 'sqlite_missing'
51
+ // | 'sqlite_blocked' | 'query_failed'
52
+ if (status && status.kind !== 'available') {
53
+ console.warn(`Session data unavailable: ${status.kind}`);
54
+ }
55
+ ```
56
+
57
+ `CodexProvider` uses the Codex `state.sqlite` database only as an index and
58
+ falls back to scanning rollout JSONL files, so it keeps working without
59
+ `sqlite3`. `ClaudeCodeProvider` reads JSONL transcripts directly and needs
60
+ nothing extra.
61
+
62
+ ### Overriding the config directory
63
+
64
+ Every reader and writer resolves `~/.config/sidekick` (`%APPDATA%/sidekick` on
65
+ Windows) lazily through `getConfigDir()`. Redirect it with either:
66
+
67
+ ```typescript
68
+ import { setConfigDir } from 'sidekick-shared';
69
+
70
+ setConfigDir('/tmp/sidekick-fixture'); // in-process; pass null to clear
71
+ ```
72
+
73
+ or the `SIDEKICK_CONFIG_DIR` environment variable, which takes effect for the
74
+ whole process and is read fresh on every call. `setConfigDir()` wins when both
75
+ are set. `getConfigDir()` returns the resolved root, `getConfigDirOverride()`
76
+ returns the active in-process override or `null`, and `getDefaultConfigDir()`
77
+ returns the platform default that both overrides bypass.
78
+
31
79
  ## API Overview
32
80
 
33
- | Module | Description |
34
- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
- | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
36
- | **Paths** | Config directory resolution, project data paths, workspace encoding |
37
- | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
38
- | **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 |
39
- | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
40
- | **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`) |
41
- | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
42
- | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
43
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
44
- | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
45
- | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
46
- | **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()`) |
47
- | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
48
- | **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) |
49
- | **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 |
50
- | **Report** | Self-contained HTML session report generation |
51
- | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
52
- | **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 deprecated observed-traffic z.ai estimator exports were removed in 0.23.0 — `resolveZaiQuota()` is the single z.ai quota API |
53
- | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
54
- | **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 |
55
- | **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 |
56
- | **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 |
57
- | **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()`) |
58
- | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
59
- | **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 |
60
- | **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 |
61
- | **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 |
62
- | **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 |
63
- | **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 |
64
- | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
65
- | **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 |
66
- | **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 |
67
115
 
68
116
  ## Supported import paths
69
117
 
70
118
  `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime.
71
119
 
72
- | Path | Runtime | What it exposes |
73
- | ------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
75
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math. |
76
- | `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). |
77
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
78
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
79
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
80
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
81
- | `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. |
82
- | `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. |
83
131
 
84
132
  ### Browser / webview runtimes
85
133
 
@@ -108,9 +156,10 @@ Hydrate the model catalog from the `node` subpath. One fetch covers both prices
108
156
  ```typescript
109
157
  import { hydratePricingCatalog, loadObservedContextWindows } from 'sidekick-shared/node';
110
158
 
111
- const { entries, contextWindowEntries } = await hydratePricingCatalog({
112
- cacheDir: '~/.config/sidekick',
113
- });
159
+ // cacheDir defaults to the Sidekick config dir, which honors
160
+ // SIDEKICK_CONFIG_DIR and setConfigDir(). Pass it only to place the cache
161
+ // somewhere else — and pass a real path, since `~` is not expanded.
162
+ const { entries, contextWindowEntries } = await hydratePricingCatalog();
114
163
 
115
164
  // Context windows a provider reported on an earlier run. These outrank the
116
165
  // catalog, which only knows each model's published maximum — Codex reports the
@@ -188,6 +237,45 @@ const humanCliPrompts = transcript.messages.filter(
188
237
 
189
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.
190
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
+
191
279
  ### Collect observed sessions with isolation and retry
192
280
 
193
281
  ```typescript
@@ -238,10 +326,13 @@ const sessions = provider.findAllSessions('/path/to/project');
238
326
  ### Read persisted tasks
239
327
 
240
328
  ```typescript
241
- import { readTasks, getProjectSlug } from 'sidekick-shared';
329
+ import { readTasks, resolveProjectIdentity } from 'sidekick-shared';
242
330
 
243
- const slug = getProjectSlug('/path/to/project');
244
- const tasks = readTasks({ projectSlug: slug });
331
+ // A bare string is treated as an already-encoded slug. Pass a project
332
+ // identity to resolve a working directory — that also gets you the
333
+ // legacy-slug fallback for stores written before symlink resolution.
334
+ const project = resolveProjectIdentity('/path/to/project');
335
+ const tasks = await readTasks(project, { status: 'pending' });
245
336
  console.log(`Found ${tasks.length} tasks`);
246
337
  ```
247
338
 
@@ -277,7 +368,10 @@ if (!peak.unavailable && peak.isPeak) {
277
368
  ```typescript
278
369
  import { fetchQuota, readClaudeMaxCredentials } from 'sidekick-shared';
279
370
 
280
- const creds = readClaudeMaxCredentials();
371
+ // Returns a Promise — without `await`, the object is always truthy and
372
+ // `accessToken` is undefined, which surfaces later as an auth failure.
373
+ // Use `readClaudeMaxAccessTokenSync()` from a synchronous call site.
374
+ const creds = await readClaudeMaxCredentials();
281
375
  if (creds) {
282
376
  const quota = await fetchQuota(creds.accessToken);
283
377
  if (quota.available) {
@@ -299,18 +393,36 @@ For first-party style messaging, `describeQuotaFailure()` maps unavailable quota
299
393
  ### Model info and cost calculation
300
394
 
301
395
  ```typescript
302
- import { getModelInfo, calculateCost, formatCost } from 'sidekick-shared';
396
+ import {
397
+ getModelInfo,
398
+ normalizeProviderUsage,
399
+ calculateNormalizedUsageCost,
400
+ formatCost,
401
+ } from 'sidekick-shared';
303
402
 
304
403
  const info = getModelInfo('claude-sonnet-4-6-20260321');
305
- console.log(info.family, info.version, info.contextWindow); // "sonnet" "4.6" 200000
404
+ console.log(info.family, info.version, info.contextWindow); // "sonnet" "4.6" 1000000
306
405
 
307
- const cost = calculateCost(
308
- { inputTokens: 1000, outputTokens: 500, cacheReadTokens: 200, cacheWriteTokens: 0 },
309
- 'claude-sonnet-4-6-20260321',
310
- );
311
- console.log(formatCost(cost)); // "$0.0045"
406
+ // Normalize first: providers disagree about whether cached and reasoning
407
+ // tokens are already counted in the input/output totals.
408
+ const usage = normalizeProviderUsage({
409
+ inputTokens: 1000,
410
+ outputTokens: 500,
411
+ cacheReadTokens: 200,
412
+ cacheWriteTokens: 0,
413
+ semantics: 'anthropic',
414
+ });
415
+ const { costUsd } = calculateNormalizedUsageCost({
416
+ usage,
417
+ modelId: 'claude-sonnet-4-6-20260321',
418
+ });
419
+ console.log(formatCost(costUsd)); // "$0.01"
312
420
  ```
313
421
 
422
+ `calculateCost()` still works but is deprecated — it predates the
423
+ cache/reasoning normalization above and misprices OpenAI-style usage, where
424
+ cached tokens are already included in the input count.
425
+
314
426
  ### Extract token usage and tool calls from events
315
427
 
316
428
  ```typescript
@@ -360,7 +472,7 @@ const snapshot = readSessionContextSnapshot(provider, '/path/to/session.jsonl');
360
472
 
361
473
  console.log(snapshot.pressure); // 'low' | 'medium' | 'high'
362
474
  console.log(snapshot.contextTokens, '/', snapshot.contextWindow);
363
- console.log(snapshot.capabilities.tools, snapshot.capabilities.mcpServers);
475
+ console.log(snapshot.capabilities.observedTools, snapshot.capabilities.mcpServers);
364
476
  console.log(snapshot.sources.length, 'evidence sources');
365
477
  ```
366
478
 
@@ -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)(),