sidekick-shared 0.26.5 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +98 -59
  3. package/dist/accountChanges.js +21 -2
  4. package/dist/accountHealth.d.ts +72 -0
  5. package/dist/accountHealth.js +286 -0
  6. package/dist/accountLaunch.d.ts +30 -0
  7. package/dist/accountLaunch.js +124 -0
  8. package/dist/accountManager.d.ts +33 -8
  9. package/dist/accountManager.js +183 -50
  10. package/dist/accountRegistry.d.ts +10 -0
  11. package/dist/accountRegistry.js +42 -22
  12. package/dist/accountSwitch.d.ts +43 -0
  13. package/dist/accountSwitch.js +115 -0
  14. package/dist/accountSync.d.ts +30 -0
  15. package/dist/accountSync.js +180 -0
  16. package/dist/accountSyncTypes.d.ts +25 -0
  17. package/dist/accountSyncTypes.js +7 -0
  18. package/dist/accounts.d.ts +58 -6
  19. package/dist/accounts.js +408 -189
  20. package/dist/claudeCredentials.d.ts +38 -0
  21. package/dist/claudeCredentials.js +74 -0
  22. package/dist/claudeProfiles.d.ts +40 -0
  23. package/dist/claudeProfiles.js +102 -10
  24. package/dist/codexAuth.d.ts +32 -0
  25. package/dist/codexAuth.js +127 -0
  26. package/dist/codexPaths.d.ts +21 -0
  27. package/dist/codexPaths.js +117 -0
  28. package/dist/codexProfiles.d.ts +58 -5
  29. package/dist/codexProfiles.js +574 -289
  30. package/dist/credentialIO.d.ts +12 -5
  31. package/dist/credentialIO.js +63 -29
  32. package/dist/credentials.d.ts +8 -0
  33. package/dist/credentials.js +28 -32
  34. package/dist/ensureDefaultAccounts.d.ts +8 -0
  35. package/dist/ensureDefaultAccounts.js +42 -82
  36. package/dist/humanPrompt.d.ts +27 -0
  37. package/dist/humanPrompt.js +93 -0
  38. package/dist/index.d.ts +31 -10
  39. package/dist/index.js +72 -13
  40. package/dist/keepAlive.d.ts +44 -0
  41. package/dist/keepAlive.js +106 -0
  42. package/dist/modelContext.js +4 -0
  43. package/dist/modelInfo.js +26 -1
  44. package/dist/node.d.ts +4 -0
  45. package/dist/node.js +9 -2
  46. package/dist/processDetection.d.ts +32 -0
  47. package/dist/processDetection.js +210 -0
  48. package/dist/promptHistory.d.ts +108 -0
  49. package/dist/promptHistory.js +616 -0
  50. package/dist/schemas/accountManager.d.ts +283 -1
  51. package/dist/schemas/accountManager.js +122 -1
  52. package/dist/schemas/assistantTurn.d.ts +2 -2
  53. package/dist/schemas/index.d.ts +6 -1
  54. package/dist/schemas/index.js +14 -1
  55. package/dist/schemas/observedSessionV1.d.ts +21 -21
  56. package/dist/schemas/quota.d.ts +18 -18
  57. package/dist/schemas/quotaHistory.d.ts +1 -1
  58. package/dist/schemas/sessionEvent.d.ts +25 -0
  59. package/dist/schemas/sessionEvent.js +3 -0
  60. package/dist/schemas/stateFile.d.ts +5 -5
  61. package/dist/terminalSync.d.ts +5 -5
  62. package/dist/terminalSync.js +7 -112
  63. package/dist/transcript.d.ts +6 -0
  64. package/dist/transcript.js +3 -0
  65. package/dist/types/sessionEvent.d.ts +8 -0
  66. package/dist/writers/atomic.d.ts +2 -0
  67. package/dist/writers/atomic.js +49 -2
  68. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,55 @@ 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.27.0] - 2026-09-22
