@oh-my-pi/omp-stats 16.3.0 → 16.3.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.
@@ -1,4 +1,4 @@
1
- /*! tailwindcss v4.3.1 | MIT License | https://tailwindcss.com */
1
+ /*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
2
2
  @layer properties;
3
3
  @layer theme, base, components, utilities;
4
4
  @layer theme {
@@ -244,6 +244,9 @@
244
244
  .w-full {
245
245
  width: 100%;
246
246
  }
247
+ .max-w-\[280px\] {
248
+ max-width: 280px;
249
+ }
247
250
  .max-w-\[440px\] {
248
251
  max-width: 440px;
249
252
  }
@@ -1,4 +1,4 @@
1
- import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails } from "./types";
1
+ import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails, ToolDashboardStats } from "./types";
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),
@@ -79,4 +79,9 @@ export declare function getRequestDetails(id: number): Promise<RequestDetails |
79
79
  */
80
80
  export declare function getTotalMessageCount(): Promise<number>;
81
81
  export declare function getBehaviorDashboardStats(range?: string | null): Promise<BehaviorDashboardStats>;
82
+ /**
83
+ * Get the tools dashboard payload: per-tool totals, per-(tool, model)
84
+ * breakdown, and the call time series (bucketed like the model series).
85
+ */
86
+ export declare function getToolDashboardStats(range?: string | null): Promise<ToolDashboardStats>;
82
87
  export {};
