@tokscale/cli 1.2.7 → 1.2.8-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +73 -17
- package/dist/cli.js.map +1 -1
- package/dist/date-utils.d.ts +10 -0
- package/dist/date-utils.d.ts.map +1 -0
- package/dist/date-utils.js +47 -0
- package/dist/date-utils.js.map +1 -0
- package/dist/graph-types.d.ts +5 -15
- package/dist/graph-types.d.ts.map +1 -1
- package/dist/native.d.ts +4 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +12 -2
- package/dist/native.js.map +1 -1
- package/dist/submit.d.ts.map +1 -1
- package/dist/submit.js +25 -0
- package/dist/submit.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +2 -0
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/hooks/useData.d.ts +2 -0
- package/dist/tui/hooks/useData.d.ts.map +1 -1
- package/dist/tui/hooks/useData.js +32 -18
- package/dist/tui/hooks/useData.js.map +1 -1
- package/dist/tui/types/index.d.ts +2 -0
- package/dist/tui/types/index.d.ts.map +1 -1
- package/dist/tui/types/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +87 -18
- package/src/date-utils.ts +51 -0
- package/src/graph-types.ts +6 -19
- package/src/native.ts +21 -12
- package/src/submit.ts +29 -0
- package/src/tui/App.tsx +2 -0
- package/src/tui/hooks/useData.ts +37 -20
- package/src/tui/types/index.ts +2 -0
package/src/cli.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { performance } from "node:perf_hooks";
|
|
|
60
60
|
import type { SourceType } from "./graph-types.js";
|
|
61
61
|
import type { TUIOptions, TabType } from "./tui/types/index.js";
|
|
62
62
|
import { loadSettings } from "./tui/config/settings.js";
|
|
63
|
+
import { formatDateLocal, parseDateStringToLocal, getStartOfDayTimestamp, getEndOfDayTimestamp, validateTimestampMs } from "./date-utils.js";
|
|
63
64
|
|
|
64
65
|
type LaunchTUIFunction = (options?: TUIOptions) => Promise<void>;
|
|
65
66
|
|
|
@@ -144,40 +145,94 @@ interface CursorSyncResult {
|
|
|
144
145
|
error?: string;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
// =============================================================================
|
|
148
|
-
// Date Helpers
|
|
149
|
-
// =============================================================================
|
|
150
148
|
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
|
|
150
|
+
interface DateFilters {
|
|
151
|
+
since?: string;
|
|
152
|
+
until?: string;
|
|
153
|
+
year?: string;
|
|
154
|
+
sinceTs?: number;
|
|
155
|
+
untilTs?: number;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
function getDateFilters(options: DateFilterOptions):
|
|
158
|
+
function getDateFilters(options: DateFilterOptions): DateFilters {
|
|
156
159
|
const today = new Date();
|
|
157
160
|
|
|
158
|
-
// --today: just today
|
|
159
161
|
if (options.today) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
let sinceTs = getStartOfDayTimestamp(today);
|
|
163
|
+
let untilTs = getEndOfDayTimestamp(today);
|
|
164
|
+
sinceTs = validateTimestampMs(sinceTs, '--today (since)');
|
|
165
|
+
untilTs = validateTimestampMs(untilTs, '--today (until)');
|
|
166
|
+
return {
|
|
167
|
+
sinceTs,
|
|
168
|
+
untilTs,
|
|
169
|
+
};
|
|
162
170
|
}
|
|
163
171
|
|
|
164
|
-
// --week: last 7 days
|
|
165
172
|
if (options.week) {
|
|
166
173
|
const weekAgo = new Date(today);
|
|
167
|
-
weekAgo.setDate(weekAgo.getDate() - 6);
|
|
168
|
-
|
|
174
|
+
weekAgo.setDate(weekAgo.getDate() - 6);
|
|
175
|
+
let sinceTs = getStartOfDayTimestamp(weekAgo);
|
|
176
|
+
let untilTs = getEndOfDayTimestamp(today);
|
|
177
|
+
sinceTs = validateTimestampMs(sinceTs, '--week (since)');
|
|
178
|
+
untilTs = validateTimestampMs(untilTs, '--week (until)');
|
|
179
|
+
return {
|
|
180
|
+
sinceTs,
|
|
181
|
+
untilTs,
|
|
182
|
+
};
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
// --month: current calendar month
|
|
172
185
|
if (options.month) {
|
|
173
186
|
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
174
|
-
|
|
187
|
+
let sinceTs = getStartOfDayTimestamp(startOfMonth);
|
|
188
|
+
let untilTs = getEndOfDayTimestamp(today);
|
|
189
|
+
sinceTs = validateTimestampMs(sinceTs, '--month (since)');
|
|
190
|
+
untilTs = validateTimestampMs(untilTs, '--month (until)');
|
|
191
|
+
return {
|
|
192
|
+
sinceTs,
|
|
193
|
+
untilTs,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (options.since || options.until) {
|
|
198
|
+
let sinceTs: number | undefined;
|
|
199
|
+
let untilTs: number | undefined;
|
|
200
|
+
|
|
201
|
+
if (options.since) {
|
|
202
|
+
const sinceDate = parseDateStringToLocal(options.since);
|
|
203
|
+
if (!sinceDate) {
|
|
204
|
+
console.error(pc.red(`\n Error: Invalid --since date '${options.since}'. Use YYYY-MM-DD format with valid date.\n`));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
sinceTs = getStartOfDayTimestamp(sinceDate);
|
|
208
|
+
sinceTs = validateTimestampMs(sinceTs, '--since');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (options.until) {
|
|
212
|
+
const untilDate = parseDateStringToLocal(options.until);
|
|
213
|
+
if (!untilDate) {
|
|
214
|
+
console.error(pc.red(`\n Error: Invalid --until date '${options.until}'. Use YYYY-MM-DD format with valid date.\n`));
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
untilTs = getEndOfDayTimestamp(untilDate);
|
|
218
|
+
untilTs = validateTimestampMs(untilTs, '--until');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (sinceTs !== undefined && untilTs !== undefined && sinceTs > untilTs) {
|
|
222
|
+
console.error(pc.red(`\n Error: --since date must be before --until date.\n`));
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
since: options.since,
|
|
228
|
+
until: options.until,
|
|
229
|
+
year: options.year,
|
|
230
|
+
sinceTs,
|
|
231
|
+
untilTs,
|
|
232
|
+
};
|
|
175
233
|
}
|
|
176
234
|
|
|
177
|
-
// Explicit filters
|
|
178
235
|
return {
|
|
179
|
-
since: options.since,
|
|
180
|
-
until: options.until,
|
|
181
236
|
year: options.year,
|
|
182
237
|
};
|
|
183
238
|
}
|
|
@@ -423,6 +478,8 @@ function buildTUIOptions(
|
|
|
423
478
|
since: dateFilters.since,
|
|
424
479
|
until: dateFilters.until,
|
|
425
480
|
year: dateFilters.year,
|
|
481
|
+
sinceTs: dateFilters.sinceTs,
|
|
482
|
+
untilTs: dateFilters.untilTs,
|
|
426
483
|
};
|
|
427
484
|
}
|
|
428
485
|
|
|
@@ -1048,7 +1105,7 @@ interface LoadedDataSources {
|
|
|
1048
1105
|
|
|
1049
1106
|
async function loadDataSourcesParallel(
|
|
1050
1107
|
localSources: SourceType[],
|
|
1051
|
-
dateFilters:
|
|
1108
|
+
dateFilters: DateFilters,
|
|
1052
1109
|
onPhase?: (phase: string) => void
|
|
1053
1110
|
): Promise<LoadedDataSources> {
|
|
1054
1111
|
const shouldParseLocal = localSources.length > 0;
|
|
@@ -1061,6 +1118,8 @@ async function loadDataSourcesParallel(
|
|
|
1061
1118
|
since: dateFilters.since,
|
|
1062
1119
|
until: dateFilters.until,
|
|
1063
1120
|
year: dateFilters.year,
|
|
1121
|
+
sinceTs: dateFilters.sinceTs,
|
|
1122
|
+
untilTs: dateFilters.untilTs,
|
|
1064
1123
|
})
|
|
1065
1124
|
: Promise.resolve(null),
|
|
1066
1125
|
]);
|
|
@@ -1142,6 +1201,8 @@ async function showModelReport(options: FilterOptions & DateFilterOptions & { be
|
|
|
1142
1201
|
since: dateFilters.since,
|
|
1143
1202
|
until: dateFilters.until,
|
|
1144
1203
|
year: dateFilters.year,
|
|
1204
|
+
sinceTs: dateFilters.sinceTs,
|
|
1205
|
+
untilTs: dateFilters.untilTs,
|
|
1145
1206
|
});
|
|
1146
1207
|
} catch (e) {
|
|
1147
1208
|
if (spinner) {
|
|
@@ -1274,6 +1335,8 @@ async function showMonthlyReport(options: FilterOptions & DateFilterOptions & {
|
|
|
1274
1335
|
since: dateFilters.since,
|
|
1275
1336
|
until: dateFilters.until,
|
|
1276
1337
|
year: dateFilters.year,
|
|
1338
|
+
sinceTs: dateFilters.sinceTs,
|
|
1339
|
+
untilTs: dateFilters.untilTs,
|
|
1277
1340
|
});
|
|
1278
1341
|
} catch (e) {
|
|
1279
1342
|
if (spinner) {
|
|
@@ -1373,6 +1436,8 @@ async function outputJsonReport(
|
|
|
1373
1436
|
since: dateFilters.since,
|
|
1374
1437
|
until: dateFilters.until,
|
|
1375
1438
|
year: dateFilters.year,
|
|
1439
|
+
sinceTs: dateFilters.sinceTs,
|
|
1440
|
+
untilTs: dateFilters.untilTs,
|
|
1376
1441
|
});
|
|
1377
1442
|
console.log(JSON.stringify(report, null, 2));
|
|
1378
1443
|
} else {
|
|
@@ -1382,6 +1447,8 @@ async function outputJsonReport(
|
|
|
1382
1447
|
since: dateFilters.since,
|
|
1383
1448
|
until: dateFilters.until,
|
|
1384
1449
|
year: dateFilters.year,
|
|
1450
|
+
sinceTs: dateFilters.sinceTs,
|
|
1451
|
+
untilTs: dateFilters.untilTs,
|
|
1385
1452
|
});
|
|
1386
1453
|
console.log(JSON.stringify(report, null, 2));
|
|
1387
1454
|
}
|
|
@@ -1425,6 +1492,8 @@ async function handleGraphCommand(options: GraphCommandOptions) {
|
|
|
1425
1492
|
since: dateFilters.since,
|
|
1426
1493
|
until: dateFilters.until,
|
|
1427
1494
|
year: dateFilters.year,
|
|
1495
|
+
sinceTs: dateFilters.sinceTs,
|
|
1496
|
+
untilTs: dateFilters.untilTs,
|
|
1428
1497
|
});
|
|
1429
1498
|
|
|
1430
1499
|
const processingTime = performance.now() - startTime;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function formatDateLocal(date: Date): string {
|
|
2
|
+
const y = date.getFullYear();
|
|
3
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
4
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
5
|
+
return `${y}-${m}-${d}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseDateStringToLocal(dateStr: string): Date | null {
|
|
9
|
+
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
const [, yearStr, monthStr, dayStr] = match;
|
|
12
|
+
const year = parseInt(yearStr);
|
|
13
|
+
const month = parseInt(monthStr) - 1;
|
|
14
|
+
const day = parseInt(dayStr);
|
|
15
|
+
const date = new Date(year, month, day);
|
|
16
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getStartOfDayTimestamp(date: Date): number {
|
|
23
|
+
const start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
|
24
|
+
return start.getTime();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getEndOfDayTimestamp(date: Date): number {
|
|
28
|
+
// Returns start of NEXT day (exclusive upper bound for half-open interval [since, until))
|
|
29
|
+
const nextDay = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, 0, 0, 0, 0);
|
|
30
|
+
return nextDay.getTime();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getContributionLocalDate(contrib: { date: string; timestampMs?: number }): string {
|
|
34
|
+
if (contrib.timestampMs != null) {
|
|
35
|
+
return formatDateLocal(new Date(contrib.timestampMs));
|
|
36
|
+
}
|
|
37
|
+
return contrib.date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MIN_VALID_TIMESTAMP_MS = 1_000_000_000_000;
|
|
41
|
+
const MAX_VALID_TIMESTAMP_MS = 4_102_444_800_000;
|
|
42
|
+
|
|
43
|
+
export function validateTimestampMs(ts: number, label: string): number {
|
|
44
|
+
if (!Number.isFinite(ts) || !Number.isSafeInteger(ts)) {
|
|
45
|
+
throw new Error(`${label} must be a finite safe integer, got ${ts}`);
|
|
46
|
+
}
|
|
47
|
+
if (ts < MIN_VALID_TIMESTAMP_MS || ts > MAX_VALID_TIMESTAMP_MS) {
|
|
48
|
+
throw new Error(`${label} out of valid range (${MIN_VALID_TIMESTAMP_MS}..${MAX_VALID_TIMESTAMP_MS}), got ${ts}`);
|
|
49
|
+
}
|
|
50
|
+
return ts;
|
|
51
|
+
}
|
package/src/graph-types.ts
CHANGED
|
@@ -46,9 +46,13 @@ export interface SourceContribution {
|
|
|
46
46
|
* Daily contribution entry with full granularity
|
|
47
47
|
*/
|
|
48
48
|
export interface DailyContribution {
|
|
49
|
-
/** ISO date string (YYYY-MM-DD) */
|
|
49
|
+
/** ISO date string (YYYY-MM-DD) in UTC - note: this is the UTC day bucket, not local date */
|
|
50
50
|
date: string;
|
|
51
51
|
|
|
52
|
+
/** Unix timestampMs (ms) of earliest message in this UTC day bucket.
|
|
53
|
+
* Undefined for days with no valid timestamps. */
|
|
54
|
+
timestampMs?: number;
|
|
55
|
+
|
|
52
56
|
/** Aggregated totals for the day */
|
|
53
57
|
totals: {
|
|
54
58
|
/** Total tokens (input + output + cache) */
|
|
@@ -157,23 +161,6 @@ export interface TokenContributionData {
|
|
|
157
161
|
contributions: DailyContribution[];
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
/**
|
|
161
|
-
* Options for graph data generation
|
|
162
|
-
*/
|
|
163
|
-
export interface GraphOptions {
|
|
164
|
-
/** Filter to specific sources */
|
|
165
|
-
sources?: SourceType[];
|
|
166
|
-
|
|
167
|
-
/** Start date filter (ISO format) */
|
|
168
|
-
since?: string;
|
|
169
|
-
|
|
170
|
-
/** End date filter (ISO format) */
|
|
171
|
-
until?: string;
|
|
172
|
-
|
|
173
|
-
/** Filter to specific year */
|
|
174
|
-
year?: string;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
164
|
/**
|
|
178
165
|
* Unified message format for aggregation
|
|
179
166
|
* Used internally to normalize data from different sources
|
|
@@ -182,7 +169,7 @@ export interface UnifiedMessage {
|
|
|
182
169
|
source: SourceType;
|
|
183
170
|
modelId: string;
|
|
184
171
|
providerId?: string;
|
|
185
|
-
|
|
172
|
+
timestampMs: number; // Unix milliseconds
|
|
186
173
|
tokens: TokenBreakdown;
|
|
187
174
|
cost: number;
|
|
188
175
|
}
|
package/src/native.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
9
|
TokenContributionData,
|
|
10
|
-
GraphOptions as TSGraphOptions,
|
|
11
10
|
SourceType,
|
|
12
11
|
} from "./graph-types.js";
|
|
13
12
|
import { loadSettings } from "./tui/config/settings.js";
|
|
@@ -41,6 +40,7 @@ interface NativeSourceContribution {
|
|
|
41
40
|
|
|
42
41
|
interface NativeDailyContribution {
|
|
43
42
|
date: string;
|
|
43
|
+
timestampMs?: number;
|
|
44
44
|
totals: NativeDailyTotals;
|
|
45
45
|
intensity: number;
|
|
46
46
|
tokenBreakdown: NativeTokenBreakdown;
|
|
@@ -81,14 +81,6 @@ interface NativeGraphResult {
|
|
|
81
81
|
contributions: NativeDailyContribution[];
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
interface NativeReportOptions {
|
|
85
|
-
homeDir?: string;
|
|
86
|
-
sources?: string[];
|
|
87
|
-
since?: string;
|
|
88
|
-
until?: string;
|
|
89
|
-
year?: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
84
|
interface NativeModelUsage {
|
|
93
85
|
source: string;
|
|
94
86
|
model: string;
|
|
@@ -165,6 +157,8 @@ interface NativeLocalParseOptions {
|
|
|
165
157
|
since?: string;
|
|
166
158
|
until?: string;
|
|
167
159
|
year?: string;
|
|
160
|
+
sinceTs?: number;
|
|
161
|
+
untilTs?: number;
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
interface NativeFinalizeReportOptions {
|
|
@@ -174,11 +168,12 @@ interface NativeFinalizeReportOptions {
|
|
|
174
168
|
since?: string;
|
|
175
169
|
until?: string;
|
|
176
170
|
year?: string;
|
|
171
|
+
sinceTs?: number;
|
|
172
|
+
untilTs?: number;
|
|
177
173
|
}
|
|
178
174
|
|
|
179
175
|
interface NativeCore {
|
|
180
176
|
version(): string;
|
|
181
|
-
healthCheck(): string;
|
|
182
177
|
parseLocalSources(options: NativeLocalParseOptions): NativeParsedMessages;
|
|
183
178
|
finalizeReport(options: NativeFinalizeReportOptions): NativeModelReport;
|
|
184
179
|
finalizeMonthlyReport(options: NativeFinalizeReportOptions): NativeMonthlyReport;
|
|
@@ -190,7 +185,6 @@ interface NativeCore {
|
|
|
190
185
|
// =============================================================================
|
|
191
186
|
|
|
192
187
|
let nativeCore: NativeCore | null = null;
|
|
193
|
-
let loadError: Error | null = null;
|
|
194
188
|
|
|
195
189
|
try {
|
|
196
190
|
// Type assertion needed because dynamic import returns module namespace
|
|
@@ -199,7 +193,7 @@ try {
|
|
|
199
193
|
(m) => (m.default || m) as unknown as NativeCore
|
|
200
194
|
);
|
|
201
195
|
} catch (e) {
|
|
202
|
-
|
|
196
|
+
void e;
|
|
203
197
|
}
|
|
204
198
|
|
|
205
199
|
// =============================================================================
|
|
@@ -254,6 +248,7 @@ function fromNativeResult(result: NativeGraphResult): TokenContributionData {
|
|
|
254
248
|
})),
|
|
255
249
|
contributions: result.contributions.map((c) => ({
|
|
256
250
|
date: c.date,
|
|
251
|
+
timestampMs: c.timestampMs ?? undefined,
|
|
257
252
|
totals: {
|
|
258
253
|
tokens: c.totals.tokens,
|
|
259
254
|
cost: c.totals.cost,
|
|
@@ -366,6 +361,8 @@ export interface LocalParseOptions {
|
|
|
366
361
|
since?: string;
|
|
367
362
|
until?: string;
|
|
368
363
|
year?: string;
|
|
364
|
+
sinceTs?: number;
|
|
365
|
+
untilTs?: number;
|
|
369
366
|
}
|
|
370
367
|
|
|
371
368
|
export interface FinalizeOptions {
|
|
@@ -374,6 +371,8 @@ export interface FinalizeOptions {
|
|
|
374
371
|
since?: string;
|
|
375
372
|
until?: string;
|
|
376
373
|
year?: string;
|
|
374
|
+
sinceTs?: number;
|
|
375
|
+
untilTs?: number;
|
|
377
376
|
}
|
|
378
377
|
|
|
379
378
|
|
|
@@ -523,6 +522,8 @@ export async function parseLocalSourcesAsync(options: LocalParseOptions): Promis
|
|
|
523
522
|
since: options.since,
|
|
524
523
|
until: options.until,
|
|
525
524
|
year: options.year,
|
|
525
|
+
sinceTs: options.sinceTs,
|
|
526
|
+
untilTs: options.untilTs,
|
|
526
527
|
};
|
|
527
528
|
|
|
528
529
|
return runInSubprocess<ParsedMessages>("parseLocalSources", [nativeOptions]);
|
|
@@ -540,6 +541,8 @@ export async function finalizeReportAsync(options: FinalizeOptions): Promise<Mod
|
|
|
540
541
|
since: options.since,
|
|
541
542
|
until: options.until,
|
|
542
543
|
year: options.year,
|
|
544
|
+
sinceTs: options.sinceTs,
|
|
545
|
+
untilTs: options.untilTs,
|
|
543
546
|
};
|
|
544
547
|
|
|
545
548
|
return runInSubprocess<ModelReport>("finalizeReport", [nativeOptions]);
|
|
@@ -557,6 +560,8 @@ export async function finalizeMonthlyReportAsync(options: FinalizeOptions): Prom
|
|
|
557
560
|
since: options.since,
|
|
558
561
|
until: options.until,
|
|
559
562
|
year: options.year,
|
|
563
|
+
sinceTs: options.sinceTs,
|
|
564
|
+
untilTs: options.untilTs,
|
|
560
565
|
};
|
|
561
566
|
|
|
562
567
|
return runInSubprocess<MonthlyReport>("finalizeMonthlyReport", [nativeOptions]);
|
|
@@ -574,6 +579,8 @@ export async function finalizeGraphAsync(options: FinalizeOptions): Promise<Toke
|
|
|
574
579
|
since: options.since,
|
|
575
580
|
until: options.until,
|
|
576
581
|
year: options.year,
|
|
582
|
+
sinceTs: options.sinceTs,
|
|
583
|
+
untilTs: options.untilTs,
|
|
577
584
|
};
|
|
578
585
|
|
|
579
586
|
const result = await runInSubprocess<NativeGraphResult>("finalizeGraph", [nativeOptions]);
|
|
@@ -602,6 +609,8 @@ export async function finalizeReportAndGraphAsync(options: FinalizeOptions): Pro
|
|
|
602
609
|
since: options.since,
|
|
603
610
|
until: options.until,
|
|
604
611
|
year: options.year,
|
|
612
|
+
sinceTs: options.sinceTs,
|
|
613
|
+
untilTs: options.untilTs,
|
|
605
614
|
};
|
|
606
615
|
|
|
607
616
|
const result = await runInSubprocess<NativeReportAndGraph>("finalizeReportAndGraph", [nativeOptions]);
|
package/src/submit.ts
CHANGED
|
@@ -13,9 +13,33 @@ import { parseLocalSourcesAsync, finalizeReportAndGraphAsync, type ParsedMessage
|
|
|
13
13
|
import { syncCursorCache, isCursorLoggedIn, hasCursorUsageCache } from "./cursor.js";
|
|
14
14
|
import type { TokenContributionData } from "./graph-types.js";
|
|
15
15
|
import { formatCurrency } from "./table.js";
|
|
16
|
+
import { parseDateStringToLocal, getStartOfDayTimestamp, getEndOfDayTimestamp, validateTimestampMs } from "./date-utils.js";
|
|
16
17
|
|
|
17
18
|
const execAsync = promisify(exec);
|
|
18
19
|
|
|
20
|
+
function getTimestampFilters(since?: string, until?: string): { sinceTs?: number; untilTs?: number } {
|
|
21
|
+
let sinceTs: number | undefined;
|
|
22
|
+
let untilTs: number | undefined;
|
|
23
|
+
|
|
24
|
+
if (since) {
|
|
25
|
+
const sinceDate = parseDateStringToLocal(since);
|
|
26
|
+
if (sinceDate) {
|
|
27
|
+
sinceTs = getStartOfDayTimestamp(sinceDate);
|
|
28
|
+
sinceTs = validateTimestampMs(sinceTs, '--since');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (until) {
|
|
33
|
+
const untilDate = parseDateStringToLocal(until);
|
|
34
|
+
if (untilDate) {
|
|
35
|
+
untilTs = getEndOfDayTimestamp(untilDate);
|
|
36
|
+
untilTs = validateTimestampMs(untilTs, '--until');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { sinceTs, untilTs };
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
interface SubmitOptions {
|
|
20
44
|
opencode?: boolean;
|
|
21
45
|
claude?: boolean;
|
|
@@ -213,6 +237,7 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
|
|
|
213
237
|
|
|
214
238
|
// Filter out cursor from local sources (it's handled separately via sync)
|
|
215
239
|
const localSources = sources?.filter((s): s is Exclude<SourceType, "cursor"> => s !== "cursor");
|
|
240
|
+
const { sinceTs, untilTs } = getTimestampFilters(options.since, options.until);
|
|
216
241
|
|
|
217
242
|
let data: TokenContributionData;
|
|
218
243
|
try {
|
|
@@ -224,6 +249,8 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
|
|
|
224
249
|
since: options.since,
|
|
225
250
|
until: options.until,
|
|
226
251
|
year: options.year,
|
|
252
|
+
sinceTs,
|
|
253
|
+
untilTs,
|
|
227
254
|
}),
|
|
228
255
|
includeCursor && isCursorLoggedIn()
|
|
229
256
|
? syncCursorCache()
|
|
@@ -243,6 +270,8 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
|
|
|
243
270
|
since: options.since,
|
|
244
271
|
until: options.until,
|
|
245
272
|
year: options.year,
|
|
273
|
+
sinceTs,
|
|
274
|
+
untilTs,
|
|
246
275
|
});
|
|
247
276
|
|
|
248
277
|
// Use graph structure for submission, report's cost for display
|
package/src/tui/App.tsx
CHANGED
|
@@ -51,6 +51,8 @@ export function App(props: AppProps) {
|
|
|
51
51
|
since: props.since,
|
|
52
52
|
until: props.until,
|
|
53
53
|
year: props.year,
|
|
54
|
+
sinceTs: props.sinceTs,
|
|
55
|
+
untilTs: props.untilTs,
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
const { data, loading, error, refresh, loadingPhase, isRefreshing } = useData(() => enabledSources(), dateFilters);
|
package/src/tui/hooks/useData.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { syncCursorCache, isCursorLoggedIn, hasCursorUsageCache } from "../../cursor.js";
|
|
24
24
|
import { getModelColor } from "../utils/colors.js";
|
|
25
25
|
import { loadCachedData, saveCachedData, isCacheStale, loadSettings } from "../config/settings.js";
|
|
26
|
+
import { formatDateLocal } from "../../date-utils.js";
|
|
26
27
|
|
|
27
28
|
export type {
|
|
28
29
|
SortType,
|
|
@@ -41,13 +42,15 @@ export interface DateFilters {
|
|
|
41
42
|
since?: string;
|
|
42
43
|
until?: string;
|
|
43
44
|
year?: string;
|
|
45
|
+
sinceTs?: number;
|
|
46
|
+
untilTs?: number;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
function buildContributionGrid(contributions: ContributionDay[]): GridCell[][] {
|
|
47
50
|
const grid: GridCell[][] = Array.from({ length: 7 }, () => []);
|
|
48
51
|
|
|
49
52
|
const today = new Date();
|
|
50
|
-
const todayStr = today
|
|
53
|
+
const todayStr = formatDateLocal(today);
|
|
51
54
|
|
|
52
55
|
const startDate = new Date(today);
|
|
53
56
|
startDate.setDate(startDate.getDate() - 364);
|
|
@@ -64,10 +67,7 @@ function buildContributionGrid(contributions: ContributionDay[]): GridCell[][] {
|
|
|
64
67
|
|
|
65
68
|
const currentDate = new Date(startDate);
|
|
66
69
|
while (currentDate <= endDate) {
|
|
67
|
-
const
|
|
68
|
-
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
|
69
|
-
const day = String(currentDate.getDate()).padStart(2, "0");
|
|
70
|
-
const dateStr = `${year}-${month}-${day}`;
|
|
70
|
+
const dateStr = formatDateLocal(currentDate);
|
|
71
71
|
const dayOfWeek = currentDate.getDay();
|
|
72
72
|
|
|
73
73
|
const isFuture = dateStr > todayStr;
|
|
@@ -80,12 +80,12 @@ function buildContributionGrid(contributions: ContributionDay[]): GridCell[][] {
|
|
|
80
80
|
return grid;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function calculatePeakHour(messages: Array<{
|
|
83
|
+
function calculatePeakHour(messages: Array<{ timestampMs: number }>): string {
|
|
84
84
|
if (messages.length === 0) return "N/A";
|
|
85
85
|
|
|
86
86
|
const hourCounts = new Array(24).fill(0);
|
|
87
87
|
for (const msg of messages) {
|
|
88
|
-
const hour = new Date(msg.
|
|
88
|
+
const hour = new Date(msg.timestampMs).getHours();
|
|
89
89
|
hourCounts[hour]++;
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -105,14 +105,14 @@ function calculatePeakHour(messages: Array<{ timestamp: number }>): string {
|
|
|
105
105
|
return `${displayHour}${suffix}`;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function calculateLongestSession(messages: Array<{ sessionId: string;
|
|
108
|
+
function calculateLongestSession(messages: Array<{ sessionId: string; timestampMs: number }>): string {
|
|
109
109
|
if (messages.length === 0) return "N/A";
|
|
110
110
|
|
|
111
111
|
const sessions = new Map<string, number[]>();
|
|
112
112
|
for (const msg of messages) {
|
|
113
113
|
if (!msg.sessionId) continue;
|
|
114
114
|
const timestamps = sessions.get(msg.sessionId) || [];
|
|
115
|
-
timestamps.push(msg.
|
|
115
|
+
timestamps.push(msg.timestampMs);
|
|
116
116
|
sessions.set(msg.sessionId, timestamps);
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -151,14 +151,14 @@ async function loadData(
|
|
|
151
151
|
const sources = Array.from(enabledSources);
|
|
152
152
|
const localSources = sources.filter(s => s !== "cursor");
|
|
153
153
|
const includeCursor = sources.includes("cursor");
|
|
154
|
-
const { since, until, year } = dateFilters ?? {};
|
|
154
|
+
const { since, until, year, sinceTs, untilTs } = dateFilters ?? {};
|
|
155
155
|
|
|
156
156
|
setPhase?.("parsing-sources");
|
|
157
157
|
|
|
158
158
|
const phase1Results = await Promise.allSettled([
|
|
159
159
|
includeCursor && isCursorLoggedIn() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0, error: undefined }),
|
|
160
160
|
localSources.length > 0
|
|
161
|
-
? parseLocalSourcesAsync({ sources: localSources as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid" | "openclaw" | "pi")[], since, until, year })
|
|
161
|
+
? parseLocalSourcesAsync({ sources: localSources as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid" | "openclaw" | "pi")[], since, until, year, sinceTs, untilTs })
|
|
162
162
|
: Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, openclawCount: 0, piCount: 0, processingTimeMs: 0 } as ParsedMessages),
|
|
163
163
|
]);
|
|
164
164
|
|
|
@@ -189,13 +189,14 @@ async function loadData(
|
|
|
189
189
|
};
|
|
190
190
|
|
|
191
191
|
setPhase?.("finalizing-report");
|
|
192
|
-
// Single call ensures consistent pricing between report and graph
|
|
193
192
|
const { report, graph } = await finalizeReportAndGraphAsync({
|
|
194
193
|
localMessages: localMessages || emptyMessages,
|
|
195
194
|
includeCursor: includeCursor && (cursorSync.synced || hasCursorUsageCache()),
|
|
196
195
|
since,
|
|
197
196
|
until,
|
|
198
197
|
year,
|
|
198
|
+
sinceTs,
|
|
199
|
+
untilTs,
|
|
199
200
|
});
|
|
200
201
|
|
|
201
202
|
const settings = loadSettings();
|
|
@@ -217,6 +218,7 @@ async function loadData(
|
|
|
217
218
|
const dailyMap = new Map<string, DailyEntry>();
|
|
218
219
|
for (const contrib of graph.contributions) {
|
|
219
220
|
const dateStr = contrib.date;
|
|
221
|
+
|
|
220
222
|
if (!dailyMap.has(dateStr)) {
|
|
221
223
|
dailyMap.set(dateStr, {
|
|
222
224
|
date: dateStr,
|
|
@@ -323,12 +325,17 @@ async function loadData(
|
|
|
323
325
|
favoriteModel,
|
|
324
326
|
totalTokens: report.totalInput + report.totalOutput + report.totalCacheRead + report.totalCacheWrite,
|
|
325
327
|
sessions: report.totalMessages,
|
|
326
|
-
longestSession: calculateLongestSession(localMessages?.messages || [])
|
|
328
|
+
longestSession: calculateLongestSession((localMessages?.messages || []).map((m) => ({
|
|
329
|
+
sessionId: m.sessionId,
|
|
330
|
+
timestampMs: m.timestamp,
|
|
331
|
+
}))),
|
|
327
332
|
currentStreak,
|
|
328
333
|
longestStreak,
|
|
329
334
|
activeDays: dailyEntries.length,
|
|
330
335
|
totalDays: graph.summary.totalDays,
|
|
331
|
-
peakHour: calculatePeakHour(localMessages?.messages || [])
|
|
336
|
+
peakHour: calculatePeakHour((localMessages?.messages || []).map((m) => ({
|
|
337
|
+
timestampMs: m.timestamp,
|
|
338
|
+
}))),
|
|
332
339
|
};
|
|
333
340
|
|
|
334
341
|
const dailyModelMap = new Map<string, Map<string, number>>();
|
|
@@ -402,6 +409,8 @@ async function loadData(
|
|
|
402
409
|
|
|
403
410
|
const dailyBreakdowns = new Map<string, DailyModelBreakdown>();
|
|
404
411
|
for (const contrib of graph.contributions) {
|
|
412
|
+
const dateStr = contrib.date;
|
|
413
|
+
|
|
405
414
|
const models = contrib.sources.map((source: { modelId: string; source: string; tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; reasoning?: number }; cost: number; messages: number }) => ({
|
|
406
415
|
modelId: source.modelId,
|
|
407
416
|
source: source.source,
|
|
@@ -416,12 +425,20 @@ async function loadData(
|
|
|
416
425
|
messages: source.messages,
|
|
417
426
|
}));
|
|
418
427
|
|
|
419
|
-
dailyBreakdowns.
|
|
420
|
-
|
|
421
|
-
cost
|
|
422
|
-
totalTokens
|
|
423
|
-
models
|
|
424
|
-
|
|
428
|
+
const existing = dailyBreakdowns.get(dateStr);
|
|
429
|
+
if (existing) {
|
|
430
|
+
existing.cost += contrib.totals.cost;
|
|
431
|
+
existing.totalTokens += contrib.totals.tokens;
|
|
432
|
+
existing.models.push(...models);
|
|
433
|
+
existing.models.sort((a, b) => b.cost - a.cost);
|
|
434
|
+
} else {
|
|
435
|
+
dailyBreakdowns.set(dateStr, {
|
|
436
|
+
date: dateStr,
|
|
437
|
+
cost: contrib.totals.cost,
|
|
438
|
+
totalTokens: contrib.totals.tokens,
|
|
439
|
+
models: models.sort((a, b) => b.cost - a.cost),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
425
442
|
}
|
|
426
443
|
|
|
427
444
|
return {
|