@tokscale/cli 1.0.6 → 1.0.8

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/src/native.ts ADDED
@@ -0,0 +1,807 @@
1
+ /**
2
+ * Native module loader for Rust core
3
+ *
4
+ * Exposes all Rust functions with proper TypeScript types.
5
+ * Falls back to TypeScript implementations when native module is unavailable.
6
+ */
7
+
8
+ import type { PricingEntry } from "./pricing.js";
9
+ import type {
10
+ TokenContributionData,
11
+ GraphOptions as TSGraphOptions,
12
+ SourceType,
13
+ } from "./graph-types.js";
14
+ import {
15
+ parseLocalSources as parseLocalSourcesTS,
16
+ type ParsedMessages as TSParsedMessages,
17
+ type UnifiedMessage,
18
+ } from "./sessions/index.js";
19
+ import {
20
+ generateModelReport as generateModelReportTS,
21
+ generateMonthlyReport as generateMonthlyReportTS,
22
+ generateGraphData as generateGraphDataTS,
23
+ } from "./sessions/reports.js";
24
+ import { readCursorMessagesFromCache } from "./cursor.js";
25
+
26
+ // =============================================================================
27
+ // Types matching Rust exports
28
+ // =============================================================================
29
+
30
+ interface NativeGraphOptions {
31
+ homeDir?: string;
32
+ sources?: string[];
33
+ since?: string;
34
+ until?: string;
35
+ year?: string;
36
+ threads?: number;
37
+ }
38
+
39
+ interface NativeScanStats {
40
+ opencodeFiles: number;
41
+ claudeFiles: number;
42
+ codexFiles: number;
43
+ geminiFiles: number;
44
+ totalFiles: number;
45
+ }
46
+
47
+ interface NativeTokenBreakdown {
48
+ input: number;
49
+ output: number;
50
+ cacheRead: number;
51
+ cacheWrite: number;
52
+ reasoning: number;
53
+ }
54
+
55
+ interface NativeDailyTotals {
56
+ tokens: number;
57
+ cost: number;
58
+ messages: number;
59
+ }
60
+
61
+ interface NativeSourceContribution {
62
+ source: string;
63
+ modelId: string;
64
+ providerId: string;
65
+ tokens: NativeTokenBreakdown;
66
+ cost: number;
67
+ messages: number;
68
+ }
69
+
70
+ interface NativeDailyContribution {
71
+ date: string;
72
+ totals: NativeDailyTotals;
73
+ intensity: number;
74
+ tokenBreakdown: NativeTokenBreakdown;
75
+ sources: NativeSourceContribution[];
76
+ }
77
+
78
+ interface NativeYearSummary {
79
+ year: string;
80
+ totalTokens: number;
81
+ totalCost: number;
82
+ rangeStart: string;
83
+ rangeEnd: string;
84
+ }
85
+
86
+ interface NativeDataSummary {
87
+ totalTokens: number;
88
+ totalCost: number;
89
+ totalDays: number;
90
+ activeDays: number;
91
+ averagePerDay: number;
92
+ maxCostInSingleDay: number;
93
+ sources: string[];
94
+ models: string[];
95
+ }
96
+
97
+ interface NativeGraphMeta {
98
+ generatedAt: string;
99
+ version: string;
100
+ dateRangeStart: string;
101
+ dateRangeEnd: string;
102
+ processingTimeMs: number;
103
+ }
104
+
105
+ interface NativeGraphResult {
106
+ meta: NativeGraphMeta;
107
+ summary: NativeDataSummary;
108
+ years: NativeYearSummary[];
109
+ contributions: NativeDailyContribution[];
110
+ }
111
+
112
+ // Types for pricing-aware APIs
113
+ interface NativePricingEntry {
114
+ modelId: string;
115
+ pricing: {
116
+ inputCostPerToken: number;
117
+ outputCostPerToken: number;
118
+ cacheReadInputTokenCost?: number;
119
+ cacheCreationInputTokenCost?: number;
120
+ };
121
+ }
122
+
123
+ interface NativeReportOptions {
124
+ homeDir?: string;
125
+ sources?: string[];
126
+ pricing: NativePricingEntry[];
127
+ since?: string;
128
+ until?: string;
129
+ year?: string;
130
+ }
131
+
132
+ interface NativeModelUsage {
133
+ source: string;
134
+ model: string;
135
+ provider: string;
136
+ input: number;
137
+ output: number;
138
+ cacheRead: number;
139
+ cacheWrite: number;
140
+ reasoning: number;
141
+ messageCount: number;
142
+ cost: number;
143
+ }
144
+
145
+ interface NativeModelReport {
146
+ entries: NativeModelUsage[];
147
+ totalInput: number;
148
+ totalOutput: number;
149
+ totalCacheRead: number;
150
+ totalCacheWrite: number;
151
+ totalMessages: number;
152
+ totalCost: number;
153
+ processingTimeMs: number;
154
+ }
155
+
156
+ interface NativeMonthlyUsage {
157
+ month: string;
158
+ models: string[];
159
+ input: number;
160
+ output: number;
161
+ cacheRead: number;
162
+ cacheWrite: number;
163
+ messageCount: number;
164
+ cost: number;
165
+ }
166
+
167
+ interface NativeMonthlyReport {
168
+ entries: NativeMonthlyUsage[];
169
+ totalCost: number;
170
+ processingTimeMs: number;
171
+ }
172
+
173
+ // Types for two-phase processing (parallel optimization)
174
+ interface NativeParsedMessage {
175
+ source: string;
176
+ modelId: string;
177
+ providerId: string;
178
+ timestamp: number;
179
+ date: string;
180
+ input: number;
181
+ output: number;
182
+ cacheRead: number;
183
+ cacheWrite: number;
184
+ reasoning: number;
185
+ sessionId: string;
186
+ }
187
+
188
+ interface NativeParsedMessages {
189
+ messages: NativeParsedMessage[];
190
+ opencodeCount: number;
191
+ claudeCount: number;
192
+ codexCount: number;
193
+ geminiCount: number;
194
+ processingTimeMs: number;
195
+ }
196
+
197
+ interface NativeLocalParseOptions {
198
+ homeDir?: string;
199
+ sources?: string[];
200
+ since?: string;
201
+ until?: string;
202
+ year?: string;
203
+ }
204
+
205
+ interface NativeFinalizeReportOptions {
206
+ homeDir?: string;
207
+ localMessages: NativeParsedMessages;
208
+ pricing: NativePricingEntry[];
209
+ includeCursor: boolean;
210
+ since?: string;
211
+ until?: string;
212
+ year?: string;
213
+ }
214
+
215
+ interface NativeCore {
216
+ version(): string;
217
+ healthCheck(): string;
218
+ generateGraph(options: NativeGraphOptions): NativeGraphResult;
219
+ generateGraphWithPricing(options: NativeReportOptions): NativeGraphResult;
220
+ scanSessions(homeDir?: string, sources?: string[]): NativeScanStats;
221
+ getModelReport(options: NativeReportOptions): NativeModelReport;
222
+ getMonthlyReport(options: NativeReportOptions): NativeMonthlyReport;
223
+ // Two-phase processing (parallel optimization)
224
+ parseLocalSources(options: NativeLocalParseOptions): NativeParsedMessages;
225
+ finalizeReport(options: NativeFinalizeReportOptions): NativeModelReport;
226
+ finalizeMonthlyReport(options: NativeFinalizeReportOptions): NativeMonthlyReport;
227
+ finalizeGraph(options: NativeFinalizeReportOptions): NativeGraphResult;
228
+ }
229
+
230
+ // =============================================================================
231
+ // Module loading
232
+ // =============================================================================
233
+
234
+ let nativeCore: NativeCore | null = null;
235
+ let loadError: Error | null = null;
236
+
237
+ try {
238
+ nativeCore = await import("@tokscale/core").then((m) => m.default || m);
239
+ } catch (e) {
240
+ loadError = e as Error;
241
+ }
242
+
243
+ // =============================================================================
244
+ // Public API
245
+ // =============================================================================
246
+
247
+ /**
248
+ * Check if native module is available
249
+ */
250
+ export function isNativeAvailable(): boolean {
251
+ return nativeCore !== null;
252
+ }
253
+
254
+ /**
255
+ * Get native module load error (if any)
256
+ */
257
+ export function getNativeLoadError(): Error | null {
258
+ return loadError;
259
+ }
260
+
261
+ /**
262
+ * Get native module version
263
+ */
264
+ export function getNativeVersion(): string | null {
265
+ return nativeCore?.version() ?? null;
266
+ }
267
+
268
+ /**
269
+ * Scan sessions using native module
270
+ */
271
+ export function scanSessionsNative(homeDir?: string, sources?: string[]): NativeScanStats | null {
272
+ if (!nativeCore) {
273
+ return null;
274
+ }
275
+ return nativeCore.scanSessions(homeDir, sources);
276
+ }
277
+
278
+ // =============================================================================
279
+ // Graph generation
280
+ // =============================================================================
281
+
282
+ /**
283
+ * Convert TypeScript graph options to native format
284
+ */
285
+ function toNativeOptions(options: TSGraphOptions): NativeGraphOptions {
286
+ return {
287
+ homeDir: undefined,
288
+ sources: options.sources,
289
+ since: options.since,
290
+ until: options.until,
291
+ year: options.year,
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Convert native result to TypeScript format
297
+ */
298
+ function fromNativeResult(result: NativeGraphResult): TokenContributionData {
299
+ return {
300
+ meta: {
301
+ generatedAt: result.meta.generatedAt,
302
+ version: result.meta.version,
303
+ dateRange: {
304
+ start: result.meta.dateRangeStart,
305
+ end: result.meta.dateRangeEnd,
306
+ },
307
+ },
308
+ summary: {
309
+ totalTokens: result.summary.totalTokens,
310
+ totalCost: result.summary.totalCost,
311
+ totalDays: result.summary.totalDays,
312
+ activeDays: result.summary.activeDays,
313
+ averagePerDay: result.summary.averagePerDay,
314
+ maxCostInSingleDay: result.summary.maxCostInSingleDay,
315
+ sources: result.summary.sources as SourceType[],
316
+ models: result.summary.models,
317
+ },
318
+ years: result.years.map((y) => ({
319
+ year: y.year,
320
+ totalTokens: y.totalTokens,
321
+ totalCost: y.totalCost,
322
+ range: {
323
+ start: y.rangeStart,
324
+ end: y.rangeEnd,
325
+ },
326
+ })),
327
+ contributions: result.contributions.map((c) => ({
328
+ date: c.date,
329
+ totals: {
330
+ tokens: c.totals.tokens,
331
+ cost: c.totals.cost,
332
+ messages: c.totals.messages,
333
+ },
334
+ intensity: c.intensity as 0 | 1 | 2 | 3 | 4,
335
+ tokenBreakdown: {
336
+ input: c.tokenBreakdown.input,
337
+ output: c.tokenBreakdown.output,
338
+ cacheRead: c.tokenBreakdown.cacheRead,
339
+ cacheWrite: c.tokenBreakdown.cacheWrite,
340
+ reasoning: c.tokenBreakdown.reasoning,
341
+ },
342
+ sources: c.sources.map((s) => ({
343
+ source: s.source as SourceType,
344
+ modelId: s.modelId,
345
+ providerId: s.providerId,
346
+ tokens: {
347
+ input: s.tokens.input,
348
+ output: s.tokens.output,
349
+ cacheRead: s.tokens.cacheRead,
350
+ cacheWrite: s.tokens.cacheWrite,
351
+ reasoning: s.tokens.reasoning,
352
+ },
353
+ cost: s.cost,
354
+ messages: s.messages,
355
+ })),
356
+ })),
357
+ };
358
+ }
359
+
360
+ /**
361
+ * Generate graph data using native module (without pricing - uses embedded costs)
362
+ * @deprecated Use generateGraphWithPricing instead
363
+ */
364
+ export function generateGraphNative(options: TSGraphOptions = {}): TokenContributionData {
365
+ if (!nativeCore) {
366
+ throw new Error("Native module not available: " + (loadError?.message || "unknown error"));
367
+ }
368
+
369
+ const nativeOptions = toNativeOptions(options);
370
+ const result = nativeCore.generateGraph(nativeOptions);
371
+ return fromNativeResult(result);
372
+ }
373
+
374
+
375
+
376
+ // =============================================================================
377
+ // Reports
378
+ // =============================================================================
379
+
380
+ export interface ModelUsage {
381
+ source: string;
382
+ model: string;
383
+ provider: string;
384
+ input: number;
385
+ output: number;
386
+ cacheRead: number;
387
+ cacheWrite: number;
388
+ reasoning: number;
389
+ messageCount: number;
390
+ cost: number;
391
+ }
392
+
393
+ export interface ModelReport {
394
+ entries: ModelUsage[];
395
+ totalInput: number;
396
+ totalOutput: number;
397
+ totalCacheRead: number;
398
+ totalCacheWrite: number;
399
+ totalMessages: number;
400
+ totalCost: number;
401
+ processingTimeMs: number;
402
+ }
403
+
404
+ export interface MonthlyUsage {
405
+ month: string;
406
+ models: string[];
407
+ input: number;
408
+ output: number;
409
+ cacheRead: number;
410
+ cacheWrite: number;
411
+ messageCount: number;
412
+ cost: number;
413
+ }
414
+
415
+ export interface MonthlyReport {
416
+ entries: MonthlyUsage[];
417
+ totalCost: number;
418
+ processingTimeMs: number;
419
+ }
420
+
421
+ // =============================================================================
422
+ // Two-Phase Processing (Parallel Optimization)
423
+ // =============================================================================
424
+
425
+ export interface ParsedMessages {
426
+ messages: Array<{
427
+ source: string;
428
+ modelId: string;
429
+ providerId: string;
430
+ timestamp: number;
431
+ date: string;
432
+ input: number;
433
+ output: number;
434
+ cacheRead: number;
435
+ cacheWrite: number;
436
+ reasoning: number;
437
+ sessionId: string;
438
+ }>;
439
+ opencodeCount: number;
440
+ claudeCount: number;
441
+ codexCount: number;
442
+ geminiCount: number;
443
+ processingTimeMs: number;
444
+ }
445
+
446
+ export interface LocalParseOptions {
447
+ sources?: SourceType[];
448
+ since?: string;
449
+ until?: string;
450
+ year?: string;
451
+ }
452
+
453
+ export interface FinalizeOptions {
454
+ localMessages: ParsedMessages;
455
+ pricing: PricingEntry[];
456
+ includeCursor: boolean;
457
+ since?: string;
458
+ until?: string;
459
+ year?: string;
460
+ }
461
+
462
+
463
+
464
+ // =============================================================================
465
+ // Async Subprocess Wrappers (Non-blocking for UI)
466
+ // =============================================================================
467
+
468
+ import { fileURLToPath } from "node:url";
469
+ import { dirname, join } from "node:path";
470
+
471
+ const __filename = fileURLToPath(import.meta.url);
472
+ const __dirname = dirname(__filename);
473
+
474
+ const DEFAULT_TIMEOUT_MS = 300_000;
475
+ const NATIVE_TIMEOUT_MS = parseInt(
476
+ process.env.TOKSCALE_NATIVE_TIMEOUT_MS || String(DEFAULT_TIMEOUT_MS),
477
+ 10
478
+ );
479
+
480
+ const SIGKILL_GRACE_MS = 500;
481
+ const DEFAULT_MAX_OUTPUT_BYTES = 100 * 1024 * 1024;
482
+ const MAX_OUTPUT_BYTES = parseInt(
483
+ process.env.TOKSCALE_MAX_OUTPUT_BYTES || String(DEFAULT_MAX_OUTPUT_BYTES),
484
+ 10
485
+ );
486
+
487
+ interface BunSubprocess {
488
+ stdin: { write: (data: string) => void; end: () => void };
489
+ stdout: { text: () => Promise<string> };
490
+ stderr: { text: () => Promise<string> };
491
+ exited: Promise<number>;
492
+ signalCode: string | null;
493
+ killed: boolean;
494
+ kill: (signal?: string) => void;
495
+ }
496
+
497
+ interface BunSpawnOptions {
498
+ stdin: string;
499
+ stdout: string;
500
+ stderr: string;
501
+ }
502
+
503
+ interface BunGlobalType {
504
+ spawn: (cmd: string[], opts: BunSpawnOptions) => BunSubprocess;
505
+ }
506
+
507
+ function safeKill(proc: unknown, signal?: string): void {
508
+ try {
509
+ (proc as { kill: (signal?: string) => void }).kill(signal);
510
+ } catch {}
511
+ }
512
+
513
+ async function runInSubprocess<T>(method: string, args: unknown[]): Promise<T> {
514
+ const runnerPath = join(__dirname, "native-runner.js");
515
+ const input = JSON.stringify({ method, args });
516
+
517
+ const BunGlobal = (globalThis as Record<string, unknown>).Bun as BunGlobalType;
518
+
519
+ let proc: BunSubprocess;
520
+ try {
521
+ proc = BunGlobal.spawn([process.execPath, runnerPath], {
522
+ stdin: "pipe",
523
+ stdout: "pipe",
524
+ stderr: "pipe",
525
+ });
526
+ } catch (e) {
527
+ throw new Error(`Failed to spawn subprocess: ${(e as Error).message}`);
528
+ }
529
+
530
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
531
+ let sigkillId: ReturnType<typeof setTimeout> | null = null;
532
+ let weInitiatedKill = false;
533
+ let aborted = false;
534
+
535
+ const cleanup = async () => {
536
+ if (timeoutId) clearTimeout(timeoutId);
537
+ if (sigkillId) clearTimeout(sigkillId);
538
+ if (aborted) {
539
+ safeKill(proc, "SIGKILL");
540
+ await proc.exited.catch(() => {});
541
+ }
542
+ };
543
+
544
+ const abort = () => {
545
+ aborted = true;
546
+ weInitiatedKill = true;
547
+ };
548
+
549
+ try {
550
+ proc.stdin.write(input);
551
+ proc.stdin.end();
552
+
553
+ const stdoutChunks: Uint8Array[] = [];
554
+ const stderrChunks: Uint8Array[] = [];
555
+ let stdoutBytes = 0;
556
+ let stderrBytes = 0;
557
+
558
+ const readStream = async (
559
+ stream: BunSubprocess["stdout"],
560
+ chunks: Uint8Array[],
561
+ getBytesRef: () => number,
562
+ setBytesRef: (n: number) => void
563
+ ): Promise<string> => {
564
+ const reader = (stream as unknown as ReadableStream<Uint8Array>).getReader();
565
+ try {
566
+ while (!aborted) {
567
+ const { done, value } = await reader.read();
568
+ if (done) break;
569
+ const newTotal = getBytesRef() + value.length;
570
+ if (newTotal > MAX_OUTPUT_BYTES) {
571
+ abort();
572
+ throw new Error(`Output exceeded ${MAX_OUTPUT_BYTES} bytes`);
573
+ }
574
+ setBytesRef(newTotal);
575
+ chunks.push(value);
576
+ }
577
+ } finally {
578
+ await reader.cancel().catch(() => {});
579
+ reader.releaseLock();
580
+ }
581
+ const combined = new Uint8Array(getBytesRef());
582
+ let offset = 0;
583
+ for (const chunk of chunks) {
584
+ combined.set(chunk, offset);
585
+ offset += chunk.length;
586
+ }
587
+ return new TextDecoder().decode(combined);
588
+ };
589
+
590
+ const timeoutPromise = new Promise<never>((_, reject) => {
591
+ timeoutId = setTimeout(() => {
592
+ abort();
593
+ safeKill(proc, "SIGTERM");
594
+ sigkillId = setTimeout(() => {
595
+ safeKill(proc, "SIGKILL");
596
+ reject(new Error(
597
+ `Subprocess '${method}' timed out after ${NATIVE_TIMEOUT_MS}ms (hard kill)`
598
+ ));
599
+ }, SIGKILL_GRACE_MS);
600
+ }, NATIVE_TIMEOUT_MS);
601
+ });
602
+
603
+ const workPromise = Promise.all([
604
+ readStream(proc.stdout, stdoutChunks, () => stdoutBytes, (n) => { stdoutBytes = n; }),
605
+ readStream(proc.stderr, stderrChunks, () => stderrBytes, (n) => { stderrBytes = n; }),
606
+ proc.exited,
607
+ ]);
608
+
609
+ const [stdout, stderr, exitCode] = await Promise.race([workPromise, timeoutPromise]);
610
+
611
+ // Note: proc.killed is always true after exit in Bun (even for normal exits), so we only check signalCode
612
+ if (weInitiatedKill || proc.signalCode) {
613
+ throw new Error(
614
+ `Subprocess '${method}' was killed (signal: ${proc.signalCode || "SIGTERM"})`
615
+ );
616
+ }
617
+
618
+ if (exitCode !== 0) {
619
+ let errorMsg = stderr || `Process exited with code ${exitCode}`;
620
+ try {
621
+ const parsed = JSON.parse(stderr);
622
+ if (parsed.error) errorMsg = parsed.error;
623
+ } catch {}
624
+ throw new Error(`Subprocess '${method}' failed: ${errorMsg}`);
625
+ }
626
+
627
+ try {
628
+ return JSON.parse(stdout) as T;
629
+ } catch (e) {
630
+ throw new Error(
631
+ `Failed to parse subprocess output: ${(e as Error).message}\nstdout: ${stdout.slice(0, 500)}`
632
+ );
633
+ }
634
+ } finally {
635
+ await cleanup();
636
+ }
637
+ }
638
+
639
+ export async function parseLocalSourcesAsync(options: LocalParseOptions): Promise<ParsedMessages> {
640
+ // Use TypeScript fallback when native module is not available
641
+ if (!isNativeAvailable()) {
642
+ const result = parseLocalSourcesTS({
643
+ sources: options.sources,
644
+ since: options.since,
645
+ until: options.until,
646
+ year: options.year,
647
+ });
648
+
649
+ // Convert TypeScript ParsedMessages to native format
650
+ return {
651
+ messages: result.messages.map((msg) => ({
652
+ source: msg.source,
653
+ modelId: msg.modelId,
654
+ providerId: msg.providerId,
655
+ timestamp: msg.timestamp,
656
+ date: msg.date,
657
+ input: msg.tokens.input,
658
+ output: msg.tokens.output,
659
+ cacheRead: msg.tokens.cacheRead,
660
+ cacheWrite: msg.tokens.cacheWrite,
661
+ reasoning: msg.tokens.reasoning,
662
+ sessionId: msg.sessionId,
663
+ })),
664
+ opencodeCount: result.opencodeCount,
665
+ claudeCount: result.claudeCount,
666
+ codexCount: result.codexCount,
667
+ geminiCount: result.geminiCount,
668
+ processingTimeMs: result.processingTimeMs,
669
+ };
670
+ }
671
+
672
+ const nativeOptions: NativeLocalParseOptions = {
673
+ homeDir: undefined,
674
+ sources: options.sources,
675
+ since: options.since,
676
+ until: options.until,
677
+ year: options.year,
678
+ };
679
+
680
+ return runInSubprocess<ParsedMessages>("parseLocalSources", [nativeOptions]);
681
+ }
682
+
683
+ function buildMessagesForFallback(options: FinalizeOptions): UnifiedMessage[] {
684
+ const messages: UnifiedMessage[] = options.localMessages.messages.map((msg) => ({
685
+ source: msg.source,
686
+ modelId: msg.modelId,
687
+ providerId: msg.providerId,
688
+ sessionId: msg.sessionId,
689
+ timestamp: msg.timestamp,
690
+ date: msg.date,
691
+ tokens: {
692
+ input: msg.input,
693
+ output: msg.output,
694
+ cacheRead: msg.cacheRead,
695
+ cacheWrite: msg.cacheWrite,
696
+ reasoning: msg.reasoning,
697
+ },
698
+ cost: 0,
699
+ }));
700
+
701
+ if (options.includeCursor) {
702
+ const cursorMessages = readCursorMessagesFromCache();
703
+ for (const cursor of cursorMessages) {
704
+ const inRange =
705
+ (!options.year || cursor.date.startsWith(options.year)) &&
706
+ (!options.since || cursor.date >= options.since) &&
707
+ (!options.until || cursor.date <= options.until);
708
+ if (inRange) {
709
+ messages.push(cursor);
710
+ }
711
+ }
712
+ }
713
+
714
+ return messages;
715
+ }
716
+
717
+ export async function finalizeReportAsync(options: FinalizeOptions): Promise<ModelReport> {
718
+ if (!isNativeAvailable()) {
719
+ const startTime = performance.now();
720
+ const messages = buildMessagesForFallback(options);
721
+ return generateModelReportTS(messages, options.pricing, startTime);
722
+ }
723
+
724
+ const nativeOptions: NativeFinalizeReportOptions = {
725
+ homeDir: undefined,
726
+ localMessages: options.localMessages,
727
+ pricing: options.pricing,
728
+ includeCursor: options.includeCursor,
729
+ since: options.since,
730
+ until: options.until,
731
+ year: options.year,
732
+ };
733
+
734
+ return runInSubprocess<ModelReport>("finalizeReport", [nativeOptions]);
735
+ }
736
+
737
+ export async function finalizeMonthlyReportAsync(options: FinalizeOptions): Promise<MonthlyReport> {
738
+ if (!isNativeAvailable()) {
739
+ const startTime = performance.now();
740
+ const messages = buildMessagesForFallback(options);
741
+ return generateMonthlyReportTS(messages, options.pricing, startTime);
742
+ }
743
+
744
+ const nativeOptions: NativeFinalizeReportOptions = {
745
+ homeDir: undefined,
746
+ localMessages: options.localMessages,
747
+ pricing: options.pricing,
748
+ includeCursor: options.includeCursor,
749
+ since: options.since,
750
+ until: options.until,
751
+ year: options.year,
752
+ };
753
+
754
+ return runInSubprocess<MonthlyReport>("finalizeMonthlyReport", [nativeOptions]);
755
+ }
756
+
757
+ export async function finalizeGraphAsync(options: FinalizeOptions): Promise<TokenContributionData> {
758
+ if (!isNativeAvailable()) {
759
+ const startTime = performance.now();
760
+ const messages = buildMessagesForFallback(options);
761
+ return generateGraphDataTS(messages, options.pricing, startTime);
762
+ }
763
+
764
+ const nativeOptions: NativeFinalizeReportOptions = {
765
+ homeDir: undefined,
766
+ localMessages: options.localMessages,
767
+ pricing: options.pricing,
768
+ includeCursor: options.includeCursor,
769
+ since: options.since,
770
+ until: options.until,
771
+ year: options.year,
772
+ };
773
+
774
+ const result = await runInSubprocess<NativeGraphResult>("finalizeGraph", [nativeOptions]);
775
+ return fromNativeResult(result);
776
+ }
777
+
778
+ export async function generateGraphWithPricingAsync(
779
+ options: TSGraphOptions & { pricing: PricingEntry[] }
780
+ ): Promise<TokenContributionData> {
781
+ // Use TypeScript fallback when native module is not available
782
+ if (!isNativeAvailable()) {
783
+ const startTime = performance.now();
784
+
785
+ // Parse local sources using TS fallback
786
+ const parsed = parseLocalSourcesTS({
787
+ sources: options.sources,
788
+ since: options.since,
789
+ until: options.until,
790
+ year: options.year,
791
+ });
792
+
793
+ return generateGraphDataTS(parsed.messages, options.pricing, startTime);
794
+ }
795
+
796
+ const nativeOptions: NativeReportOptions = {
797
+ homeDir: undefined,
798
+ sources: options.sources,
799
+ pricing: options.pricing,
800
+ since: options.since,
801
+ until: options.until,
802
+ year: options.year,
803
+ };
804
+
805
+ const result = await runInSubprocess<NativeGraphResult>("generateGraphWithPricing", [nativeOptions]);
806
+ return fromNativeResult(result);
807
+ }