@tokscale/cli 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/cli.js +27 -8
  2. package/dist/cli.js.map +1 -1
  3. package/dist/graph-types.d.ts +1 -1
  4. package/dist/graph-types.d.ts.map +1 -1
  5. package/dist/native-runner.d.ts +1 -1
  6. package/dist/native-runner.js +6 -6
  7. package/dist/native-runner.js.map +1 -1
  8. package/dist/native.d.ts +1 -0
  9. package/dist/native.d.ts.map +1 -1
  10. package/dist/native.js +32 -59
  11. package/dist/native.js.map +1 -1
  12. package/dist/sessions/types.d.ts +1 -1
  13. package/dist/sessions/types.d.ts.map +1 -1
  14. package/dist/submit.d.ts +1 -0
  15. package/dist/submit.d.ts.map +1 -1
  16. package/dist/submit.js +3 -1
  17. package/dist/submit.js.map +1 -1
  18. package/dist/tui/App.d.ts.map +1 -1
  19. package/dist/tui/App.js +4 -0
  20. package/dist/tui/App.js.map +1 -1
  21. package/dist/tui/config/settings.d.ts +1 -0
  22. package/dist/tui/config/settings.d.ts.map +1 -1
  23. package/dist/tui/config/settings.js +10 -2
  24. package/dist/tui/config/settings.js.map +1 -1
  25. package/dist/tui/hooks/useData.d.ts.map +1 -1
  26. package/dist/tui/hooks/useData.js +2 -1
  27. package/dist/tui/hooks/useData.js.map +1 -1
  28. package/dist/tui/types/index.d.ts +1 -1
  29. package/dist/tui/types/index.d.ts.map +1 -1
  30. package/dist/tui/types/index.js +2 -1
  31. package/dist/tui/types/index.js.map +1 -1
  32. package/dist/tui/utils/colors.d.ts.map +1 -1
  33. package/dist/tui/utils/colors.js +3 -0
  34. package/dist/tui/utils/colors.js.map +1 -1
  35. package/dist/wrapped.d.ts.map +1 -1
  36. package/dist/wrapped.js +5 -2
  37. package/dist/wrapped.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/cli.ts +27 -8
  40. package/src/graph-types.ts +1 -1
  41. package/src/native-runner.ts +17 -17
  42. package/src/native.ts +35 -78
  43. package/src/sessions/types.ts +1 -1
  44. package/src/submit.ts +4 -2
  45. package/src/tui/App.tsx +1 -0
  46. package/src/tui/config/settings.ts +13 -2
  47. package/src/tui/hooks/useData.ts +3 -2
  48. package/src/tui/types/index.ts +3 -2
  49. package/src/tui/utils/colors.ts +2 -0
  50. package/src/wrapped.ts +6 -3
package/src/native.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  GraphOptions as TSGraphOptions,
11
11
  SourceType,
12
12
  } from "./graph-types.js";
13
+ import { loadSettings } from "./tui/config/settings.js";
13
14
 
14
15
  // =============================================================================
15
16
  // Types matching Rust exports
@@ -152,8 +153,9 @@ interface NativeParsedMessages {
152
153
  codexCount: number;
153
154
  geminiCount: number;
154
155
  ampCount: number;
155
- droidCount?: number;
156
- openclawCount?: number;
156
+ droidCount: number;
157
+ openclawCount: number;
158
+ piCount: number;
157
159
  processingTimeMs: number;
158
160
  }
159
161
 
@@ -354,6 +356,7 @@ export interface ParsedMessages {
354
356
  ampCount: number;
355
357
  droidCount: number;
356
358
  openclawCount: number;
359
+ piCount: number;
357
360
  processingTimeMs: number;
358
361
  }
359
362
 
