sidekick-shared 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +49 -3
  3. package/dist/accountChanges.js +22 -3
  4. package/dist/accountRegistry.d.ts +22 -1
  5. package/dist/accountRegistry.js +37 -14
  6. package/dist/accountStatus.d.ts +9 -1
  7. package/dist/accountStatus.js +3 -3
  8. package/dist/accounts.d.ts +3 -1
  9. package/dist/accounts.js +84 -64
  10. package/dist/aggregation/EventAggregator.d.ts +15 -2
  11. package/dist/aggregation/EventAggregator.js +54 -9
  12. package/dist/aggregation/costProvenance.d.ts +14 -0
  13. package/dist/aggregation/costProvenance.js +35 -0
  14. package/dist/aggregation/snapshot.d.ts +13 -2
  15. package/dist/aggregation/snapshot.js +57 -6
  16. package/dist/aggregation/types.d.ts +30 -1
  17. package/dist/analytics/compactionLedger.js +15 -5
  18. package/dist/analytics/qualityScore.js +2 -1
  19. package/dist/browser.d.ts +11 -1
  20. package/dist/browser.js +29 -1
  21. package/dist/codexProfiles.d.ts +3 -1
  22. package/dist/codexProfiles.js +4 -2
  23. package/dist/codexQuota.d.ts +2 -0
  24. package/dist/codexQuota.js +11 -66
  25. package/dist/failingTools.d.ts +37 -0
  26. package/dist/failingTools.js +70 -0
  27. package/dist/formatters/sessionDump.js +13 -7
  28. package/dist/formatting.d.ts +13 -0
  29. package/dist/formatting.js +35 -0
  30. package/dist/historicalStore.d.ts +47 -0
  31. package/dist/historicalStore.js +312 -0
  32. package/dist/index.d.ts +46 -12
  33. package/dist/index.js +112 -24
  34. package/dist/node.d.ts +19 -0
  35. package/dist/node.js +47 -1
  36. package/dist/observedContextWindows.d.ts +7 -0
  37. package/dist/observedContextWindows.js +17 -2
  38. package/dist/observedSessionCollector.d.ts +3 -8
  39. package/dist/observedSessionCollector.js +10 -27
  40. package/dist/parsers/sessionActivityDetector.d.ts +24 -2
  41. package/dist/parsers/sessionActivityDetector.js +95 -17
  42. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  43. package/dist/parsers/sessionPathResolver.js +13 -10
  44. package/dist/peakHours.d.ts +34 -3
  45. package/dist/peakHours.js +92 -7
  46. package/dist/pricingCatalog.d.ts +7 -0
  47. package/dist/pricingCatalog.js +8 -5
  48. package/dist/providers/claudeCode.js +11 -96
  49. package/dist/providers/codex.d.ts +2 -0
  50. package/dist/providers/codex.js +94 -259
  51. package/dist/providers/detect.d.ts +2 -0
  52. package/dist/providers/detect.js +29 -24
  53. package/dist/providers/factory.d.ts +6 -0
  54. package/dist/providers/openCode.d.ts +2 -0
  55. package/dist/providers/openCode.js +105 -121
  56. package/dist/providers/rolloutWalker.d.ts +53 -0
  57. package/dist/providers/rolloutWalker.js +209 -0
  58. package/dist/providers/types.d.ts +30 -0
  59. package/dist/quota.d.ts +30 -3
  60. package/dist/quota.js +35 -3
  61. package/dist/quotaHistory.d.ts +1 -1
  62. package/dist/quotaHistory.js +36 -35
  63. package/dist/quotaResolver.d.ts +81 -0
  64. package/dist/quotaResolver.js +213 -0
  65. package/dist/quotaSnapshots.d.ts +15 -2
  66. package/dist/quotaSnapshots.js +25 -3
  67. package/dist/quotaThresholds.d.ts +60 -0
  68. package/dist/quotaThresholds.js +90 -0
  69. package/dist/report/htmlReportGenerator.js +10 -4
  70. package/dist/report/sessionReportInputs.d.ts +23 -0
  71. package/dist/report/sessionReportInputs.js +34 -0
  72. package/dist/schemas/index.d.ts +2 -0
  73. package/dist/schemas/index.js +10 -1
  74. package/dist/schemas/quota.d.ts +139 -0
  75. package/dist/schemas/quota.js +17 -2
  76. package/dist/schemas/quotaHistory.d.ts +1 -0
  77. package/dist/schemas/quotaHistory.js +1 -1
  78. package/dist/schemas/sessionEvent.d.ts +6 -0
  79. package/dist/schemas/sessionEvent.js +1 -0
  80. package/dist/schemas/stateFile.d.ts +198 -0
  81. package/dist/schemas/stateFile.js +77 -0
  82. package/dist/schemas/transcript.js +2 -0
  83. package/dist/sessionFingerprint.d.ts +25 -0
  84. package/dist/sessionFingerprint.js +46 -0
  85. package/dist/sessionPreviews.d.ts +7 -1
  86. package/dist/sessionPreviews.js +15 -3
  87. package/dist/sessionStats.d.ts +51 -0
  88. package/dist/sessionStats.js +166 -0
  89. package/dist/stateFile.d.ts +113 -0
  90. package/dist/stateFile.js +160 -0
  91. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  92. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  93. package/dist/statusline/formatter.d.ts +13 -0
  94. package/dist/statusline/formatter.js +48 -6
  95. package/dist/statusline/index.d.ts +10 -2
  96. package/dist/statusline/index.js +19 -1
  97. package/dist/tokenSummary.d.ts +62 -0
  98. package/dist/tokenSummary.js +72 -0
  99. package/dist/transcript.d.ts +5 -0
  100. package/dist/transcript.js +2 -0
  101. package/dist/types/observedSessionV1.d.ts +2 -0
  102. package/dist/types/observedSessionV1.js +75 -18
  103. package/dist/types/sessionEvent.d.ts +2 -0
  104. package/dist/usage/billingBlocks.d.ts +85 -0
  105. package/dist/usage/billingBlocks.js +153 -0
  106. package/dist/usage/importSessionHistory.d.ts +53 -0
  107. package/dist/usage/importSessionHistory.js +88 -0
  108. package/dist/usage/usageEvents.d.ts +92 -0
  109. package/dist/usage/usageEvents.js +322 -0
  110. package/dist/usage/usageReports.d.ts +76 -0
  111. package/dist/usage/usageReports.js +208 -0
  112. package/dist/watchers/factory.d.ts +6 -0
  113. package/dist/watchers/factory.js +30 -25
  114. package/dist/zaiQuotaApi.d.ts +2 -0
  115. package/dist/zaiQuotaApi.js +2 -0
  116. package/package.json +1 -1
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.derivePendingUserRequestV1 = derivePendingUserRequestV1;
4
+ exports.getObservedActivityReason = getObservedActivityReason;
4
5
  exports.createProviderSessionAdapterV1 = createProviderSessionAdapterV1;
