@oh-my-pi/omp-stats 12.18.3 → 12.19.2

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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "12.18.3",
4
+ "version": "12.19.2",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
- "author": "Can Bölük",
7
+ "author": "Can Boluk",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",
@@ -33,21 +33,21 @@
33
33
  "build": "bun run build.ts"
34
34
  },
35
35
  "dependencies": {
36
- "@oh-my-pi/pi-ai": "12.18.3",
37
- "@oh-my-pi/pi-utils": "12.18.3",
36
+ "@oh-my-pi/pi-ai": "12.19.2",
37
+ "@oh-my-pi/pi-utils": "12.19.2",
38
38
  "@tailwindcss/node": "4",
39
- "chart.js": "4.5.1",
40
- "date-fns": "^4.1.0",
41
- "lucide-react": "^0.564.0",
42
- "react": "^19.2.4",
43
- "react-chartjs-2": "5.3.1",
44
- "react-dom": "^19.2.4"
39
+ "chart.js": "^4.5",
40
+ "date-fns": "^4.1",
41
+ "lucide-react": "^0.575",
42
+ "react": "^19.2",
43
+ "react-chartjs-2": "^5.3",
44
+ "react-dom": "^19.2"
45
45
  },
46
46
  "devDependencies": {
47
- "@types/bun": "^1.3.9",
48
- "@types/react": "^19.2.14",
49
- "@types/react-dom": "^19.2.3",
50
- "postcss": "8.5.6",
47
+ "@types/bun": "^1.3",
48
+ "@types/react": "^19.2",
49
+ "@types/react-dom": "^19.2",
50
+ "postcss": "^8.5",
51
51
  "tailwindcss": "4"
52
52
  },
