sidekick-shared 0.28.2 → 0.29.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 (142) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +176 -30
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +14 -6
  6. package/dist/browser.js +26 -1
  7. package/dist/codex.d.ts +23 -0
  8. package/dist/codex.js +44 -0
  9. package/dist/codexProfiles.d.ts +1 -0
  10. package/dist/codexProfiles.js +8 -11
  11. package/dist/codexQuota.js +17 -4
  12. package/dist/esm/authStatus.js +87 -0
  13. package/dist/esm/browser.js +6 -2
  14. package/dist/esm/codex.js +18 -0
  15. package/dist/esm/codexProfiles.js +8 -11
  16. package/dist/esm/codexQuota.js +18 -5
  17. package/dist/esm/formatting.js +10 -1
  18. package/dist/esm/index.js +12 -9
  19. package/dist/esm/modelCatalog.js +2 -1
  20. package/dist/esm/modelInfo.js +422 -45
  21. package/dist/esm/multiProviderQuotaService.js +1 -1
  22. package/dist/esm/node.js +9 -3
  23. package/dist/esm/parsers/codexParser.js +175 -54
  24. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  25. package/dist/esm/parsers/openCodeParser.js +83 -21
  26. package/dist/esm/pricing.js +18 -0
  27. package/dist/esm/pricingCatalog.js +14 -7
  28. package/dist/esm/promptHistory.js +66 -4
  29. package/dist/esm/providerFailure.js +124 -3
  30. package/dist/esm/providers/codex.js +2 -24
  31. package/dist/esm/providers/codexDatabase.js +3 -19
  32. package/dist/esm/providers/codexRollouts.js +24 -1
  33. package/dist/esm/providers/openCode.js +9 -17
  34. package/dist/esm/quota.js +356 -24
  35. package/dist/esm/quotaHistory.js +33 -8
  36. package/dist/esm/quotaPoller.js +5 -2
  37. package/dist/esm/quotaResolver.js +31 -6
  38. package/dist/esm/quotaSnapshots.js +65 -10
  39. package/dist/esm/quotaThresholds.js +7 -2
  40. package/dist/esm/report/htmlReportGenerator.js +42 -5
  41. package/dist/esm/report/transcriptParser.js +13 -11
  42. package/dist/esm/schemas/index.js +2 -2
  43. package/dist/esm/schemas/providerFailure.js +3 -1
  44. package/dist/esm/schemas/quota.js +30 -0
  45. package/dist/esm/schemas/quotaHistory.js +10 -2
  46. package/dist/esm/schemas/stateFile.js +9 -0
  47. package/dist/esm/schemas/transcript.js +4 -0
  48. package/dist/esm/sessionPromptHistory.js +2 -2
  49. package/dist/esm/stateFile.js +18 -3
  50. package/dist/esm/statusline/claudeStatuslinePayload.js +16 -4
  51. package/dist/esm/statusline/formatter.js +57 -18
  52. package/dist/esm/statusline/index.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/testing/credentialGuard.js +87 -5
  55. package/dist/esm/toolCallOrigins.js +310 -0
  56. package/dist/esm/transcript.js +42 -3
  57. package/dist/esm/usage/usageEvents.js +146 -17
  58. package/dist/esm/usage/usageReports.js +39 -6
  59. package/dist/esm/watchers/jsonlTail.js +378 -59
  60. package/dist/esm/zaiQuotaApi.js +4 -2
  61. package/dist/formatting.d.ts +26 -1
  62. package/dist/formatting.js +11 -1
  63. package/dist/index.d.ts +22 -16
  64. package/dist/index.js +48 -12
  65. package/dist/modelCatalog.js +2 -1
  66. package/dist/modelInfo.d.ts +185 -16
  67. package/dist/modelInfo.js +431 -47
  68. package/dist/multiProviderQuotaService.js +1 -1
  69. package/dist/node.d.ts +16 -5
  70. package/dist/node.js +31 -2
  71. package/dist/parsers/codexParser.d.ts +55 -0
  72. package/dist/parsers/codexParser.js +175 -54
  73. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  74. package/dist/parsers/codexRolloutOrigin.js +111 -0
  75. package/dist/parsers/openCodeParser.d.ts +38 -0
  76. package/dist/parsers/openCodeParser.js +86 -21
  77. package/dist/pricing.d.ts +22 -0
  78. package/dist/pricing.js +50 -0
  79. package/dist/pricingCatalog.d.ts +10 -0
  80. package/dist/pricingCatalog.js +13 -6
  81. package/dist/promptHistory.d.ts +65 -4
  82. package/dist/promptHistory.js +68 -5
  83. package/dist/providerFailure.d.ts +44 -0
  84. package/dist/providerFailure.js +126 -4
  85. package/dist/providers/codex.d.ts +3 -22
  86. package/dist/providers/codex.js +3 -26
  87. package/dist/providers/codexDatabase.d.ts +3 -2
  88. package/dist/providers/codexDatabase.js +4 -22
  89. package/dist/providers/codexRollouts.d.ts +22 -0
  90. package/dist/providers/codexRollouts.js +24 -0
  91. package/dist/providers/openCode.js +8 -16
  92. package/dist/quota.d.ts +131 -1
  93. package/dist/quota.js +366 -24
  94. package/dist/quotaHistory.d.ts +10 -0
  95. package/dist/quotaHistory.js +34 -8
  96. package/dist/quotaPoller.d.ts +11 -3
  97. package/dist/quotaPoller.js +5 -2
  98. package/dist/quotaResolver.js +30 -5
  99. package/dist/quotaSnapshots.js +64 -9
  100. package/dist/quotaThresholds.js +7 -2
  101. package/dist/report/htmlReportGenerator.js +42 -5
  102. package/dist/report/transcriptParser.d.ts +10 -1
  103. package/dist/report/transcriptParser.js +13 -11
  104. package/dist/schemas/index.d.ts +3 -3
  105. package/dist/schemas/index.js +5 -2
  106. package/dist/schemas/providerFailure.d.ts +39 -0
  107. package/dist/schemas/providerFailure.js +2 -0
  108. package/dist/schemas/quota.d.ts +258 -0
  109. package/dist/schemas/quota.js +31 -1
  110. package/dist/schemas/quotaHistory.d.ts +2 -0
  111. package/dist/schemas/quotaHistory.js +10 -2
  112. package/dist/schemas/stateFile.d.ts +34 -0
  113. package/dist/schemas/stateFile.js +10 -1
  114. package/dist/schemas/transcript.d.ts +2 -0
  115. package/dist/schemas/transcript.js +4 -0
  116. package/dist/sessionPromptHistory.d.ts +7 -1
  117. package/dist/sessionPromptHistory.js +1 -1
  118. package/dist/stateFile.d.ts +18 -0
  119. package/dist/stateFile.js +17 -2
  120. package/dist/statusline/claudeStatuslinePayload.d.ts +4 -3
  121. package/dist/statusline/claudeStatuslinePayload.js +16 -4
  122. package/dist/statusline/formatter.d.ts +5 -0
  123. package/dist/statusline/formatter.js +56 -17
  124. package/dist/statusline/index.d.ts +4 -4
  125. package/dist/statusline/index.js +5 -1
  126. package/dist/statusline/payload.d.ts +10 -0
  127. package/dist/statusline/payload.js +14 -0
  128. package/dist/testing/credentialGuard.d.ts +14 -0
  129. package/dist/testing/credentialGuard.js +87 -5
  130. package/dist/toolCallOrigins.d.ts +70 -0
  131. package/dist/toolCallOrigins.js +346 -0
  132. package/dist/transcript.d.ts +12 -0
  133. package/dist/transcript.js +42 -3
  134. package/dist/types/opencode.d.ts +2 -0
  135. package/dist/usage/usageEvents.d.ts +54 -6
  136. package/dist/usage/usageEvents.js +146 -17
  137. package/dist/usage/usageReports.d.ts +35 -4
  138. package/dist/usage/usageReports.js +39 -6
  139. package/dist/watchers/jsonlTail.d.ts +101 -4
  140. package/dist/watchers/jsonlTail.js +380 -60
  141. package/dist/zaiQuotaApi.js +3 -1
  142. package/package.json +41 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,89 @@ 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.29.0] - 2026-11-01
