sidekick-shared 0.24.5 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +205 -52
- package/dist/accountChangeSignal.d.ts +3 -0
- package/dist/accountChangeSignal.js +19 -0
- package/dist/accountChanges.d.ts +21 -0
- package/dist/accountChanges.js +170 -0
- package/dist/accountRegistry.d.ts +25 -2
- package/dist/accountRegistry.js +41 -13
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +19 -4
- package/dist/browser.js +43 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/codexQuotaWatcher.d.ts +6 -0
- package/dist/codexQuotaWatcher.js +101 -47
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +67 -23
- package/dist/index.js +138 -26
- package/dist/modelAliases.d.ts +13 -0
- package/dist/modelAliases.js +58 -0
- package/dist/modelCatalog.d.ts +32 -0
- package/dist/modelCatalog.js +145 -0
- package/dist/modelContext.d.ts +36 -3
- package/dist/modelContext.js +109 -34
- package/dist/modelInfo.d.ts +30 -1
- package/dist/modelInfo.js +116 -26
- package/dist/multiProviderQuotaService.d.ts +8 -0
- package/dist/multiProviderQuotaService.js +64 -3
- package/dist/node.d.ts +23 -4
- package/dist/node.js +50 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +72 -6
- package/dist/observedSessionCollector.js +440 -47
- package/dist/parsers/codexHistory.d.ts +4 -2
- package/dist/parsers/codexHistory.js +9 -2
- package/dist/parsers/sessionActivityDetector.d.ts +28 -0
- package/dist/parsers/sessionActivityDetector.js +110 -15
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providerIds.d.ts +11 -0
- package/dist/providerIds.js +9 -0
- package/dist/providerQuota.d.ts +3 -1
- package/dist/providerQuota.js +3 -0
- package/dist/providers/claudeCode.d.ts +10 -1
- package/dist/providers/claudeCode.js +211 -131
- package/dist/providers/codex.d.ts +20 -1
- package/dist/providers/codex.js +308 -244
- package/dist/providers/codexDatabase.d.ts +9 -0
- package/dist/providers/codexDatabase.js +107 -2
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/diagnostics.d.ts +13 -0
- package/dist/providers/diagnostics.js +88 -0
- package/dist/providers/factory.d.ts +20 -0
- package/dist/providers/factory.js +57 -0
- package/dist/providers/openCode.d.ts +22 -1
- package/dist/providers/openCode.js +460 -126
- package/dist/providers/openCodeDatabase.d.ts +10 -0
- package/dist/providers/openCodeDatabase.js +69 -0
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +82 -2
- package/dist/providers/types.js +4 -0
- package/dist/quota.d.ts +34 -4
- package/dist/quota.js +37 -9
- package/dist/quotaHistory.d.ts +3 -2
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaPoller.d.ts +13 -1
- package/dist/quotaPoller.js +65 -3
- package/dist/quotaResolver.d.ts +81 -0
- package/dist/quotaResolver.js +213 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/accountManager.d.ts +2 -2
- package/dist/schemas/accountManager.js +2 -4
- package/dist/schemas/accountStatus.d.ts +3 -0
- package/dist/schemas/accountStatus.js +1 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +16 -2
- package/dist/schemas/observedSessionV1.d.ts +27 -26
- package/dist/schemas/observedSessionV1.js +6 -4
- package/dist/schemas/quota.d.ts +141 -2
- package/dist/schemas/quota.js +20 -8
- package/dist/schemas/quotaHistory.d.ts +3 -2
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/tokenEstimation.d.ts +1 -1
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionMonitor.d.ts +15 -0
- package/dist/sessionMonitor.js +142 -0
- package/dist/sessionPreviews.d.ts +33 -2
- package/dist/sessionPreviews.js +333 -6
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +8 -2
- package/dist/types/observedSessionV1.js +88 -25
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +22 -1
package/dist/quotaHistory.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* dashboard, the `sidekick quota history` CLI, external consumers) can render heatmaps and
|
|
7
7
|
* trend visualisations over a 13-week window.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import type { RuntimeQuotaProviderId } from './providerIds';
|
|
10
|
+
export type QuotaHistoryRuntimeProvider = RuntimeQuotaProviderId;
|
|
10
11
|
export interface QuotaHistorySample {
|
|
11
12
|
timestamp: string;
|
|
12
13
|
runtimeProvider: QuotaHistoryRuntimeProvider;
|
|
@@ -22,7 +23,7 @@ export interface QuotaHistorySample {
|
|
|
22
23
|
};
|
|
23
24
|
available: boolean;
|
|
24
25
|
error?: string;
|
|
25
|
-
source?: 'session' | 'cache' | 'api';
|
|
26
|
+
source?: 'session' | 'cache' | 'api' | 'statusline';
|
|
26
27
|
stale?: boolean;
|
|
27
28
|
}
|
|
28
29
|
export interface QuotaHistoryAppendOptions {
|
package/dist/quotaHistory.js
CHANGED
|
@@ -50,8 +50,10 @@ exports._resetQuotaHistoryInMemoryStateForTests = _resetQuotaHistoryInMemoryStat
|
|
|
50
50
|
const crypto = __importStar(require("crypto"));
|
|
51
51
|
const fs = __importStar(require("fs"));
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
|
+
const formatting_1 = require("./formatting");
|
|
53
54
|
const paths_1 = require("./paths");
|
|
54
55
|
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
56
|
+
const atomic_1 = require("./writers/atomic");
|
|
55
57
|
const DEFAULT_MIN_INTERVAL_MS = 60_000;
|
|
56
58
|
const DEFAULT_RETENTION_DAYS = 91;
|
|
57
59
|
const PRUNE_FILESIZE_THRESHOLD = 16 * 1024;
|
|
@@ -150,21 +152,16 @@ function readLastSampleTimestampMs(filePath) {
|
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Append-and-prune under the shared cross-process lock. Without it a prune
|
|
157
|
+
* rewrite (rename over the file) could race another process's append, and
|
|
158
|
+
* the appended line would land on the old, unlinked inode.
|
|
159
|
+
*/
|
|
160
|
+
function withHistoryFileLock(filePath, operation) {
|
|
161
|
+
return (0, atomic_1.withFileLockSync)(`${filePath}.lock`, operation);
|
|
162
|
+
}
|
|
153
163
|
function atomicRewriteFile(filePath, contents) {
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
fs.writeFileSync(tmp, contents, { encoding: 'utf8', mode: 0o600 });
|
|
157
|
-
fs.renameSync(tmp, filePath);
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
try {
|
|
161
|
-
fs.rmSync(tmp, { force: true });
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// Best effort cleanup only.
|
|
165
|
-
}
|
|
166
|
-
throw error;
|
|
167
|
-
}
|
|
164
|
+
(0, atomic_1.atomicWriteFileSync)(filePath, contents, 0o600);
|
|
168
165
|
}
|
|
169
166
|
function pruneFileSync(filePath, retentionDays) {
|
|
170
167
|
let stat;
|
|
@@ -245,18 +242,19 @@ async function runAppend(sample, filePath, options) {
|
|
|
245
242
|
}
|
|
246
243
|
ensureHistoryDir(sample.workspaceId);
|
|
247
244
|
const line = JSON.stringify(sample) + '\n';
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
245
|
+
withHistoryFileLock(filePath, () => {
|
|
246
|
+
fs.appendFileSync(filePath, line, { encoding: 'utf8', mode: 0o600 });
|
|
247
|
+
// Opportunistic prune. Skip when the file is small to avoid pointless rewrite churn.
|
|
248
|
+
try {
|
|
249
|
+
if (fs.statSync(filePath).size >= PRUNE_FILESIZE_THRESHOLD) {
|
|
250
|
+
pruneFileSync(filePath, options.retentionDays);
|
|
251
|
+
}
|
|
255
252
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
253
|
+
catch {
|
|
254
|
+
// ignore
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
lastWriteCache.set(filePath, Number.isFinite(sampleTs) ? sampleTs : Date.now());
|
|
260
258
|
// Backwards-compat: keep the latest-snapshot store hot so existing callers (DashboardViewProvider,
|
|
261
259
|
// codex session provider, external consumers) don't have to query history for "latest".
|
|
262
260
|
try {
|
|
@@ -323,23 +321,26 @@ async function readQuotaHistoryRange(options) {
|
|
|
323
321
|
samples.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
324
322
|
return samples;
|
|
325
323
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return
|
|
324
|
+
/**
|
|
325
|
+
* Daily buckets are keyed by the machine's local calendar day, matching the
|
|
326
|
+
* `today` brief, the historical-data store, and the dashboard date filters.
|
|
327
|
+
*/
|
|
328
|
+
function localDateString(ms) {
|
|
329
|
+
return (0, formatting_1.formatLocalDateKey)(ms);
|
|
332
330
|
}
|
|
333
331
|
async function readQuotaHistoryDailyBuckets(options) {
|
|
334
332
|
const samples = await readQuotaHistoryRange(options);
|
|
335
333
|
const { fromMs: defaultFromMs, toMs: defaultToMs } = defaultRangeMs();
|
|
336
334
|
const fromMs = options.from ? Date.parse(options.from) : defaultFromMs;
|
|
337
335
|
const toMs = options.to ? Date.parse(options.to) : defaultToMs;
|
|
338
|
-
const startDate =
|
|
339
|
-
const endDate =
|
|
336
|
+
const startDate = localDateString(fromMs);
|
|
337
|
+
const endDate = localDateString(toMs);
|
|
340
338
|
const grouped = new Map();
|
|
341
339
|
for (const sample of samples) {
|
|
342
|
-
const
|
|
340
|
+
const sampleMs = Date.parse(sample.timestamp);
|
|
341
|
+
if (!Number.isFinite(sampleMs))
|
|
342
|
+
continue;
|
|
343
|
+
const day = localDateString(sampleMs);
|
|
343
344
|
const bucket = grouped.get(day);
|
|
344
345
|
if (bucket) {
|
|
345
346
|
bucket.push(sample);
|
|
@@ -389,7 +390,7 @@ async function readQuotaHistoryDailyBuckets(options) {
|
|
|
389
390
|
anyUnavailable,
|
|
390
391
|
});
|
|
391
392
|
}
|
|
392
|
-
cursor =
|
|
393
|
+
cursor = (0, formatting_1.addLocalDays)(cursor, 1);
|
|
393
394
|
}
|
|
394
395
|
return buckets;
|
|
395
396
|
}
|
package/dist/quotaPoller.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module quotaPoller
|
|
8
8
|
*/
|
|
9
9
|
import type { QuotaState } from './quota';
|
|
10
|
+
import { type AccountsChangedEvent } from './accountChanges';
|
|
10
11
|
/** Disposable subscription handle. */
|
|
11
12
|
export interface Disposable {
|
|
12
13
|
dispose(): void;
|
|
@@ -20,7 +21,11 @@ export interface QuotaPollerOptions {
|
|
|
20
21
|
/** Maximum backoff delay for retries (ms). Default: 120_000 (2 minutes). */
|
|
21
22
|
maxBackoffMs?: number;
|
|
22
23
|
/** Returns the current access token. Called before each fetch. */
|
|
23
|
-
getAccessToken: () => Promise<string>;
|
|
24
|
+
getAccessToken: () => Promise<string | null | undefined>;
|
|
25
|
+
/** Optional cheap account-presence check performed before credential access. */
|
|
26
|
+
hasAccount?: () => boolean | Promise<boolean>;
|
|
27
|
+
/** Override the process-wide login/logout signal (primarily for embedded hosts/tests). */
|
|
28
|
+
subscribeAccountsChanged?: (listener: (event: AccountsChangedEvent) => void) => Disposable;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Polls Claude Max quota usage with exponential backoff on transient errors,
|
|
@@ -47,12 +52,18 @@ export declare class QuotaPoller {
|
|
|
47
52
|
private readonly idleIntervalMs;
|
|
48
53
|
private readonly maxBackoffMs;
|
|
49
54
|
private readonly getAccessToken;
|
|
55
|
+
private readonly hasAccount?;
|
|
56
|
+
private readonly subscribeAccountsChanged;
|
|
50
57
|
private timer;
|
|
51
58
|
private listeners;
|
|
52
59
|
private latest;
|
|
53
60
|
private isActive;
|
|
54
61
|
private consecutiveFailures;
|
|
55
62
|
private stopped;
|
|
63
|
+
private polling;
|
|
64
|
+
private pendingWake;
|
|
65
|
+
private dormant;
|
|
66
|
+
private accountSubscription;
|
|
56
67
|
constructor(options: QuotaPollerOptions);
|
|
57
68
|
/**
|
|
58
69
|
* Starts polling. First poll is immediate.
|
|
@@ -82,6 +93,7 @@ export declare class QuotaPoller {
|
|
|
82
93
|
*/
|
|
83
94
|
getLatest(): QuotaState | null;
|
|
84
95
|
private poll;
|
|
96
|
+
private wake;
|
|
85
97
|
private scheduleNext;
|
|
86
98
|
private notify;
|
|
87
99
|
}
|
package/dist/quotaPoller.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.QuotaPoller = void 0;
|
|
12
12
|
const quota_1 = require("./quota");
|
|
13
|
+
const accountChanges_1 = require("./accountChanges");
|
|
13
14
|
/**
|
|
14
15
|
* Polls Claude Max quota usage with exponential backoff on transient errors,
|
|
15
16
|
* cached fallback, and configurable active/idle intervals.
|
|
@@ -35,17 +36,25 @@ class QuotaPoller {
|
|
|
35
36
|
idleIntervalMs;
|
|
36
37
|
maxBackoffMs;
|
|
37
38
|
getAccessToken;
|
|
39
|
+
hasAccount;
|
|
40
|
+
subscribeAccountsChanged;
|
|
38
41
|
timer = null;
|
|
39
42
|
listeners = [];
|
|
40
43
|
latest = null;
|
|
41
44
|
isActive = false;
|
|
42
45
|
consecutiveFailures = 0;
|
|
43
46
|
stopped = false;
|
|
47
|
+
polling = false;
|
|
48
|
+
pendingWake = false;
|
|
49
|
+
dormant = false;
|
|
50
|
+
accountSubscription = null;
|
|
44
51
|
constructor(options) {
|
|
45
52
|
this.activeIntervalMs = options.activeIntervalMs ?? 60_000;
|
|
46
53
|
this.idleIntervalMs = options.idleIntervalMs ?? 300_000;
|
|
47
54
|
this.maxBackoffMs = options.maxBackoffMs ?? 120_000;
|
|
48
55
|
this.getAccessToken = options.getAccessToken;
|
|
56
|
+
this.hasAccount = options.hasAccount;
|
|
57
|
+
this.subscribeAccountsChanged = options.subscribeAccountsChanged ?? accountChanges_1.onAccountsChanged;
|
|
49
58
|
}
|
|
50
59
|
/**
|
|
51
60
|
* Starts polling. First poll is immediate.
|
|
@@ -53,17 +62,21 @@ class QuotaPoller {
|
|
|
53
62
|
start() {
|
|
54
63
|
this.stopped = false;
|
|
55
64
|
this.isActive = true;
|
|
56
|
-
|
|
65
|
+
this.accountSubscription ??= this.subscribeAccountsChanged(() => this.wake());
|
|
66
|
+
this.wake();
|
|
57
67
|
}
|
|
58
68
|
/**
|
|
59
69
|
* Stops polling and clears timers.
|
|
60
70
|
*/
|
|
61
71
|
stop() {
|
|
62
72
|
this.stopped = true;
|
|
73
|
+
this.dormant = false;
|
|
63
74
|
if (this.timer) {
|
|
64
75
|
clearTimeout(this.timer);
|
|
65
76
|
this.timer = null;
|
|
66
77
|
}
|
|
78
|
+
this.accountSubscription?.dispose();
|
|
79
|
+
this.accountSubscription = null;
|
|
67
80
|
}
|
|
68
81
|
/**
|
|
69
82
|
* Switches to active polling interval.
|
|
@@ -102,8 +115,35 @@ class QuotaPoller {
|
|
|
102
115
|
async poll() {
|
|
103
116
|
if (this.stopped)
|
|
104
117
|
return;
|
|
118
|
+
if (this.polling) {
|
|
119
|
+
this.pendingWake = true;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.polling = true;
|
|
123
|
+
this.pendingWake = false;
|
|
124
|
+
this.dormant = false;
|
|
125
|
+
let nextDelayOverride;
|
|
105
126
|
try {
|
|
127
|
+
if (this.hasAccount && !(await this.hasAccount())) {
|
|
128
|
+
this.dormant = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
106
131
|
const token = await this.getAccessToken();
|
|
132
|
+
if (!token) {
|
|
133
|
+
if (this.hasAccount) {
|
|
134
|
+
// The account exists but its token was unreadable (locked keychain,
|
|
135
|
+
// transient credential I/O). That never changes the account
|
|
136
|
+
// fingerprint, so no wake would arrive — retry at the idle cadence
|
|
137
|
+
// instead of parking forever.
|
|
138
|
+
nextDelayOverride = this.idleIntervalMs;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Without a hasAccount probe, a missing token is the no-account
|
|
142
|
+
// signal; the account-change subscription wakes the poller on login.
|
|
143
|
+
this.dormant = true;
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
107
147
|
const state = await (0, quota_1.fetchQuota)(token);
|
|
108
148
|
if (state.available) {
|
|
109
149
|
this.latest = state;
|
|
@@ -116,7 +156,7 @@ class QuotaPoller {
|
|
|
116
156
|
this.latest = state;
|
|
117
157
|
this.notify(state);
|
|
118
158
|
this.consecutiveFailures = 0;
|
|
119
|
-
this.
|
|
159
|
+
nextDelayOverride = this.idleIntervalMs;
|
|
120
160
|
return;
|
|
121
161
|
}
|
|
122
162
|
else {
|
|
@@ -148,7 +188,29 @@ class QuotaPoller {
|
|
|
148
188
|
this.latest = state;
|
|
149
189
|
this.notify(state);
|
|
150
190
|
}
|
|
151
|
-
|
|
191
|
+
finally {
|
|
192
|
+
this.polling = false;
|
|
193
|
+
if (this.pendingWake) {
|
|
194
|
+
this.pendingWake = false;
|
|
195
|
+
void this.poll();
|
|
196
|
+
}
|
|
197
|
+
else if (!this.dormant) {
|
|
198
|
+
this.scheduleNext(nextDelayOverride);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
wake() {
|
|
203
|
+
if (this.stopped)
|
|
204
|
+
return;
|
|
205
|
+
if (this.timer) {
|
|
206
|
+
clearTimeout(this.timer);
|
|
207
|
+
this.timer = null;
|
|
208
|
+
}
|
|
209
|
+
if (this.polling) {
|
|
210
|
+
this.pendingWake = true;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
void this.poll();
|
|
152
214
|
}
|
|
153
215
|
scheduleNext(overrideMs) {
|
|
154
216
|
if (this.stopped)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One quota resolution path for every Sidekick surface.
|
|
3
|
+
*
|
|
4
|
+
* `sidekick quota`, `quota --all`, the MCP facts server, and both dashboards
|
|
5
|
+
* used to pick between the live API, session logs, and the persisted snapshot
|
|
6
|
+
* in four different orders. `resolveQuota()` applies one precedence:
|
|
7
|
+
*
|
|
8
|
+
* 1. a fresh persisted snapshot (younger than `QUOTA_FRESH_MAX_AGE_MS`, whatever
|
|
9
|
+
* its origin: the official status-line sample, a session log, or an earlier
|
|
10
|
+
* API call) — zero network;
|
|
11
|
+
* 2. a session-derived sample (Codex rollouts carry rate limits; Claude and
|
|
12
|
+
* z.ai have no local equivalent);
|
|
13
|
+
* 3. the provider API, when `allowApi` is not disabled;
|
|
14
|
+
* 4. an aging or stale snapshot, labelled as such, with the API failure attached;
|
|
15
|
+
* 5. an unavailable state describing why nothing could be resolved.
|
|
16
|
+
*
|
|
17
|
+
* `preferFresh: false` skips step 1 (the `--refresh` flag). Every result carries
|
|
18
|
+
* `source`, `capturedSource`, `freshness`, `ageMs`, and a `resolution` tag.
|
|
19
|
+
*/
|
|
20
|
+
import type { ResolvedActiveAccount, SavedAccountProfile } from './accountRegistry';
|
|
21
|
+
import type { ProviderQuotaState } from './providerQuota';
|
|
22
|
+
import type { CodexProvider } from './providers/codex';
|
|
23
|
+
import type { QuotaState } from './quota';
|
|
24
|
+
import type { QuotaSnapshotProviderId } from './quotaSnapshots';
|
|
25
|
+
import type { ZaiQuotaApiOptions } from './zaiQuotaApi';
|
|
26
|
+
/** Providers the resolver knows how to read. `opencode` callers map to `zai`. */
|
|
27
|
+
export type QuotaResolveProviderId = 'claude-code' | 'codex' | 'zai';
|
|
28
|
+
/** Which step of the precedence produced the answer. */
|
|
29
|
+
export type QuotaResolution = 'snapshot-fresh' | 'session' | 'api' | 'snapshot-aging' | 'snapshot-stale' | 'unavailable';
|
|
30
|
+
type RuntimeProviderFor<T extends QuotaResolveProviderId> = T extends 'claude-code' ? 'claude' : T extends 'codex' ? 'codex' : 'zai';
|
|
31
|
+
export interface ResolvedQuota<T extends QuotaResolveProviderId = QuotaResolveProviderId> extends ProviderQuotaState<RuntimeProviderFor<T>> {
|
|
32
|
+
/** Precedence step that produced this sample. */
|
|
33
|
+
resolution: QuotaResolution;
|
|
34
|
+
}
|
|
35
|
+
export interface ResolveQuotaOptions<T extends QuotaResolveProviderId = QuotaResolveProviderId> {
|
|
36
|
+
providerId: T;
|
|
37
|
+
/** Snapshot key; derived from the active account when omitted. */
|
|
38
|
+
accountId?: string;
|
|
39
|
+
/** Workspace whose Codex rollouts are searched first for session-derived rate limits. */
|
|
40
|
+
workspacePath?: string;
|
|
41
|
+
/** Use a fresh persisted snapshot before anything else (default true). `--refresh` passes false. */
|
|
42
|
+
preferFresh?: boolean;
|
|
43
|
+
/** Allow a provider API call when no fresh local sample exists (default true). */
|
|
44
|
+
allowApi?: boolean;
|
|
45
|
+
/** Re-point a stale saved-account pointer while resolving the account (default true). */
|
|
46
|
+
selfHeal?: boolean;
|
|
47
|
+
/** Clock for freshness classification; defaults to `new Date()`. */
|
|
48
|
+
now?: Date;
|
|
49
|
+
/** Fetch implementation forwarded to every API call (tests inject a stub). */
|
|
50
|
+
fetchImpl?: typeof fetch;
|
|
51
|
+
readSnapshot?: (providerId: QuotaSnapshotProviderId, accountId: string, now?: Date) => QuotaState | null;
|
|
52
|
+
writeSnapshot?: (providerId: QuotaSnapshotProviderId, accountId: string, quota: QuotaState) => void;
|
|
53
|
+
/** Claude access token source; defaults to `readClaudeMaxCredentials()`. */
|
|
54
|
+
getClaudeAccessToken?: () => Promise<string | null>;
|
|
55
|
+
/** Claude account resolution; defaults to `resolveActiveClaudeAccount()`. */
|
|
56
|
+
resolveClaudeAccount?: (options: {
|
|
57
|
+
selfHeal?: boolean;
|
|
58
|
+
}) => ResolvedActiveAccount;
|
|
59
|
+
/** Codex account resolution; defaults to the self-healing registry lookup. */
|
|
60
|
+
resolveCodexAccount?: (options: {
|
|
61
|
+
selfHeal?: boolean;
|
|
62
|
+
}) => SavedAccountProfile | null;
|
|
63
|
+
/** Reuse a caller-owned Codex provider for the session scan. */
|
|
64
|
+
codexProvider?: CodexProvider;
|
|
65
|
+
/** Codex home whose `auth.json` and rollouts are read. */
|
|
66
|
+
codexHome?: string;
|
|
67
|
+
/** Explicit Codex access token for the API step. */
|
|
68
|
+
codexAccessToken?: string;
|
|
69
|
+
/** z.ai credential discovery and API options. */
|
|
70
|
+
zai?: ZaiQuotaApiOptions;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the current quota for one provider with the shared precedence
|
|
74
|
+
* described at the top of this file.
|
|
75
|
+
*
|
|
76
|
+
* The result always has `runtimeProvider`, `resolution`, `freshness`, and
|
|
77
|
+
* (when a capture time is known) `ageMs`; `failure` describes an API failure
|
|
78
|
+
* even when older numbers were returned from a snapshot.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveQuota<T extends QuotaResolveProviderId>(options: ResolveQuotaOptions<T>): Promise<ResolvedQuota<T>>;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One quota resolution path for every Sidekick surface.
|
|
4
|
+
*
|
|
5
|
+
* `sidekick quota`, `quota --all`, the MCP facts server, and both dashboards
|
|
6
|
+
* used to pick between the live API, session logs, and the persisted snapshot
|
|
7
|
+
* in four different orders. `resolveQuota()` applies one precedence:
|
|
8
|
+
*
|
|
9
|
+
* 1. a fresh persisted snapshot (younger than `QUOTA_FRESH_MAX_AGE_MS`, whatever
|
|
10
|
+
* its origin: the official status-line sample, a session log, or an earlier
|
|
11
|
+
* API call) — zero network;
|
|
12
|
+
* 2. a session-derived sample (Codex rollouts carry rate limits; Claude and
|
|
13
|
+
* z.ai have no local equivalent);
|
|
14
|
+
* 3. the provider API, when `allowApi` is not disabled;
|
|
15
|
+
* 4. an aging or stale snapshot, labelled as such, with the API failure attached;
|
|
16
|
+
* 5. an unavailable state describing why nothing could be resolved.
|
|
17
|
+
*
|
|
18
|
+
* `preferFresh: false` skips step 1 (the `--refresh` flag). Every result carries
|
|
19
|
+
* `source`, `capturedSource`, `freshness`, `ageMs`, and a `resolution` tag.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.resolveQuota = resolveQuota;
|
|
23
|
+
const accounts_1 = require("./accounts");
|
|
24
|
+
const codexProfiles_1 = require("./codexProfiles");
|
|
25
|
+
const codexQuota_1 = require("./codexQuota");
|
|
26
|
+
const credentials_1 = require("./credentials");
|
|
27
|
+
const quota_1 = require("./quota");
|
|
28
|
+
const quotaPresentation_1 = require("./quotaPresentation");
|
|
29
|
+
const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
30
|
+
const zaiQuotaApi_1 = require("./zaiQuotaApi");
|
|
31
|
+
const ZAI_DEFAULT_ACCOUNT_ID = 'default';
|
|
32
|
+
const CLAUDE_NO_CREDENTIALS_ERROR = 'No OAuth token available';
|
|
33
|
+
function snapshotProviderId(providerId) {
|
|
34
|
+
return providerId;
|
|
35
|
+
}
|
|
36
|
+
function defaultResolveCodexAccount(options) {
|
|
37
|
+
// Self-heal the saved pointer to the live login first so the snapshot key and
|
|
38
|
+
// the display identity both track reality after a native `codex login`.
|
|
39
|
+
(0, codexProfiles_1.resolveActiveCodexAccount)(options);
|
|
40
|
+
return (0, codexProfiles_1.getActiveCodexAccount)();
|
|
41
|
+
}
|
|
42
|
+
function resolveIdentity(options) {
|
|
43
|
+
const selfHeal = options.selfHeal ?? true;
|
|
44
|
+
switch (options.providerId) {
|
|
45
|
+
case 'claude-code': {
|
|
46
|
+
const account = (options.resolveClaudeAccount ?? accounts_1.resolveActiveClaudeAccount)({ selfHeal });
|
|
47
|
+
return {
|
|
48
|
+
accountId: options.accountId ?? account.registryAccountId,
|
|
49
|
+
codexAccount: null,
|
|
50
|
+
accountLabel: account.label ?? account.email,
|
|
51
|
+
accountDetail: account.email,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
case 'codex': {
|
|
55
|
+
const profile = (options.resolveCodexAccount ?? defaultResolveCodexAccount)({ selfHeal });
|
|
56
|
+
return {
|
|
57
|
+
accountId: options.accountId ?? profile?.id,
|
|
58
|
+
codexAccount: profile,
|
|
59
|
+
accountLabel: profile?.label,
|
|
60
|
+
accountDetail: profile?.email,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case 'zai':
|
|
64
|
+
return { accountId: options.accountId ?? ZAI_DEFAULT_ACCOUNT_ID, codexAccount: null };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function defaultClaudeAccessToken() {
|
|
68
|
+
const credentials = await (0, credentials_1.readClaudeMaxCredentials)();
|
|
69
|
+
return credentials?.accessToken ?? null;
|
|
70
|
+
}
|
|
71
|
+
function zeroWindows() {
|
|
72
|
+
return { fiveHour: { utilization: 0, resetsAt: '' }, sevenDay: { utilization: 0, resetsAt: '' } };
|
|
73
|
+
}
|
|
74
|
+
function unavailableState(providerId, error, capturedAt) {
|
|
75
|
+
return {
|
|
76
|
+
...zeroWindows(),
|
|
77
|
+
available: false,
|
|
78
|
+
error,
|
|
79
|
+
failureKind: 'unknown',
|
|
80
|
+
providerId,
|
|
81
|
+
capturedAt,
|
|
82
|
+
...(providerId === 'codex' ? { fiveHourLabel: 'Primary', sevenDayLabel: 'Secondary' } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function fetchFromApi(options, identity, capturedAt) {
|
|
86
|
+
switch (options.providerId) {
|
|
87
|
+
case 'claude-code': {
|
|
88
|
+
const token = await (options.getClaudeAccessToken ?? defaultClaudeAccessToken)();
|
|
89
|
+
if (!token) {
|
|
90
|
+
return {
|
|
91
|
+
...zeroWindows(),
|
|
92
|
+
available: false,
|
|
93
|
+
error: CLAUDE_NO_CREDENTIALS_ERROR,
|
|
94
|
+
failureKind: 'auth',
|
|
95
|
+
providerId: 'claude-code',
|
|
96
|
+
source: 'api',
|
|
97
|
+
capturedAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const state = await (0, quota_1.fetchQuota)(token, { fetchImpl: options.fetchImpl });
|
|
101
|
+
return { ...state, providerId: 'claude-code', source: 'api', capturedAt };
|
|
102
|
+
}
|
|
103
|
+
case 'codex':
|
|
104
|
+
return (0, codexQuota_1.fetchCodexQuotaFromApi)({
|
|
105
|
+
codexHome: options.codexHome,
|
|
106
|
+
fetchImpl: options.fetchImpl,
|
|
107
|
+
accessToken: options.codexAccessToken,
|
|
108
|
+
accountId: identity.codexAccount?.providerAccountId,
|
|
109
|
+
capturedAt,
|
|
110
|
+
});
|
|
111
|
+
case 'zai':
|
|
112
|
+
return (0, zaiQuotaApi_1.fetchZaiQuotaFromApi)({ ...options.zai, fetchImpl: options.fetchImpl, capturedAt });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function withAge(state, now) {
|
|
116
|
+
if (state.ageMs !== undefined && state.freshness !== undefined)
|
|
117
|
+
return state;
|
|
118
|
+
const capturedMs = state.capturedAt ? Date.parse(state.capturedAt) : NaN;
|
|
119
|
+
const ageMs = Number.isFinite(capturedMs) ? Math.max(0, now.getTime() - capturedMs) : undefined;
|
|
120
|
+
return {
|
|
121
|
+
...state,
|
|
122
|
+
...(ageMs !== undefined ? { ageMs } : {}),
|
|
123
|
+
freshness: (0, quota_1.classifyQuotaFreshness)(ageMs),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function enrich(providerId, state, identity) {
|
|
127
|
+
switch (providerId) {
|
|
128
|
+
case 'claude-code':
|
|
129
|
+
return {
|
|
130
|
+
...(0, quota_1.withQuotaProjections)(state),
|
|
131
|
+
runtimeProvider: 'claude',
|
|
132
|
+
providerId: 'claude-code',
|
|
133
|
+
accountLabel: identity.accountLabel,
|
|
134
|
+
accountDetail: identity.accountDetail,
|
|
135
|
+
};
|
|
136
|
+
case 'codex':
|
|
137
|
+
return (0, codexQuota_1.enrichCodexQuota)(state, identity.codexAccount);
|
|
138
|
+
default:
|
|
139
|
+
return (0, zaiQuotaApi_1.enrichZaiQuota)(state);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function snapshotResolution(state) {
|
|
143
|
+
if (state.freshness === 'fresh')
|
|
144
|
+
return 'snapshot-fresh';
|
|
145
|
+
if (state.freshness === 'aging')
|
|
146
|
+
return 'snapshot-aging';
|
|
147
|
+
return 'snapshot-stale';
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the current quota for one provider with the shared precedence
|
|
151
|
+
* described at the top of this file.
|
|
152
|
+
*
|
|
153
|
+
* The result always has `runtimeProvider`, `resolution`, `freshness`, and
|
|
154
|
+
* (when a capture time is known) `ageMs`; `failure` describes an API failure
|
|
155
|
+
* even when older numbers were returned from a snapshot.
|
|
156
|
+
*/
|
|
157
|
+
async function resolveQuota(options) {
|
|
158
|
+
const providerId = options.providerId;
|
|
159
|
+
const now = options.now ?? new Date();
|
|
160
|
+
const capturedAt = now.toISOString();
|
|
161
|
+
const preferFresh = options.preferFresh ?? true;
|
|
162
|
+
const allowApi = options.allowApi ?? true;
|
|
163
|
+
const readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
|
|
164
|
+
const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
|
|
165
|
+
const identity = resolveIdentity(options);
|
|
166
|
+
const finish = (state, resolution) => {
|
|
167
|
+
const enriched = withAge(enrich(providerId, state, identity), now);
|
|
168
|
+
return {
|
|
169
|
+
...enriched,
|
|
170
|
+
failure: enriched.failure ?? (0, quotaPresentation_1.describeQuotaFailure)(enriched),
|
|
171
|
+
resolution,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
const stored = identity.accountId
|
|
175
|
+
? readSnapshot(snapshotProviderId(providerId), identity.accountId, now)
|
|
176
|
+
: null;
|
|
177
|
+
const snapshot = stored?.available ? stored : null;
|
|
178
|
+
if (preferFresh && snapshot && snapshot.freshness === 'fresh') {
|
|
179
|
+
return finish(snapshot, 'snapshot-fresh');
|
|
180
|
+
}
|
|
181
|
+
if (providerId === 'codex') {
|
|
182
|
+
const local = (0, codexQuota_1.resolveCodexQuotaFromLocalSources)({
|
|
183
|
+
workspacePath: options.workspacePath,
|
|
184
|
+
provider: options.codexProvider,
|
|
185
|
+
activeAccount: identity.codexAccount,
|
|
186
|
+
codexHome: options.codexHome,
|
|
187
|
+
// Only rollout hits count as "session-derived"; the snapshot fallback is
|
|
188
|
+
// handled below so it can be labelled by age.
|
|
189
|
+
readSnapshot: () => null,
|
|
190
|
+
writeSnapshot,
|
|
191
|
+
});
|
|
192
|
+
if (local)
|
|
193
|
+
return finish(local, 'session');
|
|
194
|
+
}
|
|
195
|
+
let apiState = null;
|
|
196
|
+
if (allowApi) {
|
|
197
|
+
apiState = await fetchFromApi(options, identity, capturedAt);
|
|
198
|
+
if (apiState.available) {
|
|
199
|
+
if (identity.accountId) {
|
|
200
|
+
writeSnapshot(snapshotProviderId(providerId), identity.accountId, apiState);
|
|
201
|
+
}
|
|
202
|
+
return finish(apiState, 'api');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (snapshot) {
|
|
206
|
+
const fallback = finish(snapshot, snapshotResolution(snapshot));
|
|
207
|
+
return apiState ? { ...fallback, failure: (0, quotaPresentation_1.describeQuotaFailure)(apiState) } : fallback;
|
|
208
|
+
}
|
|
209
|
+
return finish(apiState ??
|
|
210
|
+
unavailableState(providerId, stored
|
|
211
|
+
? 'The last persisted quota sample was unavailable.'
|
|
212
|
+
: 'No quota data is available yet; run the provider once so a sample can be recorded.', capturedAt), 'unavailable');
|
|
213
|
+
}
|
package/dist/quotaSnapshots.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type QuotaState } from './quota';
|
|
2
2
|
import type { AccountProviderId } from './accountRegistry';
|
|
3
3
|
/**
|
|
4
4
|
* Storage key for a quota snapshot. Extends `AccountProviderId` with `'zai'`
|
|
@@ -6,5 +6,18 @@ import type { AccountProviderId } from './accountRegistry';
|
|
|
6
6
|
* store still needs a stable key for the z.ai quota cache.
|
|
7
7
|
*/
|
|
8
8
|
export type QuotaSnapshotProviderId = AccountProviderId | 'zai';
|
|
9
|
+
export interface QuotaSnapshotRecord {
|
|
10
|
+
providerId: QuotaSnapshotProviderId;
|
|
11
|
+
accountId: string;
|
|
12
|
+
quota: QuotaState;
|
|
13
|
+
}
|
|
9
14
|
export declare function writeQuotaSnapshot(providerId: QuotaSnapshotProviderId, accountId: string, quota: QuotaState): void;
|
|
10
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Read the persisted snapshot for one provider/account.
|
|
17
|
+
*
|
|
18
|
+
* `stale: true` and `source: 'cache'` mean "not a live fetch"; the age of the
|
|
19
|
+
* sample is reported separately as `ageMs` and `freshness` so a reader can
|
|
20
|
+
* tell a ten-second-old snapshot from a two-day-old one, and the original
|
|
21
|
+
* origin is kept as `capturedSource` (`statusline`, `session`, or `api`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function readQuotaSnapshot(providerId: QuotaSnapshotProviderId, accountId: string, now?: Date): QuotaState | null;
|