53
53
  "engines": {
@@ -0,0 +1 @@
1
+ declare module "*.css";
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { parseArgs } from "node:util";
4
+ import { formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils";
4
5
  import { getDashboardStats, getTotalMessageCount, syncAllSessions } from "./aggregator";
5
6
  import { closeDb } from "./db";
6
7
  import { startServer } from "./server";
@@ -19,15 +20,6 @@ export type {
19
20
  TimeSeriesPoint,
20
21
  } from "./types";
21
22
 
22
- /**
23
- * Format a number with appropriate suffix (K, M, etc.)
24
- */
25
- function formatNumber(n: number): string {
26
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
27
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
28
- return n.toFixed(0);
29
- }
30
-
31
23
  /**
32
24
  * Format cost in dollars.
33
25
  */
@@ -37,22 +29,6 @@ function formatCost(n: number): string {
37
29
  return `$${n.toFixed(2)}`;
38
30
  }
39
31
 
40
- /**
41
- * Format duration in ms to human-readable.
42
- */
43
- function formatDuration(ms: number | null): string {
44
- if (ms === null) return "-";
45
- if (ms < 1000) return `${ms.toFixed(0)}ms`;
46
- return `${(ms / 1000).toFixed(1)}s`;
47
- }
48
-
49
- /**
50
- * Format percentage.
51
- */
52
- function formatPercent(n: number): string {
53
- return `${(n * 100).toFixed(1)}%`;
54
- }
55
-
56
32
  /**
57
33
  * Print stats summary to console.
58
34
  */
@@ -68,8 +44,8 @@ async function printStats(): Promise<void> {
68
44
  console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
69
45
  console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
70
46
  console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
71
- console.log(` Avg Duration: ${formatDuration(overall.avgDuration)}`);
72
- console.log(` Avg TTFT: ${formatDuration(overall.avgTtft)}`);
47
+ console.log(` Avg Duration: ${overall.avgDuration !== null ? formatDuration(overall.avgDuration) : "-"}`);
48
+ console.log(` Avg TTFT: ${overall.avgTtft !== null ? formatDuration(overall.avgTtft) : "-"}`);
73
49
  if (overall.avgTokensPerSecond !== null) {
74
50
  console.log(` Avg Tokens/s: ${overall.avgTokensPerSecond.toFixed(1)}`);
75
51
  }
package/src/parser.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
4
+ import { isEnoent } from "@oh-my-pi/pi-utils";
4
5
  import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs";
5
6
  import type { MessageStats, SessionEntry, SessionMessageEntry } from "./types";
6
7
 
@@ -55,37 +56,30 @@ export async function parseSessionFile(
55
56
  sessionPath: string,
56
57
  fromOffset = 0,
57
58
  ): Promise<{ stats: MessageStats[]; newOffset: number }> {
58
- const file = Bun.file(sessionPath);
59
- const exists = await file.exists();
60
- if (!exists) {
61
- return { stats: [], newOffset: fromOffset };
59
+ let bytes: Uint8Array;
60
+ try {
61
+ bytes = await Bun.file(sessionPath).bytes();
62
+ } catch (err) {
63
+ if (isEnoent(err)) return { stats: [], newOffset: fromOffset };
64
+ throw err;
62
65
  }
63
66
 
64
- const text = await file.text();
65
- const lines = text.split("\n");
66
67
  const folder = extractFolderFromPath(sessionPath);
67
68
  const stats: MessageStats[] = [];
69
+ const start = Math.max(0, Math.min(fromOffset, bytes.length));
70
+ const unprocessed = bytes.subarray(start);
71
+ const { values, error, read } = Bun.JSONL.parseChunk(unprocessed);
72
+ if (error) throw error;
73
+ const entries = values as SessionEntry[];
68
74
 
69
- let currentOffset = 0;
70
- for (const line of lines) {
71
- const lineLength = line.length + 1; // +1 for newline
72
- if (line.trim()) {
73
- try {
74
- const entry = JSON.parse(line) as SessionEntry;
75
- if (currentOffset >= fromOffset && entry && isAssistantMessage(entry)) {
76
- const msgStats = extractStats(sessionPath, folder, entry);
77
- if (msgStats) {
78
- stats.push(msgStats);
79
- }
80
- }
81
- } catch {
82
- // Skip malformed JSONL lines
83
- }
75
+ for (const entry of entries) {
76
+ if (isAssistantMessage(entry)) {
77
+ const msgStats = extractStats(sessionPath, folder, entry);
78
+ if (msgStats) stats.push(msgStats);
84
79
  }
85
- currentOffset += lineLength;
86
80
  }
87
81
 
88
- return { stats, newOffset: currentOffset };
82
+ return { stats, newOffset: start + read };
89
83
  }
90
84
 
91
85
  /**
@@ -132,11 +126,13 @@ export async function listAllSessionFiles(): Promise<string[]> {
132
126
  * Find a specific entry in a session file.
133
127
  */
134
128
  export async function getSessionEntry(sessionPath: string, entryId: string): Promise<SessionEntry | null> {
135
- const file = Bun.file(sessionPath);
136
- if (!(await file.exists())) return null;
137
-
138
- const text = await file.bytes();
139
- const entries = Bun.JSONL.parse(text) as SessionEntry[];
129
+ let entries: SessionEntry[];
130
+ try {
131
+ entries = Bun.JSONL.parse(await Bun.file(sessionPath).bytes()) as SessionEntry[];
132
+ } catch (err) {
133
+ if (isEnoent(err)) return null;
134
+ throw err;
135
+ }
140
136
 
141
137
  for (const entry of entries) {
142
138
  if ("id" in entry && entry.id === entryId) {
package/src/server.ts CHANGED
@@ -10,7 +10,13 @@ import {
10
10
  getTotalMessageCount,
11
11
  syncAllSessions,
12
12
  } from "./aggregator";
13
- import { EMBEDDED_CLIENT_ARCHIVE_TAR_GZ_BASE64 } from "./embedded-client.generated";
13
+ import embeddedClientArchiveTxt from "./embedded-client.generated.txt";
14
+
15
+ const getEmbeddedClientArchive = (() => {
16
+ const txt = embeddedClientArchiveTxt.replaceAll(/[\s\r\n]/g, "").trim();
17
+ if (!txt) return null;
18
+ return () => Buffer.from(txt, "base64");
19
+ })();
14
20
 
15
21
  const CLIENT_DIR = path.join(import.meta.dir, "client");
16
22
  const STATIC_DIR = path.join(import.meta.dir, "..", "dist", "client");
@@ -30,8 +36,7 @@ function sanitizeArchivePath(archivePath: string): string | null {
30
36
  return normalized;
31
37
  }
32
38
 
33
- async function extractEmbeddedClientArchive(outputDir: string): Promise<void> {
34
- const archiveBytes = Buffer.from(EMBEDDED_CLIENT_ARCHIVE_TAR_GZ_BASE64, "base64");
39
+ async function extractEmbeddedClientArchive(archiveBytes: Buffer, outputDir: string): Promise<void> {
35
40
  const archive = new Bun.Archive(archiveBytes);
36
41
  const files = await archive.files();
37
42
  const extractRoot = path.resolve(outputDir);
@@ -49,13 +54,15 @@ async function extractEmbeddedClientArchive(outputDir: string): Promise<void> {
49
54
 
50
55
  async function getCompiledClientDir(): Promise<string> {
51
56
  if (!IS_BUN_COMPILED) return STATIC_DIR;
52
- if (!EMBEDDED_CLIENT_ARCHIVE_TAR_GZ_BASE64) {
57
+ if (compiledClientDirPromise) return compiledClientDirPromise;
58
+
59
+ const archiveBytes = getEmbeddedClientArchive?.();
60
+ if (!archiveBytes) {
53
61
  throw new Error("Compiled stats client bundle missing. Rebuild binary with embedded stats assets.");
54
62
  }
55
- if (compiledClientDirPromise) return compiledClientDirPromise;
56
63
 
57
64
  compiledClientDirPromise = (async () => {
58
- const bundleHash = Bun.hash(EMBEDDED_CLIENT_ARCHIVE_TAR_GZ_BASE64).toString(16);
65
+ const bundleHash = Bun.hash(archiveBytes).toString(16);
59
66
  const outputDir = path.join(COMPILED_CLIENT_DIR_ROOT, bundleHash);
60
67
  const markerPath = path.join(outputDir, "index.html");
61
68
  try {
@@ -65,7 +72,7 @@ async function getCompiledClientDir(): Promise<string> {
65
72
 
66
73
  await fs.rm(outputDir, { recursive: true, force: true });
67
74
  await fs.mkdir(outputDir, { recursive: true });
68
- await extractEmbeddedClientArchive(outputDir);
75
+ await extractEmbeddedClientArchive(archiveBytes, outputDir);
69
76
  return outputDir;
70
77
  })();
71
78
 
@@ -73,19 +80,26 @@ async function getCompiledClientDir(): Promise<string> {
73
80
  }
74
81
 
75
82
  async function getLatestMtime(dir: string): Promise<number> {
76
- let latest = 0;
77
83
  const entries = await fs.readdir(dir, { withFileTypes: true });
78
84
 
85
+ const promises = [];
79
86
  for (const entry of entries) {
80
87
  const fullPath = path.join(dir, entry.name);
81
88
  if (entry.isDirectory()) {
82
- latest = Math.max(latest, await getLatestMtime(fullPath));
89
+ promises.push(getLatestMtime(fullPath));
83
90
  } else if (entry.isFile()) {
84
- const stats = await fs.stat(fullPath);
85
- latest = Math.max(latest, stats.mtimeMs);
91
+ promises.push(fs.stat(fullPath).then(stats => stats.mtimeMs));
86
92
  }
87
93
  }
88
94
 
95
+ let latest = 0;
96
+ await Promise.allSettled(promises).then(results => {
97
+ for (const result of results) {
98
+ if (result.status === "fulfilled") {
99
+ latest = Math.max(latest, result.value);
100
+ }
101
+ }
102
+ });
89
103
  return latest;
90
104
  }
91
105