codex-usage-tracking 0.3.0__py3-none-any.whl

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 (50) hide show
  1. codex_usage_tracker/__init__.py +7 -0
  2. codex_usage_tracker/__main__.py +6 -0
  3. codex_usage_tracker/allowance.py +759 -0
  4. codex_usage_tracker/api_payloads.py +90 -0
  5. codex_usage_tracker/cli.py +1326 -0
  6. codex_usage_tracker/context.py +410 -0
  7. codex_usage_tracker/costing.py +176 -0
  8. codex_usage_tracker/dashboard.py +389 -0
  9. codex_usage_tracker/diagnostics.py +624 -0
  10. codex_usage_tracker/formatting.py +225 -0
  11. codex_usage_tracker/json_contracts.py +350 -0
  12. codex_usage_tracker/mcp_server.py +371 -0
  13. codex_usage_tracker/models.py +92 -0
  14. codex_usage_tracker/parser.py +491 -0
  15. codex_usage_tracker/paths.py +18 -0
  16. codex_usage_tracker/plugin_data/__init__.py +1 -0
  17. codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
  18. codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
  19. codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
  20. codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
  21. codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
  22. codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
  23. codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
  24. codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
  25. codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
  26. codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
  27. codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
  28. codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
  29. codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
  30. codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
  31. codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
  32. codex_usage_tracker/plugin_installer.py +312 -0
  33. codex_usage_tracker/pricing.py +57 -0
  34. codex_usage_tracker/pricing_config.py +223 -0
  35. codex_usage_tracker/pricing_estimates.py +44 -0
  36. codex_usage_tracker/pricing_openai.py +253 -0
  37. codex_usage_tracker/projects.py +347 -0
  38. codex_usage_tracker/recommendations.py +270 -0
  39. codex_usage_tracker/reports.py +637 -0
  40. codex_usage_tracker/schema.py +71 -0
  41. codex_usage_tracker/server.py +400 -0
  42. codex_usage_tracker/store.py +666 -0
  43. codex_usage_tracker/support.py +147 -0
  44. codex_usage_tracker/threads.py +183 -0
  45. codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
  46. codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
  47. codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
  48. codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
  49. codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
  50. codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,136 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Codex Usage Tracker Dashboard Guide</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ --bg: #f7f8fb;
