mycroftcompute 0.1.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 (43) hide show
  1. mycroftcompute-0.1.0/.gitignore +13 -0
  2. mycroftcompute-0.1.0/LICENSE +21 -0
  3. mycroftcompute-0.1.0/PKG-INFO +156 -0
  4. mycroftcompute-0.1.0/README.md +133 -0
  5. mycroftcompute-0.1.0/pyproject.toml +72 -0
  6. mycroftcompute-0.1.0/src/mycroftcompute/__init__.py +0 -0
  7. mycroftcompute-0.1.0/src/mycroftcompute/__main__.py +282 -0
  8. mycroftcompute-0.1.0/src/mycroftcompute/analysis/__init__.py +1 -0
  9. mycroftcompute-0.1.0/src/mycroftcompute/analysis/costing.py +168 -0
  10. mycroftcompute-0.1.0/src/mycroftcompute/analysis/findings.py +576 -0
  11. mycroftcompute-0.1.0/src/mycroftcompute/analysis/team.py +393 -0
  12. mycroftcompute-0.1.0/src/mycroftcompute/economics/__init__.py +1 -0
  13. mycroftcompute-0.1.0/src/mycroftcompute/economics/caching.py +328 -0
  14. mycroftcompute-0.1.0/src/mycroftcompute/economics/prices.py +444 -0
  15. mycroftcompute-0.1.0/src/mycroftcompute/parsers/__init__.py +1 -0
  16. mycroftcompute-0.1.0/src/mycroftcompute/parsers/claude_code.py +219 -0
  17. mycroftcompute-0.1.0/src/mycroftcompute/parsers/model.py +176 -0
  18. mycroftcompute-0.1.0/src/mycroftcompute/report/__init__.py +1 -0
  19. mycroftcompute-0.1.0/src/mycroftcompute/report/readiness.py +211 -0
  20. mycroftcompute-0.1.0/src/mycroftcompute/report/text.py +512 -0
  21. mycroftcompute-0.1.0/src/mycroftcompute/sample.py +173 -0
  22. mycroftcompute-0.1.0/src/mycroftcompute/sources/__init__.py +1 -0
  23. mycroftcompute-0.1.0/src/mycroftcompute/sources/base.py +35 -0
  24. mycroftcompute-0.1.0/src/mycroftcompute/sources/datadog.py +766 -0
  25. mycroftcompute-0.1.0/src/mycroftcompute/sources/grafana.py +580 -0
  26. mycroftcompute-0.1.0/src/mycroftcompute/sources/merging.py +156 -0
  27. mycroftcompute-0.1.0/src/mycroftcompute/sources/metrics_names.py +217 -0
  28. mycroftcompute-0.1.0/src/mycroftcompute/sources/records.py +172 -0
  29. mycroftcompute-0.1.0/src/mycroftcompute/store/__init__.py +1 -0
  30. mycroftcompute-0.1.0/src/mycroftcompute/store/history.py +456 -0
  31. mycroftcompute-0.1.0/src/mycroftcompute/store/schema.py +86 -0
  32. mycroftcompute-0.1.0/src/mycroftcompute/vendors.py +17 -0
  33. mycroftcompute-0.1.0/tests/test_caching.py +384 -0
  34. mycroftcompute-0.1.0/tests/test_claude_code.py +193 -0
  35. mycroftcompute-0.1.0/tests/test_cli.py +229 -0
  36. mycroftcompute-0.1.0/tests/test_datadog_source.py +510 -0
  37. mycroftcompute-0.1.0/tests/test_findings.py +497 -0
  38. mycroftcompute-0.1.0/tests/test_grafana_source.py +560 -0
  39. mycroftcompute-0.1.0/tests/test_history.py +241 -0
  40. mycroftcompute-0.1.0/tests/test_metric_families.py +244 -0
  41. mycroftcompute-0.1.0/tests/test_readiness.py +165 -0
  42. mycroftcompute-0.1.0/tests/test_report.py +268 -0
  43. mycroftcompute-0.1.0/tests/test_team.py +470 -0
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ *.db
11
+ .env
12
+ .env.*
13
+ .claude/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aditya Negi
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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: mycroftcompute
3
+ Version: 0.1.0
4
+ Summary: Where your Claude Code money went, priced with the prompt cache counted, and which changes would actually pay.
5
+ Project-URL: Homepage, https://www.mycroftcompute.com
6
+ Project-URL: Repository, https://github.com/negi6711/mycroftcompute
7
+ Project-URL: Issues, https://github.com/negi6711/mycroftcompute/issues
8
+ Author: Aditya Negi
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: anthropic,claude-code,cost,datadog,finops,grafana,llm,opentelemetry,prompt-caching
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development
20
+ Classifier: Topic :: System :: Monitoring
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+
24
+ # mycroftcompute
25
+
26
+ **Where your Claude Code money went, priced with the prompt cache counted, and which changes would
27
+ actually pay.**
28
+
29
+ [ccusage](https://github.com/ccusage/ccusage) tells you what Claude Code cost you. mycroftcompute
30
+ tells you what's worth changing, and what that change is worth at most.
31
+
32
+ ```sh
33
+ uvx mycroftcompute # read your own Claude Code history (~/.claude/projects)
34
+ uvx mycroftcompute --sample # or see the report for an invented 24-developer team
35
+ ```
36
+
37
+ Python 3.12+, no dependencies. Nothing leaves your machine. `pipx install mycroftcompute` works
38
+ too.
39
+
40
+ ## What a finding looks like
41
+
42
+ From `--sample`:
43
+
44
+ ```
45
+ FINDING 3 - 23.7% of your bill went on writing the prompt cache
46
+ ------------------------------------------------------------------------------------
47
+
48
+ What your metrics backend shows
49
+ - 608,889,600 tokens were written to the prompt cache, costing $3,069.00 at the
50
+ five-minute write rate - 23.7% of the $12,928.43 priced here. At the one-hour
51
+ rate the same writes would cost $4,910.40.
52
+ - 15,713,280,000 tokens were read back from cache, about 25.8 reads for every
53
+ token written.
54
+ - Read from cache instead of written, the same tokens would have cost $245.52.
55
+
56
+ At most $2,823.48 over this period
57
+ This is the gap between what these tokens cost and what they would cost under
58
+ the cheaper option. It is a ceiling, not a saving - see below.
59
+
60
+ What your metrics backend cannot tell you
61
+ - How many of these writes were a warm prefix written again - after a new session,
62
+ a compaction, a model switch, a change to the tools or system prompt, or a pause
63
+ longer than the cache lives - and how many were new context that had to be
64
+ written once. Only the first kind can be avoided ...
65
+ ```
66
+
67
+ Every finding has the same three parts: what the data shows, the most a change could be worth,
68
+ and what the data cannot tell you.
69
+
70
+ ## Why the cache changes the answer
71
+
72
+ Claude Code already serves most of its input from the prompt cache, typically above 90%. So
73
+ "turn caching on" is not the saving. What costs money is **writing** the cache: each write is
74
+ billed above list price (1.25x for five minutes, 2x for an hour), and a model switch throws the
75
+ warm cache away.
76
+
77
+ That is why a cheaper model is not always cheaper. On a typical coding-agent step (about 141,000
78
+ cached prompt tokens and 1,000 fresh ones):
79
+
80
+ | Decision | Input cost per step |
81
+ |---|---|
82
+ | Stay on Sonnet 4.6, cache warm | $0.045 |
83
+ | First request after moving to Haiku 4.5 | $0.177 - **3.9x worse** |
84
+ | Every request after that, Haiku's cache warm | $0.015 - 3x better |
85
+ | Alternating between the two | $0.426 - **9.4x worse** |
86
+
87
+ The rule that falls out: choose the model when a session starts, then stay. Every router and
88
+ dashboard compares list prices; mycroftcompute counts the cache before it suggests a model change.
89
+
90
+ ## What it finds
91
+
92
+ | Finding | Fires when |
93
+ |---|---|
94
+ | **Cache writes** | Writing the cache is 20% or more of the bill. Ceiling: those tokens priced as reads instead. |
95
+ | **Model switch, net of cache** | A cheaper model would cost less once its own cache is warm. |
96
+ | **Housekeeping on an expensive model** | The agent's own background calls (`query_source = auxiliary`) run on a flagship model (team data: the label comes from telemetry). |
97
+ | **Uncached input** | Less than half of input comes from cache, which is unusual for Claude Code. |
98
+ | **Developer concentration** | A few people account for most of the spend (team data, 4+ developers). |
99
+ | **Continuous usage** | Spend doesn't drop at weekends, so something automated is driving it (team data). |
100
+
101
+ Run it again later with `--db history.db` and the report opens with **since last time**: what
102
+ appeared, what stopped firing, and how each ceiling moved, comparing equal windows.
103
+
104
+ ## For a team: Grafana or Datadog
105
+
106
+ If your team already sends Claude Code's telemetry to Grafana Cloud (or any Prometheus) or
107
+ Datadog, mycroftcompute reads it with one read-only credential:
108
+
109
+ ```sh
110
+ export MYCROFT_METRICS_TOKEN=glc_... # Cloud Access Policy token, metrics:read
111
+ mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --discover
112
+ mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --days 14
113
+
114
+ export MYCROFT_METRICS_TOKEN=... # Datadog API key
115
+ export MYCROFT_DATADOG_APP_KEY=... # Application key: metrics_read, timeseries_query
116
+ mycroftcompute --datadog us1 --days 14
117
+ ```
118
+
119
+ `--discover` checks the connection and lists what the telemetry can and cannot support before
120
+ reading any usage. Setup, including the two settings that silently lose every number, is in
121
+ [docs/team-backends.md](docs/team-backends.md). For a two-minute check you can run inside Grafana
122
+ or Datadog without installing anything, see [docs/cache-check.md](docs/cache-check.md).
123
+
124
+ Both backends were verified against live Claude Code telemetry in September 2026.
125
+
126
+ ## How the numbers are made
127
+
128
+ - **List prices**, checked by hand against each vendor's own pricing page, for 19 models from
129
+ Anthropic, OpenAI and Google, with cache read and write multipliers and per-model cache minimums.
130
+ Prices were last checked on **24 September 2026**; when the table is more than 31 days old the
131
+ report says so. Negotiated or batch rates are lower, so every figure is correspondingly high.
132
+ - **Ceilings, not savings.** Each figure is the most a change could be worth over the period.
133
+ Ceilings overlap, so they are never added up.
134
+ - **Your own transcripts** record each cache write's lifetime, so local reports price five-minute
135
+ and one-hour writes exactly. **Metrics backends** don't, so team reports price every write at
136
+ the cheaper five-minute rate and say so.
137
+ - **Read-only.** It never reads prompts, responses or code from a backend, and never changes
138
+ anything. Credentials come from the environment and are only used to query.
139
+
140
+ ## Status
141
+
142
+ A personal project. It started as the engine of a product for platform teams, which I stopped in
143
+ October 2026; the analysis was worth keeping. The price table will age between updates, and the
144
+ report tells you when it has.
145
+
146
+ ## Development
147
+
148
+ ```sh
149
+ uv sync
150
+ uv run pytest
151
+ uv run ruff check src tests && uv run mypy
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,133 @@
1
+ # mycroftcompute
2
+
3
+ **Where your Claude Code money went, priced with the prompt cache counted, and which changes would
4
+ actually pay.**
5
+
6
+ [ccusage](https://github.com/ccusage/ccusage) tells you what Claude Code cost you. mycroftcompute
7
+ tells you what's worth changing, and what that change is worth at most.
8
+
9
+ ```sh
10
+ uvx mycroftcompute # read your own Claude Code history (~/.claude/projects)
11
+ uvx mycroftcompute --sample # or see the report for an invented 24-developer team
12
+ ```
13
+
14
+ Python 3.12+, no dependencies. Nothing leaves your machine. `pipx install mycroftcompute` works
15
+ too.
16
+
17
+ ## What a finding looks like
18
+
19
+ From `--sample`:
20
+
21
+ ```
22
+ FINDING 3 - 23.7% of your bill went on writing the prompt cache
23
+ ------------------------------------------------------------------------------------
24
+
25
+ What your metrics backend shows
26
+ - 608,889,600 tokens were written to the prompt cache, costing $3,069.00 at the
27
+ five-minute write rate - 23.7% of the $12,928.43 priced here. At the one-hour
28
+ rate the same writes would cost $4,910.40.
29
+ - 15,713,280,000 tokens were read back from cache, about 25.8 reads for every
30
+ token written.
31
+ - Read from cache instead of written, the same tokens would have cost $245.52.
32
+
33
+ At most $2,823.48 over this period
34
+ This is the gap between what these tokens cost and what they would cost under
35
+ the cheaper option. It is a ceiling, not a saving - see below.
36
+
37
+ What your metrics backend cannot tell you
38
+ - How many of these writes were a warm prefix written again - after a new session,
39
+ a compaction, a model switch, a change to the tools or system prompt, or a pause
40
+ longer than the cache lives - and how many were new context that had to be
41
+ written once. Only the first kind can be avoided ...
42
+ ```
43
+
44
+ Every finding has the same three parts: what the data shows, the most a change could be worth,
45
+ and what the data cannot tell you.
46
+
47
+ ## Why the cache changes the answer
48
+
49
+ Claude Code already serves most of its input from the prompt cache, typically above 90%. So
50
+ "turn caching on" is not the saving. What costs money is **writing** the cache: each write is
51
+ billed above list price (1.25x for five minutes, 2x for an hour), and a model switch throws the
52
+ warm cache away.
53
+
54
+ That is why a cheaper model is not always cheaper. On a typical coding-agent step (about 141,000
55
+ cached prompt tokens and 1,000 fresh ones):
56
+
57
+ | Decision | Input cost per step |
58
+ |---|---|
59
+ | Stay on Sonnet 4.6, cache warm | $0.045 |
60
+ | First request after moving to Haiku 4.5 | $0.177 - **3.9x worse** |
61
+ | Every request after that, Haiku's cache warm | $0.015 - 3x better |
62
+ | Alternating between the two | $0.426 - **9.4x worse** |
63
+
64
+ The rule that falls out: choose the model when a session starts, then stay. Every router and
65
+ dashboard compares list prices; mycroftcompute counts the cache before it suggests a model change.
66
+
67
+ ## What it finds
68
+
69
+ | Finding | Fires when |
70
+ |---|---|
71
+ | **Cache writes** | Writing the cache is 20% or more of the bill. Ceiling: those tokens priced as reads instead. |
72
+ | **Model switch, net of cache** | A cheaper model would cost less once its own cache is warm. |
73
+ | **Housekeeping on an expensive model** | The agent's own background calls (`query_source = auxiliary`) run on a flagship model (team data: the label comes from telemetry). |
74
+ | **Uncached input** | Less than half of input comes from cache, which is unusual for Claude Code. |
75
+ | **Developer concentration** | A few people account for most of the spend (team data, 4+ developers). |
76
+ | **Continuous usage** | Spend doesn't drop at weekends, so something automated is driving it (team data). |
77
+
78
+ Run it again later with `--db history.db` and the report opens with **since last time**: what
79
+ appeared, what stopped firing, and how each ceiling moved, comparing equal windows.
80
+
81
+ ## For a team: Grafana or Datadog
82
+
83
+ If your team already sends Claude Code's telemetry to Grafana Cloud (or any Prometheus) or
84
+ Datadog, mycroftcompute reads it with one read-only credential:
85
+
86
+ ```sh
87
+ export MYCROFT_METRICS_TOKEN=glc_... # Cloud Access Policy token, metrics:read
88
+ mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --discover
89
+ mycroftcompute --grafana https://prometheus-prod-NN-....grafana.net/api/prom --user 123456 --days 14
90
+
91
+ export MYCROFT_METRICS_TOKEN=... # Datadog API key
92
+ export MYCROFT_DATADOG_APP_KEY=... # Application key: metrics_read, timeseries_query
93
+ mycroftcompute --datadog us1 --days 14
94
+ ```
95
+
96
+ `--discover` checks the connection and lists what the telemetry can and cannot support before
97
+ reading any usage. Setup, including the two settings that silently lose every number, is in
98
+ [docs/team-backends.md](docs/team-backends.md). For a two-minute check you can run inside Grafana
99
+ or Datadog without installing anything, see [docs/cache-check.md](docs/cache-check.md).
100
+
101
+ Both backends were verified against live Claude Code telemetry in September 2026.
102
+
103
+ ## How the numbers are made
104
+
105
+ - **List prices**, checked by hand against each vendor's own pricing page, for 19 models from
106
+ Anthropic, OpenAI and Google, with cache read and write multipliers and per-model cache minimums.
107
+ Prices were last checked on **24 September 2026**; when the table is more than 31 days old the
108
+ report says so. Negotiated or batch rates are lower, so every figure is correspondingly high.
109
+ - **Ceilings, not savings.** Each figure is the most a change could be worth over the period.
110
+ Ceilings overlap, so they are never added up.
111
+ - **Your own transcripts** record each cache write's lifetime, so local reports price five-minute
112
+ and one-hour writes exactly. **Metrics backends** don't, so team reports price every write at
113
+ the cheaper five-minute rate and say so.
114
+ - **Read-only.** It never reads prompts, responses or code from a backend, and never changes
115
+ anything. Credentials come from the environment and are only used to query.
116
+
117
+ ## Status
118
+
119
+ A personal project. It started as the engine of a product for platform teams, which I stopped in
120
+ October 2026; the analysis was worth keeping. The price table will age between updates, and the
121
+ report tells you when it has.
122
+
123
+ ## Development
124
+
125
+ ```sh
126
+ uv sync
127
+ uv run pytest
128
+ uv run ruff check src tests && uv run mypy
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "mycroftcompute"
3
+ version = "0.1.0"
4
+ description = "Where your Claude Code money went, priced with the prompt cache counted, and which changes would actually pay."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Aditya Negi" }]
9
+ requires-python = ">=3.12"
10
+ dependencies = []
11
+ keywords = ["claude-code", "anthropic", "llm", "cost", "prompt-caching", "grafana", "datadog", "opentelemetry", "finops"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Software Development",
21
+ "Topic :: System :: Monitoring",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://www.mycroftcompute.com"
26
+ Repository = "https://github.com/negi6711/mycroftcompute"
27
+ Issues = "https://github.com/negi6711/mycroftcompute/issues"
28
+
29
+ [project.scripts]
30
+ mycroftcompute = "mycroftcompute.__main__:main"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=8.3",
35
+ "ruff>=0.8",
36
+ "mypy>=1.13",
37
+ ]
38
+
39
+ [build-system]
40
+ requires = ["hatchling>=1.27"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/mycroftcompute"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
48
+
49
+ [tool.ruff]
50
+ line-length = 100
51
+ target-version = "py312"
52
+ src = ["src", "tests"]
53
+ extend-exclude = ["site-src"]
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I", "UP", "B", "BLE", "SIM", "N", "ASYNC", "S", "RUF"]
57
+ ignore = ["S101", "E501"]
58
+
59
+ [tool.ruff.lint.per-file-ignores]
60
+ # Fake credentials in tests are the point of the test, not a leak.
61
+ "tests/**" = ["S105", "S106"]
62
+
63
+ [tool.mypy]
64
+ python_version = "3.12"
65
+ strict = true
66
+ warn_unreachable = true
67
+ mypy_path = "src"
68
+ files = ["src", "tests"]
69
+
70
+ [tool.pytest.ini_options]
71
+ testpaths = ["tests"]
72
+ pythonpath = ["src"]
File without changes
@@ -0,0 +1,282 @@
1
+ """Read Claude Code usage, price it with the prompt cache counted, and print what is worth changing.
2
+
3
+ mycroftcompute your own Claude Code history (~/.claude/projects)
4
+ mycroftcompute --sample an invented 24-developer team, to see the report
5
+ mycroftcompute --grafana <url> --user <instance-id>
6
+ mycroftcompute --datadog eu1
7
+
8
+ Credentials are read from the environment by default, never written anywhere, and used only to
9
+ query. Nothing is uploaded, and nothing is stored unless you pass --db.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import os
16
+ import sys
17
+ import tempfile
18
+ from datetime import date, timedelta
19
+ from importlib.metadata import PackageNotFoundError, version
20
+ from pathlib import Path
21
+
22
+ from mycroftcompute import sample
23
+ from mycroftcompute.analysis.findings import analyse
24
+ from mycroftcompute.economics.prices import StalePriceTableError, verify_prices
25
+ from mycroftcompute.parsers.claude_code import parse_claude_code
26
+ from mycroftcompute.parsers.model import ParsedUsage, ParseError
27
+ from mycroftcompute.report.readiness import render_readiness
28
+ from mycroftcompute.report.text import render
29
+ from mycroftcompute.sources.base import SourceError
30
+ from mycroftcompute.sources.datadog import SITES, DatadogEndpoint, DatadogSource
31
+ from mycroftcompute.sources.grafana import GrafanaSource, PrometheusEndpoint
32
+ from mycroftcompute.store.history import (
33
+ Comparison,
34
+ compare,
35
+ load_pull,
36
+ open_store,
37
+ save,
38
+ )
39
+
40
+ PROG = "mycroftcompute"
41
+
42
+
43
+ def _version() -> str:
44
+ try:
45
+ return version(PROG)
46
+ except PackageNotFoundError:
47
+ return "unknown"
48
+
49
+
50
+ def default_transcripts() -> Path:
51
+ """Where Claude Code keeps its session files on this machine.
52
+
53
+ `CLAUDE_CONFIG_DIR` moves the whole ~/.claude directory, so it moves the transcripts too.
54
+ """
55
+ config = os.environ.get("CLAUDE_CONFIG_DIR")
56
+ base = Path(config).expanduser() if config else Path.home() / ".claude"
57
+ return base / "projects"
58
+
59
+
60
+ def main(argv: list[str] | None = None) -> int:
61
+ parser = argparse.ArgumentParser(
62
+ prog=PROG,
63
+ description=(
64
+ "Where your Claude Code money went, priced with the prompt cache counted, and which "
65
+ "changes would actually pay. With no arguments it reads your own Claude Code history."
66
+ ),
67
+ )
68
+ parser.add_argument(
69
+ "path",
70
+ type=Path,
71
+ nargs="?",
72
+ help=(
73
+ "a directory of Claude Code transcripts. Defaults to ~/.claude/projects "
74
+ "(or $CLAUDE_CONFIG_DIR/projects)."
75
+ ),
76
+ )
77
+ parser.add_argument(
78
+ "--sample",
79
+ action="store_true",
80
+ help="print the report for an invented 24-developer team, to see what the report shows",
81
+ )
82
+ parser.add_argument("--version", action="version", version=f"{PROG} {_version()}")
83
+ metrics = parser.add_argument_group(
84
+ "reading a team's metrics backend",
85
+ "For a team that already sends Claude Code telemetry to Grafana or Datadog. Nothing is "
86
+ "uploaded; the backend is queried read-only.",
87
+ )
88
+ metrics.add_argument(
89
+ "--grafana",
90
+ metavar="URL",
91
+ help=(
92
+ "Prometheus-compatible query endpoint - Grafana Cloud, Grafana OSS or plain "
93
+ "Prometheus, e.g. https://prometheus-prod-01.grafana.net/api/prom"
94
+ ),
95
+ )
96
+ metrics.add_argument(
97
+ "--user",
98
+ metavar="ID",
99
+ help="Grafana Cloud metrics instance ID. Omit for an unauthenticated local Prometheus.",
100
+ )
101
+ metrics.add_argument(
102
+ "--token",
103
+ metavar="TOKEN",
104
+ help=(
105
+ "Cloud Access Policy token with the metrics:read scope. An ordinary Grafana API key "
106
+ "or service account token will not work for querying metrics. "
107
+ "Reads MYCROFT_METRICS_TOKEN if omitted, so it need not appear in shell history."
108
+ ),
109
+ )
110
+ metrics.add_argument(
111
+ "--datadog",
112
+ metavar="SITE",
113
+ nargs="?",
114
+ const="us1",
115
+ help=(
116
+ "read from Datadog instead. Takes the site your org is on ("
117
+ + ", ".join(sorted(SITES))
118
+ + "; default us1). If you pick the wrong one we will tell you which it is. "
119
+ "Needs --token as the API key and --app-key as the Application key."
120
+ ),
121
+ )
122
+ metrics.add_argument(
123
+ "--app-key",
124
+ metavar="KEY",
125
+ help=(
126
+ "Datadog Application key, scoped to metrics_read and timeseries_query. Reads "
127
+ "MYCROFT_DATADOG_APP_KEY if omitted. The API key goes in --token."
128
+ ),
129
+ )
130
+ metrics.add_argument(
131
+ "--days",
132
+ type=int,
133
+ default=30,
134
+ metavar="N",
135
+ help="how many days back to read (default 30)",
136
+ )
137
+ metrics.add_argument(
138
+ "--discover",
139
+ action="store_true",
140
+ help=(
141
+ "check the connection and stop: what we can see, what we cannot, and what each "
142
+ "blind spot costs. Run this first on a backend nobody has seen before."
143
+ ),
144
+ )
145
+ parser.add_argument(
146
+ "--db",
147
+ type=Path,
148
+ metavar="PATH",
149
+ help=(
150
+ "keep a history at this path and compare this read with the last one. Re-reading "
151
+ "overlapping days corrects them rather than double-counting."
152
+ ),
153
+ )
154
+ parser.add_argument(
155
+ "--today",
156
+ type=date.fromisoformat,
157
+ default=date.today(),
158
+ help="override today's date (used to judge how old the price table is)",
159
+ )
160
+ args = parser.parse_args(argv)
161
+
162
+ if args.grafana and args.datadog:
163
+ parser.error("read one backend at a time: --grafana or --datadog, not both")
164
+
165
+ if args.sample:
166
+ return _sample()
167
+
168
+ if args.grafana or args.datadog:
169
+ return _from_metrics(args)
170
+
171
+ path = args.path or default_transcripts()
172
+ if not path.exists():
173
+ print(
174
+ f"{PROG}: no Claude Code transcripts at {path}. Claude Code keeps them in "
175
+ "~/.claude/projects once you have used it; to read a team's Grafana or Datadog "
176
+ "instead, see --help. To see a report without any data, run with --sample.",
177
+ file=sys.stderr,
178
+ )
179
+ return 2
180
+
181
+ try:
182
+ parsed = parse_claude_code(path)
183
+ except ParseError as error:
184
+ print(f"{PROG}: {error.message}", file=sys.stderr)
185
+ return 1
186
+ return _emit(parsed, args.today, db=args.db)
187
+
188
+
189
+ def _sample() -> int:
190
+ """The invented team's second month, with its comparison against the first."""
191
+ with tempfile.TemporaryDirectory() as tmp:
192
+ path = Path(tmp) / "sample.db"
193
+ _first, second = sample.write_history(path)
194
+ connection = open_store(path)
195
+ try:
196
+ parsed = load_pull(connection, second)
197
+ since = compare(connection, second)
198
+ finally:
199
+ connection.close()
200
+ print(render(parsed, analyse(parsed), today=sample.GENERATED_ON, since=since))
201
+ print("\n(An invented team. The arithmetic is real.)")
202
+ return 0
203
+
204
+
205
+ def _source(args: argparse.Namespace) -> GrafanaSource | DatadogSource:
206
+ """Whichever backend was asked for.
207
+
208
+ Credentials come from the environment by default. A key on the command line ends up in shell
209
+ history and in `ps` output, where it outlives the session and is readable by other users, so
210
+ the environment variable is the documented path and the flag is the convenience.
211
+ """
212
+ token = args.token or os.environ.get("MYCROFT_METRICS_TOKEN")
213
+ if args.datadog:
214
+ app_key = args.app_key or os.environ.get("MYCROFT_DATADOG_APP_KEY")
215
+ if not token or not app_key:
216
+ raise SourceError(
217
+ "missing_credentials",
218
+ "Datadog needs two keys: an API key (--token or MYCROFT_METRICS_TOKEN) and an "
219
+ "Application key (--app-key or MYCROFT_DATADOG_APP_KEY). The Application key is "
220
+ "the one that carries the scopes, and it needs metrics_read and "
221
+ "timeseries_query.",
222
+ )
223
+ return DatadogSource(DatadogEndpoint(api_key=token, app_key=app_key, site=args.datadog))
224
+ return GrafanaSource(PrometheusEndpoint(args.grafana, args.user, token))
225
+
226
+
227
+ def _from_metrics(args: argparse.Namespace) -> int:
228
+ """Read from a metrics backend rather than local transcripts."""
229
+ try:
230
+ source = _source(args)
231
+
232
+ if args.discover:
233
+ where = f"Datadog ({args.datadog})" if args.datadog else args.grafana
234
+ print(render_readiness(source.discover(), where=where))
235
+ return 0
236
+
237
+ end = args.today
238
+ parsed = source.fetch(end - timedelta(days=args.days), end)
239
+ except SourceError as error:
240
+ print(f"{PROG}: {error.message}", file=sys.stderr)
241
+ return 1
242
+
243
+ return _emit(parsed, args.today, db=args.db, backend="datadog" if args.datadog else "grafana")
244
+
245
+
246
+ def _emit(
247
+ parsed: ParsedUsage,
248
+ today: date,
249
+ *,
250
+ db: Path | None = None,
251
+ backend: str | None = None,
252
+ ) -> int:
253
+ """Render, warning first if the price table has aged.
254
+
255
+ Prices are checked by hand against each vendor's page, and this is a personal project, so the
256
+ table will age. An old table still gives the right shape and roughly the right money, so the
257
+ report prints, but with a warning above it that says how old the prices are.
258
+ """
259
+ analysis = analyse(parsed)
260
+ try:
261
+ verify_prices(today)
262
+ except StalePriceTableError as error:
263
+ print(
264
+ f"{PROG}: warning - the price table may be out of date, so dollar figures may be "
265
+ f"off.\n{error}\n",
266
+ file=sys.stderr,
267
+ )
268
+
269
+ since: Comparison | None = None
270
+ if db is not None:
271
+ connection = open_store(db)
272
+ try:
273
+ since = compare(connection, save(connection, parsed, analysis, backend=backend))
274
+ finally:
275
+ connection.close()
276
+
277
+ print(render(parsed, analysis, today=today, since=since))
278
+ return 0
279
+
280
+
281
+ if __name__ == "__main__":
282
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Turning parsed usage into findings a person can act on."""