5
6
  const transcript_1 = require("../transcript");
6
7
  const sessionActivityDetector_1 = require("../parsers/sessionActivityDetector");
8
+ const sessionStats_1 = require("../sessionStats");
9
+ const sessionFingerprint_1 = require("../sessionFingerprint");
7
10
  function evidence(provider, sessionId, sourcePath, event, eventIndex) {
8
11
  return [
9
12
  {
@@ -111,6 +114,16 @@ function providerCapabilities(provider, observationOnly) {
111
114
  assetExtraction: known(true),
112
115
  };
113
116
  }
117
+ /**
118
+ * Detector reason behind each observed session's activity, kept off the
119
+ * versioned V1 shape so cache refreshes can tell a grace-period "active" from
120
+ * a genuinely ongoing one.
121
+ */
122
+ const activityReasons = new WeakMap();
123
+ /** The activity detector's reason for a session produced by this adapter, if known. */
124
+ function getObservedActivityReason(session) {
125
+ return activityReasons.get(session);
126
+ }
114
127
  function createProviderSessionAdapterV1(sessionProvider, options = {}) {
115
128
  const pollIntervalMs = options.pollIntervalMs ?? 2_000;
116
129
  const adapter = {
@@ -121,16 +134,28 @@ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
121
134
  return Promise.all(sessionProvider.findAllSessions(cwd).map((sessionPath) => adapter.read(sessionPath, cwd)));
122
135
  },
123
136
  async read(sessionPath, cwd = '') {
124
- const stats = sessionProvider.readSessionStats(sessionPath);
125
- const events = sessionProvider.createReader(sessionPath).readAll();
137
+ // One reader pass feeds stats, transcript, activity, and the pending
138
+ // request; the reader is flushed so a trailing partial line is included.
139
+ const sessionId = sessionProvider.getSessionId(sessionPath);
140
+ const reader = sessionProvider.createReader(sessionPath);
141
+ const events = reader.readAll();
142
+ reader.flush();
143
+ const stats = (0, sessionStats_1.computeSessionFileStats)(events, {
144
+ providerId: sessionProvider.id,
145
+ sessionId,
146
+ filePath: sessionPath,
147
+ label: '',
148
+ computeContextSize: (0, sessionStats_1.providerContextSizeFn)(sessionProvider),
149
+ });
126
150
  const transcript = (0, transcript_1.projectSessionTranscript)(events, {
127
151
  provider: sessionProvider.id,
128
- sessionId: stats.sessionId,
152
+ sessionId,
129
153
  sourcePath: sessionPath,
130
154
  });
131
- const refs = evidence(sessionProvider.id, stats.sessionId, sessionPath);
155
+ const refs = evidence(sessionProvider.id, sessionId, sessionPath);
132
156
  const model = Object.entries(stats.modelUsage).sort((left, right) => right[1].calls - left[1].calls)[0]?.[0] ?? null;
133
- const activityResult = (0, sessionActivityDetector_1.detectSessionActivity)(sessionPath);
157
+ const parts = (0, sessionFingerprint_1.sessionFingerprintParts)(sessionProvider, sessionPath);
158
+ const activityResult = (0, sessionActivityDetector_1.classifySessionActivity)({ events, mtimeMs: parts?.mtimeMs ?? null });
134
159
  const activity = activityResult.state === 'ongoing'
135
160
  ? 'active'
136
161
  : activityResult.state === 'stale'
@@ -139,11 +164,11 @@ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
139
164
  const allCostsReported = transcript.usage.costs.length > 0 &&
140
165
  transcript.usage.costs.every((cost) => cost.source === 'provider-reported');
141
166
  const observedAt = new Date().toISOString();
142
- return {
167
+ const session = {
143
168
  schemaVersion: 1,
144
169
  identity: {
145
170
  provider: sessionProvider.id,
146
- sessionId: stats.sessionId,
171
+ sessionId,
147
172
  sourcePath: sessionPath,
148
173
  },
149
174
  cwd: observed(cwd, 'inferred', cwd ? 0.95 : 0.4, refs),
@@ -161,29 +186,61 @@ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
161
186
  ? 'inferred'
162
187
  : 'estimated', allCostsReported ? 1 : transcript.usage.totalCostUsd === null ? 0.5 : 0.9, refs),
163
188
  },
164
- pendingUserRequest: derivePendingUserRequestV1(sessionProvider.id, stats.sessionId, sessionPath, events),
189
+ pendingUserRequest: derivePendingUserRequestV1(sessionProvider.id, sessionId, sessionPath, events),
165
190
  contentObservedAt: observedAt,
166
191
  observedAt,
167
192
  };
193
+ activityReasons.set(session, activityResult.reason);
194
+ return session;
168
195
  },
169
196
  watch(sessionPath, listener, cwd) {
170
197
  let disposed = false;
171
198
  let lastSignature = '';
199
+ let lastFingerprint = null;
200
+ let lastSession = null;
201
+ const emitIfChanged = (session) => {
202
+ const signature = JSON.stringify({
203
+ activity: session.activity,
204
+ model: session.model,
205
+ usage: session.usage,
206
+ pendingUserRequest: session.pendingUserRequest,
207
+ });
208
+ if (signature !== lastSignature) {
209
+ lastSignature = signature;
210
+ listener(session);
211
+ }
212
+ };
172
213
  const update = async () => {
173
214
  if (disposed)
174
215
  return;
175
216
  try {
176
- const session = await adapter.read(sessionPath, cwd);
177
- const signature = JSON.stringify({
178
- activity: session.activity,
179
- model: session.model,
180
- usage: session.usage,
181
- pendingUserRequest: session.pendingUserRequest,
182
- });
183
- if (signature !== lastSignature) {
184
- lastSignature = signature;
185
- listener(session);
217
+ const parts = (0, sessionFingerprint_1.sessionFingerprintParts)(sessionProvider, sessionPath);
218
+ const fingerprint = parts ? (0, sessionFingerprint_1.fingerprintString)(parts) : null;
219
+ if (lastSession && parts && fingerprint === lastFingerprint) {
220
+ // Unchanged content: only the activity can drift with time, so
221
+ // refresh it from the cached classification instead of re-reading.
222
+ const previousReason = activityReasons.get(lastSession);
223
+ const refreshed = (0, sessionActivityDetector_1.refreshSessionActivityState)(lastSession.activity.value, parts.mtimeMs, Date.now(), previousReason);
224
+ if (refreshed !== lastSession.activity.value) {
225
+ const session = {
226
+ ...lastSession,
227
+ activity: { ...lastSession.activity, value: refreshed },
228
+ observedAt: new Date().toISOString(),
229
+ };
230
+ activityReasons.set(session, refreshed === 'idle'
231
+ ? 'mtime-stale'
232
+ : previousReason === 'grace-period'
233
+ ? 'ending-event'
234
+ : 'no-activity-signal');
235
+ lastSession = session;
236
+ emitIfChanged(session);
237
+ }
238
+ return;
186
239
  }
240
+ const session = await adapter.read(sessionPath, cwd);
241
+ lastFingerprint = fingerprint;
242
+ lastSession = session;
243
+ emitIfChanged(session);
187
244
  }
188
245
  catch {
189
246
  // Observed sources can disappear between polling iterations.
@@ -47,6 +47,8 @@ export interface SessionMessage {
47
47
  normalizedUsage?: import('../usageNormalization').NormalizedUsage;
48
48
  /** Message content (may be string or structured content) */
49
49
  content?: unknown;
50
+ /** Provider stop reason (`end_turn`, `tool_use`, …) when the transcript carries one. */
51
+ stop_reason?: string;
50
52
  }
51
53
  /**
52
54
  * Canonical session event from JSONL/DB log files.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Five-hour billing blocks computed from usage events.
3
+ *
4
+ * Mirrors the ccusage convention: a block opens at the first usage event
5
+ * (aligned down to the UTC hour), lasts five hours, and an event that lands
6
+ * after the block's end or more than five hours after the previous event opens
7
+ * a new block. The active block also reports a burn rate and an end-of-block
8
+ * projection. Browser-safe: no Node imports.
9
+ *
10
+ * @module usage/billingBlocks
11
+ */
12
+ import type { AggregatedCostProvenance } from '../aggregation/types';
13
+ import type { TokenTotalsLike } from '../tokenSummary';
14
+ import type { PricingProvenance } from '../usageNormalization';
15
+ /** Length of one billing block. */
16
+ export declare const BILLING_BLOCK_DURATION_MS: number;
17
+ /** Minimal usage event the block calculator needs. */
18
+ export interface BillingBlockInput {
19
+ /** Event time (ms since epoch, ISO string, or Date). */
20
+ timestamp: number | string | Date;
21
+ tokens: TokenTotalsLike;
22
+ /** Priced cost for the event, or null when it could not be priced. */
23
+ costUsd: number | null;
24
+ costProvenance?: PricingProvenance;
25
+ model?: string;
26
+ }
27
+ export interface BillingBlockTokens {
28
+ inputTokens: number;
29
+ outputTokens: number;
30
+ cacheWriteTokens: number;
31
+ cacheReadTokens: number;
32
+ /** Cache-inclusive total (`summarizeTokens().total`). */
33
+ total: number;
34
+ }
35
+ export interface BillingBlockModelUsage {
36
+ calls: number;
37
+ /** Cache-inclusive tokens. */
38
+ tokens: number;
39
+ costUsd: number;
40
+ }
41
+ export interface BillingBlock {
42
+ /** Block identifier: the ISO start time. */
43
+ id: string;
44
+ /** ISO start (aligned to the hour when `alignToHour` is set). */
45
+ start: string;
46
+ /** ISO end (`start` + block duration). */
47
+ end: string;
48
+ firstEvent: string;
49
+ lastEvent: string;
50
+ /** `now` is inside the block and within one block length of the last event. */
51
+ isActive: boolean;
52
+ calls: number;
53
+ tokens: BillingBlockTokens;
54
+ costUsd: number;
55
+ costProvenance: AggregatedCostProvenance;
56
+ unpricedCalls: number;
57
+ models: Record<string, BillingBlockModelUsage>;
58
+ /** Time from the first event to `now` (active) or to the last event (closed). */
59
+ elapsedMs: number;
60
+ /** Time left until `end` for the active block; 0 otherwise. */
61
+ remainingMs: number;
62
+ /** Tokens per minute over `elapsedMs`. */
63
+ burnRatePerMinute: number;
64
+ /** Tokens expected by `end` at the current burn rate (equals `tokens.total` for closed blocks). */
65
+ projectedTokens: number;
66
+ /** Cost expected by `end` at the current cost per token (equals `costUsd` for closed blocks). */
67
+ projectedCostUsd: number;
68
+ }
69
+ export interface ComputeBillingBlocksOptions {
70
+ /** Clock used for activity and projections (default `Date.now()`). */
71
+ now?: Date | number;
72
+ /** Block length (default five hours). */
73
+ blockDurationMs?: number;
74
+ /** Align each block's start down to the UTC hour (default true, as ccusage does). */
75
+ alignToHour?: boolean;
76
+ }
77
+ /**
78
+ * Group usage events into billing blocks, oldest first.
79
+ *
80
+ * Events with an unparseable timestamp are ignored. Events are sorted before
81
+ * grouping, so callers may pass them in any order.
82
+ */
83
+ export declare function computeBillingBlocks(events: readonly BillingBlockInput[], options?: ComputeBillingBlocksOptions): BillingBlock[];
84
+ /** The block that is still open at the calculator's `now`, if any. */
85
+ export declare function findActiveBillingBlock(blocks: readonly BillingBlock[]): BillingBlock | null;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ /**
3
+ * Five-hour billing blocks computed from usage events.
4
+ *
5
+ * Mirrors the ccusage convention: a block opens at the first usage event
6
+ * (aligned down to the UTC hour), lasts five hours, and an event that lands
7
+ * after the block's end or more than five hours after the previous event opens
8
+ * a new block. The active block also reports a burn rate and an end-of-block
9
+ * projection. Browser-safe: no Node imports.
10
+ *
11
+ * @module usage/billingBlocks
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.BILLING_BLOCK_DURATION_MS = void 0;
15
+ exports.computeBillingBlocks = computeBillingBlocks;
16
+ exports.findActiveBillingBlock = findActiveBillingBlock;
17
+ const costProvenance_1 = require("../aggregation/costProvenance");
18
+ const BurnRateCalculator_1 = require("../statusline/BurnRateCalculator");
19
+ const tokenSummary_1 = require("../tokenSummary");
20
+ /** Length of one billing block. */
21
+ exports.BILLING_BLOCK_DURATION_MS = 5 * 3_600_000;
22
+ function toMs(value) {
23
+ if (value instanceof Date)
24
+ return value.getTime();
25
+ if (typeof value === 'number')
26
+ return value;
27
+ return Date.parse(value);
28
+ }
29
+ function normalizeInput(event) {
30
+ const timestamp = toMs(event.timestamp);
31
+ if (!Number.isFinite(timestamp))
32
+ return null;
33
+ const costUsd = typeof event.costUsd === 'number' && Number.isFinite(event.costUsd) ? event.costUsd : null;
34
+ return {
35
+ timestamp,
36
+ tokens: event.tokens,
37
+ costUsd,
38
+ costProvenance: costUsd === null ? 'unpriced' : (event.costProvenance ?? 'model-catalog'),
39
+ model: event.model ?? 'unknown',
40
+ };
41
+ }
42
+ function floorToUtcHour(ms) {
43
+ return ms - (ms % 3_600_000);
44
+ }
45
+ function buildBlock(events, start, durationMs, nowMs) {
46
+ const end = start + durationMs;
47
+ const first = events[0].timestamp;
48
+ const last = events[events.length - 1].timestamp;
49
+ const isActive = nowMs >= start && nowMs < end && nowMs - last < durationMs;
50
+ const tokens = { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 };
51
+ let total = 0;
52
+ let costUsd = 0;
53
+ let reportedCalls = 0;
54
+ let estimatedCalls = 0;
55
+ let unpricedCalls = 0;
56
+ const models = {};
57
+ const burn = new BurnRateCalculator_1.BurnRateCalculator(durationMs / 60_000);
58
+ for (const event of events) {
59
+ const summary = (0, tokenSummary_1.summarizeTokens)(event.tokens);
60
+ tokens.inputTokens += summary.input;
61
+ tokens.outputTokens += summary.output;
62
+ tokens.cacheWriteTokens += summary.cacheWrite;
63
+ tokens.cacheReadTokens += summary.cacheRead;
64
+ total += summary.total;
65
+ burn.addEvent(summary.total, new Date(event.timestamp));
66
+ const model = models[event.model] ?? { calls: 0, tokens: 0, costUsd: 0 };
67
+ model.calls += 1;
68
+ model.tokens += summary.total;
69
+ if (event.costUsd !== null) {
70
+ costUsd += event.costUsd;
71
+ model.costUsd += event.costUsd;
72
+ if (event.costProvenance === 'provider-reported')
73
+ reportedCalls += 1;
74
+ else
75
+ estimatedCalls += 1;
76
+ }
77
+ else {
78
+ unpricedCalls += 1;
79
+ }
80
+ models[event.model] = model;
81
+ }
82
+ const rateAt = isActive ? nowMs : last;
83
+ const elapsedMs = Math.max(0, rateAt - first);
84
+ const burnRatePerMinute = burn.calculateBurnRate(new Date(rateAt));
85
+ const remainingMs = isActive ? Math.max(0, end - nowMs) : 0;
86
+ const projectedTokens = isActive
87
+ ? Math.round(total + burnRatePerMinute * (remainingMs / 60_000))
88
+ : total;
89
+ const costPerToken = total > 0 ? costUsd / total : 0;
90
+ const projectedCostUsd = isActive ? costUsd + costPerToken * (projectedTokens - total) : costUsd;
91
+ const startIso = new Date(start).toISOString();
92
+ return {
93
+ id: startIso,
94
+ start: startIso,
95
+ end: new Date(end).toISOString(),
96
+ firstEvent: new Date(first).toISOString(),
97
+ lastEvent: new Date(last).toISOString(),
98
+ isActive,
99
+ calls: events.length,
100
+ tokens: { ...tokens, total },
101
+ costUsd,
102
+ costProvenance: (0, costProvenance_1.classifyCostProvenance)({ reportedCalls, estimatedCalls, unpricedCalls }),
103
+ unpricedCalls,
104
+ models,
105
+ elapsedMs,
106
+ remainingMs,
107
+ burnRatePerMinute,
108
+ projectedTokens,
109
+ projectedCostUsd,
110
+ };
111
+ }
112
+ /**
113
+ * Group usage events into billing blocks, oldest first.
114
+ *
115
+ * Events with an unparseable timestamp are ignored. Events are sorted before
116
+ * grouping, so callers may pass them in any order.
117
+ */
118
+ function computeBillingBlocks(events, options = {}) {
119
+ const durationMs = options.blockDurationMs ?? exports.BILLING_BLOCK_DURATION_MS;
120
+ const alignToHour = options.alignToHour ?? true;
121
+ const nowMs = options.now === undefined ? Date.now() : toMs(options.now);
122
+ const sorted = events
123
+ .map(normalizeInput)
124
+ .filter((event) => event !== null)
125
+ .sort((a, b) => a.timestamp - b.timestamp);
126
+ const blocks = [];
127
+ let current = [];
128
+ let currentStart = 0;
129
+ for (const event of sorted) {
130
+ const previous = current[current.length - 1];
131
+ const opensNewBlock = !previous ||
132
+ event.timestamp >= currentStart + durationMs ||
133
+ event.timestamp - previous.timestamp > durationMs;
134
+ if (opensNewBlock) {
135
+ if (current.length > 0)
136
+ blocks.push(buildBlock(current, currentStart, durationMs, nowMs));
137
+ current = [];
138
+ currentStart = alignToHour ? floorToUtcHour(event.timestamp) : event.timestamp;
139
+ }
140
+ current.push(event);
141
+ }
142
+ if (current.length > 0)
143
+ blocks.push(buildBlock(current, currentStart, durationMs, nowMs));
144
+ return blocks;
145
+ }
146
+ /** The block that is still open at the calculator's `now`, if any. */
147
+ function findActiveBillingBlock(blocks) {
148
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
149
+ if (blocks[index].isActive)
150
+ return blocks[index];
151
+ }
152
+ return null;
153
+ }
@@ -0,0 +1,53 @@
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 type { SessionProviderBase, SessionProviderDiagnostic } from '../providers/types';
18
+ import type { SessionSummary } from '../types/historicalData';
19
+ /** Files modified more recently than this are skipped: they may still be live. */
20
+ export declare const ACTIVE_SESSION_MTIME_THRESHOLD_MS = 60000;
21
+ export interface ImportSessionHistoryOptions {
22
+ providers: SessionProviderBase[];
23
+ /** Only sessions modified after this instant. */
24
+ since?: Date | string;
25
+ /** Restrict to one workspace's sessions. */
26
+ workspacePath?: string;
27
+ /** Clock used for the active-file check (default `new Date()`). */
28
+ now?: Date;
29
+ /** Override of {@link ACTIVE_SESSION_MTIME_THRESHOLD_MS}. */
30
+ activeFileThresholdMs?: number;
31
+ /** Already imported? Checked before the session is read. */
32
+ isImported: (sessionId: string, filePath: string) => boolean;
33
+ /** Apply one credited session to the store. */
34
+ applySummary: (summary: SessionSummary, filePath: string) => void;
35
+ /** Record a file as imported (called after `applySummary`, and alone for usage-free files). */
36
+ markImported: (filePath: string) => void;
37
+ onProgress?: (loaded: number, total: number) => void;
38
+ }
39
+ export interface ImportSessionHistoryResult {
40
+ /** Session files the providers listed in the window. */
41
+ filesFound: number;
42
+ /** Files read and applied. */
43
+ filesProcessed: number;
44
+ /** Files skipped because they were already imported or still live. */
45
+ filesSkipped: number;
46
+ /** Files that could not be read (`availability: 'unavailable'`) or held no usage. */
47
+ filesUnavailable: number;
48
+ sessionsImported: number;
49
+ /** Messages across the imported sessions. */
50
+ messagesImported: number;
51
+ diagnostics: SessionProviderDiagnostic[];
52
+ }
53
+ export declare function importSessionHistory(options: ImportSessionHistoryOptions): Promise<ImportSessionHistoryResult>;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * Import finished sessions from every provider into the history store.
4
+ *
5
+ * The VS Code extension used to import Claude Code only, by hand-parsing
6
+ * JSONL files; the CLI could not import at all. This walks the sessions
7
+ * every provider knows about, reads each once through the unified stats
8
+ * path, and hands a `SessionSummary` to the caller, which applies it to the
9
+ * store it owns (the extension's debounced persistence service or the CLI's
10
+ * locked atomic writer). Dedupe is the caller's `isImported` answer
11
+ * (imported file paths plus persisted session ids); the capped per-session
12
+ * list is deliberately not used as the dedupe key.
13
+ *
14
+ * Node-only: reads session files.
15
+ *
16
+ * @module usage/importSessionHistory
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.ACTIVE_SESSION_MTIME_THRESHOLD_MS = void 0;
20
+ exports.importSessionHistory = importSessionHistory;
21
+ const historicalStore_1 = require("../historicalStore");
22
+ const sessionPreviews_1 = require("../sessionPreviews");
23
+ const sessionStats_1 = require("../sessionStats");
24
+ /** Files modified more recently than this are skipped: they may still be live. */
25
+ exports.ACTIVE_SESSION_MTIME_THRESHOLD_MS = 60_000;
26
+ function yieldToEventLoop() {
27
+ return new Promise((resolve) => setImmediate(resolve));
28
+ }
29
+ async function importSessionHistory(options) {
30
+ const now = options.now ?? new Date();
31
+ const threshold = options.activeFileThresholdMs ?? exports.ACTIVE_SESSION_MTIME_THRESHOLD_MS;
32
+ const listed = await (0, sessionPreviews_1.listSessionPreviewsAsync)(options.providers, {
33
+ since: options.since,
34
+ workspacePath: options.workspacePath,
35
+ limit: Number.MAX_SAFE_INTEGER,
36
+ });
37
+ const providerById = new Map(options.providers.map((provider) => [provider.id, provider]));
38
+ const result = {
39
+ filesFound: listed.previews.length,
40
+ filesProcessed: 0,
41
+ filesSkipped: 0,
42
+ filesUnavailable: 0,
43
+ sessionsImported: 0,
44
+ messagesImported: 0,
45
+ diagnostics: [...listed.diagnostics],
46
+ };
47
+ const total = listed.previews.length;
48
+ let loaded = 0;
49
+ for (const preview of listed.previews) {
50
+ loaded += 1;
51
+ const provider = providerById.get(preview.provider);
52
+ const modifiedMs = Date.parse(preview.modifiedAt);
53
+ const recentlyModified = Number.isFinite(modifiedMs) && now.getTime() - modifiedMs < threshold;
54
+ if (!provider || recentlyModified || options.isImported(preview.sessionId, preview.filePath)) {
55
+ result.filesSkipped += 1;
56
+ options.onProgress?.(loaded, total);
57
+ continue;
58
+ }
59
+ const stats = (0, sessionStats_1.readSessionFileStats)(provider, preview.filePath);
60
+ result.filesProcessed += 1;
61
+ if (stats.availability === 'unavailable') {
62
+ result.filesUnavailable += 1;
63
+ result.diagnostics.push({
64
+ providerId: preview.provider,
65
+ kind: 'read_failed',
66
+ severity: 'warning',
67
+ phase: 'read',
68
+ message: `${preview.provider} session ${preview.sessionId}: ${stats.unavailableReason ?? 'unavailable'}`,
69
+ });
70
+ }
71
+ else if (Object.keys(stats.modelUsage).length === 0) {
72
+ // No usage record at all (for example a prompt that never got an
73
+ // answer): nothing to credit, but mark the file so it is not re-read
74
+ // on every run.
75
+ result.filesUnavailable += 1;
76
+ options.markImported(preview.filePath);
77
+ }
78
+ else {
79
+ options.applySummary((0, historicalStore_1.sessionSummaryFromStats)(stats, { project: preview.workspacePath }), preview.filePath);
80
+ options.markImported(preview.filePath);
81
+ result.sessionsImported += 1;
82
+ result.messagesImported += stats.messageCount;
83
+ }
84
+ options.onProgress?.(loaded, total);
85
+ await yieldToEventLoop();
86
+ }
87
+ return result;
88
+ }
@@ -0,0 +1,92 @@
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
+ * Node-only: reads session files and the cache directory.
14
+ *
15
+ * @module usage/usageEvents
16
+ */
17
+ import type { ProviderId, SessionProviderBase, SessionProviderDiagnostic } from '../providers/types';
18
+ import type { TokenTotals } from '../types/historicalData';
19
+ import type { PricingProvenance } from '../usageNormalization';
20
+ /** Bump when the cached record shape changes; older files are re-read. */
21
+ export declare const USAGE_CACHE_VERSION = 1;
22
+ /**
23
+ * Most-recently-used cache files kept on disk. Sized for a busy month of
24
+ * sessions (files are a few KB each); a collection larger than this still
25
+ * completes, it just re-reads what was evicted. Pruning runs once after a
26
+ * collection, never during one, so a working set is never evicted mid-run.
27
+ */
28
+ export declare const MAX_USAGE_CACHE_FILES = 5000;
29
+ /** One priced usage event from a session log. */
30
+ export interface UsageEventRecord {
31
+ /** Event time in ms since epoch. */
32
+ timestamp: number;
33
+ provider: ProviderId;
34
+ sessionId: string;
35
+ /** Workspace path recorded in the session, when known. */
36
+ project: string | null;
37
+ model: string;
38
+ /** Four buckets plus the provider-semantics-aware total. */
39
+ tokens: TokenTotals & {
40
+ totalTokens: number;
41
+ };
42
+ costUsd: number | null;
43
+ costProvenance: PricingProvenance;
44
+ }
45
+ /** One session that contributed events. */
46
+ export interface UsageSessionRecord {
47
+ provider: ProviderId;
48
+ sessionId: string;
49
+ filePath: string;
50
+ project: string | null;
51
+ fingerprint: string;
52
+ firstTimestamp: number | null;
53
+ lastTimestamp: number | null;
54
+ eventCount: number;
55
+ fromCache: boolean;
56
+ }
57
+ export interface CollectUsageEventsOptions {
58
+ providers: SessionProviderBase[];
59
+ /** Only sessions modified after this instant, and only events at or after it. */
60
+ since?: Date | string;
61
+ /** Only events at or before this instant. */
62
+ until?: Date | string;
63
+ /** Restrict to one workspace's sessions. */
64
+ workspacePath?: string;
65
+ /** Preview-read concurrency (see `listSessionPreviewsAsync`). */
66
+ concurrency?: number;
67
+ /** Cache directory (default `<configDir>/usage-cache`). */
68
+ cacheDir?: string;
69
+ /** Skip the on-disk cache entirely. */
70
+ noCache?: boolean;
71
+ onDiagnostic?: (diagnostic: SessionProviderDiagnostic) => void;
72
+ }
73
+ export interface CollectUsageEventsResult {
74
+ /** Events within the window, oldest first. */
75
+ events: UsageEventRecord[];
76
+ sessions: UsageSessionRecord[];
77
+ diagnostics: SessionProviderDiagnostic[];
78
+ cacheHits: number;
79
+ cacheMisses: number;
80
+ }
81
+ export declare function getUsageCacheDir(): string;
82
+ /** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
83
+ export declare function pruneUsageCache(keep?: number, dir?: string): number;
84
+ /**
85
+ * Collect priced usage events for every session touched in the window.
86
+ *
87
+ * Sessions are enumerated with `listSessionPreviewsAsync` (the `since`
88
+ * cutoff applies to the session mtime there and to the event timestamp
89
+ * here), read once each unless the cache already holds their fingerprint,
90
+ * and yielded between reads so long-lived hosts stay responsive.
91
+ */
92
+ export declare function collectUsageEvents(options: CollectUsageEventsOptions): Promise<CollectUsageEventsResult>;