9
+
10
+ ### Fixed
11
+
12
+ - **OpenCode usage on the OpenAI provider is no longer under-counted.** OpenCode stores every token bucket separately for every provider: input without cached tokens since 2025-11, output without reasoning since 2026-04. The parser still treated `openai` messages as cache-inclusive with reasoning inside output, so it subtracted cache reads from input a second time and never billed reasoning. It now reads the layout from the message's stored `total` (older layouts are still recognized) and defaults to the current one.
13
+ - **Cache tokens from OpenCode's older file storage are now counted.** Messages in that storage keep cache counts under `tokens.cache`, and the parser had ignored them.
14
+
15
+ ### Added
16
+
17
+ - **Lean subpaths.** Every new subpath has types, a CommonJS build and a tree-shakable ES-module build, and a contract test pins each one's module set to the root's bindings.
18
+ - `sidekick-shared/pricing`: pure, 8 modules, no Zod. Model pricing and identity, usage normalization and cost, cost provenance, cost/token/duration formatting, and text token estimates.
19
+ - `sidekick-shared/codex`: Node only, 13 modules, no Zod. Codex homes and child-process env, `auth.json` helpers, credential-store mode, `findCodexRolloutFile()`, `codexRolloutOrigin()`, auth-status parsers and atomic JSON writers.
20
+ - `sidekick-shared/providerFailure`: pure, 1 module.
21
+ - `sidekick-shared/statusline/payload`: pure, 2 modules. The status-line stdin parser without the account, credential and snapshot code the full `statusline` entry loads.
22
+ - **Usage events.** `collectUsageEvents()` events gain:
23
+ - `tokens.reasoningTokens`
24
+ - `promptOrdinal`: the prompt the call answered, using the same ordinals as `collectPromptHistory()` with default bounds
25
+ - `requestId`: the provider message id; Codex has none
26
+ - `subagent: { agentId, parentSessionId }`: Claude Code subagent transcripts, and Codex forks and spawned subagents
27
+
28
+ Priced cost is unchanged.
29
+
30
+ - **`bucketUsage()`:**
31
+ - a `session` group dimension
32
+ - `pricedCostUsd`, null when no call in the row was priced
33
+ - `reasoningTokens`
34
+ - `excludeSynthetic`
35
+ - `modelKey: 'canonical'`, which merges model spellings into one row
36
+ - **`findToolCallOrigins({ ids, workspacePaths, since })`** (Node) maps Claude Code `tool_use` ids and Codex `call_id`s to their session, file, time, tool name and the prompt ordinal they answered. Subagents and forks count against the parent's prompts.
37
+ - Workspace scoping and session keys match prompt history.
38
+ - Only lines containing a wanted id are parsed, and the walk stops once every id is found.
39
+ - **Model identity and pricing:**
40
+ - `canonicalModelId()` strips provider/region prefixes, Bedrock and Vertex forms, date suffixes, `-latest` and `[1m]`.
41
+ - `isMovingModelAlias()`.
42
+ - `parseModelId().variant` for GPT tier names, and `compareModelIds()` ranks astra > sol > terra > luna within a version.
43
+ - `registerModelPricing(entries, { onlyIfMissing })` / `clearRegisteredModelPricing()` add a local price layer (provenance `source: 'registered'`). It survives LiteLLM hydration and imported catalogs and is never exported in catalog snapshots.
44
+ - `getPricingCatalogInfo()` reports the active catalog source (`static`, `litellm`, `stale-cache`, `imported`), when it was fetched, and a stable `digest`. `HydrateResult` gains `digest`.
45
+ - **Cost provenance:** `costSourceFromPricingProvenance()`, and `mergeCostComponents()`, which ignores components not seen yet and zero-token unpriced parts. `formatCost(value, { source })` renders null or unpriced as "—" and estimates with " (est.)"; numeric output is unchanged.
46
+ - **Provider failures:** `diagnoseProviderFailure()` decodes Codex `codexErrorInfo` in string or object form (`decodeCodexErrorInfo()`, `CODEX_ERROR_INFO_KINDS`). Every diagnosis gains `retryable`, optional in the type and schema. Evidence for inputs without `codexErrorInfo` is unchanged.
47
+ - **Auth status parsers:** pure `parseClaudeAuthStatus()` for `claude auth status --json` and `parseCodexLoginStatus()` for `codex login status`. The Codex profile code now uses the latter.
48
+ - **Prompt history:**
49
+ - `collectPromptHistory({ includeText: false })` returns metadata only. Ordinals, cursors and exclusions are identical in both modes, and cursors resume across modes.
50
+ - `PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES` documents that ordinals are only comparable with the default record bound.
51
+ - **`codexRolloutOrigin()` / `codexSpawnParentFromSource()`:** pure fork and subagent detection from a rollout's `session_meta`.
52
+ - **`createJsonlTail()`:**
53
+ - `onRewrite: 'restart' | 'end'`
54
+ - `noFollow` (`JsonlTailSymlinkError`)
55
+ - `checkpoint()` / `resumeFrom()`, which survive a restart
56
+ - an async `read()`
57
+ - **`CodexRolloutParser`:** `maxPendingBytes`, `metadataOnly`, and `getPendingStats()`.
58
+ - **`installCredentialGuard({ preserveEnv })`** keeps listed variables when they point at safe locations, and throws when one points at a real home.
59
+ - **More exports:** `ProviderId` on `sidekick-shared/browser`. On `sidekick-shared/node`: `codexRefreshFingerprint()`, `compareCodexAuthFreshness()`, `getCodexCredentialStoreMode()`, `forceFileCredentialStore()` and the atomic JSON writers.
60
+
61
+ ### Changed
62
+
63
+ - **Usage cache v3.** `USAGE_CACHE_VERSION` is 3, and files are named `*.v3.json`, so a 0.28 reader sharing `usage-cache/` and this version never overwrite each other. The first collection after upgrading re-reads each session once, and old files age out through the existing LRU pruning.
64
+ - **Transcript projections.**
65
+ - Tool items gain `rawToolName` / `mcpServerName`, and projected `input` no longer carries internal `_sidekick*` keys. Raw `SessionEvent`s are unchanged.
66
+ - HTML reports show the raw tool and MCP server instead of those keys.
67
+ - The transcript schemas accept the new fields.
68
+ - **Lighter bundles.** `formatCost()` moved to the `formatting` module (still exported from `modelInfo`), and table-derived pricing lookups are lazy. A tree-shaken bundle that imports only `formatCost()`, `shortModelName()` and `parseModelId()` drops from 12.7 KB to 2.1 KB minified.
69
+ - `shortModelName()` / `getModelDisplayInfo()` use `canonicalModelId()`, so date-suffixed and provider-prefixed ids show their family label.
70
+ - `findCodexRolloutFile()` lives in the light rollout-helpers module; it is still exported from the provider.
71
+
72
+ ## [0.28.3] - 2026-10-31
73
+
74
+ ### Fixed
75
+
76
+ - **`fetchQuota()` marks a missing window instead of reporting 0%.** Claude Ultra (formerly Max 20x) has no 5-hour limit since 2026-10-30, and the usage endpoint returns `five_hour: null`. `fetchQuota()` read that as `{ utilization: 0, resetsAt: '' }`, the same as an idle window. Null or missing windows are now `{ utilization: 0, resetsAt: '', absent: true }`, with no projection; consumers that ignore `absent` see the old values.
77
+ - **`writeQuotaSnapshot()` no longer freezes a provider's snapshot.** It kept the sample with the later first-window reset, so once that window disappeared or reset without restarting, every newer sample (empty reset) lost and readers kept the pre-change numbers. An existing window that had already reset at the newer sample's capture time now yields. A newer capture wins when a window appears or disappears (`absent` changes). Status-line and history writes keep an API sample's `planLabel`, `planType`, `extraUsage`, and still-running extra meters.
78
+ - **`quotaFromStatuslinePayload()` no longer resurrects expired windows.** It copies a missing window from the fallback only when the fallback is `absent` or has not reset yet.
79
+ - **`resolveQuota()` and `resolveZaiQuota()` report an expired cache.** A snapshot whose every window has reset since is returned as unavailable, with an error naming its date and age, instead of as live numbers.
80
+ - `evaluateQuotaThresholds()` never alerts on an absent window. Quota-history daily averages count only samples where the window exists, and history samples keep `absent`.
81
+ - `formatStatusline()` leads with the second window when the first is absent, shows its reset day when more than a day away, never prints the current time for an empty reset, and labels windows from `windowMs`.
82
+ - Codex quota from `quotaFromCodexRateLimits()` names windows by `window_minutes` (`5-Hour`, `Weekly`), falling back to `Primary` / `Secondary`. It sets `windowMs` and marks a null window `absent`.
83
+
84
+ ### Added
85
+
86
+ - `quotaStateFromClaudeUsagePayload()` parses the usage endpoint's server-driven `limits[]` meters into `QuotaState.limits`: `QuotaLimitRow` with kind, group, a derived label such as `7-Day (Fable)`, utilization, reset, window length, projection, scope, severity, the headline pick, and `weekly_scoped_shares` allowances. Older servers' `seven_day_opus` / `seven_day_sonnet` / `seven_day_overage_included` fill it instead. It also parses `extra_usage` into `QuotaState.extraUsage`, and a missing headline window is filled from its `session` / `weekly_all` meter.
87
+ - `fetchQuota(token, { plan })` and `withClaudePlan()` stamp `planType` and `planLabel` from credential plan facts. `claudePlanLabel()` maps `default_claude_max_20x` to Ultra and `default_claude_max_5x` to Max. `resolveQuota()` passes the plan automatically, and `QuotaPoller`'s `getAccessToken` may return the credentials record (`QuotaPollerCredentials`) to do the same.
88
+ - Pure helpers on the root and `sidekick-shared/browser`: `quotaWindowApplies()`, `quotaWindowHasData()`, `quotaWindowLabel()`, `quotaWindowShortLabel()`, `extraQuotaRows()`, `formatExtraUsage()`, and `claudePlanLabel()`. Also `quotaSampleExpired()`, plus `quotaHistoryWindow()` on the root and statusline entries.
89
+ - Schemas: `quotaWindowSchema` gains `absent` / `windowMs`, `quotaStateSchema` gains `limits` / `extraUsage` / `planLabel` (`quotaLimitRowSchema`, `quotaExtraUsageSchema`), history samples gain `absent`, and `state.json` gains window `absent` plus quota `planLabel` / `extraLimits` (`stateFileQuotaLimitSchema`; `schemaVersion` stays 1).
90
+
8
91
  ## [0.28.2] - 2026-10-30
