sidekick-shared 0.25.0 → 0.26.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 +57 -0
- package/README.md +49 -3
- package/dist/accountChanges.js +22 -3
- package/dist/accountRegistry.d.ts +22 -1
- package/dist/accountRegistry.js +37 -14
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +29 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +112 -24
- package/dist/node.d.ts +19 -0
- package/dist/node.js +47 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +3 -8
- package/dist/observedSessionCollector.js +10 -27
- package/dist/parsers/sessionActivityDetector.d.ts +24 -2
- package/dist/parsers/sessionActivityDetector.js +95 -17
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providers/claudeCode.js +11 -96
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +94 -259
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/factory.d.ts +6 -0
- package/dist/providers/openCode.d.ts +2 -0
- package/dist/providers/openCode.js +105 -121
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +30 -0
- package/dist/quota.d.ts +30 -3
- package/dist/quota.js +35 -3
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaResolver.d.ts +81 -0
- package/dist/quotaResolver.js +213 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +139 -0
- package/dist/schemas/quota.js +17 -2
- package/dist/schemas/quotaHistory.d.ts +1 -0
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionPreviews.d.ts +7 -1
- package/dist/sessionPreviews.js +15 -3
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +2 -0
- package/dist/types/observedSessionV1.js +75 -18
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,63 @@ 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.26.0] - 2026-09-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `summarizeTokens()`, `sumTokenTotals()`, `TOKEN_TOTAL_LABEL`, and `TOKEN_CONTEXT_LABEL` define the one token vocabulary every Sidekick surface uses: `total` is input + output + cache writes + cache reads, `context` is input + cache; browser-safe
|
|
13
|
+
- `AggregatedTokens` gains `costUsd`, `costProvenance` (`reported` | `estimated` | `mixed` | `unpriced` | `none`), `reportedCostUsd`, `estimatedCostUsd`, and `unpricedCalls`; `describeCostProvenance()` and `classifyCostProvenance()` are exported; `CanonicalSessionTranscript.usage` adds `pricedCostUsd` and `unpricedEvents` (schema updated)
|
|
14
|
+
- `parseClaudeStatuslinePayload()` and `quotaFromStatuslinePayload()` (also on the `sidekick-shared/statusline` subpath, which now exports `writeQuotaSnapshot`, `appendQuotaHistorySample`, and `getWorkspaceIdFromPath`) read the JSON Claude Code pipes to a status-line command; `QuotaState.source` and quota-history samples accept `statusline`
|
|
15
|
+
- `formatStatusline({ live })` renders context %, session cost, prompt-cache hit rate, the seven-day window, and the snapshot age when it is older than five minutes
|
|
16
|
+
- `readQuotaSnapshot()` reports `ageMs` and `freshness`; `classifyQuotaFreshness()`, `formatQuotaAge()`, `QUOTA_FRESH_MAX_AGE_MS`, and `QUOTA_AGING_MAX_AGE_MS` are exported and the quota schema accepts the new fields
|
|
17
|
+
- `getScheduledPeakHoursState()`, `PEAK_HOURS_DESCRIPTION`, `PEAK_HOURS_CACHE_MS`, and `PeakHoursState.source` (`promoclock` | `schedule`); `fetchPeakHoursStatus({ force })` bypasses the ten-minute cache
|
|
18
|
+
- `evaluateQuotaThresholds()`, `describeQuotaThresholdAlert()`, and `DEFAULT_QUOTA_THRESHOLDS` for once-per-reset-window threshold alerts
|
|
19
|
+
- `formatLocalDateKey()`, `parseLocalDateKey()`, and `addLocalDays()` (root and browser entry points); `hydratePricingCatalog({ offline })`; `pruneSnapshots()` and `MAX_SNAPSHOT_FILES`; `getActiveAccountStatus(error, { selfHeal })`, mirrored on `resolveActiveClaudeAccount()` and `resolveActiveCodexAccount()`; `_resetProviderDetectionCache()` and `_resetPeakHoursCache()` for tests
|
|
20
|
+
- `resolveQuota({ providerId, workspacePath, preferFresh, allowApi, … })` resolves Claude, Codex, or z.ai quota with one precedence — a persisted sample younger than `QUOTA_FRESH_MAX_AGE_MS` (any origin), then session logs, then the provider API (persisted on success), then an aging or stale sample with the API failure attached — returning `ResolvedQuota` with `resolution`, `source`, `capturedSource`, `freshness`, and `ageMs`; every side effect is injectable
|
|
21
|
+
- `QuotaState.capturedSource` (`api` | `session` | `statusline`) records where a cached sample came from; `readQuotaSnapshot()` fills it and `writeQuotaSnapshot()` preserves it instead of storing `cache`; the quota schema accepts it; `QuotaSnapshotRecord`, `enrichCodexQuota()`, and `enrichZaiQuota()` are exported; `fetchQuota(token, { fetchImpl })` accepts an injected fetch
|
|
22
|
+
- `computeSessionFileStats(events, options)` and `readSessionFileStats(provider, sessionPath, { resolveLabel })` compute `SessionFileStats` for every provider over the shared `EventAggregator`; `firstUserPrompt(events)` derives a session label from events already in memory. `SessionFileStats` gains `availability` (`full` | `partial` | `unavailable`), `unavailableReason`, `costUsd`, `costProvenance`, `unpricedCalls`, and `toolFailures`; `reportedCost` is deprecated (same value as `costUsd`)
|
|
23
|
+
- `collectUsageEvents({ providers, since, until, workspacePath })` reads every session touched in a window once through its provider reader and returns priced usage events, caching raw token counts per session under `<configDir>/usage-cache/` keyed by size and mtime (priced at load, so a catalog refresh never leaves stale costs); `pruneUsageCache()`, `getUsageCacheDir()`, `USAGE_CACHE_VERSION`, and `MAX_USAGE_CACHE_FILES` are exported
|
|
24
|
+
- `computeBillingBlocks(events, { now })` and `findActiveBillingBlock()` (root and browser entry points) group usage events into ccusage-style five-hour blocks aligned to the UTC hour, with cache-inclusive totals, cost provenance, per-model usage, burn rate, remaining time, and end-of-block projections; `BILLING_BLOCK_DURATION_MS` is exported
|
|
25
|
+
- `bucketUsage(events, { granularity, utc, weekStartsOn, groupBy })`, `summarizeUsageRows()`, `usageBucketKey()`, and `weekKey()` (root and browser entry points) bucket usage events by event time into day, week, month, or session rows with provider, project, and model dimensions, cache-inclusive totals, and cost provenance
|
|
26
|
+
- `applySessionSummary(store, summary)`, `removeSessionSummary()`, `markFileImported()`, `isFileImported()`, and `sessionSummaryFromStats()` (root and browser entry points) are the pure `historical-data.json` mutations ported from the VS Code extension, so the CLI and the extension credit sessions identically; `importSessionHistory({ providers, since, isImported, applySummary, markImported })` reads every finished session once through `readSessionFileStats()` and hands summaries to the caller's store, skipping files modified within `ACTIVE_SESSION_MTIME_THRESHOLD_MS`
|
|
27
|
+
- `SessionFileStats.modelUsage` entries gain `costUsd` and `priced`
|
|
28
|
+
- `classifySessionActivity({ events, mtimeMs })` classifies a session from events already in memory with the same states, reasons, grace period, and staleness rule as `detectSessionActivity()`; `refreshSessionActivityState()` accepts the previous reason so a session that was active only by grace period ends once the grace period lapses; `getObservedActivityReason()` exposes the reason behind an observed session's activity; `SESSION_ACTIVITY_GRACE_PERIOD_MS` and `SESSION_ACTIVITY_STALENESS_MS` are exported
|
|
29
|
+
- `readSessionReportInputs(provider, sessionPath)` reads a session once and returns its events, aggregator metrics, and report transcript; `resolveSessionPath(provider, workspacePath, sessionId?)` is the session lookup `createWatcher()` uses (exact id, unique prefix, or most recent); `sessionFingerprintParts()` and `fingerprintString()` are exported alongside `fileFingerprintParts()`; `SessionMessage.stop_reason` is preserved by the event schema
|
|
30
|
+
- `walkRolloutFiles(roots, { maxDepth, maxFiles, limit, sessionId })` and `walkRolloutFilesAsync()` are the one capped walker for Codex rollout files: newest-dated directories first, each file stat'ed once, results newest first with size and session id, an early exit for `limit`, and defaults of six levels and 20 000 files; `findSessionFilesWithStats()` returns a Claude Code session directory's files with the stat the listing already paid for
|
|
31
|
+
- `writeStateFile()`, `readStateFile()`, `getStateFilePath()`, `quotaToStateFile()`, `billingBlockToStateFile()`, `STATE_FILE_SCHEMA_VERSION`, and the `SidekickStateFile` types (root, node, and `statusline` entry points) maintain the public, versioned `<configDir>/state.json` for external tools — account, quota windows with freshness, context usage, session cost, active billing block — writing atomically only when the content changed; `sidekickStateFileSchema` and its sub-schemas are exported from `sidekick-shared/schemas`
|
|
32
|
+
- `SessionPreviewListResult.hasMore` reports whether `listSessionPreviewsAsync()` left candidates beyond `limit`, so callers can offer a larger limit only when one would show more
|
|
33
|
+
- `mergeFailingToolWindows()`, `failingToolTrend()`, and `FAILING_TOOL_TREND_ARROWS` (root and `browser` entry points) merge the 7-day and 30-day `getTopFailingTools()` windows into per-tool rows with a trend, shared by `sidekick stats` and the VS Code Health tab
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- The HTML report and session dump label cost by provenance and render unpriced sessions as `—` with the unpriced call count; their "Total" row is cache-inclusive and labelled "Total (incl. cache)"
|
|
38
|
+
- `calculateCompactionLedger()` prices re-establishment through `calculateNormalizedUsageCost()` instead of the deprecated additive-reasoning path
|
|
39
|
+
- Codex `readSessionStats()` computes context as uncached + cached input, matching `CodexProvider.computeContextSize()`
|
|
40
|
+
- OpenCode `workspaceMatches` only matches sessions started in the workspace or a subdirectory of it
|
|
41
|
+
- `readQuotaHistoryDailyBuckets()` keys buckets by local calendar day
|
|
42
|
+
- `detectProvider()` and `getAllDetectedProviders()` share one implementation memoised for five seconds
|
|
43
|
+
- `loadObservedContextWindows()` ignores entries older than `OBSERVED_CONTEXT_WINDOW_TTL_MS` (30 days)
|
|
44
|
+
- `saveSnapshot()` writes through the fsynced atomic writer and prunes the directory to the newest 200 files every 25 saves; quota-history append and prune run under a per-file cross-process lock with fsync; the pricing catalog cache is written atomically
|
|
45
|
+
- `DEFAULT_PEAK_HOURS_TIMEOUT_MS` is 4 000 ms; `fetchPeakHoursStatus()` falls back to the schedule rather than returning `unavailable: true`
|
|
46
|
+
- `listSessionPreviews({ since })` throws `RangeError` on an unparseable cutoff
|
|
47
|
+
- `AggregatedTokens.reportedCost` is deprecated (same value as `costUsd`); `SNAPSHOT_SCHEMA_VERSION` is 5
|
|
48
|
+
- Claude Code, Codex, and OpenCode `readSessionStats()` share one implementation: per-model `tokens` are cache-inclusive (`summarizeTokens().total`) for every provider, Claude sessions report catalog-estimated cost and a tool success/failure split instead of `reportedCost: 0`, OpenCode compaction and truncation counts come from the aggregator instead of hardcoded zeros, a missing or unreadable source reports `availability: 'unavailable'` with a reason instead of silent zeros, and the label is taken from the events already read (Codex and OpenCode still prefer their database title) so the file is never opened twice
|
|
49
|
+
- OpenCode's file-backed reader implements `seekTo()` by message count (its `getPosition()` unit), so a reader restored from a snapshot emits only newer messages instead of replaying the whole session; the database-backed reader's `exists()` checks the session row (memoised for 30 s, treating a transient query failure as present) instead of always returning true
|
|
50
|
+
- `createProviderSessionAdapterV1().read()` reads a session once: stats, transcript, activity, and the pending request all come from one flushed reader pass instead of three separate file reads, and activity is classified from the events, which also fixes Codex sessions (whose raw lines never matched the Claude-only tail patterns) and database-backed OpenCode sessions (which always read as ended). `watch()` checks the size/mtime fingerprint before re-reading and refreshes only the activity in place while the content is unchanged, so a 2 s poll on an idle session costs a `stat`
|
|
51
|
+
- `ObservedSessionCollector` cache refreshes demote a session that was active only by grace period to ended after five seconds, matching a fresh classification
|
|
52
|
+
- The Codex provider's six recursive walkers (session discovery, listing, id lookup, project folders, subagent scan) and the quota resolver's separate copy all use the capped walker; `getAllProjectFolders()` reads `session_meta` only for rollouts newer than the SQLite index's mtime when the index answered, and every discovery path uses the per-path cwd cache (now bounded to 5 000 entries) instead of re-reading the first line of every rollout; `scanSubagents()` stops after the 50 newest-dated rollouts instead of enumerating the whole history; the local Codex quota scan walks at most `maxSessionFiles` rollouts and no longer stats every path a second time to sort it; `ClaudeCodeProvider.listAllSessionFiles()` no longer stats every session twice
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- `onAccountsChanged()` applies a later subscriber's faster `pollIntervalMs` and resets the interval after the last unsubscribe
|
|
57
|
+
- `OpenCodeProvider.dispose()` resets `dbStatus`; `CodexProvider.listSessionFilesAsync()` records its filesystem fallback for `getLastOperationStatus()`
|
|
58
|
+
- Observed-session cache hits could report a session as active for up to five minutes when a fresh parse would have said ended (the 0.25.0 grace-period drift note)
|
|
59
|
+
- `addCurrentAccount()` and `removeAccount()` hold the registry lock across their whole read-modify-write (new `withSavedAccountRegistryLock()` and `replaceSavedAccountProfilesUnlocked()`), so a concurrent add from another process is no longer dropped when the Claude profile set is replaced
|
|
60
|
+
- `resolveActiveClaudeAccount()`'s self-heal of the active pointer no longer fires an accounts-changed notification from a read path (`setActiveSavedAccount(..., { silent: true })`)
|
|
61
|
+
- `readSessionPreview()` / `readSessionPreviewAsync()` honour `maxPrefixBytes: 0` as "skip the prefix scan" (`firstTimestamp` and `workspacePath` come back null, the file is not opened) instead of flooring the budget to one byte
|
|
62
|
+
- `createSessionProviders()` documents that `diagnostics` is live (updated in place as providers run) and should be copied for a snapshot
|
|
63
|
+
- The packaging contract tests create their scratch projects under the OS temp directory instead of inside the package, so an interrupted run cannot leave a recursive `node_modules/sidekick-shared` link behind
|
|
64
|
+
|
|
8
65
|
## [0.25.0] - 2026-08-18
|
|
9
66
|
|
|
10
67
|
### Added
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
|
|
|
9
9
|
|
|
10
10
|
## Recent additions
|
|
11
11
|
|
|
12
|
+
- **One token vocabulary and cost provenance (0.26.0)** — `summarizeTokens()` gives every surface the same `total` (input + output + cache writes + cache reads) and `context` (input + cache) figures; `AggregatedTokens.costUsd` carries `costProvenance` so reports can say whether a cost was provider-reported or estimated; `parseClaudeStatuslinePayload()` / `quotaFromStatuslinePayload()` turn Claude Code's status-line JSON into an official `statusline` quota source; `readQuotaSnapshot()` reports `ageMs` / `freshness` and keeps the sample's origin as `capturedSource`; `resolveQuota()` is the one quota resolution path (fresh persisted sample → session logs → provider API → older sample) shared by the CLI, the MCP server, and both dashboards; `readSessionFileStats()` / `computeSessionFileStats()` give every provider one `readSessionStats()` over the shared aggregator, with `availability`, `costProvenance`, and `toolFailures` on `SessionFileStats`; `collectUsageEvents()` reads priced usage events from session logs with a per-session cache, `computeBillingBlocks()` groups them into five-hour billing blocks, and `bucketUsage()` turns them into day, week, month, or session rows; `applySessionSummary()` and `importSessionHistory()` let any host fold finished sessions into `historical-data.json` the way the extension does; observed sessions are read once per discovery, re-read only when their fingerprint changes, and classified from events by `classifySessionActivity()`; `readSessionReportInputs()` derives report metrics and transcript from one read; `writeStateFile()` / `readStateFile()` (also on the `statusline` subpath) maintain the public, versioned `state.json` for external tools, validated by `sidekickStateFileSchema`; `getScheduledPeakHoursState()` and `evaluateQuotaThresholds()` are shared by the CLI and extension dashboards.
|
|
12
13
|
- **Host-safe APIs (0.25.0)** — `listSessionPreviewsAsync()` / `readSessionPreviewAsync()` read previews with bounded concurrency and cooperative yielding; `ObservedSessionCollector.subscribe()` and `SessionMonitor.subscribe()` replace host polling loops with debounced change batches; `createSessionProviders({ onDiagnostic })` constructs every usable provider with structured diagnostics instead of throwing; `findSessionById()` resolves one session without scanning; `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` and `registerModelAlias()` transfer context/pricing resolutions across realms; `onAccountsChanged()` reports login/logout/switch events; provider constructors perform no environment I/O, and quota pollers stay dormant without an account.
|
|
13
14
|
- **Native session-file helpers** — `encodeClaudeWorkspacePath()` / `getClaudeSessionDirectory()` locate Claude Code's `~/.claude/projects/` directories with the real on-disk encoding, `findCodexRolloutFile()` resolves a Codex session id to its rollout path, `readCodexHistory()` tail-reads `~/.codex/history.jsonl`, `listSessionPreviews()` / `readSessionPreview()` build a cheap stat-first recent-sessions index across providers, and `parseMcpToolName()` splits `mcp__<server>__<tool>` identifiers (browser-safe).
|
|
14
15
|
- **Non-blocking account APIs & locked sync writers** — every login/switch entry point gains an async variant (`switchAccountAsync()`, `getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `prepareCodexAccountAsync()`, `finalizeCodexAccountAsync()`, `switchToCodexAccountAsync()`) that keeps `codex` CLI probes off the event loop, and synchronous callers get the same cross-process store lock via `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()`, with progress-based timeouts and abandoned-lock reclaim.
|
|
@@ -104,7 +105,7 @@ returns the platform default that both overrides bypass.
|
|
|
104
105
|
| **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 |
|
|
105
106
|
| **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
|
|
106
107
|
| **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` exposes the per-provider session probe |
|
|
107
|
-
| **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,
|
|
108
|
+
| **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, quota, account status, account management, quota history, and the public `state.json` contract (`sidekickStateFileSchema`) — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Account and quota APIs guarantee results valid against the same-release schemas. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
|
|
108
109
|
| **Extractors** | Pure functions for single-event processing: `extractTokenUsage()`, `extractToolCall()` (top-level `tool_use`), `extractToolCalls()` (assistant content blocks), plus Node-only session asset extraction (`gatherAssetsForCwd()`, `readClaudeAssets()`, `readCodexAssets()`) for URLs, file paths, commands, and plans |
|
|
109
110
|
| **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5 / Opus 4.8 / Opus 4.7 / Sonnet 4.7 1M and GPT-5.x variants), pricing tables with optional LiteLLM hydration, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids |
|
|
110
111
|
| **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback. Stays dormant while no account exists and wakes through the account-change signal |
|
|
@@ -130,7 +131,7 @@ returns the platform default that both overrides bypass.
|
|
|
130
131
|
| `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
|
|
131
132
|
| `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
|
|
132
133
|
| `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
|
|
133
|
-
| `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, account management, quota history) — fs-free, no Node builtins.
|
|
134
|
+
| `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. |
|
|
134
135
|
| `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
|
|
135
136
|
|
|
136
137
|
### Browser / webview runtimes
|
|
@@ -238,6 +239,32 @@ const estimate = estimateTextTokens(sourceCode, {
|
|
|
238
239
|
|
|
239
240
|
`sidekick-fallback-v1` counts Latin/source-code text at 3.5 characters per token, CJK code points at one token, emoji at two, and other non-ASCII code points at one. Empty input is zero. An injected finite, nonnegative exact result takes precedence.
|
|
240
241
|
|
|
242
|
+
### Read session stats for any provider
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { createSessionProviders, readSessionFileStats } from 'sidekick-shared';
|
|
246
|
+
|
|
247
|
+
const { providers } = createSessionProviders();
|
|
248
|
+
for (const provider of providers) {
|
|
249
|
+
for (const sessionPath of provider.findAllSessions(process.cwd())) {
|
|
250
|
+
const stats = provider.readSessionStats(sessionPath); // same as readSessionFileStats(provider, sessionPath)
|
|
251
|
+
if (stats.availability === 'unavailable') {
|
|
252
|
+
console.warn(`${provider.id}: ${stats.unavailableReason}`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
console.log(
|
|
256
|
+
stats.label,
|
|
257
|
+
stats.modelUsage,
|
|
258
|
+
stats.costUsd,
|
|
259
|
+
stats.costProvenance,
|
|
260
|
+
stats.toolFailures,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Every provider computes `SessionFileStats` the same way — one reader pass through the shared `EventAggregator` — so per-model `tokens` are cache-inclusive, `costUsd` carries `costProvenance`, `toolFailures` splits out failed calls, and compaction and truncation counts are real for Claude Code, Codex, and OpenCode alike. `computeSessionFileStats(events, options)` does the same for events you already hold, and `firstUserPrompt(events)` derives the label.
|
|
267
|
+
|
|
241
268
|
### Read canonical session history
|
|
242
269
|
|
|
243
270
|
```typescript
|
|
@@ -308,7 +335,7 @@ Hosts with a UI loop should prefer the async variants from `sidekick-shared/node
|
|
|
308
335
|
```typescript
|
|
309
336
|
import { listSessionPreviewsAsync, readSessionPreviewAsync } from 'sidekick-shared/node';
|
|
310
337
|
|
|
311
|
-
const { previews, diagnostics } = await listSessionPreviewsAsync(providers, { limit: 10 });
|
|
338
|
+
const { previews, diagnostics, hasMore } = await listSessionPreviewsAsync(providers, { limit: 10 });
|
|
312
339
|
const { preview } = await readSessionPreviewAsync(provider, filePath);
|
|
313
340
|
```
|
|
314
341
|
|
|
@@ -448,6 +475,25 @@ if (!peak.unavailable && peak.isPeak) {
|
|
|
448
475
|
}
|
|
449
476
|
```
|
|
450
477
|
|
|
478
|
+
### Resolve quota with one precedence
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
import { resolveQuota } from 'sidekick-shared';
|
|
482
|
+
|
|
483
|
+
// Fresh persisted sample (status line, session, or API; under five minutes) →
|
|
484
|
+
// session logs (Codex rollouts) → provider API → older sample labelled by age.
|
|
485
|
+
const quota = await resolveQuota({ providerId: 'claude-code' });
|
|
486
|
+
console.log(quota.resolution); // 'snapshot-fresh' | 'session' | 'api' | 'snapshot-aging' | 'snapshot-stale' | 'unavailable'
|
|
487
|
+
console.log(quota.source, quota.capturedSource, quota.freshness, quota.ageMs);
|
|
488
|
+
|
|
489
|
+
// `--refresh` semantics: ask the API first.
|
|
490
|
+
await resolveQuota({ providerId: 'codex', workspacePath: process.cwd(), preferFresh: false });
|
|
491
|
+
// Paint from local samples only (a poller will fetch live data next).
|
|
492
|
+
await resolveQuota({ providerId: 'claude-code', allowApi: false });
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
`resolveQuota()` is what `sidekick quota`, `quota --all`, the MCP `get_quota_status` tool, and both dashboards call, so every surface reports the same numbers and the same `resolution`. Every side effect is injectable (`readSnapshot`, `writeSnapshot`, `fetchImpl`, `getClaudeAccessToken`, the account resolvers) for tests.
|
|
496
|
+
|
|
451
497
|
### Fetch subscription quota
|
|
452
498
|
|
|
453
499
|
```typescript
|
package/dist/accountChanges.js
CHANGED
|
@@ -48,7 +48,8 @@ let pollTimer;
|
|
|
48
48
|
let stopRawListener;
|
|
49
49
|
let lastFingerprint;
|
|
50
50
|
let debounceTimer;
|
|
51
|
-
|
|
51
|
+
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
52
|
+
let activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
52
53
|
/**
|
|
53
54
|
* Observe login, logout, and active-account switches. Local Sidekick mutations
|
|
54
55
|
* signal immediately; filesystem watchers cover external CLIs; a low-frequency
|
|
@@ -56,8 +57,17 @@ let activePollIntervalMs = 30_000;
|
|
|
56
57
|
*/
|
|
57
58
|
function onAccountsChanged(listener, options = {}) {
|
|
58
59
|
listeners.add(listener);
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// The fastest interval any live subscriber asked for wins; a later, faster
|
|
61
|
+
// subscriber restarts the timer rather than being silently ignored.
|
|
62
|
+
const requestedPollMs = Math.max(1_000, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
63
|
+
if (!stopRawListener) {
|
|
64
|
+
activePollIntervalMs = requestedPollMs;
|
|
65
|
+
ensureMonitoring();
|
|
66
|
+
}
|
|
67
|
+
else if (requestedPollMs < activePollIntervalMs) {
|
|
68
|
+
activePollIntervalMs = requestedPollMs;
|
|
69
|
+
restartPollTimer();
|
|
70
|
+
}
|
|
61
71
|
if (options.emitCurrent)
|
|
62
72
|
emitIfChanged('poll', true, listener);
|
|
63
73
|
return {
|
|
@@ -93,9 +103,17 @@ function ensureMonitoring() {
|
|
|
93
103
|
// Missing/unwatchable directories are covered by the catch-up poll.
|
|
94
104
|
}
|
|
95
105
|
}
|
|
106
|
+
startPollTimer();
|
|
107
|
+
}
|
|
108
|
+
function startPollTimer() {
|
|
96
109
|
pollTimer = setInterval(() => emitIfChanged('poll'), activePollIntervalMs);
|
|
97
110
|
pollTimer.unref?.();
|
|
98
111
|
}
|
|
112
|
+
function restartPollTimer() {
|
|
113
|
+
if (pollTimer)
|
|
114
|
+
clearInterval(pollTimer);
|
|
115
|
+
startPollTimer();
|
|
116
|
+
}
|
|
99
117
|
function stopMonitoring() {
|
|
100
118
|
stopRawListener?.();
|
|
101
119
|
stopRawListener = undefined;
|
|
@@ -109,6 +127,7 @@ function stopMonitoring() {
|
|
|
109
127
|
clearTimeout(debounceTimer);
|
|
110
128
|
debounceTimer = undefined;
|
|
111
129
|
lastFingerprint = undefined;
|
|
130
|
+
activePollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
112
131
|
}
|
|
113
132
|
function watchTargets() {
|
|
114
133
|
return Array.from(new Set([(0, accountRegistry_1.getAccountsDir)(), path.join(os.homedir(), '.claude'), (0, codexProfiles_1.getSystemCodexHome)()]));
|
|
@@ -41,11 +41,32 @@ export interface ResolvedActiveAccount {
|
|
|
41
41
|
source: 'live' | 'registry' | 'none';
|
|
42
42
|
}
|
|
43
43
|
export declare function getAccountsDir(): string;
|
|
44
|
+
interface RegistryWriteOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Skip the local change notification. Only for writes that change no
|
|
47
|
+
* observable account state, such as re-pointing the active pointer at the
|
|
48
|
+
* profile that already matches the live login (self-heal): a read path
|
|
49
|
+
* must not wake every subscriber.
|
|
50
|
+
*/
|
|
51
|
+
silent?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run `operation` while holding the registry lock. For callers that need a
|
|
55
|
+
* read-modify-write across several steps (the legacy Claude facade's
|
|
56
|
+
* add/remove); inside it use the `*Unlocked` writers, never the locking ones:
|
|
57
|
+
* the lock is not re-entrant.
|
|
58
|
+
*/
|
|
59
|
+
export declare function withSavedAccountRegistryLock<T>(operation: () => T): T;
|
|
44
60
|
export declare function readSavedAccountRegistry(): SavedAccountRegistry | null;
|
|
45
61
|
export declare function writeSavedAccountRegistry(registry: SavedAccountRegistry): void;
|
|
46
62
|
export declare function listSavedAccountProfiles(providerId?: AccountProviderId): SavedAccountProfile[];
|
|
47
63
|
export declare function getActiveSavedAccount(providerId: AccountProviderId): SavedAccountProfile | null;
|
|
48
64
|
export declare function upsertSavedAccountProfile(profile: SavedAccountProfile): SavedAccountRegistry;
|
|
49
|
-
export declare function setActiveSavedAccount(providerId: AccountProviderId, accountId: string | null): SavedAccountRegistry;
|
|
65
|
+
export declare function setActiveSavedAccount(providerId: AccountProviderId, accountId: string | null, options?: RegistryWriteOptions): SavedAccountRegistry;
|
|
50
66
|
export declare function replaceSavedAccountProfiles(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
|
|
67
|
+
/**
|
|
68
|
+
* Same as {@link replaceSavedAccountProfiles} without taking the registry
|
|
69
|
+
* lock: for callers already inside {@link withSavedAccountRegistryLock}.
|
|
70
|
+
*/
|
|
71
|
+
export declare function replaceSavedAccountProfilesUnlocked(providerId: AccountProviderId, accounts: SavedAccountProfile[], activeId: string | null): SavedAccountRegistry;
|
|
51
72
|
export declare function removeSavedAccountProfile(providerId: AccountProviderId, accountId: string): SavedAccountProfile | null;
|
package/dist/accountRegistry.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ACCOUNT_PROVIDER_IDS = void 0;
|
|
37
37
|
exports.getAccountsDir = getAccountsDir;
|
|
38
|
+
exports.withSavedAccountRegistryLock = withSavedAccountRegistryLock;
|
|
38
39
|
exports.readSavedAccountRegistry = readSavedAccountRegistry;
|
|
39
40
|
exports.writeSavedAccountRegistry = writeSavedAccountRegistry;
|
|
40
41
|
exports.listSavedAccountProfiles = listSavedAccountProfiles;
|
|
@@ -42,6 +43,7 @@ exports.getActiveSavedAccount = getActiveSavedAccount;
|
|
|
42
43
|
exports.upsertSavedAccountProfile = upsertSavedAccountProfile;
|
|
43
44
|
exports.setActiveSavedAccount = setActiveSavedAccount;
|
|
44
45
|
exports.replaceSavedAccountProfiles = replaceSavedAccountProfiles;
|
|
46
|
+
exports.replaceSavedAccountProfilesUnlocked = replaceSavedAccountProfilesUnlocked;
|
|
45
47
|
exports.removeSavedAccountProfile = removeSavedAccountProfile;
|
|
46
48
|
const fs = __importStar(require("fs"));
|
|
47
49
|
const path = __importStar(require("path"));
|
|
@@ -62,9 +64,20 @@ function getRegistryLockPath() {
|
|
|
62
64
|
function ensureAccountsDir() {
|
|
63
65
|
fs.mkdirSync(getAccountsDir(), { recursive: true, mode: 0o700 });
|
|
64
66
|
}
|
|
65
|
-
function writeRegistryUnlocked(registry) {
|
|
67
|
+
function writeRegistryUnlocked(registry, options = {}) {
|
|
66
68
|
(0, atomic_1.atomicWriteJsonSync)(getRegistryPath(), normalizeRegistry(registry));
|
|
67
|
-
(
|
|
69
|
+
if (!options.silent)
|
|
70
|
+
(0, accountChangeSignal_1._emitAccountsChanged)('local');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Run `operation` while holding the registry lock. For callers that need a
|
|
74
|
+
* read-modify-write across several steps (the legacy Claude facade's
|
|
75
|
+
* add/remove); inside it use the `*Unlocked` writers, never the locking ones:
|
|
76
|
+
* the lock is not re-entrant.
|
|
77
|
+
*/
|
|
78
|
+
function withSavedAccountRegistryLock(operation) {
|
|
79
|
+
ensureAccountsDir();
|
|
80
|
+
return (0, atomic_1.withFileLockSync)(getRegistryLockPath(), operation);
|
|
68
81
|
}
|
|
69
82
|
/**
|
|
70
83
|
* Serializes registry read-modify-write cycles across processes. Without the
|
|
@@ -73,13 +86,13 @@ function writeRegistryUnlocked(registry) {
|
|
|
73
86
|
* mutation. Lock ordering: callers that also hold a provider auth-swap lock
|
|
74
87
|
* take that lock first and this one inside it, never the reverse.
|
|
75
88
|
*/
|
|
76
|
-
function mutateSavedAccountRegistry(mutator) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
function mutateSavedAccountRegistry(mutator, options = {}) {
|
|
90
|
+
return withSavedAccountRegistryLock(() => mutateSavedAccountRegistryUnlocked(mutator, options));
|
|
91
|
+
}
|
|
92
|
+
function mutateSavedAccountRegistryUnlocked(mutator, options = {}) {
|
|
93
|
+
const next = mutator(readSavedAccountRegistry() ?? createEmptyRegistry());
|
|
94
|
+
writeRegistryUnlocked(next, options);
|
|
95
|
+
return next;
|
|
83
96
|
}
|
|
84
97
|
function createEmptyRegistry() {
|
|
85
98
|
return {
|
|
@@ -187,20 +200,30 @@ function upsertSavedAccountProfile(profile) {
|
|
|
187
200
|
return registry;
|
|
188
201
|
});
|
|
189
202
|
}
|
|
190
|
-
function setActiveSavedAccount(providerId, accountId) {
|
|
203
|
+
function setActiveSavedAccount(providerId, accountId, options = {}) {
|
|
191
204
|
return mutateSavedAccountRegistry((registry) => {
|
|
192
205
|
registry.activeByProvider[providerId] = accountId;
|
|
193
206
|
return registry;
|
|
194
|
-
});
|
|
207
|
+
}, options);
|
|
195
208
|
}
|
|
196
|
-
function
|
|
197
|
-
return
|
|
209
|
+
function replaceProfiles(providerId, accounts, activeId) {
|
|
210
|
+
return (registry) => {
|
|
198
211
|
registry.accounts = registry.accounts
|
|
199
212
|
.filter((account) => account.providerId !== providerId)
|
|
200
213
|
.concat(accounts);
|
|
201
214
|
registry.activeByProvider[providerId] = activeId;
|
|
202
215
|
return registry;
|
|
203
|
-
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function replaceSavedAccountProfiles(providerId, accounts, activeId) {
|
|
219
|
+
return mutateSavedAccountRegistry(replaceProfiles(providerId, accounts, activeId));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Same as {@link replaceSavedAccountProfiles} without taking the registry
|
|
223
|
+
* lock: for callers already inside {@link withSavedAccountRegistryLock}.
|
|
224
|
+
*/
|
|
225
|
+
function replaceSavedAccountProfilesUnlocked(providerId, accounts, activeId) {
|
|
226
|
+
return mutateSavedAccountRegistryUnlocked(replaceProfiles(providerId, accounts, activeId));
|
|
204
227
|
}
|
|
205
228
|
function removeSavedAccountProfile(providerId, accountId) {
|
|
206
229
|
ensureAccountsDir();
|
package/dist/accountStatus.d.ts
CHANGED
|
@@ -10,10 +10,18 @@ export interface ActiveAccountStatus {
|
|
|
10
10
|
codex: ActiveProviderAccountStatus;
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
|
+
export interface ActiveAccountStatusOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Re-point a stale saved-account pointer at the live identity when they
|
|
16
|
+
* disagree (default true). Hot paths such as the status line pass `false`
|
|
17
|
+
* so they never write; an ordinary command repairs the pointer later.
|
|
18
|
+
*/
|
|
19
|
+
selfHeal?: boolean;
|
|
20
|
+
}
|
|
13
21
|
/**
|
|
14
22
|
* Reads active Claude Code and Codex account status in one filesystem pass.
|
|
15
23
|
*
|
|
16
24
|
* If reading either provider throws, the result still has provider-shaped
|
|
17
25
|
* fields so startup flows can render a consistent "not configured" state.
|
|
18
26
|
*/
|
|
19
|
-
export declare function getActiveAccountStatus(error?: string): ActiveAccountStatus;
|
|
27
|
+
export declare function getActiveAccountStatus(error?: string, options?: ActiveAccountStatusOptions): ActiveAccountStatus;
|
package/dist/accountStatus.js
CHANGED
|
@@ -12,10 +12,10 @@ function errorMessage(error) {
|
|
|
12
12
|
* If reading either provider throws, the result still has provider-shaped
|
|
13
13
|
* fields so startup flows can render a consistent "not configured" state.
|
|
14
14
|
*/
|
|
15
|
-
function getActiveAccountStatus(error) {
|
|
15
|
+
function getActiveAccountStatus(error, options = {}) {
|
|
16
16
|
try {
|
|
17
|
-
const claudeAccount = (0, accounts_1.resolveActiveClaudeAccount)();
|
|
18
|
-
const codexAccount = (0, codexProfiles_1.resolveActiveCodexAccount)();
|
|
17
|
+
const claudeAccount = (0, accounts_1.resolveActiveClaudeAccount)(options);
|
|
18
|
+
const codexAccount = (0, codexProfiles_1.resolveActiveCodexAccount)(options);
|
|
19
19
|
const claude = claudeAccount.source !== 'none'
|
|
20
20
|
? {
|
|
21
21
|
present: true,
|
package/dist/accounts.d.ts
CHANGED
|
@@ -50,5 +50,7 @@ export declare function getActiveAccount(): AccountEntry | null;
|
|
|
50
50
|
* re-pointed so registry-keyed data tracks reality too. Never creates or deletes
|
|
51
51
|
* profiles; an unknown live account is shown as-is with no label and no write.
|
|
52
52
|
*/
|
|
53
|
-
export declare function resolveActiveClaudeAccount(
|
|
53
|
+
export declare function resolveActiveClaudeAccount(options?: {
|
|
54
|
+
selfHeal?: boolean;
|
|
55
|
+
}): ResolvedActiveAccount;
|
|
54
56
|
export declare function isMultiAccountEnabled(): boolean;
|
package/dist/accounts.js
CHANGED
|
@@ -201,7 +201,14 @@ function readAccountRegistry() {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
function writeAccountRegistry(registry) {
|
|
204
|
-
|
|
204
|
+
(0, accountRegistry_1.replaceSavedAccountProfiles)('claude-code', mapClaudeProfiles(registry), registry.activeAccountUuid);
|
|
205
|
+
}
|
|
206
|
+
/** For callers already holding the registry lock (see withSavedAccountRegistryLock). */
|
|
207
|
+
function writeAccountRegistryUnlocked(registry) {
|
|
208
|
+
(0, accountRegistry_1.replaceSavedAccountProfilesUnlocked)('claude-code', mapClaudeProfiles(registry), registry.activeAccountUuid);
|
|
209
|
+
}
|
|
210
|
+
function mapClaudeProfiles(registry) {
|
|
211
|
+
return registry.accounts.map((account) => ({
|
|
205
212
|
id: account.uuid,
|
|
206
213
|
providerId: 'claude-code',
|
|
207
214
|
providerAccountId: account.uuid,
|
|
@@ -212,7 +219,6 @@ function writeAccountRegistry(registry) {
|
|
|
212
219
|
email: account.email,
|
|
213
220
|
},
|
|
214
221
|
}));
|
|
215
|
-
(0, accountRegistry_1.replaceSavedAccountProfiles)('claude-code', mappedClaudeProfiles, registry.activeAccountUuid);
|
|
216
222
|
}
|
|
217
223
|
// ── Read active Claude account ───────────────────────────────────────────
|
|
218
224
|
function readActiveClaudeAccount() {
|
|
@@ -254,17 +260,34 @@ function addCurrentAccount(label) {
|
|
|
254
260
|
return { success: false, error: 'Could not read Claude config file.' };
|
|
255
261
|
}
|
|
256
262
|
ensureDirs();
|
|
257
|
-
// 4
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
// 4–7 are one read-modify-write of the registry: hold the registry lock
|
|
264
|
+
// across them so a concurrent add from another process is not dropped
|
|
265
|
+
// when this write replaces the whole Claude profile set.
|
|
266
|
+
return (0, accountRegistry_1.withSavedAccountRegistryLock)(() => {
|
|
267
|
+
// 4. Load or create registry
|
|
268
|
+
const registry = readAccountRegistry() ?? {
|
|
269
|
+
version: 1,
|
|
270
|
+
activeAccountUuid: null,
|
|
271
|
+
accounts: [],
|
|
272
|
+
};
|
|
273
|
+
// 5. Check if account already exists — update credentials + label
|
|
274
|
+
const existing = registry.accounts.find((a) => a.uuid === active.uuid);
|
|
275
|
+
if (existing) {
|
|
276
|
+
if (label !== undefined)
|
|
277
|
+
existing.label = label;
|
|
278
|
+
(0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
279
|
+
(0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
280
|
+
try {
|
|
281
|
+
writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
/* flat backups remain available */
|
|
285
|
+
}
|
|
286
|
+
registry.activeAccountUuid = active.uuid;
|
|
287
|
+
writeAccountRegistryUnlocked(registry);
|
|
288
|
+
return { success: true };
|
|
289
|
+
}
|
|
290
|
+
// 6. New account — back up credentials
|
|
268
291
|
(0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
269
292
|
(0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
270
293
|
try {
|
|
@@ -273,29 +296,17 @@ function addCurrentAccount(label) {
|
|
|
273
296
|
catch {
|
|
274
297
|
/* flat backups remain available */
|
|
275
298
|
}
|
|
299
|
+
// 7. Add entry
|
|
300
|
+
registry.accounts.push({
|
|
301
|
+
uuid: active.uuid,
|
|
302
|
+
email: active.email,
|
|
303
|
+
label,
|
|
304
|
+
addedAt: new Date().toISOString(),
|
|
305
|
+
});
|
|
276
306
|
registry.activeAccountUuid = active.uuid;
|
|
277
|
-
|
|
307
|
+
writeAccountRegistryUnlocked(registry);
|
|
278
308
|
return { success: true };
|
|
279
|
-
}
|
|
280
|
-
// 6. New account — back up credentials
|
|
281
|
-
(0, atomic_1.atomicWriteJsonSync)(path.join(getCredentialsDir(), `${active.uuid}.credentials.json`), credBlob);
|
|
282
|
-
(0, atomic_1.atomicWriteJsonSync)(path.join(getConfigsDir(), `${active.uuid}.config.json`), configBlob);
|
|
283
|
-
try {
|
|
284
|
-
writeClaudeProfileMirror(active.uuid, credBlob, configBlob);
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
/* flat backups remain available */
|
|
288
|
-
}
|
|
289
|
-
// 7. Add entry
|
|
290
|
-
registry.accounts.push({
|
|
291
|
-
uuid: active.uuid,
|
|
292
|
-
email: active.email,
|
|
293
|
-
label,
|
|
294
|
-
addedAt: new Date().toISOString(),
|
|
295
309
|
});
|
|
296
|
-
registry.activeAccountUuid = active.uuid;
|
|
297
|
-
writeAccountRegistry(registry);
|
|
298
|
-
return { success: true };
|
|
299
310
|
}
|
|
300
311
|
// ── Switch to account ────────────────────────────────────────────────────
|
|
301
312
|
/**
|
|
@@ -486,34 +497,37 @@ function reconcileClaudeAuthState() {
|
|
|
486
497
|
}
|
|
487
498
|
// ── Remove account ───────────────────────────────────────────────────────
|
|
488
499
|
function removeAccount(uuid) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
registry.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
500
|
+
// One read-modify-write under the registry lock (see addCurrentAccount).
|
|
501
|
+
return (0, accountRegistry_1.withSavedAccountRegistryLock)(() => {
|
|
502
|
+
const registry = readAccountRegistry();
|
|
503
|
+
if (!registry) {
|
|
504
|
+
return { success: false, error: 'No account registry found.' };
|
|
505
|
+
}
|
|
506
|
+
const idx = registry.accounts.findIndex((a) => a.uuid === uuid);
|
|
507
|
+
if (idx === -1) {
|
|
508
|
+
return { success: false, error: `Account ${uuid} not found.` };
|
|
509
|
+
}
|
|
510
|
+
// Remove backed-up files
|
|
511
|
+
try {
|
|
512
|
+
fs.unlinkSync(path.join(getCredentialsDir(), `${uuid}.credentials.json`));
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
/* ok */
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
fs.unlinkSync(path.join(getConfigsDir(), `${uuid}.config.json`));
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
/* ok */
|
|
522
|
+
}
|
|
523
|
+
// Remove from registry
|
|
524
|
+
registry.accounts.splice(idx, 1);
|
|
525
|
+
if (registry.activeAccountUuid === uuid) {
|
|
526
|
+
registry.activeAccountUuid = registry.accounts[0]?.uuid ?? null;
|
|
527
|
+
}
|
|
528
|
+
writeAccountRegistryUnlocked(registry);
|
|
529
|
+
return { success: true };
|
|
530
|
+
});
|
|
517
531
|
}
|
|
518
532
|
// ── Query helpers ────────────────────────────────────────────────────────
|
|
519
533
|
function listAccounts() {
|
|
@@ -546,7 +560,8 @@ function getActiveAccount() {
|
|
|
546
560
|
* re-pointed so registry-keyed data tracks reality too. Never creates or deletes
|
|
547
561
|
* profiles; an unknown live account is shown as-is with no label and no write.
|
|
548
562
|
*/
|
|
549
|
-
function resolveActiveClaudeAccount() {
|
|
563
|
+
function resolveActiveClaudeAccount(options = {}) {
|
|
564
|
+
const selfHeal = options.selfHeal ?? true;
|
|
550
565
|
const live = readActiveClaudeAccount();
|
|
551
566
|
if (live) {
|
|
552
567
|
const profiles = (0, accountRegistry_1.listSavedAccountProfiles)('claude-code');
|
|
@@ -554,12 +569,17 @@ function resolveActiveClaudeAccount() {
|
|
|
554
569
|
profiles.find((p) => (p.email ?? p.metadata?.email) === live.email);
|
|
555
570
|
if (match) {
|
|
556
571
|
const active = (0, accountRegistry_1.getActiveSavedAccount)('claude-code');
|
|
557
|
-
|
|
572
|
+
// Hot paths (the status line runs on every prompt) pass selfHeal: false
|
|
573
|
+
// so a persistently failing registry write never becomes a write per
|
|
574
|
+
// prompt; the next ordinary command repairs the pointer instead.
|
|
575
|
+
if (selfHeal && (!active || active.id !== match.id)) {
|
|
558
576
|
// Self-heal is best-effort: a registry write failure (read-only/full
|
|
559
577
|
// disk) must never break display or extension activation. We still
|
|
560
578
|
// return the correct live identity below.
|
|
579
|
+
// Silent: this is a read path repairing a pointer to the profile that
|
|
580
|
+
// already matches the live login, so subscribers have nothing new to see.
|
|
561
581
|
try {
|
|
562
|
-
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', match.id);
|
|
582
|
+
(0, accountRegistry_1.setActiveSavedAccount)('claude-code', match.id, { silent: true });
|
|
563
583
|
}
|
|
564
584
|
catch {
|
|
565
585
|
/* keep going with the live identity */
|