9
+
10
+ ### Added
11
+
12
+ - `collectPromptHistory(options)` (Node-only; exported from `sidekick-shared/node` and the package root, not `sidekick-shared/browser`) returns the prompts a person typed in Claude Code and Codex sessions whose recorded working directory lies inside `workspacePaths` (realpath-normalized; git worktree siblings are added unless `includeWorktrees: false`). Scope fails closed: each prompt's cwd (Claude: the line's `cwd`; Codex: `session_meta` / `turn_context`) must realpath-resolve to a root or inside one. Entries hold `provider`, `sessionId`, a stable per-session `ordinal` (every human prompt consumes one, in or out of scope), the log-line `timestamp` (lines without one are skipped and counted), full `text`, realpath `cwd`, `gitBranch`, and the `model` / `usage` of the first answering model call. Codex subagent sessions and forked sessions (whose replayed parent history cannot be separated) are skipped. `cursor` is JSON-serializable, keyed without paths, and records `size`, `mtimeMs`, `lastOrdinal`, and a byte `offset` per file; unchanged files are skipped, appended files resume after `lastOrdinal`, and rewritten (shrunk) files are rescanned. `bounds` (`maxSessions` 1000, `maxFileBytes` 16 MiB, `maxTotalBytes` 256 MiB, `deadlineMs` 30 000) and `signal` never throw: hits are listed in `boundsHit`, an interrupted file contributes no entries, and the cursor covers only fully processed files. `stats` reports sessions scanned, skipped unchanged, out of scope, non-interactive, and over the size limit, plus prompts dropped for missing timestamps or scope.
13
+ - `isHumanPrompt(message, provider)` and `humanPromptText(message, provider)` classify `CanonicalTranscriptMessage`s. Claude Code: user role, entrypoint in `HUMAN_CLAUDE_ENTRYPOINTS` (`cli`, `claude-desktop`, `sdk-cli`), not meta, sidechain, or compaction summary, `origin.kind` absent or `human`, `promptSource` not `system`, and not tool-result-only, local-command output or caveat, task notification, system reminder, or interrupt marker; slash commands come back as typed (`/review foo`). Codex: user role with injected `AGENTS.md`, `<environment_context>`, `<user_instructions>`, turn-aborted, skill, subagent-notification, and internal-context blocks and `<image>` wrappers dropped. `HUMAN_CODEX_SOURCES` (`cli`, `vscode`, `exec`) is the session-level Codex rule applied by `collectPromptHistory()`.
14
+ - `TranscriptSourceProvenance` gains optional `isCompactSummary`, `originKind`, and `promptSource`, and `SessionEventBase` gains `isCompactSummary`, `origin`, and `promptSource`, all preserved from Claude Code JSONL so `readSessionTranscript()` output can be classified without reparsing.
15
+
16
+ - GPT-6 Sol (`gpt-6-sol`: $2 input / $10 output / $0.20 cached input per 1M tokens) and GPT-6 Luna (`gpt-6-luna`: $0.10 / $0.50 / $0.01) pricing and 1.05M context-window entries.
17
+
18
+ ### Fixed
19
+
20
+ - Claude Opus 5.5 (`claude-opus-5-5` / `claude-opus-5.5`) is priced at $4 input / $20 output / $5 cache write / $0.20 cache read per 1M tokens with a 1M context window. It previously prefix-matched `claude-opus-5` ($5 / $25 / $0.50), and the LiteLLM catalog has no entry for it.
21
+
22
+ ## [0.26.6] - 2026-09-16
23
+
24
+ ### Added
25
+
26
+ - `syncLiveAccountState()` reconciles the live Claude Code and Codex logins with the saved profiles: it registers logins the registry has never seen (label = email, `metadata.origin: 'live-sync'`), folds rotated tokens back into their profile when newer, merges duplicate Codex profiles for one workspace (oldest keeps id and label; losers are stashed), and re-points the active pointer. `ensureDefaultAccounts()` runs it at startup after `cleanupAbandonedLogins()`, and `onAccountsChanged()` runs it on filesystem and poll events.
27
+ - Per-account credential health: `getAccountHealth()` and `listAccountsWithHealth()` report `fresh | expiring | expired | unknown | missing` with access/refresh expiry from a secret-free `health.json` sidecar written by every credential store; `AccountView` and `AccountHealth` types and schemas.
28
+ - `SwitchAccountResult`: switches are two-phase (fold the outgoing login, install, verify by re-reading the store, then move the pointer), refuse targets whose stored credential is `expired`/`missing` (`needsLogin`, `--force` to override), report `runningConsumers` (`claude-cli`, `claude-desktop`, `codex-cli`, `codex-app`, `vscode-extension-host`) with reachability text, and return an `undoToken`; `undoLastSwitch()` and `getLastSwitch()` revert the last switch per provider.
29
+ - `detectRunningAccountConsumers()` / `findRunningProcesses()` detect running CLIs and desktop apps on macOS, Linux (`ps`), and Windows (`tasklist`), including npm-installed `claude` under `node`.
30
+ - `getAccountLaunchEnv()` builds an isolated `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment for running a CLI as a saved account without switching; `refreshInactiveAccounts()` is an opt-in keep-alive that refreshes inactive profiles through the official CLIs.
31
+ - `getCodexCredentialStoreMode()`; Codex profiles get a `config.toml` forced to `cli_auth_credentials_store = "file"`, and adding an account in keyring mode is refused with the fix.
32
+ - `beginAccountLogin()` accepts `existingAccountId` to re-authenticate a saved profile in place and defaults an empty label to the signed-in email; `readClaudeMaxCredentialsRaw()`, `getLiveClaudeHome()`, `claudeKeychainAccountName()`, `deleteStoredCredentials()`, pure `parseClaudeCredentialBlob()` / Codex auth helpers.
33
+
34
+ ### Changed
35
+
36
+ - Claude logins run `claude auth login` instead of `claude /login` (override with `SIDEKICK_CLAUDE_LOGIN_ARGS`); the isolated home is seeded past first-run onboarding; `spawnAccountLogin` treats `timeoutMs` as an inactivity budget with a 900 s `maxTimeoutMs` ceiling and removes the pending profile on failure.
37
+ - The Claude Keychain service suffix hashes the NFC-normalised config directory and the no-argument form follows `CLAUDE_CONFIG_DIR`, matching Claude Code 2.1.x; macOS reads fall back to `.credentials.json` when the Keychain item is missing; the Keychain account name is sanitised like the CLI does.
38
+ - Atomic writes retry transient `EPERM`/`EBUSY`/`EACCES` renames on Windows; `credentialIO` file writes go through the atomic writer.
39
+ - `prepareCodexAccount` folds a re-add of an already saved login into the existing profile instead of creating a duplicate; the switch preflight warns on stale Codex backups and refuses dead ones.
40
+ - `addCurrentAccount` / `removeAccount` keep their read-modify-write in one registry lock through new `*Unlocked` registry helpers.
41
+
42
+ ### Fixed
43
+
44
+ - Codex profile `config.toml` files get `cli_auth_credentials_store = "file"` at the top level; it was appended after the last `[table]`, where Codex ignored it, so isolated logins on Codex ≥ 0.140 still went to the OS keyring.
45
+ - Adding a Codex account whose login is already saved no longer fails with "was not prepared": finalizing an already-finalized profile activates it (or reports success with `activate: false`).
46
+ - A Codex switch records the account that was really live as `previousAccountId` (read after the phase-one sync, as the Claude path already did), so undo and rollback return to it instead of to a stale pointer.
47
+ - Switching refuses to run while `CLAUDE_CONFIG_DIR` / `CODEX_HOME` point inside sidekick's own profile homes (for example a shell that ran `eval "$(sidekick accounts env …)"`), instead of overwriting that profile with the target's credentials.
48
+ - A finished or discarded isolated Claude login deletes its temporary Keychain item instead of orphaning live tokens (macOS).
49
+ - `ensureDefaultAccounts()` reports a keyring-mode Codex login as `skipped` rather than `error` and no longer logs it as a failure at every startup.
50
+ - `findRunningProcessesSync()` allows a 16 MiB process listing like its async twin, so busy hosts do not silently report no running consumers.
51
+ - The Claude live-state fingerprint no longer includes the mtime of `~/.claude.json`, which Claude Code rewrites on nearly every prompt; watch-triggered syncs (and the macOS Keychain read behind them) now run only when the identity or credential file changes, or when the Keychain fold interval elapses.
52
+
53
+ ### Removed
54
+
55
+ - `setTerminalActiveProfile`, `installShellHook`, `uninstallShellHook`, and `isShellHookInstalled` (no consumers; a global export would silently move the live home). `writeLauncher` / `removeLauncher` remain and now unset `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
56
+
8
57
  ## [0.26.5] - 2026-09-15
