prompt-analytics-for-claude-code 0.3.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 (86) hide show
  1. prompt_analytics_for_claude_code-0.3.0/.env.example +6 -0
  2. prompt_analytics_for_claude_code-0.3.0/.gitattributes +13 -0
  3. prompt_analytics_for_claude_code-0.3.0/.github/workflows/ci.yml +100 -0
  4. prompt_analytics_for_claude_code-0.3.0/.github/workflows/pricing-drift.yml +157 -0
  5. prompt_analytics_for_claude_code-0.3.0/.github/workflows/release.yml +32 -0
  6. prompt_analytics_for_claude_code-0.3.0/.gitignore +25 -0
  7. prompt_analytics_for_claude_code-0.3.0/.streamlit/config.toml +18 -0
  8. prompt_analytics_for_claude_code-0.3.0/CHANGELOG.md +50 -0
  9. prompt_analytics_for_claude_code-0.3.0/CONTRIBUTING.md +111 -0
  10. prompt_analytics_for_claude_code-0.3.0/LICENSE +21 -0
  11. prompt_analytics_for_claude_code-0.3.0/PKG-INFO +348 -0
  12. prompt_analytics_for_claude_code-0.3.0/README.md +301 -0
  13. prompt_analytics_for_claude_code-0.3.0/demo_data/categories.csv +799 -0
  14. prompt_analytics_for_claude_code-0.3.0/demo_data/config.yml +4 -0
  15. prompt_analytics_for_claude_code-0.3.0/demo_data/prompts.csv +799 -0
  16. prompt_analytics_for_claude_code-0.3.0/demo_data/quota_log.csv +181 -0
  17. prompt_analytics_for_claude_code-0.3.0/demo_data/requests.csv +2766 -0
  18. prompt_analytics_for_claude_code-0.3.0/demo_data/sessions.csv +81 -0
  19. prompt_analytics_for_claude_code-0.3.0/demo_data/token_types.csv +7 -0
  20. prompt_analytics_for_claude_code-0.3.0/demo_data/tokens.csv +4325 -0
  21. prompt_analytics_for_claude_code-0.3.0/docs/architecture.md +162 -0
  22. prompt_analytics_for_claude_code-0.3.0/docs/dashboard.md +115 -0
  23. prompt_analytics_for_claude_code-0.3.0/docs/screenshots/cli-by-category.png +0 -0
  24. prompt_analytics_for_claude_code-0.3.0/docs/screenshots/dashboard-home.png +0 -0
  25. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/__init__.py +5 -0
  26. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/__main__.py +18 -0
  27. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/analytics.py +2206 -0
  28. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/categorize.py +1021 -0
  29. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/cli.py +1123 -0
  30. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/config.py +105 -0
  31. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/__init__.py +1 -0
  32. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/app.py +377 -0
  33. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/data.py +449 -0
  34. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/echarts.py +350 -0
  35. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/filters.py +463 -0
  36. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/10_how_it_works.py +256 -0
  37. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/11_explorer.py +375 -0
  38. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/1_overview.py +431 -0
  39. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/2_models.py +322 -0
  40. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/3_prompts.py +406 -0
  41. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/4_session_depth.py +353 -0
  42. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/5_sessions.py +418 -0
  43. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/6_optimize.py +323 -0
  44. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/7_quotas.py +512 -0
  45. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/pages/__init__.py +1 -0
  46. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/dashboard/theme.py +329 -0
  47. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/data/__init__.py +0 -0
  48. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/data/pricing.yml +270 -0
  49. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/extract.py +1086 -0
  50. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/paths.py +60 -0
  51. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/pricing.py +247 -0
  52. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/py.typed +0 -0
  53. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/render.py +143 -0
  54. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/schema.py +295 -0
  55. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/snapshot.py +203 -0
  56. prompt_analytics_for_claude_code-0.3.0/prompt_analytics/storage.py +107 -0
  57. prompt_analytics_for_claude_code-0.3.0/pyproject.toml +130 -0
  58. prompt_analytics_for_claude_code-0.3.0/requirements.txt +9 -0
  59. prompt_analytics_for_claude_code-0.3.0/scripts/capture_fixture.py +238 -0
  60. prompt_analytics_for_claude_code-0.3.0/scripts/fetch_copilot_pricing.py +200 -0
  61. prompt_analytics_for_claude_code-0.3.0/scripts/generate_demo_data.py +527 -0
  62. prompt_analytics_for_claude_code-0.3.0/scripts/reconcile_ccusage.py +235 -0
  63. prompt_analytics_for_claude_code-0.3.0/tests/__init__.py +1 -0
  64. prompt_analytics_for_claude_code-0.3.0/tests/conftest.py +71 -0
  65. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/.gitkeep +0 -0
  66. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/README.md +33 -0
  67. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/agent_delta.jsonl +2 -0
  68. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/claude-code-2.1.173/demo-project/d42b86da-418a-49a0-842a-43af6212339e.jsonl +94 -0
  69. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/session_alpha.jsonl +6 -0
  70. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/session_beta.jsonl +4 -0
  71. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/session_delta.jsonl +17 -0
  72. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/session_delta_resumed.jsonl +4 -0
  73. prompt_analytics_for_claude_code-0.3.0/tests/fixtures/session_gamma.jsonl +2 -0
  74. prompt_analytics_for_claude_code-0.3.0/tests/test_analytics.py +548 -0
  75. prompt_analytics_for_claude_code-0.3.0/tests/test_analytics_power.py +368 -0
  76. prompt_analytics_for_claude_code-0.3.0/tests/test_analytics_requests.py +368 -0
  77. prompt_analytics_for_claude_code-0.3.0/tests/test_categorize.py +878 -0
  78. prompt_analytics_for_claude_code-0.3.0/tests/test_cli.py +560 -0
  79. prompt_analytics_for_claude_code-0.3.0/tests/test_config.py +47 -0
  80. prompt_analytics_for_claude_code-0.3.0/tests/test_dashboard.py +597 -0
  81. prompt_analytics_for_claude_code-0.3.0/tests/test_demo_data.py +99 -0
  82. prompt_analytics_for_claude_code-0.3.0/tests/test_extract.py +692 -0
  83. prompt_analytics_for_claude_code-0.3.0/tests/test_fixtures_versioned.py +54 -0
  84. prompt_analytics_for_claude_code-0.3.0/tests/test_pricing.py +405 -0
  85. prompt_analytics_for_claude_code-0.3.0/tests/test_snapshot.py +259 -0
  86. prompt_analytics_for_claude_code-0.3.0/tests/test_storage.py +132 -0
