@oh-my-pi/omp-stats 16.3.6 → 16.3.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/client/index.js +37 -37
  3. package/dist/types/aggregator.d.ts +1 -1
  4. package/dist/types/client/api.d.ts +1 -1
  5. package/dist/types/client/app/AppLayout.d.ts +2 -2
  6. package/dist/types/client/app/NavRail.d.ts +1 -1
  7. package/dist/types/client/app/RangeControl.d.ts +1 -1
  8. package/dist/types/client/app/TopBar.d.ts +2 -2
  9. package/dist/types/client/components/AgentTokenShare.d.ts +1 -1
  10. package/dist/types/client/components/models-table-shared.d.ts +2 -2
  11. package/dist/types/client/components/range-meta.d.ts +1 -1
  12. package/dist/types/client/data/useHashRoute.d.ts +2 -2
  13. package/dist/types/client/data/view-models.d.ts +1 -1
  14. package/dist/types/client/index.d.ts +1 -1
  15. package/dist/types/client/routes/BehaviorRoute.d.ts +1 -1
  16. package/dist/types/client/routes/CostsRoute.d.ts +1 -1
  17. package/dist/types/client/routes/ErrorsRoute.d.ts +1 -1
  18. package/dist/types/client/routes/GainRoute.d.ts +1 -1
  19. package/dist/types/client/routes/ModelsRoute.d.ts +1 -1
  20. package/dist/types/client/routes/OverviewRoute.d.ts +1 -1
  21. package/dist/types/client/routes/ProjectsRoute.d.ts +1 -1
  22. package/dist/types/client/routes/RequestsRoute.d.ts +1 -1
  23. package/dist/types/client/routes/ToolsRoute.d.ts +1 -1
  24. package/dist/types/client/routes/index.d.ts +9 -9
  25. package/dist/types/client/types.d.ts +2 -2
  26. package/dist/types/client/ui/MetricCluster.d.ts +1 -1
  27. package/dist/types/client/ui/index.d.ts +11 -11
  28. package/dist/types/db.d.ts +1 -1
  29. package/dist/types/gain-aggregator.d.ts +1 -1
  30. package/dist/types/index.d.ts +6 -6
  31. package/dist/types/parser.d.ts +1 -1
  32. package/dist/types/sync-worker.d.ts +1 -1
  33. package/dist/types/types.d.ts +2 -2
  34. package/package.json +4 -4
  35. package/src/gain-aggregator.ts +47 -17
  36. package/src/parser.ts +7 -10
@@ -1,4 +1,4 @@
1
- import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails, ToolDashboardStats } from "./types";
1
+ import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails, ToolDashboardStats } from "./types.js";
2
2
  /**
3
3
  * Progress event emitted after each session file is fully processed.
4
4
  * `current` is the number of files completed (skipped + parsed),
@@ -1,4 +1,4 @@
1
- import type { BehaviorDashboardStats, CostDashboardStats, FolderStats, GainDashboardStats, MessageStats, ModelDashboardStats, OverviewStats, RequestDetails, TimeRange, ToolDashboardStats } from "./types";
1
+ import type { BehaviorDashboardStats, CostDashboardStats, FolderStats, GainDashboardStats, MessageStats, ModelDashboardStats, OverviewStats, RequestDetails, TimeRange, ToolDashboardStats } from "./types.js";
2
2
  export declare class ApiError extends Error {
3
3
  status: number;
4
4
  endpoint: string;
@@ -1,6 +1,6 @@
1
1
  import type React from "react";
2
- import type { TimeRange } from "../types";
3
- import type { DashboardSection } from "./routes";
2
+ import type { TimeRange } from "../types.js";
3
+ import type { DashboardSection } from "./routes.js";
4
4
  export interface AppLayoutProps {
5
5
  activeSection: DashboardSection;
6
6
  onSectionChange: (section: DashboardSection) => void;
@@ -1,4 +1,4 @@
1
- import { type DashboardSection } from "./routes";
1
+ import { type DashboardSection } from "./routes.js";
2
2
  export interface NavRailProps {
3
3
  activeSection: DashboardSection;
4
4
  onSectionChange: (section: DashboardSection) => void;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface RangeControlProps {
3
3
  value: TimeRange;
4
4
  onChange: (value: TimeRange) => void;
@@ -1,5 +1,5 @@
1
- import type { TimeRange } from "../types";
2
- import type { DashboardSection } from "./routes";
1
+ import type { TimeRange } from "../types.js";
2
+ import type { DashboardSection } from "./routes.js";
3
3
  export interface TopBarProps {
4
4
  activeSection: DashboardSection;
5
5
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { AgentTypeStats } from "../types";
1
+ import type { AgentTypeStats } from "../types.js";
2
2
  export interface AgentTokenShareProps {
3
3
  stats: AgentTypeStats[];
4
4
  }
@@ -4,8 +4,8 @@
4
4
  * surface chrome, expand-row plumbing, theme palette, the mini-sparkline, and
5
5
  * the shared plugin/scale config consumed by multi-line detail charts.
6
6
  */
