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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily, weekly, monthly, and per-session usage rows bucketed from usage
|
|
3
|
+
* events by the time of each event, so a session that crosses midnight is
|
|
4
|
+
* split across the days it actually ran in. Browser-safe: no Node imports.
|
|
5
|
+
*
|
|
6
|
+
* @module usage/usageReports
|
|
7
|
+
*/
|
|
8
|
+
import { classifyCostProvenance } from '../aggregation/costProvenance';
|
|
9
|
+
import { formatLocalDateKey } from '../formatting';
|
|
10
|
+
function dayParts(ms, utc) {
|
|
11
|
+
const date = new Date(ms);
|
|
12
|
+
return utc
|
|
13
|
+
? {
|
|
14
|
+
year: date.getUTCFullYear(),
|
|
15
|
+
month: date.getUTCMonth(),
|
|
16
|
+
day: date.getUTCDate(),
|
|
17
|
+
weekday: date.getUTCDay(),
|
|
18
|
+
}
|
|
19
|
+
: {
|
|
20
|
+
year: date.getFullYear(),
|
|
21
|
+
month: date.getMonth(),
|
|
22
|
+
day: date.getDate(),
|
|
23
|
+
weekday: date.getDay(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function pad(value) {
|
|
27
|
+
return String(value).padStart(2, '0');
|
|
28
|
+
}
|
|
29
|
+
/** The `YYYY-MM-DD` key of the week containing `ms`, starting on `weekStartsOn`. */
|
|
30
|
+
export function weekKey(ms, options = {}) {
|
|
31
|
+
const utc = options.utc ?? false;
|
|
32
|
+
const start = options.weekStartsOn ?? 1;
|
|
33
|
+
const parts = dayParts(ms, utc);
|
|
34
|
+
const offset = (parts.weekday - start + 7) % 7;
|
|
35
|
+
if (utc) {
|
|
36
|
+
return new Date(Date.UTC(parts.year, parts.month, parts.day - offset))
|
|
37
|
+
.toISOString()
|
|
38
|
+
.slice(0, 10);
|
|
39
|
+
}
|
|
40
|
+
return formatLocalDateKey(new Date(parts.year, parts.month, parts.day - offset));
|
|
41
|
+
}
|
|
42
|
+
/** The time key for one event at the requested granularity. */
|
|
43
|
+
export function usageBucketKey(event, options) {
|
|
44
|
+
const utc = options.utc ?? false;
|
|
45
|
+
switch (options.granularity) {
|
|
46
|
+
case 'session':
|
|
47
|
+
return event.sessionId;
|
|
48
|
+
case 'week':
|
|
49
|
+
return weekKey(event.timestamp, { utc, weekStartsOn: options.weekStartsOn });
|
|
50
|
+
case 'month': {
|
|
51
|
+
const parts = dayParts(event.timestamp, utc);
|
|
52
|
+
return `${parts.year}-${pad(parts.month + 1)}`;
|
|
53
|
+
}
|
|
54
|
+
case 'day':
|
|
55
|
+
default:
|
|
56
|
+
return utc
|
|
57
|
+
? new Date(event.timestamp).toISOString().slice(0, 10)
|
|
58
|
+
: formatLocalDateKey(event.timestamp);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function emptyTokens() {
|
|
62
|
+
return { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 };
|
|
63
|
+
}
|
|
64
|
+
function addTokens(target, source) {
|
|
65
|
+
target.inputTokens += source.inputTokens;
|
|
66
|
+
target.outputTokens += source.outputTokens;
|
|
67
|
+
target.cacheWriteTokens += source.cacheWriteTokens;
|
|
68
|
+
target.cacheReadTokens += source.cacheReadTokens;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Group usage events into rows. Rows are sorted by key (sessions by first
|
|
72
|
+
* event), then provider, project, and model, so tables read oldest first.
|
|
73
|
+
*/
|
|
74
|
+
export function bucketUsage(events, options) {
|
|
75
|
+
const groupBy = new Set(options.groupBy ?? ['provider']);
|
|
76
|
+
const isSession = options.granularity === 'session';
|
|
77
|
+
const byProvider = isSession || groupBy.has('provider');
|
|
78
|
+
const byProject = isSession || groupBy.has('project');
|
|
79
|
+
const byModel = groupBy.has('model');
|
|
80
|
+
const buckets = new Map();
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
const key = usageBucketKey(event, options);
|
|
83
|
+
const provider = byProvider ? event.provider : null;
|
|
84
|
+
const project = byProject ? event.project : null;
|
|
85
|
+
const model = byModel ? event.model : null;
|
|
86
|
+
const id = [key, provider ?? '', project ?? '', model ?? ''].join('|');
|
|
87
|
+
let acc = buckets.get(id);
|
|
88
|
+
if (!acc) {
|
|
89
|
+
acc = {
|
|
90
|
+
row: {
|
|
91
|
+
key,
|
|
92
|
+
granularity: options.granularity,
|
|
93
|
+
provider,
|
|
94
|
+
project,
|
|
95
|
+
model,
|
|
96
|
+
sessionId: isSession ? event.sessionId : null,
|
|
97
|
+
models: [],
|
|
98
|
+
calls: 0,
|
|
99
|
+
sessions: 0,
|
|
100
|
+
tokens: emptyTokens(),
|
|
101
|
+
totalTokens: 0,
|
|
102
|
+
costUsd: 0,
|
|
103
|
+
costProvenance: 'none',
|
|
104
|
+
unpricedCalls: 0,
|
|
105
|
+
firstTimestamp: event.timestamp,
|
|
106
|
+
lastTimestamp: event.timestamp,
|
|
107
|
+
},
|
|
108
|
+
sessionIds: new Set(),
|
|
109
|
+
modelTokens: new Map(),
|
|
110
|
+
reportedCalls: 0,
|
|
111
|
+
estimatedCalls: 0,
|
|
112
|
+
};
|
|
113
|
+
buckets.set(id, acc);
|
|
114
|
+
}
|
|
115
|
+
const { row } = acc;
|
|
116
|
+
row.calls += 1;
|
|
117
|
+
addTokens(row.tokens, event.tokens);
|
|
118
|
+
row.totalTokens += event.tokens.totalTokens;
|
|
119
|
+
row.firstTimestamp = Math.min(row.firstTimestamp, event.timestamp);
|
|
120
|
+
row.lastTimestamp = Math.max(row.lastTimestamp, event.timestamp);
|
|
121
|
+
acc.sessionIds.add(event.sessionId);
|
|
122
|
+
acc.modelTokens.set(event.model, (acc.modelTokens.get(event.model) ?? 0) + event.tokens.totalTokens);
|
|
123
|
+
if (event.costUsd !== null) {
|
|
124
|
+
row.costUsd += event.costUsd;
|
|
125
|
+
if (event.costProvenance === 'provider-reported')
|
|
126
|
+
acc.reportedCalls += 1;
|
|
127
|
+
else
|
|
128
|
+
acc.estimatedCalls += 1;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
row.unpricedCalls += 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const rows = [];
|
|
135
|
+
for (const acc of buckets.values()) {
|
|
136
|
+
acc.row.sessions = acc.sessionIds.size;
|
|
137
|
+
acc.row.models = [...acc.modelTokens.entries()]
|
|
138
|
+
.sort((a, b) => b[1] - a[1])
|
|
139
|
+
.map(([model]) => model);
|
|
140
|
+
acc.row.costProvenance = classifyCostProvenance({
|
|
141
|
+
reportedCalls: acc.reportedCalls,
|
|
142
|
+
estimatedCalls: acc.estimatedCalls,
|
|
143
|
+
unpricedCalls: acc.row.unpricedCalls,
|
|
144
|
+
});
|
|
145
|
+
rows.push(acc.row);
|
|
146
|
+
}
|
|
147
|
+
return rows.sort((a, b) => (isSession ? a.firstTimestamp - b.firstTimestamp : a.key.localeCompare(b.key)) ||
|
|
148
|
+
(a.provider ?? '').localeCompare(b.provider ?? '') ||
|
|
149
|
+
(a.project ?? '').localeCompare(b.project ?? '') ||
|
|
150
|
+
(a.model ?? '').localeCompare(b.model ?? ''));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Sum rows that do not overlap (for example every row of one `bucketUsage`
|
|
154
|
+
* call). Pass the events too when rows are not per-session, so distinct
|
|
155
|
+
* sessions are counted once instead of once per row they appear in.
|
|
156
|
+
*/
|
|
157
|
+
export function summarizeUsageRows(rows, events) {
|
|
158
|
+
const totals = {
|
|
159
|
+
calls: 0,
|
|
160
|
+
sessions: 0,
|
|
161
|
+
tokens: emptyTokens(),
|
|
162
|
+
totalTokens: 0,
|
|
163
|
+
costUsd: 0,
|
|
164
|
+
costProvenance: 'none',
|
|
165
|
+
unpricedCalls: 0,
|
|
166
|
+
};
|
|
167
|
+
let reportedCalls = 0;
|
|
168
|
+
let estimatedCalls = 0;
|
|
169
|
+
const sessionIds = new Set();
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
totals.calls += row.calls;
|
|
172
|
+
addTokens(totals.tokens, row.tokens);
|
|
173
|
+
totals.totalTokens += row.totalTokens;
|
|
174
|
+
totals.costUsd += row.costUsd;
|
|
175
|
+
totals.unpricedCalls += row.unpricedCalls;
|
|
176
|
+
const priced = row.calls - row.unpricedCalls;
|
|
177
|
+
if (row.costProvenance === 'reported')
|
|
178
|
+
reportedCalls += priced;
|
|
179
|
+
else if (row.costProvenance === 'estimated')
|
|
180
|
+
estimatedCalls += priced;
|
|
181
|
+
else if (row.costProvenance === 'mixed') {
|
|
182
|
+
reportedCalls += 1;
|
|
183
|
+
estimatedCalls += 1;
|
|
184
|
+
}
|
|
185
|
+
if (row.sessionId)
|
|
186
|
+
sessionIds.add(row.sessionId);
|
|
187
|
+
}
|
|
188
|
+
if (events) {
|
|
189
|
+
for (const event of events)
|
|
190
|
+
sessionIds.add(event.sessionId);
|
|
191
|
+
totals.sessions = sessionIds.size;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
totals.sessions = sessionIds.size || rows.reduce((sum, row) => sum + row.sessions, 0);
|
|
195
|
+
}
|
|
196
|
+
totals.costProvenance = classifyCostProvenance({
|
|
197
|
+
reportedCalls,
|
|
198
|
+
estimatedCalls,
|
|
199
|
+
unpricedCalls: totals.unpricedCalls,
|
|
200
|
+
});
|
|
201
|
+
return totals;
|
|
202
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getModelPricing } from './modelInfo';
|
|
2
|
+
function tokenCount(value) {
|
|
3
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
4
|
+
? Math.min(Number.MAX_SAFE_INTEGER, Math.floor(value))
|
|
5
|
+
: 0;
|
|
6
|
+
}
|
|
7
|
+
function reportedCost(value) {
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Convert provider-specific counters into finite, nonnegative, disjoint categories. */
|
|
11
|
+
export function normalizeProviderUsage(input) {
|
|
12
|
+
const rawInput = tokenCount(input.inputTokens);
|
|
13
|
+
const cacheReadTokens = tokenCount(input.cacheReadTokens);
|
|
14
|
+
const cacheWriteTokens = tokenCount(input.cacheWriteTokens);
|
|
15
|
+
const outputTokens = tokenCount(input.outputTokens);
|
|
16
|
+
const reasoningTokens = tokenCount(input.reasoningTokens);
|
|
17
|
+
const isOpenAI = input.semantics === 'openai';
|
|
18
|
+
const uncachedInputTokens = isOpenAI ? Math.max(0, rawInput - cacheReadTokens) : rawInput;
|
|
19
|
+
const reasoningIncludedInOutput = input.reasoningIncludedInOutput ?? input.semantics === 'openai';
|
|
20
|
+
const cacheInclusiveInputTokens = uncachedInputTokens + cacheReadTokens + cacheWriteTokens;
|
|
21
|
+
const billableOutputTokens = outputTokens + (reasoningIncludedInOutput ? 0 : reasoningTokens);
|
|
22
|
+
const totalTokens = cacheInclusiveInputTokens + billableOutputTokens;
|
|
23
|
+
const semantics = isOpenAI
|
|
24
|
+
? 'openai-inclusive-cache'
|
|
25
|
+
: input.semantics === 'anthropic'
|
|
26
|
+
? 'anthropic-additive-cache'
|
|
27
|
+
: 'sidekick-disjoint';
|
|
28
|
+
return {
|
|
29
|
+
uncachedInputTokens,
|
|
30
|
+
cacheReadTokens,
|
|
31
|
+
cacheWriteTokens,
|
|
32
|
+
outputTokens,
|
|
33
|
+
reasoningTokens,
|
|
34
|
+
reasoningIncludedInOutput,
|
|
35
|
+
cacheInclusiveInputTokens,
|
|
36
|
+
billableOutputTokens,
|
|
37
|
+
totalTokens,
|
|
38
|
+
...(input.model ? { model: input.model } : {}),
|
|
39
|
+
...(reportedCost(input.reportedCostUsd) !== undefined
|
|
40
|
+
? { reportedCostUsd: reportedCost(input.reportedCostUsd) }
|
|
41
|
+
: {}),
|
|
42
|
+
provenance: {
|
|
43
|
+
semantics,
|
|
44
|
+
...(input.provider ? { provider: input.provider } : {}),
|
|
45
|
+
...(input.source ? { source: input.source } : {}),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Extract normalized usage from an event, preserving parser-owned semantics when present. */
|
|
50
|
+
export function extractNormalizedUsage(event) {
|
|
51
|
+
if (event.message?.normalizedUsage)
|
|
52
|
+
return event.message.normalizedUsage;
|
|
53
|
+
const usage = event.message?.usage;
|
|
54
|
+
if (!usage)
|
|
55
|
+
return null;
|
|
56
|
+
const providerId = event.providerMetadata?.providerId;
|
|
57
|
+
return normalizeProviderUsage({
|
|
58
|
+
semantics: 'sidekick',
|
|
59
|
+
provider: providerId,
|
|
60
|
+
source: event.providerMetadata?.source ?? 'legacy-message-usage',
|
|
61
|
+
model: event.message?.model,
|
|
62
|
+
inputTokens: usage.input_tokens,
|
|
63
|
+
outputTokens: usage.output_tokens,
|
|
64
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
65
|
+
cacheWriteTokens: usage.cache_creation_input_tokens,
|
|
66
|
+
reasoningTokens: usage.reasoning_tokens,
|
|
67
|
+
reasoningIncludedInOutput: providerId === 'codex' || providerId === 'openai',
|
|
68
|
+
reportedCostUsd: usage.reported_cost,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Price normalized, disjoint usage without double-counting reasoning tokens. */
|
|
72
|
+
export function calculateNormalizedUsageCost(input) {
|
|
73
|
+
const { usage } = input;
|
|
74
|
+
const modelId = input.modelId ?? usage.model;
|
|
75
|
+
if (usage.reportedCostUsd !== undefined) {
|
|
76
|
+
return { costUsd: usage.reportedCostUsd, source: 'provider-reported', modelId, usage };
|
|
77
|
+
}
|
|
78
|
+
const explicit = input.pricing !== undefined;
|
|
79
|
+
const pricing = explicit ? input.pricing : modelId ? getModelPricing(modelId) : null;
|
|
80
|
+
if (!pricing)
|
|
81
|
+
return { costUsd: null, source: 'unpriced', modelId, usage };
|
|
82
|
+
const costUsd = (usage.uncachedInputTokens / 1_000_000) * pricing.inputCostPerMillion +
|
|
83
|
+
(usage.cacheReadTokens / 1_000_000) * pricing.cacheReadCostPerMillion +
|
|
84
|
+
(usage.cacheWriteTokens / 1_000_000) * pricing.cacheWriteCostPerMillion +
|
|
85
|
+
(usage.billableOutputTokens / 1_000_000) * pricing.outputCostPerMillion;
|
|
86
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
87
|
+
return { costUsd: null, source: 'unpriced', modelId, usage };
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
costUsd,
|
|
91
|
+
source: explicit ? 'explicit-pricing' : 'model-catalog',
|
|
92
|
+
modelId,
|
|
93
|
+
usage,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts SessionEvent (rich canonical format) to FollowEvent (flat summary).
|
|
3
|
+
*
|
|
4
|
+
* This bridge enables the two-tier event system:
|
|
5
|
+
* Provider logs → Shared parsers → SessionEvent → toFollowEvents() → FollowEvent
|
|
6
|
+
*
|
|
7
|
+
* SessionEvent preserves full tool inputs, results, and message structure.
|
|
8
|
+
* FollowEvent is a lossy summary for timeline display and CLI output.
|
|
9
|
+
*/
|
|
10
|
+
import { formatToolSummary } from '../formatters/toolSummary';
|
|
11
|
+
/**
|
|
12
|
+
* Converts a single SessionEvent to one or more FollowEvents.
|
|
13
|
+
*
|
|
14
|
+
* A single SessionEvent (e.g. an assistant message with tool_use blocks)
|
|
15
|
+
* may produce multiple FollowEvents (one per tool_use + one for text).
|
|
16
|
+
*/
|
|
17
|
+
export function toFollowEvents(event, providerId) {
|
|
18
|
+
const events = [];
|
|
19
|
+
const ts = event.timestamp || new Date().toISOString();
|
|
20
|
+
const permissionMode = event.permissionMode;
|
|
21
|
+
const usage = event.message?.usage;
|
|
22
|
+
const tokens = usage
|
|
23
|
+
? { input: usage.input_tokens || 0, output: usage.output_tokens || 0 }
|
|
24
|
+
: undefined;
|
|
25
|
+
const cacheTokens = usage && (usage.cache_read_input_tokens || usage.cache_creation_input_tokens)
|
|
26
|
+
? { read: usage.cache_read_input_tokens || 0, write: usage.cache_creation_input_tokens || 0 }
|
|
27
|
+
: undefined;
|
|
28
|
+
const cost = usage?.reported_cost;
|
|
29
|
+
const usageKind = usage ? event.message?.usageKind : undefined;
|
|
30
|
+
const model = event.message?.model;
|
|
31
|
+
const rateLimits = event.rateLimits;
|
|
32
|
+
switch (event.type) {
|
|
33
|
+
case 'user': {
|
|
34
|
+
const content = event.message?.content;
|
|
35
|
+
// Extract tool_result blocks from user messages
|
|
36
|
+
if (Array.isArray(content)) {
|
|
37
|
+
for (const block of content) {
|
|
38
|
+
if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
|
|
39
|
+
const resultText = typeof block.content === 'string' ? truncate(block.content, 120) : '';
|
|
40
|
+
events.push({
|
|
41
|
+
providerId,
|
|
42
|
+
type: 'tool_result',
|
|
43
|
+
timestamp: ts,
|
|
44
|
+
summary: resultText || '(tool result)',
|
|
45
|
+
raw: block,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const text = extractTextContent(content);
|
|
51
|
+
if (text || events.length === 0) {
|
|
52
|
+
events.push({
|
|
53
|
+
providerId,
|
|
54
|
+
type: 'user',
|
|
55
|
+
timestamp: ts,
|
|
56
|
+
summary: text || '(user message)',
|
|
57
|
+
model,
|
|
58
|
+
raw: event,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'assistant': {
|
|
64
|
+
const content = event.message?.content;
|
|
65
|
+
// Extract tool_use blocks first
|
|
66
|
+
if (Array.isArray(content)) {
|
|
67
|
+
for (const block of content) {
|
|
68
|
+
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
69
|
+
const input = block.input
|
|
70
|
+
? summarizeToolInput(block.name, block.input)
|
|
71
|
+
: '';
|
|
72
|
+
events.push({
|
|
73
|
+
providerId,
|
|
74
|
+
type: 'tool_use',
|
|
75
|
+
timestamp: ts,
|
|
76
|
+
summary: input ? `${block.name} ${input}` : block.name,
|
|
77
|
+
toolName: block.name,
|
|
78
|
+
toolInput: input,
|
|
79
|
+
model,
|
|
80
|
+
raw: block,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Emit assistant text (if any)
|
|
86
|
+
const text = extractTextContent(content);
|
|
87
|
+
if (text || events.length === 0) {
|
|
88
|
+
events.push({
|
|
89
|
+
providerId,
|
|
90
|
+
type: 'assistant',
|
|
91
|
+
timestamp: ts,
|
|
92
|
+
summary: text || '(thinking...)',
|
|
93
|
+
tokens,
|
|
94
|
+
cacheTokens,
|
|
95
|
+
...(usageKind ? { usageKind } : {}),
|
|
96
|
+
cost,
|
|
97
|
+
model,
|
|
98
|
+
raw: event,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else if (tokens) {
|
|
102
|
+
// Attach tokens to the last tool_use event if no separate text
|
|
103
|
+
const last = events[events.length - 1];
|
|
104
|
+
last.tokens = tokens;
|
|
105
|
+
last.cacheTokens = cacheTokens;
|
|
106
|
+
if (usageKind)
|
|
107
|
+
last.usageKind = usageKind;
|
|
108
|
+
last.cost = cost;
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'tool_use': {
|
|
113
|
+
const name = event.tool?.name || 'unknown';
|
|
114
|
+
const input = event.tool?.input ? summarizeToolInput(name, event.tool.input) : '';
|
|
115
|
+
events.push({
|
|
116
|
+
providerId,
|
|
117
|
+
type: 'tool_use',
|
|
118
|
+
timestamp: ts,
|
|
119
|
+
summary: input ? `${name} ${input}` : name,
|
|
120
|
+
toolName: name,
|
|
121
|
+
toolInput: input,
|
|
122
|
+
model,
|
|
123
|
+
raw: event,
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'tool_result': {
|
|
128
|
+
const output = event.result?.output;
|
|
129
|
+
const text = typeof output === 'string' ? truncate(output, 120) : '';
|
|
130
|
+
events.push({
|
|
131
|
+
providerId,
|
|
132
|
+
type: 'tool_result',
|
|
133
|
+
timestamp: ts,
|
|
134
|
+
summary: text || '(tool result)',
|
|
135
|
+
raw: event,
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'summary': {
|
|
140
|
+
events.push({
|
|
141
|
+
providerId,
|
|
142
|
+
type: 'summary',
|
|
143
|
+
timestamp: ts,
|
|
144
|
+
summary: 'Context compacted',
|
|
145
|
+
compaction: event.compaction,
|
|
146
|
+
raw: event,
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 'system': {
|
|
151
|
+
const text = extractTextContent(event.message?.content);
|
|
152
|
+
const label = event.message?.sourceLabel || event.message?.role || 'system';
|
|
153
|
+
const summary = text ||
|
|
154
|
+
(usage ? `Tokens: ${usage.input_tokens || 0} in / ${usage.output_tokens || 0} out` : label);
|
|
155
|
+
events.push({
|
|
156
|
+
providerId,
|
|
157
|
+
type: 'system',
|
|
158
|
+
timestamp: ts,
|
|
159
|
+
summary: label && text ? `${label}: ${text}` : summary,
|
|
160
|
+
tokens,
|
|
161
|
+
cacheTokens,
|
|
162
|
+
...(usageKind ? { usageKind } : {}),
|
|
163
|
+
cost,
|
|
164
|
+
model,
|
|
165
|
+
rateLimits,
|
|
166
|
+
raw: event,
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
default: {
|
|
171
|
+
// Handle 'result' and other types as system events
|
|
172
|
+
const evtType = event.type;
|
|
173
|
+
if (evtType === 'result') {
|
|
174
|
+
events.push({
|
|
175
|
+
providerId,
|
|
176
|
+
type: 'system',
|
|
177
|
+
timestamp: ts,
|
|
178
|
+
summary: 'Session ended',
|
|
179
|
+
raw: event,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Propagate permission mode to all generated events
|
|
186
|
+
if (permissionMode) {
|
|
187
|
+
for (const e of events) {
|
|
188
|
+
e.permissionMode = permissionMode;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (rateLimits) {
|
|
192
|
+
for (const e of events) {
|
|
193
|
+
e.rateLimits = e.rateLimits ?? rateLimits;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return events;
|
|
197
|
+
}
|
|
198
|
+
// ── Helpers ──
|
|
199
|
+
function extractTextContent(content) {
|
|
200
|
+
if (!content)
|
|
201
|
+
return '';
|
|
202
|
+
if (typeof content === 'string')
|
|
203
|
+
return truncate(content, 200);
|
|
204
|
+
if (Array.isArray(content)) {
|
|
205
|
+
for (const block of content) {
|
|
206
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
207
|
+
return truncate(block.text, 200);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
function summarizeToolInput(toolName, input) {
|
|
214
|
+
return formatToolSummary(toolName, input);
|
|
215
|
+
}
|
|
216
|
+
function truncate(text, maxLen) {
|
|
217
|
+
const clean = text.replace(/\s+/g, ' ').trim();
|
|
218
|
+
return clean.length > maxLen ? clean.substring(0, maxLen - 3) + '...' : clean;
|
|
219
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating the correct session watcher by provider.
|
|
3
|
+
*/
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { getOpenCodeDataDir } from '../providers/openCode';
|
|
6
|
+
import { JsonlSessionWatcher } from './jsonlWatcher';
|
|
7
|
+
import { ProviderReaderSessionWatcher } from './providerReaderWatcher';
|
|
8
|
+
import { SqliteSessionWatcher } from './sqliteWatcher';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the session a command should act on: an exact or unique-prefix
|
|
11
|
+
* match of `sessionId` among the workspace's sessions, or the most recent
|
|
12
|
+
* session when no id is given. Throws with the available ids on a miss.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveSessionPath(provider, workspacePath, sessionId) {
|
|
15
|
+
const sessions = provider.findAllSessions(workspacePath);
|
|
16
|
+
if (sessions.length === 0) {
|
|
17
|
+
throw new Error(`No sessions found for ${provider.displayName} in ${workspacePath}`);
|
|
18
|
+
}
|
|
19
|
+
if (!sessionId)
|
|
20
|
+
return sessions[0]; // most recent
|
|
21
|
+
const matches = sessions.map((sessionPath) => ({
|
|
22
|
+
sessionPath,
|
|
23
|
+
id: provider.getSessionId(sessionPath),
|
|
24
|
+
}));
|
|
25
|
+
const exact = matches.filter((candidate) => candidate.id === sessionId);
|
|
26
|
+
const candidates = exact.length > 0 ? exact : matches.filter((candidate) => candidate.id.startsWith(sessionId));
|
|
27
|
+
if (candidates.length === 0) {
|
|
28
|
+
throw new Error(`Session ${sessionId} not found. Available: ${sessions
|
|
29
|
+
.slice(0, 5)
|
|
30
|
+
.map((s) => path.basename(s))
|
|
31
|
+
.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
if (candidates.length > 1) {
|
|
34
|
+
throw new Error(`Session ${sessionId} is ambiguous. Matches: ${candidates
|
|
35
|
+
.slice(0, 5)
|
|
36
|
+
.map((candidate) => candidate.id)
|
|
37
|
+
.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
return candidates[0].sessionPath;
|
|
40
|
+
}
|
|
41
|
+
export function createWatcher(options) {
|
|
42
|
+
const { provider, workspacePath, sessionId, callbacks } = options;
|
|
43
|
+
const sessionPath = resolveSessionPath(provider, workspacePath, sessionId);
|
|
44
|
+
const watcher = createWatcherForProvider(provider, sessionPath, callbacks);
|
|
45
|
+
return { watcher, sessionPath };
|
|
46
|
+
}
|
|
47
|
+
function createWatcherForProvider(provider, sessionPath, callbacks) {
|
|
48
|
+
switch (provider.id) {
|
|
49
|
+
case 'claude-code':
|
|
50
|
+
return new JsonlSessionWatcher(provider.id, sessionPath, callbacks);
|
|
51
|
+
case 'codex':
|
|
52
|
+
return new ProviderReaderSessionWatcher(provider, sessionPath, callbacks);
|
|
53
|
+
case 'opencode': {
|
|
54
|
+
const dataDir = getOpenCodeDataDir();
|
|
55
|
+
const dbPath = path.join(dataDir, 'opencode.db');
|
|
56
|
+
// Session ID is the basename without extension
|
|
57
|
+
const sid = path.basename(sessionPath, '.json');
|
|
58
|
+
return new SqliteSessionWatcher(dbPath, sid, callbacks);
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`Unsupported provider: ${provider.id}`);
|
|
62
|
+
}
|
|
63
|
+
}
|