9
58
 
10
59
  ### Added
package/README.md CHANGED
@@ -9,10 +9,12 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
+ - **0.27.0: human prompt history** — `collectPromptHistory()` (Node-only) returns the prompts a person typed in Claude Code and Codex sessions under a set of workspace roots (plus git worktree siblings), with fail-closed cwd scoping, untruncated text, stable per-session ordinals, the answering model and usage, a JSON-serializable resume cursor, and bounds that stop cleanly and report themselves in `boundsHit`. `isHumanPrompt()` / `humanPromptText()` expose the same classification for any canonical transcript message, and transcript provenance now keeps Claude Code's `isCompactSummary`, `originKind`, and `promptSource`. Claude Opus 5.5 gets its own pricing and context entries.
13
+ - **0.26.6: verified account switching** — `syncLiveAccountState()` reconciles the live Claude Code and Codex logins with saved profiles (registers unknown logins under their email, folds rotated tokens, merges duplicate Codex profiles, re-points the active pointer) and runs at startup, on account-change events, and as phase one of every switch; `getAccountHealth()` / `listAccountsWithHealth()` report secret-free `fresh | expiring | expired | unknown | missing` health; `switchAccount[Async]()` returns a `SwitchAccountResult` with verification, `runningConsumers`, `needsLogin`, and an `undoToken` for `undoLastSwitch()`; `detectRunningAccountConsumers()`, `getAccountLaunchEnv()`, `refreshInactiveAccounts()`, and `getCodexCredentialStoreMode()` are new. Claude logins run `claude auth login`, the Keychain suffix follows Claude Code 2.1.x, and the unused shell-hook helpers were removed.
12
14
  - **0.26.5: event-scoped collection** — `ObservedSessionCollector.subscribe()` reconciles each watch event with one `statWatchedSessionFile()` stat and runs a full `discover()` only for unknown paths, catch-up polls, and the initial pass, gated by `minReconcileGapMs`; `discover({ limit, since })` / `collect({ limit, since })` bound a pass, `maxCacheEntries` / `maxCacheBytes` bound the parse cache, `DirectoryListingCache` makes a full pass one stat per unchanged directory, and the info-severity `provider-discovery-completed` diagnostic reports what each pass cost.
13
15
  - **0.26.3–0.26.4: provider failure diagnosis and service status** — `diagnoseProviderFailure()` (root and browser) classifies caller-designated terminal errors into bounded diagnosis and recovery identifiers with evidence provenance; `fetchProviderServiceStatus()` (root and Node) returns explicit observed/unavailable public status with component and incident associations. 0.26.4 recognizes expired-login and re-authentication messages, gives explicit refresh-token rejection precedence over conversation-expiry wording, and classifies `denied by policy` as `execution_policy_denied`.
14
16
  - **0.26.2: reliable monitoring and search** — complete-line JSONL cursors preserve Unicode and Codex parser context; watcher `onBatchComplete` signals a delivered batch. `searchSessions()` adds workspace-path filtering and cancellation, and Doctor accepts an explicit session provider.
