oc2token 0.1.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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +399 -0
  3. package/dist/src/accounting/index.d.ts +1 -0
  4. package/dist/src/accounting/index.js +2 -0
  5. package/dist/src/accounting/index.js.map +1 -0
  6. package/dist/src/accounting/reducer.d.ts +21 -0
  7. package/dist/src/accounting/reducer.js +105 -0
  8. package/dist/src/accounting/reducer.js.map +1 -0
  9. package/dist/src/antigravity/discovery.d.ts +18 -0
  10. package/dist/src/antigravity/discovery.js +95 -0
  11. package/dist/src/antigravity/discovery.js.map +1 -0
  12. package/dist/src/antigravity/doctor.d.ts +6 -0
  13. package/dist/src/antigravity/doctor.js +78 -0
  14. package/dist/src/antigravity/doctor.js.map +1 -0
  15. package/dist/src/antigravity/index.d.ts +3 -0
  16. package/dist/src/antigravity/index.js +4 -0
  17. package/dist/src/antigravity/index.js.map +1 -0
  18. package/dist/src/antigravity/proto.d.ts +73 -0
  19. package/dist/src/antigravity/proto.js +259 -0
  20. package/dist/src/antigravity/proto.js.map +1 -0
  21. package/dist/src/antigravity/scanner.d.ts +8 -0
  22. package/dist/src/antigravity/scanner.js +552 -0
  23. package/dist/src/antigravity/scanner.js.map +1 -0
  24. package/dist/src/application.d.ts +73 -0
  25. package/dist/src/application.js +657 -0
  26. package/dist/src/application.js.map +1 -0
  27. package/dist/src/cache/filesystem.d.ts +8 -0
  28. package/dist/src/cache/filesystem.js +70 -0
  29. package/dist/src/cache/filesystem.js.map +1 -0
  30. package/dist/src/cache/index.d.ts +5 -0
  31. package/dist/src/cache/index.js +6 -0
  32. package/dist/src/cache/index.js.map +1 -0
  33. package/dist/src/cache/lock.d.ts +40 -0
  34. package/dist/src/cache/lock.js +227 -0
  35. package/dist/src/cache/lock.js.map +1 -0
  36. package/dist/src/cache/schema.d.ts +45 -0
  37. package/dist/src/cache/schema.js +308 -0
  38. package/dist/src/cache/schema.js.map +1 -0
  39. package/dist/src/cache/store.d.ts +46 -0
  40. package/dist/src/cache/store.js +396 -0
  41. package/dist/src/cache/store.js.map +1 -0
  42. package/dist/src/cache/types.d.ts +130 -0
  43. package/dist/src/cache/types.js +6 -0
  44. package/dist/src/cache/types.js.map +1 -0
  45. package/dist/src/cli.d.ts +8 -0
  46. package/dist/src/cli.js +874 -0
  47. package/dist/src/cli.js.map +1 -0
  48. package/dist/src/codex/discovery.d.ts +6 -0
  49. package/dist/src/codex/discovery.js +88 -0
  50. package/dist/src/codex/discovery.js.map +1 -0
  51. package/dist/src/codex/doctor.d.ts +6 -0
  52. package/dist/src/codex/doctor.js +52 -0
  53. package/dist/src/codex/doctor.js.map +1 -0
  54. package/dist/src/codex/index.d.ts +3 -0
  55. package/dist/src/codex/index.js +3 -0
  56. package/dist/src/codex/index.js.map +1 -0
  57. package/dist/src/codex/scanner.d.ts +6 -0
  58. package/dist/src/codex/scanner.js +487 -0
  59. package/dist/src/codex/scanner.js.map +1 -0
  60. package/dist/src/codex/types.d.ts +31 -0
  61. package/dist/src/codex/types.js +5 -0
  62. package/dist/src/codex/types.js.map +1 -0
  63. package/dist/src/collector/collector.d.ts +15 -0
  64. package/dist/src/collector/collector.js +597 -0
  65. package/dist/src/collector/collector.js.map +1 -0
  66. package/dist/src/collector/index.d.ts +3 -0
  67. package/dist/src/collector/index.js +3 -0
  68. package/dist/src/collector/index.js.map +1 -0
  69. package/dist/src/collector/types.d.ts +55 -0
  70. package/dist/src/collector/types.js +18 -0
  71. package/dist/src/collector/types.js.map +1 -0
  72. package/dist/src/dashboard/render/ansi.d.ts +53 -0
  73. package/dist/src/dashboard/render/ansi.js +106 -0
  74. package/dist/src/dashboard/render/ansi.js.map +1 -0
  75. package/dist/src/dashboard/render/dashboard.d.ts +13 -0
  76. package/dist/src/dashboard/render/dashboard.js +750 -0
  77. package/dist/src/dashboard/render/dashboard.js.map +1 -0
  78. package/dist/src/dashboard/render/format.d.ts +23 -0
  79. package/dist/src/dashboard/render/format.js +169 -0
  80. package/dist/src/dashboard/render/format.js.map +1 -0
  81. package/dist/src/dashboard/render/redraw.d.ts +13 -0
  82. package/dist/src/dashboard/render/redraw.js +24 -0
  83. package/dist/src/dashboard/render/redraw.js.map +1 -0
  84. package/dist/src/dashboard/render/types.d.ts +83 -0
  85. package/dist/src/dashboard/render/types.js +528 -0
  86. package/dist/src/dashboard/render/types.js.map +1 -0
  87. package/dist/src/dashboard/scheduler/index.d.ts +1 -0
  88. package/dist/src/dashboard/scheduler/index.js +2 -0
  89. package/dist/src/dashboard/scheduler/index.js.map +1 -0
  90. package/dist/src/dashboard/scheduler/refresh-scheduler.d.ts +46 -0
  91. package/dist/src/dashboard/scheduler/refresh-scheduler.js +137 -0
  92. package/dist/src/dashboard/scheduler/refresh-scheduler.js.map +1 -0
  93. package/dist/src/dashboard/settings/index.d.ts +25 -0
  94. package/dist/src/dashboard/settings/index.js +162 -0
  95. package/dist/src/dashboard/settings/index.js.map +1 -0
  96. package/dist/src/dashboard/state/index.d.ts +2 -0
  97. package/dist/src/dashboard/state/index.js +3 -0
  98. package/dist/src/dashboard/state/index.js.map +1 -0
  99. package/dist/src/dashboard/state/refresh-coordinator.d.ts +72 -0
  100. package/dist/src/dashboard/state/refresh-coordinator.js +346 -0
  101. package/dist/src/dashboard/state/refresh-coordinator.js.map +1 -0
  102. package/dist/src/dashboard/state/types.d.ts +68 -0
  103. package/dist/src/dashboard/state/types.js +2 -0
  104. package/dist/src/dashboard/state/types.js.map +1 -0
  105. package/dist/src/domain/contracts.d.ts +219 -0
  106. package/dist/src/domain/contracts.js +2 -0
  107. package/dist/src/domain/contracts.js.map +1 -0
  108. package/dist/src/domain/errors.d.ts +25 -0
  109. package/dist/src/domain/errors.js +65 -0
  110. package/dist/src/domain/errors.js.map +1 -0
  111. package/dist/src/domain/index.d.ts +5 -0
  112. package/dist/src/domain/index.js +6 -0
  113. package/dist/src/domain/index.js.map +1 -0
  114. package/dist/src/domain/records.d.ts +59 -0
  115. package/dist/src/domain/records.js +141 -0
  116. package/dist/src/domain/records.js.map +1 -0
  117. package/dist/src/domain/tokens.d.ts +51 -0
  118. package/dist/src/domain/tokens.js +93 -0
  119. package/dist/src/domain/tokens.js.map +1 -0
  120. package/dist/src/domain/windows.d.ts +59 -0
  121. package/dist/src/domain/windows.js +272 -0
  122. package/dist/src/domain/windows.js.map +1 -0
  123. package/dist/src/index.d.ts +9 -0
  124. package/dist/src/index.js +9 -0
  125. package/dist/src/index.js.map +1 -0
  126. package/dist/src/opencode/adapter.d.ts +47 -0
  127. package/dist/src/opencode/adapter.js +118 -0
  128. package/dist/src/opencode/adapter.js.map +1 -0
  129. package/dist/src/opencode/client.d.ts +64 -0
  130. package/dist/src/opencode/client.js +118 -0
  131. package/dist/src/opencode/client.js.map +1 -0
  132. package/dist/src/opencode/doctor.d.ts +28 -0
  133. package/dist/src/opencode/doctor.js +82 -0
  134. package/dist/src/opencode/doctor.js.map +1 -0
  135. package/dist/src/opencode/index.d.ts +5 -0
  136. package/dist/src/opencode/index.js +6 -0
  137. package/dist/src/opencode/index.js.map +1 -0
  138. package/dist/src/opencode/stats.d.ts +8 -0
  139. package/dist/src/opencode/stats.js +174 -0
  140. package/dist/src/opencode/stats.js.map +1 -0
  141. package/dist/src/opencode/transport.d.ts +54 -0
  142. package/dist/src/opencode/transport.js +621 -0
  143. package/dist/src/opencode/transport.js.map +1 -0
  144. package/dist/src/output/index.d.ts +20 -0
  145. package/dist/src/output/index.js +46 -0
  146. package/dist/src/output/index.js.map +1 -0
  147. package/dist/src/output/json.d.ts +72 -0
  148. package/dist/src/output/json.js +133 -0
  149. package/dist/src/output/json.js.map +1 -0
  150. package/dist/src/output/table.d.ts +4 -0
  151. package/dist/src/output/table.js +81 -0
  152. package/dist/src/output/table.js.map +1 -0
  153. package/dist/src/pricing/index.d.ts +1 -0
  154. package/dist/src/pricing/index.js +2 -0
  155. package/dist/src/pricing/index.js.map +1 -0
  156. package/dist/src/pricing/pricing.d.ts +19 -0
  157. package/dist/src/pricing/pricing.js +441 -0
  158. package/dist/src/pricing/pricing.js.map +1 -0
  159. package/package.json +27 -0
