codex-stats 1.9.0__tar.gz → 1.11.2__tar.gz
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.
- {codex_stats-1.9.0/src/codex_stats.egg-info → codex_stats-1.11.2}/PKG-INFO +11 -5
- {codex_stats-1.9.0 → codex_stats-1.11.2}/README.md +1 -0
- codex_stats-1.11.2/pyproject.toml +77 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/cli.py +48 -2
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/display.py +344 -1
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/ingest.py +68 -5
- codex_stats-1.11.2/src/codex_stats/leaderboard.py +365 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/metrics.py +183 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/models.py +181 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/sources.py +252 -14
- codex_stats-1.11.2/src/codex_stats/tools.py +335 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2/src/codex_stats.egg-info}/PKG-INFO +11 -5
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats.egg-info/SOURCES.txt +4 -0
- codex_stats-1.11.2/tests/test_behavior.py +759 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/tests/test_cli.py +5 -1
- codex_stats-1.11.2/tests/test_leaderboard.py +454 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/tests/test_metrics.py +1 -1
- codex_stats-1.9.0/pyproject.toml +0 -46
- {codex_stats-1.9.0 → codex_stats-1.11.2}/LICENSE +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/setup.cfg +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/__init__.py +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/__main__.py +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats/config.py +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats.egg-info/dependency_links.txt +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats.egg-info/entry_points.txt +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats.egg-info/requires.txt +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/src/codex_stats.egg-info/top_level.txt +0 -0
- {codex_stats-1.9.0 → codex_stats-1.11.2}/tests/test_config.py +0 -0
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-stats
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: Local
|
|
3
|
+
Version: 1.11.2
|
|
4
|
+
Summary: Local-first usage dashboard and leaderboard for AI coding agent sessions: tokens, cost and activity across Codex, OpenCode, Claude Code and Hermes, read straight from the databases those tools already keep on your machine.
|
|
5
5
|
Author: Vivek
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/vivek378521/codex-stats
|
|
8
8
|
Project-URL: Repository, https://github.com/vivek378521/codex-stats
|
|
9
9
|
Project-URL: Issues, https://github.com/vivek378521/codex-stats/issues
|
|
10
|
-
|
|
10
|
+
Project-URL: Changelog, https://github.com/vivek378521/codex-stats/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Leaderboard, https://codex-stats-leaderboard.vercel.app
|
|
12
|
+
Project-URL: Funding, https://github.com/sponsors/vivek378521
|
|
13
|
+
Keywords: ai,analytics,claude,cli,codex,cost,dashboard,developer-tools,hermes,leaderboard,local-first,metrics,observability,opencode,privacy,reporting,sqlite,terminal,tokens,usage
|
|
11
14
|
Classifier: Development Status :: 3 - Alpha
|
|
12
15
|
Classifier: Environment :: Console
|
|
13
16
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier:
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
15
18
|
Classifier: Programming Language :: Python :: 3
|
|
16
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
25
|
Classifier: Topic :: Utilities
|
|
21
26
|
Requires-Python: >=3.11
|
|
22
27
|
Description-Content-Type: text/markdown
|
|
@@ -39,6 +44,7 @@ It reads local session data from every coding assistant installed on the machine
|
|
|
39
44
|
- per-tool cost overrides and a stacked per-tool token trend on the Overview
|
|
40
45
|
- anomaly-aware usage insights and recommendations
|
|
41
46
|
- file-level impact tracking: a "Most Edited Files" panel showing per-file edit counts and add/delete line totals parsed from Codex and Claude Code rollouts
|
|
47
|
+
- **tool behavior tracking**: a "Tool Behavior" panel showing the tools each agent actually called — normalized into shared categories (Read, Edit, Execute, Search, Web, Subagent, Plan), with per-tool failure rates, repeated-call detection, read/write ratio, and abandoned-turn counts, across every tool with recorded calls
|
|
42
48
|
- shareable JPG cards and browser PDF export from the dashboard
|
|
43
49
|
|
|
44
50
|
## Data Sources
|
|
@@ -12,6 +12,7 @@ It reads local session data from every coding assistant installed on the machine
|
|
|
12
12
|
- per-tool cost overrides and a stacked per-tool token trend on the Overview
|
|
13
13
|
- anomaly-aware usage insights and recommendations
|
|
14
14
|
- file-level impact tracking: a "Most Edited Files" panel showing per-file edit counts and add/delete line totals parsed from Codex and Claude Code rollouts
|
|
15
|
+
- **tool behavior tracking**: a "Tool Behavior" panel showing the tools each agent actually called — normalized into shared categories (Read, Edit, Execute, Search, Web, Subagent, Plan), with per-tool failure rates, repeated-call detection, read/write ratio, and abandoned-turn counts, across every tool with recorded calls
|
|
15
16
|
- shareable JPG cards and browser PDF export from the dashboard
|
|
16
17
|
|
|
17
18
|
## Data Sources
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codex-stats"
|
|
7
|
+
version = "1.11.2"
|
|
8
|
+
description = "Local-first usage dashboard and leaderboard for AI coding agent sessions: tokens, cost and activity across Codex, OpenCode, Claude Code and Hermes, read straight from the databases those tools already keep on your machine."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
# SPDX form (PEP 639). The License classifier is deliberately absent: setuptools
|
|
12
|
+
# rejects having both, and the old license = { text = ... } table is deprecated.
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Vivek" }
|
|
17
|
+
]
|
|
18
|
+
keywords = [
|
|
19
|
+
"ai",
|
|
20
|
+
"analytics",
|
|
21
|
+
"claude",
|
|
22
|
+
"cli",
|
|
23
|
+
"codex",
|
|
24
|
+
"cost",
|
|
25
|
+
"dashboard",
|
|
26
|
+
"developer-tools",
|
|
27
|
+
"hermes",
|
|
28
|
+
"leaderboard",
|
|
29
|
+
"local-first",
|
|
30
|
+
"metrics",
|
|
31
|
+
"observability",
|
|
32
|
+
"opencode",
|
|
33
|
+
"privacy",
|
|
34
|
+
"reporting",
|
|
35
|
+
"sqlite",
|
|
36
|
+
"terminal",
|
|
37
|
+
"tokens",
|
|
38
|
+
"usage",
|
|
39
|
+
]
|
|
40
|
+
classifiers = [
|
|
41
|
+
"Development Status :: 3 - Alpha",
|
|
42
|
+
"Environment :: Console",
|
|
43
|
+
"Intended Audience :: Developers",
|
|
44
|
+
"Operating System :: OS Independent",
|
|
45
|
+
"Programming Language :: Python :: 3",
|
|
46
|
+
"Programming Language :: Python :: 3.11",
|
|
47
|
+
"Programming Language :: Python :: 3.12",
|
|
48
|
+
"Programming Language :: Python :: 3.13",
|
|
49
|
+
"Topic :: Software Development :: Build Tools",
|
|
50
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
51
|
+
"Topic :: System :: Monitoring",
|
|
52
|
+
"Topic :: Utilities",
|
|
53
|
+
]
|
|
54
|
+
# "Typing :: Typed" is deliberately absent: it promises an inline py.typed
|
|
55
|
+
# marker, and the package does not ship one. Add both together if that changes.
|
|
56
|
+
|
|
57
|
+
[project.optional-dependencies]
|
|
58
|
+
dev = [
|
|
59
|
+
"build>=1.2.2",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[project.scripts]
|
|
63
|
+
codex-stats = "codex_stats.cli:main"
|
|
64
|
+
|
|
65
|
+
[project.urls]
|
|
66
|
+
Homepage = "https://github.com/vivek378521/codex-stats"
|
|
67
|
+
Repository = "https://github.com/vivek378521/codex-stats"
|
|
68
|
+
Issues = "https://github.com/vivek378521/codex-stats/issues"
|
|
69
|
+
Changelog = "https://github.com/vivek378521/codex-stats/blob/main/CHANGELOG.md"
|
|
70
|
+
Leaderboard = "https://codex-stats-leaderboard.vercel.app"
|
|
71
|
+
Funding = "https://github.com/sponsors/vivek378521"
|
|
72
|
+
|
|
73
|
+
[tool.setuptools]
|
|
74
|
+
package-dir = { "" = "src" }
|
|
75
|
+
|
|
76
|
+
[tool.setuptools.packages.find]
|
|
77
|
+
where = ["src"]
|
|
@@ -8,10 +8,12 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
from .config import Paths, PricingConfig, load_pricing_config
|
|
10
10
|
from .display import format_dashboard_html
|
|
11
|
+
from .leaderboard import LeaderboardConfig, LeaderboardSubmitServer
|
|
11
12
|
from .metrics import (
|
|
12
13
|
local_date,
|
|
13
14
|
summarize_activity_heatmap_from_details,
|
|
14
15
|
summarize_badges,
|
|
16
|
+
summarize_behavior_from_details,
|
|
15
17
|
summarize_compare_from_details,
|
|
16
18
|
summarize_costs_from_details,
|
|
17
19
|
summarize_daily_from_details,
|
|
@@ -49,13 +51,45 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
49
51
|
paths = Paths.discover()
|
|
50
52
|
|
|
51
53
|
dashboard = _build_dashboard(paths)
|
|
52
|
-
|
|
54
|
+
submit_server = _start_leaderboard(dashboard)
|
|
55
|
+
output_path = _write_dashboard_output(
|
|
56
|
+
format_dashboard_html(dashboard, leaderboard=_leaderboard_preview(submit_server))
|
|
57
|
+
)
|
|
53
58
|
print(f"Wrote dashboard to {output_path}")
|
|
54
59
|
_open_report_in_browser(output_path)
|
|
55
60
|
print(f"Opened dashboard in browser: {output_path}")
|
|
61
|
+
if submit_server is None:
|
|
62
|
+
return 0
|
|
63
|
+
print("Leaderboard enabled: use Submit to Leaderboard in the dashboard. Press Ctrl+C to stop.")
|
|
64
|
+
try:
|
|
65
|
+
submit_server.wait()
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
print("\nLeaderboard submit endpoint stopped.")
|
|
68
|
+
finally:
|
|
69
|
+
submit_server.close()
|
|
56
70
|
return 0
|
|
57
71
|
|
|
58
72
|
|
|
73
|
+
def _start_leaderboard(dashboard: DashboardData) -> LeaderboardSubmitServer | None:
|
|
74
|
+
"""Bring up the loopback submit endpoint when the leaderboard is configured.
|
|
75
|
+
|
|
76
|
+
Binding here, before the dashboard is rendered, is what lets the generated
|
|
77
|
+
HTML carry a URL that is already live.
|
|
78
|
+
"""
|
|
79
|
+
config = LeaderboardConfig.from_env()
|
|
80
|
+
if config is None:
|
|
81
|
+
return None
|
|
82
|
+
server = LeaderboardSubmitServer(config, dashboard)
|
|
83
|
+
server.start()
|
|
84
|
+
return server
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _leaderboard_preview(server: LeaderboardSubmitServer | None) -> dict[str, object] | None:
|
|
88
|
+
if server is None:
|
|
89
|
+
return None
|
|
90
|
+
return server.preview()
|
|
91
|
+
|
|
92
|
+
|
|
59
93
|
def _build_dashboard(paths: Paths, now: datetime | None = None) -> DashboardData:
|
|
60
94
|
current_time = now or datetime.now().astimezone()
|
|
61
95
|
pricing = load_pricing_config(paths)
|
|
@@ -197,6 +231,7 @@ def _build_window(
|
|
|
197
231
|
daily_points = summarize_daily_from_details(current_details, days=max(trend_days, 1), now=now, pricing=pricing)
|
|
198
232
|
activity_heatmap = summarize_activity_heatmap_from_details(current_details, timezone=now.tzinfo)
|
|
199
233
|
file_impact = summarize_files_from_details(current_details, limit=10)
|
|
234
|
+
behavior = summarize_behavior_from_details(current_details)
|
|
200
235
|
return DashboardWindow(
|
|
201
236
|
key=key,
|
|
202
237
|
label=label,
|
|
@@ -211,7 +246,14 @@ def _build_window(
|
|
|
211
246
|
costs=costs,
|
|
212
247
|
insights=insights,
|
|
213
248
|
activity_heatmap=activity_heatmap,
|
|
214
|
-
takeaways=summarize_takeaways(
|
|
249
|
+
takeaways=summarize_takeaways(
|
|
250
|
+
summary=summary,
|
|
251
|
+
comparison=comparison,
|
|
252
|
+
costs=costs,
|
|
253
|
+
insights=insights,
|
|
254
|
+
file_impact=file_impact,
|
|
255
|
+
behavior=behavior,
|
|
256
|
+
),
|
|
215
257
|
badges=summarize_badges(summary=summary, daily_points=daily_points, activity_heatmap=activity_heatmap),
|
|
216
258
|
expensive_session=summarize_expensive_session(current_details, pricing),
|
|
217
259
|
work_rhythm=summarize_work_rhythm(daily_points, activity_heatmap),
|
|
@@ -223,6 +265,7 @@ def _build_window(
|
|
|
223
265
|
limit=5,
|
|
224
266
|
),
|
|
225
267
|
file_impact=file_impact,
|
|
268
|
+
behavior=behavior,
|
|
226
269
|
tool_breakdown=(
|
|
227
270
|
summarize_source_breakdown_from_details(current_details, pricing)
|
|
228
271
|
if build_tool_breakdown
|
|
@@ -265,6 +308,7 @@ def _build_all_time_window(
|
|
|
265
308
|
daily_points = summarize_daily_from_details(trend_details, days=trend_days, now=now, pricing=pricing)
|
|
266
309
|
activity_heatmap = summarize_activity_heatmap_from_details(all_details, timezone=now.tzinfo)
|
|
267
310
|
file_impact = summarize_files_from_details(all_details, limit=10)
|
|
311
|
+
behavior = summarize_behavior_from_details(all_details)
|
|
268
312
|
return DashboardWindow(
|
|
269
313
|
key="all",
|
|
270
314
|
label="All Time",
|
|
@@ -285,6 +329,7 @@ def _build_all_time_window(
|
|
|
285
329
|
costs=costs,
|
|
286
330
|
insights=insights,
|
|
287
331
|
file_impact=file_impact,
|
|
332
|
+
behavior=behavior,
|
|
288
333
|
),
|
|
289
334
|
badges=summarize_badges(summary=summary, daily_points=daily_points, activity_heatmap=activity_heatmap),
|
|
290
335
|
expensive_session=summarize_expensive_session(all_details, pricing),
|
|
@@ -297,6 +342,7 @@ def _build_all_time_window(
|
|
|
297
342
|
limit=5,
|
|
298
343
|
),
|
|
299
344
|
file_impact=file_impact,
|
|
345
|
+
behavior=behavior,
|
|
300
346
|
tool_breakdown=(
|
|
301
347
|
summarize_source_breakdown_from_details(all_details, pricing)
|
|
302
348
|
if build_tool_breakdown
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import textwrap
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from html import escape
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
from .models import DashboardData, DashboardWindow, HeatmapCell, ProjectDrilldown
|
|
9
10
|
from .sources import source_label
|
|
@@ -16,7 +17,7 @@ SOURCE_CHART_COLORS: dict[str, str] = {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
20
|
+
def format_dashboard_html(dashboard: DashboardData, *, leaderboard: dict[str, Any] | None = None) -> str:
|
|
20
21
|
generated_at = dashboard.generated_at.strftime("%Y-%m-%d %H:%M %Z")
|
|
21
22
|
scopes = list(dashboard.scopes)
|
|
22
23
|
windows = list(dashboard.windows)
|
|
@@ -66,6 +67,16 @@ def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
|
66
67
|
},
|
|
67
68
|
separators=(",", ":"),
|
|
68
69
|
)
|
|
70
|
+
leaderboard_json = "null"
|
|
71
|
+
leaderboard_button = ""
|
|
72
|
+
leaderboard_dialog = ""
|
|
73
|
+
if leaderboard:
|
|
74
|
+
leaderboard_json = json.dumps(leaderboard, separators=(",", ":"))
|
|
75
|
+
leaderboard_button = (
|
|
76
|
+
'<button class="action-button" type="button" data-action="submit-leaderboard">'
|
|
77
|
+
"Submit to Leaderboard</button>"
|
|
78
|
+
)
|
|
79
|
+
leaderboard_dialog = _format_leaderboard_dialog()
|
|
69
80
|
return f"""<!DOCTYPE html>
|
|
70
81
|
<html lang="en">
|
|
71
82
|
<head>
|
|
@@ -450,6 +461,13 @@ def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
|
450
461
|
font-size: 0.92rem;
|
|
451
462
|
line-height: 1.45;
|
|
452
463
|
}}
|
|
464
|
+
.panel-hint {{
|
|
465
|
+
display: block;
|
|
466
|
+
margin: 16px 0 0;
|
|
467
|
+
color: var(--muted);
|
|
468
|
+
font-size: 0.86rem;
|
|
469
|
+
line-height: 1.5;
|
|
470
|
+
}}
|
|
453
471
|
.split {{
|
|
454
472
|
display: grid;
|
|
455
473
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -776,6 +794,83 @@ def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
|
776
794
|
.window.is-active {{
|
|
777
795
|
display: block !important;
|
|
778
796
|
}}
|
|
797
|
+
dialog {{
|
|
798
|
+
display: none !important;
|
|
799
|
+
}}
|
|
800
|
+
}}
|
|
801
|
+
.leaderboard-dialog {{
|
|
802
|
+
width: min(460px, calc(100vw - 32px));
|
|
803
|
+
padding: 0;
|
|
804
|
+
border: 1px solid var(--line);
|
|
805
|
+
border-radius: 24px;
|
|
806
|
+
background: var(--panel-strong);
|
|
807
|
+
color: var(--ink);
|
|
808
|
+
box-shadow: 0 30px 80px rgba(75, 56, 40, 0.28);
|
|
809
|
+
}}
|
|
810
|
+
.leaderboard-dialog::backdrop {{
|
|
811
|
+
background: rgba(31, 26, 23, 0.42);
|
|
812
|
+
}}
|
|
813
|
+
.leaderboard-form {{
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
gap: 10px;
|
|
817
|
+
padding: 26px 28px 22px;
|
|
818
|
+
}}
|
|
819
|
+
.leaderboard-form h2 {{
|
|
820
|
+
margin: 0;
|
|
821
|
+
font-size: 22px;
|
|
822
|
+
}}
|
|
823
|
+
.leaderboard-note {{
|
|
824
|
+
margin: 0 0 4px;
|
|
825
|
+
color: var(--muted);
|
|
826
|
+
font-size: 14px;
|
|
827
|
+
line-height: 1.5;
|
|
828
|
+
}}
|
|
829
|
+
.leaderboard-label {{
|
|
830
|
+
font-size: 12px;
|
|
831
|
+
letter-spacing: 0.1em;
|
|
832
|
+
text-transform: uppercase;
|
|
833
|
+
color: var(--muted);
|
|
834
|
+
font-weight: 700;
|
|
835
|
+
}}
|
|
836
|
+
.leaderboard-input {{
|
|
837
|
+
width: 100%;
|
|
838
|
+
padding: 12px 14px;
|
|
839
|
+
border: 1px solid var(--line);
|
|
840
|
+
border-radius: 14px;
|
|
841
|
+
background: #fff;
|
|
842
|
+
color: var(--ink);
|
|
843
|
+
font: inherit;
|
|
844
|
+
}}
|
|
845
|
+
.leaderboard-input:focus {{
|
|
846
|
+
outline: 2px solid var(--accent);
|
|
847
|
+
outline-offset: 1px;
|
|
848
|
+
}}
|
|
849
|
+
.leaderboard-preview {{
|
|
850
|
+
margin: 0;
|
|
851
|
+
padding: 12px 14px;
|
|
852
|
+
border-radius: 14px;
|
|
853
|
+
background: rgba(15, 118, 110, 0.08);
|
|
854
|
+
color: var(--ink);
|
|
855
|
+
font-size: 14px;
|
|
856
|
+
line-height: 1.5;
|
|
857
|
+
}}
|
|
858
|
+
.leaderboard-error {{
|
|
859
|
+
margin: 0;
|
|
860
|
+
color: #9f1239;
|
|
861
|
+
font-size: 14px;
|
|
862
|
+
}}
|
|
863
|
+
.leaderboard-status {{
|
|
864
|
+
margin: 0;
|
|
865
|
+
color: var(--accent);
|
|
866
|
+
font-size: 14px;
|
|
867
|
+
font-weight: 700;
|
|
868
|
+
}}
|
|
869
|
+
.leaderboard-actions {{
|
|
870
|
+
display: flex;
|
|
871
|
+
gap: 10px;
|
|
872
|
+
justify-content: flex-end;
|
|
873
|
+
margin-top: 6px;
|
|
779
874
|
}}
|
|
780
875
|
</style>
|
|
781
876
|
</head>
|
|
@@ -801,6 +896,7 @@ def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
|
801
896
|
<div class="actions">
|
|
802
897
|
<button class="action-button" type="button" data-action="copy-summary">Copy Summary</button>
|
|
803
898
|
<span class="copy-feedback" data-copy-feedback></span>
|
|
899
|
+
{leaderboard_button}
|
|
804
900
|
<div class="export-wrap">
|
|
805
901
|
<button class="action-button primary" type="button" data-action="toggle-export">Export</button>
|
|
806
902
|
<div class="export-menu" data-export-menu>
|
|
@@ -842,6 +938,7 @@ def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
|
842
938
|
{scope_sections}
|
|
843
939
|
<div class="footer">Generated by codex-stats</div>
|
|
844
940
|
</main>
|
|
941
|
+
{leaderboard_dialog}
|
|
845
942
|
<script>
|
|
846
943
|
const dashboardAssets = {assets_json};
|
|
847
944
|
const dashboardSummaries = {summaries_json};
|
|
@@ -1201,6 +1298,103 @@ def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
|
1201
1298
|
exportMenu?.classList.remove("is-open");
|
|
1202
1299
|
}}
|
|
1203
1300
|
}});
|
|
1301
|
+
const leaderboardConfig = {leaderboard_json};
|
|
1302
|
+
const leaderboardDialog = document.querySelector("[data-leaderboard-dialog]");
|
|
1303
|
+
if (leaderboardConfig && leaderboardDialog) {{
|
|
1304
|
+
const leaderboardInput = leaderboardDialog.querySelector("[data-leaderboard-username]");
|
|
1305
|
+
const leaderboardError = leaderboardDialog.querySelector("[data-leaderboard-error]");
|
|
1306
|
+
const leaderboardStatus = leaderboardDialog.querySelector("[data-leaderboard-status]");
|
|
1307
|
+
const leaderboardPreview = leaderboardDialog.querySelector("[data-leaderboard-preview]");
|
|
1308
|
+
const leaderboardConfirm = leaderboardDialog.querySelector("[data-leaderboard-confirm]");
|
|
1309
|
+
const leaderboardFormatter = new Intl.NumberFormat("en-US");
|
|
1310
|
+
|
|
1311
|
+
function formatLeaderboardCount(value) {{
|
|
1312
|
+
const count = Number(value) || 0;
|
|
1313
|
+
if (count >= 1000000000) {{
|
|
1314
|
+
return `${{(count / 1000000000).toFixed(2)}}B`;
|
|
1315
|
+
}}
|
|
1316
|
+
if (count >= 1000000) {{
|
|
1317
|
+
return `${{(count / 1000000).toFixed(2)}}M`;
|
|
1318
|
+
}}
|
|
1319
|
+
if (count >= 1000) {{
|
|
1320
|
+
return `${{(count / 1000).toFixed(1)}}K`;
|
|
1321
|
+
}}
|
|
1322
|
+
return leaderboardFormatter.format(count);
|
|
1323
|
+
}}
|
|
1324
|
+
|
|
1325
|
+
function setLeaderboardMessage(node, message) {{
|
|
1326
|
+
node.textContent = message;
|
|
1327
|
+
node.hidden = !message;
|
|
1328
|
+
}}
|
|
1329
|
+
|
|
1330
|
+
function renderLeaderboardPreview() {{
|
|
1331
|
+
const parts = (leaderboardConfig.clis ?? []).map(
|
|
1332
|
+
(item) => `${{item.label}} ${{formatLeaderboardCount(item.tokens)}}`
|
|
1333
|
+
);
|
|
1334
|
+
leaderboardPreview.textContent = parts.length
|
|
1335
|
+
? `Sending ${{formatLeaderboardCount(leaderboardConfig.totalTokens)}} tokens across ${{parts.join(", ")}}`
|
|
1336
|
+
: "No CLI usage has been recorded on this machine yet.";
|
|
1337
|
+
}}
|
|
1338
|
+
|
|
1339
|
+
async function submitToLeaderboard() {{
|
|
1340
|
+
const username = leaderboardInput.value.trim();
|
|
1341
|
+
if (!/^[A-Za-z0-9_-]{{1,20}}$/.test(username)) {{
|
|
1342
|
+
setLeaderboardMessage(leaderboardError, "Use 1-20 characters: letters, numbers, _ or -.");
|
|
1343
|
+
return;
|
|
1344
|
+
}}
|
|
1345
|
+
setLeaderboardMessage(leaderboardError, "");
|
|
1346
|
+
setLeaderboardMessage(leaderboardStatus, "");
|
|
1347
|
+
leaderboardConfirm.disabled = true;
|
|
1348
|
+
leaderboardConfirm.textContent = "Submitting…";
|
|
1349
|
+
try {{
|
|
1350
|
+
// Only the username is sent. Totals are read from local data by codex-stats.
|
|
1351
|
+
// text/plain keeps this a CORS-simple request, so there is no preflight.
|
|
1352
|
+
const response = await fetch(leaderboardConfig.submitUrl, {{
|
|
1353
|
+
method: "POST",
|
|
1354
|
+
headers: {{ "Content-Type": "text/plain" }},
|
|
1355
|
+
body: JSON.stringify({{ username }}),
|
|
1356
|
+
}});
|
|
1357
|
+
const payload = await response.json();
|
|
1358
|
+
if (!response.ok || !payload.ok) {{
|
|
1359
|
+
throw new Error(payload.error || `Request failed with ${{response.status}}`);
|
|
1360
|
+
}}
|
|
1361
|
+
setLeaderboardMessage(
|
|
1362
|
+
leaderboardStatus,
|
|
1363
|
+
`${{payload.updated}} You are #${{payload.rank}} with ${{formatLeaderboardCount(payload.totalTokens)}} tokens.`
|
|
1364
|
+
);
|
|
1365
|
+
leaderboardInput.disabled = true;
|
|
1366
|
+
leaderboardConfirm.textContent = "Submitted";
|
|
1367
|
+
}} catch (error) {{
|
|
1368
|
+
setLeaderboardMessage(leaderboardError, error.message);
|
|
1369
|
+
leaderboardConfirm.disabled = false;
|
|
1370
|
+
leaderboardConfirm.textContent = "Submit";
|
|
1371
|
+
}}
|
|
1372
|
+
}}
|
|
1373
|
+
|
|
1374
|
+
document.querySelector('[data-action="submit-leaderboard"]')?.addEventListener("click", () => {{
|
|
1375
|
+
renderLeaderboardPreview();
|
|
1376
|
+
setLeaderboardMessage(leaderboardError, "");
|
|
1377
|
+
setLeaderboardMessage(leaderboardStatus, "");
|
|
1378
|
+
leaderboardDialog.showModal();
|
|
1379
|
+
leaderboardInput.focus();
|
|
1380
|
+
leaderboardInput.select();
|
|
1381
|
+
}});
|
|
1382
|
+
leaderboardDialog.querySelector("[data-leaderboard-cancel]")?.addEventListener("click", () => {{
|
|
1383
|
+
leaderboardDialog.close();
|
|
1384
|
+
}});
|
|
1385
|
+
leaderboardConfirm?.addEventListener("click", submitToLeaderboard);
|
|
1386
|
+
leaderboardInput?.addEventListener("keydown", (event) => {{
|
|
1387
|
+
if (event.key === "Enter") {{
|
|
1388
|
+
event.preventDefault();
|
|
1389
|
+
submitToLeaderboard();
|
|
1390
|
+
}}
|
|
1391
|
+
}});
|
|
1392
|
+
leaderboardDialog.addEventListener("close", () => {{
|
|
1393
|
+
leaderboardInput.disabled = false;
|
|
1394
|
+
leaderboardConfirm.disabled = false;
|
|
1395
|
+
leaderboardConfirm.textContent = "Submit";
|
|
1396
|
+
}});
|
|
1397
|
+
}}
|
|
1204
1398
|
reflectActive();
|
|
1205
1399
|
</script>
|
|
1206
1400
|
</body>
|
|
@@ -1208,6 +1402,26 @@ def format_dashboard_html(dashboard: DashboardData) -> str:
|
|
|
1208
1402
|
"""
|
|
1209
1403
|
|
|
1210
1404
|
|
|
1405
|
+
def _format_leaderboard_dialog() -> str:
|
|
1406
|
+
return """ <dialog class="leaderboard-dialog" data-leaderboard-dialog>
|
|
1407
|
+
<form method="dialog" class="leaderboard-form">
|
|
1408
|
+
<h2>Submit to the leaderboard</h2>
|
|
1409
|
+
<p class="leaderboard-note">Pick a public name. Submitting again from this machine updates your existing record.</p>
|
|
1410
|
+
<label class="leaderboard-label" for="leaderboard-username">Username</label>
|
|
1411
|
+
<input class="leaderboard-input" id="leaderboard-username" name="username" type="text" maxlength="20"
|
|
1412
|
+
pattern="[A-Za-z0-9_-]{1,20}" autocomplete="off" spellcheck="false" placeholder="ada_lovelace"
|
|
1413
|
+
data-leaderboard-username>
|
|
1414
|
+
<p class="leaderboard-preview" data-leaderboard-preview></p>
|
|
1415
|
+
<p class="leaderboard-error" data-leaderboard-error hidden></p>
|
|
1416
|
+
<p class="leaderboard-status" data-leaderboard-status hidden></p>
|
|
1417
|
+
<div class="leaderboard-actions">
|
|
1418
|
+
<button class="action-button" type="button" data-leaderboard-cancel>Cancel</button>
|
|
1419
|
+
<button class="action-button primary" type="button" data-leaderboard-confirm>Submit</button>
|
|
1420
|
+
</div>
|
|
1421
|
+
</form>
|
|
1422
|
+
</dialog>"""
|
|
1423
|
+
|
|
1424
|
+
|
|
1211
1425
|
def format_dashboard_svg_assets(window: DashboardWindow, *, scope_label: str = "Codex") -> dict[str, str]:
|
|
1212
1426
|
title = f"{scope_label} Stats {window.label}"
|
|
1213
1427
|
return {
|
|
@@ -1430,6 +1644,8 @@ def _format_dashboard_window_section(
|
|
|
1430
1644
|
|
|
1431
1645
|
{_format_file_impact_panel(window)}
|
|
1432
1646
|
|
|
1647
|
+
{_format_behavior_panel(window)}
|
|
1648
|
+
|
|
1433
1649
|
<section class="panel">
|
|
1434
1650
|
<div class="section-header">
|
|
1435
1651
|
<div>
|
|
@@ -1784,6 +2000,133 @@ def _format_file_impact_panel(window: DashboardWindow) -> str:
|
|
|
1784
2000
|
"""
|
|
1785
2001
|
|
|
1786
2002
|
|
|
2003
|
+
def _format_behavior_panel(window: DashboardWindow) -> str:
|
|
2004
|
+
behavior = window.behavior
|
|
2005
|
+
if behavior is None or not behavior.supported or not behavior.total_calls:
|
|
2006
|
+
return f"""
|
|
2007
|
+
<section class="panel">
|
|
2008
|
+
<div class="section-header">
|
|
2009
|
+
<div>
|
|
2010
|
+
<p class="section-kicker">What the Agent Did</p>
|
|
2011
|
+
<h2>Tool Behavior</h2>
|
|
2012
|
+
</div>
|
|
2013
|
+
</div>
|
|
2014
|
+
{_format_empty_showcase("No tool calls recorded for this view.", "This is either a genuinely quiet range, or a scope that does not report tool use. Tool calls are read from Codex rollouts, Claude Code transcripts, the OpenCode part table, and the Hermes message log; switch to All Time to see which applies.")}
|
|
2015
|
+
</section>
|
|
2016
|
+
"""
|
|
2017
|
+
|
|
2018
|
+
category_svg = _svg_bar_chart(
|
|
2019
|
+
[(entry.label, float(entry.calls)) for entry in behavior.categories],
|
|
2020
|
+
bar_color="#0f766e",
|
|
2021
|
+
value_formatter=lambda value: f"{int(value):,}",
|
|
2022
|
+
empty_label="No tool categories recorded for this view.",
|
|
2023
|
+
)
|
|
2024
|
+
tool_svg = _svg_bar_chart(
|
|
2025
|
+
[(entry.name, float(entry.calls)) for entry in behavior.tools[:8]],
|
|
2026
|
+
bar_color="#7c3aed",
|
|
2027
|
+
value_formatter=lambda value: f"{int(value):,}",
|
|
2028
|
+
empty_label="No individual tools recorded for this view.",
|
|
2029
|
+
)
|
|
2030
|
+
rows = "".join(
|
|
2031
|
+
f"""
|
|
2032
|
+
<tr>
|
|
2033
|
+
<td>{escape(entry.name)}</td>
|
|
2034
|
+
<td>{escape(entry.category.title())}</td>
|
|
2035
|
+
<td>{entry.calls:,}</td>
|
|
2036
|
+
<td>{entry.errors:,}</td>
|
|
2037
|
+
<td>{entry.recovered_errors:,}</td>
|
|
2038
|
+
<td>{escape(_fmt_percent(entry.error_rate))}</td>
|
|
2039
|
+
</tr>
|
|
2040
|
+
"""
|
|
2041
|
+
for entry in behavior.tools[:10]
|
|
2042
|
+
)
|
|
2043
|
+
return f"""
|
|
2044
|
+
<section class="panel">
|
|
2045
|
+
<div class="section-header">
|
|
2046
|
+
<div>
|
|
2047
|
+
<p class="section-kicker">What the Agent Did</p>
|
|
2048
|
+
<h2>Tool Behavior</h2>
|
|
2049
|
+
</div>
|
|
2050
|
+
<div class="spotlight-kpis">
|
|
2051
|
+
<div class="spotlight-kpi"><strong>{behavior.total_calls:,}</strong><span>Tool calls</span></div>
|
|
2052
|
+
<div class="spotlight-kpi"><strong>{escape(_fmt_percent(behavior.error_rate))}</strong><span>Failed</span></div>
|
|
2053
|
+
<div class="spotlight-kpi"><strong>{escape(_fmt_percent(behavior.repeat_rate))}</strong><span>Repeated</span></div>
|
|
2054
|
+
<div class="spotlight-kpi"><strong>{escape(_fmt_behavior_percent(behavior.repeat_time_rate))}</strong><span>Repeated time</span></div>
|
|
2055
|
+
<div class="spotlight-kpi"><strong>{_fmt_behavior_ratio(behavior.read_write_ratio)}</strong><span>Read per edit</span></div>
|
|
2056
|
+
</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
<div class="chart-grid">
|
|
2059
|
+
<div class="chart-card">
|
|
2060
|
+
<h3>Calls by Category</h3>
|
|
2061
|
+
{category_svg}
|
|
2062
|
+
</div>
|
|
2063
|
+
<div class="chart-card">
|
|
2064
|
+
<h3>Most Used Tools</h3>
|
|
2065
|
+
{tool_svg}
|
|
2066
|
+
</div>
|
|
2067
|
+
<div class="chart-card">
|
|
2068
|
+
<h3>Failure Detail</h3>
|
|
2069
|
+
<div class="table-wrap">
|
|
2070
|
+
<table>
|
|
2071
|
+
<thead>
|
|
2072
|
+
<tr>
|
|
2073
|
+
<th>Tool</th>
|
|
2074
|
+
<th>Category</th>
|
|
2075
|
+
<th>Calls</th>
|
|
2076
|
+
<th>Failed</th>
|
|
2077
|
+
<th>Recovered</th>
|
|
2078
|
+
<th>Error rate</th>
|
|
2079
|
+
</tr>
|
|
2080
|
+
</thead>
|
|
2081
|
+
<tbody>{rows}</tbody>
|
|
2082
|
+
</table>
|
|
2083
|
+
</div>
|
|
2084
|
+
</div>
|
|
2085
|
+
</div>
|
|
2086
|
+
<p class="panel-hint">{escape(_format_behavior_note(behavior))}</p>
|
|
2087
|
+
</section>
|
|
2088
|
+
"""
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _format_behavior_note(behavior) -> str:
|
|
2092
|
+
sessions_noun = "session" if behavior.sessions_with_calls == 1 else "sessions"
|
|
2093
|
+
note = (
|
|
2094
|
+
"Categories are normalized across tools, so exec_command, Bash and bash all count "
|
|
2095
|
+
f"as Execute. Repeated calls are counted within a single session, across "
|
|
2096
|
+
f"{behavior.sessions_with_calls:,} {sessions_noun} with tool use."
|
|
2097
|
+
)
|
|
2098
|
+
if behavior.abandoned_turns:
|
|
2099
|
+
turn_noun = "turn was" if behavior.abandoned_turns == 1 else "turns were"
|
|
2100
|
+
note += f" {behavior.abandoned_turns} {turn_noun} abandoned before finishing."
|
|
2101
|
+
else:
|
|
2102
|
+
note += " Abandoned turns are only recorded where the source reports them (Codex)."
|
|
2103
|
+
if behavior.total_duration_ms:
|
|
2104
|
+
timed_noun = "call" if behavior.timed_calls == 1 else "calls"
|
|
2105
|
+
note += (
|
|
2106
|
+
f" Repeated time is the share of tool-call time spent on repeat work, measured over the "
|
|
2107
|
+
f"{behavior.timed_calls:,} timed {timed_noun} (OpenCode and Claude record durations; Codex does not)."
|
|
2108
|
+
)
|
|
2109
|
+
else:
|
|
2110
|
+
note += " This scope does not record tool-call durations, so repeated time is unavailable."
|
|
2111
|
+
note += " A recovered failure is one that was later retried to success in the same session; "
|
|
2112
|
+
note += "recovery is an exact command match, so a retry that changes the command is not counted."
|
|
2113
|
+
return note
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def _fmt_behavior_percent(value: float | None) -> str:
|
|
2117
|
+
if value is None:
|
|
2118
|
+
return "n/a"
|
|
2119
|
+
return f"{value * 100:.0f}%"
|
|
2120
|
+
|
|
2121
|
+
|
|
2122
|
+
def _fmt_behavior_ratio(value: float | None) -> str:
|
|
2123
|
+
if value is None:
|
|
2124
|
+
return "n/a"
|
|
2125
|
+
if value >= 10:
|
|
2126
|
+
return f"{value:.0f}"
|
|
2127
|
+
return f"{value:.1f}"
|
|
2128
|
+
|
|
2129
|
+
|
|
1787
2130
|
def _format_empty_showcase(title: str, detail: str) -> str:
|
|
1788
2131
|
return f"""
|
|
1789
2132
|
<div class="empty-showcase">
|