11
+ --panel: #ffffff;
12
+ --ink: #172033;
13
+ --muted: #69758a;
14
+ --line: #dde3ee;
15
+ --blue: #2563eb;
16
+ }
17
+ body {
18
+ margin: 0;
19
+ background: var(--bg);
20
+ color: var(--ink);
21
+ font: 15px/1.6 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
22
+ }
23
+ main {
24
+ max-width: 980px;
25
+ margin: 0 auto;
26
+ padding: 32px 18px 56px;
27
+ }
28
+ h1, h2 { line-height: 1.2; }
29
+ h1 { margin: 0 0 8px; font-size: clamp(28px, 4vw, 42px); }
30
+ h2 { margin-top: 32px; padding-top: 24px; border-top: 1px solid var(--line); }
31
+ p, li { color: var(--muted); }
32
+ code {
33
+ padding: 2px 5px;
34
+ border-radius: 5px;
35
+ background: #eef2f8;
36
+ color: var(--ink);
37
+ font-size: 0.92em;
38
+ }
39
+ pre {
40
+ overflow: auto;
41
+ padding: 14px;
42
+ border: 1px solid var(--line);
43
+ border-radius: 8px;
44
+ background: #101827;
45
+ color: #f8fafc;
46
+ }
47
+ pre code { padding: 0; background: transparent; color: inherit; }
48
+ img {
49
+ display: block;
50
+ width: 100%;
51
+ margin: 14px 0 20px;
52
+ border: 1px solid var(--line);
53
+ border-radius: 8px;
54
+ background: var(--panel);
55
+ }
56
+ .note {
57
+ padding: 14px 16px;
58
+ border: 1px solid #bfdbfe;
59
+ border-radius: 8px;
60
+ background: #eff6ff;
61
+ color: #1e3a8a;
62
+ }
63
+ .disclaimer {
64
+ padding: 14px 16px;
65
+ border: 1px solid #fed7aa;
66
+ border-radius: 8px;
67
+ background: #fff7ed;
68
+ color: #7c2d12;
69
+ font-weight: 650;
70
+ }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <main>
75
+ <h1>Dashboard Guide</h1>
76
+ <p class="disclaimer">Unofficial project: Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.</p>
77
+ <p>This local guide uses synthetic screenshots and does not contain prompts, assistant text, tool output, or real Codex session content.</p>
78
+
79
+ <h2>Open The Dashboard</h2>
80
+ <p>For the best experience, run the localhost dashboard server:</p>
81
+ <pre><code>codex-usage-tracker setup
82
+ codex-usage-tracker update-pricing
83
+ codex-usage-tracker update-rate-card
84
+ codex-usage-tracker serve-dashboard --open</code></pre>
85
+ <p>For optional allowance context, run <code>codex-usage-tracker init-allowance</code> or <code>codex-usage-tracker parse-allowance "5h 79% 6:50 PM Weekly 33% Jun 7"</code> with current values copied from Codex Usage or <code>/status</code>.</p>
86
+ <p>To tune review thresholds locally, run <code>codex-usage-tracker init-thresholds</code> and edit <code>~/.codex-usage-tracker/thresholds.json</code>. These thresholds control low-cache, high-context, high-uncached-input, large-thread, reasoning-spike, low-output, and high-cost recommendations.</p>
87
+ <p>To tune project attribution locally, run <code>codex-usage-tracker init-projects</code> and edit <code>~/.codex-usage-tracker/projects.json</code>. The dashboard derives project name, relative cwd, branch, tags, and a hashed remote origin from aggregate <code>cwd</code> and local Git metadata when available.</p>
88
+ <p>Before sharing screenshots or generated artifacts, put <code>--privacy-mode redacted</code> or <code>--privacy-mode strict</code> before the subcommand, such as <code>codex-usage-tracker --privacy-mode strict serve-dashboard --open</code>. Redacted mode hides raw cwd/source paths, hides Git remote labels, and hashes unnamed projects while preserving configured aliases. Strict mode also hides project-relative cwd, Git branch, and tags. The dashboard header shows the active metadata mode.</p>
89
+ <p>The server enables live aggregate refresh and on-demand context loading. Static file mode can still filter, sort, and inspect aggregate fields, but cannot refresh logs or load context.</p>
90
+ <p>The localhost server uses a random per-server token for refresh and context API calls, validates loopback <code>Host</code> and <code>Origin</code> headers, and can run as aggregate-only with <code>codex-usage-tracker serve-dashboard --no-context-api</code>.</p>
91
+
92
+ <h2>Insights View</h2>
93
+ <img src="assets/dashboard-insights.png" alt="Insights view with ranked attention cards, investigation presets, and top threads by attention score.">
94
+ <p>The dashboard opens in <code>Insights</code> view. It ranks costly threads, Codex allowance usage, low cache reuse, context bloat, unpriced usage, estimated pricing, and reasoning-output spikes from aggregate fields only.</p>
95
+ <ul>
96
+ <li><code>Needs Attention</code> cards pair each signal with a next action.</li>
97
+ <li><code>Investigation Presets</code> apply a view, derived filter, sort order, and explanatory caption together.</li>
98
+ <li>Presets include highest-cost threads, highest Codex credits, context bloat, cache misses, pricing gaps, and estimated-price review.</li>
99
+ </ul>
100
+
101
+ <h2>Calls View</h2>
102
+ <img src="assets/dashboard-calls.png" alt="Calls view showing filters, totals, table rows, and details panel.">
103
+ <p>Use <code>Calls</code> view to inspect individual model calls. Sort by attention, time, thread, model, tokens, cost, highest Codex credits, cache, context, or signals. Hover or click a row to pin grouped aggregate fields in <code>Call Details</code>. Top cards show cached input, uncached input, Codex credit usage, and optional usage remaining. The <code>Time</code> filter supports all time, today, this week, last 7 days, this month, and custom calendar ranges. Presets are relative to the browser's local date; custom ranges use inclusive start and end dates. The <code>History</code> control defaults to <code>Active sessions only</code>; switch to <code>All history</code> only when you want live refresh to scan archived session logs. The <code>Confidence</code> filter separates exact cost, estimated cost, unpriced cost, exact credit-rate matches, inferred credit mappings, user credit overrides, and missing credit rates. The URL tracks the active view, filters, time preset or custom range, history scope, sort, preset, selected row or thread, page, and expanded threads. <code>Copy link</code> copies that state, and <code>Export CSV</code> downloads the currently filtered aggregate calls. The header uses short status chips; exact refresh time and source details live in hover titles so live refreshes do not reflow the page. A <code>Parser warnings</code> chip appears only when the latest refresh reports skipped token events, missing expected token fields, invalid counters, duplicate cumulative snapshots, or unknown event shapes.</p>
104
+ <ul>
105
+ <li><code>Last call total</code> is the selected model call.</li>
106
+ <li><code>Session cumulative</code> is the running total Codex logged for that session.</li>
107
+ <li><code>Cached input</code> and <code>Uncached input</code> make cache behavior visible without storing transcript text.</li>
108
+ <li>A cost with <code>*</code> means the pricing row is a marked best-guess estimate.</li>
109
+ <li>Codex credits are estimated from aggregate input, cached-input, and output counters using bundled or locally configured rate-card values. Direct matches are exact, inferred mappings are estimated, and local credit-rate overrides are user-provided.</li>
110
+ <li>Search matches derived project names, project-relative cwd values, tags, branch names, and redacted remote labels.</li>
111
+ <li>Time values are shown in your browser's local date/time format while sorting and time filtering still use the logged timestamp.</li>
112
+ <li>In redacted or strict privacy mode, search only sees the redacted metadata fields included in the dashboard payload.</li>
113
+ <li><code>Usage Remaining</code> is not read from the logged-in account plan. Configure <code>~/.codex-usage-tracker/allowance.json</code> with values copied from Codex Settings &gt; Usage, the Codex Usage dashboard, or <code>/status</code> when you want current remaining allowance context.</li>
114
+ <li>Call details include a recommended action and a "why flagged" explanation derived only from aggregate counters and pricing/allowance metadata.</li>
115
+ <li>Raw aggregate identifiers and source file metadata are collapsed until you need them.</li>
116
+ </ul>
117
+
118
+ <h2>Threads View</h2>
119
+ <img src="assets/dashboard-threads.png" alt="Threads view with one expanded thread and chronological child calls.">
120
+ <p>Use <code>Threads</code> view to understand a work session as a group. Expand a thread to see calls oldest to newest. Subagents with logged parent session ids are shown under their parent thread; inferred auto-review attachments are marked in the details panel. Selected threads also show lifecycle signals such as first expensive turn, largest cumulative jump, cache trend, context trend, and whether subagent or auto-review work appeared before a usage spike.</p>
121
+
122
+ <h2>Details And Context</h2>
123
+ <img src="assets/dashboard-details.png" alt="Details panel showing aggregate usage fields for a selected call.">
124
+ <p>The details panel shows primary cost, Codex credits, allowance impact, cache, context, pricing, and next-action signals first. It then groups thread narrative, token/pricing breakdowns, credit confidence and rate-card source metadata, collapsed raw identifiers, and source metadata. When served from localhost with the context API enabled, <code>Load context</code> fetches one redacted, size-limited source excerpt on demand. When started with <code>--no-context-api</code>, context buttons stay disabled and the dashboard remains aggregate-only.</p>
125
+
126
+ <h2>Investigating Long Chat Growth</h2>
127
+ <p class="note">Prompt caching helps, but cached input is not free. Long-running chats can carry a large cached prefix into later turns, so usage can climb quickly even when the visible request looks small.</p>
128
+ <p>Watch <code>Cached input</code>, <code>Uncached input</code>, <code>Session cumulative</code>, <code>Context use</code>, and <code>Cache ratio</code> together. When old context is no longer useful, starting a fresh Codex thread may be more efficient than carrying a large cached history forward.</p>
129
+
130
+ <h2>Privacy Model</h2>
131
+ <p>The dashboard includes session ids, thread names, cwd values, source file paths, timestamps, model labels, reasoning effort, token counts, cost estimates, Codex credit estimates, optional manually entered allowance windows, and derived ratios. It does not include prompts, assistant responses, raw tool output, pasted secrets, message snippets, or transcript text. Remaining 5-hour and weekly allowance is not read from Codex logs or inferred from the logged-in account plan automatically. Local Codex logs may also omit usage from other ChatGPT agentic surfaces that share the same allowance. Archived sessions are excluded from dashboard payloads by default; <code>All history</code> is an explicit opt-in because archived logs can make refreshes slower and make current dashboards look inflated by older work.</p>
132
+ <p>Use <code>--privacy-mode redacted</code> or <code>--privacy-mode strict</code> before sharing generated dashboards, CSV exports, query JSON, or support bundles. Redacted mode removes raw cwd/source paths and hides unnamed project names behind stable hashes. Strict mode also hides project-relative cwd, branch, and tags. Configured project aliases are treated as explicit display opt-ins in both modes.</p>
133
+ <p>Pricing and Codex credit estimates are source-stamped local calculations. Use <code>codex-usage-tracker pin-pricing --output &lt;path&gt;</code> when a report needs to keep the same USD pricing snapshot over time, and use <code>codex-usage-tracker update-rate-card</code> when you want an explicit local copy of the bundled Codex credit rate-card snapshot.</p>
134
+ </main>
135
+ </body>
136
+ </html>
@@ -0,0 +1,69 @@
1
+ {
2
+ "schema": "codex-usage-tracker-codex-rate-card-v1",
3
+ "version": "2026-06-03",
4
+ "source": {
5
+ "name": "OpenAI Codex rate card",
6
+ "url": "https://help.openai.com/en/articles/20001106-codex-rate-card",
7
+ "pricing_url": "https://developers.openai.com/codex/pricing",
8
+ "fetched_at": "2026-06-03",
9
+ "basis": "credits per 1M input, cached input, and output tokens",
10
+ "tier": "standard"
11
+ },
12
+ "credit_rates": {
13
+ "gpt-5.5": {
14
+ "input_per_million": 125.0,
15
+ "cached_input_per_million": 12.5,
16
+ "output_per_million": 750.0,
17
+ "confidence": "exact",
18
+ "source_url": "https://help.openai.com/en/articles/20001106-codex-rate-card",
19
+ "fetched_at": "2026-06-03",
20
+ "tier": "standard"
21
+ },
22
+ "gpt-5.4": {
23
+ "input_per_million": 62.5,
24
+ "cached_input_per_million": 6.25,
25
+ "output_per_million": 375.0,
26
+ "confidence": "exact",
27
+ "source_url": "https://help.openai.com/en/articles/20001106-codex-rate-card",
28
+ "fetched_at": "2026-06-03",
29
+ "tier": "standard"
30
+ },
31
+ "gpt-5.4-mini": {
32
+ "input_per_million": 18.75,
33
+ "cached_input_per_million": 1.875,
34
+ "output_per_million": 113.0,
35
+ "confidence": "exact",
36
+ "source_url": "https://help.openai.com/en/articles/20001106-codex-rate-card",
37
+ "fetched_at": "2026-06-03",
38
+ "tier": "standard"
39
+ },
40
+ "gpt-5.3-codex": {
41
+ "input_per_million": 43.75,
42
+ "cached_input_per_million": 4.375,
43
+ "output_per_million": 350.0,
44
+ "confidence": "exact",
45
+ "source_url": "https://help.openai.com/en/articles/20001106-codex-rate-card",
46
+ "fetched_at": "2026-06-03",
47
+ "tier": "standard"
48
+ },
49
+ "gpt-5.2": {
50
+ "input_per_million": 43.75,
51
+ "cached_input_per_million": 4.375,
52
+ "output_per_million": 350.0,
53
+ "confidence": "exact",
54
+ "source_url": "https://help.openai.com/en/articles/20001106-codex-rate-card",
55
+ "fetched_at": "2026-06-03",
56
+ "tier": "standard"
57
+ }
58
+ },
59
+ "aliases": {
60
+ "codex-auto-review": {
61
+ "model": "gpt-5.3-codex",
62
+ "confidence": "estimated",
63
+ "note": "Inferred from the Codex rate card note that code review runs on GPT-5.3-Codex.",
64
+ "source_url": "https://help.openai.com/en/articles/20001106-codex-rate-card",
65
+ "fetched_at": "2026-06-03",
66
+ "alias_reason": "Codex rate card describes code review as GPT-5.3-Codex, while local logs can label it codex-auto-review."
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: codex-usage-api
3
+ description: Use when the user wants to discuss, investigate, compare, or explain Codex usage using the Codex Usage Tracker API or MCP tools.
4
+ ---
5
+
6
+ # Codex Usage API Companion
7
+
8
+ Use this companion skill as the conversational analyst for Codex Usage Tracker data. It helps Codex choose the right aggregate-only API calls, interpret the results, and answer the user's usage questions with evidence.
9
+
10
+ ## Privacy Boundary
11
+
12
+ Normal usage answers must use aggregate-only API data. Do not expose prompts, assistant messages, tool output, pasted secrets, or raw transcript snippets.
13
+
14
+ When a user plans to share JSON, CSV, dashboards, screenshots, or support bundles, prefer `privacy_mode="strict"` for MCP calls or the CLI global option `--privacy-mode strict` before the subcommand. Explain that configured project aliases are treated as explicit display opt-ins.
15
+
16
+ The only exception is `usage_call_context`, which reads one selected record's local source JSONL on demand. Use it only when the user explicitly asks to inspect actual logged context, and state that the returned text is local, redacted, size-limited, and not persisted by the tracker.
17
+
18
+ ## First Steps
19
+
20
+ 1. For "Open dashboard" or similar dashboard-open requests, do not inspect repository files, plugin manifests, tool registries, git status, or local logs first. Run `codex-usage-tracker open-dashboard --refresh` immediately, then report the opened path or a brief failure.
21
+ 2. For "Heaviest thread?", "Thread leaderboard", or similar thread-ranking requests, do not inspect repository files, SQLite schemas, plugin manifests, process lists, dashboard servers, or local logs manually. Refresh the aggregate index, then call `usage_summary(group_by="thread", limit=10, response_format="json")`. If MCP tools are unavailable, run `codex-usage-tracker refresh --json` and `codex-usage-tracker summary --group-by thread --limit 10 --json`.
22
+ 3. For normal usage questions, do not inspect repository files, plugin manifests, or local logs first. Start with the aggregate MCP tools. If MCP tools are unavailable, use the CLI JSON fallback below.
23
+ 4. Refresh before analysis with `refresh_usage_index` unless the user asks for a static historical snapshot. Keep archived sessions excluded unless the user explicitly asks for all history.
24
+ 5. Use `usage_doctor(response_format="json")` when setup, indexing, pricing, MCP discovery, or dashboard freshness is uncertain.
25
+ 6. Prefer JSON responses for analysis:
26
+ - `usage_summary(..., response_format="json")`
27
+ - `session_usage(..., response_format="json")`
28
+ - `most_expensive_usage_calls(..., response_format="json")`
29
+ - `usage_recommendations(..., response_format="json")`
30
+ - `usage_pricing_coverage(..., response_format="json")`
31
+ - `usage_query(...)`
32
+ 7. Check the top-level `schema` field before interpreting structured output. Known schema ids are documented in `docs/cli-json-schemas.md`.
33
+ 8. If MCP tools are unavailable, fall back to the CLI equivalents:
34
+ - `codex-usage-tracker refresh --json`
35
+ - `codex-usage-tracker summary --group-by thread --json`
36
+ - `codex-usage-tracker query`
37
+ - `codex-usage-tracker session --json`
38
+ - `codex-usage-tracker expensive --json`
39
+ - `codex-usage-tracker recommendations --json`
40
+ - `codex-usage-tracker pricing-coverage --json`
41
+ 9. If the `codex-usage-tracker` command is missing, run `codex-usage-tracker doctor --suggest-repair --json` only if the command is available through an absolute path or known environment. Otherwise report that the CLI is not on `PATH` and ask the user to run `codex-usage-tracker setup` or reinstall with `pipx`.
42
+ 10. Use source-checkout fallbacks only when you are already inside the repo checkout: `PYTHONPATH=src .venv/bin/python -m codex_usage_tracker.cli <command>`. Do not use `PYTHONPATH=src` outside that checkout, and do not keep exploring plugin files after a setup failure.
43
+
44
+ ## Routing Questions To API Calls
45
+
46
+ - "What used the most?" Use `usage_summary(group_by="thread", response_format="json")` first for thread totals, then `most_expensive_usage_calls(response_format="json")` for supporting calls.
47
+ - "Which project/thread/model is driving usage?" Use `usage_summary` grouped by `project`, `thread`, or `model`.
48
+ - "Can I share this?" Use redacted or strict privacy mode and avoid `usage_call_context`.
49
+ - "Why did usage spike?" Use `usage_recommendations(response_format="json")` first for ranked causes, then `usage_query` with `since`, `project`, `thread`, `model`, `effort`, `min_tokens`, or `min_credits` for supporting rows.
50
+ - "What is unpriced or estimated?" Use `usage_pricing_coverage(response_format="json")` and `usage_query(pricing_status="unpriced")` or `usage_query(credit_confidence="estimated")`.
51
+ - "How does this affect my allowance?" Use rows from `usage_query` and summarize `usage_credits`, `usage_credit_confidence`, and `allowanceImpact`. Explain that remaining allowance is only as accurate as the user's local allowance config.
52
+ - "What happened in this session?" Use `session_usage(session_id=..., response_format="json")`.
53
+ - "What should I do next?" Use `usage_recommendations(response_format="json")` and explain the primary recommendation, secondary signals, recommendation score, and top thread rollups.
54
+
55
+ ## Answer Style
56
+
57
+ - Lead with the direct answer and the key metric.
58
+ - For default prompt workflows, use at most one short progress update such as "Refreshing aggregate usage, then ranking threads." Avoid narrating tool discovery, process inspection, SQLite schema inspection, or plugin-file lookup.
59
+ - Name the data scope, such as time window, project, thread, model, row count, and whether rows were truncated.
60
+ - Separate exact facts from estimates. Call out `pricing_estimated`, missing `pricing_model`, `usage_credit_confidence`, and missing allowance windows.
61
+ - Include the next useful investigation when the answer depends on unclear pricing, stale allowance values, or a broad time window.
62
+ - Keep explanations tied to aggregate fields rather than guessing from conversation content.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: codex-usage-tracker
3
+ description: Use when the user asks about Codex token usage, model/reasoning efficiency, usage dashboards, CSV exports, or per-session/per-turn Codex usage stats from local logs.
4
+ ---
5
+
6
+ # Codex Usage Tracker
7
+
8
+ Unofficial project: Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.
9
+
10
+ Use this plugin to inspect aggregate token usage from local Codex session logs.
11
+
12
+ ## Privacy Boundary
13
+
14
+ The index, dashboard payload, CSV export, and normal summaries are aggregate-only. They should never return prompts, assistant message text, tool outputs, pasted secrets, or raw transcript snippets.
15
+
16
+ The only exception is `usage_call_context`, which intentionally reads one selected record's source JSONL on demand. It requires `CODEX_USAGE_TRACKER_ALLOW_RAW_CONTEXT=1` in the MCP server environment. Use it only when the user explicitly asks to inspect actual context, and mention that returned text is local, redacted, size-limited, and not persisted by the tracker.
17
+
18
+ ## Fast Paths
19
+
20
+ - For "Open dashboard" or similar dashboard-open requests, do not inspect repository files, plugin manifests, tool registries, git status, or local logs first. Run `codex-usage-tracker open-dashboard --refresh` immediately.
21
+ - For "Heaviest thread?", "Thread leaderboard", or similar thread-ranking requests, do not inspect repository files, SQLite schemas, plugin manifests, process lists, dashboard servers, or local logs manually. Use the tracker API: refresh the aggregate index, then rank threads with `usage_summary(group_by="thread", limit=10, response_format="json")`.
22
+ - If MCP tools are unavailable for thread-ranking requests, run `codex-usage-tracker refresh --json` and `codex-usage-tracker summary --group-by thread --limit 10 --json`. The summary is already ordered by `total_tokens` descending.
23
+ - Answer thread-ranking requests directly from the summary rows. For the heaviest-thread question, lead with the first row's thread and total tokens; for leaderboard requests, show a compact ranked list.
24
+ - If the CLI command is missing for open-dashboard requests and you are already inside the source checkout, use `PYTHONPATH=src .venv/bin/python -m codex_usage_tracker.cli open-dashboard --refresh`.
25
+ - If the CLI command is missing for thread-ranking requests and you are already inside the source checkout, use `PYTHONPATH=src .venv/bin/python -m codex_usage_tracker.cli refresh --json` and `PYTHONPATH=src .venv/bin/python -m codex_usage_tracker.cli summary --group-by thread --limit 10 --json`.
26
+ - If neither command is available, say briefly that the tracker CLI is not on `PATH` and ask the user to run `codex-usage-tracker setup` or reinstall with `pipx`.
27
+ - Keep open-dashboard narration minimal: one short progress note if needed, then the opened path or the failure. Do not narrate plugin discovery.
28
+
29
+ ## Common Workflows
30
+
31
+ - Refresh the index before answering usage questions.
32
+ - Use `usage_doctor` when setup, plugin discovery, MCP launch, dashboard output, or pricing estimates look wrong.
33
+ - Use `usage_summary` for high-level totals by date, model, effort, cwd, thread, or session.
34
+ - Use `usage_query` for stable JSON rows filtered by date, project, model, effort, thread, pricing status, token minimums, or Codex credit minimums.
35
+ - Use `usage_recommendations` when the user asks what to inspect next or wants ranked action items by aggregate severity.
36
+ - Use `usage_summary` presets `today`, `last-7-days`, `by-model`, `by-cwd`, `by-thread`, and `expensive` for common requests.
37
+ - Use `usage_pricing_coverage` when the user asks whether costs are fully priced or which models use estimated or missing pricing.
38
+ - Use `session_usage` for per-call and per-turn detail for one session.
39
+ - Use `usage_call_context` for one selected model call when the user asks to load actual logged context on demand.
40
+ - Use `most_expensive_usage_calls` to identify high-token calls and aggregate efficiency signals.
41
+ - Use `privacy_mode="redacted"` or `privacy_mode="strict"` for MCP tools, or the CLI global option `--privacy-mode strict` before a subcommand, when the user plans to share dashboards, CSV, JSON, screenshots, or support bundles.
42
+ - Use `generate_usage_dashboard` when the user wants a visual hoverable report, including flat calls, threaded-by-thread views, parent-thread latching for spawned subagents, auto-review attachment details, an active-only default, and explicit all-history archived-session opt-in.
43
+ - Use `export_usage_csv` when the user wants local spreadsheet-friendly data.
44
+ - Use `update_usage_pricing_config` when the user wants cost estimates based on OpenAI-published text-token pricing. This refreshes the local pricing cache and does not send local usage data anywhere. Internal Codex labels may include explicitly marked best-guess estimates when no public pricing row exists.
45
+ - Use `init_usage_pricing_config` only when the user wants a manual local pricing template or override file.
46
+ - Codex credit estimates are aggregate-only and use bundled or locally configured Codex rate-card values. Direct model matches are exact; aliases and inferred labels are marked estimated.
47
+ - Use `init_usage_allowance_config` only when the user wants a local allowance template for manually copied 5-hour or weekly remaining usage from Codex Usage or `/status`.
@@ -0,0 +1,312 @@
1
+ """Install a generated local Codex plugin wrapper for the package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from importlib import resources
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from codex_usage_tracker import __version__
14
+ from codex_usage_tracker.paths import DEFAULT_MARKETPLACE_PATH, DEFAULT_PLUGIN_LINK
15
+
16
+ PLUGIN_NAME = "codex-usage-tracker"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class PluginInstallResult:
21
+ plugin_dir: Path
22
+ marketplace_path: Path
23
+ python_executable: Path
24
+ replaced_existing: bool
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class PluginUninstallResult:
29
+ plugin_dir: Path
30
+ marketplace_path: Path
31
+ removed_plugin_path: bool
32
+ removed_marketplace_entry: bool
33
+
34
+
35
+ def install_plugin(
36
+ *,
37
+ plugin_dir: Path = DEFAULT_PLUGIN_LINK,
38
+ marketplace_path: Path = DEFAULT_MARKETPLACE_PATH,
39
+ python_executable: Path | None = None,
40
+ force: bool = False,
41
+ ) -> PluginInstallResult:
42
+ """Create or refresh a local Codex plugin wrapper for this installed package."""
43
+
44
+ plugin_dir = plugin_dir.expanduser()
45
+ marketplace_path = marketplace_path.expanduser()
46
+ python_path = _absolute_path(Path(python_executable or sys.executable))
47
+ replaced_existing = _prepare_plugin_dir(plugin_dir, force=force)
48
+ _write_plugin_files(plugin_dir=plugin_dir, python_executable=python_path)
49
+ marketplace_path.parent.mkdir(parents=True, exist_ok=True)
50
+ marketplace = _load_marketplace(marketplace_path)
51
+ _upsert_marketplace_entry(marketplace, plugin_dir)
52
+ marketplace_path.write_text(
53
+ json.dumps(marketplace, indent=2, sort_keys=False) + "\n",
54
+ encoding="utf-8",
55
+ )
56
+ return PluginInstallResult(
57
+ plugin_dir=plugin_dir,
58
+ marketplace_path=marketplace_path,
59
+ python_executable=python_path,
60
+ replaced_existing=replaced_existing,
61
+ )
62
+
63
+
64
+ def uninstall_plugin(
65
+ *,
66
+ plugin_dir: Path = DEFAULT_PLUGIN_LINK,
67
+ marketplace_path: Path = DEFAULT_MARKETPLACE_PATH,
68
+ ) -> PluginUninstallResult:
69
+ """Remove the package-owned local Codex plugin wrapper and marketplace entry."""
70
+
71
+ plugin_dir = plugin_dir.expanduser()
72
+ marketplace_path = marketplace_path.expanduser()
73
+ removed_plugin_path = _remove_plugin_dir(plugin_dir)
74
+ removed_marketplace_entry = False
75
+ if marketplace_path.exists():
76
+ marketplace = _load_marketplace(marketplace_path)
77
+ removed_marketplace_entry = _remove_marketplace_entry(marketplace)
78
+ marketplace_path.write_text(
79
+ json.dumps(marketplace, indent=2, sort_keys=False) + "\n",
80
+ encoding="utf-8",
81
+ )
82
+ return PluginUninstallResult(
83
+ plugin_dir=plugin_dir,
84
+ marketplace_path=marketplace_path,
85
+ removed_plugin_path=removed_plugin_path,
86
+ removed_marketplace_entry=removed_marketplace_entry,
87
+ )
88
+
89
+
90
+ def _prepare_plugin_dir(plugin_dir: Path, *, force: bool) -> bool:
91
+ if plugin_dir.is_symlink():
92
+ if not force:
93
+ raise FileExistsError(
94
+ f"{plugin_dir} is a symlink. Use --force to replace the old source-checkout plugin link."
95
+ )
96
+ plugin_dir.unlink()
97
+ plugin_dir.mkdir(parents=True, exist_ok=True)
98
+ return True
99
+ if plugin_dir.exists():
100
+ if not _is_existing_tracker_plugin(plugin_dir):
101
+ raise FileExistsError(
102
+ f"{plugin_dir} exists but does not look like a Codex Usage Tracker plugin."
103
+ )
104
+ if not force:
105
+ return False
106
+ shutil.rmtree(plugin_dir)
107
+ plugin_dir.mkdir(parents=True, exist_ok=True)
108
+ return True
109
+ plugin_dir.mkdir(parents=True, exist_ok=True)
110
+ return False
111
+
112
+
113
+ def _remove_plugin_dir(plugin_dir: Path) -> bool:
114
+ if plugin_dir.is_symlink():
115
+ target = plugin_dir.resolve()
116
+ if target.exists() and not _is_existing_tracker_plugin(target):
117
+ raise FileExistsError(
118
+ f"{plugin_dir} points to {target}, which does not look like a Codex Usage Tracker plugin."
119
+ )
120
+ plugin_dir.unlink()
121
+ return True
122
+ if not plugin_dir.exists():
123
+ return False
124
+ if not _is_existing_tracker_plugin(plugin_dir):
125
+ raise FileExistsError(
126
+ f"{plugin_dir} exists but does not look like a Codex Usage Tracker plugin."
127
+ )
128
+ shutil.rmtree(plugin_dir)
129
+ return True
130
+
131
+
132
+ def _is_existing_tracker_plugin(plugin_dir: Path) -> bool:
133
+ manifest_path = plugin_dir / ".codex-plugin" / "plugin.json"
134
+ if not manifest_path.exists():
135
+ return False
136
+ try:
137
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
138
+ except (OSError, json.JSONDecodeError):
139
+ return False
140
+ return isinstance(manifest, dict) and manifest.get("name") == PLUGIN_NAME
141
+
142
+
143
+ def _absolute_path(path: Path) -> Path:
144
+ expanded = path.expanduser()
145
+ if expanded.is_absolute():
146
+ return expanded
147
+ return Path.cwd() / expanded
148
+
149
+
150
+ def _write_plugin_files(*, plugin_dir: Path, python_executable: Path) -> None:
151
+ (plugin_dir / ".codex-plugin").mkdir(parents=True, exist_ok=True)
152
+ (plugin_dir / ".codex-plugin" / "plugin.json").write_text(
153
+ json.dumps(plugin_manifest(), indent=2) + "\n",
154
+ encoding="utf-8",
155
+ )
156
+ (plugin_dir / ".mcp.json").write_text(
157
+ json.dumps(_mcp_config(python_executable), indent=2) + "\n",
158
+ encoding="utf-8",
159
+ )
160
+ _copy_tree("assets", plugin_dir / "assets")
161
+ _copy_tree("skills", plugin_dir / "skills")
162
+
163
+
164
+ def _copy_tree(resource_name: str, destination: Path) -> None:
165
+ source = resources.files("codex_usage_tracker.plugin_data").joinpath(resource_name)
166
+ if destination.exists():
167
+ shutil.rmtree(destination)
168
+ destination.mkdir(parents=True, exist_ok=True)
169
+ _copy_resource_tree(source, destination)
170
+
171
+
172
+ def _copy_resource_tree(source: Any, destination: Path) -> None:
173
+ for child in source.iterdir():
174
+ target = destination / child.name
175
+ if child.is_dir():
176
+ target.mkdir(parents=True, exist_ok=True)
177
+ _copy_resource_tree(child, target)
178
+ else:
179
+ with child.open("rb") as input_file, target.open("wb") as output_file:
180
+ shutil.copyfileobj(input_file, output_file)
181
+
182
+
183
+ def plugin_manifest() -> dict[str, Any]:
184
+ """Return the package-owned Codex plugin manifest."""
185
+
186
+ return {
187
+ "name": PLUGIN_NAME,
188
+ "version": __version__,
189
+ "description": "Unofficial local tracker for aggregate Codex token usage from local session logs.",
190
+ "author": {"name": "Douglas Monsky"},
191
+ "homepage": "https://github.com/douglasmonsky/codex-usage-tracker",
192
+ "repository": "https://github.com/douglasmonsky/codex-usage-tracker",
193
+ "license": "MIT",
194
+ "keywords": ["codex", "tokens", "usage", "mcp", "dashboard"],
195
+ "skills": "./skills/",
196
+ "mcpServers": "./.mcp.json",
197
+ "interface": {
198
+ "displayName": "Codex Usage Tracker",
199
+ "shortDescription": "Unofficial local aggregate token usage analytics for Codex",
200
+ "longDescription": (
201
+ "Unofficial independent project, not made by, affiliated with, endorsed by, "
202
+ "sponsored by, or supported by OpenAI. Reads local Codex session logs, "
203
+ "aggregates exact token usage counters, and generates summaries, CSV "
204
+ "exports, and a hoverable dashboard with optional localhost-only raw "
205
+ "context loading."
206
+ ),
207
+ "developerName": "Douglas Monsky",
208
+ "category": "Productivity",
209
+ "capabilities": ["Interactive", "Read", "Write"],
210
+ "websiteURL": "https://github.com/douglasmonsky/codex-usage-tracker",
211
+ "privacyPolicyURL": "https://github.com/douglasmonsky/codex-usage-tracker",
212
+ "termsOfServiceURL": "https://github.com/douglasmonsky/codex-usage-tracker",
213
+ "defaultPrompt": [
214
+ "Open dashboard",
215
+ "Heaviest thread?",
216
+ "Thread leaderboard",
217
+ ],
218
+ "brandColor": "#2563EB",
219
+ "composerIcon": "./assets/icon.svg",
220
+ "logo": "./assets/icon.svg",
221
+ "screenshots": [],
222
+ },
223
+ }
224
+
225
+
226
+ def _mcp_config(python_executable: Path) -> dict[str, Any]:
227
+ server: dict[str, Any] = {
228
+ "command": str(python_executable),
229
+ "args": ["-m", "codex_usage_tracker.mcp_server"],
230
+ "cwd": ".",
231
+ }
232
+ source_root = _source_checkout_for_python(python_executable)
233
+ if source_root:
234
+ server["env"] = {"PYTHONPATH": str(source_root / "src")}
235
+ return {"mcpServers": {PLUGIN_NAME: server}}
236
+
237
+
238
+ def _source_checkout_for_python(python_executable: Path) -> Path | None:
239
+ """Return a source checkout root when the Python executable lives in its venv."""
240
+
241
+ path = python_executable.expanduser()
242
+ parents = list(path.parents)
243
+ if len(parents) < 3:
244
+ return None
245
+ candidate = parents[2]
246
+ if (candidate / "src" / "codex_usage_tracker").is_dir() and (
247
+ candidate / "pyproject.toml"
248
+ ).exists():
249
+ return candidate
250
+ return None
251
+
252
+
253
+ def _load_marketplace(path: Path) -> dict[str, Any]:
254
+ if not path.exists():
255
+ return {
256
+ "name": "local",
257
+ "interface": {"displayName": "Local Plugins"},
258
+ "plugins": [],
259
+ }
260
+ try:
261
+ data = json.loads(path.read_text(encoding="utf-8"))
262
+ except json.JSONDecodeError as exc:
263
+ raise SystemExit(f"Invalid marketplace JSON at {path}: {exc}") from exc
264
+ if not isinstance(data, dict):
265
+ raise SystemExit(f"Marketplace JSON must be an object: {path}")
266
+ data.setdefault("name", "local")
267
+ data.setdefault("interface", {"displayName": "Local Plugins"})
268
+ data.setdefault("plugins", [])
269
+ if not isinstance(data["plugins"], list):
270
+ raise SystemExit(f"Marketplace plugins field must be a list: {path}")
271
+ return data
272
+
273
+
274
+ def _upsert_marketplace_entry(marketplace: dict[str, Any], plugin_dir: Path) -> None:
275
+ entry = {
276
+ "name": PLUGIN_NAME,
277
+ "source": {
278
+ "source": "local",
279
+ "path": _marketplace_plugin_path(plugin_dir),
280
+ },
281
+ "policy": {
282
+ "installation": "AVAILABLE",
283
+ "authentication": "ON_INSTALL",
284
+ },
285
+ "category": "Productivity",
286
+ }
287
+ plugins = marketplace["plugins"]
288
+ for index, existing in enumerate(plugins):
289
+ if isinstance(existing, dict) and existing.get("name") == PLUGIN_NAME:
290
+ plugins[index] = entry
291
+ return
292
+ plugins.append(entry)
293
+
294
+
295
+ def _remove_marketplace_entry(marketplace: dict[str, Any]) -> bool:
296
+ plugins = marketplace["plugins"]
297
+ before = len(plugins)
298
+ marketplace["plugins"] = [
299
+ entry
300
+ for entry in plugins
301
+ if not (isinstance(entry, dict) and entry.get("name") == PLUGIN_NAME)
302
+ ]
303
+ return len(marketplace["plugins"]) != before
304
+
305
+
306
+ def _marketplace_plugin_path(plugin_dir: Path) -> str:
307
+ default_parent = Path.home() / "plugins"
308
+ try:
309
+ relative = plugin_dir.resolve().relative_to(default_parent.resolve())
310
+ except ValueError:
311
+ return str(plugin_dir.resolve())
312
+ return f"./plugins/{relative.as_posix()}"