codex-stats 1.4.1__tar.gz → 1.4.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.4.1/src/codex_stats.egg-info → codex_stats-1.4.2}/PKG-INFO +5 -5
- {codex_stats-1.4.1 → codex_stats-1.4.2}/README.md +4 -4
- {codex_stats-1.4.1 → codex_stats-1.4.2}/pyproject.toml +1 -1
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/__init__.py +1 -1
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/cli.py +31 -3
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/display.py +138 -100
- {codex_stats-1.4.1 → codex_stats-1.4.2/src/codex_stats.egg-info}/PKG-INFO +5 -5
- {codex_stats-1.4.1 → codex_stats-1.4.2}/tests/test_cli.py +17 -4
- {codex_stats-1.4.1 → codex_stats-1.4.2}/tests/test_metrics.py +6 -3
- {codex_stats-1.4.1 → codex_stats-1.4.2}/LICENSE +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/setup.cfg +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/__main__.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/completions.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/config.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/ingest.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/metrics.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/models.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/otel.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/transfer.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats/watch_state.py +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats.egg-info/SOURCES.txt +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats.egg-info/dependency_links.txt +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats.egg-info/entry_points.txt +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats.egg-info/requires.txt +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/src/codex_stats.egg-info/top_level.txt +0 -0
- {codex_stats-1.4.1 → codex_stats-1.4.2}/tests/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-stats
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Local AI usage observability CLI for Codex sessions.
|
|
5
5
|
Author: Vivek
|
|
6
6
|
License: MIT
|
|
@@ -110,15 +110,15 @@ python3 -m pip install codex-stats
|
|
|
110
110
|
- `codex-stats report weekly --format html`
|
|
111
111
|
Generate a standalone HTML report for sharing, including inline charts.
|
|
112
112
|
- `codex-stats report weekly --format svg`
|
|
113
|
-
Generate a
|
|
113
|
+
Generate a bundle of smaller standalone SVG assets in the current working directory, including a summary card plus graph-focused assets.
|
|
114
114
|
- `codex-stats report weekly --project backend-api`
|
|
115
115
|
Generate a weekly report for one project.
|
|
116
116
|
- `codex-stats report weekly --format markdown --output weekly-report.md`
|
|
117
117
|
Write a formatted report to a file.
|
|
118
118
|
- `codex-stats report weekly --format html --output weekly-report.html`
|
|
119
119
|
Write a polished standalone HTML report to a file.
|
|
120
|
-
- `codex-stats report weekly --format svg --output weekly-report
|
|
121
|
-
Write
|
|
120
|
+
- `codex-stats report weekly --format svg --output weekly-report-assets`
|
|
121
|
+
Write the SVG asset bundle into the output directory you provide.
|
|
122
122
|
- `codex-stats report weekly --format html --render`
|
|
123
123
|
Generate an HTML report, write it to a temp file, and open it in the default browser.
|
|
124
124
|
- `codex-stats report weekly --format svg --render`
|
|
@@ -227,7 +227,7 @@ The current priority list lives in [docs/roadmap.md](/Users/vivek/Desktop/Salad/
|
|
|
227
227
|
|
|
228
228
|
## Shareable Assets
|
|
229
229
|
|
|
230
|
-
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 `report --format svg` can generate
|
|
230
|
+
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 `report --format svg` can generate a full bundle of report assets directly from local usage data.
|
|
231
231
|
|
|
232
232
|
## Watch Alerts
|
|
233
233
|
|
|
@@ -83,15 +83,15 @@ python3 -m pip install codex-stats
|
|
|
83
83
|
- `codex-stats report weekly --format html`
|
|
84
84
|
Generate a standalone HTML report for sharing, including inline charts.
|
|
85
85
|
- `codex-stats report weekly --format svg`
|
|
86
|
-
Generate a
|
|
86
|
+
Generate a bundle of smaller standalone SVG assets in the current working directory, including a summary card plus graph-focused assets.
|
|
87
87
|
- `codex-stats report weekly --project backend-api`
|
|
88
88
|
Generate a weekly report for one project.
|
|
89
89
|
- `codex-stats report weekly --format markdown --output weekly-report.md`
|
|
90
90
|
Write a formatted report to a file.
|
|
91
91
|
- `codex-stats report weekly --format html --output weekly-report.html`
|
|
92
92
|
Write a polished standalone HTML report to a file.
|
|
93
|
-
- `codex-stats report weekly --format svg --output weekly-report
|
|
94
|
-
Write
|
|
93
|
+
- `codex-stats report weekly --format svg --output weekly-report-assets`
|
|
94
|
+
Write the SVG asset bundle into the output directory you provide.
|
|
95
95
|
- `codex-stats report weekly --format html --render`
|
|
96
96
|
Generate an HTML report, write it to a temp file, and open it in the default browser.
|
|
97
97
|
- `codex-stats report weekly --format svg --render`
|
|
@@ -200,7 +200,7 @@ The current priority list lives in [docs/roadmap.md](/Users/vivek/Desktop/Salad/
|
|
|
200
200
|
|
|
201
201
|
## Shareable Assets
|
|
202
202
|
|
|
203
|
-
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 `report --format svg` can generate
|
|
203
|
+
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 `report --format svg` can generate a full bundle of report assets directly from local usage data.
|
|
204
204
|
|
|
205
205
|
## Watch Alerts
|
|
206
206
|
|
|
@@ -25,6 +25,7 @@ from .display import (
|
|
|
25
25
|
format_report,
|
|
26
26
|
format_report_html,
|
|
27
27
|
format_report_markdown,
|
|
28
|
+
format_report_svg_assets,
|
|
28
29
|
format_report_svg,
|
|
29
30
|
format_session,
|
|
30
31
|
format_summary,
|
|
@@ -416,17 +417,27 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
416
417
|
elif args.format == "html":
|
|
417
418
|
content = format_report_html(report, daily_points=daily_points)
|
|
418
419
|
elif args.format == "svg":
|
|
419
|
-
|
|
420
|
+
assets = format_report_svg_assets(report, daily_points=daily_points)
|
|
421
|
+
output_dir = _write_report_svg_assets(
|
|
422
|
+
assets,
|
|
423
|
+
args.output,
|
|
424
|
+
base_name=_default_report_basename(args.period, args.project_name),
|
|
425
|
+
)
|
|
426
|
+
print(f"Wrote SVG assets to {output_dir}")
|
|
427
|
+
if args.render:
|
|
428
|
+
summary_path = output_dir / f"{_default_report_basename(args.period, args.project_name)}-summary-card.svg"
|
|
429
|
+
_open_report_in_browser(summary_path)
|
|
430
|
+
print(f"Opened report in browser: {summary_path}")
|
|
431
|
+
return 0
|
|
420
432
|
elif args.format == "markdown":
|
|
421
433
|
content = format_report_markdown(report)
|
|
422
434
|
else:
|
|
423
435
|
content = format_report(report, options)
|
|
424
|
-
if args.output or args.render
|
|
436
|
+
if args.output or args.render:
|
|
425
437
|
output_path = _write_report_output(
|
|
426
438
|
content,
|
|
427
439
|
args.output,
|
|
428
440
|
suffix=f".{args.format}" if args.format in {"html", "svg"} else ".txt",
|
|
429
|
-
default_name=_default_report_filename(args.period, args.project_name, args.format) if args.format == "svg" else None,
|
|
430
441
|
)
|
|
431
442
|
print(f"Wrote report to {output_path}")
|
|
432
443
|
if args.render:
|
|
@@ -635,5 +646,22 @@ def _default_report_filename(period: str, project_name: str | None, fmt: str) ->
|
|
|
635
646
|
return f"codex-stats-{period}.{fmt}"
|
|
636
647
|
|
|
637
648
|
|
|
649
|
+
def _default_report_basename(period: str, project_name: str | None) -> str:
|
|
650
|
+
if project_name:
|
|
651
|
+
safe_project = "".join(char.lower() if char.isalnum() else "-" for char in project_name).strip("-")
|
|
652
|
+
safe_project = safe_project or "project"
|
|
653
|
+
return f"codex-stats-{period}-{safe_project}"
|
|
654
|
+
return f"codex-stats-{period}"
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _write_report_svg_assets(assets: dict[str, str], output: str | None, *, base_name: str) -> Path:
|
|
658
|
+
output_dir = Path(output).expanduser() if output else Path.cwd()
|
|
659
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
660
|
+
for asset_name, content in assets.items():
|
|
661
|
+
asset_path = output_dir / f"{base_name}-{asset_name}.svg"
|
|
662
|
+
asset_path.write_text(content + ("" if content.endswith("\n") else "\n"), encoding="utf-8")
|
|
663
|
+
return output_dir
|
|
664
|
+
|
|
665
|
+
|
|
638
666
|
if __name__ == "__main__":
|
|
639
667
|
raise SystemExit(main())
|
|
@@ -788,110 +788,59 @@ def format_report_html(report: ReportData, daily_points: list[DailyPoint] | None
|
|
|
788
788
|
"""
|
|
789
789
|
|
|
790
790
|
|
|
791
|
-
def
|
|
791
|
+
def format_report_svg_assets(report: ReportData, daily_points: list[DailyPoint] | None = None) -> dict[str, str]:
|
|
792
792
|
title = f"Codex Stats {report.period.title()} Report"
|
|
793
793
|
if report.project_name:
|
|
794
794
|
title = f"{title}: {report.project_name}"
|
|
795
795
|
daily_points = daily_points or []
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
<feDropShadow dx="0" dy="18" stdDeviation="22" flood-color="rgba(66,41,18,0.14)"/>
|
|
841
|
-
</filter>
|
|
842
|
-
</defs>
|
|
843
|
-
<rect width="1200" height="1200" fill="url(#bg)"/>
|
|
844
|
-
<rect width="1200" height="1200" fill="url(#glowA)"/>
|
|
845
|
-
<rect width="1200" height="1200" fill="url(#glowB)"/>
|
|
796
|
+
assets = {
|
|
797
|
+
"summary-card": _format_summary_card_svg(report, title),
|
|
798
|
+
"daily-tokens": _wrap_chart_svg(
|
|
799
|
+
title="Daily Token Trend",
|
|
800
|
+
subtitle=title,
|
|
801
|
+
chart_svg=_svg_line_chart(
|
|
802
|
+
[(point.day[5:], float(point.total_tokens)) for point in daily_points],
|
|
803
|
+
stroke="#0f766e",
|
|
804
|
+
fill="rgba(15, 118, 110, 0.14)",
|
|
805
|
+
value_formatter=lambda value: f"{int(value):,}",
|
|
806
|
+
),
|
|
807
|
+
),
|
|
808
|
+
"daily-cost": _wrap_chart_svg(
|
|
809
|
+
title="Daily Cost Trend",
|
|
810
|
+
subtitle=title,
|
|
811
|
+
chart_svg=_svg_line_chart(
|
|
812
|
+
[(point.day[5:], float(point.estimated_cost_usd)) for point in daily_points],
|
|
813
|
+
stroke="#b45309",
|
|
814
|
+
fill="rgba(180, 83, 9, 0.14)",
|
|
815
|
+
value_formatter=lambda value: f"${value:.2f}",
|
|
816
|
+
),
|
|
817
|
+
),
|
|
818
|
+
"project-share": _wrap_chart_svg(
|
|
819
|
+
title="Project Share",
|
|
820
|
+
subtitle=title,
|
|
821
|
+
chart_svg=_svg_bar_chart(
|
|
822
|
+
[(entry.name, float(entry.total_tokens)) for entry in report.projects[:5]],
|
|
823
|
+
bar_color="#0f766e",
|
|
824
|
+
value_formatter=lambda value: f"{int(value):,}",
|
|
825
|
+
empty_label="No project breakdown for this scoped report.",
|
|
826
|
+
),
|
|
827
|
+
),
|
|
828
|
+
"top-sessions": _wrap_chart_svg(
|
|
829
|
+
title="Top Sessions",
|
|
830
|
+
subtitle=title,
|
|
831
|
+
chart_svg=_svg_bar_chart(
|
|
832
|
+
[(f"{entry.project_name} / {entry.model or 'unknown'}", float(entry.total_tokens)) for entry in report.top_sessions[:5]],
|
|
833
|
+
bar_color="#b45309",
|
|
834
|
+
value_formatter=lambda value: f"{int(value):,}",
|
|
835
|
+
empty_label="No top session data available.",
|
|
836
|
+
),
|
|
837
|
+
),
|
|
838
|
+
}
|
|
839
|
+
return assets
|
|
846
840
|
|
|
847
|
-
<rect x="38" y="34" width="1124" height="1132" rx="36" fill="rgba(255,250,241,0.88)" stroke="rgba(72,53,36,0.10)" filter="url(#shadow)"/>
|
|
848
841
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
<text x="64" y="194" font-size="22" fill="#6d645d">Standalone shareable report asset</text>
|
|
852
|
-
<text x="64" y="240" font-size="22" fill="#4b4036">Top model: {escape(report.summary.top_model or "unknown")} • Trend: {escape("n/a" if report.comparison.total_tokens_delta_pct is None else f"{report.comparison.total_tokens_delta_pct:+.1f}%")} • Cache ratio: {escape(_fmt_percent(report.summary.cache_ratio))}</text>
|
|
853
|
-
|
|
854
|
-
<rect x="64" y="282" width="240" height="128" rx="24" fill="#fffaf1" stroke="rgba(72,53,36,0.10)"/>
|
|
855
|
-
<text x="88" y="320" font-size="16" letter-spacing="2" fill="#6d645d">TOKENS</text>
|
|
856
|
-
<text x="88" y="372" font-size="44" font-weight="700" fill="#1f1a17">{report.summary.total_tokens:,}</text>
|
|
857
|
-
<text x="88" y="398" font-size="18" fill="#6d645d">{report.summary.requests} requests</text>
|
|
858
|
-
|
|
859
|
-
<rect x="320" y="282" width="240" height="128" rx="24" fill="#fffaf1" stroke="rgba(72,53,36,0.10)"/>
|
|
860
|
-
<text x="344" y="320" font-size="16" letter-spacing="2" fill="#6d645d">EST. COST</text>
|
|
861
|
-
<text x="344" y="372" font-size="44" font-weight="700" fill="#1f1a17">${report.summary.estimated_cost_usd:.2f}</text>
|
|
862
|
-
<text x="344" y="398" font-size="18" fill="#6d645d">Projected ${report.costs.projected_monthly_cost_usd:.2f}</text>
|
|
863
|
-
|
|
864
|
-
<rect x="576" y="282" width="240" height="128" rx="24" fill="#fffaf1" stroke="rgba(72,53,36,0.10)"/>
|
|
865
|
-
<text x="600" y="320" font-size="16" letter-spacing="2" fill="#6d645d">AVG / REQUEST</text>
|
|
866
|
-
<text x="600" y="372" font-size="44" font-weight="700" fill="#1f1a17">{report.summary.average_tokens_per_request:,.0f}</text>
|
|
867
|
-
<text x="600" y="398" font-size="18" fill="#6d645d">Largest {report.summary.largest_session_tokens:,}</text>
|
|
868
|
-
|
|
869
|
-
<rect x="832" y="282" width="304" height="128" rx="24" fill="#0f766e" opacity="0.96"/>
|
|
870
|
-
<text x="856" y="320" font-size="16" letter-spacing="2" fill="#d8f3ef">RECOMMENDATION</text>
|
|
871
|
-
<text x="856" y="360" font-size="24" font-weight="700" fill="#ffffff">{escape(report.insights.suggestion[:34])}</text>
|
|
872
|
-
<text x="856" y="390" font-size="18" fill="#d8f3ef">{escape(report.insights.suggestion[34:68])}</text>
|
|
873
|
-
|
|
874
|
-
<rect x="64" y="442" width="520" height="310" rx="28" fill="#fffaf1" stroke="rgba(72,53,36,0.10)"/>
|
|
875
|
-
<text x="88" y="480" font-size="24" font-weight="700" fill="#1f1a17">Daily Token Trend</text>
|
|
876
|
-
<g transform="translate(0 10)">{token_trend_svg}</g>
|
|
877
|
-
|
|
878
|
-
<rect x="616" y="442" width="520" height="310" rx="28" fill="#fffaf1" stroke="rgba(72,53,36,0.10)"/>
|
|
879
|
-
<text x="640" y="480" font-size="24" font-weight="700" fill="#1f1a17">Project Share</text>
|
|
880
|
-
<g transform="translate(0 28)">{project_share_svg}</g>
|
|
881
|
-
|
|
882
|
-
<rect x="64" y="782" width="1072" height="110" rx="28" fill="#fffaf1" stroke="rgba(72,53,36,0.10)"/>
|
|
883
|
-
<text x="88" y="820" font-size="24" font-weight="700" fill="#1f1a17">Top Sessions</text>
|
|
884
|
-
{top_sessions_text}
|
|
885
|
-
|
|
886
|
-
<text x="64" y="930" font-size="24" font-weight="700" fill="#1f1a17">Anomalies</text>
|
|
887
|
-
{anomaly_badges}
|
|
888
|
-
|
|
889
|
-
<text x="64" y="1036" font-size="24" font-weight="700" fill="#1f1a17">Recommended Actions</text>
|
|
890
|
-
{recommendation_lines}
|
|
891
|
-
|
|
892
|
-
<text x="1136" y="1134" text-anchor="end" font-size="18" fill="#6d645d">Generated by codex-stats</text>
|
|
893
|
-
</svg>
|
|
894
|
-
"""
|
|
842
|
+
def format_report_svg(report: ReportData, daily_points: list[DailyPoint] | None = None) -> str:
|
|
843
|
+
return format_report_svg_assets(report, daily_points)["summary-card"]
|
|
895
844
|
|
|
896
845
|
|
|
897
846
|
def _svg_line_chart(
|
|
@@ -941,7 +890,7 @@ def _svg_line_chart(
|
|
|
941
890
|
for step in range(4)
|
|
942
891
|
)
|
|
943
892
|
return (
|
|
944
|
-
f'<svg class="chart-svg" viewBox="0 0 {width} {height}" role="img" aria-label="Trend chart">'
|
|
893
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" class="chart-svg" viewBox="0 0 {width} {height}" role="img" aria-label="Trend chart">'
|
|
945
894
|
f'{grid}'
|
|
946
895
|
f'<polygon points="{area_points}" fill="{fill}" />'
|
|
947
896
|
f'<polyline points="{line_points}" fill="none" stroke="{stroke}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />'
|
|
@@ -975,7 +924,96 @@ def _svg_bar_chart(
|
|
|
975
924
|
f'<rect x="{padding + label_width}" y="{y + 6}" width="{bar_width:.1f}" height="16" rx="8" fill="{bar_color}" />'
|
|
976
925
|
f'<text x="{padding + label_width + bar_max_width + 10}" y="{y + 19}" font-size="12" fill="#6d645d">{escape(value_formatter(value))}</text>'
|
|
977
926
|
)
|
|
978
|
-
return f'<svg class="chart-svg" viewBox="0 0 {width} {height}" role="img" aria-label="Bar chart">{"".join(rows)}</svg>'
|
|
927
|
+
return f'<svg xmlns="http://www.w3.org/2000/svg" class="chart-svg" viewBox="0 0 {width} {height}" role="img" aria-label="Bar chart">{"".join(rows)}</svg>'
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _format_summary_card_svg(report: ReportData, title: str) -> str:
|
|
931
|
+
delta_text = "n/a" if report.comparison.total_tokens_delta_pct is None else f"{report.comparison.total_tokens_delta_pct:+.1f}%"
|
|
932
|
+
anomalies = report.insights.anomalies[:2] or ["No active anomalies"]
|
|
933
|
+
recommendations = report.insights.recommendations[:2] or ["Usage looks healthy."]
|
|
934
|
+
first_action = recommendations[0]
|
|
935
|
+
second_action = recommendations[1] if len(recommendations) > 1 else recommendations[0]
|
|
936
|
+
first_anomaly = anomalies[0]
|
|
937
|
+
second_anomaly = anomalies[1] if len(anomalies) > 1 else anomalies[0]
|
|
938
|
+
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-label="{escape(title)}">
|
|
939
|
+
<defs>
|
|
940
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
941
|
+
<stop offset="0%" stop-color="#fbf5ec"/>
|
|
942
|
+
<stop offset="100%" stop-color="#efe2cf"/>
|
|
943
|
+
</linearGradient>
|
|
944
|
+
<linearGradient id="teal" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
945
|
+
<stop offset="0%" stop-color="#115e59"/>
|
|
946
|
+
<stop offset="100%" stop-color="#0f766e"/>
|
|
947
|
+
</linearGradient>
|
|
948
|
+
<linearGradient id="amber" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
949
|
+
<stop offset="0%" stop-color="#b45309"/>
|
|
950
|
+
<stop offset="100%" stop-color="#d97706"/>
|
|
951
|
+
</linearGradient>
|
|
952
|
+
</defs>
|
|
953
|
+
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
954
|
+
<rect x="34" y="28" width="1132" height="574" rx="32" fill="#fffaf2" stroke="rgba(66,48,31,0.10)"/>
|
|
955
|
+
|
|
956
|
+
<circle cx="1056" cy="126" r="118" fill="rgba(15,118,110,0.10)"/>
|
|
957
|
+
<circle cx="1116" cy="206" r="72" fill="rgba(217,119,6,0.10)"/>
|
|
958
|
+
<circle cx="1012" cy="214" r="18" fill="#0f766e"/>
|
|
959
|
+
<circle cx="1066" cy="164" r="18" fill="#d97706"/>
|
|
960
|
+
<circle cx="1120" cy="114" r="18" fill="#0f766e"/>
|
|
961
|
+
|
|
962
|
+
<text x="74" y="86" font-size="18" letter-spacing="3" fill="#0f766e">CODEX STATS</text>
|
|
963
|
+
<text x="74" y="150" font-size="60" font-weight="700" fill="#1f1a17">{escape(title)}</text>
|
|
964
|
+
<text x="74" y="194" font-size="24" fill="#6d645d">Compact share card for release notes, README embeds, and social posts.</text>
|
|
965
|
+
|
|
966
|
+
<rect x="74" y="226" width="336" height="52" rx="26" fill="rgba(15,118,110,0.08)"/>
|
|
967
|
+
<text x="102" y="258" font-size="20" fill="#134e4a">Top model {escape(report.summary.top_model or "unknown")}</text>
|
|
968
|
+
<rect x="426" y="226" width="190" height="52" rx="26" fill="rgba(180,83,9,0.10)"/>
|
|
969
|
+
<text x="454" y="258" font-size="20" fill="#92400e">Trend {escape(delta_text)}</text>
|
|
970
|
+
<rect x="632" y="226" width="214" height="52" rx="26" fill="rgba(66,48,31,0.06)"/>
|
|
971
|
+
<text x="660" y="258" font-size="20" fill="#4b4036">Cache {escape(_fmt_percent(report.summary.cache_ratio))}</text>
|
|
972
|
+
|
|
973
|
+
<rect x="74" y="314" width="240" height="120" rx="24" fill="url(#teal)"/>
|
|
974
|
+
<text x="98" y="350" font-size="16" letter-spacing="2" fill="#d7faf5">TOKENS</text>
|
|
975
|
+
<text x="98" y="397" font-size="40" font-weight="700" fill="#ffffff">{report.summary.total_tokens:,}</text>
|
|
976
|
+
<text x="98" y="423" font-size="18" fill="#d7faf5">{report.summary.sessions} sessions</text>
|
|
977
|
+
|
|
978
|
+
<rect x="334" y="314" width="220" height="120" rx="24" fill="#fffaf2" stroke="rgba(66,48,31,0.10)"/>
|
|
979
|
+
<text x="358" y="350" font-size="16" letter-spacing="2" fill="#6d645d">EST. COST</text>
|
|
980
|
+
<text x="358" y="397" font-size="40" font-weight="700" fill="#1f1a17">${report.summary.estimated_cost_usd:.2f}</text>
|
|
981
|
+
<text x="358" y="423" font-size="18" fill="#6d645d">Projected ${report.costs.projected_monthly_cost_usd:.2f}</text>
|
|
982
|
+
|
|
983
|
+
<rect x="574" y="314" width="220" height="120" rx="24" fill="#fffaf2" stroke="rgba(66,48,31,0.10)"/>
|
|
984
|
+
<text x="598" y="350" font-size="16" letter-spacing="2" fill="#6d645d">REQUESTS</text>
|
|
985
|
+
<text x="598" y="397" font-size="40" font-weight="700" fill="#1f1a17">{report.summary.requests}</text>
|
|
986
|
+
<text x="598" y="423" font-size="18" fill="#6d645d">Largest {report.summary.largest_session_tokens:,}</text>
|
|
987
|
+
|
|
988
|
+
<rect x="814" y="314" width="312" height="120" rx="24" fill="url(#amber)"/>
|
|
989
|
+
<text x="838" y="350" font-size="16" letter-spacing="2" fill="#ffedd5">AVG / REQUEST</text>
|
|
990
|
+
<text x="838" y="397" font-size="40" font-weight="700" fill="#ffffff">{report.summary.average_tokens_per_request:,.0f}</text>
|
|
991
|
+
<text x="838" y="423" font-size="18" fill="#ffedd5">{escape(report.insights.suggestion[:34])}</text>
|
|
992
|
+
|
|
993
|
+
<rect x="74" y="466" width="492" height="94" rx="24" fill="#fffaf2" stroke="rgba(66,48,31,0.10)"/>
|
|
994
|
+
<text x="98" y="500" font-size="18" letter-spacing="2" fill="#8a4b0f">ANOMALIES</text>
|
|
995
|
+
<text x="98" y="530" font-size="20" fill="#8a4b0f">• {escape(first_anomaly)}</text>
|
|
996
|
+
<text x="98" y="556" font-size="20" fill="#8a4b0f">• {escape(second_anomaly)}</text>
|
|
997
|
+
|
|
998
|
+
<rect x="592" y="466" width="534" height="94" rx="24" fill="#fffaf2" stroke="rgba(66,48,31,0.10)"/>
|
|
999
|
+
<text x="616" y="500" font-size="18" letter-spacing="2" fill="#4b4036">DO NEXT</text>
|
|
1000
|
+
<text x="616" y="530" font-size="20" fill="#4b4036">• {escape(first_action)}</text>
|
|
1001
|
+
<text x="616" y="556" font-size="20" fill="#4b4036">• {escape(second_action)}</text>
|
|
1002
|
+
|
|
1003
|
+
<text x="1128" y="590" text-anchor="end" font-size="18" fill="#6d645d">Generated by codex-stats</text>
|
|
1004
|
+
</svg>"""
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _wrap_chart_svg(title: str, subtitle: str, chart_svg: str) -> str:
|
|
1008
|
+
chart_body = chart_svg.strip()
|
|
1009
|
+
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-label="{escape(title)}">
|
|
1010
|
+
<rect width="1200" height="720" fill="#f6efe4"/>
|
|
1011
|
+
<rect x="40" y="36" width="1120" height="648" rx="30" fill="#fffaf2" stroke="rgba(66,48,31,0.10)"/>
|
|
1012
|
+
<text x="72" y="94" font-size="18" letter-spacing="3" fill="#0f766e">CODEX STATS</text>
|
|
1013
|
+
<text x="72" y="148" font-size="42" font-weight="700" fill="#1f1a17">{escape(title)}</text>
|
|
1014
|
+
<text x="72" y="184" font-size="22" fill="#6d645d">{escape(subtitle)}</text>
|
|
1015
|
+
<g transform="translate(72 220)">{chart_body}</g>
|
|
1016
|
+
</svg>"""
|
|
979
1017
|
|
|
980
1018
|
|
|
981
1019
|
def format_watch_dashboard(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-stats
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.2
|
|
4
4
|
Summary: Local AI usage observability CLI for Codex sessions.
|
|
5
5
|
Author: Vivek
|
|
6
6
|
License: MIT
|
|
@@ -110,15 +110,15 @@ python3 -m pip install codex-stats
|
|
|
110
110
|
- `codex-stats report weekly --format html`
|
|
111
111
|
Generate a standalone HTML report for sharing, including inline charts.
|
|
112
112
|
- `codex-stats report weekly --format svg`
|
|
113
|
-
Generate a
|
|
113
|
+
Generate a bundle of smaller standalone SVG assets in the current working directory, including a summary card plus graph-focused assets.
|
|
114
114
|
- `codex-stats report weekly --project backend-api`
|
|
115
115
|
Generate a weekly report for one project.
|
|
116
116
|
- `codex-stats report weekly --format markdown --output weekly-report.md`
|
|
117
117
|
Write a formatted report to a file.
|
|
118
118
|
- `codex-stats report weekly --format html --output weekly-report.html`
|
|
119
119
|
Write a polished standalone HTML report to a file.
|
|
120
|
-
- `codex-stats report weekly --format svg --output weekly-report
|
|
121
|
-
Write
|
|
120
|
+
- `codex-stats report weekly --format svg --output weekly-report-assets`
|
|
121
|
+
Write the SVG asset bundle into the output directory you provide.
|
|
122
122
|
- `codex-stats report weekly --format html --render`
|
|
123
123
|
Generate an HTML report, write it to a temp file, and open it in the default browser.
|
|
124
124
|
- `codex-stats report weekly --format svg --render`
|
|
@@ -227,7 +227,7 @@ The current priority list lives in [docs/roadmap.md](/Users/vivek/Desktop/Salad/
|
|
|
227
227
|
|
|
228
228
|
## Shareable Assets
|
|
229
229
|
|
|
230
|
-
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 `report --format svg` can generate
|
|
230
|
+
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 `report --format svg` can generate a full bundle of report assets directly from local usage data.
|
|
231
231
|
|
|
232
232
|
## Watch Alerts
|
|
233
233
|
|
|
@@ -8,7 +8,14 @@ from unittest import mock
|
|
|
8
8
|
|
|
9
9
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
10
10
|
|
|
11
|
-
from codex_stats.cli import
|
|
11
|
+
from codex_stats.cli import (
|
|
12
|
+
_default_report_basename,
|
|
13
|
+
_default_report_filename,
|
|
14
|
+
_open_report_in_browser,
|
|
15
|
+
_write_report_output,
|
|
16
|
+
_write_report_svg_assets,
|
|
17
|
+
build_parser,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
class CliTestCase(unittest.TestCase):
|
|
@@ -197,10 +204,14 @@ class CliTestCase(unittest.TestCase):
|
|
|
197
204
|
self.assertTrue(output_path.exists())
|
|
198
205
|
self.assertIn("report.html", str(output_path))
|
|
199
206
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
svg_dir = _write_report_svg_assets(
|
|
208
|
+
{"summary-card": "<svg></svg>", "daily-tokens": "<svg></svg>"},
|
|
209
|
+
tmpdir,
|
|
210
|
+
base_name="codex-stats-weekly",
|
|
211
|
+
)
|
|
212
|
+
self.assertEqual(svg_dir, Path(tmpdir))
|
|
213
|
+
svg_output = Path(tmpdir) / "codex-stats-weekly-summary-card.svg"
|
|
202
214
|
self.assertTrue(svg_output.exists())
|
|
203
|
-
self.assertEqual(svg_output, Path(tmpdir) / "codex-stats-weekly.svg")
|
|
204
215
|
with mock.patch("codex_stats.cli.webbrowser.open") as open_mock:
|
|
205
216
|
_open_report_in_browser(svg_output)
|
|
206
217
|
open_mock.assert_called_once()
|
|
@@ -208,6 +219,8 @@ class CliTestCase(unittest.TestCase):
|
|
|
208
219
|
def test_default_report_filename(self) -> None:
|
|
209
220
|
self.assertEqual(_default_report_filename("weekly", None, "svg"), "codex-stats-weekly.svg")
|
|
210
221
|
self.assertEqual(_default_report_filename("monthly", "Backend API", "svg"), "codex-stats-monthly-backend-api.svg")
|
|
222
|
+
self.assertEqual(_default_report_basename("weekly", None), "codex-stats-weekly")
|
|
223
|
+
self.assertEqual(_default_report_basename("monthly", "Backend API"), "codex-stats-monthly-backend-api")
|
|
211
224
|
|
|
212
225
|
|
|
213
226
|
if __name__ == "__main__":
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
12
12
|
|
|
13
13
|
from codex_stats.config import Paths
|
|
14
|
-
from codex_stats.display import format_report_html, format_report_svg, format_watch_dashboard
|
|
14
|
+
from codex_stats.display import format_report_html, format_report_svg, format_report_svg_assets, format_watch_dashboard
|
|
15
15
|
from codex_stats.ingest import get_session, get_session_details
|
|
16
16
|
from codex_stats.metrics import (
|
|
17
17
|
apply_watch_state,
|
|
@@ -460,10 +460,13 @@ class MetricsTestCase(unittest.TestCase):
|
|
|
460
460
|
report = build_report(self.paths, "weekly", now=now)
|
|
461
461
|
daily_points = summarize_daily(self.paths, days=7, now=now)
|
|
462
462
|
svg = format_report_svg(report, daily_points=daily_points)
|
|
463
|
+
assets = format_report_svg_assets(report, daily_points=daily_points)
|
|
463
464
|
self.assertIn("<svg", svg)
|
|
464
|
-
self.assertIn("
|
|
465
|
-
self.assertIn("
|
|
465
|
+
self.assertIn("Compact share card for release notes, README embeds, and social posts.", svg)
|
|
466
|
+
self.assertIn("ANOMALIES", svg)
|
|
466
467
|
self.assertIn("Generated by codex-stats", svg)
|
|
468
|
+
self.assertEqual(set(assets), {"summary-card", "daily-tokens", "daily-cost", "project-share", "top-sessions"})
|
|
469
|
+
self.assertIn("Daily Token Trend", assets["daily-tokens"])
|
|
467
470
|
|
|
468
471
|
|
|
469
472
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|