sidekick-shared 0.28.3 → 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.
- package/CHANGELOG.md +64 -0
- package/README.md +149 -19
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +12 -6
- package/dist/browser.js +17 -1
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +4 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +9 -6
- package/dist/esm/modelCatalog.js +2 -1
- package/dist/esm/modelInfo.js +422 -45
- package/dist/esm/node.js +9 -3
- package/dist/esm/parsers/codexParser.js +175 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +83 -21
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +14 -7
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/codex.js +2 -24
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +42 -3
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/jsonlTail.js +378 -59
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +16 -10
- package/dist/index.js +35 -12
- package/dist/modelCatalog.js +2 -1
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +431 -47
- package/dist/node.d.ts +16 -5
- package/dist/node.js +31 -2
- package/dist/parsers/codexParser.d.ts +55 -0
- package/dist/parsers/codexParser.js +175 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +86 -21
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +13 -6
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +3 -26
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +42 -3
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/jsonlTail.d.ts +101 -4
- package/dist/watchers/jsonlTail.js +380 -60
- package/package.json +41 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,70 @@ 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
|
+
|
|
8
72
|
## [0.28.3] - 2026-10-31
|
|
9
73
|
|
|
10
74
|
### Fixed
|
package/README.md
CHANGED
|
@@ -9,6 +9,28 @@ 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 })`
|
|
12
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.
|
|
13
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.
|
|
14
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/`.
|
|
@@ -134,20 +156,24 @@ returns the platform default that both overrides bypass.
|
|
|
134
156
|
|
|
135
157
|
## Supported import paths
|
|
136
158
|
|
|
137
|
-
`sidekick-shared` ships three
|
|
138
|
-
|
|
139
|
-
| Path
|
|
140
|
-
|
|
|
141
|
-
| `sidekick-shared`
|
|
142
|
-
| `sidekick-shared/browser`
|
|
143
|
-
| `sidekick-shared/node`
|
|
144
|
-
| `sidekick-shared/phrases`
|
|
145
|
-
| `sidekick-shared/modelContext`
|
|
146
|
-
| `sidekick-shared/modelInfo`
|
|
147
|
-
| `sidekick-shared/formatting`
|
|
148
|
-
| `sidekick-shared/schemas`
|
|
149
|
-
| `sidekick-shared/statusline`
|
|
150
|
-
| `sidekick-shared/testing`
|
|
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. |
|
|
151
177
|
|
|
152
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.
|
|
153
179
|
|
|
@@ -165,7 +191,7 @@ const guard = installCredentialGuard();
|
|
|
165
191
|
afterEach(() => guard.assertNoViolations());
|
|
166
192
|
```
|
|
167
193
|
|
|
168
|
-
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
|
|
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.
|
|
169
195
|
|
|
170
196
|
### Browser / webview runtimes
|
|
171
197
|
|
|
@@ -383,11 +409,32 @@ for (const exclusion of result.exclusions) console.warn('record left out', exclu
|
|
|
383
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.
|
|
384
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.
|
|
385
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.
|
|
386
|
-
- **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`.
|
|
387
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`.
|
|
388
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.
|
|
389
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.
|
|
390
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
|
+
|
|
391
438
|
### Collect whole sessions of prompts
|
|
392
439
|
|
|
393
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.
|
|
@@ -587,6 +634,38 @@ is read and counted once. The result's `roots` names what was listed, and
|
|
|
587
634
|
every event carries the `filePath` of its session. When no root resolves,
|
|
588
635
|
no session is read.
|
|
589
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
|
+
|
|
590
669
|
### Detect the active session provider
|
|
591
670
|
|
|
592
671
|
For a single-provider surface, auto-detect and construct just one:
|
|
@@ -626,9 +705,9 @@ console.log(`Found ${tasks.length} tasks`);
|
|
|
626
705
|
|
|
627
706
|
### Diagnose terminal provider failures
|
|
628
707
|
|
|
629
|
-
`diagnoseProviderFailure()` is a pure, synchronous helper exported from `sidekick-shared` and `sidekick-shared/
|
|
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.
|
|
630
709
|
|
|
631
|
-
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.
|
|
632
711
|
|
|
633
712
|
```typescript
|
|
634
713
|
import { diagnoseProviderFailure } from 'sidekick-shared/browser';
|
|
@@ -668,6 +747,8 @@ The result contains `provider`, caller-supplied `credentialKind`, `diagnosis`, `
|
|
|
668
747
|
|
|
669
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.
|
|
670
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
|
+
|
|
671
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.
|
|
672
753
|
|
|
673
754
|
### Check public provider service status
|
|
@@ -806,6 +887,48 @@ console.log(formatCost(costUsd)); // "$0.01"
|
|
|
806
887
|
cache/reasoning normalization above and misprices OpenAI-style usage, where
|
|
807
888
|
cached tokens are already included in the input count.
|
|
808
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
|
+
|
|
809
932
|
### Extract token usage and tool calls from events
|
|
810
933
|
|
|
811
934
|
```typescript
|
|
@@ -922,7 +1045,14 @@ const tail = createJsonlTail({
|
|
|
922
1045
|
tail.start();
|
|
923
1046
|
```
|
|
924
1047
|
|
|
925
|
-
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.
|
|
926
1056
|
|
|
927
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.
|
|
928
1058
|
|
|
@@ -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;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure parsers for the provider CLIs' read-only sign-in status commands,
|
|
4
|
+
* `claude auth status --json` and `codex login status`. They do no I/O. The
|
|
5
|
+
* caller runs the command with its own environment, home, and timeout, and
|
|
6
|
+
* passes the output here.
|
|
7
|
+
*
|
|
8
|
+
* A status command reports which credential the CLI would use. It does not
|
|
9
|
+
* prove that a request will authenticate: an expired or revoked token can
|
|
10
|
+
* still read as logged in.
|
|
11
|
+
*
|
|
12
|
+
* @module authStatus
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.parseClaudeAuthStatus = parseClaudeAuthStatus;
|
|
16
|
+
exports.parseCodexLoginStatus = parseCodexLoginStatus;
|
|
17
|
+
/** Larger outputs are not a status report; reading stops there. */
|
|
18
|
+
const MAX_STATUS_OUTPUT = 256 * 1024;
|
|
19
|
+
const MAX_DETAIL = 200;
|
|
20
|
+
function stringField(record, key) {
|
|
21
|
+
const value = record[key];
|
|
22
|
+
if (typeof value !== 'string')
|
|
23
|
+
return undefined;
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed && trimmed.length <= 512 ? trimmed : undefined;
|
|
26
|
+
}
|
|
27
|
+
function parseJsonObject(output) {
|
|
28
|
+
const trimmed = output.trim();
|
|
29
|
+
const candidates = [trimmed];
|
|
30
|
+
// Tolerate warning lines printed ahead of the JSON document, which must
|
|
31
|
+
// then start a line and run to the end of the output.
|
|
32
|
+
const start = trimmed.search(/\n[ \t]*\{/);
|
|
33
|
+
if (start >= 0)
|
|
34
|
+
candidates.push(trimmed.slice(start).trim());
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
if (!candidate.startsWith('{') || !candidate.endsWith('}'))
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(candidate);
|
|
40
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* Try the next candidate. */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse the stdout of `claude auth status --json`. Claude Code exits 1 while
|
|
51
|
+
* printing valid signed-out JSON, so parse stdout whatever the exit status
|
|
52
|
+
* was. Only a JSON `"loggedIn": true` counts as logged in.
|
|
53
|
+
*/
|
|
54
|
+
function parseClaudeAuthStatus(stdout) {
|
|
55
|
+
const raw = typeof stdout === 'string' && stdout.length <= MAX_STATUS_OUTPUT
|
|
56
|
+
? parseJsonObject(stdout)
|
|
57
|
+
: undefined;
|
|
58
|
+
if (!raw)
|
|
59
|
+
return { loggedIn: false };
|
|
60
|
+
const method = stringField(raw, 'authMethod');
|
|
61
|
+
const email = stringField(raw, 'email');
|
|
62
|
+
const subscriptionType = stringField(raw, 'subscriptionType');
|
|
63
|
+
return {
|
|
64
|
+
loggedIn: raw.loggedIn === true,
|
|
65
|
+
...(method ? { method } : {}),
|
|
66
|
+
...(email ? { email } : {}),
|
|
67
|
+
...(subscriptionType ? { subscriptionType } : {}),
|
|
68
|
+
raw,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse the output of `codex login status`. Codex prints its status line to
|
|
73
|
+
* stderr (`Logged in using ChatGPT`, `Logged in using an API key - sk-…`,
|
|
74
|
+
* `Not logged in`), so pass both streams. The exit status is not consulted:
|
|
75
|
+
* callers that require exit 0 check it themselves.
|
|
76
|
+
*/
|
|
77
|
+
function parseCodexLoginStatus(stdout, stderr) {
|
|
78
|
+
const combined = `${typeof stdout === 'string' ? stdout : ''}\n${typeof stderr === 'string' ? stderr : ''}`.trim();
|
|
79
|
+
const firstLine = combined.split(/\r?\n/, 1)[0]?.trim() ?? '';
|
|
80
|
+
const detail = (/^(Logged in using .*?API key)\s+-\s.*$/i.exec(firstLine)?.[1] ?? firstLine)
|
|
81
|
+
.slice(0, MAX_DETAIL)
|
|
82
|
+
.trim();
|
|
83
|
+
const withDetail = detail ? { detail } : {};
|
|
84
|
+
if (!/^Logged in/i.test(combined))
|
|
85
|
+
return { loggedIn: false, ...withDetail };
|
|
86
|
+
if (/API key/i.test(combined))
|
|
87
|
+
return { loggedIn: true, method: 'api-key', ...withDetail };
|
|
88
|
+
if (/ChatGPT/i.test(combined))
|
|
89
|
+
return { loggedIn: true, method: 'chatgpt', ...withDetail };
|
|
90
|
+
return { loggedIn: true, ...withDetail };
|
|
91
|
+
}
|
package/dist/browser.d.ts
CHANGED
|
@@ -24,12 +24,14 @@ export { parseMcpToolName } from './parsers/mcpToolName';
|
|
|
24
24
|
export type { McpToolNameParts } from './parsers/mcpToolName';
|
|
25
25
|
export { buildSessionContextSnapshot, calculateSessionContextPressure, createSessionContextProjector, } from './context/sessionContext';
|
|
26
26
|
export { ACCOUNT_PROVIDER_IDS, MODEL_PROVIDER_IDS, QUOTA_PROVIDER_IDS, RUNTIME_QUOTA_PROVIDER_IDS, SESSION_PROVIDER_IDS, } from './providerIds';
|
|
27
|
-
export type { AccountProviderId, ModelProviderId, QuotaProviderId, RuntimeQuotaProviderId, SessionProviderId,
|
|
27
|
+
export type { AccountProviderId, ModelProviderId, QuotaProviderId, RuntimeQuotaProviderId, SessionProviderId,
|
|
28
|
+
/** Same type as `ProviderId` from the package root and `sidekick-shared/node`. */
|
|
29
|
+
SessionProviderId as ProviderId, } from './providerIds';
|
|
28
30
|
export type { BuildSessionContextSnapshotOptions, SessionContextCapabilities, SessionContextLayerBreakdown, SessionContextPressure, SessionContextProjector, SessionContextSnapshot, SessionContextSource, SessionContextSourceType, } from './context/sessionContext';
|
|
29
31
|
export { assistantTurnEventsFromSessionEvents, extractTurnSubagents, isAssistantTurnSubagentTool, reasoningSummary, segmentAssistantTurn, } from './turns/assistantTurn';
|
|
30
32
|
export type { AssistantTurnEvent, AssistantTurnEventType, AssistantTurnProcess, AssistantTurnProcessStep, AssistantTurnProjection, AssistantTurnSubagent, AssistantTurnSubagentStatus, AssistantTurnTimelineItem, AssistantTurnToolRef, ExtractTurnSubagentsOptions, ReasoningSummary, SegmentAssistantTurnOptions, } from './turns/assistantTurn';
|
|
31
|
-
export { parseModelId, getModelPricing, resolveModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, } from './modelInfo';
|
|
32
|
-
export type { ModelPricing, CostTokenUsage, CostSource, CostProvenanceInput, CostWithProvenance, ModelProvider, ParsedModelId, ModelInfo, ModelDisplayInfo, ModelPricingMatch, ModelPricingProvenance, ModelPricingSource, ResolvedModelPricing, } from './modelInfo';
|
|
33
|
+
export { parseModelId, getModelPricing, resolveModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, canonicalModelId, isMovingModelAlias, registerModelPricing, clearRegisteredModelPricing, getPricingCatalogInfo, costSourceFromPricingProvenance, mergeCostComponents, } from './modelInfo';
|
|
34
|
+
export type { ModelPricing, ResolveModelPricingOptions, RegisterModelPricingOptions, PricingCatalogInfo, PricingCatalogSource, CostComponent, FormatCostOptions, CostTokenUsage, CostSource, CostProvenanceInput, CostWithProvenance, ModelProvider, ParsedModelId, ModelInfo, ModelDisplayInfo, ModelPricingMatch, ModelPricingProvenance, ModelPricingSource, ResolvedModelPricing, } from './modelInfo';
|
|
33
35
|
export { exportResolvedModelCatalog, importResolvedModelCatalog, registerModelAlias, resolveModelAlias, RESOLVED_MODEL_CATALOG_SCHEMA_VERSION, } from './modelCatalog';
|
|
34
36
|
export type { ImportResolvedModelCatalogResult, ResolvedModelCatalogEntry, ResolvedModelCatalogSnapshot, } from './modelCatalog';
|
|
35
37
|
export { BILLING_BLOCK_DURATION_MS, computeBillingBlocks, findActiveBillingBlock, } from './usage/billingBlocks';
|
|
@@ -40,14 +42,18 @@ export { applySessionSummary, isFileImported, markFileImported, removeSessionSum
|
|
|
40
42
|
export type { ApplySessionSummaryOptions, SessionSummaryFromStatsOptions } from './historicalStore';
|
|
41
43
|
export { FAILING_TOOL_TREND_ARROWS, failingToolTrend, mergeFailingToolWindows, } from './failingTools';
|
|
42
44
|
export type { FailingToolTrend, FailingToolTrendRow, FailingToolWindow } from './failingTools';
|
|
43
|
-
export { diagnoseProviderFailure, PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from './providerFailure';
|
|
44
|
-
export
|
|
45
|
+
export { diagnoseProviderFailure, PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, CODEX_ERROR_INFO_KINDS, decodeCodexErrorInfo, } from './providerFailure';
|
|
46
|
+
export { parseClaudeAuthStatus, parseCodexLoginStatus } from './authStatus';
|
|
47
|
+
export type { ParsedClaudeAuthStatus, ParsedCodexLoginStatus } from './authStatus';
|
|
48
|
+
export type { CodexErrorInfoKind, CodexErrorInfoSummary, ProviderCredentialKind, ProviderAuthenticationEvidence, ProviderFailureInput, ProviderFailureCode, ProviderRecoveryAction, ProviderRetryAfter, ProviderFailureEvidence, ProviderFailureDiagnosis, } from './providerFailure';
|
|
45
49
|
export type { ProviderServiceSeverity, ProviderServiceComponent, ProviderServiceIncident, ProviderServiceStatusFailureReason, ObservedProviderServiceStatus, UnavailableProviderServiceStatus, ProviderServiceStatus, FetchProviderServiceStatusOptions, } from './providerServiceStatusTypes';
|
|
46
50
|
export type { SessionEvent, ClaudeSessionEvent, MessageUsage, SessionMessage, SessionEventBase, MessageSessionEvent, UserSessionEvent, AssistantSessionEvent, SummarySessionEvent, SystemSessionEvent, ToolUseSessionEvent, ToolResultSessionEvent, TokenUsage, ToolCall, ToolAnalytics, TimelineEvent, PendingToolCall, TaskStatus as SessionTaskStatus, TrackedTask, TaskState, SubagentStats, PendingUserRequest, ResponseLatency, LatencyStats, PlanStep, PlanState, SessionStats, CompactionEvent, TruncationEvent, ContextAttribution, TurnAttribution, ContextSizePoint, PermissionMode, PermissionModeChange, } from './types/sessionEvent';
|
|
47
51
|
export type { CodexRolloutLine, CodexSessionMeta, CodexResponseItem, CodexMessageItem, CodexContentPart, CodexReasoningItem, CodexFunctionCallItem, CodexFunctionCallOutputItem, CodexLocalShellCallItem, CodexWebSearchCallItem, CodexCustomToolCallItem, CodexCustomToolCallOutputItem, CodexCompacted, CodexTurnContext, CodexEventMsg, CodexEvent, CodexTurnStartedEvent, CodexTurnCompleteEvent, CodexTaskStartedEvent, CodexTaskCompleteEvent, CodexTurnAbortedEvent, CodexTokenCountEvent, CodexTokenUsage, CodexRateLimits, CodexAgentMessageEvent, CodexAgentReasoningEvent, CodexUserMessageEvent, CodexExecCommandBeginEvent, CodexExecCommandEndEvent, CodexMcpToolCallBeginEvent, CodexMcpToolCallEndEvent, CodexErrorEvent, CodexContextCompactedEvent, CodexPatchAppliedEvent, CodexBackgroundEvent, CodexGenericEvent, CodexDbThread, CodexReasoningSummary, } from './types/codex';
|
|
48
52
|
export { isAggregateCodexLimit } from './types/codex';
|
|
49
53
|
export { CodexRolloutParser, DEFAULT_MAX_TRACKED_TOOL_CALLS, extractPatchFilePaths, normalizeCodexToolName, normalizeCodexToolInput, resolveCodexCallUsage, } from './parsers/codexParser';
|
|
50
|
-
export type { CodexRolloutParserOptions } from './parsers/codexParser';
|
|
54
|
+
export type { CodexPendingStats, CodexRolloutParserOptions } from './parsers/codexParser';
|
|
55
|
+
export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
|
|
56
|
+
export type { CodexRolloutOrigin } from './parsers/codexRolloutOrigin';
|
|
51
57
|
export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
|
|
52
58
|
export type { HumanPromptProvider } from './humanPrompt';
|
|
53
59
|
export { renderHandoffUrlTemplate } from './handoffUrl';
|