codex-stats 1.4.2__tar.gz → 1.5.0__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.
Files changed (31) hide show
  1. codex_stats-1.5.0/PKG-INFO +111 -0
  2. codex_stats-1.5.0/README.md +84 -0
  3. {codex_stats-1.4.2 → codex_stats-1.5.0}/pyproject.toml +1 -1
  4. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/__init__.py +1 -1
  5. codex_stats-1.5.0/src/codex_stats/cli.py +263 -0
  6. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/display.py +757 -41
  7. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/models.py +44 -0
  8. codex_stats-1.5.0/src/codex_stats.egg-info/PKG-INFO +111 -0
  9. codex_stats-1.5.0/tests/test_cli.py +50 -0
  10. {codex_stats-1.4.2 → codex_stats-1.5.0}/tests/test_metrics.py +84 -4
  11. codex_stats-1.4.2/PKG-INFO +0 -308
  12. codex_stats-1.4.2/README.md +0 -281
  13. codex_stats-1.4.2/src/codex_stats/cli.py +0 -667
  14. codex_stats-1.4.2/src/codex_stats.egg-info/PKG-INFO +0 -308
  15. codex_stats-1.4.2/tests/test_cli.py +0 -227
  16. {codex_stats-1.4.2 → codex_stats-1.5.0}/LICENSE +0 -0
  17. {codex_stats-1.4.2 → codex_stats-1.5.0}/setup.cfg +0 -0
  18. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/__main__.py +0 -0
  19. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/completions.py +0 -0
  20. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/config.py +0 -0
  21. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/ingest.py +0 -0
  22. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/metrics.py +0 -0
  23. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/otel.py +0 -0
  24. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/transfer.py +0 -0
  25. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats/watch_state.py +0 -0
  26. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/SOURCES.txt +0 -0
  27. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/dependency_links.txt +0 -0
  28. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/entry_points.txt +0 -0
  29. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/requires.txt +0 -0
  30. {codex_stats-1.4.2 → codex_stats-1.5.0}/src/codex_stats.egg-info/top_level.txt +0 -0
  31. {codex_stats-1.4.2 → codex_stats-1.5.0}/tests/test_config.py +0 -0
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: codex-stats
3
+ Version: 1.5.0
4
+ Summary: Local AI usage observability CLI for Codex sessions.
5
+ Author: Vivek
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/vivek378521/codex-stats
8
+ Project-URL: Repository, https://github.com/vivek378521/codex-stats
9
+ Project-URL: Issues, https://github.com/vivek378521/codex-stats/issues
10
+ Keywords: codex,cli,analytics,sqlite,usage
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.2.2; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # codex-stats
29
+
30
+ `codex-stats` is a local analytics tool for Codex usage.
31
+
32
+ It reads your local Codex state from `~/.codex` and surfaces:
33
+
34
+ - a browser dashboard with day, week, month, and all-time tabs
35
+ - model and project breakdowns
36
+ - recent session history
37
+ - estimated token-based cost
38
+ - anomaly-aware usage insights and recommendations
39
+ - export for cross-device snapshots
40
+ - shareable SVG cards and browser PDF export from the dashboard
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pipx install codex-stats
46
+ ```
47
+
48
+ Or with `pip`:
49
+
50
+ ```bash
51
+ python3 -m pip install codex-stats
52
+ ```
53
+
54
+ ## Command Reference
55
+
56
+ - `codex-stats`
57
+ Generate a standalone dashboard HTML file and open it in the default browser.
58
+ - `codex-stats --output codex-stats-dashboard.html`
59
+ Write the dashboard HTML to a fixed path.
60
+ - `codex-stats --output codex-stats-dashboard.html --no-open`
61
+ Write the dashboard HTML without opening the browser.
62
+ - `codex-stats export codex-stats-export.json`
63
+ Export normalized local stats to JSON.
64
+ - `codex-stats export codex-stats-export.json --since 30d`
65
+ Export only a rolling window of recent sessions.
66
+
67
+ Inside the dashboard, use the action bar to:
68
+
69
+ - switch between `Day`, `Week`, `Month`, and `All Time`
70
+ - print the active tab to PDF
71
+ - download shareable SVG cards for summary, cost, focus, and project share
72
+
73
+ ## How It Works
74
+
75
+ `codex-stats` does not proxy or intercept Codex API traffic.
76
+
77
+ It reads local Codex artifacts, including:
78
+
79
+ - `state_5.sqlite` for session metadata
80
+ - rollout JSONL files for request and token snapshots
81
+
82
+ ## Notes
83
+
84
+ - Costs are estimates, not billing values.
85
+ - Output depends on local Codex file formats remaining compatible.
86
+ - `export --since Nd` limits snapshots to a rolling window before sharing.
87
+
88
+ ## Roadmap
89
+
90
+ The current priority list lives in [docs/roadmap.md](/Users/vivek/Desktop/Salad/codex_stats/docs/roadmap.md).
91
+
92
+ ## Shareable Assets
93
+
94
+ Share-ready assets now live under [docs/assets/codex-stats-share-card.svg](/Users/vivek/Desktop/Salad/codex_stats/docs/assets/codex-stats-share-card.svg), and the dashboard can generate a fresh SVG download set directly from your local usage data.
95
+
96
+ ## Development
97
+
98
+ For local development from the repo:
99
+
100
+ ```bash
101
+ python3 -m venv .venv
102
+ source .venv/bin/activate
103
+ python -m pip install -U pip setuptools
104
+ python -m pip install -e .
105
+ ```
106
+
107
+ Run without installing:
108
+
109
+ ```bash
110
+ PYTHONPATH=src python3 -m codex_stats
111
+ ```
@@ -0,0 +1,84 @@
1
+ # codex-stats
2
+
3
+ `codex-stats` is a local analytics tool for Codex usage.
4
+
5
+ It reads your local Codex state from `~/.codex` and surfaces:
6
+
7
+ - a browser dashboard with day, week, month, and all-time tabs
8
+ - model and project breakdowns
9
+ - recent session history
10
+ - estimated token-based cost
11
+ - anomaly-aware usage insights and recommendations
12
+ - export for cross-device snapshots
13
+ - shareable SVG cards and browser PDF export from the dashboard
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pipx install codex-stats
19
+ ```
20
+
21
+ Or with `pip`:
22
+
23
+ ```bash
24
+ python3 -m pip install codex-stats
25
+ ```
26
+
27
+ ## Command Reference
28
+
29
+ - `codex-stats`
30
+ Generate a standalone dashboard HTML file and open it in the default browser.
31
+ - `codex-stats --output codex-stats-dashboard.html`
32
+ Write the dashboard HTML to a fixed path.
33
+ - `codex-stats --output codex-stats-dashboard.html --no-open`
34
+ Write the dashboard HTML without opening the browser.
35
+ - `codex-stats export codex-stats-export.json`
36
+ Export normalized local stats to JSON.
37
+ - `codex-stats export codex-stats-export.json --since 30d`
38
+ Export only a rolling window of recent sessions.
39
+
40
+ Inside the dashboard, use the action bar to:
41
+
42
+ - switch between `Day`, `Week`, `Month`, and `All Time`
43
+ - print the active tab to PDF
44
+ - download shareable SVG cards for summary, cost, focus, and project share
45
+
46
+ ## How It Works
47
+
48
+ `codex-stats` does not proxy or intercept Codex API traffic.
49
+
50
+ It reads local Codex artifacts, including:
51
+
52
+ - `state_5.sqlite` for session metadata
53
+ - rollout JSONL files for request and token snapshots
54
+
55
+ ## Notes
56
+
57
+ - Costs are estimates, not billing values.
58
+ - Output depends on local Codex file formats remaining compatible.
59
+ - `export --since Nd` limits snapshots to a rolling window before sharing.
60
+
61
+ ## Roadmap
62
+
63
+ The current priority list lives in [docs/roadmap.md](/Users/vivek/Desktop/Salad/codex_stats/docs/roadmap.md).
64
+
65
+ ## Shareable Assets
66
+
67
+ Share-ready assets now live under [docs/assets/codex-stats-share-card.svg](/Users/vivek/Desktop/Salad/codex_stats/docs/assets/codex-stats-share-card.svg), and the dashboard can generate a fresh SVG download set directly from your local usage data.
68
+
69
+ ## Development
70
+
71
+ For local development from the repo:
72
+
73
+ ```bash
74
+ python3 -m venv .venv
75
+ source .venv/bin/activate
76
+ python -m pip install -U pip setuptools
77
+ python -m pip install -e .
78
+ ```
79
+
80
+ Run without installing:
81
+
82
+ ```bash
83
+ PYTHONPATH=src python3 -m codex_stats
84
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codex-stats"
7
- version = "1.4.2"
7
+ version = "1.5.0"
8
8
  description = "Local AI usage observability CLI for Codex sessions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "1.4.2"
3
+ __version__ = "1.4.3"
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import tempfile
5
+ import webbrowser
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+
9
+ from .config import Paths, PricingConfig, load_pricing_config
10
+ from .display import format_dashboard_html
11
+ from .metrics import (
12
+ local_date,
13
+ summarize_compare_from_details,
14
+ summarize_costs_from_details,
15
+ summarize_daily_from_details,
16
+ summarize_details,
17
+ summarize_history_from_details,
18
+ summarize_insights_from_details,
19
+ summarize_projects_from_details,
20
+ summarize_top_sessions_from_details,
21
+ )
22
+ from .ingest import iter_session_details
23
+ from .models import DashboardData, DashboardWindow, SessionDetails
24
+ from .transfer import write_export
25
+
26
+
27
+ def build_parser() -> argparse.ArgumentParser:
28
+ parser = argparse.ArgumentParser(
29
+ prog="codex-stats",
30
+ description="Open a local Codex usage dashboard in the browser or export normalized stats.",
31
+ )
32
+ parser.add_argument(
33
+ "--output",
34
+ dest="dashboard_output",
35
+ help="Write dashboard HTML to a file instead of a temp file.",
36
+ )
37
+ parser.add_argument("--no-open", action="store_true", help="Write the dashboard without opening the browser.")
38
+ subparsers = parser.add_subparsers(dest="command")
39
+
40
+ export_parser = subparsers.add_parser("export", help="Export normalized local stats to JSON.")
41
+ export_parser.add_argument("output", help="Output JSON file.")
42
+ export_parser.add_argument("--since", help="Only export the last Nd of sessions, for example 30d.")
43
+ return parser
44
+
45
+
46
+ def main(argv: list[str] | None = None) -> int:
47
+ parser = build_parser()
48
+ args = parser.parse_args(argv)
49
+ paths = Paths.discover()
50
+
51
+ if args.command == "export":
52
+ output_path = write_export(paths, Path(args.output).expanduser(), since=args.since)
53
+ print(f"Exported stats to {output_path}")
54
+ return 0
55
+
56
+ if args.command is not None:
57
+ parser.print_help()
58
+ return 1
59
+
60
+ dashboard = _build_dashboard(paths)
61
+ output_path = _write_dashboard_output(format_dashboard_html(dashboard), args.dashboard_output)
62
+ print(f"Wrote dashboard to {output_path}")
63
+ if not args.no_open:
64
+ _open_report_in_browser(output_path)
65
+ print(f"Opened dashboard in browser: {output_path}")
66
+ return 0
67
+
68
+
69
+ def _build_dashboard(paths: Paths, now: datetime | None = None) -> DashboardData:
70
+ current_time = now or datetime.now().astimezone()
71
+ pricing = load_pricing_config(paths)
72
+ all_details = iter_session_details(paths)
73
+
74
+ today_details = _details_for_last_days(all_details, 1, current_time)
75
+ week_details = _details_for_last_days(all_details, 7, current_time)
76
+ month_details = _details_for_last_days(all_details, 30, current_time)
77
+
78
+ windows = [
79
+ _build_window(
80
+ key="day",
81
+ label="Day",
82
+ description="Today’s usage with a direct comparison to yesterday.",
83
+ current_details=today_details,
84
+ previous_details=_details_for_previous_window(all_details, 1, current_time),
85
+ current_label="today",
86
+ previous_label="yesterday",
87
+ trend_days=1,
88
+ all_details=all_details,
89
+ pricing=pricing,
90
+ now=current_time,
91
+ ),
92
+ _build_window(
93
+ key="week",
94
+ label="Week",
95
+ description="Rolling 7-day totals, recent trend, and the busiest sessions this week.",
96
+ current_details=week_details,
97
+ previous_details=_details_for_previous_window(all_details, 7, current_time),
98
+ current_label="last 7 days",
99
+ previous_label="previous 7 days",
100
+ trend_days=7,
101
+ all_details=all_details,
102
+ pricing=pricing,
103
+ now=current_time,
104
+ ),
105
+ _build_window(
106
+ key="month",
107
+ label="Month",
108
+ description="Rolling 30-day totals with project concentration and cost pressure.",
109
+ current_details=month_details,
110
+ previous_details=_details_for_previous_window(all_details, 30, current_time),
111
+ current_label="last 30 days",
112
+ previous_label="previous 30 days",
113
+ trend_days=30,
114
+ all_details=all_details,
115
+ pricing=pricing,
116
+ now=current_time,
117
+ ),
118
+ _build_all_time_window(
119
+ all_details=all_details,
120
+ pricing=pricing,
121
+ now=current_time,
122
+ ),
123
+ ]
124
+ return DashboardData(generated_at=current_time, windows=windows)
125
+
126
+
127
+ def _build_window(
128
+ *,
129
+ key: str,
130
+ label: str,
131
+ description: str,
132
+ current_details: list[SessionDetails],
133
+ previous_details: list[SessionDetails],
134
+ current_label: str,
135
+ previous_label: str,
136
+ trend_days: int,
137
+ all_details: list[SessionDetails],
138
+ pricing: PricingConfig,
139
+ now: datetime,
140
+ ) -> DashboardWindow:
141
+ summary = summarize_details(current_label, current_details, pricing)
142
+ comparison = summarize_compare_from_details(
143
+ current_details,
144
+ previous_details,
145
+ current_label=current_label,
146
+ previous_label=previous_label,
147
+ pricing=pricing,
148
+ )
149
+ insights = summarize_insights_from_details(current_details, pricing=pricing, month=summary, now=now)
150
+ costs = summarize_costs_from_details(
151
+ current_details,
152
+ pricing=pricing,
153
+ today=summary,
154
+ week=summary,
155
+ month=summary,
156
+ now=now,
157
+ )
158
+ history_source = current_details if current_details else all_details
159
+ return DashboardWindow(
160
+ key=key,
161
+ label=label,
162
+ description=description,
163
+ comparison_label=f"{current_label} vs {previous_label}",
164
+ summary=summary,
165
+ comparison=comparison,
166
+ projects=summarize_projects_from_details(current_details, pricing)[:10],
167
+ top_sessions=summarize_top_sessions_from_details(current_details, pricing, limit=10),
168
+ history=summarize_history_from_details(history_source, pricing, limit=10),
169
+ daily_points=summarize_daily_from_details(current_details, days=max(trend_days, 1), now=now, pricing=pricing),
170
+ costs=costs,
171
+ insights=insights,
172
+ )
173
+
174
+
175
+ def _build_all_time_window(
176
+ *,
177
+ all_details: list[SessionDetails],
178
+ pricing: PricingConfig,
179
+ now: datetime,
180
+ ) -> DashboardWindow:
181
+ summary = summarize_details("all time", all_details, pricing)
182
+ recent_details = _details_for_last_days(all_details, 30, now)
183
+ previous_details = _details_for_previous_window(all_details, 30, now)
184
+ comparison = summarize_compare_from_details(
185
+ recent_details,
186
+ previous_details,
187
+ current_label="recent 30 days",
188
+ previous_label="prior 30 days",
189
+ pricing=pricing,
190
+ )
191
+ costs = summarize_costs_from_details(all_details, pricing=pricing, now=now)
192
+ trend_days = _all_time_trend_days(all_details, now)
193
+ trend_details = _details_for_last_days(all_details, trend_days, now)
194
+ return DashboardWindow(
195
+ key="all",
196
+ label="All Time",
197
+ description=f"All recorded sessions. Trend charts cover the last {trend_days} days so the page stays readable.",
198
+ comparison_label="recent 30 days vs prior 30 days",
199
+ summary=summary,
200
+ comparison=comparison,
201
+ projects=summarize_projects_from_details(all_details, pricing)[:10],
202
+ top_sessions=summarize_top_sessions_from_details(all_details, pricing, limit=10),
203
+ history=summarize_history_from_details(all_details, pricing, limit=10),
204
+ daily_points=summarize_daily_from_details(trend_details, days=trend_days, now=now, pricing=pricing),
205
+ costs=costs,
206
+ insights=summarize_insights_from_details(all_details, pricing=pricing, month=summary, now=now),
207
+ )
208
+
209
+
210
+ def _details_for_last_days(details: list[SessionDetails], days: int, now: datetime) -> list[SessionDetails]:
211
+ safe_days = max(days, 1)
212
+ end_day = now.date()
213
+ start_day = end_day - timedelta(days=safe_days - 1)
214
+ return _filter_details_between(details, start_day, end_day, now)
215
+
216
+
217
+ def _details_for_previous_window(details: list[SessionDetails], days: int, now: datetime) -> list[SessionDetails]:
218
+ safe_days = max(days, 1)
219
+ end_day = now.date() - timedelta(days=safe_days)
220
+ start_day = end_day - timedelta(days=safe_days - 1)
221
+ return _filter_details_between(details, start_day, end_day, now)
222
+
223
+
224
+ def _filter_details_between(
225
+ details: list[SessionDetails],
226
+ start_day,
227
+ end_day,
228
+ now: datetime,
229
+ ) -> list[SessionDetails]:
230
+ return [
231
+ detail
232
+ for detail in details
233
+ if start_day <= local_date(detail.session.created_at, now.tzinfo) <= end_day
234
+ ]
235
+
236
+
237
+ def _all_time_trend_days(details: list[SessionDetails], now: datetime) -> int:
238
+ if not details:
239
+ return 30
240
+ newest_day = max(local_date(detail.session.created_at, now.tzinfo) for detail in details)
241
+ oldest_day = min(local_date(detail.session.created_at, now.tzinfo) for detail in details)
242
+ span_days = (newest_day - oldest_day).days + 1
243
+ return min(max(span_days, 1), 90)
244
+
245
+
246
+ def _write_dashboard_output(content: str, output: str | None) -> Path:
247
+ if output:
248
+ output_path = Path(output).expanduser()
249
+ else:
250
+ handle = tempfile.NamedTemporaryFile(prefix="codex-stats-dashboard-", suffix=".html", delete=False)
251
+ handle.close()
252
+ output_path = Path(handle.name)
253
+ output_path.parent.mkdir(parents=True, exist_ok=True)
254
+ output_path.write_text(content + ("" if content.endswith("\n") else "\n"), encoding="utf-8")
255
+ return output_path
256
+
257
+
258
+ def _open_report_in_browser(path: Path) -> None:
259
+ webbrowser.open(path.resolve().as_uri())
260
+
261
+
262
+ if __name__ == "__main__":
263
+ raise SystemExit(main())