9
92
 
10
93
  ### Fixed
package/README.md CHANGED
@@ -9,6 +9,29 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
+ - **0.29.0: lean entries, usage attribution, and honest pricing**
13
+ - **New subpaths:**
14
+ - `sidekick-shared/pricing`: pure, 8 modules. Pricing, model identity, usage cost, cost provenance, formatting and token estimates.
15
+ - `sidekick-shared/codex`: Node, no Zod, 13 modules. Codex homes and env, `auth.json` helpers, rollout lookup and origin, auth-status parsers.
16
+ - `sidekick-shared/providerFailure`: pure.
17
+ - `sidekick-shared/statusline/payload`: pure. The status-line stdin parser without account or quota code.
18
+ - **Usage events:** `collectUsageEvents()` events carry reasoning tokens, the `promptOrdinal` they answered (the same ordinals as prompt history), the provider message id, and `subagent` attribution. The cache is v3, written as `*.v3.json` so 0.28 readers sharing the folder are unaffected.
19
+ - **`bucketUsage()`** gains a `session` dimension, `pricedCostUsd` (null when nothing was priced), `reasoningTokens`, `excludeSynthetic`, and `modelKey: 'canonical'`.
20
+ - **`findToolCallOrigins()`** maps tool-call ids to their session, time and prompt ordinal.
21
+ - **Pricing:**
22
+ - `canonicalModelId()` merges spellings of one model id.
23
+ - `registerModelPricing()` adds local prices that survive catalog hydration.
24
+ - `getPricingCatalogInfo()` reports which catalog priced a cost, with a stable digest.
25
+ - `formatCost(value, { source })` renders null and unpriced costs as "—" and estimates with " (est.)".
26
+ - **Provider failures:** `diagnoseProviderFailure()` decodes Codex `codexErrorInfo` and sets `retryable`.
27
+ - **Also new:**
28
+ - `parseClaudeAuthStatus()` and `parseCodexLoginStatus()`
29
+ - `collectPromptHistory({ includeText: false })`
30
+ - `createJsonlTail()` checkpoints, `onRewrite: 'end'`, `noFollow` and async `read()`
31
+ - Codex parser `maxPendingBytes` and `metadataOnly`
32
+ - transcript projections expose `rawToolName` and `mcpServerName` without leaking internal keys
33
+ - `installCredentialGuard({ preserveEnv })`
34
+ - **0.28.3: plans without a 5-hour window** — a null `five_hour` from the Claude usage endpoint (Ultra, since 2026-10-30) becomes `fiveHour.absent` instead of 0% used. The endpoint's server-driven `limits[]` meters, `extra_usage`, and the credential plan land on `QuotaState.limits` / `extraUsage` / `planLabel` (`quotaStateFromClaudeUsagePayload()`, `fetchQuota(token, { plan })`). Pure helpers `quotaWindowApplies()` / `quotaWindowHasData()` / `quotaWindowLabel()` / `quotaWindowShortLabel()` / `extraQuotaRows()` / `formatExtraUsage()` / `claudePlanLabel()` are on the root and browser entries, and `quotaHistoryWindow()` is on the root and statusline entries. `writeQuotaSnapshot()` no longer freezes once a window disappears or resets, and `resolveQuota()` reports a fully expired cache as unavailable.
12
35
  - **0.28.1: credential guard keeps `promisify(execFile)`** — after `installCredentialGuard()`, `promisify(execFile)` and `promisify(exec)` resolve to `{ stdout, stderr }` again (0.28.0 dropped Node's `util.promisify.custom`, so they returned the bare stdout string), and the promisified calls are guarded too.
13
36
  - **0.28.0: ES modules, narrow entries, and new readers** — a `module` export condition gives bundlers a tree-shakable ES-module build (Node keeps CommonJS); browser-safe parsers and helpers move onto `/browser` and the provider-home helpers onto `/node`; `sidekick-shared/testing` publishes the credential guard; `providerFailureDiagnosisSchema` joins `/schemas`; prompt history reports `changed` / `progressed` / `roots` and takes `sessionRefs`; signals carry `toolCategory` and `mcpServer`; `collectUsageEvents()` takes `workspacePaths`; `createJsonlTail()` is bounded and reports resets; and Claude sessions follow `CLAUDE_CONFIG_DIR` while Codex lookups include `archived_sessions/`.
14
37
  - **0.27.6: one usable copy per login** — Claude Code and Codex rotate refresh tokens, so a second usable copy of a login invalidates the first ("Not logged in · Please run /login"). The live account now keeps no profile-home credential, only a private backup; `syncLiveAccountState()` demotes a leftover copy (installing it first when it is newer) and reports `demoted`, `repaired`, and `journal`. Live writes pass `decideLiveClaudeWrite()` or the Codex equivalent: a switch captures the outgoing login first even with `force`, holds Claude Code's refresh lock, is journaled so the next sync finishes or rolls it back, and keeps live MCP tokens. Claude Keychain writes use Claude Code's hex method and are read back (`ClaudeCredentialWriteError`), so blobs over about 4 KB are no longer truncated. New root APIs: `readClaudeCredentialStore()`, `inspectClaudeCredentialStores()` / `repairClaudeCredentialStores()`, `inspectCodexCredentialStores()` / `repairCodexCredentialStores()`, `readClaudeCredentialLedger()`, `readStoredCodexAccount()`, `syncCodexLiveStateAsync()`, `discardAccountLogin()`, `writeAccountLauncher()`, and non-persisting health inspection (`persist: false`). `writeActiveCredentials()` now requires a config directory; `writeLauncher()`, `applyActiveClaudeToLiveHome()`, and `resolveActiveClaudeHome()` are deprecated.
@@ -114,8 +137,8 @@ returns the platform default that both overrides bypass.
114
137
  | **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) |