15
- - **One token vocabulary and cost provenance (0.26.0)** — `summarizeTokens()` gives every surface the same `total` (input + output + cache writes + cache reads) and `context` (input + cache) figures; `AggregatedTokens.costUsd` carries `costProvenance` so reports can say whether a cost was provider-reported or estimated; `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` turn Claude Code's status-line JSON into an official `statusline` quota source; `readQuotaSnapshot()` reports `ageMs` / `freshness` and keeps the sample's origin as `capturedSource`; `resolveQuota()` shares quota resolution across CLI, MCP, and dashboard startup, with default local-sample preferences and an API-first option used by one-shot Codex queries; `readSessionFileStats()` / `computeSessionFileStats()` give every provider one `readSessionStats()` over the shared aggregator, with `availability`, `costProvenance`, and `toolFailures` on `SessionFileStats`; `collectUsageEvents()` reads priced usage events from session logs with a per-session cache, `computeBillingBlocks()` groups them into five-hour billing blocks, and `bucketUsage()` turns them into day, week, month, or session rows; `applySessionSummary()` and `importSessionHistory()` let any host fold finished sessions into `historical-data.json` the way the extension does; observed sessions are read once per discovery, re-read only when their fingerprint changes, and classified from events by `classifySessionActivity()`; `readSessionReportInputs()` derives report metrics and transcript from one read; `writeStateFile()` / `readStateFile()` (also on the `statusline` subpath) maintain the public, versioned `state.json` for external tools, validated by `sidekickStateFileSchema`; `getScheduledPeakHoursState()` and `evaluateQuotaThresholds()` are shared by the CLI and extension dashboards.
17
+ - **One token vocabulary and cost provenance (0.26.0–0.26.1)** — `summarizeTokens()` gives every surface the same `total` (input + output + cache writes + cache reads) and `context` (input + cache) figures; `AggregatedTokens.costUsd` carries `costProvenance` so reports can say whether a cost was provider-reported or estimated; `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` turn Claude Code's status-line JSON into an official `statusline` quota source; `readQuotaSnapshot()` reports `ageMs` / `freshness` and keeps the sample's origin as `capturedSource`; `resolveQuota()` shares quota resolution across CLI, MCP, and dashboard startup, with default local-sample preferences and an API-first option used by one-shot Codex queries; `readSessionFileStats()` / `computeSessionFileStats()` give every provider one `readSessionStats()` over the shared aggregator, with `availability`, `costProvenance`, and `toolFailures` on `SessionFileStats`; `collectUsageEvents()` reads priced usage events from session logs with a per-session cache, `computeBillingBlocks()` groups them into five-hour billing blocks, and `bucketUsage()` turns them into day, week, month, or session rows; `applySessionSummary()` and `importSessionHistory()` let any host fold finished sessions into `historical-data.json` the way the extension does; observed sessions are read once per discovery, re-read only when their fingerprint changes, and classified from events by `classifySessionActivity()`; `readSessionReportInputs()` derives report metrics and transcript from one read; `writeStateFile()` / `readStateFile()` (also on the `statusline` subpath) maintain the public, versioned `state.json` for external tools, validated by `sidekickStateFileSchema` from `sidekick-shared/schemas`; `getScheduledPeakHoursState()` and `evaluateQuotaThresholds()` are shared by the CLI and extension dashboards.
16
18
  - **Host-safe APIs (0.25.0)** — `listSessionPreviewsAsync()` / `readSessionPreviewAsync()` read previews with bounded concurrency and cooperative yielding; `ObservedSessionCollector.subscribe()` and `SessionMonitor.subscribe()` replace host polling loops with debounced change batches; `createSessionProviders({ onDiagnostic })` constructs every usable provider with structured diagnostics instead of throwing; `findSessionById()` resolves one session without scanning; `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` and `registerModelAlias()` transfer context/pricing resolutions across realms; `onAccountsChanged()` reports login/logout/switch events; provider constructors perform no environment I/O, and quota pollers stay dormant without an account.
17
19
  - **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).
18
20
  - **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.
@@ -24,7 +26,7 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
24
26
  - **Doctor report** — `runDoctor()` builds a typed cross-provider health report (project identity, sessions, accounts, providers, dependencies); `formatHealthReport()` renders it for terminals.
25
27
  - **Analytics engines** — `scoreSessionQuality()` (beta) with weekly trends via `calculateQualityTrend()`, `calculateCodeImpact()` for cost per changed line and per-model churn, and `calculateCompactionLedger()` for reported-vs-heuristic compaction accounting.
26
28
  - **Error taxonomy & history** — `categorizeError()` assigns one shared taxonomy at ingest, and `appendErrorHistory()` / `getTopFailingTools()` keep an append-only per-project failure history.
