sidekick-shared 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +49 -3
- package/dist/accountChanges.js +22 -3
- package/dist/accountRegistry.d.ts +22 -1
- package/dist/accountRegistry.js +37 -14
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +29 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +112 -24
- package/dist/node.d.ts +19 -0
- package/dist/node.js +47 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +3 -8
- package/dist/observedSessionCollector.js +10 -27
- package/dist/parsers/sessionActivityDetector.d.ts +24 -2
- package/dist/parsers/sessionActivityDetector.js +95 -17
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providers/claudeCode.js +11 -96
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +94 -259
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/factory.d.ts +6 -0
- package/dist/providers/openCode.d.ts +2 -0
- package/dist/providers/openCode.js +105 -121
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +30 -0
- package/dist/quota.d.ts +30 -3
- package/dist/quota.js +35 -3
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaResolver.d.ts +81 -0
- package/dist/quotaResolver.js +213 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +139 -0
- package/dist/schemas/quota.js +17 -2
- package/dist/schemas/quotaHistory.d.ts +1 -0
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionPreviews.d.ts +7 -1
- package/dist/sessionPreviews.js +15 -3
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +2 -0
- package/dist/types/observedSessionV1.js +75 -18
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One capped walker for Codex rollout files.
|
|
4
|
+
*
|
|
5
|
+
* Codex writes `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`.
|
|
6
|
+
* Six recursive, uncapped walkers used to exist across the provider and the
|
|
7
|
+
* quota resolver; this module replaces them with a single walk that caps
|
|
8
|
+
* depth and file count, visits the newest-dated directories first so a
|
|
9
|
+
* `limit` can stop early, stats each file exactly once, and returns results
|
|
10
|
+
* newest first with the size and session id already attached.
|
|
11
|
+
*
|
|
12
|
+
* With `limit`, the early exit is by directory date: a rollout that was
|
|
13
|
+
* created days ago but is still being written lands in its creation day's
|
|
14
|
+
* directory, so a very small limit can miss it in favour of newer-dated,
|
|
15
|
+
* older-modified files. Callers that need exact mtime order over the whole
|
|
16
|
+
* history omit `limit` (the `maxFiles` cap still applies).
|
|
17
|
+
*
|
|
18
|
+
* @module providers/rolloutWalker
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = void 0;
|
|
55
|
+
exports.isRolloutFile = isRolloutFile;
|
|
56
|
+
exports.extractRolloutSessionId = extractRolloutSessionId;
|
|
57
|
+
exports.walkRolloutFiles = walkRolloutFiles;
|
|
58
|
+
exports.walkRolloutFilesAsync = walkRolloutFilesAsync;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
|
|
62
|
+
exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = 20_000;
|
|
63
|
+
/** Test if a filename is a Codex rollout file. */
|
|
64
|
+
function isRolloutFile(filename) {
|
|
65
|
+
return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Extract the session UUID from a rollout filename.
|
|
69
|
+
* Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
|
|
70
|
+
*/
|
|
71
|
+
function extractRolloutSessionId(filename) {
|
|
72
|
+
const base = path.basename(filename, '.jsonl');
|
|
73
|
+
// rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
|
|
74
|
+
const parts = base.split('-');
|
|
75
|
+
// The UUID is typically the last 5 segments (8-4-4-4-12)
|
|
76
|
+
if (parts.length >= 6) {
|
|
77
|
+
const possibleUuid = parts.slice(-5).join('-');
|
|
78
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(possibleUuid)) {
|
|
79
|
+
return possibleUuid;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Fallback: use everything after "rollout-"
|
|
83
|
+
return base.replace(/^rollout-/, '');
|
|
84
|
+
}
|
|
85
|
+
function walkState(options) {
|
|
86
|
+
const maxFiles = Math.max(0, options.maxFiles ?? exports.DEFAULT_ROLLOUT_WALK_MAX_FILES);
|
|
87
|
+
const limit = options.limit === undefined ? Infinity : Math.max(0, options.limit);
|
|
88
|
+
return {
|
|
89
|
+
results: [],
|
|
90
|
+
seen: new Set(),
|
|
91
|
+
cap: Math.min(maxFiles, limit),
|
|
92
|
+
maxDepth: options.maxDepth ?? exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH,
|
|
93
|
+
sessionId: options.sessionId ? options.sessionId.trim().toLowerCase() : null,
|
|
94
|
+
includeEmpty: options.includeEmpty ?? false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Newest-dated first: directory and file names sort descending. */
|
|
98
|
+
function byNameDescending(a, b) {
|
|
99
|
+
return b.name.localeCompare(a.name);
|
|
100
|
+
}
|
|
101
|
+
function matchesFilter(state, name) {
|
|
102
|
+
if (!isRolloutFile(name))
|
|
103
|
+
return false;
|
|
104
|
+
return (state.sessionId === null || extractRolloutSessionId(name).toLowerCase() === state.sessionId);
|
|
105
|
+
}
|
|
106
|
+
function record(state, fullPath, name, stat) {
|
|
107
|
+
if (!state.includeEmpty && stat.size <= 0)
|
|
108
|
+
return;
|
|
109
|
+
if (state.seen.has(fullPath))
|
|
110
|
+
return;
|
|
111
|
+
state.seen.add(fullPath);
|
|
112
|
+
state.results.push({
|
|
113
|
+
path: fullPath,
|
|
114
|
+
mtime: stat.mtime,
|
|
115
|
+
sizeBytes: stat.size,
|
|
116
|
+
sessionId: extractRolloutSessionId(name),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function finish(state) {
|
|
120
|
+
return state.results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
121
|
+
}
|
|
122
|
+
function visitSync(state, dir, depth) {
|
|
123
|
+
if (depth > state.maxDepth || state.results.length >= state.cap)
|
|
124
|
+
return;
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
entries.sort(byNameDescending);
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (state.results.length >= state.cap)
|
|
135
|
+
return;
|
|
136
|
+
if (!entry.isFile() || !matchesFilter(state, entry.name))
|
|
137
|
+
continue;
|
|
138
|
+
const fullPath = path.join(dir, entry.name);
|
|
139
|
+
try {
|
|
140
|
+
record(state, fullPath, entry.name, fs.statSync(fullPath));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Skip files that vanish between readdir and stat.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (state.results.length >= state.cap)
|
|
148
|
+
return;
|
|
149
|
+
if (entry.isDirectory())
|
|
150
|
+
visitSync(state, path.join(dir, entry.name), depth + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Walk one or more `sessions` roots for rollout files, newest first.
|
|
155
|
+
* Missing or unreadable directories are skipped; paths are deduplicated.
|
|
156
|
+
*/
|
|
157
|
+
function walkRolloutFiles(roots, options = {}) {
|
|
158
|
+
const state = walkState(options);
|
|
159
|
+
for (const root of roots)
|
|
160
|
+
visitSync(state, root, 0);
|
|
161
|
+
return finish(state);
|
|
162
|
+
}
|
|
163
|
+
function yieldToEventLoop() {
|
|
164
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
165
|
+
}
|
|
166
|
+
async function visitAsync(state, dir, depth) {
|
|
167
|
+
if (depth > state.maxDepth || state.results.length >= state.cap)
|
|
168
|
+
return;
|
|
169
|
+
let entries;
|
|
170
|
+
try {
|
|
171
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
entries.sort(byNameDescending);
|
|
177
|
+
let sinceYield = 0;
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (state.results.length >= state.cap)
|
|
180
|
+
return;
|
|
181
|
+
if (!entry.isFile() || !matchesFilter(state, entry.name))
|
|
182
|
+
continue;
|
|
183
|
+
const fullPath = path.join(dir, entry.name);
|
|
184
|
+
try {
|
|
185
|
+
record(state, fullPath, entry.name, await fs.promises.stat(fullPath));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Skip files that vanish between readdir and stat.
|
|
189
|
+
}
|
|
190
|
+
if (++sinceYield >= 200) {
|
|
191
|
+
sinceYield = 0;
|
|
192
|
+
await yieldToEventLoop();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
await yieldToEventLoop();
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (state.results.length >= state.cap)
|
|
198
|
+
return;
|
|
199
|
+
if (entry.isDirectory())
|
|
200
|
+
await visitAsync(state, path.join(dir, entry.name), depth + 1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Async twin of {@link walkRolloutFiles} that yields between directories. */
|
|
204
|
+
async function walkRolloutFilesAsync(roots, options = {}) {
|
|
205
|
+
const state = walkState(options);
|
|
206
|
+
for (const root of roots)
|
|
207
|
+
await visitAsync(state, root, 0);
|
|
208
|
+
return finish(state);
|
|
209
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
|
|
12
12
|
import type { SessionEvent, SubagentStats, TokenUsage, ContextAttribution } from '../types/sessionEvent';
|
|
13
13
|
import { SESSION_PROVIDER_IDS } from '../providerIds';
|
|
14
|
+
import type { AggregatedCostProvenance } from '../aggregation/types';
|
|
14
15
|
/** Stable runtime list for consumers that must enumerate session providers. */
|
|
15
16
|
export { SESSION_PROVIDER_IDS } from '../providerIds';
|
|
16
17
|
export type ProviderId = (typeof SESSION_PROVIDER_IDS)[number];
|
|
@@ -58,6 +59,12 @@ export interface SessionFileInfo {
|
|
|
58
59
|
/** Optional provider-recorded creation timestamp. */
|
|
59
60
|
createdAt?: Date;
|
|
60
61
|
}
|
|
62
|
+
/** How much of a session `readSessionStats()` could read. */
|
|
63
|
+
export type SessionFileStatsAvailability = 'full' | 'partial' | 'unavailable';
|
|
64
|
+
/**
|
|
65
|
+
* Aggregated stats for one session file, computed by `computeSessionFileStats()`
|
|
66
|
+
* over the shared `EventAggregator` for every provider.
|
|
67
|
+
*/
|
|
61
68
|
export interface SessionFileStats {
|
|
62
69
|
providerId: ProviderId;
|
|
63
70
|
sessionId: string;
|
|
@@ -66,19 +73,42 @@ export interface SessionFileStats {
|
|
|
66
73
|
startTime: string;
|
|
67
74
|
endTime: string;
|
|
68
75
|
messageCount: number;
|
|
76
|
+
/** Token buckets; `summarizeTokens()` turns them into the cache-inclusive total. */
|
|
69
77
|
tokens: {
|
|
70
78
|
input: number;
|
|
71
79
|
output: number;
|
|
72
80
|
cacheWrite: number;
|
|
73
81
|
cacheRead: number;
|
|
74
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Per-model calls, cache-inclusive token total (the `summarizeTokens().total`
|
|
85
|
+
* vocabulary), priced cost, and whether every call could be priced.
|
|
86
|
+
*/
|
|
75
87
|
modelUsage: Record<string, {
|
|
76
88
|
calls: number;
|
|
77
89
|
tokens: number;
|
|
90
|
+
costUsd: number;
|
|
91
|
+
priced: boolean;
|
|
78
92
|
}>;
|
|
93
|
+
/** Calls per tool name: successful, failed, and still pending. */
|
|
79
94
|
toolUsage: Record<string, number>;
|
|
95
|
+
/** Failed calls per tool name; a subset of `toolUsage`. */
|
|
96
|
+
toolFailures: Record<string, number>;
|
|
80
97
|
compactionEstimate: number;
|
|
81
98
|
truncationCount: number;
|
|
99
|
+
/** Cost of the priced portion of the session; read `costProvenance` before labelling it. */
|
|
100
|
+
costUsd: number;
|
|
101
|
+
costProvenance: AggregatedCostProvenance;
|
|
102
|
+
/** Usage events with neither a provider-reported cost nor a catalog price. */
|
|
103
|
+
unpricedCalls: number;
|
|
104
|
+
/**
|
|
105
|
+
* `full` when every event was read, `partial` when the reader reported a
|
|
106
|
+
* truncated source, `unavailable` when the session could not be read at all
|
|
107
|
+
* (`unavailableReason` says why and every number is zero).
|
|
108
|
+
*/
|
|
109
|
+
availability: SessionFileStatsAvailability;
|
|
110
|
+
unavailableReason?: string;
|
|
111
|
+
/** @deprecated Same value as `costUsd`; use it together with `costProvenance`. */
|
|
82
112
|
reportedCost: number;
|
|
83
113
|
}
|
|
84
114
|
export interface ProjectFolderInfo {
|
package/dist/quota.d.ts
CHANGED
|
@@ -46,12 +46,26 @@ export interface QuotaState {
|
|
|
46
46
|
projectedSevenDay?: number;
|
|
47
47
|
/** Provider that produced this quota sample */
|
|
48
48
|
providerId?: QuotaProviderId;
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Source of the sample. `statusline` is the official rate-limit block Claude
|
|
51
|
+
* Code hands to its status-line command; `session` is inferred from session
|
|
52
|
+
* logs; `api` is a live usage-endpoint fetch; `cache` is a persisted snapshot.
|
|
53
|
+
*/
|
|
54
|
+
source?: 'api' | 'session' | 'cache' | 'statusline';
|
|
55
|
+
/**
|
|
56
|
+
* Where a cached sample originally came from. `readQuotaSnapshot()` forces
|
|
57
|
+
* `source` to `cache`; this keeps the origin (`statusline`, `session`, `api`)
|
|
58
|
+
* so a reader can rank an official status-line sample above a session guess.
|
|
59
|
+
*/
|
|
60
|
+
capturedSource?: 'api' | 'session' | 'statusline';
|
|
51
61
|
/** ISO timestamp when the sample was captured */
|
|
52
62
|
capturedAt?: string;
|
|
53
63
|
/** Whether the sample is stale cached data */
|
|
54
64
|
stale?: boolean;
|
|
65
|
+
/** Milliseconds between `capturedAt` and the read, when read from a snapshot. */
|
|
66
|
+
ageMs?: number;
|
|
67
|
+
/** Freshness tier derived from `ageMs`; see `classifyQuotaFreshness`. */
|
|
68
|
+
freshness?: QuotaFreshness;
|
|
55
69
|
/** Provider-specific display label for the first window */
|
|
56
70
|
fiveHourLabel?: string;
|
|
57
71
|
/** Provider-specific display label for the second window */
|
|
@@ -71,6 +85,16 @@ export interface QuotaState {
|
|
|
71
85
|
}
|
|
72
86
|
export declare const FIVE_HOUR_WINDOW_MS: number;
|
|
73
87
|
export declare const SEVEN_DAY_WINDOW_MS: number;
|
|
88
|
+
/** How much to trust a cached quota sample, by age. */
|
|
89
|
+
export type QuotaFreshness = 'fresh' | 'aging' | 'stale';
|
|
90
|
+
/** A snapshot younger than this is as good as live. */
|
|
91
|
+
export declare const QUOTA_FRESH_MAX_AGE_MS: number;
|
|
92
|
+
/** A snapshot older than this should be labelled as stale wherever it is shown. */
|
|
93
|
+
export declare const QUOTA_AGING_MAX_AGE_MS: number;
|
|
94
|
+
/** Classify a cached sample's age. Non-finite or negative ages count as stale. */
|
|
95
|
+
export declare function classifyQuotaFreshness(ageMs: number | undefined): QuotaFreshness;
|
|
96
|
+
/** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
|
|
97
|
+
export declare function formatQuotaAge(ageMs: number | undefined): string;
|
|
74
98
|
export interface QuotaProjectionInput {
|
|
75
99
|
utilization: number;
|
|
76
100
|
resetsAt: string;
|
|
@@ -95,6 +119,9 @@ export declare function withQuotaProjections<T extends QuotaState>(state: T, opt
|
|
|
95
119
|
* it in a polling loop, VS Code EventEmitter, or CLI interval as needed.
|
|
96
120
|
*
|
|
97
121
|
* @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
|
|
122
|
+
* @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
|
|
98
123
|
* @returns QuotaState with utilization data and elapsed-time projections
|
|
99
124
|
*/
|
|
100
|
-
export declare function fetchQuota(accessToken: string
|
|
125
|
+
export declare function fetchQuota(accessToken: string, options?: {
|
|
126
|
+
fetchImpl?: typeof fetch;
|
|
127
|
+
}): Promise<QuotaState>;
|
package/dist/quota.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SEVEN_DAY_WINDOW_MS = exports.FIVE_HOUR_WINDOW_MS = exports.QUOTA_PROVIDER_IDS = void 0;
|
|
3
|
+
exports.QUOTA_AGING_MAX_AGE_MS = exports.QUOTA_FRESH_MAX_AGE_MS = exports.SEVEN_DAY_WINDOW_MS = exports.FIVE_HOUR_WINDOW_MS = exports.QUOTA_PROVIDER_IDS = void 0;
|
|
4
|
+
exports.classifyQuotaFreshness = classifyQuotaFreshness;
|
|
5
|
+
exports.formatQuotaAge = formatQuotaAge;
|
|
4
6
|
exports.projectQuotaWindow = projectQuotaWindow;
|
|
5
7
|
exports.withQuotaProjections = withQuotaProjections;
|
|
6
8
|
exports.fetchQuota = fetchQuota;
|
|
@@ -11,6 +13,34 @@ const BETA_HEADER = 'oauth-2025-04-20';
|
|
|
11
13
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
12
14
|
exports.FIVE_HOUR_WINDOW_MS = 5 * 3_600_000;
|
|
13
15
|
exports.SEVEN_DAY_WINDOW_MS = 7 * 86_400_000;
|
|
16
|
+
/** A snapshot younger than this is as good as live. */
|
|
17
|
+
exports.QUOTA_FRESH_MAX_AGE_MS = 5 * 60_000;
|
|
18
|
+
/** A snapshot older than this should be labelled as stale wherever it is shown. */
|
|
19
|
+
exports.QUOTA_AGING_MAX_AGE_MS = 60 * 60_000;
|
|
20
|
+
/** Classify a cached sample's age. Non-finite or negative ages count as stale. */
|
|
21
|
+
function classifyQuotaFreshness(ageMs) {
|
|
22
|
+
if (typeof ageMs !== 'number' || !Number.isFinite(ageMs) || ageMs < 0)
|
|
23
|
+
return 'stale';
|
|
24
|
+
if (ageMs < exports.QUOTA_FRESH_MAX_AGE_MS)
|
|
25
|
+
return 'fresh';
|
|
26
|
+
if (ageMs < exports.QUOTA_AGING_MAX_AGE_MS)
|
|
27
|
+
return 'aging';
|
|
28
|
+
return 'stale';
|
|
29
|
+
}
|
|
30
|
+
/** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
|
|
31
|
+
function formatQuotaAge(ageMs) {
|
|
32
|
+
if (typeof ageMs !== 'number' || !Number.isFinite(ageMs) || ageMs < 0)
|
|
33
|
+
return 'age unknown';
|
|
34
|
+
const minutes = Math.floor(ageMs / 60_000);
|
|
35
|
+
if (minutes < 1)
|
|
36
|
+
return 'just now';
|
|
37
|
+
if (minutes < 60)
|
|
38
|
+
return `${minutes}m ago`;
|
|
39
|
+
const hours = Math.floor(minutes / 60);
|
|
40
|
+
if (hours < 48)
|
|
41
|
+
return `${hours}h ago`;
|
|
42
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
43
|
+
}
|
|
14
44
|
/**
|
|
15
45
|
* Projects utilization at end of window based on elapsed time.
|
|
16
46
|
*
|
|
@@ -88,14 +118,16 @@ function parseRetryAfterMs(retryAfter) {
|
|
|
88
118
|
* it in a polling loop, VS Code EventEmitter, or CLI interval as needed.
|
|
89
119
|
*
|
|
90
120
|
* @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
|
|
121
|
+
* @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
|
|
91
122
|
* @returns QuotaState with utilization data and elapsed-time projections
|
|
92
123
|
*/
|
|
93
|
-
async function fetchQuota(accessToken) {
|
|
124
|
+
async function fetchQuota(accessToken, options = {}) {
|
|
94
125
|
const controller = new AbortController();
|
|
95
126
|
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
96
127
|
timeout.unref();
|
|
97
128
|
try {
|
|
98
|
-
const
|
|
129
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
130
|
+
const res = await fetchImpl(USAGE_URL, {
|
|
99
131
|
method: 'GET',
|
|
100
132
|
signal: controller.signal,
|
|
101
133
|
headers: {
|
package/dist/quotaHistory.d.ts
CHANGED
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
|
}
|
|
@@ -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 {};
|