115
138
  | **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 |
116
139
  | **Report** | Self-contained HTML session report generation; `readSessionReportInputs()` returns metrics, transcript, and `subagents` for the session total |
117
- | **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows). `readClaudeCredentialStore()` returns `ok` / `absent` / `corrupt` / `error`; `writeActiveCredentials()` requires a config directory and reads every write back (`ClaudeCredentialWriteError`). `decideLiveClaudeWrite()`, `claudeRefreshFingerprint()`, `claudeLoginOnly()`, and `mergeClaudeLoginIntoLive()` expose the live-write policy. As of 0.27.6, `inspectClaudeCredentialStores()` / `repairClaudeCredentialStores()` and `inspectCodexCredentialStores()` / `repairCodexCredentialStores()` find and repair unused, duplicate, and unreadable stores Sidekick provably owns (repairs are dry runs unless `dryRun: false`), with Claude ownership recorded by `readClaudeCredentialLedger()` |
118
- | **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 |
140
+ | **Credentials** | Claude subscription (Pro / Max / Ultra) OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows). `readClaudeCredentialStore()` returns `ok` / `absent` / `corrupt` / `error`; `writeActiveCredentials()` requires a config directory and reads every write back (`ClaudeCredentialWriteError`). `decideLiveClaudeWrite()`, `claudeRefreshFingerprint()`, `claudeLoginOnly()`, and `mergeClaudeLoginIntoLive()` expose the live-write policy. As of 0.27.6, `inspectClaudeCredentialStores()` / `repairClaudeCredentialStores()` and `inspectCodexCredentialStores()` / `repairCodexCredentialStores()` find and repair unused, duplicate, and unreadable stores Sidekick provably owns (repairs are dry runs unless `dryRun: false`), with Claude ownership recorded by `readClaudeCredentialLedger()` |
141
+ | **Quota** | Claude subscription quota fetching (5-hour and 7-day windows; `fiveHour.absent` on plans without a 5-hour limit such as Ultra; server meters in `limits`, `extraUsage`, and `planLabel`), 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 |
119
142
  | **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()`) |
120
143
  | **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 |
121
144
  | **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 |
@@ -133,20 +156,24 @@ returns the platform default that both overrides bypass.
133
156
 
134
157
  ## Supported import paths
135
158
 
136
- `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.
137
-
138
- | Path | Runtime | What it exposes |
139
- | ------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
140
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
141
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`, the `CodexRolloutParser` with the Codex rollout and session-event types, human-prompt detection (`isHumanPrompt()` / `humanPromptText()`), and `renderHandoffUrlTemplate()`. |
142
- | `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()`, and whole sessions with `collectSessionPromptHistory()`), usage events from session logs (`collectUsageEvents()` with `createSessionProviders()` and `SYNTHETIC_MODEL`), session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), resilient observed-session collection (`fs` + providers), the provider homes the CLIs read (`getLiveClaudeHome()`, `getClaudeProjectsDir()`, `getSystemCodexHome()`, `getCodexMonitoringHomes()`, `getCodexExecutionEnv()`), Claude session-directory encoding (`encodeClaudeWorkspacePath()`, `getClaudeSessionDirectory()`), and the config root (`getConfigDir()`, `CONFIG_DIR_ENV_VAR`). |
143
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
144
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
145
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
146
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
147
- | `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. |
148
- | `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()`. |
149
- | `sidekick-shared/testing` | Node test runners | `installCredentialGuard()` keeps a test process away from the real `security` / `claude` / `codex` binaries and the real Claude, Codex, and Sidekick homes, and the Claude Code / Codex session-log builders (`createSessionLogFixtures()`, `writeJsonl()`, …). Framework-free. |
159
+ `sidekick-shared` ships three main entry points (the root, `/browser`, `/node`) plus narrower single-purpose 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.
160
+
161
+ | Path | Runtime | What it exposes |
162
+ | ------------------------------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
164
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math, `parseMcpToolName()`, the `CodexRolloutParser` with the Codex rollout and session-event types, human-prompt detection (`isHumanPrompt()` / `humanPromptText()`), `renderHandoffUrlTemplate()`, and the quota window helpers (`quotaWindowApplies()`, `quotaWindowHasData()`, `quotaWindowLabel()`, `quotaWindowShortLabel()`, `extraQuotaRows()`, `formatExtraUsage()`, `claudePlanLabel()`). |
165
+ | `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()`, and whole sessions with `collectSessionPromptHistory()`), tool-call origins (`findToolCallOrigins()`), usage events from session logs (`collectUsageEvents()` with `createSessionProviders()` and `SYNTHETIC_MODEL`), session previews (`listSessionPreviews()` / `readSessionPreview()` plus the bounded-concurrency `listSessionPreviewsAsync()` / `readSessionPreviewAsync()`), Codex history/rollout lookup (`readCodexHistory()`, `findCodexRolloutFile()`), resilient observed-session collection (`fs` + providers), the provider homes the CLIs read (`getLiveClaudeHome()`, `getClaudeProjectsDir()`, `getSystemCodexHome()`, `getCodexMonitoringHomes()`, `getCodexExecutionEnv()`), Codex `auth.json` and credential-store helpers (`codexRefreshFingerprint()`, `compareCodexAuthFreshness()`, `getCodexCredentialStoreMode()`, `forceFileCredentialStore()`), the atomic JSON writers, Claude session-directory encoding (`encodeClaudeWorkspacePath()`, `getClaudeSessionDirectory()`), and the config root (`getConfigDir()`, `CONFIG_DIR_ENV_VAR`). |
166
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
167
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
168
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
169
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure cost, token, and duration display helpers (`formatCost()`, `formatTokenCount()`, `formatDurationMs()`). |
170
+ | `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. |
171
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers and writers, `formatStatusline()`, burn-rate estimates, quota freshness and window helpers (`quotaWindowApplies()`, `quotaWindowHasData()`, `quotaWindowShortLabel()`, `quotaHistoryWindow()`), `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()`, and `readStateFile()` / `writeStateFile()`. |
172
+ | `sidekick-shared/testing` | Node test runners | `installCredentialGuard()` keeps a test process away from the real `security` / `claude` / `codex` binaries and the real Claude, Codex, and Sidekick homes, and the Claude Code / Codex session-log builders (`createSessionLogFixtures()`, `writeJsonl()`, …). Framework-free. |
173
+ | `sidekick-shared/pricing` | Any runtime | Pure, 8 modules, no Zod: model pricing and identity (`canonicalModelId()`, `parseModelId()`, `compareModelIds()`, `resolveModelPricing()`, `registerModelPricing()`, `getPricingCatalogInfo()`), usage normalization and cost (`normalizeProviderUsage()`, `calculateNormalizedUsageCost()`), cost provenance (`costSourceFromPricingProvenance()`, `mergeCostComponents()`, `mergeCostSources()`), `formatCost()` / `formatTokenCount()` / `formatDurationMs()`, and `estimateTextTokens()`. |
174
+ | `sidekick-shared/codex` | Node only | 13 modules, no Zod: Codex homes and the child-process env (`getSystemCodexHome()`, `resolveSidekickCodexHome()`, `getCodexExecutionEnv()`), `auth.json` helpers (`parseCodexAuthJson()`, `codexRefreshFingerprint()`, `compareCodexAuthFreshness()`), `getCodexCredentialStoreMode()` / `forceFileCredentialStore()`, `findCodexRolloutFile()`, `codexRolloutOrigin()`, `parseCodexLoginStatus()` / `parseClaudeAuthStatus()`, and `atomicWriteJson()`. |
175
+ | `sidekick-shared/providerFailure` | Any runtime | Pure, 1 module: `diagnoseProviderFailure()`, `decodeCodexErrorInfo()`, and the identifier lists. |
176
+ | `sidekick-shared/statusline/payload` | Any runtime | Pure, 2 modules: `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` without the account, credential, or snapshot code the full `statusline` entry loads. |
150
177
 
151
178
  Since 0.28.0 every subpath also has a `module` condition pointing at an ES-module build in `dist/esm/`. Bundlers that read it (esbuild, Vite, Rollup, webpack) tree-shake unused exports, so importing one helper from `sidekick-shared/node` bundles a few hundred bytes instead of the whole entry. Node itself never reads `module` and keeps loading the CommonJS build for both `require` and `import`, so a process never holds two copies of module state.
152
179
 
@@ -164,7 +191,7 @@ const guard = installCredentialGuard();
164
191
  afterEach(() => guard.assertNoViolations());
165
192
  ```
