sidekick-shared 0.21.1 → 0.21.2

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/README.md CHANGED
@@ -40,13 +40,13 @@ npm install sidekick-shared
40
40
  | **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 |
41
41
  | **Report** | Self-contained HTML session report generation |
42
42
  | **Credentials** | Claude Max OAuth credential reading from `~/.claude/.credentials.json` |
43
- | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams, and **estimated** z.ai Coding Plan quota derivation from observed OpenCode traffic (`accumulateZaiUsage()`, `inferZaiQuotaState()`, `parseZaiQuotaError()`, `resolveZaiTier()`, `ZAI_TIER_BUDGETS`) — z.ai exposes no usage API, so this is a best-effort estimate, not authoritative |
43
+ | **Quota** | Claude Max subscription quota fetching (5-hour and 7-day windows), Codex rate-limit extraction from event streams, 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 deprecated observed-traffic z.ai estimator helpers remain exported for backward compatibility only |
44
44
  | **Provider Status** | API health checking via status.claude.com and status.openai.com (indicator, components, incidents) |
45
45
  | **Schemas** | Zod schemas for runtime validation of data crossing process/IPC boundaries — JSONL session events (`sessionEventSchema`, `messageUsageSchema`, `sessionMessageSchema`), assistant turns, quota, account status, and quota history — plus `extractSessionEvents()` to unwrap `progress`-wrapped events. Also published fs-free via the [`sidekick-shared/schemas`](#supported-import-paths) subpath |
46
46
  | **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 |
47
47
  | **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, null-aware cost (`calculateCost()`), provenance-preserving cost (`calculateCostWithProvenance()`, `mergeCostSources()`), and display helpers (`shortModelName()`, `getModelDisplayInfo()`, `compareModelIds()`, `sortModelIds()`, `formatCost()`) |
48
48
  | **Quota Polling** | `QuotaPoller` class with exponential backoff, active/idle intervals, and cached fallback |
49
- | **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; `ZaiQuotaWatcher` derives an estimated z.ai Coding Plan quota from observed OpenCode turns (event-driven, snapshot + history) |
49
+ | **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 |
50
50
  | **Accounts** | Multi-provider account registry (v2) with per-provider active account, save/switch/remove, v1 migration, `ensureDefaultAccounts()` for first-run bootstrap of the active system Claude/Codex credentials as a "Default" saved account, and `getActiveAccountStatus()` for a single-pass active-account read across providers |
51
51
  | **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. Claude accounts get canonical profile homes with account-scoped macOS keychain suffixes and startup migration from legacy flat backups |
52
52
  | **Terminal sync & auto-switch** | Opt-in terminal profile pointers, shell-hook/launcher helpers, and a default-off `AutoSwitchController` (`decideAutoSwitch()`) that moves to a healthier saved account when quota crosses a configured threshold |
package/dist/index.d.ts CHANGED
@@ -125,6 +125,8 @@ export { accumulateZaiUsage, extractNextFlushTime, inferZaiQuotaState, isZaiProv
125
125
  export type { ZaiAccumulatedUsage, ZaiAssistantTurn, ZaiOpenCodeRow, ZaiProviderId, ZaiQuotaError, ZaiQuotaErrorKind, ZaiTier, InferZaiQuotaStateOptions, } from './zaiQuota';
126
126
  export { ZaiQuotaWatcher } from './zaiQuotaWatcher';
127
127
  export type { ZaiQuotaWatcherOptions } from './zaiQuotaWatcher';
128
+ export { fetchZaiQuotaFromApi, quotaStateFromZaiQuotaLimitPayload, readZaiCredentials, resolveZaiQuota, } from './zaiQuotaApi';
129
+ export type { ReadZaiCredentialsOptions, ZaiCredentialSource, ZaiCredentials, ZaiPlatform, ZaiQuotaApiOptions, ZaiQuotaResolveOptions, } from './zaiQuotaApi';
128
130
  export { MultiProviderQuotaService } from './multiProviderQuotaService';
129
131
  export type { MultiProviderQuotaServiceOptions } from './multiProviderQuotaService';
130
132
  export type { ProviderQuotaMap, ProviderQuotaState, RuntimeQuotaProvider } from './providerQuota';
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ exports.discoverSessionDirectory = exports.getClaudeSessionDirectory = exports.e
7
7
  exports.highlightEvent = exports.formatSessionJson = exports.formatSessionMarkdown = exports.formatSessionText = exports.classifyNoise = exports.shouldMergeWithPrevious = exports.classifyFollowEvent = exports.classifyMessage = exports.getSoftNoiseReason = exports.isHardNoiseFollowEvent = exports.isHardNoise = exports.formatToolSummary = exports.formatTokenCount = exports.formatDurationMs = exports.createJsonlTail = exports.toFollowEvents = exports.createWatcher = exports.parseChangelog = exports.extractProposedPlanShared = exports.parsePlanMarkdownShared = exports.PlanExtractor = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.readSessionContextSnapshot = exports.createSessionContextProjector = exports.calculateSessionContextPressure = exports.buildSessionContextSnapshot = exports.composeContext = exports.FilterEngine = exports.searchSessions = exports.CodexDatabase = exports.OpenCodeDatabase = exports.discoverDebugLogs = exports.collapseDuplicates = exports.filterByLevel = exports.parseDebugLog = exports.scanSubagentTraces = exports.findAllSessionsWithWorktrees = exports.discoverWorktreeSiblings = exports.resolveWorktreeMainRepo = exports.getAllClaudeProjectFolders = exports.decodeEncodedPath = exports.getMostRecentlyActiveSessionDir = exports.findSubdirectorySessionDirs = exports.findSessionsInDirectory = exports.findAllClaudeSessions = exports.findActiveClaudeSession = void 0;
8
8
  exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.resolveClaudeLoginCommand = exports.listAllAccounts = exports.switchAccount = exports.spawnAccountLogin = exports.finalizeAccountLogin = exports.getAccountLoginStatus = exports.beginAccountLogin = exports.isMultiAccountEnabled = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.reconcileClaudeAuthState = exports.applyActiveClaudeToLiveHome = exports.resolveActiveClaudeHome = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.ensureDefaultAccounts = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscriptFromEvents = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = exports.HeatmapTracker = exports.FrequencyTracker = exports.getSnapshotPath = exports.isSnapshotValid = exports.deleteSnapshot = exports.loadSnapshot = exports.saveSnapshot = exports.parseTodoDependencies = exports.EventAggregator = exports.getRandomPhrase = exports.PHRASE_CATEGORIES = exports.ALL_PHRASES = exports.HIGHLIGHT_CSS = exports.clearHighlightCache = void 0;
9
9
  exports.isZaiProviderId = exports.inferZaiQuotaState = exports.extractNextFlushTime = exports.accumulateZaiUsage = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = exports.fetchCodexQuotaFromApi = exports.getWorkspaceIdFromPath = exports.pruneQuotaHistory = exports.readQuotaHistoryDailyBuckets = exports.readQuotaHistoryRange = exports.appendQuotaHistorySample = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.fetchQuota = exports.AutoSwitchController = exports.decideAutoSwitch = exports.DEFAULT_AUTO_SWITCH_CONFIG = exports.removeLauncher = exports.writeLauncher = exports.isShellHookInstalled = exports.uninstallShellHook = exports.installShellHook = exports.setTerminalActiveProfile = exports.removeCodexAccount = exports.switchToCodexAccount = exports.finalizeCodexAccount = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.readClaudeProfileIdentity = exports.isClaudeProfileAuthenticated = exports.claudeKeychainService = exports.claudeKeychainSuffix = exports.getClaudeProfileHome = exports.getClaudeProfilesDir = exports.getActiveAccountStatus = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = void 0;
10
- exports.quotaFailureDescriptorSchema = exports.peakHoursStateSchema = exports.quotaSourceSchema = exports.quotaProviderIdSchema = exports.quotaFailureKindSchema = exports.quotaStateSchema = exports.quotaWindowSchema = exports.extractSessionEvents = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.gatherAssetsForCwd = exports.codexSessions = exports.readCodexAssets = exports.claudeSessions = exports.readClaudeAssets = exports.extractCommands = exports.extractFilePaths = exports.extractUrls = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.ZaiQuotaWatcher = exports.ZAI_TIER_BUDGETS = exports.ZAI_PROVIDER_IDS = exports.ZAI_PROMPT_INVOCATIONS = exports.turnTokenWeight = exports.rowsToZaiTurnsAndErrors = exports.resolveZaiTier = exports.parseZaiQuotaError = exports.makeUnavailableZaiQuotaState = void 0;
11
- exports.scopePeakHoursToSessionProvider = exports.isClaudeCodeSessionProvider = exports.fetchPeakHoursStatus = exports.createPeakHoursNotApplicableState = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.assistantTurnToolRefSchema = exports.assistantTurnToolGroupStepSchema = exports.assistantTurnTimelineItemSchema = exports.assistantTurnSubagentStatusSchema = exports.assistantTurnSubagentSchema = exports.assistantTurnReasoningTimelineItemSchema = exports.assistantTurnProjectionSchema = exports.assistantTurnProcessStepSchema = exports.assistantTurnProcessSchema = exports.assistantTurnNarrationStepSchema = exports.assistantTurnEventTypeSchema = exports.assistantTurnEventSchema = exports.listAllAccountsResultSchema = exports.savedAccountProfileSchema = exports.accountEntrySchema = exports.accountLoginStatusSchema = exports.beginAccountLoginResultSchema = exports.accountManagerResultSchema = exports.accountProviderIdSchema = exports.activeAccountStatusSchema = exports.activeProviderAccountStatusSchema = exports.quotaHistoryDailyBucketSchema = exports.quotaHistorySampleSchema = exports.quotaHistoryRuntimeProviderSchema = exports.providerQuotaMapSchema = exports.zaiProviderQuotaStateSchema = exports.codexProviderQuotaStateSchema = exports.claudeProviderQuotaStateSchema = exports.providerQuotaStateSchema = exports.runtimeQuotaProviderSchema = void 0;
10
+ exports.quotaFailureKindSchema = exports.quotaStateSchema = exports.quotaWindowSchema = exports.extractSessionEvents = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.gatherAssetsForCwd = exports.codexSessions = exports.readCodexAssets = exports.claudeSessions = exports.readClaudeAssets = exports.extractCommands = exports.extractFilePaths = exports.extractUrls = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.resolveZaiQuota = exports.readZaiCredentials = exports.quotaStateFromZaiQuotaLimitPayload = exports.fetchZaiQuotaFromApi = exports.ZaiQuotaWatcher = exports.ZAI_TIER_BUDGETS = exports.ZAI_PROVIDER_IDS = exports.ZAI_PROMPT_INVOCATIONS = exports.turnTokenWeight = exports.rowsToZaiTurnsAndErrors = exports.resolveZaiTier = exports.parseZaiQuotaError = exports.makeUnavailableZaiQuotaState = void 0;
11
+ exports.scopePeakHoursToSessionProvider = exports.isClaudeCodeSessionProvider = exports.fetchPeakHoursStatus = exports.createPeakHoursNotApplicableState = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.assistantTurnToolRefSchema = exports.assistantTurnToolGroupStepSchema = exports.assistantTurnTimelineItemSchema = exports.assistantTurnSubagentStatusSchema = exports.assistantTurnSubagentSchema = exports.assistantTurnReasoningTimelineItemSchema = exports.assistantTurnProjectionSchema = exports.assistantTurnProcessStepSchema = exports.assistantTurnProcessSchema = exports.assistantTurnNarrationStepSchema = exports.assistantTurnEventTypeSchema = exports.assistantTurnEventSchema = exports.listAllAccountsResultSchema = exports.savedAccountProfileSchema = exports.accountEntrySchema = exports.accountLoginStatusSchema = exports.beginAccountLoginResultSchema = exports.accountManagerResultSchema = exports.accountProviderIdSchema = exports.activeAccountStatusSchema = exports.activeProviderAccountStatusSchema = exports.quotaHistoryDailyBucketSchema = exports.quotaHistorySampleSchema = exports.quotaHistoryRuntimeProviderSchema = exports.providerQuotaMapSchema = exports.zaiProviderQuotaStateSchema = exports.codexProviderQuotaStateSchema = exports.claudeProviderQuotaStateSchema = exports.providerQuotaStateSchema = exports.runtimeQuotaProviderSchema = exports.quotaFailureDescriptorSchema = exports.peakHoursStateSchema = exports.quotaSourceSchema = exports.quotaProviderIdSchema = void 0;
12
12
  var taskPersistence_1 = require("./types/taskPersistence");
13
13
  Object.defineProperty(exports, "TASK_PERSISTENCE_SCHEMA_VERSION", { enumerable: true, get: function () { return taskPersistence_1.TASK_PERSISTENCE_SCHEMA_VERSION; } });
14
14
  Object.defineProperty(exports, "normalizeTaskStatus", { enumerable: true, get: function () { return taskPersistence_1.normalizeTaskStatus; } });
@@ -311,6 +311,11 @@ Object.defineProperty(exports, "ZAI_PROVIDER_IDS", { enumerable: true, get: func
311
311
  Object.defineProperty(exports, "ZAI_TIER_BUDGETS", { enumerable: true, get: function () { return zaiQuota_1.ZAI_TIER_BUDGETS; } });
312
312
  var zaiQuotaWatcher_1 = require("./zaiQuotaWatcher");
313
313
  Object.defineProperty(exports, "ZaiQuotaWatcher", { enumerable: true, get: function () { return zaiQuotaWatcher_1.ZaiQuotaWatcher; } });
314
+ var zaiQuotaApi_1 = require("./zaiQuotaApi");
315
+ Object.defineProperty(exports, "fetchZaiQuotaFromApi", { enumerable: true, get: function () { return zaiQuotaApi_1.fetchZaiQuotaFromApi; } });
316
+ Object.defineProperty(exports, "quotaStateFromZaiQuotaLimitPayload", { enumerable: true, get: function () { return zaiQuotaApi_1.quotaStateFromZaiQuotaLimitPayload; } });
317
+ Object.defineProperty(exports, "readZaiCredentials", { enumerable: true, get: function () { return zaiQuotaApi_1.readZaiCredentials; } });
318
+ Object.defineProperty(exports, "resolveZaiQuota", { enumerable: true, get: function () { return zaiQuotaApi_1.resolveZaiQuota; } });
314
319
  var multiProviderQuotaService_1 = require("./multiProviderQuotaService");
315
320
  Object.defineProperty(exports, "MultiProviderQuotaService", { enumerable: true, get: function () { return multiProviderQuotaService_1.MultiProviderQuotaService; } });
316
321
  // Model Context
@@ -93,10 +93,10 @@ export declare class OpenCodeProvider implements SessionProviderBase {
93
93
  * Derives a z.ai coding-plan `QuotaState` from OpenCode assistant turns
94
94
  * tagged with `providerID ∈ {zai, zai-coding-plan}`.
95
95
  *
96
- * z.ai does not expose a quota API (verified vs. docs.z.ai/openapi.json),
97
- * so this method accumulates observed per-turn tokens from OpenCode's DB
98
- * into 5-hour / 7-day rolling windows and infers utilization against the
99
- * configured tier budget. Returns null when no z.ai routing is detected.
96
+ * This compatibility estimator predates the authoritative z.ai quota API.
97
+ * New product code should use `zaiQuotaApi.ts`; this method remains for
98
+ * consumers that still want a local OpenCode DB-derived estimate. Returns
99
+ * null when no z.ai routing is detected.
100
100
  *
101
101
  * @param tier Plan tier; `'auto'` infers from observed weekly volume.
102
102
  * When the caller has no opinion, the heuristic falls back to
@@ -1572,15 +1572,15 @@ class OpenCodeProvider {
1572
1572
  this.sessionMetaCache.clear();
1573
1573
  this.dynamicContextWindowLimit = null;
1574
1574
  }
1575
- // --- z.ai (GLM Coding Plan) quota derivation ---
1575
+ // --- z.ai (GLM Coding Plan) compatibility quota derivation ---
1576
1576
  /**
1577
1577
  * Derives a z.ai coding-plan `QuotaState` from OpenCode assistant turns
1578
1578
  * tagged with `providerID ∈ {zai, zai-coding-plan}`.
1579
1579
  *
1580
- * z.ai does not expose a quota API (verified vs. docs.z.ai/openapi.json),
1581
- * so this method accumulates observed per-turn tokens from OpenCode's DB
1582
- * into 5-hour / 7-day rolling windows and infers utilization against the
1583
- * configured tier budget. Returns null when no z.ai routing is detected.
1580
+ * This compatibility estimator predates the authoritative z.ai quota API.
1581
+ * New product code should use `zaiQuotaApi.ts`; this method remains for
1582
+ * consumers that still want a local OpenCode DB-derived estimate. Returns
1583
+ * null when no z.ai routing is detected.
1584
1584
  *
1585
1585
  * @param tier Plan tier; `'auto'` infers from observed weekly volume.
1586
1586
  * When the caller has no opinion, the heuristic falls back to
@@ -3,7 +3,7 @@ import type { AccountProviderId } from './accountRegistry';
3
3
  /**
4
4
  * Storage key for a quota snapshot. Extends `AccountProviderId` with `'zai'`
5
5
  * because z.ai has no full account-management surface in v1, but the snapshot
6
- * store still needs a stable key for the derived z.ai quota cache.
6
+ * store still needs a stable key for the z.ai quota cache.
7
7
  */
8
8
  export type QuotaSnapshotProviderId = AccountProviderId | 'zai';
9
9
  export declare function writeQuotaSnapshot(providerId: QuotaSnapshotProviderId, accountId: string, quota: QuotaState): void;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Stateless quota accumulator for z.ai coding plans.
2
+ * Deprecated stateless quota accumulator for z.ai coding plans.
3
3
  *
4
- * z.ai (GLM Coding Plan) does not expose an HTTP quota/usage API (verified
5
- * against the canonical OpenAPI spec at https://docs.z.ai/openapi.json).
6
- * The only programmatic signals are:
4
+ * This estimator is kept for compatibility with consumers that imported the
5
+ * old helpers directly. Product code should use `zaiQuotaApi.ts`, which calls
6
+ * z.ai's authoritative quota endpoint. The legacy estimator only uses:
7
7
  *
8
8
  * 1. Per-call token `usage` returned inline on each chat completion —
9
9
  * which OpenCode persists into `opencode.db` message rows tagged with
@@ -13,8 +13,8 @@
13
13
  * in the human-readable message text.
14
14
  *
15
15
  * This module is therefore a **derivation** rather than a fetcher: callers
16
- * feed it observed assistant turns (and any error events), and it computes
17
- * an estimated `QuotaState` keyed against the published per-tier budgets.
16
+ * feed it observed assistant turns (and any error events), and it computes a
17
+ * compatibility `QuotaState` keyed against the published per-tier budgets.
18
18
  *
19
19
  * Polling / eventing / persistence are the caller's responsibility —
20
20
  * see `ZaiQuotaWatcher` for the bundled watcher.
@@ -0,0 +1,33 @@
1
+ import type { QuotaState } from './quota';
2
+ import type { ProviderQuotaState } from './providerQuota';
3
+ type ZaiSnapshotProvider = 'zai';
4
+ type SnapshotReader = (providerId: ZaiSnapshotProvider, accountId: string) => QuotaState | null;
5
+ type SnapshotWriter = (providerId: ZaiSnapshotProvider, accountId: string, quota: QuotaState) => void;
6
+ export type ZaiPlatform = 'ZAI' | 'ZHIPU';
7
+ export type ZaiCredentialSource = 'opencode' | 'env';
8
+ export interface ZaiCredentials {
9
+ authToken: string;
10
+ baseUrl: string;
11
+ platform: ZaiPlatform;
12
+ source: ZaiCredentialSource;
13
+ }
14
+ export interface ReadZaiCredentialsOptions {
15
+ openCodeDataDir?: string;
16
+ env?: Record<string, string | undefined>;
17
+ }
18
+ export interface ZaiQuotaApiOptions extends ReadZaiCredentialsOptions {
19
+ credentials?: ZaiCredentials | null;
20
+ fetchImpl?: typeof fetch;
21
+ capturedAt?: string;
22
+ timeoutMs?: number;
23
+ }
24
+ export interface ZaiQuotaResolveOptions extends ZaiQuotaApiOptions {
25
+ accountId?: string;
26
+ readSnapshot?: SnapshotReader;
27
+ writeSnapshot?: SnapshotWriter;
28
+ }
29
+ export declare function readZaiCredentials(options?: ReadZaiCredentialsOptions): ZaiCredentials | null;
30
+ export declare function quotaStateFromZaiQuotaLimitPayload(payload: unknown, capturedAt?: string): QuotaState;
31
+ export declare function fetchZaiQuotaFromApi(options?: ZaiQuotaApiOptions): Promise<QuotaState>;
32
+ export declare function resolveZaiQuota(options?: ZaiQuotaResolveOptions): Promise<ProviderQuotaState<'zai'>>;
33
+ export {};
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readZaiCredentials = readZaiCredentials;
7
+ exports.quotaStateFromZaiQuotaLimitPayload = quotaStateFromZaiQuotaLimitPayload;
8
+ exports.fetchZaiQuotaFromApi = fetchZaiQuotaFromApi;
9
+ exports.resolveZaiQuota = resolveZaiQuota;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const quotaSnapshots_1 = require("./quotaSnapshots");
14
+ const DEFAULT_ZAI_BASE_URL = 'https://api.z.ai/api/anthropic';
15
+ const QUOTA_PATH = '/api/monitor/usage/quota/limit';
16
+ const DEFAULT_ACCOUNT_ID = 'default';
17
+ const DEFAULT_TIMEOUT_MS = 10_000;
18
+ function unavailableZaiQuotaState(error, meta = {}, capturedAt = new Date().toISOString()) {
19
+ return {
20
+ fiveHour: { utilization: 0, resetsAt: '' },
21
+ sevenDay: { utilization: 0, resetsAt: '' },
22
+ available: false,
23
+ error,
24
+ providerId: 'zai',
25
+ source: 'api',
26
+ capturedAt,
27
+ fiveHourLabel: '5-Hour',
28
+ sevenDayLabel: 'Weekly',
29
+ ...meta,
30
+ };
31
+ }
32
+ function openCodeDataDirCandidates() {
33
+ const candidates = [];
34
+ const xdg = process.env.XDG_DATA_HOME;
35
+ if (xdg)
36
+ candidates.push(path_1.default.join(xdg, 'opencode'));
37
+ candidates.push(path_1.default.join(os_1.default.homedir(), '.local', 'share', 'opencode'));
38
+ if (process.platform === 'darwin') {
39
+ candidates.push(path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support', 'opencode'));
40
+ }
41
+ else if (process.platform === 'win32') {
42
+ candidates.push(path_1.default.join(process.env.LOCALAPPDATA || process.env.APPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'opencode'));
43
+ }
44
+ return Array.from(new Set(candidates));
45
+ }
46
+ function readOpenCodeAuthToken(openCodeDataDir) {
47
+ const candidates = openCodeDataDir ? [openCodeDataDir] : openCodeDataDirCandidates();
48
+ for (const dataDir of candidates) {
49
+ try {
50
+ const parsed = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dataDir, 'auth.json'), 'utf8'));
51
+ const codingPlan = parsed['zai-coding-plan'];
52
+ const zai = parsed.zai;
53
+ if (typeof codingPlan?.key === 'string' && codingPlan.key.trim())
54
+ return codingPlan.key.trim();
55
+ if (typeof zai?.key === 'string' && zai.key.trim())
56
+ return zai.key.trim();
57
+ }
58
+ catch {
59
+ // Try the next candidate.
60
+ }
61
+ }
62
+ return null;
63
+ }
64
+ function platformFromBaseUrl(baseUrl) {
65
+ if (baseUrl.includes('api.z.ai'))
66
+ return 'ZAI';
67
+ if (baseUrl.includes('open.bigmodel.cn') || baseUrl.includes('dev.bigmodel.cn'))
68
+ return 'ZHIPU';
69
+ return null;
70
+ }
71
+ function readZaiCredentials(options = {}) {
72
+ const openCodeToken = readOpenCodeAuthToken(options.openCodeDataDir);
73
+ if (openCodeToken) {
74
+ return {
75
+ authToken: openCodeToken,
76
+ baseUrl: DEFAULT_ZAI_BASE_URL,
77
+ platform: 'ZAI',
78
+ source: 'opencode',
79
+ };
80
+ }
81
+ const env = options.env ?? process.env;
82
+ const authToken = env.ANTHROPIC_AUTH_TOKEN?.trim();
83
+ const baseUrl = env.ANTHROPIC_BASE_URL?.trim();
84
+ if (!authToken || !baseUrl)
85
+ return null;
86
+ const platform = platformFromBaseUrl(baseUrl);
87
+ if (!platform)
88
+ return null;
89
+ return {
90
+ authToken,
91
+ baseUrl,
92
+ platform,
93
+ source: 'env',
94
+ };
95
+ }
96
+ function quotaLimitUrl(baseUrl) {
97
+ try {
98
+ const parsed = new URL(baseUrl);
99
+ return `${parsed.protocol}//${parsed.host}${QUOTA_PATH}`;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ function tokenLimitEntries(payload) {
106
+ const limits = payload.data?.limits ?? payload.limits ?? [];
107
+ return limits.filter((item) => item?.type === 'TOKENS_LIMIT' && typeof item.percentage === 'number');
108
+ }
109
+ function isFiveHourLimit(item) {
110
+ return item.type === 'TOKENS_LIMIT' && item.unit === 3 && item.number === 5;
111
+ }
112
+ function isWeeklyLimit(item) {
113
+ return item.type === 'TOKENS_LIMIT' && item.unit === 6 && item.number === 1;
114
+ }
115
+ function isoFromEpochMs(value) {
116
+ if (value == null)
117
+ return '';
118
+ const ms = typeof value === 'number' ? value : Number(value);
119
+ if (!Number.isFinite(ms) || ms <= 0)
120
+ return '';
121
+ return new Date(ms).toISOString();
122
+ }
123
+ function displayPlanName(level) {
124
+ if (!level)
125
+ return 'z.ai Coding Plan';
126
+ return `z.ai ${level.charAt(0).toUpperCase()}${level.slice(1)}`;
127
+ }
128
+ function quotaStateFromZaiQuotaLimitPayload(payload, capturedAt = new Date().toISOString()) {
129
+ const data = payload;
130
+ const tokenLimits = tokenLimitEntries(data);
131
+ const fiveHourLimit = tokenLimits.find(isFiveHourLimit) ?? tokenLimits[0];
132
+ const weeklyLimit = tokenLimits.find(isWeeklyLimit) ?? tokenLimits.find(item => item !== fiveHourLimit);
133
+ const level = data.data?.level;
134
+ if (!fiveHourLimit || !weeklyLimit) {
135
+ return unavailableZaiQuotaState('z.ai quota API returned no token quota windows.', {
136
+ failureKind: 'unknown',
137
+ }, capturedAt);
138
+ }
139
+ return {
140
+ fiveHour: {
141
+ utilization: fiveHourLimit.percentage ?? 0,
142
+ resetsAt: isoFromEpochMs(fiveHourLimit.nextResetTime),
143
+ },
144
+ sevenDay: {
145
+ utilization: weeklyLimit.percentage ?? 0,
146
+ resetsAt: isoFromEpochMs(weeklyLimit.nextResetTime),
147
+ },
148
+ available: true,
149
+ providerId: 'zai',
150
+ source: 'api',
151
+ capturedAt,
152
+ fiveHourLabel: '5-Hour',
153
+ sevenDayLabel: 'Weekly',
154
+ planType: level,
155
+ limitId: level ? `zai-${level}` : 'zai-coding-plan',
156
+ limitName: displayPlanName(level),
157
+ };
158
+ }
159
+ function parseRetryAfterMs(retryAfter) {
160
+ if (!retryAfter)
161
+ return undefined;
162
+ const seconds = Number(retryAfter);
163
+ if (Number.isFinite(seconds) && seconds >= 0)
164
+ return seconds * 1000;
165
+ const dateMs = Date.parse(retryAfter);
166
+ if (Number.isFinite(dateMs))
167
+ return Math.max(0, dateMs - Date.now());
168
+ return undefined;
169
+ }
170
+ function failureKindForStatus(status) {
171
+ if (status === 401 || status === 403)
172
+ return 'auth';
173
+ if (status === 429)
174
+ return 'rate_limit';
175
+ if (status >= 500)
176
+ return 'server';
177
+ return 'unknown';
178
+ }
179
+ function sanitizeErrorMessage(message, authToken) {
180
+ let result = message;
181
+ if (authToken) {
182
+ result = result.split(authToken).join('<redacted>');
183
+ }
184
+ return result.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, 'Bearer <redacted>');
185
+ }
186
+ async function fetchZaiQuotaFromApi(options = {}) {
187
+ const capturedAt = options.capturedAt ?? new Date().toISOString();
188
+ const credentials = options.credentials ?? readZaiCredentials(options);
189
+ if (!credentials) {
190
+ return unavailableZaiQuotaState('No z.ai credentials found. Sign in to OpenCode with z.ai or set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN.', { failureKind: 'auth' }, capturedAt);
191
+ }
192
+ const url = quotaLimitUrl(credentials.baseUrl);
193
+ if (!url) {
194
+ return unavailableZaiQuotaState('Invalid z.ai base URL.', { failureKind: 'unknown' }, capturedAt);
195
+ }
196
+ const fetchImpl = options.fetchImpl ?? fetch;
197
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
198
+ const controller = new AbortController();
199
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
200
+ timeout.unref?.();
201
+ try {
202
+ const response = await fetchImpl(url, {
203
+ method: 'GET',
204
+ headers: {
205
+ Authorization: credentials.authToken,
206
+ 'Accept-Language': 'en-US,en',
207
+ Accept: 'application/json',
208
+ 'Content-Type': 'application/json',
209
+ },
210
+ signal: controller.signal,
211
+ });
212
+ const body = await response.text();
213
+ let parsed;
214
+ try {
215
+ parsed = JSON.parse(body);
216
+ }
217
+ catch {
218
+ parsed = null;
219
+ }
220
+ if (!response.ok) {
221
+ const failureKind = failureKindForStatus(response.status);
222
+ const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'));
223
+ const detail = parsed && typeof parsed === 'object' && 'msg' in parsed
224
+ ? String(parsed.msg ?? '')
225
+ : '';
226
+ const baseMessage = failureKind === 'auth'
227
+ ? `z.ai quota API rejected credentials (HTTP ${response.status}).`
228
+ : `z.ai quota API error (HTTP ${response.status}).`;
229
+ return unavailableZaiQuotaState(sanitizeErrorMessage([baseMessage, detail].filter(Boolean).join(' '), credentials.authToken), { failureKind, httpStatus: response.status, retryAfterMs }, capturedAt);
230
+ }
231
+ return quotaStateFromZaiQuotaLimitPayload(parsed, capturedAt);
232
+ }
233
+ catch (error) {
234
+ const isAbort = error instanceof Error && error.name === 'AbortError';
235
+ return unavailableZaiQuotaState(isAbort ? 'z.ai quota API timed out.' : 'z.ai quota API network error.', { failureKind: 'network' }, capturedAt);
236
+ }
237
+ finally {
238
+ clearTimeout(timeout);
239
+ }
240
+ }
241
+ function enrichZaiQuota(state) {
242
+ return {
243
+ ...state,
244
+ runtimeProvider: 'zai',
245
+ providerId: 'zai',
246
+ fiveHourLabel: state.fiveHourLabel ?? '5-Hour',
247
+ sevenDayLabel: state.sevenDayLabel ?? 'Weekly',
248
+ };
249
+ }
250
+ async function resolveZaiQuota(options = {}) {
251
+ const accountId = options.accountId ?? DEFAULT_ACCOUNT_ID;
252
+ const readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
253
+ const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
254
+ const apiQuota = await fetchZaiQuotaFromApi(options);
255
+ if (apiQuota.available) {
256
+ writeSnapshot('zai', accountId, apiQuota);
257
+ return enrichZaiQuota(apiQuota);
258
+ }
259
+ const cached = readSnapshot('zai', accountId);
260
+ if (cached) {
261
+ return enrichZaiQuota({
262
+ ...cached,
263
+ providerId: 'zai',
264
+ source: 'cache',
265
+ stale: true,
266
+ fiveHourLabel: cached.fiveHourLabel ?? '5-Hour',
267
+ sevenDayLabel: cached.sevenDayLabel ?? 'Weekly',
268
+ });
269
+ }
270
+ return enrichZaiQuota(apiQuota);
271
+ }
@@ -2,15 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZaiQuotaWatcher = void 0;
4
4
  /**
5
- * Event-driven watcher that derives a z.ai coding-plan `QuotaState` from
6
- * observed OpenCode assistant turns.
5
+ * Deprecated event-driven watcher that derives a z.ai coding-plan
6
+ * `QuotaState` from observed OpenCode assistant turns.
7
7
  *
8
- * z.ai does not expose a quota API, so this watcher maintains rolling
9
- * 5-hour / 7-day windows in memory and recomputes an estimated `QuotaState`
10
- * whenever a new turn arrives. The consumer is responsible for feeding it
11
- * z.ai-routed assistant turns (and any trapped error events) — typically
12
- * by subscribing to `SessionMonitor.onSessionEvent` and filtering on
13
- * `providerID ∈ {zai, zai-coding-plan}`.
8
+ * This watcher is retained for compatibility. Product code now uses
9
+ * `zaiQuotaApi.ts` to call z.ai's authoritative quota endpoint. The legacy
10
+ * watcher maintains rolling 5-hour / 7-day windows in memory and recomputes a
11
+ * derived `QuotaState` whenever a new turn arrives. The consumer is
12
+ * responsible for feeding it z.ai-routed assistant turns (and any trapped
13
+ * error events) — typically by subscribing to `SessionMonitor.onSessionEvent`
14
+ * and filtering on `providerID ∈ {zai, zai-coding-plan}`.
14
15
  *
15
16
  * Optionally writes snapshots + history samples for cross-session cache
16
17
  * and trend visualisation, mirroring `CodexQuotaWatcher`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",