27
- - **Statusline & burn rate** — `formatStatusline()` and `BurnRateCalculator` render a cache-only account/quota/burn footer, importable from the new [`sidekick-shared/statusline`](#supported-import-paths) subpath.
29
+ - **Statusline & burn rate** — `formatStatusline()` and `BurnRateCalculator` render a cache-only account/quota/burn footer, importable from the [`sidekick-shared/statusline`](#supported-import-paths) subpath.
28
30
  - **Codex reset credits** — `fetchCodexResetCreditsFromApi()` reads ChatGPT's reset-credit endpoint into a `CodexResetCreditsSnapshot`, and `fetchCodexQuotaFromApi()` attaches it to the returned quota state (preserved across session-sourced snapshot writes).
29
31
 
30
32
  See the [full changelog](https://github.com/cesarandreslopez/sidekick-agent-hub/blob/main/CHANGELOG.md) for everything.
@@ -86,56 +88,56 @@ returns the platform default that both overrides bypass.
86
88
 
87
89
  ## API Overview
88
90
 
89
- | Module | Description |
90
- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
- | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
92
- | **Paths** | Config directory resolution, project data paths, workspace encoding |
93
- | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
94
- | **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 |
95
- | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem. As of 0.25.0 constructors perform no environment I/O — build them all with `createSessionProviders({ onDiagnostic })`, which reports unavailable providers as structured diagnostics instead of failing; `findSessionById()` resolves one session through provider-native indexes; stable provider-id const arrays (`SESSION_PROVIDER_IDS`, `ACCOUNT_PROVIDER_IDS`, `QUOTA_PROVIDER_IDS`, `MODEL_PROVIDER_IDS`) accompany the derived unions. As of 0.26.5 Claude Code and Codex enumerate through a per-instance `DirectoryListingCache` (one stat per unchanged directory) and implement `statWatchedSessionFile()` so a watch event resolves with one stat; `splitWatchedPath()` and the `ListSessionFilesOptions` / `WatchedSessionFile` / `SessionEnumerationCounters` types are exported alongside |
96
- | **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`) |
97
- | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
98
- | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
99
- | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
100
- | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
101
- | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
102
- | **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()`) |
103
- | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
104
- | **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) |
105
- | **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 |
106
- | **Report** | Self-contained HTML session report generation |
107
- | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
108
- | **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 |
109
- | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
110
- | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` retains the legacy Claude Code path probe; `runDoctor({ provider })` diagnoses the selected session provider |
111
- | **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, account management, quota history, and the public `state.json` contract (`sidekickStateFileSchema`) — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Account and quota APIs guarantee results valid against the same-release schemas. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
112
- | **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 |
113
- | **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()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids |
114
- | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback. Stays dormant while no account exists and wakes through the account-change signal |
115
- | **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. All three stay dormant without a matching account and wake on account changes |
116
- | **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, `onAccountsChanged()` for push-based login/logout/switch notification (process-local mutations, filesystem watches, and a catch-up poll), and live-first `resolveActiveClaudeAccount()` / `resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
117
- | **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 |
118
- | **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()` |
119
- | **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 |
120
- | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
121
- | **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 |
122
- | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
91
+ | Module | Description |
92
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
93
+ | **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
94
+ | **Paths** | Config directory resolution, project data paths, workspace encoding |
95
+ | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
96
+ | **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 |
97
+ | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem. As of 0.25.0 constructors perform no environment I/O — build them all with `createSessionProviders({ onDiagnostic })`, which reports unavailable providers as structured diagnostics instead of failing; `findSessionById()` resolves one session through provider-native indexes; stable provider-id const arrays (`SESSION_PROVIDER_IDS`, `ACCOUNT_PROVIDER_IDS`, `QUOTA_PROVIDER_IDS`, `MODEL_PROVIDER_IDS`) accompany the derived unions. As of 0.26.5 Claude Code and Codex enumerate through a per-instance `DirectoryListingCache` (one stat per unchanged directory) and implement `statWatchedSessionFile()` so a watch event resolves with one stat; `splitWatchedPath()` and the `ListSessionFilesOptions` / `WatchedSessionFile` / `SessionEnumerationCounters` types are exported alongside |
98
+ | **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`) |
99
+ | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
100
+ | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
101
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
102
+ | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
103
+ | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
104
+ | **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()`) |
105
+ | **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
106
+ | **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) |
107
+ | **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 |
108
+ | **Report** | Self-contained HTML session report generation |
109
+ | **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows) |
110
+ | **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 |
111
+ | **Provider Status** | `fetchProviderServiceStatus()` returns explicit observed/unavailable public status with components and unresolved incidents from status.claude.com and status.openai.com; `fetchProviderStatus()` / `fetchOpenAIStatus()` remain as lossy legacy projections (`toLegacyProviderStatus()`) |
112
+ | **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` retains the legacy Claude Code path probe; `runDoctor({ provider })` diagnoses the selected session provider |
113
+ | **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, account management, quota history, and the public `state.json` contract (`sidekickStateFileSchema`, exported only from `sidekick-shared/schemas`) — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Account and quota APIs guarantee results valid against the same-release schemas. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
114
+ | **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 |
115
+ | **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.1 / Opus 5.5 / Opus 5 / Opus 4.8 / Opus 4.7 1M and GPT-5.x / GPT-6 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()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids |
116
+ | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback. Stays dormant while no account exists and wakes through the account-change signal |
117
+ | **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. All three stay dormant without a matching account and wake on account changes |
118
+ | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for startup bootstrap (migrations, abandoned-login cleanup, then `syncLiveAccountState()`), `syncLiveAccountState()` to register live logins the registry has never seen under their email, fold rotated tokens into their profile, and merge duplicate Codex profiles, `getAccountHealth()` / `listAccountsWithHealth()` for secret-free `fresh`/`expiring`/`expired`/`unknown`/`missing` health with access/refresh expiry,`getActiveAccountStatus()` for a single-pass active-account read across providers,`onAccountsChanged()`for push-based login/logout/switch notification (process-local mutations, filesystem watches, and a catch-up poll), and live-first`resolveActiveClaudeAccount()`/`resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
119
+ | **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; as of 0.26.6 switches are two-phase and verified, return a `SwitchAccountResult` with `runningConsumers`, `needsLogin`, and an `undoToken` (`undoLastSwitch()`), refuse expired or missing credentials unless forced, and `beginAccountLogin({ existingAccountId })` re-authenticates a saved profile in place. 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 |
120
+ | **Isolated launch & auto-switch** | `getAccountLaunchEnv()` builds an isolated `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment (plus `envUnset`) for running a CLI as a saved account without switching, `writeLauncher()` / `removeLauncher()` write POSIX launcher scripts, `detectRunningAccountConsumers()` / `findRunningProcesses()` find CLIs and desktop apps that hold a login (`ps` / `tasklist`), `refreshInactiveAccounts()` is an opt-in keep-alive through the official CLIs, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) moves to a healthier saved account when quota crosses a configured threshold; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
121
+ | **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 |
122
+ | **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
123
+ | **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 |
124
+ | **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
123
125
 
124
126
  ## Supported import paths
125
127
 
126
128
  `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime. As of 0.25.0 every documented subpath declares explicit `types` / `import` / `require` / `default` export conditions and resolves under Vite and TypeScript's legacy `moduleResolution: node` without consumer-side aliases.
127
129
 
128
- | Path | Runtime | What it exposes |
129
- | ------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
131
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
132
- | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, observed context-window persistence, recent-session/transcript reads, session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
133
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
134
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
135
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
136
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
137
- | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, account management, quota history, the `state.json` contract) — fs-free, no Node builtins. |
138
- | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
130
+ | Path | Runtime | What it exposes |
131
+ | ------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
132
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
133
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`. |
134
+ | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, public provider service status (`fetchProviderServiceStatus()`), observed context-window persistence, recent-session/transcript reads, human prompt history (`collectPromptHistory()`), session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), and resilient observed-session collection (`fs` + providers). |
135
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
136
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
137
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
138
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
139
+ | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, account management, quota history, the `state.json` contract) — fs-free, no Node builtins. |
140
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers and writers, `formatStatusline()`, burn-rate estimates, quota freshness helpers, `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()`, and `readStateFile()` / `writeStateFile()`. |
139
141
 