@@ -1,4 +1,4 @@
1
- import type { BehaviorDashboardStats, CostDashboardStats, FolderStats, GainDashboardStats, MessageStats, ModelDashboardStats, OverviewStats, RequestDetails, TimeRange } from "./types";
1
+ import type { BehaviorDashboardStats, CostDashboardStats, FolderStats, GainDashboardStats, MessageStats, ModelDashboardStats, OverviewStats, RequestDetails, TimeRange, ToolDashboardStats } from "./types";
2
2
  export declare class ApiError extends Error {
3
3
  status: number;
4
4
  endpoint: string;
@@ -18,3 +18,4 @@ export declare function sync(signal?: AbortSignal): Promise<{
18
18
  export declare function getBehaviorDashboardStats(range?: TimeRange, signal?: AbortSignal): Promise<BehaviorDashboardStats>;
19
19
  export declare function getFolderStats(range?: TimeRange, signal?: AbortSignal): Promise<FolderStats[]>;
20
20
  export declare function getGainDashboardStats(range?: TimeRange, project?: string | null, signal?: AbortSignal): Promise<GainDashboardStats>;
21
+ export declare function getToolDashboardStats(range?: TimeRange, signal?: AbortSignal): Promise<ToolDashboardStats>;
@@ -1,5 +1,5 @@
1
1
  import type React from "react";
2
- export type DashboardSection = "overview" | "requests" | "errors" | "models" | "costs" | "behavior" | "projects" | "gain";
2
+ export type DashboardSection = "overview" | "requests" | "errors" | "models" | "tools" | "costs" | "behavior" | "projects" | "gain";
3
3
  export interface DashboardRoute {
4
4
  id: DashboardSection;
5
5
  label: string;
@@ -1,4 +1,4 @@
1
- import type { AgentType, AgentTypeStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, FolderStats, ModelPerformancePoint, TimeRange } from "../types";
1
+ import type { AgentType, AgentTypeStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, FolderStats, ModelPerformancePoint, TimeRange, ToolUsageStats } from "../types";
2
2
  export interface AgentTokenSegment {
3
3
  agentType: AgentType;
4
4
  /** input + output + cache read + cache write — the displayed denominator. */
@@ -57,3 +57,11 @@ export declare function buildCostSummary(costSeries: CostTimeSeriesPoint[]): Cos
57
57
  export declare function buildModelPerformanceLookup(points: ModelPerformancePoint[], range: TimeRange): Map<string, ModelPerformanceSeries>;
58
58
  export declare function buildBehaviorSummary(overall: BehaviorOverallStats, series: BehaviorTimeSeriesPoint[]): BehaviorSummaryView;
59
59
  export declare function buildFolderRows(folders: FolderStats[]): FolderRowView[];
60
+ /** Table row for the Tools route: usage stats plus derived rates/shares. */
61
+ export interface ToolRowView extends ToolUsageStats {
62
+ /** errors / calls (0 for zero calls). */
63
+ errorRate: number;
64
+ /** Calls relative to the busiest tool, 0-100, for the share bar. */
65
+ callsPercentage: number;
66
+ }
67
+ export declare function buildToolRows(tools: ToolUsageStats[]): ToolRowView[];
@@ -0,0 +1,7 @@
1
+ import type { TimeRange } from "../types";
2
+ export interface ToolsRouteProps {
3
+ active: boolean;
4
+ range: TimeRange;
5
+ refreshTrigger: number;
6
+ }
7
+ export declare function ToolsRoute({ active, range, refreshTrigger }: ToolsRouteProps): import("react").JSX.Element;
@@ -6,3 +6,4 @@ export * from "./ModelsRoute";
6
6
  export * from "./OverviewRoute";
7
7
  export * from "./ProjectsRoute";
8
8
  export * from "./RequestsRoute";
9
+ export * from "./ToolsRoute";
@@ -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, 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";
3
3
  /**
4
4
  * Initialize the database and create tables.
5
5
  */
@@ -112,3 +112,33 @@ export declare function getBehaviorOverall(cutoff?: number | null): BehaviorOver
112
112
  * user messages that never received an assistant reply.
113
113
  */
114
114
  export declare function getBehaviorByModel(cutoff?: number | null): BehaviorModelStats[];
115
+ /**
116
+ * Insert tool-call rows. Idempotent via UNIQUE(session_file, tool_call_id);
117
+ * the `WHERE NOT EXISTS` guard mirrors {@link insertMessageStats}: forked
118
+ * sessions deep-copy assistant entries (same `entry_id`, `timestamp`, and
119
+ * tool-call ids under a new file), so first-write-wins across the lineage
120
+ * keeps aggregates from double counting. Keyed on the assistant entry
121
+ * identity, not the call id alone — provider call ids are not a global
122
+ * namespace across unrelated sessions.
123
+ */
124
+ export declare function insertToolCalls(calls: ToolCallStats[]): number;
125
+ /**
126
+ * Attach result size / error flag to persisted tool-call rows. Results can
127
+ * land in a later incremental sync pass than the call that produced them, so
128
+ * this is an UPDATE keyed by (session_file, tool_call_id). The `IS NULL`
129
+ * guard makes re-syncs idempotent; rows skipped by the fork guard simply
130
+ * never match.
131
+ */
132
+ export declare function updateToolResults(links: ToolResultLink[]): number;
133
+ /**
134
+ * Get tool usage aggregated by tool name.
135
+ */
136
+ export declare function getToolStats(cutoff?: number): ToolUsageStats[];
137
+ /**
138
+ * Get tool usage aggregated by (tool, model, provider).
139
+ */
140
+ export declare function getToolStatsByModel(cutoff?: number): ToolModelStats[];
141
+ /**
142
+ * Get tool-call time series (one point per bucket per tool).
143
+ */
144
+ export declare function getToolTimeSeries(days?: number, cutoff?: number | null, bucketMs?: number): ToolTimeSeriesPoint[];
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- export { getDashboardStats, getTotalMessageCount, type SyncOptions, type SyncProgress, smokeTestSyncWorker, syncAllSessions, } from "./aggregator";
2
+ export { getDashboardStats, getToolDashboardStats, getTotalMessageCount, type SyncOptions, type SyncProgress, smokeTestSyncWorker, syncAllSessions, } from "./aggregator";
3
3
  export { closeDb } from "./db";
4
4
  export { getGainDashboardStats } from "./gain-aggregator";
5
5
  export { startServer } from "./server";
6
6
  export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint, } from "./shared-types";
7
- export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, } from "./types";
7
+ export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types";
@@ -1,4 +1,4 @@
1
- import type { AgentType, MessageStats, SessionEntry, UserMessageLink, UserMessageStats } from "./types";
1
+ import type { AgentType, MessageStats, SessionEntry, ToolCallStats, ToolResultLink, UserMessageLink, UserMessageStats } from "./types";
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`
@@ -30,6 +30,8 @@ export interface ParseSessionResult {
30
30
  stats: MessageStats[];
31
31
  userStats: UserMessageStats[];
32
32
  userLinks: UserMessageLink[];
33
+ toolCalls: ToolCallStats[];
34
+ toolResults: ToolResultLink[];
33
35
  newOffset: number;
34
36
  }
35
37
  export declare function parseSessionFile(sessionPath: string, fromOffset?: number): Promise<ParseSessionResult>;
@@ -251,3 +251,51 @@ export interface GainDashboardStats {
251
251
  /** All distinct projects seen in the data, for the selector. */
252
252
  projects: string[];
253
253
  }
254
+ /**
255
+ * Aggregated usage for a single tool over the active range.
256
+ *
257
+ * Token/cost fields are the *real* provider usage of the assistant turns that
258
+ * invoked the tool, split evenly across that turn's tool calls so the numbers
259
+ * stay additive (a turn with 3 calls contributes a third of its usage to each
260
+ * tool). Payload fields (`argsChars`/`resultChars`) are raw character counts
261
+ * of the serialized arguments and the text fed back into context — a size
262
+ * proxy, not provider-counted tokens.
263
+ */
264
+ export interface ToolUsageStats {
265
+ /** Tool name as recorded on the tool call. */
266
+ tool: string;
267
+ /** Number of tool calls. */
268
+ calls: number;
269
+ /** Calls whose result came back with `isError`. */
270
+ errors: number;
271
+ /** Serialized tool-call argument characters. */
272
+ argsChars: number;
273
+ /** Text characters of tool results fed back into context. */
274
+ resultChars: number;
275
+ /** Total provider tokens of invoking turns, attributed per call share. */
276
+ totalTokensShare: number;
277
+ /** Output tokens of invoking turns, attributed per call share. */
278
+ outputTokensShare: number;
279
+ /** Cost (USD) of invoking turns, attributed per call share. */
280
+ costShare: number;
281
+ /** Unix ms of the most recent call in range. */
282
+ lastUsed: number;
283
+ }
284
+ /** Per-(tool, model) breakdown with the same attribution as {@link ToolUsageStats}. */
285
+ export interface ToolModelStats extends ToolUsageStats {
286
+ model: string;
287
+ provider: string;
288
+ }
289
+ /** Tool-call time-series point (one bucket per tool). */
290
+ export interface ToolTimeSeriesPoint {
291
+ timestamp: number;
292
+ tool: string;
293
+ calls: number;
294
+ errors: number;
295
+ }
296
+ /** Complete tools dashboard payload. */
297
+ export interface ToolDashboardStats {
298
+ byTool: ToolUsageStats[];
299
+ byToolModel: ToolModelStats[];
300
+ series: ToolTimeSeriesPoint[];
301
+ }
@@ -121,3 +121,44 @@ export interface UserMessageLink {
121
121
  model: string;
122
122
  provider: string;
123
123
  }
124
+ /**
125
+ * One tool call extracted from an assistant message's `toolCall` content
126
+ * blocks. `callsInTurn` records how many calls that assistant turn contained
127
+ * so aggregation can split the turn's real provider usage evenly per call.
128
+ */
129
+ export interface ToolCallStats {
130
+ /** Session file path */
131
+ sessionFile: string;
132
+ /** Assistant-message entry ID that emitted the call */
133
+ entryId: string;
134
+ /** Provider-assigned tool call ID (unique within a session) */
135
+ toolCallId: string;
136
+ /** Folder/project path (extracted from session filename) */
137
+ folder: string;
138
+ /** Tool name */
139
+ toolName: string;
140
+ /** Model that emitted the call */
141
+ model: string;
142
+ /** Provider name */
143
+ provider: string;
144
+ /** Assistant-message timestamp (Unix ms) */
145
+ timestamp: number;
146
+ /** Which agent produced the call */
147
+ agentType: AgentType;
148
+ /** Total tool calls in the same assistant turn (>= 1) */
149
+ callsInTurn: number;
150
+ /** Serialized argument characters */
151
+ argsChars: number;
152
+ }
153
+ /**
154
+ * Result linkage emitted when the parser sees a `toolResult` message entry.
155
+ * Applied as an UPDATE on the persisted tool-call row — results can land in a
156
+ * later incremental sync pass than the call that produced them.
157
+ */
158
+ export interface ToolResultLink {
159
+ sessionFile: string;
160
+ toolCallId: string;
161
+ /** Text characters fed back into context */
162
+ resultChars: number;
163
+ isError: boolean;
164
+ }
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.0",
4
+ "version": "16.3.2",
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.0",
43
- "@oh-my-pi/pi-catalog": "16.3.0",
44
- "@oh-my-pi/pi-utils": "16.3.0",
42
+ "@oh-my-pi/pi-ai": "16.3.2",
43
+ "@oh-my-pi/pi-catalog": "16.3.2",
44
+ "@oh-my-pi/pi-utils": "16.3.2",
45
45
  "@tailwindcss/node": "^4.3.0",
46
46
  "chart.js": "^4.5.1",
47
47
  "date-fns": "^4.4.0",
package/src/aggregator.ts CHANGED
@@ -17,10 +17,15 @@ import {
17
17
  getStatsByFolder,
18
18
  getStatsByModel,
19
19
  getTimeSeries,
20
+ getToolStats,
21
+ getToolStatsByModel,
22
+ getToolTimeSeries,
20
23
  initDb,
21
24
  insertMessageStats,
25
+ insertToolCalls,
22
26
  insertUserMessageStats,
23
27
  setFileOffset,
28
+ updateToolResults,
24
29
  updateUserMessageLinks,
25
30
  } from "./db";
26
31
  import { getSessionEntry, listAllSessionFiles, type ParseSessionResult, parseSessionFile } from "./parser";
@@ -29,7 +34,7 @@ import type { SyncWorkerRequest, SyncWorkerResponse } from "./sync-worker";
29
34
  // hidden argv mode, so the compiled binary and npm bundle only need one
30
35
  // JavaScript entry. Standalone source `omp-stats` keeps using this package's
31
36
  // own sync-worker source file.
32
- import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails } from "./types";
37
+ import type { BehaviorDashboardStats, DashboardStats, MessageStats, RequestDetails, ToolDashboardStats } from "./types";
33
38
 
34
39
  /**
35
40
  * Apply a freshly parsed result to the database. Runs entirely on the
@@ -39,6 +44,8 @@ function applyParseResult(sessionFile: string, lastModified: number, result: Par
39
44
  if (result.stats.length > 0) insertMessageStats(result.stats);
40
45
  if (result.userStats.length > 0) insertUserMessageStats(result.userStats);
41
46
  if (result.userLinks.length > 0) updateUserMessageLinks(result.userLinks);
47
+ if (result.toolCalls.length > 0) insertToolCalls(result.toolCalls);
48
+ if (result.toolResults.length > 0) updateToolResults(result.toolResults);
42
49
  setFileOffset(sessionFile, result.newOffset, lastModified);
43
50
  return result.stats.length + result.userStats.length;
44
51
  }
@@ -478,3 +485,17 @@ export async function getBehaviorDashboardStats(range?: string | null): Promise<
478
485
  behaviorSeries: getBehaviorTimeSeries(cutoff),
479
486
  };
480
487
  }
488
+
489
+ /**
490
+ * Get the tools dashboard payload: per-tool totals, per-(tool, model)
491
+ * breakdown, and the call time series (bucketed like the model series).
492
+ */
493
+ export async function getToolDashboardStats(range?: string | null): Promise<ToolDashboardStats> {
494
+ await initDb();
495
+ const { modelSeriesDays, modelSeriesBucketMs, cutoff } = getTimeRangeConfig(range);
496
+ return {
497
+ byTool: getToolStats(cutoff ?? undefined),
498
+ byToolModel: getToolStatsByModel(cutoff ?? undefined),
499
+ series: getToolTimeSeries(modelSeriesDays, cutoff, modelSeriesBucketMs),
500
+ };
501
+ }
@@ -11,6 +11,7 @@ import {
11
11
  OverviewRoute,
12
12
  ProjectsRoute,
13
13
  RequestsRoute,
14
+ ToolsRoute,
14
15
  } from "./routes";
15
16
  import { RequestDrawer } from "./ui/RequestDrawer";
16
17
 
@@ -72,6 +73,8 @@ export default function App() {
72
73
  );
73
74
  case "models":
74
75
  return <ModelsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
76
+ case "tools":
77
+ return <ToolsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
75
78
  case "costs":
76
79
  return <CostsRoute active={isActive} range={range} refreshTrigger={refreshTrigger} />;
77
80
  case "behavior":
package/src/client/api.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  OverviewStats,
9
9
  RequestDetails,
10
10
  TimeRange,
11
+ ToolDashboardStats,
11
12
  } from "./types";
12
13
 
13
14
  const API_BASE = "/api";
@@ -92,3 +93,10 @@ export async function getGainDashboardStats(
92
93
  if (project) params.set("project", project);
93
94
  return fetchJson<GainDashboardStats>(`${API_BASE}/stats/gain?${params}`, { signal });
94
95
  }
96
+
97
+ export async function getToolDashboardStats(
98
+ range: TimeRange = "24h",
99
+ signal?: AbortSignal,
100
+ ): Promise<ToolDashboardStats> {
101
+ return fetchJson<ToolDashboardStats>(`${API_BASE}/stats/tools?range=${encodeURIComponent(range)}`, { signal });
102
+ }
@@ -1,4 +1,4 @@
1
- import { Activity, AlertCircle, Coins, Cpu, Folder, LayoutDashboard, Smile, TrendingUp } from "lucide-react";
1
+ import { Activity, AlertCircle, Coins, Cpu, Folder, LayoutDashboard, Smile, TrendingUp, Wrench } from "lucide-react";
2
2
  import type React from "react";
3
3
 
4
4
  export type DashboardSection =
@@ -6,6 +6,7 @@ export type DashboardSection =
6
6
  | "requests"
7
7
  | "errors"
8
8
  | "models"
9
+ | "tools"
9
10
  | "costs"
10
11
  | "behavior"
11
12
  | "projects"
@@ -39,6 +40,11 @@ export const routes: DashboardRoute[] = [
39
40
  label: "Models",
40
41
  icon: Cpu,
41
42
  },
43
+ {
44
+ id: "tools",
45
+ label: "Tools",
46
+ icon: Wrench,
47
+ },
42
48
  {
43
49
  id: "costs",
44
50
  label: "Costs",
@@ -7,6 +7,7 @@ const VALID_SECTIONS: DashboardSection[] = [
7
7
  "requests",
8
8
  "errors",
9
9
  "models",
10
+ "tools",
10
11
  "costs",
11
12
  "behavior",
12
13
  "projects",
@@ -8,6 +8,7 @@ import type {
8
8
  FolderStats,
9
9
  ModelPerformancePoint,
10
10
  TimeRange,
11
+ ToolUsageStats,
11
12
  } from "../types";
12
13
 
13
14
  /** Fixed display order for the agent-token-share breakdown. */
@@ -231,3 +232,20 @@ export function buildFolderRows(folders: FolderStats[]): FolderRowView[] {
231
232
  requestsPercentage: maxRequests > 0 ? (f.totalRequests / maxRequests) * 100 : 0,
232
233
  }));
233
234
  }
235
+
236
+ /** Table row for the Tools route: usage stats plus derived rates/shares. */
237
+ export interface ToolRowView extends ToolUsageStats {
238
+ /** errors / calls (0 for zero calls). */
239
+ errorRate: number;
240
+ /** Calls relative to the busiest tool, 0-100, for the share bar. */
241
+ callsPercentage: number;
242
+ }
243
+
244
+ export function buildToolRows(tools: ToolUsageStats[]): ToolRowView[] {
245
+ const maxCalls = tools.reduce((max, t) => Math.max(max, t.calls), 0);
246
+ return tools.map(t => ({
247
+ ...t,
248
+ errorRate: t.calls > 0 ? t.errors / t.calls : 0,
249
+ callsPercentage: maxCalls > 0 ? (t.calls / maxCalls) * 100 : 0,
250
+ }));
251
+ }