@tokscale/cli 1.2.7 → 1.2.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/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
- function formatDate(date: Date): string {
152
- return date.toISOString().split("T")[0];
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): { since?: string; until?: string; year?: string } {
158
+ function getDateFilters(options: DateFilterOptions): DateFilters {
156
159
  const today = new Date();
157
160
 
158
- // --today: just today
159
161
  if (options.today) {
160
- const todayStr = formatDate(today);
161
- return { since: todayStr, until: todayStr };
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); // Include today = 7 days
168
- return { since: formatDate(weekAgo), until: formatDate(today) };
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
- return { since: formatDate(startOfMonth), until: formatDate(today) };
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: { since?: string; until?: string; year?: string },
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
+ }
@@ -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
- timestamp: number; // Unix milliseconds
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
- loadError = e as Error;
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);
@@ -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.toISOString().split("T")[0];
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 year = currentDate.getFullYear();
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<{ timestamp: number }>): string {
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.timestamp).getHours();
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; timestamp: number }>): 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.timestamp);
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.set(contrib.date, {
420
- date: contrib.date,
421
- cost: contrib.totals.cost,
422
- totalTokens: contrib.totals.tokens,
423
- models: models.sort((a, b) => b.cost - a.cost),
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 {
@@ -137,6 +137,8 @@ export interface TUIOptions {
137
137
  since?: string;
138
138
  until?: string;
139
139
  year?: string;
140
+ sinceTs?: number;
141
+ untilTs?: number;
140
142
  colorPalette?: ColorPaletteName;
141
143
  }
142
144