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,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import finished sessions from every provider into the history store.
|
|
3
|
+
*
|
|
4
|
+
* The VS Code extension used to import Claude Code only, by hand-parsing
|
|
5
|
+
* JSONL files; the CLI could not import at all. This walks the sessions
|
|
6
|
+
* every provider knows about, reads each once through the unified stats
|
|
7
|
+
* path, and hands a `SessionSummary` to the caller, which applies it to the
|
|
8
|
+
* store it owns (the extension's debounced persistence service or the CLI's
|
|
9
|
+
* locked atomic writer). Dedupe is the caller's `isImported` answer
|
|
10
|
+
* (imported file paths plus persisted session ids); the capped per-session
|
|
11
|
+
* list is deliberately not used as the dedupe key.
|
|
12
|
+
*
|
|
13
|
+
* Node-only: reads session files.
|
|
14
|
+
*
|
|
15
|
+
* @module usage/importSessionHistory
|
|
16
|
+
*/
|
|
17
|
+
import { sessionSummaryFromStats } from '../historicalStore';
|
|
18
|
+
import { listSessionPreviewsAsync } from '../sessionPreviews';
|
|
19
|
+
import { readSessionFileStats } from '../sessionStats';
|
|
20
|
+
/** Files modified more recently than this are skipped: they may still be live. */
|
|
21
|
+
export const ACTIVE_SESSION_MTIME_THRESHOLD_MS = 60_000;
|
|
22
|
+
function yieldToEventLoop() {
|
|
23
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
24
|
+
}
|
|
25
|
+
export async function importSessionHistory(options) {
|
|
26
|
+
const now = options.now ?? new Date();
|
|
27
|
+
const threshold = options.activeFileThresholdMs ?? ACTIVE_SESSION_MTIME_THRESHOLD_MS;
|
|
28
|
+
const listed = await listSessionPreviewsAsync(options.providers, {
|
|
29
|
+
since: options.since,
|
|
30
|
+
workspacePath: options.workspacePath,
|
|
31
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
32
|
+
});
|
|
33
|
+
const providerById = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
34
|
+
const result = {
|
|
35
|
+
filesFound: listed.previews.length,
|
|
36
|
+
filesProcessed: 0,
|
|
37
|
+
filesSkipped: 0,
|
|
38
|
+
filesUnavailable: 0,
|
|
39
|
+
sessionsImported: 0,
|
|
40
|
+
messagesImported: 0,
|
|
41
|
+
diagnostics: [...listed.diagnostics],
|
|
42
|
+
};
|
|
43
|
+
const total = listed.previews.length;
|
|
44
|
+
let loaded = 0;
|
|
45
|
+
for (const preview of listed.previews) {
|
|
46
|
+
loaded += 1;
|
|
47
|
+
const provider = providerById.get(preview.provider);
|
|
48
|
+
const modifiedMs = Date.parse(preview.modifiedAt);
|
|
49
|
+
const recentlyModified = Number.isFinite(modifiedMs) && now.getTime() - modifiedMs < threshold;
|
|
50
|
+
if (!provider || recentlyModified || options.isImported(preview.sessionId, preview.filePath)) {
|
|
51
|
+
result.filesSkipped += 1;
|
|
52
|
+
options.onProgress?.(loaded, total);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const stats = readSessionFileStats(provider, preview.filePath);
|
|
56
|
+
result.filesProcessed += 1;
|
|
57
|
+
if (stats.availability === 'unavailable') {
|
|
58
|
+
result.filesUnavailable += 1;
|
|
59
|
+
result.diagnostics.push({
|
|
60
|
+
providerId: preview.provider,
|
|
61
|
+
kind: 'read_failed',
|
|
62
|
+
severity: 'warning',
|
|
63
|
+
phase: 'read',
|
|
64
|
+
message: `${preview.provider} session ${preview.sessionId}: ${stats.unavailableReason ?? 'unavailable'}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else if (Object.keys(stats.modelUsage).length === 0) {
|
|
68
|
+
// No usage record at all (for example a prompt that never got an
|
|
69
|
+
// answer): nothing to credit, but mark the file so it is not re-read
|
|
70
|
+
// on every run.
|
|
71
|
+
result.filesUnavailable += 1;
|
|
72
|
+
options.markImported(preview.filePath);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
options.applySummary(sessionSummaryFromStats(stats, { project: preview.workspacePath }), preview.filePath);
|
|
76
|
+
options.markImported(preview.filePath);
|
|
77
|
+
result.sessionsImported += 1;
|
|
78
|
+
result.messagesImported += stats.messageCount;
|
|
79
|
+
}
|
|
80
|
+
options.onProgress?.(loaded, total);
|
|
81
|
+
await yieldToEventLoop();
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accumulate normalized usage into a subagent's token fields so every
|
|
3
|
+
* provider's `scanSubagents()` reports the same four buckets and the same
|
|
4
|
+
* cache-inclusive total as `summarizeTokens()`.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no Node imports.
|
|
7
|
+
*
|
|
8
|
+
* @module usage/subagentUsage
|
|
9
|
+
*/
|
|
10
|
+
/** Add one usage record (a call or a correction) to a subagent's token totals. */
|
|
11
|
+
export function addUsageToSubagent(stats, usage) {
|
|
12
|
+
stats.inputTokens += usage.uncachedInputTokens;
|
|
13
|
+
stats.outputTokens += usage.outputTokens;
|
|
14
|
+
stats.cacheReadTokens = (stats.cacheReadTokens ?? 0) + usage.cacheReadTokens;
|
|
15
|
+
stats.cacheWriteTokens = (stats.cacheWriteTokens ?? 0) + usage.cacheWriteTokens;
|
|
16
|
+
stats.reasoningTokens = (stats.reasoningTokens ?? 0) + usage.reasoningTokens;
|
|
17
|
+
stats.totalTokens = (stats.totalTokens ?? 0) + usage.totalTokens;
|
|
18
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage events collected from session logs, cached per session.
|
|
3
|
+
*
|
|
4
|
+
* Feeds the billing-block view and the log-derived daily/weekly/monthly
|
|
5
|
+
* reports, so CLI-only users get numbers without the extension's history
|
|
6
|
+
* store. Each session is read once through its provider reader; the
|
|
7
|
+
* extracted usage is cached under `<configDir>/usage-cache/` keyed by the
|
|
8
|
+
* session's size and mtime, so a repeat run only re-reads sessions that
|
|
9
|
+
* changed. Token counts are cached raw and priced at load time, so a pricing
|
|
10
|
+
* catalog refresh never leaves stale costs behind. Cache files are touched on
|
|
11
|
+
* every hit and pruned least-recently-used, once per collection.
|
|
12
|
+
*
|
|
13
|
+
* A Claude Code session's subagents write their own transcripts under
|
|
14
|
+
* `<sessionId>/subagents/`; their usage is attributed to the parent session
|
|
15
|
+
* and their sizes and mtimes join the fingerprint. (Codex spawned subagents
|
|
16
|
+
* are top-level rollouts, already listed as sessions of their own.)
|
|
17
|
+
*
|
|
18
|
+
* Node-only: reads session files and the cache directory.
|
|
19
|
+
*
|
|
20
|
+
* @module usage/usageEvents
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import { getConfigDir } from '../paths';
|
|
25
|
+
import { listSessionPreviewsAsync } from '../sessionPreviews';
|
|
26
|
+
import { calculateNormalizedUsageCost, extractNormalizedUsage } from '../usageNormalization';
|
|
27
|
+
import { atomicWriteJson } from '../writers/atomic';
|
|
28
|
+
import { resolveWorkspaceRoots } from '../workspaceRoots';
|
|
29
|
+
/**
|
|
30
|
+
* Bump when the cached record shape or the counting rules change; older files
|
|
31
|
+
* are re-read. v2: Claude split lines and duplicate Codex `token_count` events
|
|
32
|
+
* are counted once, and Claude subagent transcripts are included.
|
|
33
|
+
*/
|
|
34
|
+
export const USAGE_CACHE_VERSION = 2;
|
|
35
|
+
/**
|
|
36
|
+
* Most-recently-used cache files kept on disk. Sized for a busy month of
|
|
37
|
+
* sessions (files are a few KB each); a collection larger than this still
|
|
38
|
+
* completes, it just re-reads what was evicted. Pruning runs once after a
|
|
39
|
+
* collection, never during one, so a working set is never evicted mid-run.
|
|
40
|
+
*/
|
|
41
|
+
export const MAX_USAGE_CACHE_FILES = 5000;
|
|
42
|
+
export function getUsageCacheDir() {
|
|
43
|
+
return path.join(getConfigDir(), 'usage-cache');
|
|
44
|
+
}
|
|
45
|
+
function cacheFilePath(dir, provider, sessionId) {
|
|
46
|
+
const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
47
|
+
return path.join(dir, `${provider}--${safe}.json`);
|
|
48
|
+
}
|
|
49
|
+
function toMs(value, label) {
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
return null;
|
|
52
|
+
const ms = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
53
|
+
if (!Number.isFinite(ms))
|
|
54
|
+
throw new RangeError(`Invalid \`${label}\` value: ${String(value)}`);
|
|
55
|
+
return ms;
|
|
56
|
+
}
|
|
57
|
+
/** Mark a cache file as recently used so LRU pruning keeps it. Best-effort. */
|
|
58
|
+
function touch(filePath) {
|
|
59
|
+
try {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
fs.utimesSync(filePath, now, now);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Read-only cache or vanished file; the next miss rewrites it.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readCache(filePath) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
70
|
+
if (parsed?.version !== USAGE_CACHE_VERSION || !Array.isArray(parsed.events))
|
|
71
|
+
return null;
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
|
|
79
|
+
export function pruneUsageCache(keep = MAX_USAGE_CACHE_FILES, dir = getUsageCacheDir()) {
|
|
80
|
+
try {
|
|
81
|
+
const entries = fs
|
|
82
|
+
.readdirSync(dir)
|
|
83
|
+
.filter((name) => name.endsWith('.json'))
|
|
84
|
+
.map((name) => {
|
|
85
|
+
const filePath = path.join(dir, name);
|
|
86
|
+
try {
|
|
87
|
+
return { filePath, mtimeMs: fs.statSync(filePath).mtimeMs };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
.filter((entry) => entry !== null)
|
|
94
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
95
|
+
let removed = 0;
|
|
96
|
+
for (const entry of entries.slice(Math.max(0, keep))) {
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(entry.filePath);
|
|
99
|
+
removed += 1;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Already gone; skip.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return removed;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function cachedEventFromUsage(usage, timestamp, model) {
|
|
112
|
+
const record = {
|
|
113
|
+
t: timestamp,
|
|
114
|
+
m: model,
|
|
115
|
+
i: usage.uncachedInputTokens,
|
|
116
|
+
o: usage.outputTokens,
|
|
117
|
+
cw: usage.cacheWriteTokens,
|
|
118
|
+
cr: usage.cacheReadTokens,
|
|
119
|
+
bo: usage.billableOutputTokens,
|
|
120
|
+
tt: usage.totalTokens,
|
|
121
|
+
};
|
|
122
|
+
if (usage.reportedCostUsd !== undefined)
|
|
123
|
+
record.rc = usage.reportedCostUsd;
|
|
124
|
+
return record;
|
|
125
|
+
}
|
|
126
|
+
/** Price a cached event with the current catalog (or its provider-reported cost). */
|
|
127
|
+
function priceCachedEvent(cached, provider, sessionId, project, sessionFile) {
|
|
128
|
+
const usage = {
|
|
129
|
+
uncachedInputTokens: cached.i,
|
|
130
|
+
cacheReadTokens: cached.cr,
|
|
131
|
+
cacheWriteTokens: cached.cw,
|
|
132
|
+
outputTokens: cached.o,
|
|
133
|
+
reasoningTokens: 0,
|
|
134
|
+
reasoningIncludedInOutput: true,
|
|
135
|
+
cacheInclusiveInputTokens: cached.i + cached.cr + cached.cw,
|
|
136
|
+
billableOutputTokens: cached.bo,
|
|
137
|
+
totalTokens: cached.tt,
|
|
138
|
+
...(cached.rc !== undefined ? { reportedCostUsd: cached.rc } : {}),
|
|
139
|
+
provenance: { semantics: 'sidekick-disjoint' },
|
|
140
|
+
};
|
|
141
|
+
const priced = calculateNormalizedUsageCost({
|
|
142
|
+
usage,
|
|
143
|
+
modelId: cached.m === 'unknown' ? undefined : cached.m,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
timestamp: cached.t,
|
|
147
|
+
provider,
|
|
148
|
+
sessionId,
|
|
149
|
+
project,
|
|
150
|
+
model: cached.m,
|
|
151
|
+
tokens: {
|
|
152
|
+
inputTokens: cached.i,
|
|
153
|
+
outputTokens: cached.o,
|
|
154
|
+
cacheWriteTokens: cached.cw,
|
|
155
|
+
cacheReadTokens: cached.cr,
|
|
156
|
+
totalTokens: cached.tt,
|
|
157
|
+
},
|
|
158
|
+
costUsd: priced.costUsd,
|
|
159
|
+
costProvenance: priced.source,
|
|
160
|
+
filePath: sessionFile,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const CLAUDE_AGENT_FILE = /^agent-.+\.jsonl$/;
|
|
164
|
+
/** Claude Code subagent transcripts of a session, sorted by name. */
|
|
165
|
+
function claudeSubagentFiles(preview) {
|
|
166
|
+
if (preview.provider !== 'claude-code')
|
|
167
|
+
return [];
|
|
168
|
+
const dir = path.join(path.dirname(preview.filePath), preview.sessionId, 'subagents');
|
|
169
|
+
let names;
|
|
170
|
+
try {
|
|
171
|
+
names = fs.readdirSync(dir).filter((name) => CLAUDE_AGENT_FILE.test(name));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const files = [];
|
|
177
|
+
for (const name of names.sort()) {
|
|
178
|
+
try {
|
|
179
|
+
const stat = fs.statSync(path.join(dir, name));
|
|
180
|
+
files.push({
|
|
181
|
+
path: path.join(dir, name),
|
|
182
|
+
name,
|
|
183
|
+
sizeBytes: stat.size,
|
|
184
|
+
modifiedAt: stat.mtimeMs,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Removed between listing and stat; the next collection re-reads.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return files;
|
|
192
|
+
}
|
|
193
|
+
function sessionFingerprint(preview, subagents) {
|
|
194
|
+
const base = `${preview.sizeBytes}:${preview.modifiedAt}`;
|
|
195
|
+
if (subagents.length === 0)
|
|
196
|
+
return base;
|
|
197
|
+
return `${base}|${subagents.map((f) => `${f.name}:${f.sizeBytes}:${f.modifiedAt}`).join(',')}`;
|
|
198
|
+
}
|
|
199
|
+
function readTranscriptUsage(provider, filePath, cached) {
|
|
200
|
+
const reader = provider.createReader(filePath);
|
|
201
|
+
const events = reader.readAll();
|
|
202
|
+
reader.flush();
|
|
203
|
+
// A correction tops up the call its message already recorded, so it merges
|
|
204
|
+
// into that record instead of counting as another call.
|
|
205
|
+
const byMessageId = new Map();
|
|
206
|
+
for (const event of events) {
|
|
207
|
+
const usage = extractNormalizedUsage(event);
|
|
208
|
+
if (!usage)
|
|
209
|
+
continue;
|
|
210
|
+
const messageId = event.message?.id;
|
|
211
|
+
const target = event.message?.usageKind === 'correction' && messageId
|
|
212
|
+
? byMessageId.get(messageId)
|
|
213
|
+
: undefined;
|
|
214
|
+
if (target) {
|
|
215
|
+
target.i += usage.uncachedInputTokens;
|
|
216
|
+
target.o += usage.outputTokens;
|
|
217
|
+
target.cw += usage.cacheWriteTokens;
|
|
218
|
+
target.cr += usage.cacheReadTokens;
|
|
219
|
+
target.bo += usage.billableOutputTokens;
|
|
220
|
+
target.tt += usage.totalTokens;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const timestamp = Date.parse(event.timestamp);
|
|
224
|
+
if (!Number.isFinite(timestamp))
|
|
225
|
+
continue;
|
|
226
|
+
const record = cachedEventFromUsage(usage, timestamp, event.message?.model ?? usage.model ?? 'unknown');
|
|
227
|
+
cached.push(record);
|
|
228
|
+
if (messageId)
|
|
229
|
+
byMessageId.set(messageId, record);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function readSessionUsage(provider, preview, subagents) {
|
|
233
|
+
const cached = [];
|
|
234
|
+
readTranscriptUsage(provider, preview.filePath, cached);
|
|
235
|
+
for (const file of subagents)
|
|
236
|
+
readTranscriptUsage(provider, file.path, cached);
|
|
237
|
+
cached.sort((a, b) => a.t - b.t);
|
|
238
|
+
return cached;
|
|
239
|
+
}
|
|
240
|
+
function yieldToEventLoop() {
|
|
241
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Collect priced usage events for every session touched in the window.
|
|
245
|
+
*
|
|
246
|
+
* Sessions are enumerated with `listSessionPreviewsAsync` (the `since`
|
|
247
|
+
* cutoff applies to the session mtime there and to the event timestamp
|
|
248
|
+
* here), read once each unless the cache already holds their fingerprint,
|
|
249
|
+
* and yielded between reads so long-lived hosts stay responsive.
|
|
250
|
+
*/
|
|
251
|
+
export async function collectUsageEvents(options) {
|
|
252
|
+
const sinceMs = toMs(options.since, 'since');
|
|
253
|
+
const untilMs = toMs(options.until, 'until');
|
|
254
|
+
const cacheDir = options.cacheDir ?? getUsageCacheDir();
|
|
255
|
+
const useCache = !options.noCache;
|
|
256
|
+
const signal = options.signal;
|
|
257
|
+
signal?.throwIfAborted();
|
|
258
|
+
const scope = usageScope(options);
|
|
259
|
+
const listed = await listScopedPreviews(options, scope?.listingRoots);
|
|
260
|
+
signal?.throwIfAborted();
|
|
261
|
+
const diagnostics = [...listed.diagnostics];
|
|
262
|
+
for (const diagnostic of listed.diagnostics)
|
|
263
|
+
options.onDiagnostic?.(diagnostic);
|
|
264
|
+
const providerById = new Map(options.providers.map((provider) => [provider.id, provider]));
|
|
265
|
+
const events = [];
|
|
266
|
+
const sessions = [];
|
|
267
|
+
let cacheHits = 0;
|
|
268
|
+
let cacheMisses = 0;
|
|
269
|
+
let wroteCache = false;
|
|
270
|
+
for (const preview of listed.previews) {
|
|
271
|
+
signal?.throwIfAborted();
|
|
272
|
+
const provider = providerById.get(preview.provider);
|
|
273
|
+
if (!provider)
|
|
274
|
+
continue;
|
|
275
|
+
const subagentFiles = claudeSubagentFiles(preview);
|
|
276
|
+
const fingerprint = sessionFingerprint(preview, subagentFiles);
|
|
277
|
+
const filePath = cacheFilePath(cacheDir, preview.provider, preview.sessionId);
|
|
278
|
+
let cachedEvents = null;
|
|
279
|
+
let project = preview.workspacePath;
|
|
280
|
+
let fromCache = false;
|
|
281
|
+
if (useCache) {
|
|
282
|
+
const cached = readCache(filePath);
|
|
283
|
+
if (cached && cached.fingerprint === fingerprint && cached.sessionId === preview.sessionId) {
|
|
284
|
+
cachedEvents = cached.events;
|
|
285
|
+
project = project ?? cached.project;
|
|
286
|
+
fromCache = true;
|
|
287
|
+
cacheHits += 1;
|
|
288
|
+
touch(filePath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!cachedEvents) {
|
|
292
|
+
cacheMisses += 1;
|
|
293
|
+
try {
|
|
294
|
+
cachedEvents = readSessionUsage(provider, preview, subagentFiles);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
const diagnostic = {
|
|
298
|
+
providerId: preview.provider,
|
|
299
|
+
kind: 'read_failed',
|
|
300
|
+
severity: 'warning',
|
|
301
|
+
phase: 'read',
|
|
302
|
+
message: `Could not read usage for ${preview.provider} session ${preview.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
303
|
+
};
|
|
304
|
+
diagnostics.push(diagnostic);
|
|
305
|
+
options.onDiagnostic?.(diagnostic);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (useCache) {
|
|
309
|
+
const record = {
|
|
310
|
+
version: USAGE_CACHE_VERSION,
|
|
311
|
+
provider: preview.provider,
|
|
312
|
+
sessionId: preview.sessionId,
|
|
313
|
+
fingerprint,
|
|
314
|
+
project,
|
|
315
|
+
events: cachedEvents,
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
await atomicWriteJson(filePath, record);
|
|
319
|
+
wroteCache = true;
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// The cache is an accelerator; a failed write only costs a re-read.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
await yieldToEventLoop();
|
|
326
|
+
}
|
|
327
|
+
let first = null;
|
|
328
|
+
let last = null;
|
|
329
|
+
let count = 0;
|
|
330
|
+
for (const cached of cachedEvents) {
|
|
331
|
+
if (sinceMs !== null && cached.t < sinceMs)
|
|
332
|
+
continue;
|
|
333
|
+
if (untilMs !== null && cached.t > untilMs)
|
|
334
|
+
continue;
|
|
335
|
+
events.push(priceCachedEvent(cached, preview.provider, preview.sessionId, project, preview.filePath));
|
|
336
|
+
first = first === null ? cached.t : Math.min(first, cached.t);
|
|
337
|
+
last = last === null ? cached.t : Math.max(last, cached.t);
|
|
338
|
+
count += 1;
|
|
339
|
+
}
|
|
340
|
+
sessions.push({
|
|
341
|
+
provider: preview.provider,
|
|
342
|
+
sessionId: preview.sessionId,
|
|
343
|
+
filePath: preview.filePath,
|
|
344
|
+
project,
|
|
345
|
+
fingerprint,
|
|
346
|
+
firstTimestamp: first,
|
|
347
|
+
lastTimestamp: last,
|
|
348
|
+
eventCount: count,
|
|
349
|
+
fromCache,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (wroteCache)
|
|
353
|
+
pruneUsageCache(MAX_USAGE_CACHE_FILES, cacheDir);
|
|
354
|
+
events.sort((a, b) => a.timestamp - b.timestamp);
|
|
355
|
+
return {
|
|
356
|
+
events,
|
|
357
|
+
sessions,
|
|
358
|
+
diagnostics,
|
|
359
|
+
cacheHits,
|
|
360
|
+
cacheMisses,
|
|
361
|
+
...(scope ? { roots: scope.roots } : {}),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/** The roots to list, or undefined for an unscoped (or legacy single-root) call. */
|
|
365
|
+
function usageScope(options) {
|
|
366
|
+
if (options.workspacePaths) {
|
|
367
|
+
return resolveWorkspaceRoots(options.workspacePaths, {
|
|
368
|
+
includeWorktrees: options.includeWorktrees !== false,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (options.workspacePath && options.includeWorktrees === true) {
|
|
372
|
+
return resolveWorkspaceRoots([options.workspacePath], { includeWorktrees: true });
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
/** Lists previews once per root and keeps each session file once. */
|
|
377
|
+
async function listScopedPreviews(options, listingRoots) {
|
|
378
|
+
const list = (workspacePath) => listSessionPreviewsAsync(options.providers, {
|
|
379
|
+
since: options.since,
|
|
380
|
+
workspacePath,
|
|
381
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
382
|
+
concurrency: options.concurrency,
|
|
383
|
+
});
|
|
384
|
+
if (!listingRoots)
|
|
385
|
+
return list(options.workspacePath);
|
|
386
|
+
const byFile = new Map();
|
|
387
|
+
const diagnostics = new Map();
|
|
388
|
+
for (const root of listingRoots) {
|
|
389
|
+
options.signal?.throwIfAborted();
|
|
390
|
+
const listed = await list(root);
|
|
391
|
+
// Each root lists every provider again; report each problem once.
|
|
392
|
+
for (const diagnostic of listed.diagnostics) {
|
|
393
|
+
diagnostics.set(JSON.stringify(diagnostic), diagnostic);
|
|
394
|
+
}
|
|
395
|
+
for (const preview of listed.previews) {
|
|
396
|
+
if (!byFile.has(preview.filePath))
|
|
397
|
+
byFile.set(preview.filePath, preview);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const previews = [...byFile.values()].sort((left, right) => Date.parse(right.modifiedAt) - Date.parse(left.modifiedAt));
|
|
401
|
+
return { previews, diagnostics: [...diagnostics.values()] };
|
|
402
|
+
}
|