@@ -0,0 +1,657 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { DomainError, cancellationError, containsInstant, createUsageTrendBuckets, createUsageWindows, isCancellationError, sumUsageRecords, toCollectionError, toUsageTotals, } from "./domain/index.js";
4
+ import { collectUsageParallel } from "./collector/index.js";
5
+ import { UsageRecordReducer } from "./accounting/reducer.js";
6
+ import { collectCodex } from "./codex/index.js";
7
+ import { collectAntigravity } from "./antigravity/index.js";
8
+ import { createNormalizedCacheStore, } from "./cache/index.js";
9
+ import { createOpenCodeAdapter, isStatsRangeMismatch, } from "./opencode/index.js";
10
+ /** The default is intentionally explicit so a second CLI instance is safe. */
11
+ export function defaultCacheDirectory() {
12
+ const root = process.env.XDG_CACHE_HOME ?? join(homedir(), "Library", "Caches");
13
+ return join(root, "oc2token");
14
+ }
15
+ function appendError(coverage, error) {
16
+ return {
17
+ ...coverage,
18
+ complete: false,
19
+ errors: [...coverage.errors, error],
20
+ };
21
+ }
22
+ function errorFor(error) {
23
+ if (error instanceof DomainError) {
24
+ return {
25
+ code: error.code === "cache-busy" ? "cache-busy" : "unknown",
26
+ message: error.message,
27
+ ...(error.sessionID === undefined ? {} : { sessionID: error.sessionID }),
28
+ retryable: error.retryable,
29
+ };
30
+ }
31
+ return {
32
+ code: "unknown",
33
+ message: error instanceof Error ? error.message : String(error),
34
+ retryable: false,
35
+ };
36
+ }
37
+ function addUsageTotals(left, right) {
38
+ return toUsageTotals({
39
+ input: left.input + right.input,
40
+ output: left.output + right.output,
41
+ reasoning: left.reasoning + right.reasoning,
42
+ cacheRead: left.cacheRead + right.cacheRead,
43
+ cacheWrite: left.cacheWrite + right.cacheWrite,
44
+ });
45
+ }
46
+ function emptyUsageTotals() {
47
+ return toUsageTotals({ input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 });
48
+ }
49
+ function mergeBreakdowns(values) {
50
+ const merged = new Map();
51
+ for (const value of values) {
52
+ const key = `${value.name}\0${value.provider ?? ""}`;
53
+ const current = merged.get(key);
54
+ merged.set(key, current === undefined
55
+ ? value
56
+ : { ...current, totals: addUsageTotals(current.totals, value.totals) });
57
+ }
58
+ return [...merged.values()].sort((left, right) => right.totals.recorded_total - left.totals.recorded_total || left.name.localeCompare(right.name));
59
+ }
60
+ function mergeTrends(successful, request) {
61
+ if (successful.length === 0)
62
+ return undefined;
63
+ const merged = {};
64
+ for (const window of request.windows) {
65
+ const planned = createUsageTrendBuckets(window);
66
+ const buckets = planned.map((bucket, index) => {
67
+ let totals = emptyUsageTotals();
68
+ for (const { result } of successful) {
69
+ const supplied = result.trendsByWindow?.[window.kind]?.[index];
70
+ const exact = supplied !== undefined &&
71
+ supplied.from.getTime() === bucket.from.getTime() &&
72
+ supplied.to.getTime() === bucket.to.getTime();
73
+ const sourceTotals = exact
74
+ ? supplied.totals
75
+ : sumUsageRecords(result.records, {
76
+ ...window,
77
+ from: new Date(bucket.from.getTime()),
78
+ to: new Date(bucket.to.getTime()),
79
+ label: bucket.label,
80
+ });
81
+ totals = addUsageTotals(totals, sourceTotals);
82
+ }
83
+ return {
84
+ label: bucket.label,
85
+ from: new Date(bucket.from.getTime()),
86
+ to: new Date(bucket.to.getTime()),
87
+ totals,
88
+ };
89
+ });
90
+ merged[window.kind] = buckets;
91
+ }
92
+ return merged;
93
+ }
94
+ /**
95
+ * Selects exactly one accounting source for a refresh. Stats is used only if
96
+ * every requested range is honored; a broader/ignored range switches the
97
+ * whole refresh to the message scan and never combines the two totals.
98
+ */
99
+ export class HybridUsageSource {
100
+ adapter;
101
+ collectorOptions;
102
+ statsUnsupportedFingerprint;
103
+ constructor(adapter, collectorOptions = {}) {
104
+ this.adapter = adapter;
105
+ this.collectorOptions = collectorOptions;
106
+ }
107
+ async connectIfNeeded(signal) {
108
+ if (signal?.aborted)
109
+ throw new DomainError("cancelled", "The refresh was cancelled");
110
+ if (this.adapter.transport === undefined) {
111
+ await this.adapter.connect(signal);
112
+ }
113
+ }
114
+ async fallback(request) {
115
+ const transport = this.adapter.transport;
116
+ if (transport === undefined) {
117
+ throw new DomainError("transport", "OpenCode 2 transport is not connected");
118
+ }
119
+ const result = await collectUsageParallel(transport, request, this.collectorOptions);
120
+ const connection = this.adapter.currentConnection;
121
+ return {
122
+ ...result,
123
+ ...(connection === undefined
124
+ ? {}
125
+ : {
126
+ serverFingerprint: connection.health.fingerprint,
127
+ serverVersion: connection.health.version,
128
+ }),
129
+ };
130
+ }
131
+ async collect(request) {
132
+ await this.connectIfNeeded(request.signal);
133
+ const connection = this.adapter.currentConnection;
134
+ const fingerprint = connection?.health.fingerprint;
135
+ if (this.statsUnsupportedFingerprint !== undefined && this.statsUnsupportedFingerprint === fingerprint) {
136
+ return this.fallback(request);
137
+ }
138
+ let reconnected = false;
139
+ for (;;) {
140
+ try {
141
+ const result = await this.adapter.collect(request);
142
+ const currentConnection = this.adapter.currentConnection;
143
+ return {
144
+ ...result,
145
+ ...(currentConnection === undefined
146
+ ? {}
147
+ : {
148
+ serverFingerprint: currentConnection.health.fingerprint,
149
+ serverVersion: currentConnection.health.version,
150
+ }),
151
+ };
152
+ }
153
+ catch (error) {
154
+ if (!reconnected &&
155
+ this.adapter.options?.transport === undefined &&
156
+ error instanceof DomainError &&
157
+ error.code === "transport") {
158
+ // A service restart invalidates the generated client connection. A
159
+ // single reconnect keeps this refresh bounded; the coordinator's
160
+ // next generation remains the retry path after a second failure.
161
+ reconnected = true;
162
+ await this.adapter.connect(request.signal);
163
+ this.statsUnsupportedFingerprint = undefined;
164
+ continue;
165
+ }
166
+ if (!isStatsRangeMismatch(error))
167
+ throw error;
168
+ // Cache this decision for the current server fingerprint. A version or
169
+ // PID change causes the next refresh to probe the new implementation.
170
+ this.statsUnsupportedFingerprint = fingerprint ?? "unknown";
171
+ return this.fallback(request);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ export class CodexFileSource {
177
+ directory;
178
+ constructor(directory) {
179
+ this.directory = directory;
180
+ }
181
+ async collect(request) {
182
+ if (request.signal?.aborted)
183
+ throw cancellationError();
184
+ if (this.directory !== undefined) {
185
+ const previous = process.env.CODEX_HOME;
186
+ process.env.CODEX_HOME = this.directory;
187
+ try {
188
+ return await collectCodex(request);
189
+ }
190
+ finally {
191
+ if (previous === undefined)
192
+ delete process.env.CODEX_HOME;
193
+ else
194
+ process.env.CODEX_HOME = previous;
195
+ }
196
+ }
197
+ return collectCodex(request);
198
+ }
199
+ }
200
+ export class AntigravityFileSource {
201
+ directory;
202
+ constructor(directory) {
203
+ this.directory = directory;
204
+ }
205
+ async collect(request) {
206
+ if (request.signal?.aborted)
207
+ throw cancellationError();
208
+ if (this.directory !== undefined) {
209
+ const previous = process.env.ANTIGRAVITY_HOME;
210
+ process.env.ANTIGRAVITY_HOME = this.directory;
211
+ try {
212
+ return await collectAntigravity(request);
213
+ }
214
+ finally {
215
+ if (previous === undefined)
216
+ delete process.env.ANTIGRAVITY_HOME;
217
+ else
218
+ process.env.ANTIGRAVITY_HOME = previous;
219
+ }
220
+ }
221
+ return collectAntigravity(request);
222
+ }
223
+ }
224
+ export class UnifiedUsageSource {
225
+ opencode;
226
+ codex;
227
+ antigravity;
228
+ filterProviders;
229
+ constructor(opencode, codex, antigravity, options = {}) {
230
+ this.opencode = opencode;
231
+ this.codex = codex;
232
+ this.antigravity = antigravity;
233
+ this.filterProviders = options.filterProviders === undefined || options.filterProviders.size === 0
234
+ ? undefined
235
+ : new Set(options.filterProviders);
236
+ }
237
+ getFilterProviders() {
238
+ return this.filterProviders === undefined ? undefined : new Set(this.filterProviders);
239
+ }
240
+ setFilterProviders(filter) {
241
+ if (filter === undefined || filter.size === 0) {
242
+ this.filterProviders = undefined;
243
+ }
244
+ else {
245
+ this.filterProviders = new Set(filter);
246
+ }
247
+ }
248
+ async collect(request) {
249
+ if (request.signal?.aborted) {
250
+ throw cancellationError(request.signal.reason instanceof Error ? request.signal.reason.message : undefined);
251
+ }
252
+ if (request.windows.length === 0) {
253
+ throw new DomainError("invalid-window", "Collection request must contain at least one usage window");
254
+ }
255
+ const filter = this.filterProviders;
256
+ const shouldCollect = (provider) => filter === undefined || filter.size === 0 || filter.has(provider);
257
+ const tasks = [];
258
+ if (shouldCollect("opencode"))
259
+ tasks.push({ provider: "opencode", source: this.opencode });
260
+ if (shouldCollect("codex"))
261
+ tasks.push({ provider: "codex", source: this.codex });
262
+ if (shouldCollect("antigravity"))
263
+ tasks.push({ provider: "antigravity", source: this.antigravity });
264
+ if (tasks.length === 0) {
265
+ const emptyRecords = [];
266
+ const totalsByWindow = Object.fromEntries(request.windows.map((w) => [w.kind, sumUsageRecords(emptyRecords, w)]));
267
+ return {
268
+ capturedAt: new Date(request.capturedAt.getTime()),
269
+ windows: request.windows.map((w) => ({
270
+ ...w,
271
+ from: new Date(w.from.getTime()),
272
+ to: new Date(w.to.getTime()),
273
+ })),
274
+ source: "unified",
275
+ records: emptyRecords,
276
+ totalsByWindow,
277
+ coverage: {
278
+ complete: true,
279
+ sessionsDiscovered: 0,
280
+ sessionsScanned: 0,
281
+ sessionsSkipped: 0,
282
+ pagesRead: 0,
283
+ jobsRetried: 0,
284
+ provisionalMessages: 0,
285
+ errors: [],
286
+ },
287
+ };
288
+ }
289
+ const promises = tasks.map((task) => task.source.collect(request));
290
+ const results = await Promise.allSettled(promises);
291
+ for (const result of results) {
292
+ if (result.status === "rejected" && isCancellationError(result.reason)) {
293
+ throw result.reason instanceof DomainError
294
+ ? result.reason
295
+ : cancellationError(result.reason instanceof Error ? result.reason.message : undefined);
296
+ }
297
+ }
298
+ if (request.signal?.aborted) {
299
+ throw cancellationError(request.signal.reason instanceof Error ? request.signal.reason.message : undefined);
300
+ }
301
+ const reducer = new UsageRecordReducer();
302
+ let sessionsDiscovered = 0;
303
+ let sessionsScanned = 0;
304
+ let sessionsSkipped = 0;
305
+ let pagesRead = 0;
306
+ let jobsRetried = 0;
307
+ let provisionalMessages = 0;
308
+ let overallComplete = true;
309
+ const prefixedErrors = [];
310
+ const successful = [];
311
+ for (let i = 0; i < tasks.length; i++) {
312
+ const task = tasks[i];
313
+ const settled = results[i];
314
+ if (settled.status === "fulfilled") {
315
+ const result = settled.value;
316
+ successful.push({ provider: task.provider, result });
317
+ const cov = result.coverage;
318
+ sessionsDiscovered += cov.sessionsDiscovered;
319
+ sessionsScanned += cov.sessionsScanned;
320
+ sessionsSkipped += cov.sessionsSkipped;
321
+ pagesRead += cov.pagesRead;
322
+ jobsRetried += cov.jobsRetried;
323
+ provisionalMessages += cov.provisionalMessages;
324
+ if (!cov.complete)
325
+ overallComplete = false;
326
+ for (const err of cov.errors) {
327
+ prefixedErrors.push({
328
+ ...err,
329
+ message: `[${task.provider}] ${err.message}`,
330
+ });
331
+ }
332
+ reducer.upsertMany(result.records);
333
+ }
334
+ else {
335
+ overallComplete = false;
336
+ const raw = settled.reason;
337
+ const base = toCollectionError(raw, "unknown");
338
+ prefixedErrors.push({
339
+ ...base,
340
+ message: `[${task.provider}] ${base.message}`,
341
+ });
342
+ }
343
+ }
344
+ const sortedErrors = [...prefixedErrors].sort((a, b) => (String(a.sessionID ?? "") + "\0" + a.code + "\0" + a.message).localeCompare(String(b.sessionID ?? "") + "\0" + b.code + "\0" + b.message));
345
+ const coverage = {
346
+ complete: overallComplete && sortedErrors.length === 0,
347
+ sessionsDiscovered,
348
+ sessionsScanned,
349
+ sessionsSkipped,
350
+ pagesRead,
351
+ jobsRetried,
352
+ provisionalMessages,
353
+ errors: sortedErrors,
354
+ };
355
+ let records = reducer.records();
356
+ records = [...records].sort((a, b) => {
357
+ const providerCompare = a.provider.localeCompare(b.provider);
358
+ if (providerCompare !== 0)
359
+ return providerCompare;
360
+ return a.key.localeCompare(b.key);
361
+ });
362
+ const totalsByWindow = Object.fromEntries(request.windows.map((w) => {
363
+ const totals = successful.reduce((current, { result }) => {
364
+ const sourceTotals = result.totalsByWindow[w.kind] ?? sumUsageRecords(result.records, w);
365
+ return addUsageTotals(current, sourceTotals);
366
+ }, emptyUsageTotals());
367
+ return [w.kind, totals];
368
+ }));
369
+ const providersMutable = {};
370
+ const modelsMutable = {};
371
+ const projectsMutable = {};
372
+ for (const w of request.windows) {
373
+ const windowRecords = records.filter((r) => containsInstant(w, r.createdAt));
374
+ if (windowRecords.length > 0) {
375
+ const byProvider = new Map();
376
+ const byModel = new Map();
377
+ const byProject = new Map();
378
+ for (const r of windowRecords) {
379
+ const prov = r.provider;
380
+ const arrP = byProvider.get(prov);
381
+ if (arrP === undefined)
382
+ byProvider.set(prov, [r]);
383
+ else
384
+ arrP.push(r);
385
+ const modelName = r.model;
386
+ const arrM = byModel.get(modelName);
387
+ if (arrM === undefined)
388
+ byModel.set(modelName, [r]);
389
+ else
390
+ arrM.push(r);
391
+ if (r.project !== undefined && r.project.trim().length > 0) {
392
+ const proj = r.project;
393
+ const arrJ = byProject.get(proj);
394
+ if (arrJ === undefined)
395
+ byProject.set(proj, [r]);
396
+ else
397
+ arrJ.push(r);
398
+ }
399
+ }
400
+ if (byProvider.size > 0) {
401
+ const breakdowns = [];
402
+ for (const [provName, recs] of byProvider.entries()) {
403
+ const totals = sumUsageRecords(recs, w);
404
+ breakdowns.push({ name: provName, provider: provName, totals });
405
+ }
406
+ breakdowns.sort((a, b) => b.totals.recorded_total - a.totals.recorded_total || a.name.localeCompare(b.name));
407
+ providersMutable[w.kind] = breakdowns;
408
+ }
409
+ if (byModel.size > 0) {
410
+ const breakdowns = [];
411
+ for (const [modelName, recs] of byModel.entries()) {
412
+ const totals = sumUsageRecords(recs, w);
413
+ breakdowns.push({ name: modelName, totals });
414
+ }
415
+ breakdowns.sort((a, b) => b.totals.recorded_total - a.totals.recorded_total || a.name.localeCompare(b.name));
416
+ modelsMutable[w.kind] = breakdowns;
417
+ }
418
+ if (byProject.size > 0) {
419
+ const breakdowns = [];
420
+ for (const [projName, recs] of byProject.entries()) {
421
+ const totals = sumUsageRecords(recs, w);
422
+ breakdowns.push({ name: projName, totals });
423
+ }
424
+ breakdowns.sort((a, b) => b.totals.recorded_total - a.totals.recorded_total || a.name.localeCompare(b.name));
425
+ projectsMutable[w.kind] = breakdowns;
426
+ }
427
+ }
428
+ const suppliedProviders = successful.flatMap(({ result }) => result.providersByWindow?.[w.kind] ?? []);
429
+ if (suppliedProviders.length > 0) {
430
+ providersMutable[w.kind] = mergeBreakdowns([
431
+ ...(providersMutable[w.kind] ?? []),
432
+ ...suppliedProviders,
433
+ ]);
434
+ }
435
+ const suppliedModels = successful.flatMap(({ result }) => result.modelsByWindow?.[w.kind] ?? []);
436
+ if (suppliedModels.length > 0) {
437
+ modelsMutable[w.kind] = mergeBreakdowns([
438
+ ...(modelsMutable[w.kind] ?? []),
439
+ ...suppliedModels,
440
+ ]);
441
+ }
442
+ const suppliedProjects = successful.flatMap(({ result }) => result.projectsByWindow?.[w.kind] ?? []);
443
+ if (suppliedProjects.length > 0) {
444
+ projectsMutable[w.kind] = mergeBreakdowns([
445
+ ...(projectsMutable[w.kind] ?? []),
446
+ ...suppliedProjects,
447
+ ]);
448
+ }
449
+ }
450
+ const providersByWindow = providersMutable;
451
+ const modelsByWindow = modelsMutable;
452
+ const projectsByWindow = projectsMutable;
453
+ const presentProviders = new Set(records.map((r) => r.provider));
454
+ let source;
455
+ if (presentProviders.size > 1) {
456
+ source = "unified";
457
+ }
458
+ else if (presentProviders.size === 1) {
459
+ const sole = [...presentProviders][0];
460
+ if (sole === "codex")
461
+ source = "codex";
462
+ else if (sole === "antigravity")
463
+ source = "antigravity";
464
+ else {
465
+ const opencodeResult = successful.find((s) => s.provider === "opencode");
466
+ source = opencodeResult?.result.source ?? "message-scan";
467
+ }
468
+ }
469
+ else {
470
+ if (tasks.length > 1) {
471
+ const distinctSources = new Set(successful.map((s) => s.result.source));
472
+ if (distinctSources.size === 1) {
473
+ const soleSource = [...distinctSources][0];
474
+ source = tasks.length > 1 ? "unified" : soleSource;
475
+ }
476
+ else {
477
+ source = "unified";
478
+ }
479
+ }
480
+ else if (tasks.length === 1) {
481
+ const soleTask = tasks[0];
482
+ if (soleTask.provider === "codex")
483
+ source = "codex";
484
+ else if (soleTask.provider === "antigravity")
485
+ source = "antigravity";
486
+ else {
487
+ const opencodeResult = successful.find((s) => s.provider === "opencode");
488
+ source = opencodeResult?.result.source ?? "message-scan";
489
+ }
490
+ }
491
+ else {
492
+ source = "unified";
493
+ }
494
+ }
495
+ const opencodeSuccess = successful.find((s) => s.provider === "opencode");
496
+ const serverFingerprint = opencodeSuccess?.result.serverFingerprint;
497
+ const serverVersion = opencodeSuccess?.result.serverVersion;
498
+ const trendsByWindow = mergeTrends(successful, request);
499
+ return {
500
+ capturedAt: new Date(request.capturedAt.getTime()),
501
+ windows: request.windows.map((w) => ({
502
+ ...w,
503
+ from: new Date(w.from.getTime()),
504
+ to: new Date(w.to.getTime()),
505
+ })),
506
+ source,
507
+ records,
508
+ totalsByWindow,
509
+ ...(Object.keys(providersMutable).length > 0
510
+ ? { providersByWindow, providers: [...(providersMutable.week ?? providersMutable.day ?? providersMutable.hour ?? Object.values(providersMutable).flat())] }
511
+ : {}),
512
+ ...(Object.keys(modelsMutable).length > 0
513
+ ? { modelsByWindow, models: [...(modelsMutable.week ?? modelsMutable.day ?? modelsMutable.hour ?? Object.values(modelsMutable).flat())] }
514
+ : {}),
515
+ ...(Object.keys(projectsMutable).length > 0
516
+ ? { projectsByWindow, projects: [...(projectsMutable.week ?? projectsMutable.day ?? projectsMutable.hour ?? Object.values(projectsMutable).flat())] }
517
+ : {}),
518
+ ...(trendsByWindow === undefined ? {} : { trendsByWindow }),
519
+ coverage,
520
+ ...(serverFingerprint === undefined ? {} : { serverFingerprint }),
521
+ ...(serverVersion === undefined ? {} : { serverVersion }),
522
+ };
523
+ }
524
+ }
525
+ /** Persist successful normalized metadata without making the cache a source of truth. */
526
+ export class CachedUsageSource {
527
+ source;
528
+ store;
529
+ constructor(source, store) {
530
+ this.source = source;
531
+ this.store = store;
532
+ }
533
+ getFilterProviders() {
534
+ const inner = this.source;
535
+ return inner.getFilterProviders?.();
536
+ }
537
+ setFilterProviders(filter) {
538
+ const inner = this.source;
539
+ inner.setFilterProviders?.(filter);
540
+ }
541
+ get innerSource() {
542
+ return this.source;
543
+ }
544
+ async collect(request) {
545
+ const result = await this.source.collect(request);
546
+ const snapshot = {
547
+ schemaVersion: 1,
548
+ generation: request.generation ?? 0,
549
+ capturedAt: new Date(result.capturedAt.getTime()),
550
+ requestedWindows: result.windows,
551
+ source: result.source,
552
+ records: result.records,
553
+ totalsByWindow: result.totalsByWindow,
554
+ ...(result.models === undefined ? {} : { models: result.models }),
555
+ ...(result.providers === undefined ? {} : { providers: result.providers }),
556
+ ...(result.projects === undefined ? {} : { projects: result.projects }),
557
+ ...(result.modelsByWindow === undefined ? {} : { modelsByWindow: result.modelsByWindow }),
558
+ ...(result.providersByWindow === undefined ? {} : { providersByWindow: result.providersByWindow }),
559
+ ...(result.projectsByWindow === undefined ? {} : { projectsByWindow: result.projectsByWindow }),
560
+ ...(result.trendsByWindow === undefined ? {} : { trendsByWindow: result.trendsByWindow }),
561
+ coverage: result.coverage,
562
+ ...(result.serverFingerprint === undefined ? {} : { serverFingerprint: result.serverFingerprint }),
563
+ ...(result.serverVersion === undefined ? {} : { serverVersion: result.serverVersion }),
564
+ };
565
+ try {
566
+ const committed = await this.store.commitDetailed(snapshot, result.records);
567
+ if (committed.status === "cache_busy") {
568
+ return { ...result, coverage: appendError(result.coverage, {
569
+ code: "cache-busy",
570
+ message: "Another oc2token process owns the cache; using in-memory data",
571
+ retryable: true,
572
+ }) };
573
+ }
574
+ if (committed.status === "cache_unavailable") {
575
+ return { ...result, coverage: appendError(result.coverage, {
576
+ code: "cache-unavailable",
577
+ message: "The normalized cache could not be updated",
578
+ retryable: true,
579
+ }) };
580
+ }
581
+ }
582
+ catch (error) {
583
+ return { ...result, coverage: appendError(result.coverage, errorFor(error)) };
584
+ }
585
+ return result;
586
+ }
587
+ }
588
+ export async function readCachedSnapshot(directory = defaultCacheDirectory()) {
589
+ const store = createNormalizedCacheStore({ directory });
590
+ const cached = await store.readDetailed();
591
+ if (cached.status !== "available")
592
+ return { snapshot: undefined, store };
593
+ const snapshot = cached.snapshot;
594
+ return {
595
+ store,
596
+ snapshot: {
597
+ capturedAt: new Date(snapshot.capturedAt.getTime()),
598
+ windows: snapshot.requestedWindows,
599
+ source: snapshot.source,
600
+ records: snapshot.records,
601
+ totalsByWindow: snapshot.totalsByWindow,
602
+ ...(snapshot.models === undefined ? {} : { models: snapshot.models }),
603
+ ...(snapshot.providers === undefined ? {} : { providers: snapshot.providers }),
604
+ ...(snapshot.projects === undefined ? {} : { projects: snapshot.projects }),
605
+ ...(snapshot.modelsByWindow === undefined ? {} : { modelsByWindow: snapshot.modelsByWindow }),
606
+ ...(snapshot.providersByWindow === undefined ? {} : { providersByWindow: snapshot.providersByWindow }),
607
+ ...(snapshot.projectsByWindow === undefined ? {} : { projectsByWindow: snapshot.projectsByWindow }),
608
+ ...(snapshot.trendsByWindow === undefined ? {} : { trendsByWindow: snapshot.trendsByWindow }),
609
+ coverage: snapshot.coverage,
610
+ ...(snapshot.serverFingerprint === undefined ? {} : { serverFingerprint: snapshot.serverFingerprint }),
611
+ ...(snapshot.serverVersion === undefined ? {} : { serverVersion: snapshot.serverVersion }),
612
+ },
613
+ };
614
+ }
615
+ export function newCollectionRequest(now, timezone, options = {}) {
616
+ const windows = Object.values(createUsageWindows(now, timezone));
617
+ return {
618
+ capturedAt: new Date(now.getTime()),
619
+ windows,
620
+ ...(options.project === undefined ? {} : { project: options.project }),
621
+ includeSubagents: options.includeSubagents ?? true,
622
+ includeProvisional: options.includeProvisional ?? false,
623
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
624
+ };
625
+ }
626
+ export function emptyCollectionResult(now, timezone) {
627
+ const windows = Object.values(createUsageWindows(now, timezone));
628
+ const coverage = {
629
+ complete: false,
630
+ sessionsDiscovered: 0,
631
+ sessionsScanned: 0,
632
+ sessionsSkipped: 0,
633
+ pagesRead: 0,
634
+ jobsRetried: 0,
635
+ provisionalMessages: 0,
636
+ errors: [],
637
+ };
638
+ return {
639
+ capturedAt: new Date(now.getTime()),
640
+ windows,
641
+ source: "message-scan",
642
+ records: [],
643
+ totalsByWindow: {},
644
+ coverage,
645
+ };
646
+ }
647
+ export function createApplicationSource(options = {}) {
648
+ const adapter = createOpenCodeAdapter(options);
649
+ const hybrid = new HybridUsageSource(adapter, options.collectorOptions);
650
+ const codexSource = new CodexFileSource(options.codexDirectory);
651
+ const antigravitySource = new AntigravityFileSource(options.antigravityDirectory);
652
+ const unifiedOptions = options.filterProviders === undefined ? undefined : { filterProviders: options.filterProviders };
653
+ const unified = new UnifiedUsageSource(hybrid, codexSource, antigravitySource, unifiedOptions);
654
+ const store = createNormalizedCacheStore({ directory: options.cacheDirectory ?? defaultCacheDirectory() });
655
+ return { adapter, unified, source: new CachedUsageSource(unified, store), store };
656
+ }
657
+ //# sourceMappingURL=application.js.map