140
142
  ### Browser / webview runtimes
141
143
 
@@ -217,10 +219,10 @@ const usage = normalizeProviderUsage({
217
219
  reasoningTokens: 40,
218
220
  });
219
221
 
220
- // uncached=700, cacheRead=300, billableOutput=200, total=1200.
222
+ // uncachedInputTokens=700, cacheReadTokens=300, billableOutputTokens=200, totalTokens=1200.
221
223
  // Reasoning remains visible but is not counted twice because OpenAI includes it in output.
222
224
  const priced = calculateNormalizedUsageCost({ usage });
223
- // { costUsd: number | null, source: 'provider-reported' | 'model-catalog' | ... }
225
+ // { costUsd: number | null, source: 'provider-reported' | 'model-catalog' | 'explicit-pricing' | 'unpriced' }
224
226
  ```
225
227
 
226
228
  For `SessionEvent`s, `extractNormalizedUsage(event)` prefers the parser-attached `message.normalizedUsage` and falls back to normalizing the event's raw `usage` with `sidekick` semantics.
@@ -282,18 +284,51 @@ const transcript = readSessionTranscript(provider, recent.sessionPath, {
282
284
 
283
285
  The same API works with `ClaudeCodeProvider` and `OpenCodeProvider`. It covers provider sessions discoverable by those readers. Remote Claude SDK-only history and Codex history entries whose rollout file has disappeared are not synthesized.
284
286
 
285
- Canonical transcript messages retain provider-origin facts on `message.source`, including the original role, entrypoint, meta/sidechain flags, and source identifiers. Transcript-level `cwd` and `gitBranch` use the latest non-empty value reported by the provider. Consumers can therefore distinguish a human Claude Code CLI prompt without reparsing JSONL:
287
+ Canonical transcript messages retain provider-origin facts on `message.source`, including the original role, entrypoint, meta/sidechain flags, and source identifiers. Transcript-level `cwd` and `gitBranch` use the latest non-empty value reported by the provider. Claude Code messages also carry `isCompactSummary`, `originKind` (`origin.kind`), and `promptSource`. Consumers can therefore pick out the prompts a person typed without reparsing JSONL, using the same rule `collectPromptHistory()` applies:
286
288
 
287
289
  ```typescript