@@ -381,25 +384,21 @@ export interface FinalizeOptions {
381
384
 
382
385
  import { fileURLToPath } from "node:url";
383
386
  import { dirname, join } from "node:path";
384
- import { writeFileSync, unlinkSync, mkdirSync } from "node:fs";
387
+ import { writeFileSync, readFileSync, unlinkSync, mkdirSync, existsSync } from "node:fs";
385
388
  import { tmpdir } from "node:os";
386
389
  import { randomUUID } from "node:crypto";
387
390
 
388
391
  const __filename = fileURLToPath(import.meta.url);
389
392
  const __dirname = dirname(__filename);
390
393
 
391
- const DEFAULT_TIMEOUT_MS = 300_000;
392
- const NATIVE_TIMEOUT_MS = parseInt(
393
- process.env.TOKSCALE_NATIVE_TIMEOUT_MS || String(DEFAULT_TIMEOUT_MS),
394
- 10
395
- );
396
-
397
394
  const SIGKILL_GRACE_MS = 500;
398
- const DEFAULT_MAX_OUTPUT_BYTES = 100 * 1024 * 1024;
399
- const MAX_OUTPUT_BYTES = parseInt(
400
- process.env.TOKSCALE_MAX_OUTPUT_BYTES || String(DEFAULT_MAX_OUTPUT_BYTES),
401
- 10
402
- );
395
+
396
+ function getNativeTimeoutMs(): number {
397
+ const settings = loadSettings();
398
+ return process.env.TOKSCALE_NATIVE_TIMEOUT_MS
399
+ ? parseInt(process.env.TOKSCALE_NATIVE_TIMEOUT_MS, 10)
400
+ : (settings.nativeTimeoutMs ?? 300_000);
401
+ }
403
402
 
404
403
  interface BunSubprocess {
405
404
  stdout: { text: () => Promise<string> };
@@ -411,8 +410,8 @@ interface BunSubprocess {
411
410
  }
412
411
 
413
412
  interface BunSpawnOptions {
414
- stdout: string;
415
- stderr: string;
413
+ stdout: "pipe" | "ignore";
414
+ stderr: "pipe" | "ignore";
416
415
  }
417
416
 
418
417
  interface BunGlobalType {
@@ -426,89 +425,46 @@ function safeKill(proc: unknown, signal?: string): void {
426
425
  }
427
426
 
428
427
  async function runInSubprocess<T>(method: string, args: unknown[]): Promise<T> {
428
+ const NATIVE_TIMEOUT_MS = getNativeTimeoutMs();
429
429
  const runnerPath = join(__dirname, "native-runner.js");
430
430
  const input = JSON.stringify({ method, args });
431
431
 
432
432
  const tmpDir = join(tmpdir(), "tokscale");
433
433
  mkdirSync(tmpDir, { recursive: true });
434
- const inputFile = join(tmpDir, `input-${randomUUID()}.json`);
435
-
434
+ const id = randomUUID();
435
+ const inputFile = join(tmpDir, `input-${id}.json`);
436
+ const outputFile = join(tmpDir, `output-${id}.json`);
437
+
436
438
  writeFileSync(inputFile, input, "utf-8");
437
439
 
438
440
  const BunGlobal = (globalThis as Record<string, unknown>).Bun as BunGlobalType;
439
441
 
440
442
  let proc: BunSubprocess;
441
443
  try {
442
- proc = BunGlobal.spawn([process.execPath, runnerPath, inputFile], {
443
- stdout: "pipe",
444
+ proc = BunGlobal.spawn([process.execPath, runnerPath, inputFile, outputFile], {
445
+ stdout: "ignore",
444
446
  stderr: "pipe",
445
447
  });
446
448
  } catch (e) {
447
- unlinkSync(inputFile);
449
+ try { unlinkSync(inputFile); } catch {}
448
450
  throw new Error(`Failed to spawn subprocess: ${(e as Error).message}`);
449
451
  }
450
452
 
451
453
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
452
454
  let sigkillId: ReturnType<typeof setTimeout> | null = null;
453
455
  let weInitiatedKill = false;
454
- let aborted = false;
455
456
 
456
457
  const cleanup = async () => {
457
458
  if (timeoutId) clearTimeout(timeoutId);
458
459
  if (sigkillId) clearTimeout(sigkillId);
459
460
  try { unlinkSync(inputFile); } catch {}
460
- if (aborted) {
461
- safeKill(proc, "SIGKILL");
462
- await proc.exited.catch(() => {});
463
- }
464
- };
465
-
466
- const abort = () => {
467
- aborted = true;
468
- weInitiatedKill = true;
461
+ try { unlinkSync(outputFile); } catch {}
469
462
  };
470
463
 
471
464
  try {
472
- const stdoutChunks: Uint8Array[] = [];
473
- const stderrChunks: Uint8Array[] = [];
474
- let stdoutBytes = 0;
475
- let stderrBytes = 0;
476
-
477
- const readStream = async (
478
- stream: BunSubprocess["stdout"],
479
- chunks: Uint8Array[],
480
- getBytesRef: () => number,
481
- setBytesRef: (n: number) => void
482
- ): Promise<string> => {
483
- const reader = (stream as unknown as ReadableStream<Uint8Array>).getReader();
484
- try {
485
- while (!aborted) {
486
- const { done, value } = await reader.read();
487
- if (done) break;
488
- const newTotal = getBytesRef() + value.length;
489
- if (newTotal > MAX_OUTPUT_BYTES) {
490
- abort();
491
- throw new Error(`Output exceeded ${MAX_OUTPUT_BYTES} bytes`);
492
- }
493
- setBytesRef(newTotal);
494
- chunks.push(value);
495
- }
496
- } finally {
497
- await reader.cancel().catch(() => {});
498
- reader.releaseLock();
499
- }
500
- const combined = new Uint8Array(getBytesRef());
501
- let offset = 0;
502
- for (const chunk of chunks) {
503
- combined.set(chunk, offset);
504
- offset += chunk.length;
505
- }
506
- return new TextDecoder().decode(combined);
507
- };
508
-
509
465
  const timeoutPromise = new Promise<never>((_, reject) => {
510
466
  timeoutId = setTimeout(() => {
511
- abort();
467
+ weInitiatedKill = true;
512
468
  safeKill(proc, "SIGTERM");
513
469
  sigkillId = setTimeout(() => {
514
470
  safeKill(proc, "SIGKILL");
@@ -519,15 +475,10 @@ async function runInSubprocess<T>(method: string, args: unknown[]): Promise<T> {
519
475
  }, NATIVE_TIMEOUT_MS);
520
476
  });
521
477
 
522
- const workPromise = Promise.all([
523
- readStream(proc.stdout, stdoutChunks, () => stdoutBytes, (n) => { stdoutBytes = n; }),
524
- readStream(proc.stderr, stderrChunks, () => stderrBytes, (n) => { stderrBytes = n; }),
525
- proc.exited,
526
- ]);
478
+ const exitCode = await Promise.race([proc.exited, timeoutPromise]);
527
479
 
528
- const [stdout, stderr, exitCode] = await Promise.race([workPromise, timeoutPromise]);
480
+ if (timeoutId) clearTimeout(timeoutId);
529
481
 
530
- // Note: proc.killed is always true after exit in Bun (even for normal exits), so we only check signalCode
531
482
  if (weInitiatedKill || proc.signalCode) {
532
483
  throw new Error(
533
484
  `Subprocess '${method}' was killed (signal: ${proc.signalCode || "SIGTERM"})`
@@ -535,6 +486,7 @@ async function runInSubprocess<T>(method: string, args: unknown[]): Promise<T> {
535
486
  }
536
487
 
537
488
  if (exitCode !== 0) {
489
+ const stderr = await proc.stderr.text();
538
490
  let errorMsg = stderr || `Process exited with code ${exitCode}`;
539
491
  try {
540
492
  const parsed = JSON.parse(stderr);
@@ -543,11 +495,16 @@ async function runInSubprocess<T>(method: string, args: unknown[]): Promise<T> {
543
495
  throw new Error(`Subprocess '${method}' failed: ${errorMsg}`);
544
496
  }
545
497
 
498
+ if (!existsSync(outputFile)) {
499
+ throw new Error(`Subprocess '${method}' did not produce output file`);
500
+ }
501
+
546
502
  try {
547
- return JSON.parse(stdout) as T;
503
+ const output = readFileSync(outputFile, "utf-8");
504
+ return JSON.parse(output) as T;
548
505
  } catch (e) {
549
506
  throw new Error(
550
- `Failed to parse subprocess output: ${(e as Error).message}\nstdout: ${stdout.slice(0, 500)}`
507
+ `Failed to parse subprocess output: ${(e as Error).message}`
551
508
  );
552
509
  }
553
510
  } finally {
@@ -22,7 +22,7 @@ export interface UnifiedMessage {
22
22
  agent?: string;
23
23
  }
24
24
 
25
- export type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor" | "amp" | "droid" | "openclaw";
25
+ export type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor" | "amp" | "droid" | "openclaw" | "pi";
26
26
 
27
27
  /**
28
28
  * Convert Unix milliseconds timestamp to YYYY-MM-DD date string
package/src/submit.ts CHANGED
@@ -25,6 +25,7 @@ interface SubmitOptions {
25
25
  amp?: boolean;
26
26
  droid?: boolean;
27
27
  openclaw?: boolean;
28
+ pi?: boolean;
28
29
  since?: string;
29
30
  until?: string;
30
31
  year?: string;
@@ -50,7 +51,7 @@ interface SubmitResponse {
50
51
  details?: string[];
51
52
  }
52
53
 
53
- type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor" | "amp" | "droid" | "openclaw";
54
+ type SourceType = "opencode" | "claude" | "codex" | "gemini" | "cursor" | "amp" | "droid" | "openclaw" | "pi";
54
55
 
55
56
  async function checkGhCliExists(): Promise<boolean> {
56
57
  try {
@@ -193,7 +194,7 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
193
194
 
194
195
  console.log(pc.gray(" Scanning local session data..."));
195
196
 
196
- const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid || options.openclaw;
197
+ const hasFilter = options.opencode || options.claude || options.codex || options.gemini || options.cursor || options.amp || options.droid || options.openclaw || options.pi;
197
198
  let sources: SourceType[] | undefined;
198
199
  let includeCursor = true;
199
200
  if (hasFilter) {
@@ -206,6 +207,7 @@ export async function submit(options: SubmitOptions = {}): Promise<void> {
206
207
  if (options.amp) sources.push("amp");
207
208
  if (options.droid) sources.push("droid");
208
209
  if (options.openclaw) sources.push("openclaw");
210
+ if (options.pi) sources.push("pi");
209
211
  includeCursor = sources.includes("cursor");
210
212
  }
211
213
 
package/src/tui/App.tsx CHANGED
@@ -288,6 +288,7 @@ export function App(props: AppProps) {
288
288
  if (key.name === "6") { handleSourceToggle("amp"); return; }
289
289
  if (key.name === "7") { handleSourceToggle("droid"); return; }
290
290
  if (key.name === "8") { handleSourceToggle("openclaw"); return; }
291
+ if (key.name === "9") { handleSourceToggle("pi"); return; }
291
292
 
292
293
  if (key.name === "up") {
293
294
  if (activeTab() === "overview") {
@@ -14,11 +14,16 @@ const MIN_AUTO_REFRESH_MS = 30000;
14
14
  const MAX_AUTO_REFRESH_MS = 3600000;
15
15
  const DEFAULT_AUTO_REFRESH_MS = 60000;
16
16
 
17
+ const DEFAULT_NATIVE_TIMEOUT_MS = 300_000; // 5 minutes
18
+ const MIN_NATIVE_TIMEOUT_MS = 5_000; // 5 seconds
19
+ const MAX_NATIVE_TIMEOUT_MS = 3_600_000; // 1 hour
20
+
17
21
  export interface TokscaleSettings {
18
22
  colorPalette: string;
19
23
  autoRefreshEnabled?: boolean;
20
24
  autoRefreshMs?: number;
21
25
  includeUnusedModels?: boolean;
26
+ nativeTimeoutMs?: number;
22
27
  }
23
28
 
24
29
  function validateSettings(raw: unknown): TokscaleSettings {
@@ -27,6 +32,7 @@ function validateSettings(raw: unknown): TokscaleSettings {
27
32
  autoRefreshEnabled: false,
28
33
  autoRefreshMs: DEFAULT_AUTO_REFRESH_MS,
29
34
  includeUnusedModels: false,
35
+ nativeTimeoutMs: DEFAULT_NATIVE_TIMEOUT_MS,
30
36
  };
31
37
 
32
38
  if (!raw || typeof raw !== "object") return defaults;
@@ -43,7 +49,12 @@ function validateSettings(raw: unknown): TokscaleSettings {
43
49
 
44
50
  const includeUnusedModels = typeof obj.includeUnusedModels === "boolean" ? obj.includeUnusedModels : defaults.includeUnusedModels;
45
51
 
46
- return { colorPalette, autoRefreshEnabled, autoRefreshMs, includeUnusedModels };
52
+ let nativeTimeoutMs = defaults.nativeTimeoutMs;
53
+ if (typeof obj.nativeTimeoutMs === "number" && Number.isFinite(obj.nativeTimeoutMs)) {
54
+ nativeTimeoutMs = Math.min(MAX_NATIVE_TIMEOUT_MS, Math.max(MIN_NATIVE_TIMEOUT_MS, obj.nativeTimeoutMs));
55
+ }
56
+
57
+ return { colorPalette, autoRefreshEnabled, autoRefreshMs, includeUnusedModels, nativeTimeoutMs };
47
58
  }
48
59
 
49
60
  interface CachedTUIData {
@@ -66,7 +77,7 @@ export function loadSettings(): TokscaleSettings {
66
77
  }
67
78
  } catch {
68
79
  }
69
- return { colorPalette: "blue", autoRefreshEnabled: false, autoRefreshMs: DEFAULT_AUTO_REFRESH_MS, includeUnusedModels: false };
80
+ return { colorPalette: "blue", autoRefreshEnabled: false, autoRefreshMs: DEFAULT_AUTO_REFRESH_MS, includeUnusedModels: false, nativeTimeoutMs: DEFAULT_NATIVE_TIMEOUT_MS };
70
81
  }
71
82
 
72
83
  export function saveSettings(updates: Partial<TokscaleSettings>): void {
@@ -158,8 +158,8 @@ async function loadData(
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")[], since, until, year })
162
- : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, openclawCount: 0, processingTimeMs: 0 } as ParsedMessages),
161
+ ? parseLocalSourcesAsync({ sources: localSources as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid" | "openclaw" | "pi")[], since, until, year })
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
 
165
165
  const cursorSync = phase1Results[0].status === "fulfilled"
@@ -184,6 +184,7 @@ async function loadData(
184
184
  ampCount: 0,
185
185
  droidCount: 0,
186
186
  openclawCount: 0,
187
+ piCount: 0,
187
188
  processingTimeMs: 0,
188
189
  };
189
190
 
@@ -2,7 +2,7 @@ import type { ColorPaletteName } from "../config/themes.js";
2
2
 
3
3
  export type TabType = "overview" | "model" | "daily" | "stats";
4
4
  export type SortType = "cost" | "tokens" | "date";
5
- export type SourceType = "opencode" | "claude" | "codex" | "cursor" | "gemini" | "amp" | "droid" | "openclaw";
5
+ export type SourceType = "opencode" | "claude" | "codex" | "cursor" | "gemini" | "amp" | "droid" | "openclaw" | "pi";
6
6
 
7
7
  export type { ColorPaletteName };
8
8
 
@@ -163,7 +163,8 @@ export const SOURCE_LABELS: Record<SourceType, string> = {
163
163
  amp: "AM",
164
164
  droid: "DR",
165
165
  openclaw: "CL",
166
+ pi: "PI",
166
167
  } as const;
167
168
 
168
169
  export const TABS: readonly TabType[] = ["overview", "model", "daily", "stats"] as const;
169
- export const ALL_SOURCES: readonly SourceType[] = ["opencode", "claude", "codex", "cursor", "gemini", "amp", "droid", "openclaw"] as const;
170
+ export const ALL_SOURCES: readonly SourceType[] = ["opencode", "claude", "codex", "cursor", "gemini", "amp", "droid", "openclaw", "pi"] as const;
@@ -61,6 +61,7 @@ export const SOURCE_COLORS: Record<SourceType, string> = {
61
61
  amp: "#EC4899",
62
62
  droid: "#10b981",
63
63
  openclaw: "#ef4444",
64
+ pi: "#f97316",
64
65
  };
65
66
 
66
67
  export function getSourceColor(source: SourceType | string): string {
@@ -70,5 +71,6 @@ export function getSourceColor(source: SourceType | string): string {
70
71
  export function getSourceDisplayName(source: string): string {
71
72
  if (source === "droid") return "Droid";
72
73
  if (source === "openclaw") return "OpenClaw";
74
+ if (source === "pi") return "Pi";
73
75
  return source.charAt(0).toUpperCase() + source.slice(1);
74
76
  }
package/src/wrapped.ts CHANGED
@@ -65,6 +65,7 @@ const SOURCE_DISPLAY_NAMES: Record<string, string> = {
65
65
  amp: "Amp",
66
66
  droid: "Droid",
67
67
  openclaw: "OpenClaw",
68
+ pi: "Pi",
68
69
  };
69
70
 
70
71
  const ASSETS_BASE_URL = "https://tokscale.ai/assets/logos";
@@ -97,6 +98,7 @@ const CLIENT_LOGO_URLS: Record<string, string> = {
97
98
  "Amp": `${ASSETS_BASE_URL}/amp.png`,
98
99
  "Droid": `${ASSETS_BASE_URL}/droid.png`,
99
100
  "OpenClaw": `${ASSETS_BASE_URL}/openclaw.png`,
101
+ "Pi": `${ASSETS_BASE_URL}/pi.png`,
100
102
  };
101
103
 
102
104
  const PROVIDER_LOGO_URLS: Record<string, string> = {
@@ -214,8 +216,8 @@ async function ensureFontsLoaded(): Promise<void> {
214
216
 
215
217
  async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
216
218
  const year = options.year || new Date().getFullYear().toString();
217
- const sources = options.sources || ["opencode", "claude", "codex", "gemini", "cursor", "amp", "droid", "openclaw"];
218
- const localSources = sources.filter(s => s !== "cursor") as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid" | "openclaw")[];
219
+ const sources = options.sources || ["opencode", "claude", "codex", "gemini", "cursor", "amp", "droid", "openclaw", "pi"];
220
+ const localSources = sources.filter(s => s !== "cursor") as ("opencode" | "claude" | "codex" | "gemini" | "amp" | "droid" | "openclaw" | "pi")[];
219
221
  const includeCursor = sources.includes("cursor");
220
222
 
221
223
  const since = `${year}-01-01`;
@@ -225,7 +227,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
225
227
  includeCursor && isCursorLoggedIn() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0, error: undefined }),
226
228
  localSources.length > 0
227
229
  ? parseLocalSourcesAsync({ sources: localSources, since, until, year })
228
- : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, openclawCount: 0, processingTimeMs: 0 } as ParsedMessages),
230
+ : Promise.resolve({ messages: [], opencodeCount: 0, claudeCount: 0, codexCount: 0, geminiCount: 0, ampCount: 0, droidCount: 0, openclawCount: 0, piCount: 0, processingTimeMs: 0 } as ParsedMessages),
229
231
  ]);
230
232
 
231
233
  const cursorSync = phase1Results[0].status === "fulfilled"
@@ -249,6 +251,7 @@ async function loadWrappedData(options: WrappedOptions): Promise<WrappedData> {
249
251
  ampCount: 0,
250
252
  droidCount: 0,
251
253
  openclawCount: 0,
254
+ piCount: 0,
252
255
  processingTimeMs: 0,
253
256
  };
254
257