sidekick-shared 0.27.6 → 0.28.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 +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `readSessionStats()` implementation for every session provider.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code, Codex, and OpenCode used to compute session stats three
|
|
5
|
+
* different ways (a hand-rolled JSONL scan, an aggregator drain, and a direct
|
|
6
|
+
* SQLite read with hardcoded zeros). Every provider now drains its reader once
|
|
7
|
+
* into the shared `EventAggregator`, so per-model totals use the
|
|
8
|
+
* cache-inclusive vocabulary, cost carries provenance, tool failures are
|
|
9
|
+
* split out, and compaction and truncation counts are real for all three.
|
|
10
|
+
*/
|
|
11
|
+
import { EventAggregator } from './aggregation/EventAggregator';
|
|
12
|
+
const LABEL_MAX_LENGTH = 60;
|
|
13
|
+
function isTextBlock(block) {
|
|
14
|
+
return (typeof block === 'object' &&
|
|
15
|
+
block !== null &&
|
|
16
|
+
typeof block.text === 'string' &&
|
|
17
|
+
typeof block.type === 'string' &&
|
|
18
|
+
['text', 'input_text', 'output_text'].includes(block.type));
|
|
19
|
+
}
|
|
20
|
+
function messageText(content) {
|
|
21
|
+
if (typeof content === 'string')
|
|
22
|
+
return content;
|
|
23
|
+
if (!Array.isArray(content))
|
|
24
|
+
return null;
|
|
25
|
+
const block = content.find((item) => isTextBlock(item) && item.text.trim().length > 0);
|
|
26
|
+
return block && isTextBlock(block) ? block.text : null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* First user prompt in a session, compacted to one line and truncated the
|
|
30
|
+
* way every provider's `extractSessionLabel()` truncates (60 characters).
|
|
31
|
+
*/
|
|
32
|
+
export function firstUserPrompt(events, maxLength = LABEL_MAX_LENGTH) {
|
|
33
|
+
for (const event of events) {
|
|
34
|
+
if (event.type !== 'user')
|
|
35
|
+
continue;
|
|
36
|
+
const text = messageText(event.message?.content);
|
|
37
|
+
if (!text)
|
|
38
|
+
continue;
|
|
39
|
+
const compact = text.replace(/\s+/g, ' ').trim();
|
|
40
|
+
if (!compact)
|
|
41
|
+
continue;
|
|
42
|
+
return compact.length > maxLength ? `${compact.slice(0, maxLength - 3)}...` : compact;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** Compute stats for an already-read event array through the shared aggregator. */
|
|
47
|
+
export function computeSessionFileStats(events, options) {
|
|
48
|
+
const aggregator = new EventAggregator({
|
|
49
|
+
providerId: options.providerId,
|
|
50
|
+
computeContextSize: options.computeContextSize,
|
|
51
|
+
});
|
|
52
|
+
for (const event of events)
|
|
53
|
+
aggregator.processEvent(event);
|
|
54
|
+
const metrics = aggregator.getMetrics();
|
|
55
|
+
const modelUsage = {};
|
|
56
|
+
for (const model of metrics.modelStats) {
|
|
57
|
+
// `tokens` is the aggregator's cache-inclusive total for the model, the
|
|
58
|
+
// same figure `summarizeTokens().total` reports.
|
|
59
|
+
modelUsage[model.model] = {
|
|
60
|
+
calls: model.calls,
|
|
61
|
+
tokens: model.tokens,
|
|
62
|
+
costUsd: model.cost,
|
|
63
|
+
priced: model.priced !== false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const toolUsage = {};
|
|
67
|
+
const toolFailures = {};
|
|
68
|
+
for (const tool of metrics.toolStats) {
|
|
69
|
+
toolUsage[tool.name] = tool.successCount + tool.failureCount + tool.pendingCount;
|
|
70
|
+
if (tool.failureCount > 0)
|
|
71
|
+
toolFailures[tool.name] = tool.failureCount;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
providerId: options.providerId,
|
|
75
|
+
sessionId: options.sessionId,
|
|
76
|
+
filePath: options.filePath,
|
|
77
|
+
label: options.label ?? firstUserPrompt(events),
|
|
78
|
+
startTime: metrics.sessionStartTime ?? '',
|
|
79
|
+
endTime: metrics.lastEventTime ?? '',
|
|
80
|
+
messageCount: metrics.messageCount,
|
|
81
|
+
tokens: {
|
|
82
|
+
input: metrics.tokens.inputTokens,
|
|
83
|
+
output: metrics.tokens.outputTokens,
|
|
84
|
+
cacheWrite: metrics.tokens.cacheWriteTokens,
|
|
85
|
+
cacheRead: metrics.tokens.cacheReadTokens,
|
|
86
|
+
},
|
|
87
|
+
modelUsage,
|
|
88
|
+
toolUsage,
|
|
89
|
+
toolFailures,
|
|
90
|
+
compactionEstimate: metrics.compactionCount,
|
|
91
|
+
truncationCount: metrics.truncationCount,
|
|
92
|
+
costUsd: metrics.tokens.costUsd,
|
|
93
|
+
costProvenance: metrics.tokens.costProvenance,
|
|
94
|
+
unpricedCalls: metrics.tokens.unpricedCalls,
|
|
95
|
+
availability: options.availability ?? 'full',
|
|
96
|
+
...(options.unavailableReason ? { unavailableReason: options.unavailableReason } : {}),
|
|
97
|
+
reportedCost: metrics.tokens.costUsd,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** The aggregator's context-size hook for a provider, or undefined for the default formula. */
|
|
101
|
+
export function providerContextSizeFn(provider) {
|
|
102
|
+
if (!provider.computeContextSize)
|
|
103
|
+
return undefined;
|
|
104
|
+
return (usage) => provider.computeContextSize({ ...usage, model: '', timestamp: new Date() });
|
|
105
|
+
}
|
|
106
|
+
function errorMessage(error) {
|
|
107
|
+
return error instanceof Error ? error.message : String(error);
|
|
108
|
+
}
|
|
109
|
+
function describeMissingSource(provider) {
|
|
110
|
+
const status = provider.getRuntimeStatus?.();
|
|
111
|
+
if (status && !status.available) {
|
|
112
|
+
return status.message ?? `Provider runtime unavailable (${status.kind}).`;
|
|
113
|
+
}
|
|
114
|
+
return 'Session source not found.';
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read a session once through the provider's reader and compute its stats.
|
|
118
|
+
*
|
|
119
|
+
* The reader is drained with `readAll()` and then `flush()`ed so a final line
|
|
120
|
+
* without a trailing newline is included, and it is never opened a second
|
|
121
|
+
* time for the label. A source that cannot be read yields
|
|
122
|
+
* `availability: 'unavailable'` with a reason instead of silent zeros; a
|
|
123
|
+
* reader that reports truncation yields `partial`.
|
|
124
|
+
*/
|
|
125
|
+
export function readSessionFileStats(provider, sessionPath, options = {}) {
|
|
126
|
+
const base = {
|
|
127
|
+
providerId: provider.id,
|
|
128
|
+
sessionId: provider.getSessionId(sessionPath),
|
|
129
|
+
filePath: sessionPath,
|
|
130
|
+
};
|
|
131
|
+
let reader;
|
|
132
|
+
let events;
|
|
133
|
+
try {
|
|
134
|
+
reader = provider.createReader(sessionPath);
|
|
135
|
+
events = reader.readAll();
|
|
136
|
+
// Readers buffer line-oriented input; flushing emits a trailing partial
|
|
137
|
+
// line into the same array `readAll()` returned.
|
|
138
|
+
reader.flush();
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return computeSessionFileStats([], {
|
|
142
|
+
...base,
|
|
143
|
+
availability: 'unavailable',
|
|
144
|
+
unavailableReason: errorMessage(error),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (events.length === 0 && !reader.exists()) {
|
|
148
|
+
return computeSessionFileStats([], {
|
|
149
|
+
...base,
|
|
150
|
+
availability: 'unavailable',
|
|
151
|
+
unavailableReason: describeMissingSource(provider),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return computeSessionFileStats(events, {
|
|
155
|
+
...base,
|
|
156
|
+
label: options.resolveLabel?.() ?? undefined,
|
|
157
|
+
computeContextSize: providerContextSizeFn(provider),
|
|
158
|
+
availability: reader.wasTruncated() ? 'partial' : 'full',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session's token total, main thread plus subagents, in the shared
|
|
3
|
+
* `summarizeTokens` vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* Subagents run in their own transcripts (Claude Code `subagents/agent-*.jsonl`,
|
|
6
|
+
* Codex spawned rollouts, OpenCode child sessions), so the main-thread reader
|
|
7
|
+
* never sees their usage. Every surface that shows "the session total" goes
|
|
8
|
+
* through this module so dump, report, and both dashboards agree.
|
|
9
|
+
*
|
|
10
|
+
* @module sessionTokenTotals
|
|
11
|
+
*/
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { extractNormalizedUsage } from './usageNormalization';
|
|
14
|
+
import { summarizeTokens, sumTokenTotals, } from './tokenSummary';
|
|
15
|
+
/** Token record of one subagent, in `TokenTotalsLike` form. */
|
|
16
|
+
export function subagentTokenTotals(stats) {
|
|
17
|
+
return {
|
|
18
|
+
inputTokens: stats.inputTokens,
|
|
19
|
+
outputTokens: stats.outputTokens,
|
|
20
|
+
cacheReadTokens: stats.cacheReadTokens,
|
|
21
|
+
cacheWriteTokens: stats.cacheWriteTokens,
|
|
22
|
+
reasoningTokens: stats.reasoningTokens,
|
|
23
|
+
...(typeof stats.totalTokens === 'number' ? { totalTokens: stats.totalTokens } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Combine an already-known main-thread total with a session's subagent stats. */
|
|
27
|
+
export function combineSessionTokenTotals(mainThread, subagents) {
|
|
28
|
+
const subagentRecords = subagents.map(subagentTokenTotals);
|
|
29
|
+
const main = summarizeTokens(mainThread);
|
|
30
|
+
const mainRecord = {
|
|
31
|
+
inputTokens: main.input,
|
|
32
|
+
outputTokens: main.output,
|
|
33
|
+
cacheReadTokens: main.cacheRead,
|
|
34
|
+
cacheWriteTokens: main.cacheWrite,
|
|
35
|
+
reasoningTokens: main.reasoning,
|
|
36
|
+
totalTokens: main.total,
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
mainThread: main,
|
|
40
|
+
subagents: subagents.map((stats, index) => ({
|
|
41
|
+
agentId: stats.agentId,
|
|
42
|
+
...(stats.agentType ? { agentType: stats.agentType } : {}),
|
|
43
|
+
...(stats.description ? { description: stats.description } : {}),
|
|
44
|
+
summary: summarizeTokens(subagentRecords[index]),
|
|
45
|
+
})),
|
|
46
|
+
subagentTotal: summarizeTokens(sumTokenTotals(subagentRecords)),
|
|
47
|
+
combined: summarizeTokens(sumTokenTotals([mainRecord, ...subagentRecords])),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Sum the usage a provider reader reports for one session transcript. */
|
|
51
|
+
export function readMainThreadTokenTotals(provider, sessionPath) {
|
|
52
|
+
const reader = provider.createReader(sessionPath);
|
|
53
|
+
const events = reader.readAll();
|
|
54
|
+
reader.flush();
|
|
55
|
+
let inputTokens = 0;
|
|
56
|
+
let outputTokens = 0;
|
|
57
|
+
let cacheReadTokens = 0;
|
|
58
|
+
let cacheWriteTokens = 0;
|
|
59
|
+
let reasoningTokens = 0;
|
|
60
|
+
let totalTokens = 0;
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
const usage = extractNormalizedUsage(event);
|
|
63
|
+
if (!usage)
|
|
64
|
+
continue;
|
|
65
|
+
inputTokens += usage.uncachedInputTokens;
|
|
66
|
+
outputTokens += usage.outputTokens;
|
|
67
|
+
cacheReadTokens += usage.cacheReadTokens;
|
|
68
|
+
cacheWriteTokens += usage.cacheWriteTokens;
|
|
69
|
+
reasoningTokens += usage.reasoningTokens;
|
|
70
|
+
totalTokens += usage.totalTokens;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
inputTokens,
|
|
74
|
+
outputTokens,
|
|
75
|
+
cacheReadTokens,
|
|
76
|
+
cacheWriteTokens,
|
|
77
|
+
reasoningTokens,
|
|
78
|
+
totalTokens,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Main thread plus subagents for one session. Pass `mainThread` when the
|
|
83
|
+
* caller already aggregated the transcript (an `EventAggregator`'s tokens) to
|
|
84
|
+
* skip re-reading it. Subagent discovery failures degrade to "no subagents".
|
|
85
|
+
*/
|
|
86
|
+
export function collectSessionTokenTotals(provider, sessionPath, options = {}) {
|
|
87
|
+
const mainThread = options.mainThread ?? readMainThreadTokenTotals(provider, sessionPath);
|
|
88
|
+
return combineSessionTokenTotals(mainThread, scanSessionSubagents(provider, sessionPath));
|
|
89
|
+
}
|
|
90
|
+
/** A session's subagents by transcript path; discovery failures yield none. */
|
|
91
|
+
export function scanSessionSubagents(provider, sessionPath) {
|
|
92
|
+
try {
|
|
93
|
+
return provider.scanSubagents(path.dirname(sessionPath), provider.getSessionId(sessionPath));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { projectSessionTranscript, } from './transcript';
|
|
3
|
+
/** List recent sessions using the provider's existing discovery and metadata APIs. */
|
|
4
|
+
export function listRecentSessions(provider, workspacePath, options = {}) {
|
|
5
|
+
const limit = Math.max(0, options.limit ?? 20);
|
|
6
|
+
// Sort on mtime alone first so labels — the only per-file content read —
|
|
7
|
+
// are extracted just for the returned slice, not every discovered session.
|
|
8
|
+
return provider
|
|
9
|
+
.findAllSessions(workspacePath)
|
|
10
|
+
.map((sessionPath) => ({
|
|
11
|
+
sessionPath,
|
|
12
|
+
modifiedAt: sessionModifiedAt(provider, sessionPath).toISOString(),
|
|
13
|
+
}))
|
|
14
|
+
.sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt))
|
|
15
|
+
.slice(0, limit)
|
|
16
|
+
.map(({ sessionPath, modifiedAt }) => ({
|
|
17
|
+
provider: provider.id,
|
|
18
|
+
sessionId: provider.getSessionId(sessionPath),
|
|
19
|
+
sessionPath,
|
|
20
|
+
label: provider.extractSessionLabel(sessionPath),
|
|
21
|
+
modifiedAt,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
function sessionModifiedAt(provider, sessionPath) {
|
|
25
|
+
const metadata = provider.getSessionMetadata?.(sessionPath);
|
|
26
|
+
if (metadata)
|
|
27
|
+
return metadata.mtime;
|
|
28
|
+
try {
|
|
29
|
+
return statSync(sessionPath).mtime;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return new Date(0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Read a canonical transcript through a provider's existing reader. */
|
|
36
|
+
export function readSessionTranscript(provider, sessionPath, options = {}) {
|
|
37
|
+
const reader = provider.createReader(sessionPath);
|
|
38
|
+
try {
|
|
39
|
+
return projectSessionTranscript(reader.readAll(), {
|
|
40
|
+
...options,
|
|
41
|
+
provider: provider.id,
|
|
42
|
+
sessionId: provider.getSessionId(sessionPath),
|
|
43
|
+
sourcePath: sessionPath,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
reader.flush();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `state.json`: a small, versioned file for external consumers (tmux
|
|
3
|
+
* status bars, menu-bar apps, scripts) that want Sidekick's current account,
|
|
4
|
+
* quota windows with freshness, context usage, session cost, and the active
|
|
5
|
+
* billing block without running a Sidekick command.
|
|
6
|
+
*
|
|
7
|
+
* Written by the status line on every prompt and by both dashboards on their
|
|
8
|
+
* refresh ticks, but only when the content changed, so the hot path costs one
|
|
9
|
+
* small read in the common case. The schema is public: `schemaVersion` is 1,
|
|
10
|
+
* fields are only ever added, and the matching zod schema lives in
|
|
11
|
+
* `sidekick-shared/schemas` (`sidekickStateFileSchema`). This module does not
|
|
12
|
+
* import zod, so the status-line fast path stays small.
|
|
13
|
+
*
|
|
14
|
+
* @module stateFile
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import { getGlobalDataPath } from './paths';
|
|
18
|
+
import { classifyQuotaFreshness } from './quota';
|
|
19
|
+
import { atomicWriteJsonSync } from './writers/atomic';
|
|
20
|
+
export const STATE_FILE_SCHEMA_VERSION = 1;
|
|
21
|
+
export const STATE_FILE_NAME = 'state.json';
|
|
22
|
+
export function getStateFilePath() {
|
|
23
|
+
return getGlobalDataPath(STATE_FILE_NAME);
|
|
24
|
+
}
|
|
25
|
+
/** Read the current state file; null when absent or unreadable. */
|
|
26
|
+
export function readStateFile(filePath = getStateFilePath()) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
29
|
+
return parsed && parsed.schemaVersion === STATE_FILE_SCHEMA_VERSION ? parsed : null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Key-order-independent serialisation, dropping undefined values. */
|
|
36
|
+
function stableStringify(value) {
|
|
37
|
+
if (Array.isArray(value))
|
|
38
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
39
|
+
if (value && typeof value === 'object') {
|
|
40
|
+
const record = value;
|
|
41
|
+
const entries = Object.keys(record)
|
|
42
|
+
.sort()
|
|
43
|
+
.filter((key) => record[key] !== undefined)
|
|
44
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
|
|
45
|
+
return `{${entries.join(',')}}`;
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(value) ?? 'null';
|
|
48
|
+
}
|
|
49
|
+
/** The content that decides whether a rewrite is needed: everything the writer does not stamp. */
|
|
50
|
+
function comparable(state) {
|
|
51
|
+
return stableStringify({ ...state, schemaVersion: undefined, writtenAt: undefined });
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Write the state file atomically, but only when something other than
|
|
55
|
+
* `writtenAt` changed since the last write. Returns true when a write
|
|
56
|
+
* happened. Never throws: the state file is a convenience for other tools.
|
|
57
|
+
*/
|
|
58
|
+
export function writeStateFile(state, options = {}) {
|
|
59
|
+
const filePath = options.filePath ?? getStateFilePath();
|
|
60
|
+
try {
|
|
61
|
+
const existing = readStateFile(filePath);
|
|
62
|
+
if (existing && comparable(existing) === comparable(state))
|
|
63
|
+
return false;
|
|
64
|
+
const next = {
|
|
65
|
+
schemaVersion: STATE_FILE_SCHEMA_VERSION,
|
|
66
|
+
writtenAt: (options.now ?? new Date()).toISOString(),
|
|
67
|
+
...state,
|
|
68
|
+
};
|
|
69
|
+
atomicWriteJsonSync(filePath, next);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Project a quota sample onto the state-file shape. A live sample (for
|
|
78
|
+
* example from the status-line payload) carries `capturedAt` but no age, so
|
|
79
|
+
* the age and freshness are derived from `now` when the sample omits them.
|
|
80
|
+
*/
|
|
81
|
+
export function quotaToStateFile(quota, now = Date.now()) {
|
|
82
|
+
if (!quota || !quota.available)
|
|
83
|
+
return null;
|
|
84
|
+
let ageMs = typeof quota.ageMs === 'number' ? quota.ageMs : null;
|
|
85
|
+
let freshness = quota.freshness ?? null;
|
|
86
|
+
if (ageMs === null && quota.capturedAt) {
|
|
87
|
+
const capturedMs = Date.parse(quota.capturedAt);
|
|
88
|
+
if (Number.isFinite(capturedMs)) {
|
|
89
|
+
ageMs = Math.max(0, now - capturedMs);
|
|
90
|
+
freshness ??= classifyQuotaFreshness(ageMs);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
fiveHour: { utilization: quota.fiveHour.utilization, resetsAt: quota.fiveHour.resetsAt },
|
|
95
|
+
sevenDay: { utilization: quota.sevenDay.utilization, resetsAt: quota.sevenDay.resetsAt },
|
|
96
|
+
source: quota.source ?? null,
|
|
97
|
+
capturedSource: quota.capturedSource ?? null,
|
|
98
|
+
capturedAt: quota.capturedAt ?? null,
|
|
99
|
+
ageMs,
|
|
100
|
+
freshness,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Project a billing block onto the state-file shape (totals only). */
|
|
104
|
+
export function billingBlockToStateFile(block) {
|
|
105
|
+
if (!block)
|
|
106
|
+
return null;
|
|
107
|
+
return {
|
|
108
|
+
start: block.start,
|
|
109
|
+
end: block.end,
|
|
110
|
+
isActive: block.isActive,
|
|
111
|
+
tokens: block.tokens.total,
|
|
112
|
+
costUsd: block.costUsd,
|
|
113
|
+
costProvenance: block.costProvenance,
|
|
114
|
+
burnRatePerMinute: block.burnRatePerMinute,
|
|
115
|
+
projectedTokens: block.projectedTokens,
|
|
116
|
+
projectedCostUsd: block.projectedCostUsd,
|
|
117
|
+
remainingMs: block.remainingMs,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Sliding-window burn-rate calculator shared by every Sidekick surface. */
|
|
2
|
+
export class BurnRateCalculator {
|
|
3
|
+
events = [];
|
|
4
|
+
windowMs;
|
|
5
|
+
constructor(windowMinutes = 5) {
|
|
6
|
+
this.windowMs = windowMinutes * 60_000;
|
|
7
|
+
}
|
|
8
|
+
addEvent(tokens, timestamp = new Date()) {
|
|
9
|
+
this.events.push({ tokens, timestamp });
|
|
10
|
+
this.pruneOldEvents(timestamp);
|
|
11
|
+
}
|
|
12
|
+
calculateBurnRate(now = new Date()) {
|
|
13
|
+
this.pruneOldEvents(now);
|
|
14
|
+
if (this.events.length === 0)
|
|
15
|
+
return 0;
|
|
16
|
+
const totalTokens = this.events.reduce((sum, event) => sum + event.tokens, 0);
|
|
17
|
+
const elapsedMinutes = Math.max((now.getTime() - this.events[0].timestamp.getTime()) / 60_000, 1);
|
|
18
|
+
return totalTokens / elapsedMinutes;
|
|
19
|
+
}
|
|
20
|
+
estimateTimeToQuota(currentTokens, quotaLimit, now = new Date()) {
|
|
21
|
+
return estimateTimeToQuota(currentTokens, quotaLimit, this.calculateBurnRate(now));
|
|
22
|
+
}
|
|
23
|
+
getEventCount() {
|
|
24
|
+
return this.events.length;
|
|
25
|
+
}
|
|
26
|
+
reset() {
|
|
27
|
+
this.events = [];
|
|
28
|
+
}
|
|
29
|
+
pruneOldEvents(now) {
|
|
30
|
+
const cutoff = now.getTime() - this.windowMs;
|
|
31
|
+
this.events = this.events.filter((event) => event.timestamp.getTime() >= cutoff);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Pure ETA helper for cached or externally computed burn rates. */
|
|
35
|
+
export function estimateTimeToQuota(currentUsage, quotaLimit, burnRatePerMinute) {
|
|
36
|
+
if (burnRatePerMinute <= 0)
|
|
37
|
+
return null;
|
|
38
|
+
const remaining = quotaLimit - currentUsage;
|
|
39
|
+
return remaining <= 0 ? 0 : remaining / burnRatePerMinute;
|
|
40
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for the JSON Claude Code pipes to its status-line command.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code runs the configured `statusLine.command` after every response
|
|
5
|
+
* (debounced at 300 ms) with a JSON document on stdin describing the session:
|
|
6
|
+
* model, workspace, context-window usage, estimated session cost, and — for
|
|
7
|
+
* Claude.ai Pro and Max subscribers — the *official* five-hour and seven-day
|
|
8
|
+
* rate-limit windows. That last block is the same data `/usage` shows and
|
|
9
|
+
* needs no network call, so Sidekick treats it as its most authoritative
|
|
10
|
+
* quota source whenever the status line is installed.
|
|
11
|
+
*
|
|
12
|
+
* Only the fields Sidekick reads are typed here; the raw document is kept on
|
|
13
|
+
* `raw` for callers that need more. Browser-safe: no Node imports.
|
|
14
|
+
*
|
|
15
|
+
* @module statusline/claudeStatuslinePayload
|
|
16
|
+
*/
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function num(value) {
|
|
21
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
function nullableNum(value) {
|
|
24
|
+
if (value === null)
|
|
25
|
+
return null;
|
|
26
|
+
return num(value);
|
|
27
|
+
}
|
|
28
|
+
function str(value) {
|
|
29
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
30
|
+
}
|
|
31
|
+
function bool(value) {
|
|
32
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function window(value) {
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
return undefined;
|
|
37
|
+
const usedPercentage = num(value.used_percentage);
|
|
38
|
+
const resetsAt = num(value.resets_at);
|
|
39
|
+
if (usedPercentage === undefined || resetsAt === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
return { usedPercentage, resetsAt };
|
|
42
|
+
}
|
|
43
|
+
function defined(value) {
|
|
44
|
+
return Object.values(value).some((entry) => entry !== undefined) ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a status-line document. Returns `null` for empty input, non-JSON, or
|
|
48
|
+
* JSON that is not an object; never throws.
|
|
49
|
+
*/
|
|
50
|
+
export function parseClaudeStatuslinePayload(text) {
|
|
51
|
+
if (typeof text !== 'string' || !text.trim())
|
|
52
|
+
return null;
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(raw))
|
|
61
|
+
return null;
|
|
62
|
+
const model = isRecord(raw.model) ? raw.model : undefined;
|
|
63
|
+
const workspace = isRecord(raw.workspace) ? raw.workspace : undefined;
|
|
64
|
+
const contextWindow = isRecord(raw.context_window) ? raw.context_window : undefined;
|
|
65
|
+
const cost = isRecord(raw.cost) ? raw.cost : undefined;
|
|
66
|
+
const rateLimits = isRecord(raw.rate_limits) ? raw.rate_limits : undefined;
|
|
67
|
+
const promptCache = isRecord(raw.prompt_cache) ? raw.prompt_cache : undefined;
|
|
68
|
+
return {
|
|
69
|
+
sessionId: str(raw.session_id),
|
|
70
|
+
transcriptPath: str(raw.transcript_path),
|
|
71
|
+
cwd: str(raw.cwd),
|
|
72
|
+
model: model ? defined({ id: str(model.id), displayName: str(model.display_name) }) : undefined,
|
|
73
|
+
workspace: workspace
|
|
74
|
+
? defined({
|
|
75
|
+
currentDir: str(workspace.current_dir),
|
|
76
|
+
projectDir: str(workspace.project_dir),
|
|
77
|
+
gitWorktree: str(workspace.git_worktree),
|
|
78
|
+
})
|
|
79
|
+
: undefined,
|
|
80
|
+
contextWindow: contextWindow
|
|
81
|
+
? defined({
|
|
82
|
+
usedPercentage: nullableNum(contextWindow.used_percentage),
|
|
83
|
+
remainingPercentage: nullableNum(contextWindow.remaining_percentage),
|
|
84
|
+
contextWindowSize: num(contextWindow.context_window_size),
|
|
85
|
+
totalInputTokens: num(contextWindow.total_input_tokens),
|
|
86
|
+
totalOutputTokens: num(contextWindow.total_output_tokens),
|
|
87
|
+
})
|
|
88
|
+
: undefined,
|
|
89
|
+
cost: cost
|
|
90
|
+
? defined({
|
|
91
|
+
totalCostUsd: num(cost.total_cost_usd),
|
|
92
|
+
totalDurationMs: num(cost.total_duration_ms),
|
|
93
|
+
totalApiDurationMs: num(cost.total_api_duration_ms),
|
|
94
|
+
totalLinesAdded: num(cost.total_lines_added),
|
|
95
|
+
totalLinesRemoved: num(cost.total_lines_removed),
|
|
96
|
+
})
|
|
97
|
+
: undefined,
|
|
98
|
+
rateLimits: rateLimits
|
|
99
|
+
? defined({
|
|
100
|
+
fiveHour: window(rateLimits.five_hour),
|
|
101
|
+
sevenDay: window(rateLimits.seven_day),
|
|
102
|
+
spendLimit: window(rateLimits.spend_limit),
|
|
103
|
+
})
|
|
104
|
+
: undefined,
|
|
105
|
+
promptCache: promptCache
|
|
106
|
+
? defined({
|
|
107
|
+
warm: bool(promptCache.warm),
|
|
108
|
+
cachingObserved: bool(promptCache.caching_observed),
|
|
109
|
+
hitRatio: nullableNum(promptCache.hit_ratio),
|
|
110
|
+
requests: num(promptCache.requests),
|
|
111
|
+
misses: num(promptCache.misses),
|
|
112
|
+
expectedRebuilds: num(promptCache.expected_rebuilds),
|
|
113
|
+
})
|
|
114
|
+
: undefined,
|
|
115
|
+
raw,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function toWindow(live, fallback) {
|
|
119
|
+
if (live) {
|
|
120
|
+
return {
|
|
121
|
+
utilization: Math.max(0, live.usedPercentage),
|
|
122
|
+
resetsAt: new Date(live.resetsAt * 1000).toISOString(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return fallback ?? { utilization: 0, resetsAt: '' };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Build a `QuotaState` from the official rate-limit block. Returns `null`
|
|
129
|
+
* when the payload carries no rate limits at all (API-key users, or before
|
|
130
|
+
* the first response of a session), so callers fall back to their other
|
|
131
|
+
* sources.
|
|
132
|
+
*/
|
|
133
|
+
export function quotaFromStatuslinePayload(payload, options = {}) {
|
|
134
|
+
const limits = payload.rateLimits;
|
|
135
|
+
if (!limits?.fiveHour && !limits?.sevenDay)
|
|
136
|
+
return null;
|
|
137
|
+
const now = options.now ?? new Date();
|
|
138
|
+
const fallback = options.fallback ?? undefined;
|
|
139
|
+
return {
|
|
140
|
+
fiveHour: toWindow(limits.fiveHour, fallback?.fiveHour),
|
|
141
|
+
sevenDay: toWindow(limits.sevenDay, fallback?.sevenDay),
|
|
142
|
+
available: true,
|
|
143
|
+
providerId: 'claude-code',
|
|
144
|
+
source: 'statusline',
|
|
145
|
+
capturedAt: now.toISOString(),
|
|
146
|
+
stale: false,
|
|
147
|
+
};
|
|
148
|
+
}
|