288
- const humanCliPrompts = transcript.messages.filter(
289
- (message) =>
290
- message.source.originalRole === 'user' &&
291
- message.source.entrypoint === 'cli' &&
292
- message.source.isMeta !== true &&
293
- message.source.isSidechain !== true,
294
- );
290
+ import { isHumanPrompt, humanPromptText } from 'sidekick-shared/node';
291
+
292
+ const humanPrompts = transcript.messages
293
+ .filter((message) => isHumanPrompt(message, 'claude-code')) // or 'codex'
294
+ .map((message) => humanPromptText(message, 'claude-code')); // `/review foo` for slash commands
295
+ ```
296
+
297
+ A Claude Code message is a human prompt when it has the user role, an entrypoint in `HUMAN_CLAUDE_ENTRYPOINTS` (`cli`, `claude-desktop`, `sdk-cli` — Agent SDK programs report `sdk-ts` / `sdk-py` and are excluded), is not meta, sidechain, or a compaction summary, has no non-human `origin.kind` (task notifications, coordinator messages, auto-continuations) or `promptSource: 'system'`, and is not tool-result-only, local-command output or caveat, a system reminder, or an interrupt marker. A Codex message is one when it has the user role and some text is left after injected `AGENTS.md`, `<environment_context>`, `<user_instructions>`, turn-aborted, skill, subagent-notification, and internal-context blocks and `<image>` wrappers are dropped. Codex session source is a session-level rule: `collectPromptHistory()` reads only sessions whose `session_meta.source` is in `HUMAN_CODEX_SOURCES` (`cli`, `vscode`, `exec`) and skips subagent and forked sessions.
298
+
299
+ ### Collect human prompt history for workspace roots
300
+
301
+ ```typescript
302
+ import { collectPromptHistory, type PromptHistoryCursor } from 'sidekick-shared/node';
303
+
304
+ let cursor: PromptHistoryCursor | undefined = loadCursor(); // your own JSON store
305
+ const result = await collectPromptHistory({
306
+ workspacePaths: ['/Users/me/code/app'], // realpath-normalized; worktree siblings added
307
+ providers: ['claude-code', 'codex'], // default: both
308
+ since: new Date('2026-09-01'),
309
+ cursor,
310
+ bounds: {
311
+ maxSessions: 1000,
312
+ maxFileBytes: 16 << 20,
313
+ maxTotalBytes: 256 << 20,
314
+ deadlineMs: 30_000,
315
+ },
316
+ signal: AbortSignal.timeout(60_000),
317
+ });
318
+
319
+ for (const entry of result.entries) {
320
+ // { provider, sessionId, ordinal, timestamp, text, cwd, gitBranch?, model?, usage? }
321
+ }
322
+ saveCursor(result.cursor); // unchanged files are skipped next time; appended files resume
323
+ if (result.boundsHit.length > 0) console.warn('partial pass', result.boundsHit, result.stats);
295
324
  ```
296
325
 
326
+ - **Scope fails closed.** A prompt is returned only when its recorded working directory (the Claude line's `cwd`; the Codex `session_meta` / `turn_context` cwd) realpath-resolves to a root or somewhere inside one. Missing or unresolvable cwds are excluded and counted in `stats.promptsOutOfScope`; Codex scope never comes from the rollout's location. `cwd` is the only path in the output, and cursor keys hold no paths.
327
+ - **Parse-only.** No redaction and no network I/O; text is untruncated.
328
+ - **Ordinals are stable.** Every human prompt in a file consumes one ordinal, even when it is out of scope, older than `since`, or lacks a timestamp (such lines are skipped and counted, never given a synthesized time). Entries are ordered by provider, session, then ordinal.
329
+ - **Model and usage** come from the first model call that answered the prompt (Claude: the first assistant message; Codex: the first `token_count` with the model from `turn_context`). `inputTokens` is uncached input. The last prompt of a still-running session can be returned before its answer exists and is not revisited.
330
+ - **Bounds never throw.** Hitting `maxSessions`, `maxTotalBytes`, the deadline, or an abort stops the pass; a file over `maxFileBytes` is skipped and counted in `stats.filesOverSizeLimit`. An interrupted file contributes no entries, and the cursor covers only fully processed files, so the next call picks up where this one stopped.
331
+
297
332
  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.
298
333
 
299
334
  ### Locate provider-native session files
@@ -371,7 +406,7 @@ Discovery and reads are isolated, retries use bounded 30-second-to-5-minute expo
371
406
 
372
407
  As of 0.25.0, parses are cached by fingerprint — a second `collect()` over unchanged files performs no content reads (cache hits refresh `observedAt` and activity state; `contentObservedAt` reports how old the cached usage/model data actually is, and `cacheHit` marks them). Observations expose structured `fingerprintParts: { sizeBytes, mtimeMs }` alongside the opaque fingerprint string, and diagnostics carry `severity` and `phase` in addition to their existing `kind`.
373
408
 
374
- As of 0.26.5, the parse cache is bounded (`maxCacheEntries`, default 20,000; `maxCacheBytes`, default 128 MiB — keep both above your session count) and a pass can be bounded with `discover({ limit, since })` / `collect({ limit, since })`. Every discovery emits an info-severity `provider-discovery-completed` diagnostic with `trigger`, `durationMs`, `referenceCount`, and the `directoriesListed` / `directoriesStatted` / `filesStatted` counters, so a host can see what each pass cost; hosts that surface every diagnostic should branch on `severity`.
409
+ As of 0.26.5, the parse cache is bounded (`maxCacheEntries`, default 20,000; `maxCacheBytes`, default 128 MiB — keep both above your session count) and a pass can be bounded with `discover({ limit, since })` / `collect({ limit, since })`. Every discovery emits an info-severity `provider-discovery-completed` diagnostic with `trigger`, `durationMs`, `referenceCount`, `partial`, and the `directoriesListed` / `directoriesStatted` / `filesStatted` counters, so a host can see what each pass cost; hosts that surface every diagnostic should branch on `severity`.
375
410
 
376
411
  Instead of a host polling loop, subscribe to debounced change batches. Each change carries the previous and current fingerprints. A recursive-watch event is reconciled with one stat when the provider implements `statWatchedSessionFile()`; a full `discover()` runs only for unknown paths, the catch-up poll (for filesystems that drop events), and the initial pass, at most once per `minReconcileGapMs` (2 s ceiling, scaled down to four times the last walk's duration for cheap sources):
377
412
 
@@ -536,7 +571,9 @@ Consumers own caching, stale-evidence presentation, polling, and request coalesc
536
571
  import { fetchPeakHoursStatus } from 'sidekick-shared';
537
572
 
538
573
  // Third-party endpoint: promoclock.co/api/status (unaffiliated with Anthropic).
539
- // Returns a `unavailable: true` fallback on any network or parse error.
574
+ // Falls back to the published weekday schedule (source: 'schedule') on any network,
575
+ // timeout, or parse error; unavailable: true only if the schedule itself cannot be
576
+ // evaluated. Cached 10 min; pass { force: true } to bypass.
540
577
  const peak = await fetchPeakHoursStatus();
541
578
  if (!peak.unavailable && peak.isPeak) {
542
579
  console.log(`${peak.label} — off-peak in ${peak.minutesUntilChange}m`);
@@ -885,6 +922,8 @@ Compiles TypeScript to `dist/` via `tsc`.
885
922
  npm test
886
923
  ```
887
924
 
925
+ `npm test` builds first (`pretest` runs `npm run build`), so a cold run includes the `tsc` step.
926
+
888
927
  Uses Vitest. Run `npm run test:watch` for watch mode.
889
928
 
890
929
  ## See Also
@@ -41,7 +41,9 @@ const path = __importStar(require("path"));
41
41
  const accountChangeSignal_1 = require("./accountChangeSignal");
42
42
  const accountStatus_1 = require("./accountStatus");
43
43
  const accountRegistry_1 = require("./accountRegistry");
44
- const codexProfiles_1 = require("./codexProfiles");
44
+ const accountSync_1 = require("./accountSync");
45
+ const claudeProfiles_1 = require("./claudeProfiles");
46
+ const codexPaths_1 = require("./codexPaths");
45
47
  const listeners = new Set();
46
48
  let watchers = [];
47
49
  let pollTimer;
@@ -130,7 +132,12 @@ function stopMonitoring() {
130
132
  activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
131
133
  }
132
134
  function watchTargets() {
133
- return Array.from(new Set([(0, accountRegistry_1.getAccountsDir)(), path.join(os.homedir(), '.claude'), (0, codexProfiles_1.getSystemCodexHome)()]));
135
+ return Array.from(new Set([
136
+ (0, accountRegistry_1.getAccountsDir)(),
137
+ (0, claudeProfiles_1.getLiveClaudeHome)(),
138
+ path.join(os.homedir(), '.claude'),
139
+ (0, codexPaths_1.getSystemCodexHome)(),
140
+ ]));
134
141
  }