7
- import type { ChartTheme } from "./chart-shared";
8
- export { CHART_THEMES as TABLE_CHART_THEMES, MODEL_COLORS } from "./chart-shared";
7
+ import type { ChartTheme } from "./chart-shared.js";
8
+ export { CHART_THEMES as TABLE_CHART_THEMES, MODEL_COLORS } from "./chart-shared.js";
9
9
  export type TableChartTheme = ChartTheme;
10
10
  /** Style defaults for one line in a non-stacked detail chart. */
11
11
  export declare function lineSeriesStyle(color: string): {
@@ -3,7 +3,7 @@
3
3
  * counts, and x-axis date formatting in sync with the server-side bucketing
4
4
  * defined in `aggregator.ts`.
5
5
  */
6
- import type { TimeRange } from "../types";
6
+ import type { TimeRange } from "../types.js";
7
7
  export interface RangeMeta {
8
8
  /** Human label used in chart subtitles ("the last 24 hours"). */
9
9
  windowLabel: string;
@@ -1,5 +1,5 @@
1
- import type { DashboardSection } from "../app/routes";
2
- import type { TimeRange } from "../types";
1
+ import type { DashboardSection } from "../app/routes.js";
2
+ import type { TimeRange } from "../types.js";
3
3
  export declare function useHashRoute(): {
4
4
  section: DashboardSection;
5
5
  setSection: (newSection: DashboardSection) => void;
@@ -1,4 +1,4 @@
1
- import type { AgentType, AgentTypeStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, FolderStats, ModelPerformancePoint, TimeRange, ToolUsageStats } from "../types";
1
+ import type { AgentType, AgentTypeStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, FolderStats, ModelPerformancePoint, TimeRange, ToolUsageStats } from "../types.js";
2
2
  export interface AgentTokenSegment {
3
3
  agentType: AgentType;
4
4
  /** input + output + cache read + cache write — the displayed denominator. */
@@ -1,2 +1,2 @@
1
1
  import "./styles.css";
2
- import "./data/charts";
2
+ import "./data/charts.js";
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface BehaviorRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface CostsRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface ErrorsRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface GainRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface ModelsRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface OverviewRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface ProjectsRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface RequestsRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,4 +1,4 @@
1
- import type { TimeRange } from "../types";
1
+ import type { TimeRange } from "../types.js";
2
2
  export interface ToolsRouteProps {
3
3
  active: boolean;
4
4
  range: TimeRange;
@@ -1,9 +1,9 @@
1
- export * from "./BehaviorRoute";
2
- export * from "./CostsRoute";
3
- export * from "./ErrorsRoute";
4
- export * from "./GainRoute";
5
- export * from "./ModelsRoute";
6
- export * from "./OverviewRoute";
7
- export * from "./ProjectsRoute";
8
- export * from "./RequestsRoute";
9
- export * from "./ToolsRoute";
1
+ export * from "./BehaviorRoute.js";
2
+ export * from "./CostsRoute.js";
3
+ export * from "./ErrorsRoute.js";
4
+ export * from "./GainRoute.js";
5
+ export * from "./ModelsRoute.js";
6
+ export * from "./OverviewRoute.js";
7
+ export * from "./ProjectsRoute.js";
8
+ export * from "./RequestsRoute.js";
9
+ export * from "./ToolsRoute.js";
@@ -11,8 +11,8 @@
11
11
  * - `TimeRange`, `OverviewStats`, `ModelDashboardStats`,
12
12
  * `CostDashboardStats` are UI-only view shapes the server never produces.
13
13
  */
14
- import type { AgentTypeStats, AggregatedStats, CostTimeSeriesPoint, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint } from "../shared-types";
15
- export * from "../shared-types";
14
+ import type { AgentTypeStats, AggregatedStats, CostTimeSeriesPoint, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint } from "../shared-types.js";
15
+ export * from "../shared-types.js";
16
16
  export interface Usage {
17
17
  input: number;
18
18
  output: number;
@@ -1,4 +1,4 @@
1
- import type { AggregatedStats } from "../types";
1
+ import type { AggregatedStats } from "../types.js";
2
2
  export interface MetricClusterProps {
3
3
  stats: AggregatedStats;
4
4
  }
@@ -1,11 +1,11 @@
1
- export * from "./AsyncBoundary";
2
- export * from "./DataTable";
3
- export * from "./EmptyState";
4
- export * from "./ErrorState";
5
- export * from "./JsonBlock";
6
- export * from "./MetricCluster";
7
- export * from "./Panel";
8
- export * from "./RequestDrawer";
9
- export * from "./SegmentedControl";
10
- export * from "./Skeleton";
11
- export * from "./StatusPill";
1
+ export * from "./AsyncBoundary.js";
2
+ export * from "./DataTable.js";
3
+ export * from "./EmptyState.js";
4
+ export * from "./ErrorState.js";
5
+ export * from "./JsonBlock.js";
6
+ export * from "./MetricCluster.js";
7
+ export * from "./Panel.js";
8
+ export * from "./RequestDrawer.js";
9
+ export * from "./SegmentedControl.js";
10
+ export * from "./Skeleton.js";
11
+ export * from "./StatusPill.js";
@@ -1,5 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
- import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types";
2
+ import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
3
3
  /**
4
4
  * Initialize the database and create tables.
5
5
  */
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Missing files are treated as zero records — never an error.
8
8
  */
9
- import type { GainDashboardStats } from "./shared-types";
9
+ import type { GainDashboardStats } from "./shared-types.js";
10
10
  /**
11
11
  * Collapse conventional worktree sub-paths to their logical project root.
12
12
  *
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- export { getDashboardStats, getToolDashboardStats, getTotalMessageCount, type SyncOptions, type SyncProgress, smokeTestSyncWorker, syncAllSessions, } from "./aggregator";
3
- export { closeDb } from "./db";
4
- export { getGainDashboardStats } from "./gain-aggregator";
5
- export { startServer } from "./server";
6
- export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint, } from "./shared-types";
7
- export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types";
2
+ export { getDashboardStats, getToolDashboardStats, getTotalMessageCount, type SyncOptions, type SyncProgress, smokeTestSyncWorker, syncAllSessions, } from "./aggregator.js";
3
+ export { closeDb } from "./db.js";
4
+ export { getGainDashboardStats } from "./gain-aggregator.js";
5
+ export { startServer } from "./server.js";
6
+ export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint, } from "./shared-types.js";
7
+ export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types.js";
@@ -1,4 +1,4 @@
1
- import type { AgentType, MessageStats, SessionEntry, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types";
1
+ import type { AgentType, MessageStats, SessionEntry, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types.js";
2
2
  /**
3
3
  * Classify which agent produced a transcript from its path within the sessions
4
4
  * directory. Layout: `<sessionsDir>/<project>/<file>.jsonl` is the `main`
@@ -10,7 +10,7 @@
10
10
  * worker actually spawns and runs in compiled binaries (regression coverage
11
11
  * for issue #1011 / PR #1027, where the worker silently failed to load).
12
12
  */
13
- import { type ParseSessionResult } from "./parser";
13
+ import { type ParseSessionResult } from "./parser.js";
14
14
  export type SyncWorkerRequest = {
15
15
  kind?: "parse";
16
16
  sessionFile: string;
@@ -1,6 +1,6 @@
1
1
  import type { AssistantMessage, ServiceTier, ServiceTierByFamily, StopReason, Usage } from "@oh-my-pi/pi-ai";
2
- import type { AgentType } from "./shared-types";
3
- export * from "./shared-types";
2
+ import type { AgentType } from "./shared-types.js";
3
+ export * from "./shared-types.js";
4
4
  /**
5
5
  * Extracted stats from an assistant message.
6
6
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/omp-stats",
4
- "version": "16.3.6",
4
+ "version": "16.3.8",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,9 +39,9 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "16.3.6",
43
- "@oh-my-pi/pi-catalog": "16.3.6",
44
- "@oh-my-pi/pi-utils": "16.3.6",
42
+ "@oh-my-pi/pi-ai": "16.3.8",
43
+ "@oh-my-pi/pi-catalog": "16.3.8",
44
+ "@oh-my-pi/pi-utils": "16.3.8",
45
45
  "@tailwindcss/node": "^4.3.0",
46
46
  "chart.js": "^4.5.1",
47
47
  "date-fns": "^4.4.0",
@@ -7,6 +7,8 @@
7
7
  * Missing files are treated as zero records — never an error.
8
8
  */
9
9
 
10
+ import type { Stats } from "node:fs";
11
+ import * as fs from "node:fs/promises";
10
12
  import * as path from "node:path";
11
13
  import { getStatsDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
12
14
  import { getTimeRangeConfig } from "./aggregator";
@@ -149,37 +151,65 @@ async function readProjectsBySession(sessions: readonly string[]): Promise<Map<s
149
151
  return projectsBySession;
150
152
  }
151
153
 
154
+ interface SnapcompactCache {
155
+ key: string;
156
+ records: SnapcompactRecord[];
157
+ }
158
+
159
+ let snapcompactCache: SnapcompactCache | undefined;
160
+
152
161
  async function readSnapcompactRecords(cutoff: number | null, project: string | null): Promise<SnapcompactSets> {
153
162
  const filePath = path.join(path.dirname(getStatsDbPath()), "snapcompact-savings.jsonl");
154
- let text: string;
163
+
164
+ let stat: Stats;
155
165
  try {
156
- text = await Bun.file(filePath).text();
166
+ stat = await fs.stat(filePath);
157
167
  } catch (err) {
158
168
  if (isEnoent(err)) return { records: [], projects: new Set() };
159
- logger.debug("gain-aggregator: failed to read snapcompact-savings.jsonl", { err: String(err) });
169
+ logger.debug("gain-aggregator: failed to stat snapcompact-savings.jsonl", { err: String(err) });
160
170
  return { records: [], projects: new Set() };
161
171
  }
162
172
 
163
- const seen = new Set<string>();
164
- const parsed: SnapcompactRecord[] = [];
165
- for (const line of text.split("\n")) {
166
- if (!line.trim()) continue;
173
+ const cacheKey = `${filePath}:${stat.mtimeMs}:${stat.size}`;
174
+ let parsed: SnapcompactRecord[];
175
+ if (snapcompactCache?.key === cacheKey) {
176
+ parsed = snapcompactCache.records;
177
+ } else {
178
+ let text: string;
167
179
  try {
168
- const rec = JSON.parse(line) as SnapcompactRecord;
169
- if (cutoff !== null && rec.ts < cutoff) continue;
170
- const key = `${rec.session}:${rec.toolCallId}`;
171
- if (seen.has(key)) continue;
172
- seen.add(key);
173
- parsed.push(rec);
174
- } catch {
175
- /* skip malformed line */
180
+ text = await Bun.file(filePath).text();
181
+ } catch (readErr) {
182
+ if (isEnoent(readErr)) return { records: [], projects: new Set() };
183
+ logger.debug("gain-aggregator: failed to read snapcompact-savings.jsonl", { err: String(readErr) });
184
+ return { records: [], projects: new Set() };
176
185
  }
186
+
187
+ parsed = [];
188
+ for (const line of text.split("\n")) {
189
+ if (!line.trim()) continue;
190
+ try {
191
+ const rec = JSON.parse(line) as SnapcompactRecord;
192
+ parsed.push(rec);
193
+ } catch {
194
+ /* skip malformed line */
195
+ }
196
+ }
197
+ snapcompactCache = { key: cacheKey, records: parsed };
177
198
  }
178
199
 
179
- const projectsBySession = await readProjectsBySession(parsed.map(rec => rec.session));
200
+ const filtered = cutoff === null ? parsed : parsed.filter(rec => rec.ts >= cutoff);
201
+ const seen = new Set<string>();
202
+ const deduped: SnapcompactRecord[] = [];
203
+ for (const rec of filtered) {
204
+ const key = `${rec.session}:${rec.toolCallId}`;
205
+ if (seen.has(key)) continue;
206
+ seen.add(key);
207
+ deduped.push(rec);
208
+ }
209
+ const projectsBySession = await readProjectsBySession(deduped.map(rec => rec.session));
180
210
  const projects = new Set<string>();
181
211
  const records: SnapcompactRecord[] = [];
182
- for (const rec of parsed) {
212
+ for (const rec of deduped) {
183
213
  const sessionProjects = projectsBySession.get(rec.session);
184
214
  if (sessionProjects) {
185
215
  for (const sessionProject of sessionProjects) projects.add(sessionProject);
package/src/parser.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  type ServiceTierByFamily,
9
9
  type ToolResultMessage,
10
10
  } from "@oh-my-pi/pi-ai";
11
- import { getSessionsDir, isEnoent } from "@oh-my-pi/pi-utils";
11
+ import { getSessionsDir, isEnoent, readLines } from "@oh-my-pi/pi-utils";
12
12
  import type {
13
13
  AgentType,
14
14
  MessageStats,
@@ -432,19 +432,16 @@ export async function listAllSessionFiles(): Promise<string[]> {
432
432
  * Find a specific entry in a session file.
433
433
  */
434
434
  export async function getSessionEntry(sessionPath: string, entryId: string): Promise<SessionEntry | null> {
435
- let bytes: Uint8Array;
436
435
  try {
437
- bytes = await Bun.file(sessionPath).bytes();
436
+ for await (const line of readLines(Bun.file(sessionPath).stream())) {
437
+ const entry = parseJsonLine(line, 0, line.length);
438
+ if (entry && "id" in entry && entry.id === entryId) {
439
+ return entry;
440
+ }
441
+ }
438
442
  } catch (err) {
439
443
  if (isEnoent(err)) return null;
440
444
  throw err;
441
445
  }
442
-
443
- const { entries } = parseSessionEntriesLenient(bytes);
444
- for (const entry of entries) {
445
- if ("id" in entry && entry.id === entryId) {
446
- return entry;
447
- }
448
- }
449
446
  return null;
450
447
  }