sidekick-shared 0.27.5 → 0.27.6
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 +30 -0
- package/README.md +35 -34
- package/dist/accountHealth.d.ts +14 -0
- package/dist/accountHealth.js +61 -14
- package/dist/accountLaunch.d.ts +6 -0
- package/dist/accountLaunch.js +61 -4
- package/dist/accountLease.d.ts +5 -0
- package/dist/accountLease.js +98 -0
- package/dist/accountManager.d.ts +7 -0
- package/dist/accountManager.js +226 -89
- package/dist/accountSync.d.ts +1 -5
- package/dist/accountSync.js +22 -6
- package/dist/accountSyncTypes.d.ts +9 -0
- package/dist/accounts.d.ts +75 -18
- package/dist/accounts.js +712 -189
- package/dist/claudeCredentialDoctor.d.ts +50 -0
- package/dist/claudeCredentialDoctor.js +277 -0
- package/dist/claudeCredentialLedger.d.ts +15 -0
- package/dist/claudeCredentialLedger.js +130 -0
- package/dist/claudeCredentials.d.ts +20 -3
- package/dist/claudeCredentials.js +50 -3
- package/dist/claudeLiveGuard.d.ts +33 -0
- package/dist/claudeLiveGuard.js +46 -0
- package/dist/claudeProfiles.d.ts +14 -0
- package/dist/claudeProfiles.js +22 -0
- package/dist/claudeRefreshLock.d.ts +15 -0
- package/dist/claudeRefreshLock.js +145 -0
- package/dist/codexAuth.d.ts +4 -0
- package/dist/codexAuth.js +18 -0
- package/dist/codexCredentialDoctor.d.ts +33 -0
- package/dist/codexCredentialDoctor.js +302 -0
- package/dist/codexCredentialStore.d.ts +29 -0
- package/dist/codexCredentialStore.js +213 -0
- package/dist/codexKeepAlive.d.ts +2 -0
- package/dist/codexKeepAlive.js +116 -0
- package/dist/codexPaths.d.ts +8 -2
- package/dist/codexPaths.js +51 -7
- package/dist/codexProfiles.d.ts +37 -31
- package/dist/codexProfiles.js +842 -671
- package/dist/credentialIO.d.ts +67 -7
- package/dist/credentialIO.js +211 -47
- package/dist/ensureDefaultAccounts.js +1 -8
- package/dist/index.d.ts +14 -4
- package/dist/index.js +29 -6
- package/dist/keepAlive.d.ts +2 -1
- package/dist/keepAlive.js +116 -29
- package/dist/liveCredentialGuard.d.ts +28 -0
- package/dist/liveCredentialGuard.js +36 -0
- package/dist/processDetection.d.ts +10 -3
- package/dist/processDetection.js +44 -11
- package/dist/schemas/accountManager.d.ts +22 -0
- package/dist/schemas/accountManager.js +4 -0
- package/dist/schemas/assistantTurn.d.ts +2 -2
- package/dist/terminalSync.d.ts +12 -2
- package/dist/terminalSync.js +27 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to sidekick-shared will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.27.6] - 2026-09-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **One usable copy per Claude refresh-token chain.** Claude Code rotates the refresh token on every refresh, so a second usable copy of a login invalidates the first ("Not logged in · Please run /login").
|
|
13
|
+
- **Keychain writes** use Claude Code's method: `-X <hex>` through `security -i` up to 4032 bytes, argv beyond. The old `-w '<json>'` line was silently truncated at 4095 bytes once `mcpOAuth` grew. Every write is read back; a mismatch or an unverifiable read-back throws `ClaudeCredentialWriteError`. Reads and deletes pass the account name like Claude Code, and profile items carry a `sidekick-agent-hub:<dir>` comment.
|
|
14
|
+
- **The live account keeps no profile-home credential.** `storeClaudeProfileCredentials()` is the chokepoint: for the live account it writes only the private backup and the sidecar. `syncLiveAccountState()` removes a leftover copy (`report.claude.demoted`) and installs it first when it is strictly newer than live (`repaired`). It also refuses to fold when the live credential's fingerprint belongs to another profile.
|
|
15
|
+
- **Guarded live writes.** Only `installIntoLiveClaudeStore` writes the live store, after `decideLiveClaudeWrite()`: a switch requires the outgoing login captured (mandatory even with `force`), a re-login requires a strictly fresher credential of the same account, and a rollback never clobbers a store that changed. The installed blob keeps live's `mcpOAuth` and swaps `claudeAiOauth`; profile copies hold `claudeAiOauth` only.
|
|
16
|
+
- **Switch.** Holds Claude Code's refresh locks (`<home>/.oauth_refresh.lock` plus the legacy lock), re-checks the live fingerprint before installing, and writes `accounts/claude/switch-journal.json`. The next sync finishes or rolls back an interrupted switch from the live fingerprint (`report.claude.journal`). Sidecars record `refreshFp`, and read-only probes keep the recorded `source`.
|
|
17
|
+
- **`readStoredClaudeAccount()`** is read-only and returns the fresher of the profile store and the backup. `reconcileClaudeAuthState()` skips the live account.
|
|
18
|
+
- **Keep-alive** skips the live account, a copy with the live refresh token, `live-sync` provenance, a profile without its own copy, and a copy older than its backup. It takes a lease under the swap lock that makes a switch to that account refuse until it is done.
|
|
19
|
+
- **Login homes** are recorded in the ledger before the CLI runs, and `finalizeAccountLogin`, `spawnAccountLogin`, and `cleanupAbandonedClaudeLogins` (authenticated logins after 24 h) delete their Keychain item on every exit.
|
|
20
|
+
- **`getAccountLaunchEnv()`** points the live Claude account at the live home and restores an inactive profile from its backup (skip with `restoreBackup: false`).
|
|
21
|
+
- **Leftover Keychain items.** Running against a throwaway `SIDEKICK_CONFIG_DIR` with the real `HOME` no longer creates a Keychain item for the live login.
|
|
22
|
+
- **Profile-home shells.** When `CLAUDE_CONFIG_DIR` points at a Sidekick profile home (a shell from `sidekick accounts env` or `shell`), `syncLiveAccountState()` reports `claude.skipped: 'managed-home'` as it does for Codex, `applyClaudeProfileToLiveHome()` refuses, and `inspectClaudeCredentialStores()` reports no live duplicate, so that profile's only usable copy is never demoted.
|
|
23
|
+
- **Codex OAuth credential safety:** private byte-preserved backups, shared live-write guard policy, verified outgoing capture, journal recovery, and non-bypassable consumer/lease checks replace duplicate profile credentials and timestamped stashes. Live launches use the live home. Codex keep-alive uses the official app-server refresh request and requires a verified credential advance. Sign-in homes are separate and cleaned up after use. Added `inspectCodexCredentialStores`, `repairCodexCredentialStores`, `readStoredCodexAccount`, `syncCodexLiveStateAsync`, and non-persisting health inspection (`persist: false`). File storage is required, and unrelated MCP data is preserved.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `readClaudeCredentialStore()` (`ok` / `absent` / `corrupt` / `error`), `decideLiveClaudeWrite()`, `claudeRefreshFingerprint()`, `claudeLoginOnly()`, `mergeClaudeLoginIntoLive()`, and `ClaudeCredentialWriteError`.
|
|
28
|
+
- A ledger of credential homes (`accounts/claude/credential-homes.json`, `readClaudeCredentialLedger()`), plus `inspectClaudeCredentialStores()` and `repairClaudeCredentialStores({ dryRun })`: `orphan`, `live-duplicate`, `corrupt`, and `stale-entry` findings. Ownership is proven by the ledger, the comment tag, or the profile directory, never by name.
|
|
29
|
+
- `discardAccountLogin(provider, loginId)` and `writeAccountLauncher(name, provider, accountId)`.
|
|
30
|
+
- `ProviderSyncReport` gains `demoted`, `repaired`, and `journal`, and the sidecar gains `refreshFp`; the schemas are updated.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- `writeActiveCredentials()` throws without a config directory. `deleteStoredCredentials()` returns whether the store is gone.
|
|
35
|
+
- Deprecated: `writeLauncher()`, `applyActiveClaudeToLiveHome()` (now a guarded re-login that refuses another account), and `resolveActiveClaudeHome()` (the live home for the live account).
|
|
36
|
+
- A global test guard (`src/testing/credentialGuard.setup.ts`, also used by the CLI and extension suites) fails any test that reaches the real `security`, `claude`, or `codex` binaries or real homes.
|
|
37
|
+
|
|
8
38
|
## [0.27.5] - 2026-09-23
|
|
9
39
|
|
|
10
40
|
### 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
|
+
- **0.27.6: one usable copy per login** — Claude Code and Codex rotate refresh tokens, so a second usable copy of a login invalidates the first ("Not logged in · Please run /login"). The live account now keeps no profile-home credential, only a private backup; `syncLiveAccountState()` demotes a leftover copy (installing it first when it is newer) and reports `demoted`, `repaired`, and `journal`. Live writes pass `decideLiveClaudeWrite()` or the Codex equivalent: a switch captures the outgoing login first even with `force`, holds Claude Code's refresh lock, is journaled so the next sync finishes or rolls it back, and keeps live MCP tokens. Claude Keychain writes use Claude Code's hex method and are read back (`ClaudeCredentialWriteError`), so blobs over about 4 KB are no longer truncated. New root APIs: `readClaudeCredentialStore()`, `inspectClaudeCredentialStores()` / `repairClaudeCredentialStores()`, `inspectCodexCredentialStores()` / `repairCodexCredentialStores()`, `readClaudeCredentialLedger()`, `readStoredCodexAccount()`, `syncCodexLiveStateAsync()`, `discardAccountLogin()`, `writeAccountLauncher()`, and non-persisting health inspection (`persist: false`). `writeActiveCredentials()` now requires a config directory; `writeLauncher()`, `applyActiveClaudeToLiveHome()`, and `resolveActiveClaudeHome()` are deprecated.
|
|
12
13
|
- **0.27.5: whole-session prompt history** — `collectSessionPromptHistory()` (root and Node) returns every human prompt of each matching Claude Code or Codex session, grouped by session and read from the start with the `collectPromptHistory()` scanners, so text, scope, model and usage, and ordinals are identical. Select with `sessionIds`, `since` (file mtime), or `limit`; each session carries `startedAt` / `lastActivityAt`, `cwds`, `gitBranches`, `models`, and `complete` / `truncated` / `droppedPrompts` / `excludedRecords`. `include.replies` adds the agent's last text for each prompt, and `include.signals` adds `interrupt`, `toolRejected`, `toolError`, `compaction`, `apiError`, and `rollback` signals. Sessions that `maxSessions`, `maxTotalBytes`, the deadline, or an abort cut off are listed in `unread` instead of being returned partially.
|
|
13
14
|
- **0.27.4: usage events from the Node entry** — `sidekick-shared/node` now exports `createSessionProviders()`, `SYNTHETIC_MODEL`, and the `ProviderId` / `SessionProviderBase` / `SessionProviderDiagnostic` / `CreateSessionProviders*` types, so `collectUsageEvents()` callers never import the package root (a CommonJS barrel that bundlers cannot tree-shake). `collectUsageEvents()` accepts an optional `signal` that stops the collection before listing and between sessions.
|
|
14
15
|
- **0.27.3: honest session totals** — Claude Code split lines are counted once: `ClaudeUsageDeduper` keeps a `message.id`'s first usage, strips identical repeats, and turns streamed growth into a `usageKind: 'correction'` delta that `EventAggregator` adds to tokens and cost without counting another call. `resolveCodexCallUsage()` drops duplicate Codex `token_count` events. `collectSessionTokenTotals()` (root and Node) returns `mainThread`, per-`subagents`, `subagentTotal`, and `combined` in the `summarizeTokens()` vocabulary, and `formatTokenBreakdown()` renders one line whose parts add up to the headline. `TokenSummary` gains `reasoning` / `billedOutsideBuckets`, and `SubagentStats` from every provider now carries cache, reasoning, and total tokens. The session dumps and `HtmlReportOptions` accept `subagents`, `readSessionReportInputs()` returns them, `collectUsageEvents()` reads Claude Code subagent transcripts, and `USAGE_CACHE_VERSION` 2 re-reads caches built with the old counting.
|
|
@@ -93,40 +94,40 @@ returns the platform default that both overrides bypass.
|
|
|
93
94
|
|
|
94
95
|
## API Overview
|
|
95
96
|
|
|
96
|
-
| Module | Description
|
|
97
|
-
| --------------------------------- |
|
|
98
|
-
| **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data)
|
|
99
|
-
| **Paths** | Config directory resolution, project data paths, workspace encoding
|
|
100
|
-
| **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/`
|
|
101
|
-
| **Writers** | Atomic persistence writers — `atomicWriteJson()` / `updateJsonStoreAtomic()` and their synchronous twins `atomicWriteJsonSync()` / `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()` (same cross-process lock file, progress-based timeouts, abandoned-lock reclaim), plus lock-coordinated capture helpers `addTask()`, `completeTask()`, `addDecision()`, `addNote()` that merge into the shared stores without clobbering concurrent writers
|
|
102
|
-
| **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem. As of 0.25.0 constructors perform no environment I/O — build them all with `createSessionProviders({ onDiagnostic })`, which reports unavailable providers as structured diagnostics instead of failing; `findSessionById()` resolves one session through provider-native indexes; stable provider-id const arrays (`SESSION_PROVIDER_IDS`, `ACCOUNT_PROVIDER_IDS`, `QUOTA_PROVIDER_IDS`, `MODEL_PROVIDER_IDS`) accompany the derived unions. As of 0.26.5 Claude Code and Codex enumerate through a per-instance `DirectoryListingCache` (one stat per unchanged directory) and implement `statWatchedSessionFile()` so a watch event resolves with one stat; `splitWatchedPath()` and the `ListSessionFilesOptions` / `WatchedSessionFile` / `SessionEnumerationCounters` types are exported alongside
|
|
103
|
-
| **Observed Sessions (V1)** | Versioned provider-neutral observation contracts — `createProviderSessionAdapterV1()` / `derivePendingUserRequestV1()` with `ObservedAgentSessionV1`, `ProviderCapabilitiesV1`, `PendingUserRequestV1`, `SessionEvidenceRefV1`, and provenance/confidence wrapper types (Zod schemas in `sidekick-shared/schemas`)
|
|
104
|
-
| **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing
|
|
105
|
-
| **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers
|
|
106
|
-
| **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), the shared token vocabulary (`summarizeTokens()`, `formatTokenBreakdown()`, `TOKEN_*_LABEL`s) and session totals incl. subagents (`collectSessionTokenTotals()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting
|
|
107
|
-
| **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date)
|
|
108
|
-
| **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction
|
|
109
|
-
| **Analytics** | Session-quality scoring (beta) with factor contributions and weekly trends (`scoreSessionQuality()`, `calculateQualityTrend()`), code-impact metrics including cost per changed line and per-model churn/cost (`calculateCodeImpact()`), and reported-vs-heuristic compaction accounting (`calculateCompactionLedger()`, `formatCompactionLedger()`)
|
|
110
|
-
| **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups
|
|
111
|
-
| **Session Context** | Provider-neutral context evidence snapshots (`buildSessionContextSnapshot()`, `calculateSessionContextPressure()`, `createSessionContextProjector()`, `readSessionContextSnapshot()`): layered evidence sources, low/medium/high context pressure, and observed capabilities (tools, MCP servers, permission mode, rate limits)
|
|
112
|
-
| **Assistant Turns** | Browser-safe timeline/process/answer projection for provider-normalized assistant turns (`segmentAssistantTurn()`, `assistantTurnEventsFromSessionEvents()`), including interleaved reasoning, compact tool groups, and Claude `Task` subagent refs without prompt leakage
|
|
113
|
-
| **Report** | Self-contained HTML session report generation; `readSessionReportInputs()` returns metrics, transcript, and `subagents` for the session total
|
|
114
|
-
| **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows)
|
|
115
|
-
| **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
|
|
116
|
-
| **Provider Status** | `fetchProviderServiceStatus()` returns explicit observed/unavailable public status with components and unresolved incidents from status.claude.com and status.openai.com; `fetchProviderStatus()` / `fetchOpenAIStatus()` remain as lossy legacy projections (`toLegacyProviderStatus()`)
|
|
117
|
-
| **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` retains the legacy Claude Code path probe; `runDoctor({ provider })` diagnoses the selected session provider
|
|
118
|
-
| **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, quota, account status, account management, quota history, and the public `state.json` contract (`sidekickStateFileSchema`, exported only from `sidekick-shared/schemas`) — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Account and quota APIs guarantee results valid against the same-release schemas. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath
|
|
119
|
-
| **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
|
|
120
|
-
| **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5.1 / Opus 5.5 / Opus 5 / Opus 4.8 / Opus 4.7 1M and GPT-5.x / GPT-6 variants), pricing tables with optional LiteLLM hydration, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids
|
|
121
|
-
| **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
|
|
122
|
-
| **Multi-Provider Quota** | `MultiProviderQuotaService` orchestrates Claude polling + peak-hours + account labels + Codex/z.ai quota updates behind one typed event stream. `CodexQuotaWatcher` watches the active Codex rollout for live rate limits with snapshot fallback; z.ai quota uses the API resolver with snapshot fallback. All three stay dormant without a matching account and wake on account changes
|
|
123
|
-
| **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for startup bootstrap (migrations, abandoned-login cleanup, then `syncLiveAccountState()`), `syncLiveAccountState()` to register live logins the registry has never seen under their email, fold rotated tokens into
|
|
124
|
-
| **Account Management 2.0** | Provider-neutral acquisition + switching: `beginAccountLogin()` / `getAccountLoginStatus()` / `finalizeAccountLogin()` / `spawnAccountLogin()` drive a TTY-less, profile-isolated login that doesn't disturb the active account until finalization; `listAllAccounts()` and `switchAccount()` expose a shared switcher across Claude and Codex; as of 0.26.6 switches are two-phase and verified, return a `SwitchAccountResult` with `runningConsumers`, `needsLogin`, and an `undoToken` (`undoLastSwitch()`), refuse expired or missing credentials unless forced, and `beginAccountLogin({ existingAccountId })` re-authenticates a saved profile in place. Every status/finalize/switch entry point also has an `-Async` variant (`getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `switchAccountAsync()`, …) that keeps `codex` CLI probes off the event loop — prefer those from hosts with a UI loop. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
|
|
125
|
-
| **Isolated launch & auto-switch** | `getAccountLaunchEnv()` builds an isolated `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment (plus `envUnset`) for running a CLI as a saved account without switching, `
|
|
126
|
-
| **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — switching atomically swaps the profile's backed-up credentials into the system `~/.codex/auth.json`, with rotated-token staleness protection, one-time dual-home migration, and legacy multi-home session monitoring
|
|
127
|
-
| **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback
|
|
128
|
-
| **Statusline** | Cache-only one-line account/quota/burn footer — `formatStatusline()`, `selectStatuslineAccount()`, `BurnRateCalculator`, and `estimateTimeToQuota()`, also published via the [`sidekick-shared/statusline`](#supported-import-paths) subpath for fast per-prompt rendering
|
|
129
|
-
| **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI
|
|
97
|
+
| Module | Description |
|
|
98
|
+
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| **Types** | Session events, OpenCode/Codex format types, persistence schemas (tasks, decisions, notes, plans, historical data) |
|
|
100
|
+
| **Paths** | Config directory resolution, project data paths, workspace encoding |
|
|
101
|
+
| **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
|
|
102
|
+
| **Writers** | Atomic persistence writers — `atomicWriteJson()` / `updateJsonStoreAtomic()` and their synchronous twins `atomicWriteJsonSync()` / `atomicWriteFileSync()` / `updateJsonStoreAtomicSync()` / `withFileLockSync()` (same cross-process lock file, progress-based timeouts, abandoned-lock reclaim), plus lock-coordinated capture helpers `addTask()`, `completeTask()`, `addDecision()`, `addNote()` that merge into the shared stores without clobbering concurrent writers |
|
|
103
|
+
| **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem. As of 0.25.0 constructors perform no environment I/O — build them all with `createSessionProviders({ onDiagnostic })`, which reports unavailable providers as structured diagnostics instead of failing; `findSessionById()` resolves one session through provider-native indexes; stable provider-id const arrays (`SESSION_PROVIDER_IDS`, `ACCOUNT_PROVIDER_IDS`, `QUOTA_PROVIDER_IDS`, `MODEL_PROVIDER_IDS`) accompany the derived unions. As of 0.26.5 Claude Code and Codex enumerate through a per-instance `DirectoryListingCache` (one stat per unchanged directory) and implement `statWatchedSessionFile()` so a watch event resolves with one stat; `splitWatchedPath()` and the `ListSessionFilesOptions` / `WatchedSessionFile` / `SessionEnumerationCounters` types are exported alongside |
|
|
104
|
+
| **Observed Sessions (V1)** | Versioned provider-neutral observation contracts — `createProviderSessionAdapterV1()` / `derivePendingUserRequestV1()` with `ObservedAgentSessionV1`, `ProviderCapabilitiesV1`, `PendingUserRequestV1`, `SessionEvidenceRefV1`, and provenance/confidence wrapper types (Zod schemas in `sidekick-shared/schemas`) |
|
|
105
|
+
| **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
|
|
106
|
+
| **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
|
|
107
|
+
| **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), the shared token vocabulary (`summarizeTokens()`, `formatTokenBreakdown()`, `TOKEN_*_LABEL`s) and session totals incl. subagents (`collectSessionTokenTotals()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
|
|
108
|
+
| **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
|
|
109
|
+
| **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
|
|
110
|
+
| **Analytics** | Session-quality scoring (beta) with factor contributions and weekly trends (`scoreSessionQuality()`, `calculateQualityTrend()`), code-impact metrics including cost per changed line and per-model churn/cost (`calculateCodeImpact()`), and reported-vs-heuristic compaction accounting (`calculateCompactionLedger()`, `formatCompactionLedger()`) |
|
|
111
|
+
| **Error History** | Shared error taxonomy (`categorizeError()`, `extractErrorMessage()`) plus append-only per-project failure history (`appendErrorHistory()`, `readErrorHistory()`, `getTopFailingTools()`) behind per-tool/hour/model error rollups |
|
|
112
|
+
| **Session Context** | Provider-neutral context evidence snapshots (`buildSessionContextSnapshot()`, `calculateSessionContextPressure()`, `createSessionContextProjector()`, `readSessionContextSnapshot()`): layered evidence sources, low/medium/high context pressure, and observed capabilities (tools, MCP servers, permission mode, rate limits) |
|
|
113
|
+
| **Assistant Turns** | Browser-safe timeline/process/answer projection for provider-normalized assistant turns (`segmentAssistantTurn()`, `assistantTurnEventsFromSessionEvents()`), including interleaved reasoning, compact tool groups, and Claude `Task` subagent refs without prompt leakage |
|
|
114
|
+
| **Report** | Self-contained HTML session report generation; `readSessionReportInputs()` returns metrics, transcript, and `subagents` for the session total |
|
|
115
|
+
| **Credentials** | Claude Max OAuth credential reading (macOS Keychain with a `~/.claude/.credentials.json` fallback; the file on Linux and Windows). `readClaudeCredentialStore()` returns `ok` / `absent` / `corrupt` / `error`; `writeActiveCredentials()` requires a config directory and reads every write back (`ClaudeCredentialWriteError`). `decideLiveClaudeWrite()`, `claudeRefreshFingerprint()`, `claudeLoginOnly()`, and `mergeClaudeLoginIntoLive()` expose the live-write policy. As of 0.27.6, `inspectClaudeCredentialStores()` / `repairClaudeCredentialStores()` and `inspectCodexCredentialStores()` / `repairCodexCredentialStores()` find and repair unused, duplicate, and unreadable stores Sidekick provably owns (repairs are dry runs unless `dryRun: false`), with Claude ownership recorded by `readClaudeCredentialLedger()` |
|
|
116
|
+
| **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 |
|
|
117
|
+
| **Provider Status** | `fetchProviderServiceStatus()` returns explicit observed/unavailable public status with components and unresolved incidents from status.claude.com and status.openai.com; `fetchProviderStatus()` / `fetchOpenAIStatus()` remain as lossy legacy projections (`toLegacyProviderStatus()`) |
|
|
118
|
+
| **Doctor** | Typed cross-provider health diagnostics — `runDoctor()` checks project identity, sessions, accounts, providers, and dependencies into a `HealthReport`; `formatHealthReport()` renders it and `getSessionDiagnostics()` retains the legacy Claude Code path probe; `runDoctor({ provider })` diagnoses the selected session provider |
|
|
119
|
+
| **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, observed-session V1 records, quota, account status, account management, quota history, and the public `state.json` contract (`sidekickStateFileSchema`, exported only from `sidekick-shared/schemas`) — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Account and quota APIs guarantee results valid against the same-release schemas. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
|
|
120
|
+
| **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 |
|
|
121
|
+
| **Model Info & Pricing** | Model family parsing (Anthropic / OpenAI / Google, including legacy `claude-3-opus-…` and `claude-3-5-sonnet-…` IDs), context-window lookup (including Fable 5.1 / Opus 5.5 / Opus 5 / Opus 4.8 / Opus 4.7 1M and GPT-5.x / GPT-6 variants), pricing tables with optional LiteLLM hydration, normalized non-double-counting cost (`calculateNormalizedUsageCost()`, fed by `normalizeProviderUsage()` / `extractNormalizedUsage()`), legacy cost helpers retained but deprecated (`calculateCost()`, `calculateCostWithProvenance()`), `mergeCostSources()`, display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`), and cross-realm catalog transfer — `exportResolvedModelCatalog()` / `importResolvedModelCatalog()` move resolved context windows and pricing (with provenance, prefix inheritance explicit) between Node and browser realms, and `registerModelAlias()` resolves short or host-specific names to canonical ids |
|
|
122
|
+
| **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 |
|
|
123
|
+
| **Multi-Provider Quota** | `MultiProviderQuotaService` orchestrates Claude polling + peak-hours + account labels + Codex/z.ai quota updates behind one typed event stream. `CodexQuotaWatcher` watches the active Codex rollout for live rate limits with snapshot fallback; z.ai quota uses the API resolver with snapshot fallback. All three stay dormant without a matching account and wake on account changes |
|
|
124
|
+
| **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for startup bootstrap (migrations, abandoned-login cleanup, then `syncLiveAccountState()`), `syncLiveAccountState()` to register live logins the registry has never seen under their email, fold rotated tokens into a private backup (the live account keeps no profile-home copy; `ProviderSyncReport` reports `demoted`, `repaired`, and switch-`journal` recovery), and merge duplicate Codex profiles, `getAccountHealth()` / `listAccountsWithHealth()` for secret-free `fresh`/`expiring`/`expired`/`unknown`/`missing` health with access/refresh expiry, `getActiveAccountStatus()` for a single-pass active-account read across providers, `onAccountsChanged()` for push-based login/logout/switch notification (process-local mutations, filesystem watches, and a catch-up poll), and live-first `resolveActiveClaudeAccount()` /`resolveActiveCodexAccount()` that report the currently logged-in account (self-healing the saved pointer) for display |
|
|
125
|
+
| **Account Management 2.0** | Provider-neutral acquisition + switching: `beginAccountLogin()` / `getAccountLoginStatus()` / `finalizeAccountLogin()` / `spawnAccountLogin()` drive a TTY-less, profile-isolated login that doesn't disturb the active account until finalization; `listAllAccounts()` and `switchAccount()` expose a shared switcher across Claude and Codex; as of 0.26.6 switches are two-phase and verified, return a `SwitchAccountResult` with `runningConsumers`, `needsLogin`, and an `undoToken` (`undoLastSwitch()`), refuse expired or missing credentials unless forced, and `beginAccountLogin(provider, label, { existingAccountId })` re-authenticates a saved profile in place. As of 0.27.6 a switch always captures the outgoing login first (even with `force`), and `discardAccountLogin()` releases an abandoned login home. Every status/finalize/switch entry point also has an `-Async` variant (`getAccountLoginStatusAsync()`, `finalizeAccountLoginAsync()`, `switchAccountAsync()`, …) that keeps `codex` CLI probes off the event loop — prefer those from hosts with a UI loop. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes (the live account's home holds no credential) and startup migration from legacy flat backups |
|
|
126
|
+
| **Isolated launch & auto-switch** | `getAccountLaunchEnv()` builds an isolated `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment (plus `envUnset`) for running a CLI as a saved account without switching — the live account gets the live home, and an inactive profile is restored from its backup first unless `restoreBackup: false` — `writeAccountLauncher()` / `removeLauncher()` write POSIX launcher scripts that resolve the account's home when they run (`writeLauncher()` is deprecated), `detectRunningAccountConsumers()` / `findRunningProcesses()` find CLIs and desktop apps that hold a login (`ps` / `tasklist`), `refreshInactiveAccounts()` is an opt-in keep-alive through the official CLIs, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) moves to a healthier saved account when quota crosses a configured threshold; its `switchAccount` callback may return a Promise and defaults to `switchAccountAsync()` |
|
|
127
|
+
| **Codex Profiles** | Codex account lifecycle — prepare, finalize, switch, remove — switching atomically swaps the profile's backed-up credentials into the system `~/.codex/auth.json`, with rotated-token staleness protection, one-time dual-home migration, and legacy multi-home session monitoring. As of 0.27.6 every account keeps a private byte-preserved backup, the live account keeps no profile copy, switches are journaled and refuse while Codex consumers run or keep-alive holds the account (`force` bypasses neither), file-mode credential storage is required, and `readStoredCodexAccount()` / `syncCodexLiveStateAsync()` are public |
|
|
128
|
+
| **Quota Snapshots** | Persistent quota caching per provider/account for offline fallback |
|
|
129
|
+
| **Statusline** | Cache-only one-line account/quota/burn footer — `formatStatusline()`, `selectStatuslineAccount()`, `BurnRateCalculator`, and `estimateTimeToQuota()`, also published via the [`sidekick-shared/statusline`](#supported-import-paths) subpath for fast per-prompt rendering |
|
|
130
|
+
| **Phrases** | Curated humorous phrases for loading/idle states, available as a flat `ALL_PHRASES` array or grouped via `PHRASE_CATEGORIES` for category-aware UI |
|
|
130
131
|
|
|
131
132
|
## Supported import paths
|
|
132
133
|
|
package/dist/accountHealth.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface AccountHealthSidecar {
|
|
|
12
12
|
lastRefreshAt?: number;
|
|
13
13
|
hasRefreshToken?: boolean;
|
|
14
14
|
authMode?: 'chatgpt' | 'api-key' | 'oauth';
|
|
15
|
+
/**
|
|
16
|
+
* sha256(refreshToken)[:16] of the stored provider credential: lets sync tell
|
|
17
|
+
* whose chain the live store holds without keeping another token copy.
|
|
18
|
+
*/
|
|
19
|
+
refreshFp?: string;
|
|
20
|
+
/** Provenance of the stored credential; read-only probes preserve it. */
|
|
15
21
|
source: AccountHealthSource;
|
|
16
22
|
}
|
|
17
23
|
export interface AccountHealth {
|
|
@@ -48,6 +54,8 @@ export interface AccountHealthOptions {
|
|
|
48
54
|
* is paid once per profile and never on a hot path that opts into `cache`.
|
|
49
55
|
*/
|
|
50
56
|
probe?: 'cache' | 'store' | 'auto';
|
|
57
|
+
/** Inspection-only callers must not persist health metadata. Default true. */
|
|
58
|
+
persist?: boolean;
|
|
51
59
|
now?: number;
|
|
52
60
|
}
|
|
53
61
|
export declare function getHealthSidecarPath(provider: AccountProviderId, accountId: string): string;
|
|
@@ -60,6 +68,12 @@ export declare function writeClaudeHealthSidecar(accountId: string, blob: unknow
|
|
|
60
68
|
export declare function codexHealthSidecarFromAuth(authRaw: string | null, source: AccountHealthSource, now?: number, fallbackPath?: string): AccountHealthSidecar | null;
|
|
61
69
|
/** Record the health of a Codex profile from the auth file just stored. */
|
|
62
70
|
export declare function writeCodexHealthSidecar(accountId: string, authRaw: string | null, source: AccountHealthSource, now?: number): AccountHealthSidecar | null;
|
|
71
|
+
/**
|
|
72
|
+
* The credential a Claude profile would use right now: the live store for the
|
|
73
|
+
* live account (its profile home is empty by design), otherwise the fresher of
|
|
74
|
+
* the profile-home store and Sidekick's backup.
|
|
75
|
+
*/
|
|
76
|
+
export declare function readEffectiveClaudeCredential(accountId: string): unknown;
|
|
63
77
|
/**
|
|
64
78
|
* Health of one saved account. `probe: 'cache'` never spawns anything; a
|
|
65
79
|
* missing sidecar on a profile that has credentials is reported as `unknown`
|
package/dist/accountHealth.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.claudeHealthSidecarFromBlob = claudeHealthSidecarFromBlob;
|
|
|
39
39
|
exports.writeClaudeHealthSidecar = writeClaudeHealthSidecar;
|
|
40
40
|
exports.codexHealthSidecarFromAuth = codexHealthSidecarFromAuth;
|
|
41
41
|
exports.writeCodexHealthSidecar = writeCodexHealthSidecar;
|
|
42
|
+
exports.readEffectiveClaudeCredential = readEffectiveClaudeCredential;
|
|
42
43
|
exports.getAccountHealth = getAccountHealth;
|
|
43
44
|
exports.listAccountsWithHealth = listAccountsWithHealth;
|
|
44
45
|
exports.describeClaudeCredential = describeClaudeCredential;
|
|
@@ -55,6 +56,7 @@ const credentialIO_1 = require("./credentialIO");
|
|
|
55
56
|
const codexAuth_1 = require("./codexAuth");
|
|
56
57
|
const codexPaths_1 = require("./codexPaths");
|
|
57
58
|
const accountRegistry_1 = require("./accountRegistry");
|
|
59
|
+
const codexCredentialStore_1 = require("./codexCredentialStore");
|
|
58
60
|
const atomic_1 = require("./writers/atomic");
|
|
59
61
|
/** Refresh tokens that expire within this window count as `expiring`. */
|
|
60
62
|
const REFRESH_EXPIRY_WARNING_MS = 3 * 24 * 60 * 60 * 1000;
|
|
@@ -93,6 +95,7 @@ function claudeHealthSidecarFromBlob(blob, source, now = Date.now()) {
|
|
|
93
95
|
refreshExpiryEstimated: refresh?.estimated,
|
|
94
96
|
hasRefreshToken: Boolean(credentials.refreshToken),
|
|
95
97
|
authMode: 'oauth',
|
|
98
|
+
refreshFp: (0, claudeCredentials_1.claudeRefreshFingerprint)(credentials) ?? undefined,
|
|
96
99
|
source,
|
|
97
100
|
};
|
|
98
101
|
}
|
|
@@ -114,7 +117,8 @@ function codexHealthSidecarFromAuth(authRaw, source, now = Date.now(), fallbackP
|
|
|
114
117
|
capturedAt: now,
|
|
115
118
|
accessExpiresAt: (0, codexAuth_1.readAccessTokenExpiry)(authRaw) ?? undefined,
|
|
116
119
|
lastRefreshAt,
|
|
117
|
-
hasRefreshToken:
|
|
120
|
+
hasRefreshToken: (0, codexAuth_1.codexRefreshFingerprint)(authRaw) !== null,
|
|
121
|
+
refreshFp: (0, codexAuth_1.codexRefreshFingerprint)(authRaw) ?? undefined,
|
|
118
122
|
authMode: identity.authMode,
|
|
119
123
|
source,
|
|
120
124
|
};
|
|
@@ -185,15 +189,54 @@ function isLiveCodexAccount(profile) {
|
|
|
185
189
|
workspaceId: profile.metadata?.workspaceId,
|
|
186
190
|
});
|
|
187
191
|
}
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
function readClaudeBackupBlob(accountId) {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(fs.readFileSync((0, claudeProfiles_1.getClaudeCredentialBackupPath)(accountId), 'utf8'));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
191
199
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
/**
|
|
201
|
+
* The credential a Claude profile would use right now: the live store for the
|
|
202
|
+
* live account (its profile home is empty by design), otherwise the fresher of
|
|
203
|
+
* the profile-home store and Sidekick's backup.
|
|
204
|
+
*/
|
|
205
|
+
function readEffectiveClaudeCredential(accountId) {
|
|
206
|
+
if (isLiveClaudeAccount(accountId))
|
|
207
|
+
return (0, credentialIO_1.readActiveCredentials)();
|
|
208
|
+
const store = (0, credentialIO_1.readClaudeCredentialStore)((0, claudeProfiles_1.getClaudeProfileHome)(accountId));
|
|
209
|
+
const fromStore = store.status === 'ok' ? store.blob : null;
|
|
210
|
+
const backup = readClaudeBackupBlob(accountId);
|
|
211
|
+
const storeFreshness = (0, claudeCredentials_1.claudeCredentialFreshness)((0, claudeCredentials_1.parseClaudeCredentialBlob)(fromStore));
|
|
212
|
+
const backupFreshness = (0, claudeCredentials_1.claudeCredentialFreshness)((0, claudeCredentials_1.parseClaudeCredentialBlob)(backup));
|
|
213
|
+
if ((0, claudeCredentials_1.parseClaudeCredentialBlob)(backup) && backupFreshness > storeFreshness)
|
|
214
|
+
return backup;
|
|
215
|
+
return (0, claudeCredentials_1.parseClaudeCredentialBlob)(fromStore) ? fromStore : null;
|
|
216
|
+
}
|
|
217
|
+
function probeClaudeStore(accountId, now, persist = true) {
|
|
218
|
+
const blob = readEffectiveClaudeCredential(accountId);
|
|
219
|
+
if (!blob)
|
|
220
|
+
return null;
|
|
221
|
+
// A probe reads; it must not rewrite provenance (keep-alive relies on it).
|
|
222
|
+
const source = readHealthSidecar('claude-code', accountId)?.source ??
|
|
223
|
+
(isLiveClaudeAccount(accountId) ? 'live-sync' : 'migration');
|
|
224
|
+
return persist
|
|
225
|
+
? writeClaudeHealthSidecar(accountId, blob, source, now)
|
|
226
|
+
: claudeHealthSidecarFromBlob(blob, source, now);
|
|
227
|
+
}
|
|
228
|
+
function probeCodexStore(accountId, now, persist = true) {
|
|
229
|
+
const profile = (0, accountRegistry_1.listSavedAccountProfiles)('codex').find((p) => p.id === accountId);
|
|
230
|
+
const live = profile !== undefined && isLiveCodexAccount(profile);
|
|
231
|
+
const home = (0, codexCredentialStore_1.readCodexCredentialStore)(live ? (0, codexPaths_1.getSystemCodexHome)() : (0, codexPaths_1.getCodexProfileHome)(accountId));
|
|
232
|
+
const backup = live ? null : (0, codexCredentialStore_1.readCodexBackup)(accountId);
|
|
233
|
+
let raw = home.status === 'ok' ? home.snapshot.authRaw : null;
|
|
234
|
+
if (backup?.status === 'ok' &&
|
|
235
|
+
(!raw || (0, codexAuth_1.compareCodexAuthFreshness)(backup.snapshot.authRaw, raw) > 0))
|
|
236
|
+
raw = backup.snapshot.authRaw;
|
|
237
|
+
const source = readHealthSidecar('codex', accountId)?.source ?? (live ? 'live-sync' : 'migration');
|
|
238
|
+
const sidecar = codexHealthSidecarFromAuth(raw, source, now);
|
|
239
|
+
if (sidecar && persist)
|
|
197
240
|
writeSidecar('codex', accountId, sidecar);
|
|
198
241
|
return sidecar;
|
|
199
242
|
}
|
|
@@ -201,12 +244,14 @@ function hasStoredClaudeCredential(accountId) {
|
|
|
201
244
|
const home = (0, claudeProfiles_1.getClaudeProfileHome)(accountId);
|
|
202
245
|
return (fs.existsSync(path.join(home, '.credentials.json')) ||
|
|
203
246
|
fs.existsSync(path.join((0, claudeProfiles_1.getClaudeProfileDir)(accountId), 'health.json')) ||
|
|
204
|
-
fs.existsSync(
|
|
247
|
+
fs.existsSync((0, claudeProfiles_1.getClaudeCredentialBackupPath)(accountId)) ||
|
|
205
248
|
(process.platform === 'darwin' && fs.existsSync(path.join(home, '.claude.json'))));
|
|
206
249
|
}
|
|
207
250
|
function hasStoredCodexCredential(accountId) {
|
|
208
251
|
const home = (0, codexPaths_1.getCodexProfileHome)(accountId);
|
|
209
|
-
return (fs.existsSync(
|
|
252
|
+
return (fs.existsSync((0, codexPaths_1.getCodexBackupPath)(accountId)) ||
|
|
253
|
+
fs.existsSync((0, codexPaths_1.getCodexBackupPath)(accountId, true)) ||
|
|
254
|
+
fs.existsSync(path.join(home, 'auth.json')) ||
|
|
210
255
|
fs.existsSync(path.join(home, '.credentials.json')));
|
|
211
256
|
}
|
|
212
257
|
/**
|
|
@@ -224,17 +269,19 @@ function getAccountHealth(provider, accountId, options = {}) {
|
|
|
224
269
|
let sidecar = null;
|
|
225
270
|
if (probe === 'store') {
|
|
226
271
|
sidecar =
|
|
227
|
-
provider === 'codex'
|
|
272
|
+
provider === 'codex'
|
|
273
|
+
? probeCodexStore(accountId, now, options.persist)
|
|
274
|
+
: probeClaudeStore(accountId, now, options.persist);
|
|
228
275
|
}
|
|
229
276
|
sidecar ??= readHealthSidecar(provider, accountId);
|
|
230
277
|
if (!sidecar && provider === 'codex') {
|
|
231
278
|
// Codex sidecars are cheap to derive from the plaintext file.
|
|
232
|
-
sidecar = probeCodexStore(accountId, now);
|
|
279
|
+
sidecar = probeCodexStore(accountId, now, options.persist);
|
|
233
280
|
}
|
|
234
281
|
if (!sidecar && probe === 'auto' && hasStoredClaudeCredential(accountId)) {
|
|
235
282
|
// First look at a profile that predates the health model: read its store
|
|
236
283
|
// once and record the sidecar so later reads are cache hits.
|
|
237
|
-
sidecar = probeClaudeStore(accountId, now);
|
|
284
|
+
sidecar = probeClaudeStore(accountId, now, options.persist);
|
|
238
285
|
}
|
|
239
286
|
const base = { providerId: provider, accountId, checkedAt: now, isLive };
|
|
240
287
|
if (!sidecar) {
|
package/dist/accountLaunch.d.ts
CHANGED
|
@@ -26,5 +26,11 @@ export interface AccountLaunchOptions {
|
|
|
26
26
|
materialize?: boolean;
|
|
27
27
|
/** Launch even when the stored credential is reported expired. */
|
|
28
28
|
force?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Claude: when the profile home has no credential but its backup does,
|
|
31
|
+
* restore it there first (default true). Keep-alive passes false: it never
|
|
32
|
+
* restores backups.
|
|
33
|
+
*/
|
|
34
|
+
restoreBackup?: boolean;
|
|
29
35
|
}
|
|
30
36
|
export declare function getAccountLaunchEnv(provider: AccountProviderId, accountId: string, options?: AccountLaunchOptions): AccountLaunchEnv;
|
package/dist/accountLaunch.js
CHANGED
|
@@ -44,6 +44,9 @@ const path = __importStar(require("path"));
|
|
|
44
44
|
const accountRegistry_1 = require("./accountRegistry");
|
|
45
45
|
const accountHealth_1 = require("./accountHealth");
|
|
46
46
|
const claudeProfiles_1 = require("./claudeProfiles");
|
|
47
|
+
const accounts_1 = require("./accounts");
|
|
48
|
+
const codexCredentialStore_1 = require("./codexCredentialStore");
|
|
49
|
+
const codexProfiles_1 = require("./codexProfiles");
|
|
47
50
|
const codexPaths_1 = require("./codexPaths");
|
|
48
51
|
const atomic_1 = require("./writers/atomic");
|
|
49
52
|
function materializeClaudeHome(home) {
|
|
@@ -70,10 +73,11 @@ function materializeClaudeHome(home) {
|
|
|
70
73
|
function materializeCodexHome(home) {
|
|
71
74
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
72
75
|
const configTarget = path.join(home, 'config.toml');
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const current = (0, codexPaths_1.readFileOrNull)(configTarget);
|
|
77
|
+
const source = current ?? (0, codexPaths_1.readFileOrNull)(path.join((0, codexPaths_1.getSystemCodexHome)(), 'config.toml')) ?? '';
|
|
78
|
+
const fileMode = (0, codexPaths_1.forceFileCredentialStore)(source);
|
|
79
|
+
if (fileMode !== current)
|
|
80
|
+
(0, atomic_1.atomicWriteFileSync)(configTarget, fileMode);
|
|
77
81
|
}
|
|
78
82
|
function getAccountLaunchEnv(provider, accountId, options = {}) {
|
|
79
83
|
const profile = (0, accountRegistry_1.listSavedAccountProfiles)(provider).find((p) => p.id === accountId);
|
|
@@ -98,6 +102,40 @@ function getAccountLaunchEnv(provider, accountId, options = {}) {
|
|
|
98
102
|
if (!profile) {
|
|
99
103
|
return { ...base, error: `${provider} account ${accountId} not found.` };
|
|
100
104
|
}
|
|
105
|
+
if (provider === 'codex' && (0, codexCredentialStore_1.isSidekickCodexHome)((0, codexPaths_1.getSystemCodexHome)())) {
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
error: 'Unset CODEX_HOME before managing accounts; it names a Sidekick-owned home.',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (provider === 'codex' && !['file', 'unknown'].includes((0, codexPaths_1.getCodexCredentialStoreMode)())) {
|
|
112
|
+
return { ...base, error: 'Codex account launches require readable file-mode credentials.' };
|
|
113
|
+
}
|
|
114
|
+
if (provider === 'codex' && (0, codexProfiles_1.isLiveCodexProfile)(profile)) {
|
|
115
|
+
const liveHome = (0, codexPaths_1.getSystemCodexHome)();
|
|
116
|
+
return {
|
|
117
|
+
...base,
|
|
118
|
+
home: liveHome,
|
|
119
|
+
env: { CODEX_HOME: liveHome },
|
|
120
|
+
warnings: ['This account is the live login, so the session uses the normal Codex home.'],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (provider === 'claude-code' && health.isLive) {
|
|
124
|
+
// The live account's only CLI-usable copy is the live store; a session in
|
|
125
|
+
// its profile home would be a second copy racing it over the refresh
|
|
126
|
+
// token. Point the session at the live home instead.
|
|
127
|
+
const liveHome = (0, claudeProfiles_1.getLiveClaudeHome)();
|
|
128
|
+
const customHome = !(0, claudeProfiles_1.isDefaultClaudeHome)();
|
|
129
|
+
return {
|
|
130
|
+
...base,
|
|
131
|
+
home: liveHome,
|
|
132
|
+
env: customHome ? { CLAUDE_CONFIG_DIR: liveHome } : {},
|
|
133
|
+
envUnset: customHome
|
|
134
|
+
? [claudeProfiles_1.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]
|
|
135
|
+
: ['CLAUDE_CONFIG_DIR', claudeProfiles_1.CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV],
|
|
136
|
+
warnings: ['This account is the live login, so the session uses the normal Claude home.'],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
101
139
|
if ((health.state === 'expired' || health.state === 'missing') && !options.force) {
|
|
102
140
|
return {
|
|
103
141
|
...base,
|
|
@@ -109,6 +147,25 @@ function getAccountLaunchEnv(provider, accountId, options = {}) {
|
|
|
109
147
|
}
|
|
110
148
|
if (health.state === 'expiring' && health.reason)
|
|
111
149
|
warnings.push(health.reason);
|
|
150
|
+
if (provider === 'claude-code' && options.restoreBackup !== false) {
|
|
151
|
+
if ((0, accounts_1.restoreClaudeProfileFromBackup)(accountId) === 'restored') {
|
|
152
|
+
warnings.push("Restored this account's saved login into its profile home. If a session elsewhere still uses this login, close it first: two copies of one login invalidate each other.");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (provider === 'codex') {
|
|
156
|
+
const restored = (0, codexProfiles_1.restoreCodexProfileFromBackup)(accountId, {
|
|
157
|
+
restore: options.restoreBackup !== false,
|
|
158
|
+
});
|
|
159
|
+
if (restored === 'none')
|
|
160
|
+
return { ...base, error: 'No safe inactive Codex credential is available; sign in again.' };
|
|
161
|
+
if (restored === 'live')
|
|
162
|
+
return { ...base, home: (0, codexPaths_1.getSystemCodexHome)(), env: { CODEX_HOME: (0, codexPaths_1.getSystemCodexHome)() } };
|
|
163
|
+
}
|
|
164
|
+
if (provider === 'codex' &&
|
|
165
|
+
options.materialize === false &&
|
|
166
|
+
!['file', 'unknown'].includes((0, codexPaths_1.getCodexCredentialStoreMode)(home))) {
|
|
167
|
+
return { ...base, error: 'The isolated Codex home must use file credential storage.' };
|
|
168
|
+
}
|
|
112
169
|
if (options.materialize !== false) {
|
|
113
170
|
try {
|
|
114
171
|
if (provider === 'codex')
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AccountProviderId } from './accountRegistry';
|
|
2
|
+
export declare function isAccountLeaseHeld(provider: AccountProviderId, accountId: string, now?: number): boolean;
|
|
3
|
+
/** Take the lease; the caller holds the provider's swap lock. False when held elsewhere. */
|
|
4
|
+
export declare function acquireAccountLease(provider: AccountProviderId, accountId: string, durationMs: number, now?: number): boolean;
|
|
5
|
+
export declare function releaseAccountLease(provider: AccountProviderId, accountId: string): void;
|