@@ -0,0 +1,6 @@
1
+ # LLM providers for optional categorization (only one required)
2
+ # Option 1: Anthropic direct (recommended — you already have a Claude account)
3
+ ANTHROPIC_API_KEY=
4
+
5
+ # Option 2: OpenRouter (fallback)
6
+ OPENROUTER_API_KEY=
@@ -0,0 +1,13 @@
1
+ # Lock line endings to LF in the repository regardless of each contributor's
2
+ # core.autocrlf: fixtures (.jsonl) and demo CSVs are test *data* whose bytes
3
+ # must be stable across checkouts (cross-platform audit 2026-06-11, M1).
4
+ * text=auto eol=lf
5
+ *.py text eol=lf
6
+ *.md text eol=lf
7
+ *.yml text eol=lf
8
+ *.yaml text eol=lf
9
+ *.toml text eol=lf
10
+ *.json text eol=lf
11
+ *.jsonl text eol=lf
12
+ *.csv text eol=lf
13
+ *.png binary
@@ -0,0 +1,100 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: test (py${{ matrix.python-version }} · ${{ matrix.os }})
16
+ runs-on: ${{ matrix.os }}
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os: [ubuntu-latest, windows-latest]
21
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
22
+ include:
23
+ - os: macos-latest
24
+ python-version: "3.12"
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v5
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ enable-cache: true
33
+ cache-dependency-glob: "pyproject.toml"
34
+
35
+ - name: Install project (all extras)
36
+ run: uv sync --all-extras
37
+
38
+ - name: Ruff lint
39
+ run: uv run ruff check .
40
+
41
+ - name: Ruff format check
42
+ run: uv run ruff format --check .
43
+
44
+ - name: Type check (package + dashboard + tests)
45
+ run: uv run mypy prompt_analytics tests
46
+
47
+ - name: Tests with coverage (fails under 85%)
48
+ run: uv run pytest tests/ -q
49
+
50
+ package:
51
+ name: build · twine check · clean-venv smoke test (${{ matrix.os }})
52
+ runs-on: ${{ matrix.os }}
53
+ strategy:
54
+ fail-fast: false
55
+ matrix:
56
+ os: [ubuntu-latest, windows-latest]
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+
60
+ - name: Install uv
61
+ uses: astral-sh/setup-uv@v5
62
+ with:
63
+ python-version: "3.12"
64
+ enable-cache: true
65
+ cache-dependency-glob: "pyproject.toml"
66
+
67
+ - name: Build sdist + wheel
68
+ run: uv build
69
+
70
+ - name: twine check
71
+ run: uvx twine check dist/*
72
+
73
+ - name: Install the wheel into a clean virtualenv and smoke-test it
74
+ shell: bash
75
+ run: |
76
+ set -euxo pipefail
77
+ python -m venv clean
78
+ # bin/ on POSIX, Scripts/ on Windows (Git Bash on the Windows runner)
79
+ if [ -d clean/bin ]; then BIN=clean/bin; else BIN=clean/Scripts; fi
80
+ "$BIN/pip" install dist/*.whl
81
+ "$BIN/prompt-analytics" --help
82
+ # Analysis commands must not crash on an empty history; exit 1 = "no
83
+ # data" is expected here. Assert the exact code: exit 2 would be a
84
+ # traceback, which is a bug.
85
+ rc=0
86
+ "$BIN/prompt-analytics" summary --output-dir empty_out || rc=$?
87
+ if [ "$rc" -ne 1 ]; then
88
+ echo "Expected exit 1 from summary on empty dir, got $rc" >&2
89
+ exit 1
90
+ fi
91
+ # Timezone canary: tzdata must resolve Europe/Paris on the bare wheel
92
+ # (m5 — the Windows runner needs tzdata bundled as a dep).
93
+ # Exit 0 (empty data written) or 1 (no data) are both OK; exit 2
94
+ # means a ValueError / ImportError and is a bug.
95
+ tz=0
96
+ "$BIN/prompt-analytics" extract --timezone Europe/Paris --output-dir tz_out || tz=$?
97
+ if [ "$tz" -eq 2 ]; then
98
+ echo "extract --timezone Europe/Paris exited 2 (timezone not found on bare wheel)" >&2
99
+ exit 1
100
+ fi
@@ -0,0 +1,157 @@
1
+ name: Pricing drift check
2
+
3
+ on:
4
+ schedule:
5
+ # Every Monday at 08:00 UTC
6
+ - cron: "0 8 * * 1"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: write
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ drift:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install pyyaml requests
28
+
29
+ - name: Fetch LiteLLM model prices
30
+ run: |
31
+ python - << 'EOF'
32
+ import json, urllib.request, sys, yaml, pathlib
33
+
34
+ LITELLM_URL = (
35
+ "https://raw.githubusercontent.com/BerriAI/litellm/main/"
36
+ "model_prices_and_context_window.json"
37
+ )
38
+ with urllib.request.urlopen(LITELLM_URL, timeout=30) as r:
39
+ litellm: dict = json.loads(r.read().decode())
40
+
41
+ # Claude model keys in LiteLLM use the form "claude/claude-opus-4-8" or
42
+ # just "claude-opus-4-8"; we normalise to the bare model ID.
43
+ def price_per_mtok(v, key):
44
+ val = v.get(key)
45
+ return round(float(val) * 1_000_000, 6) if val is not None else None
46
+
47
+ drift = []
48
+ pricing_yml = pathlib.Path("prompt_analytics/data/pricing.yml")
49
+ data = yaml.safe_load(pricing_yml.read_text(encoding="utf-8"))
50
+ embedded = data.get("providers", {}).get("anthropic", {}).get("models", {})
51
+
52
+ for model_id, entry in embedded.items():
53
+ # Try common LiteLLM key forms
54
+ candidates = [model_id, f"claude/{model_id}", f"anthropic/{model_id}"]
55
+ litellm_entry = None
56
+ for c in candidates:
57
+ if c in litellm:
58
+ litellm_entry = litellm[c]
59
+ break
60
+ if litellm_entry is None:
61
+ print(f" skip {model_id}: not in LiteLLM", flush=True)
62
+ continue
63
+
64
+ checks = [
65
+ ("input", "input_cost_per_token"),
66
+ ("output", "output_cost_per_token"),
67
+ ("cache_read", "cache_read_input_token_cost"),
68
+ ("cache_write_5m", "cache_creation_input_token_cost"),
69
+ ]
70
+ for our_key, ll_key in checks:
71
+ ll_val = price_per_mtok(litellm_entry, ll_key)
72
+ if ll_val is None:
73
+ continue
74
+ our_val = entry.get(our_key)
75
+ if our_val is None:
76
+ continue
77
+ # Allow 1 % tolerance for rounding
78
+ if abs(ll_val - our_val) / max(abs(ll_val), 1e-9) > 0.01:
79
+ drift.append(
80
+ f" {model_id}/{our_key}: ours={our_val}, LiteLLM={ll_val}"
81
+ )
82
+
83
+ if drift:
84
+ print("DRIFT DETECTED (Anthropic vs LiteLLM):", flush=True)
85
+ for line in drift:
86
+ print(line, flush=True)
87
+ pathlib.Path("drift_litellm.txt").write_text("\n".join(drift) + "\n", encoding="utf-8")
88
+ else:
89
+ print("No drift vs LiteLLM.", flush=True)
90
+ EOF
91
+
92
+ - name: Fetch Copilot pricing
93
+ run: |
94
+ pip install pyyaml # already installed but be explicit
95
+ python scripts/fetch_copilot_pricing.py --output /tmp/copilot_fresh.yml
96
+
97
+ - name: Compare Copilot pricing
98
+ run: |
99
+ python - << 'EOF'
100
+ import yaml, pathlib, sys
101
+
102
+ fresh = yaml.safe_load(
103
+ pathlib.Path("/tmp/copilot_fresh.yml").read_text(encoding="utf-8")
104
+ )
105
+ pricing_yml = pathlib.Path("prompt_analytics/data/pricing.yml")
106
+ data = yaml.safe_load(pricing_yml.read_text(encoding="utf-8"))
107
+ embedded = data.get("providers", {}).get("copilot", {}).get("models", {})
108
+ fresh_claude = fresh.get("vendors", {}).get("anthropic", {})
109
+
110
+ drift = []
111
+ for model_id, fresh_entry in fresh_claude.items():
112
+ our_entry = embedded.get(model_id)
113
+ if our_entry is None:
114
+ drift.append(f" NEW model {model_id}: not in embedded copilot grid")
115
+ continue
116
+ for key in ("input", "output", "cache_read"):
117
+ fv = fresh_entry.get(key)
118
+ ov = our_entry.get(key)
119
+ if fv is None or ov is None:
120
+ continue
121
+ if abs(fv - ov) / max(abs(fv), 1e-9) > 0.01:
122
+ drift.append(f" {model_id}/{key}: ours={ov}, Copilot page={fv}")
123
+ # cache_write in fresh → compare against cache_write_5m
124
+ fw = fresh_entry.get("cache_write")
125
+ ow = our_entry.get("cache_write_5m")
126
+ if fw is not None and ow is not None:
127
+ if abs(fw - ow) / max(abs(fw), 1e-9) > 0.01:
128
+ drift.append(f" {model_id}/cache_write_5m: ours={ow}, Copilot page={fw}")
129
+
130
+ if drift:
131
+ print("DRIFT DETECTED (Copilot):", flush=True)
132
+ for line in drift:
133
+ print(line, flush=True)
134
+ with open("drift_litellm.txt", "a", encoding="utf-8") as f:
135
+ f.write("\n".join(drift) + "\n")
136
+ else:
137
+ print("No drift vs Copilot page.", flush=True)
138
+ EOF
139
+
140
+ - name: Open PR if drift detected
141
+ if: ${{ hashFiles('drift_litellm.txt') != '' }}
142
+ env:
143
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
144
+ run: |
145
+ BRANCH="pricing-drift-$(date +%Y%m%d)"
146
+ git config user.name "github-actions[bot]"
147
+ git config user.email "github-actions[bot]@users.noreply.github.com"
148
+ git checkout -b "$BRANCH"
149
+ git add -A
150
+ git commit -m "chore: pricing drift detected $(date +%Y-%m-%d)" --allow-empty
151
+ git push origin "$BRANCH"
152
+ DRIFT=$(cat drift_litellm.txt)
153
+ gh pr create \
154
+ --title "Pricing drift detected $(date +%Y-%m-%d)" \
155
+ --body "$(printf '## Pricing drift\n\nThe weekly pricing check found the following differences between \`pricing.yml\` and the upstream sources (LiteLLM / Copilot page).\n\n```\n%s\n```\n\nPlease update \`prompt_analytics/data/pricing.yml\` accordingly.' "$DRIFT")" \
156
+ --base main \
157
+ --head "$BRANCH"
@@ -0,0 +1,32 @@
1
+ name: Release
2
+
3
+ # Build and publish to PyPI on a version tag, using PyPI trusted publishing
4
+ # (OIDC — no API token stored). The PyPI project's trusted publisher must match:
5
+ # owner: romainfjgaspard
6
+ # repository: prompt-analytics-for-claude-code
7
+ # workflow: release.yml
8
+ # environment: pypi
9
+ on:
10
+ push:
11
+ tags: ["v*"]
12
+
13
+ jobs:
14
+ pypi:
15
+ name: Build & publish to PyPI
16
+ runs-on: ubuntu-latest
17
+ environment: pypi
18
+ permissions:
19
+ id-token: write # required for trusted publishing
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ - name: Build sdist + wheel
29
+ run: uv build
30
+
31
+ - name: Publish to PyPI (trusted publishing)
32
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,25 @@
1
+ output/
2
+ !output/.gitkeep
3
+ .env
4
+ __pycache__/
5
+ *.pyc
6
+ *.log
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .coverage
11
+ .coverage.*
12
+ htmlcov/
13
+ .idea/
14
+ .claude/
15
+ dist/
16
+ *.egg-info/
17
+ .venv/
18
+ build/
19
+ # Not committed: Streamlit Community Cloud prioritizes uv.lock over
20
+ # requirements.txt and `uv sync` skips the `dashboard` extra, so the hosted demo
21
+ # would miss streamlit-echarts. Cloud uses requirements.txt (.[dashboard]); CI
22
+ # and local dev resolve from pyproject.toml.
23
+ uv.lock
24
+ .streamlit/secrets.toml
25
+ scripts/reconcile_dump.json
@@ -0,0 +1,18 @@
1
+ # Dashboard theme — dark, forced (no light/dark toggle).
2
+ #
3
+ # A single dark theme (deep navy, Anthropic coral accent, Space Grotesk). Defining
4
+ # only [theme] (no [theme.light]/[theme.dark] pair) forces dark for every visitor
5
+ # and removes the in-app theme switch — deliberate: this is a dark-first showcase
6
+ # (a [theme.light]/[theme.dark] pair would instead follow the visitor's OS).
7
+ [theme]
8
+ base = "dark"
9
+ primaryColor = "#D97757"
10
+ backgroundColor = "#0B1220"
11
+ secondaryBackgroundColor = "#111827"
12
+ textColor = "#F8FAFC"
13
+ borderColor = "#2B3954"
14
+ font = "'Space Grotesk':https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap"
15
+ headingFont = "Space Grotesk"
16
+ codeFont = "'JetBrains Mono':https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400..600&display=swap"
17
+ baseFontSize = 14
18
+ showWidgetBorder = true
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — kept at
6
+ `0.x` on purpose: the upstream Claude Code JSONL format is unstable, so parsing
7
+ breakage is treated as expected and reflected in the version.
8
+
9
+ ## [0.3.0] — 2026-06-15 — Initial public release
10
+
11
+ Prompt-level analytics for Claude Code: every prompt in your local
12
+ `~/.claude/projects/**/*.jsonl` logs becomes one priced row — no account, no API
13
+ key, nothing leaves your machine. Two surfaces on the same data: a terminal CLI
14
+ and a Streamlit dashboard.
15
+
16
+ ### Highlights
17
+ - **Per-prompt dataset** — tokens and cost per prompt, project, model, category
18
+ and session, with a Pareto view of where the spend concentrates.
19
+ - **Power-user analyses at the request grain** — `context` (accumulated context
20
+ by session depth), `ttl` (cache-TTL expiry losses), `compactions`, `overhead`,
21
+ `by-token-type` (the **context-rent** share of the bill), `model-category
22
+ --whatif`, `recommend`, `burn-rate`, and `break-even` (is a Pro/Max plan worth
23
+ it vs the API?).
24
+ - **Automatic categorization** — a local, zero-dependency FR+EN heuristic labels
25
+ every prompt across eleven categories and scores its *observed* complexity 1–5;
26
+ an optional LLM pass (Anthropic / OpenRouter / local Ollama) refines it.
27
+ - **Streamlit dashboard** — Apache ECharts on a dark-by-default theme, global
28
+ cross-filtering (click to filter, brush a date range), an Explorer drill-down
29
+ (day → session → prompt), and a public synthetic-data demo.
30
+ - **Accurate by construction** — global cross-file deduplication on
31
+ `message.id + requestId` (fixes the ~2.5× token inflation and double-counted
32
+ resumed / `--resume` sessions), fake-prompt filtering, an explicit subagent
33
+ policy, and cache writes split by TTL (5m vs 1h). Totals reconcile
34
+ bucket-for-bucket with [ccusage](https://github.com/ryoppippi/ccusage) on real
35
+ history.
36
+ - **Generic multi-provider pricing** — `pricing.yml` ships `anthropic` and
37
+ `copilot` grids and accepts any rate card; costs are computed at read time from
38
+ raw counts, so a pricing change never needs a re-extract.
39
+ - **Inspectable exports** — relational CSVs keyed by `prompt_id` / `session_id`,
40
+ `--format table|csv|json` on every command, and `export --flat` for Excel/BI.
41
+ - **`snapshot`** — records plan quota utilization over time via the OAuth usage
42
+ endpoint Claude Code already uses (kept out of any public API; fails
43
+ gracefully).
44
+
45
+ ### Privacy
46
+ Fully local by default: `extract` and every analysis command touch only your
47
+ local logs. `snapshot` calls Anthropic's own OAuth usage endpoint with your
48
+ existing token; `categorize --llm` sends prompt excerpts only to the provider you
49
+ choose (OpenRouter is a third party). See the README's Privacy section for the
50
+ full read/write/network breakdown.
@@ -0,0 +1,111 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in `prompt-analytics-for-claude-code`. This is a small,
4
+ quasi-stdlib tool with a strict quality bar (typed, tested, linted). PRs are
5
+ welcome — please run the checks below before submitting.
6
+
7
+ ## Dev setup
8
+
9
+ The project uses [uv](https://docs.astral.sh/uv/). Clone, then sync all extras
10
+ (core + `categorize` + `dashboard` + `dev`):
11
+
12
+ ```bash
13
+ uv sync --all-extras
14
+ ```
15
+
16
+ Run the full local CI — the same steps the GitHub workflow runs:
17
+
18
+ ```bash
19
+ uv run ruff check .
20
+ uv run ruff format --check .
21
+ uv run mypy prompt_analytics tests
22
+ uv run pytest # coverage gate: 85%
23
+ ```
24
+
25
+ Architecture and the data flow (`extract → analytics → {cli, dashboard, csv}`)
26
+ are documented in [`docs/architecture.md`](docs/architecture.md). The CSV column
27
+ contract lives in one place, [`prompt_analytics/schema.py`](prompt_analytics/schema.py)
28
+ — change a column there and every writer/reader follows.
29
+
30
+ A few ground rules:
31
+
32
+ - **The core stays light.** Core depends only on `python-dotenv`, `pyyaml`, and
33
+ `rich`. Heavy deps belong in extras: `anthropic`/`openai` in `categorize`,
34
+ `streamlit`/`plotly`/`pandas`/`numpy` in `dashboard`. Don't import an extra
35
+ from core code.
36
+ - **`tokens.csv` stores raw counts, never costs.** Costs are computed at read
37
+ time in `analytics.py` from the pricing grid, so a pricing change never
38
+ requires a re-extract. Keep it that way.
39
+ - **Every bug fix gets a regression test.** The happy path is not enough.
40
+
41
+ ## Adding a pricing provider
42
+
43
+ Pricing is a generic multi-provider grid in
44
+ [`prompt_analytics/data/pricing.yml`](prompt_analytics/data/pricing.yml). To add
45
+ a provider (your company's internal rates, a Bedrock tier, …), add a key under
46
+ `providers:`. All prices are **USD per 1,000,000 tokens**:
47
+
48
+ ```yaml
49
+ providers:
50
+ my-company:
51
+ models:
52
+ claude-opus-4-8:
53
+ input: 4.50 # negotiated rate
54
+ output: 22.50
55
+ cache_read: 0.45
56
+ cache_write_5m: 5.625
57
+ cache_write_1h: 9.00
58
+ fallbacks: # matched by longest model-name prefix
59
+ claude-opus:
60
+ input: 4.50
61
+ output: 22.50
62
+ cache_read: 0.45
63
+ cache_write_5m: 5.625
64
+ cache_write_1h: 9.00
65
+ ```
66
+
67
+ Then use it with `--providers anthropic,my-company` on `compare`, or `--provider
68
+ my-company` on the other commands. Users can keep their grid out of the repo and
69
+ pass it with `--pricing ./my-pricing.yml`.
70
+
71
+ Lookup rules (see `pricing.get_model_pricing`): an exact model id wins; otherwise
72
+ the **longest matching prefix** under `fallbacks` is used; `[1m]` and
73
+ long-context suffixes are stripped before lookup. An unpriced model is never
74
+ silently zeroed — it surfaces in the extraction/analytics report so you know to
75
+ add an entry.
76
+
77
+ The bundled `anthropic` and `copilot` grids are validated weekly by CI
78
+ (`.github/workflows/pricing-drift.yml`): the job diffs `anthropic` against
79
+ LiteLLM's `model_prices_and_context_window.json` and re-runs
80
+ `scripts/fetch_copilot_pricing.py` against the live Copilot pricing page, opening
81
+ a PR if either drifts. Update those two grids through that job, not by hand.
82
+
83
+ ## Capturing a test fixture
84
+
85
+ Claude Code's JSONL format changes without notice, so parsing is pinned against
86
+ fixture files **per Claude Code version**, under
87
+ `tests/fixtures/claude-code-<version>/`. When you hit a new format, capture one
88
+ from your own logs — anonymized, so it is safe to commit:
89
+
90
+ ```bash
91
+ uv run python scripts/capture_fixture.py path/to/real/session.jsonl
92
+ # --version 2.1.180 override the auto-detected version label
93
+ # --project demo-app generic project folder name in the fixture
94
+ ```
95
+
96
+ `capture_fixture.py` rewrites the log **character by character** (letters → `x`,
97
+ digits → `0`, punctuation and length preserved) while keeping everything the
98
+ parser counts: structure, ids, the `uuid`/`parentUuid` attribution chain,
99
+ `message.usage`, `model`, `timestamp`, `version`, and the filtering markers
100
+ (`<command-name>`, `[Request interrupted…`). It scrubs your username, paths and
101
+ prompt text — verify the result before committing. `test_fixtures_versioned.py`
102
+ then acts as a drift canary: it asserts the fixture parses cleanly (no invalid
103
+ lines, no unknown event types) and deterministically.
104
+
105
+ ## Submitting
106
+
107
+ - Branch off `main`, keep commits focused, and describe the *why* in the PR.
108
+ - Add or update tests and docs (README / `docs/`) alongside the code.
109
+ - Update [`CHANGELOG.md`](CHANGELOG.md) under `[Unreleased]`.
110
+ - Make sure the four checks above pass locally — CI runs them on Python
111
+ 3.10–3.14 across Ubuntu and Windows.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Romain Gaspard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.