135
142
  function scheduleCheck(reason) {
136
143
  if (debounceTimer)
@@ -145,6 +152,18 @@ function scheduleCheck(reason) {
145
152
  debounceTimer.unref?.();
146
153
  }
147
154
  function emitIfChanged(reason, force = false, singleListener) {
155
+ // External CLIs write the live homes directly: fold their logins into the
156
+ // saved profiles before reporting status, so a native `claude /login` is
157
+ // registered and its rotated tokens are backed up without a host round-trip.
158
+ // `local` signals come from our own registry writes and need no sync.
159
+ if (reason === 'filesystem' || reason === 'poll') {
160
+ try {
161
+ (0, accountSync_1.syncLiveAccountStateSync)({ reason: reason === 'poll' ? 'poll' : 'watch' });
162
+ }
163
+ catch {
164
+ // Sync problems surface through the health model, never through the watcher.
165
+ }
166
+ }
148
167
  const status = (0, accountStatus_1.getActiveAccountStatus)();
149
168
  const nextFingerprint = fingerprint(status);
150
169
  if (!force && nextFingerprint === lastFingerprint)
@@ -0,0 +1,72 @@
1
+ import { type ClaudeOauthCredentials } from './claudeCredentials';
2
+ import { type AccountProviderId } from './accountRegistry';
3
+ export type AccountHealthState = 'fresh' | 'expiring' | 'expired' | 'unknown' | 'missing';
4
+ export type AccountHealthSource = 'live-sync' | 'login' | 'switch' | 'keepalive' | 'migration';
5
+ export interface AccountHealthSidecar {
6
+ version: 1;
7
+ capturedAt: number;
8
+ accessExpiresAt?: number;
9
+ refreshExpiresAt?: number;
10
+ /** True when `refreshExpiresAt` was estimated rather than recorded by the CLI. */
11
+ refreshExpiryEstimated?: boolean;
12
+ lastRefreshAt?: number;
13
+ hasRefreshToken?: boolean;
14
+ authMode?: 'chatgpt' | 'api-key' | 'oauth';
15
+ source: AccountHealthSource;
16
+ }
17
+ export interface AccountHealth {
18
+ providerId: AccountProviderId;
19
+ accountId: string;
20
+ state: AccountHealthState;
21
+ accessExpiresAt?: number;
22
+ refreshExpiresAt?: number;
23
+ refreshExpiryEstimated?: boolean;
24
+ lastRefreshAt?: number;
25
+ checkedAt: number;
26
+ /** True when this profile is the account the live home is logged in to. */
27
+ isLive: boolean;
28
+ reason?: string;
29
+ }
30
+ export interface AccountView {
31
+ id: string;
32
+ providerId: AccountProviderId;
33
+ label?: string;
34
+ email?: string;
35
+ planType?: string;
36
+ isActive: boolean;
37
+ health: AccountHealth;
38
+ /** `learned` = registered automatically from a login sidekick observed. */
39
+ source: 'registered' | 'learned';
40
+ addedAt: string;
41
+ }
42
+ export interface AccountHealthOptions {
43
+ /**
44
+ * `cache` (default for {@link getAccountHealth}) reads only the sidecar;
45
+ * `store` re-reads the stored credential (a bounded Keychain call on macOS)
46
+ * and rewrites the sidecar; `auto` (default for {@link listAccountsWithHealth})
47
+ * probes the store only for profiles that have no sidecar yet, so the cost
48
+ * is paid once per profile and never on a hot path that opts into `cache`.
49
+ */
50
+ probe?: 'cache' | 'store' | 'auto';
51
+ now?: number;
52
+ }
53
+ export declare function getHealthSidecarPath(provider: AccountProviderId, accountId: string): string;
54
+ export declare function readHealthSidecar(provider: AccountProviderId, accountId: string): AccountHealthSidecar | null;
55
+ /** Build the sidecar for a Claude credential blob. */
56
+ export declare function claudeHealthSidecarFromBlob(blob: unknown, source: AccountHealthSource, now?: number): AccountHealthSidecar | null;
57
+ /** Record the health of a Claude profile from the credential just stored. */
58
+ export declare function writeClaudeHealthSidecar(accountId: string, blob: unknown, source: AccountHealthSource, now?: number): AccountHealthSidecar | null;
59
+ /** Build the sidecar for a raw Codex `auth.json`. */
60
+ export declare function codexHealthSidecarFromAuth(authRaw: string | null, source: AccountHealthSource, now?: number, fallbackPath?: string): AccountHealthSidecar | null;
61
+ /** Record the health of a Codex profile from the auth file just stored. */
62
+ export declare function writeCodexHealthSidecar(accountId: string, authRaw: string | null, source: AccountHealthSource, now?: number): AccountHealthSidecar | null;
63
+ /**
64
+ * Health of one saved account. `probe: 'cache'` never spawns anything; a
65
+ * missing sidecar on a profile that has credentials is reported as `unknown`
66
+ * (never `expired`) until a store probe or the next sync rewrites it.
67
+ */
68
+ export declare function getAccountHealth(provider: AccountProviderId, accountId: string, options?: AccountHealthOptions): AccountHealth;
69
+ /** Saved accounts with their health, in registry order, grouped by provider. */
70
+ export declare function listAccountsWithHealth(provider?: AccountProviderId, options?: AccountHealthOptions): AccountView[];
71
+ /** Sidecar helper shared by the credential chokepoints: extracts the Claude credential shape. */
72
+ export declare function describeClaudeCredential(blob: unknown): ClaudeOauthCredentials | null;