166
193
 
167
- The guard points `HOME` (and `USERPROFILE`, `APPDATA`, `LOCALAPPDATA`) at a temp directory it removes on exit, unsets `CLAUDE_CONFIG_DIR`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `CODEX_HOME`, and `SIDEKICK_CONFIG_DIR`, and patches the real `child_process` and `fs` modules. Spawning `security`, `claude`, or `codex` throws (a promisified `execFile` or `exec` rejects instead, and otherwise keeps Node's `{ stdout, stderr }` result), writing under the real `~/.claude`, `~/.claude.json`, `~/.codex`, or `~/.config/sidekick` throws, and reads there are recorded. Production code often swallows errors, so `assertNoViolations()` reports what was recorded even when nothing threw. A `vi.mock('child_process')` factory replaces the module and bypasses the guard for that file.
194
+ The guard points `HOME` (and `USERPROFILE`, `APPDATA`, `LOCALAPPDATA`) at a temp directory it removes on exit, unsets `CLAUDE_CONFIG_DIR`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `CODEX_HOME`, and `SIDEKICK_CONFIG_DIR` (pass `preserveEnv: ['SIDEKICK_CONFIG_DIR', …]` to keep any of these four when they point at a fixture; one pointing at a real home throws), and patches the real `child_process` and `fs` modules. Spawning `security`, `claude`, or `codex` throws (a promisified `execFile` or `exec` rejects instead, and otherwise keeps Node's `{ stdout, stderr }` result), writing under the real `~/.claude`, `~/.claude.json`, `~/.codex`, or `~/.config/sidekick` throws, and reads there are recorded. Production code often swallows errors, so `assertNoViolations()` reports what was recorded even when nothing threw. A `vi.mock('child_process')` factory replaces the module and bypasses the guard for that file.
168
195
 
169
196
  ### Browser / webview runtimes
170
197
 
@@ -382,11 +409,32 @@ for (const exclusion of result.exclusions) console.warn('record left out', exclu
382
409
  - **Model and usage** come from the first model call that answered the prompt (Claude: the first non-synthetic assistant message, with split records of that call replacing rather than adding usage; Codex: the first `token_count` with the model from `turn_context`). Later tool-loop calls never replace it. `inputTokens` is uncached input, and missing usage stays `undefined`, never zero.
383
410
  - **Late answers enrich the same prompt.** A prompt can be collected before its answer is written. `metadataStatus` is `pending` (no answering call yet), `provisional` (Claude call still being written, `stop_reason: null`), or `final`. While a file's last prompt is not final, the cursor keeps the byte offset and length of its line, its timestamp, and its answer state so far (first-answer message id, model, and usage, never prompt or answer text). The next call continues from where the file stopped, however far the answer is, and returns the prompt again with the same provider, sessionId, ordinal, timestamp, and text only when its model, usage, or status changed; the text comes from re-reading that one line. A prompt closed by the next prompt without an answer comes back once as `final` with no usage. Consumers should upsert by identity.
384
411
  - **Large files are streamed and resumable.** Files are read in 1 MiB chunks and every pass stops after a complete line, so memory stays near one chunk plus one record whatever the file size. `maxFileBytes` is the most one file may read in one call: a file with more left keeps what it returned, records `backlog` in its cursor, is counted in `stats.filesOverSizeLimit`, and continues on the next call even if its size and mtime have not changed. `maxSessions`, `maxTotalBytes`, `maxEntries`, the deadline, and an abort stop the whole call the same way, and `cursor.next` makes the next call start with the file that was stopped, so a large or busy session cannot starve the others. The first file of a call always finishes its first record, so a record larger than the budget still moves forward. Bytes appended during a call and a half-written final line wait for the next call. `boundsHit` lists these resumable stops and `hasMore` says whether calling again would read more. No bound throws.
385
- - **Permanent exclusions are reported.** A record over `maxRecordBytes` is skipped as it streams past without being buffered, even when it is still being written, and a complete line that is not JSON is skipped. Each is counted (`stats.recordsOverSizeLimit`, `stats.recordsMalformed`) and listed in `exclusions` with its byte offset. A prompt inside such a record is lost and does not consume an ordinal. A file that shrank or whose first 4 KiB changed is treated as rewritten: it is read again from the start with fresh ordinals and counted in `stats.sessionsRewritten`.
412
+ - **Permanent exclusions are reported.** A record over `maxRecordBytes` is skipped as it streams past without being buffered, even when it is still being written, and a complete line that is not JSON is skipped. Each is counted (`stats.recordsOverSizeLimit`, `stats.recordsMalformed`) and listed in `exclusions` with its byte offset. A prompt inside such a record is lost and does not consume an ordinal. Ordinals are therefore only comparable across runs and consumers that use the default bound, exported as `PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES` (16 MiB). A file that shrank or whose first 4 KiB changed is treated as rewritten: it is read again from the start with fresh ordinals and counted in `stats.sessionsRewritten`.
386
413
  - **Change reporting.** `changed` lists the sessions (provider, sessionId, `sessionKey`) whose cursor entry this call added, advanced, moved, or dropped after a rewrite, so a consumer never has to diff cursor internals. `progressed` is false when nothing new was read, which tells a stalled `hasMore` loop apart from one that is moving. `sessionKey` is opaque and equal to the cursor key; it tells apart two files that carry the same session id. `isPromptHistorySessionSettled(cursor.sessions[key])` says whether a cursor entry is fully read with no prompt waiting for its answer, and `parsePromptHistoryCursor(stored)` validates a stored cursor (versions 1 to 3) and returns it as `version: 3` with malformed entries dropped, or `undefined`.
387
414
  - **Moved files.** A session whose file is listed again under another path (Codex moves an archived thread's rollout to `archived_sessions/`) keeps its cursor entry and ordinals instead of being read again as a new session.
388
415
  - **Cursor versions.** Output is `version: 3`. Cursors from 0.27.0 (`version: 1`) and 0.27.1 (`version: 2`) are accepted: each file is re-verified once, and a 0.27.1 pending prompt is replayed from its line in resumable passes, so it now settles even when its answer is far away. Files 0.27.1 skipped for size were never in its cursor and are read in full on the first call. Metadata that 0.27.1 already gave up on (a pending replay that exceeded `maxFileBytes`) was not saved and cannot be recovered, and a 0.27.0 cursor still cannot recover prompts it returned without metadata.
389
416
 
417
+ - **Metadata only.** `includeText: false` returns the same entries, ordinals, cursors and exclusions with `text: ''` and `textOmitted: true`, for callers that only need ordinals and timestamps. A cursor written in either mode resumes in the other.
418
+
419
+ ### Find where a tool call came from
420
+
421
+ `findToolCallOrigins()` (Node-only) maps Claude Code `tool_use` ids and Codex `call_id`s to the record that issued them. It uses the scope, listing and session keys of `collectPromptHistory()`, so results join with prompt entries:
422
+
423
+ ```typescript
424
+ import { findToolCallOrigins } from 'sidekick-shared/node';
425
+
426
+ const origins = await findToolCallOrigins({
427
+ ids: ['toolu_01…', 'call_4df5…'],
428
+ workspacePaths: ['/Users/me/code/app'],
429
+ since: new Date('2026-10-01'), // prunes by file modification time
430
+ });
431
+ for (const [id, origin] of origins) {
432
+ // { provider, sessionId, sessionKey, filePath, timestamp, afterOrdinal, toolName?, subagent? }
433
+ }
434
+ ```
435
+
436
+ Only lines containing a wanted id are parsed, Claude Code subagent transcripts are searched too, and the walk stops once every id is found. `afterOrdinal` counts in the prompt history of `sessionId`. For a Codex subagent or fork, that is the parent session; the call's own rollout is in `filePath` and `subagent.agentId`. Ids that are not found are absent from the map.
437
+
390
438
  ### Collect whole sessions of prompts
391
439
 
392
440
  `collectSessionPromptHistory()` (Node-only) returns every human prompt of each matching session, grouped by session, for consumers that classify a session as a whole. It reads each session from the start with the same scanners as `collectPromptHistory()`, so text, scope, model and usage, and ordinals are identical. The replies and interaction signals that the prompt filter leaves out are opt-in.
@@ -586,6 +634,38 @@ is read and counted once. The result's `roots` names what was listed, and
586
634
  every event carries the `filePath` of its session. When no root resolves,
587
635
  no session is read.
588
636
 
637
+ #### Attribute usage to prompts and subagents
638
+
639
+ Since 0.29.0 each event says which prompt it answered and who made the call:
640
+
641
+ - **`promptOrdinal`** is the ordinal of the last human prompt at or before the call. These are the same ordinals `collectPromptHistory()` returns with default bounds, so usage joins with prompt entries on `(provider, sessionId, ordinal)`. It is `null` before the first prompt, and for a Codex fork or subagent, which has no prompts of its own. It is absent for OpenCode, which has no prompt history.
642
+ - **`tokens.reasoningTokens`** is set when the provider reported reasoning. It is informational: cost is unchanged because it is already priced from the billable output. Codex counts reasoning inside `outputTokens`; OpenCode reports it separately.
643
+ - **`requestId`** is the provider's message id (Claude Code, OpenCode). Codex records none.
644
+ - **`subagent: { agentId, parentSessionId }`** marks calls made by Claude Code subagent transcripts and by Codex spawned subagents or forks.
645
+
646
+ ```typescript
647
+ import { bucketUsage } from 'sidekick-shared/browser';
648
+
649
+ // Per session per day, model spellings merged, placeholders dropped.
650
+ const rows = bucketUsage(events, {
651
+ granularity: 'day',
652
+ groupBy: ['session', 'model'],
653
+ modelKey: 'canonical', // claude-opus-5-5-20261001 and claude-opus-5-5 share a row
654
+ excludeSynthetic: true,
655
+ });
656
+ // row.pricedCostUsd is null when no call in the row was priced; row.costUsd keeps 0 for compatibility.
657
+
658
+ // Cost per prompt:
659
+ const perPrompt = new Map<string, number>();
660
+ for (const event of events) {
661
+ if (event.promptOrdinal == null || event.costUsd == null) continue;
662
+ const key = `${event.provider}:${event.sessionId}:${event.promptOrdinal}`;
663
+ perPrompt.set(key, (perPrompt.get(key) ?? 0) + event.costUsd);
664
+ }
665
+ ```
666
+
667
+ The cache is versioned (`USAGE_CACHE_VERSION` 3, files named `*.v3.json`), so the first collection after upgrading re-reads each session once.
668
+
589
669
  ### Detect the active session provider
590
670
 
591
671
  For a single-provider surface, auto-detect and construct just one:
@@ -625,9 +705,9 @@ console.log(`Found ${tasks.length} tasks`);
625
705
 
626
706
  ### Diagnose terminal provider failures
627
707
 
628
- `diagnoseProviderFailure()` is a pure, synchronous helper exported from `sidekick-shared` and `sidekick-shared/browser`. The caller decides whether an SDK event is terminal: a Codex `ErrorItem`, including a reconnect notice, must not become terminal merely because its message can be classified.
708
+ `diagnoseProviderFailure()` is a pure, synchronous helper exported from `sidekick-shared`, `sidekick-shared/browser`, and `sidekick-shared/providerFailure`. The caller decides whether an SDK event is terminal: a Codex `ErrorItem`, including a reconnect notice, must not become terminal merely because its message can be classified.
629
709
 
630
- The identifier lists behind the result types are exported alongside it as readonly arrays (`PROVIDER_FAILURE_CODES`, `PROVIDER_RECOVERY_ACTIONS`, `PROVIDER_CREDENTIAL_KINDS`, `PROVIDER_AUTHENTICATION_STATES`, `PROVIDER_AUTHENTICATION_SOURCES`, `PROVIDER_FAILURE_EVIDENCE_SOURCES`). To validate a diagnosis that crossed a process boundary, use `providerFailureDiagnosisSchema` from `sidekick-shared/schemas`; it is built from the same arrays, so it accepts every value a given release can return.
710
+ The identifier lists behind the result types are exported alongside it as readonly arrays (`PROVIDER_FAILURE_CODES`, `PROVIDER_RECOVERY_ACTIONS`, `PROVIDER_CREDENTIAL_KINDS`, `PROVIDER_AUTHENTICATION_STATES`, `PROVIDER_AUTHENTICATION_SOURCES`, `PROVIDER_FAILURE_EVIDENCE_SOURCES`, and since 0.29.0 `CODEX_ERROR_INFO_KINDS`). To validate a diagnosis that crossed a process boundary, use `providerFailureDiagnosisSchema` from `sidekick-shared/schemas`; it is built from the same arrays, so it accepts every value a given release can return.
631
711
 
632
712
  ```typescript
633
713
  import { diagnoseProviderFailure } from 'sidekick-shared/browser';
@@ -667,6 +747,8 @@ The result contains `provider`, caller-supplied `credentialKind`, `diagnosis`, `
667
747
 
668
748
  Recognized structured request codes take precedence over HTTP status, followed by narrow message patterns. A bare 401 establishes credential rejection, with recovery chosen from the credential kind; a bare 403 does not distinguish authentication from execution policy and remains unknown, while a message containing `denied by policy` is `execution_policy_denied` regardless of status. An explicit OAuth or refresh-token rejection (`refresh token is invalid`) outranks overlapping thread/conversation-expiry wording; `login has expired` and `please re-authenticate` resolve according to the credential kind. Ambiguous phrases such as `Session expired` or `Permission denied` on their own remain unknown. Explicit credential rejection from the request overrides a successful local OAuth check. OAuth observations never validate API keys. Thread/conversation failures, 5xx responses, DNS failures, and connection resets do not establish rejected account credentials. The reported `Reconnecting... 2/5 (unexpected status 503 Service Unavailable: upstream connect error or disconnect/reset before headers. reset reason: connection termination)` is classified as `service_unavailable`, with message provenance and HTTP 503, if the caller supplies it as a terminal error.
669
749
 
750
+ Since 0.29.0 every diagnosis also carries `retryable`. It is true when sending the same request again, after any `retryAfter`, can reasonably succeed: a rate limit that lifts, overload, 5xx, timeouts and failed connections, but not a Codex `usageLimitExceeded` with no known reset. The field is optional in the type and the schema, so diagnoses from older producers stay valid. Codex's structured `codexErrorInfo` is decoded on the error or any nested `error`, `cause`, `response`, `data`, `body` or `errors` entry, in string or object form (`usageLimitExceeded`, `serverOverloaded`, `httpConnectionFailed: { httpStatusCode }`, `unauthorized`, `contextWindowExceeded`, …). The evidence entry records it as `codexErrorInfo`, and `decodeCodexErrorInfo()` exposes the decoder. `parseClaudeAuthStatus()` and `parseCodexLoginStatus()` are pure parsers for `claude auth status --json` and `codex login status` output.
751
+
670
752
  The projection contains no raw errors, stacks, credentials, or request URLs. Product wording belongs to consumers. The helper performs no I/O, credential reads, SDK calls, stream control, retries, or UI actions. Existing transcript/tool `ErrorCategory` and quota-specific guidance contracts remain unchanged.
671
753
 
672
754
  ### Check public provider service status
@@ -723,7 +805,8 @@ if (!peak.unavailable && peak.isPeak) {
723
805
  import { resolveQuota } from 'sidekick-shared';
724
806
 
725
807
  // Default: fresh persisted sample (status line, session, or API; under five minutes) →
726
- // session logs (Codex rollouts) → provider API → older sample labelled by age.
808
+ // session logs (Codex rollouts) → provider API → older sample labelled by age
809
+ // (one whose every window has already reset resolves as 'unavailable').
727
810
  const quota = await resolveQuota({ providerId: 'claude-code' });
728
811
  console.log(quota.resolution); // 'snapshot-fresh' | 'session' | 'api' | 'snapshot-aging' | 'snapshot-stale' | 'unavailable'
729
812
  console.log(quota.source, quota.capturedSource, quota.freshness, quota.ageMs);
@@ -735,21 +818,26 @@ await resolveQuota({ providerId: 'codex', workspacePath: process.cwd(), preferFr
735
818
  await resolveQuota({ providerId: 'claude-code', allowApi: false });
736
819
  ```
737
820
 
738
- `resolveQuota()` is shared by `sidekick quota`, `quota --all`, the MCP `get_quota_status` tool, and dashboard startup. CLI and MCP Codex queries pass `preferFresh: false` to fetch live utilization and reset credits every time; dashboard schedules and default cache preferences are unchanged. After a failed live query, the newer session or persisted sample wins by `capturedAt`, with ties going to the persisted sample and missing timestamps treated as oldest. Only a selected session sample is persisted. Fallback results keep their source and age plus the API `failure` descriptor. `allowApi: false` always prevents network access, including when `preferFresh` is false. Every side effect is injectable (`readSnapshot`, `writeSnapshot`, `fetchImpl`, `getClaudeAccessToken`, the account resolvers) for tests.
821
+ `resolveQuota()` is shared by `sidekick quota`, `quota --all`, the MCP `get_quota_status` tool, and dashboard startup. CLI and MCP Codex queries pass `preferFresh: false` to fetch live utilization and reset credits every time; dashboard schedules and default cache preferences are unchanged. After a failed live query, the newer session or persisted sample wins by `capturedAt`, with ties going to the persisted sample and missing timestamps treated as oldest. Only a selected session sample is persisted. Fallback results keep their source and age plus the API `failure` descriptor. `allowApi: false` always prevents network access, including when `preferFresh` is false. Claude results carry `planLabel` from the stored credentials (not when `getClaudeAccessToken` is injected), and a snapshot whose every window has reset resolves as `unavailable` with an error naming its date and age. Every side effect is injectable (`readSnapshot`, `writeSnapshot`, `fetchImpl`, `getClaudeAccessToken`, the account resolvers) for tests.
739
822
 
740
823
  ### Fetch subscription quota
741
824
 
742
825
  ```typescript
743
- import { fetchQuota, readClaudeMaxCredentials } from 'sidekick-shared';
826
+ import { extraQuotaRows, fetchQuota, readClaudeMaxCredentials } from 'sidekick-shared';
744
827
 
745
828
  // Returns a Promise — without `await`, the object is always truthy and
746
829
  // `accessToken` is undefined, which surfaces later as an auth failure.
747
830
  // Use `readClaudeMaxAccessTokenSync()` from a synchronous call site.
748
831
  const creds = await readClaudeMaxCredentials();
749
832
  if (creds) {
750
- const quota = await fetchQuota(creds.accessToken);
833
+ // Passing the credentials as `plan` stamps planLabel ('Pro' | 'Max' | 'Ultra' | …).
834
+ const quota = await fetchQuota(creds.accessToken, { plan: creds });
751
835
  if (quota.available) {
752
- console.log(`5-hour utilization: ${quota.fiveHour.utilization}%`);
836
+ if (quota.fiveHour.absent) console.log(`No 5-hour limit on ${quota.planLabel ?? 'this plan'}`);
837
+ else console.log(`5-hour utilization: ${quota.fiveHour.utilization}%`);
838
+ console.log(`7-day utilization: ${quota.sevenDay.utilization}%`);
839
+ // Extra meters beyond the two headline windows, e.g. "7-Day (Fable)".
840
+ for (const row of extraQuotaRows(quota)) console.log(`${row.label}: ${row.utilization}%`);
753
841
  } else {
754
842
  console.log(quota.failureKind, quota.httpStatus, quota.retryAfterMs);
755
843
  }
@@ -762,6 +850,8 @@ Unavailable quota responses remain non-throwing and may include:
762
850
  - `httpStatus`: HTTP response status when available
763
851
  - `retryAfterMs`: retry delay in milliseconds for `429` responses when the API provides `Retry-After`
764
852
 
853
+ `fiveHour` and `sevenDay` are always present; `absent: true` marks a window the plan does not have (Claude Ultra's five-hour limit, a Codex plan's missing second window), with `0` / `''` placeholders so older readers keep working. Use `quotaWindowApplies()` / `quotaWindowHasData()` instead of testing `resetsAt` yourself, and `quotaStateFromClaudeUsagePayload()` to normalize a usage-endpoint body you fetched elsewhere.
854
+
765
855
  For first-party style messaging, `describeQuotaFailure()` maps unavailable quota states to stable alert keys plus display-ready severity/title/message/detail fields for CLI and VS Code consumers.
766
856
 
767
857
  ### Model info and cost calculation
@@ -797,6 +887,48 @@ console.log(formatCost(costUsd)); // "$0.01"
797
887
  cache/reasoning normalization above and misprices OpenAI-style usage, where
798
888
  cached tokens are already included in the input count.
799
889
 
890
+ #### Model identity, local prices, and the pricing basis
891
+
892
+ ```typescript
893
+ import {
894
+ canonicalModelId,
895
+ registerModelPricing,
896
+ getPricingCatalogInfo,
897
+ formatCost,
898
+ mergeCostComponents,
899
+ costSourceFromPricingProvenance,
900
+ } from 'sidekick-shared/pricing';
901
+
902
+ canonicalModelId('us.anthropic.claude-opus-5-5-20261001-v1:0'); // 'claude-opus-5-5'
903
+ canonicalModelId('azure/us/gpt-6.1-terra'); // 'gpt-6.1-terra'
904
+
905
+ // Fill a gap the catalogs do not price yet (never overrides an exact price)...
906
+ registerModelPricing(
907
+ {
908
+ 'gpt-7-sol': {
909
+ inputCostPerMillion: 3,
910
+ outputCostPerMillion: 15,
911
+ cacheWriteCostPerMillion: 3.75,
912
+ cacheReadCostPerMillion: 0.3,
913
+ },
914
+ },
915
+ { onlyIfMissing: true },
916
+ );
917
+ // ...or, without onlyIfMissing, set prices that win over every catalog (e.g. negotiated rates).
918
+
919
+ getPricingCatalogInfo();
920
+ // { source: 'litellm' | 'stale-cache' | 'imported' | 'static', fetchedAt?, digest: 'fnv1a64:…', catalogEntries, registeredEntries }
921
+
922
+ formatCost(null); // '—'
923
+ formatCost(1.5, { source: 'estimated' }); // '$1.50 (est.)'
924
+ mergeCostComponents([
925
+ { source: 'reported', tokens: 1200 },
926
+ { source: 'unpriced', tokens: 0 }, // zero-token unpriced parts do not taint the total
927
+ ]); // 'reported'
928
+ ```
929
+
930
+ Registered prices survive `hydratePricingCatalog()` and are reported with provenance `source: 'registered'`. They are local configuration, so `exportResolvedModelCatalog()` never includes them. Store `getPricingCatalogInfo().digest` next to a synced cost so a later re-price can be told apart from a usage change. `isMovingModelAlias()` names agent-CLI aliases (`default`, `auto`, `sonnet`, `opus`, …) whose concrete model changes over time; don't apply dollar caps to them. `parseModelId()` reports a `variant` for GPT tier names, and `compareModelIds()` ranks astra > sol > terra > luna within a version.
931
+
800
932
  ### Extract token usage and tool calls from events
801
933
 
802
934
  ```typescript
@@ -913,7 +1045,14 @@ const tail = createJsonlTail({
913
1045
  tail.start();
914
1046
  ```
915
1047
 
916
- A poll reads from the last position to the end of the file in chunks of `maxReadBytes` (default 1 MiB), so a cold start on a large file never allocates more than one chunk plus one line. `onEvent` receives each line's `{ offset, length }` as its second argument. A line longer than `maxLineBytes` (default 16 MiB) is skipped as it streams past, even while it is still being written, and reported to `onError`. The tail also checks that it is still reading the same content. When the path now names another file (`replaced`), the file shrank (`truncated`), or its first 4 KiB or the byte before the resume point changed (`rewritten`), it starts over from offset 0 and calls `onReset({ reason, previousOffset })` before the events from the start arrive. `createJsonlTail()` is also exported from `sidekick-shared/node`.
1048
+ A poll reads from the last position to the end of the file in chunks of `maxReadBytes` (default 1 MiB), so a cold start on a large file never allocates more than one chunk plus one line. `onEvent` receives each line's `{ offset, length }` as its second argument. A line longer than `maxLineBytes` (default 16 MiB) is skipped as it streams past, even while it is still being written, and reported to `onError`. The tail also checks that it is still reading the same content. When the path now names another file (`replaced`), the file shrank (`truncated`), or its first 4 KiB or the byte before the resume point changed (`rewritten`), it starts over (from offset 0 by default; see `onRewrite`) and calls `onReset({ reason, previousOffset })` before the events from the start arrive. `createJsonlTail()` is also exported from `sidekick-shared/node`.
1049
+
1050
+ Since 0.29.0:
1051
+
1052
+ - `onRewrite: 'end'` continues from the rewritten file's last complete line instead of replaying it, and `onReset` then also receives the `offset` reading continues from. The default is `'restart'`.
1053
+ - `noFollow: true` refuses a symlinked path (`JsonlTailSymlinkError`, code `JSONL_TAIL_SYMLINK`) instead of following it.
1054
+ - `checkpoint()` returns a JSON-serializable `{ version: 1, offset, identity, prefixDigest }`. A new tail's `resumeFrom(checkpoint)` continues from it after a restart, and returns false (applying the rewrite policy) when the file is no longer the one checkpointed.
1055
+ - `read()` is an async pull that returns the new complete lines.
917
1056
 
918
1057
  `SessionWatcherCallbacks.onBatchComplete` is also available through `createWatcher()`. Save aggregation state only after a complete event batch and only when the state includes the full history (a completed replay or a compatible restored checkpoint). Live-only state is incomplete and must not replace a full-history checkpoint. JSONL positions end at complete lines; Codex readers reconstruct parser context when seeking. OpenCode's SQLite timestamp cursor does not persist row-version deduplication, so the dashboards replay OpenCode history instead of restoring a snapshot.
919
1058
 
@@ -938,12 +1077,13 @@ console.log(matches, report.sessionProvider, report.checks);
938
1077
  ### Poll quota with backoff
939
1078
 
940
1079
  ```typescript
941
- import { QuotaPoller } from 'sidekick-shared';
1080
+ import { QuotaPoller, readClaudeMaxCredentials } from 'sidekick-shared';
942
1081
 
943
1082
  const poller = new QuotaPoller({
944
1083
  activeIntervalMs: 300_000,
945
1084
  idleIntervalMs: 300_000,
946
- getAccessToken: async () => token,
1085
+ // Return the token, or the credentials record to also stamp planLabel.
1086
+ getAccessToken: async () => readClaudeMaxCredentials(),
947
1087
  });
948
1088
  poller.onUpdate((state) => console.log(state));
949
1089
  poller.start();
@@ -952,7 +1092,7 @@ poller.start();
952
1092
  ### Orchestrate quota across Claude and Codex
953
1093
 
954
1094
  ```typescript
955
- import { MultiProviderQuotaService } from 'sidekick-shared';
1095
+ import { MultiProviderQuotaService, quotaWindowApplies } from 'sidekick-shared';
956
1096
 
957
1097
  const service = new MultiProviderQuotaService({
958
1098
  // Optional — when set, an internal CodexQuotaWatcher is created and managed.
@@ -960,7 +1100,13 @@ const service = new MultiProviderQuotaService({
960
1100
  });
961
1101
 
962
1102
  service.onUpdate(({ claude, codex }) => {
963
- if (claude) console.log('Claude:', claude.fiveHour.utilization, claude.peakHours?.label);
1103
+ // Ultra has no five-hour window (`fiveHour.absent`); show the weekly one instead.
1104
+ if (claude) {
1105
+ const fiveHour = quotaWindowApplies(claude.fiveHour)
1106
+ ? `${claude.fiveHour.utilization}%`
1107
+ : 'no 5-hour limit';
1108
+ console.log('Claude:', fiveHour, `7d ${claude.sevenDay.utilization}%`, claude.peakHours?.label);
1109
+ }
964
1110
  if (codex) console.log('Codex:', codex.fiveHour.utilization, codex.accountLabel);
965
1111
  });
966
1112
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Pure parsers for the provider CLIs' read-only sign-in status commands,
3
+ * `claude auth status --json` and `codex login status`. They do no I/O. The
4
+ * caller runs the command with its own environment, home, and timeout, and
5
+ * passes the output here.
6
+ *
7
+ * A status command reports which credential the CLI would use. It does not
8
+ * prove that a request will authenticate: an expired or revoked token can
9
+ * still read as logged in.
10
+ *
11
+ * @module authStatus
12
+ */
13
+ /** `claude auth status --json`, projected. */
14
+ export interface ParsedClaudeAuthStatus {
15
+ /** True only when the output is a JSON object with `"loggedIn": true`. */
16
+ loggedIn: boolean;
17
+ /** `authMethod` as reported, e.g. `claude.ai`, `oauth_token`, or `api_key`. */
18
+ method?: string;
19
+ email?: string;
20
+ subscriptionType?: string;
21
+ /**
22
+ * The parsed JSON object. When it is absent the output was not a JSON
23
+ * object, so `loggedIn: false` means "unrecognized", not "signed out".
24
+ */
25
+ raw?: unknown;
26
+ }
27
+ /** `codex login status`, projected. */
28
+ export interface ParsedCodexLoginStatus {
29
+ /** True only when the output starts with `Logged in`. */
30
+ loggedIn: boolean;
31
+ /**
32
+ * `api-key` for any API-key login, `chatgpt` for a ChatGPT sign-in. Absent
33
+ * for other logins (access tokens) and when not logged in.
34
+ */
35
+ method?: 'chatgpt' | 'api-key';
36
+ /**
37
+ * The first line of output, bounded, without the masked key Codex appends to
38
+ * an API-key login. For display and logs, not for decisions.
39
+ */
40
+ detail?: string;
41
+ }
42
+ /**
43
+ * Parse the stdout of `claude auth status --json`. Claude Code exits 1 while
44
+ * printing valid signed-out JSON, so parse stdout whatever the exit status
45
+ * was. Only a JSON `"loggedIn": true` counts as logged in.
46
+ */
47
+ export declare function parseClaudeAuthStatus(stdout: string): ParsedClaudeAuthStatus;
48
+ /**
49
+ * Parse the output of `codex login status`. Codex prints its status line to
50
+ * stderr (`Logged in using ChatGPT`, `Logged in using an API key - sk-…`,
51
+ * `Not logged in`), so pass both streams. The exit status is not consulted:
52
+ * callers that require exit 0 check it themselves.
53
+ */
54
+ export declare function parseCodexLoginStatus(stdout: string, stderr?: string): ParsedCodexLoginStatus;