@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/dist/cli.d.ts +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +1 -114
- package/dist/native.js.map +1 -1
- package/dist/tui/components/Header.js +1 -1
- package/dist/tui/components/Header.js.map +1 -1
- package/package.json +5 -10
- package/src/auth.ts +211 -0
- package/src/cli.ts +1042 -0
- package/src/credentials.ts +123 -0
- package/src/cursor.ts +558 -0
- package/src/graph-types.ts +188 -0
- package/src/graph.ts +485 -0
- package/src/native-runner.ts +105 -0
- package/src/native.ts +807 -0
- package/src/pricing.ts +309 -0
- package/src/sessions/claudecode.ts +119 -0
- package/src/sessions/codex.ts +227 -0
- package/src/sessions/gemini.ts +108 -0
- package/src/sessions/index.ts +126 -0
- package/src/sessions/opencode.ts +94 -0
- package/src/sessions/reports.ts +475 -0
- package/src/sessions/types.ts +59 -0
- package/src/spinner.ts +283 -0
- package/src/submit.ts +175 -0
- package/src/table.ts +233 -0
- package/src/tui/components/Header.tsx +1 -1
- package/src/types.d.ts +28 -0
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
|
+
}
|