agentabacus 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.
- agentabacus-0.1.0/.claude-plugin/marketplace.json +13 -0
- agentabacus-0.1.0/.github/workflows/ci.yml +107 -0
- agentabacus-0.1.0/.github/workflows/release.yml +65 -0
- agentabacus-0.1.0/.gitignore +11 -0
- agentabacus-0.1.0/CONTRIBUTING.md +58 -0
- agentabacus-0.1.0/LICENSE +21 -0
- agentabacus-0.1.0/PKG-INFO +168 -0
- agentabacus-0.1.0/README.md +147 -0
- agentabacus-0.1.0/plugin/.claude-plugin/plugin.json +8 -0
- agentabacus-0.1.0/plugin/README.md +40 -0
- agentabacus-0.1.0/plugin/hooks/hooks.json +15 -0
- agentabacus-0.1.0/pyproject.toml +35 -0
- agentabacus-0.1.0/src/agentabacus/__init__.py +3 -0
- agentabacus-0.1.0/src/agentabacus/adapters/__init__.py +17 -0
- agentabacus-0.1.0/src/agentabacus/adapters/base.py +101 -0
- agentabacus-0.1.0/src/agentabacus/adapters/claude_code.py +230 -0
- agentabacus-0.1.0/src/agentabacus/adapters/codex.py +122 -0
- agentabacus-0.1.0/src/agentabacus/cli.py +294 -0
- agentabacus-0.1.0/src/agentabacus/collect.py +94 -0
- agentabacus-0.1.0/src/agentabacus/config.py +37 -0
- agentabacus-0.1.0/src/agentabacus/data/__init__.py +0 -0
- agentabacus-0.1.0/src/agentabacus/data/pricing.csv +12 -0
- agentabacus-0.1.0/src/agentabacus/discovery.py +75 -0
- agentabacus-0.1.0/src/agentabacus/pricing.py +79 -0
- agentabacus-0.1.0/src/agentabacus/report.py +200 -0
- agentabacus-0.1.0/src/agentabacus/schema.py +260 -0
- agentabacus-0.1.0/src/agentabacus/store.py +221 -0
- agentabacus-0.1.0/tests/test_dedupe.py +142 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentabacus",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Ayoade Adegbite"
|
|
5
|
+
},
|
|
6
|
+
"plugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "agentabacus",
|
|
9
|
+
"source": "./plugin",
|
|
10
|
+
"description": "Archives this session's token usage and tool calls to your local agentabacus database when the session ends, before Claude Code's cleanup can delete the transcript."
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, macos-latest]
|
|
15
|
+
python: ["3.10", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
# setup-python must come first: it provides the matrix interpreter.
|
|
20
|
+
# Without it, `uv pip install --system` targets the runner's own Python,
|
|
21
|
+
# which on macOS is Homebrew-managed and refuses installs (PEP 668).
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python }}
|
|
25
|
+
|
|
26
|
+
- name: Install uv
|
|
27
|
+
uses: astral-sh/setup-uv@v5
|
|
28
|
+
|
|
29
|
+
- name: Install
|
|
30
|
+
run: uv pip install --system -e .
|
|
31
|
+
|
|
32
|
+
# The dedupe contract is the one invariant that, if broken, makes every
|
|
33
|
+
# cost figure the tool reports wrong. It runs before anything else.
|
|
34
|
+
- name: Contract tests
|
|
35
|
+
run: python tests/test_dedupe.py
|
|
36
|
+
|
|
37
|
+
- name: CLI smoke test
|
|
38
|
+
run: |
|
|
39
|
+
agentabacus --help
|
|
40
|
+
agentabacus doctor
|
|
41
|
+
agentabacus collect
|
|
42
|
+
agentabacus report --since all || true
|
|
43
|
+
|
|
44
|
+
# A fresh machine has no agent logs. Collecting nothing must exit 0 and
|
|
45
|
+
# print something sane rather than traceback -- that is most contributors'
|
|
46
|
+
# very first run.
|
|
47
|
+
- name: Empty-environment behaviour
|
|
48
|
+
env:
|
|
49
|
+
CLAUDE_CONFIG_DIR: /tmp/definitely-not-here
|
|
50
|
+
CODEX_HOME: /tmp/definitely-not-here
|
|
51
|
+
AGENTABACUS_HOME: /tmp/al-empty
|
|
52
|
+
run: |
|
|
53
|
+
agentabacus doctor
|
|
54
|
+
agentabacus collect
|
|
55
|
+
|
|
56
|
+
pricing:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
|
|
61
|
+
# Pricing is community-edited. A malformed row would silently zero out
|
|
62
|
+
# cost for that model, so the CSV is validated on every PR.
|
|
63
|
+
- name: Validate pricing table
|
|
64
|
+
run: |
|
|
65
|
+
python - <<'PY'
|
|
66
|
+
import csv, sys
|
|
67
|
+
from datetime import date
|
|
68
|
+
|
|
69
|
+
required = [
|
|
70
|
+
"model_id", "speed", "valid_from", "valid_to",
|
|
71
|
+
"input_per_mtok", "output_per_mtok", "cache_read_per_mtok",
|
|
72
|
+
"cache_write_5m_per_mtok", "cache_write_1h_per_mtok", "source_note",
|
|
73
|
+
]
|
|
74
|
+
path = "src/agentabacus/data/pricing.csv"
|
|
75
|
+
rows = list(csv.DictReader(open(path)))
|
|
76
|
+
errors = []
|
|
77
|
+
|
|
78
|
+
if not rows:
|
|
79
|
+
errors.append("pricing.csv is empty")
|
|
80
|
+
if rows and list(rows[0].keys()) != required:
|
|
81
|
+
errors.append(f"columns must be exactly {required}")
|
|
82
|
+
|
|
83
|
+
seen = set()
|
|
84
|
+
for i, r in enumerate(rows, start=2):
|
|
85
|
+
key = (r["model_id"], r["speed"], r["valid_from"])
|
|
86
|
+
if key in seen:
|
|
87
|
+
errors.append(f"line {i}: duplicate {key}")
|
|
88
|
+
seen.add(key)
|
|
89
|
+
try:
|
|
90
|
+
date.fromisoformat(r["valid_from"])
|
|
91
|
+
if r["valid_to"]:
|
|
92
|
+
date.fromisoformat(r["valid_to"])
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
errors.append(f"line {i}: bad date ({exc})")
|
|
95
|
+
for col in required[4:9]:
|
|
96
|
+
try:
|
|
97
|
+
if float(r[col]) < 0:
|
|
98
|
+
errors.append(f"line {i}: {col} is negative")
|
|
99
|
+
except ValueError:
|
|
100
|
+
errors.append(f"line {i}: {col}={r[col]!r} is not a number")
|
|
101
|
+
if r["speed"] not in {"standard", "fast"}:
|
|
102
|
+
errors.append(f"line {i}: speed must be standard or fast")
|
|
103
|
+
|
|
104
|
+
for e in errors:
|
|
105
|
+
print(f"::error file={path}::{e}")
|
|
106
|
+
sys.exit(1 if errors else 0)
|
|
107
|
+
PY
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI via Trusted Publishing (OIDC). No API token is stored in
|
|
4
|
+
# this repo or on any developer machine -- GitHub proves the workflow's identity
|
|
5
|
+
# to PyPI directly.
|
|
6
|
+
#
|
|
7
|
+
# One-time setup on PyPI (https://pypi.org/manage/account/publishing/):
|
|
8
|
+
# PyPI project name : agentabacus
|
|
9
|
+
# Owner : tripleaceme
|
|
10
|
+
# Repository name : agentabacus
|
|
11
|
+
# Workflow name : release.yml
|
|
12
|
+
# Environment name : pypi
|
|
13
|
+
#
|
|
14
|
+
# Then release with:
|
|
15
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
tags: ["v*"]
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
build:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.11"
|
|
30
|
+
- uses: astral-sh/setup-uv@v5
|
|
31
|
+
|
|
32
|
+
- name: Verify the tag matches the packaged version
|
|
33
|
+
run: |
|
|
34
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
35
|
+
PKG=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)
|
|
36
|
+
if [ "$TAG" != "$PKG" ]; then
|
|
37
|
+
echo "::error::tag v$TAG does not match pyproject version $PKG"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
41
|
+
|
|
42
|
+
- name: Contract tests
|
|
43
|
+
run: |
|
|
44
|
+
uv pip install --system -e .
|
|
45
|
+
python tests/test_dedupe.py
|
|
46
|
+
|
|
47
|
+
- run: uv build
|
|
48
|
+
|
|
49
|
+
- uses: actions/upload-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
publish:
|
|
55
|
+
needs: build
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
environment: pypi
|
|
58
|
+
permissions:
|
|
59
|
+
id-token: write # required for Trusted Publishing
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/download-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist/
|
|
65
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Two contributions matter more than any others, and both are small.
|
|
4
|
+
|
|
5
|
+
## 1. Add or correct a model price
|
|
6
|
+
|
|
7
|
+
`src/agentabacus/data/pricing.csv` — one line, no Python:
|
|
8
|
+
|
|
9
|
+
```csv
|
|
10
|
+
model_id,speed,valid_from,valid_to,input_per_mtok,output_per_mtok,cache_read_per_mtok,cache_write_5m_per_mtok,cache_write_1h_per_mtok,source_note
|
|
11
|
+
claude-opus-5,standard,2020-01-01,,5.00,25.00,0.50,6.25,10.00,anthropic list price
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
|
|
16
|
+
- **`valid_to` empty means "still current."** To change a price, close the old row with a `valid_to` and add a new row starting the next day. Never edit a historical row's rates — that silently reprices everyone's past sessions.
|
|
17
|
+
- **Cache columns are not optional.** Reads bill at 0.1× input, 5-minute writes at 1.25×, 1-hour writes at 2×. If a provider prices them differently, put the real numbers in; the schema doesn't assume the multipliers.
|
|
18
|
+
- **Dated snapshot IDs need no row.** `claude-haiku-4-5-20251001` normalizes to `claude-haiku-4-5` automatically.
|
|
19
|
+
- Cite where the numbers came from in `source_note`.
|
|
20
|
+
|
|
21
|
+
CI validates column order, date parsing, duplicate keys, and negative values.
|
|
22
|
+
|
|
23
|
+
Run `agentabacus doctor` to see models present in your own data with no pricing row — that list is the to-do list.
|
|
24
|
+
|
|
25
|
+
## 2. Add an adapter
|
|
26
|
+
|
|
27
|
+
One module exposing `parse(path, kind, start_offset) -> Batch`, a walker in `discovery.py`, one line in `adapters/__init__.py`.
|
|
28
|
+
|
|
29
|
+
Read `adapters/claude_code.py` as the reference and `adapters/codex.py` as the minimal template.
|
|
30
|
+
|
|
31
|
+
**The one rule: be a tolerant parser.** These log formats are undocumented, version-dependent, and change without notice. Route on known shapes, count what you skip, and never raise — a vendor's routine release must not become a crash for every user. Strictness belongs in `schema.py`, not at the edges.
|
|
32
|
+
|
|
33
|
+
Things that have already bitten this codebase, so check for them in yours:
|
|
34
|
+
|
|
35
|
+
- **Repeated usage across records.** Claude Code writes one line per content block, each carrying the parent response's *full* usage. Summing per line overcounts 2–3×. Key on a request id and merge with `max()`.
|
|
36
|
+
- **More than one file layout.** Claude Code nests subagent transcripts at two different depths. A fixed-depth glob silently dropped the majority of them. Recurse.
|
|
37
|
+
- **Torn trailing lines.** A file caught mid-write must be re-read whole next run, never half-parsed. `iter_lines()` handles this; use it.
|
|
38
|
+
- **Model IDs in two forms.** Aliased and date-stamped. Normalize before joining to pricing.
|
|
39
|
+
|
|
40
|
+
### Verifying the Codex adapter
|
|
41
|
+
|
|
42
|
+
`adapters/codex.py` is shape-agnostic and **unverified** — it was written without access to real rollout files. If you have `~/.codex/sessions/*.jsonl`, paste two or three redacted records into a test fixture and tighten the parsing. That's the highest-value PR in the repo right now.
|
|
43
|
+
|
|
44
|
+
## Running things
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv venv && uv pip install -e .
|
|
48
|
+
python tests/test_dedupe.py # the contract tests
|
|
49
|
+
agentabacus doctor # what's discoverable on your machine
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What this project deliberately does not do
|
|
53
|
+
|
|
54
|
+
- No hosted service, no account, no telemetry.
|
|
55
|
+
- **Prompt and response bodies never enter the pipeline.** The `prompts` table stores a hash and a length; there is no column for the text. Please don't add one — that property is why the tool is installable inside companies.
|
|
56
|
+
- No agent that acts on the data.
|
|
57
|
+
|
|
58
|
+
Team/warehouse sinks and a dbt package are on the roadmap, not in scope yet.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayoade Adegbite
|
|
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,168 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentabacus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first analytics for AI coding agents. Reads the session logs already on your disk.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tripleaceme/agentabacus
|
|
6
|
+
Project-URL: Issues, https://github.com/tripleaceme/agentabacus/issues
|
|
7
|
+
Author: Ayoade Adegbite
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,analytics,claude,codex,cost,duckdb,llm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: duckdb>=1.0
|
|
18
|
+
Requires-Dist: rich>=13.0
|
|
19
|
+
Requires-Dist: typer>=0.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# agentabacus
|
|
23
|
+
|
|
24
|
+
**Local-first analytics for AI coding agents.** Every agent CLI writes session logs to your disk in its own format. Nothing reads all of them. `agentabacus` normalizes them into one schema and answers: what did this cost, which model actually finishes the work, and where is the spend going?
|
|
25
|
+
|
|
26
|
+
No server. No account. No network calls. It reads files that are already on your machine and writes one DuckDB file.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx agentabacus collect # read new log data into the archive
|
|
30
|
+
uvx agentabacus report # cost and tokens, last 30 days
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Why this exists
|
|
36
|
+
|
|
37
|
+
Three things make the naive version of this tool wrong, and all three are handled here.
|
|
38
|
+
|
|
39
|
+
### 1. Summing usage per log line overcounts by 2–3×
|
|
40
|
+
|
|
41
|
+
Claude Code writes **one JSONL line per content block** — thinking, text, each `tool_use` — and every one of those lines repeats the **full usage of the parent API response**. Measured on a real session: 16 assistant lines, 6 actual requests.
|
|
42
|
+
|
|
43
|
+
| | naive per-line sum | deduped by `requestId` | overcount |
|
|
44
|
+
|---|---:|---:|---:|
|
|
45
|
+
| input | 5,273 | 1,759 | 3.0× |
|
|
46
|
+
| output | 18,555 | 7,861 | 2.4× |
|
|
47
|
+
| cache read | 712,283 | 264,865 | 2.7× |
|
|
48
|
+
| cache write | 61,219 | 25,647 | 2.4× |
|
|
49
|
+
|
|
50
|
+
The multiplier depends on how many content blocks a response happened to emit, so it can't be corrected after the fact with a constant. `agentabacus` keys the `turns` table on `request_id` and merges with `MAX()`.
|
|
51
|
+
|
|
52
|
+
### 2. Cache writes are not one number
|
|
53
|
+
|
|
54
|
+
A **1-hour** TTL cache write bills at **2×** base input. A **5-minute** write bills at **1.25×**. A cache read bills at **0.1×**. Claude Code records the split (`cache_creation.ephemeral_1h_input_tokens` vs `ephemeral_5m_input_tokens`); collapsing them into a single `cache_creation_input_tokens` figure misprices exactly the long sessions where cache tokens accumulate.
|
|
55
|
+
|
|
56
|
+
### 3. Subagent transcripts live in separate files
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
~/.claude/projects/<slug>/<uuid>.jsonl # main transcript
|
|
60
|
+
~/.claude/projects/<slug>/<uuid>/subagents/agent-*.jsonl # plain subagent
|
|
61
|
+
~/.claude/projects/<slug>/<uuid>/subagents/workflows/wf_*/agent-*.jsonl # workflow subagent
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Two things bite here. A `projects/*/*.jsonl` glob — the obvious one — misses every subagent file. And a `*/subagents/*.jsonl` glob still misses the **workflow** subagents one level deeper, which on a machine that runs workflows are the *majority* (measured: 80 of 127). Discovery has to recurse.
|
|
65
|
+
|
|
66
|
+
Subagent files carry the **parent's** `sessionId` plus their own `agentId`, so the thread is what separates them, not the session. `agentabacus report --by thread` splits main-loop from subagent spend — a number no other tool surfaces.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uvx agentabacus report # zero-install trial
|
|
74
|
+
pipx install agentabacus # permanent CLI
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
agentabacus doctor # what's discoverable, what's collected, what has no price
|
|
81
|
+
agentabacus collect # incremental; safe to run repeatedly
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Collect automatically (Claude Code plugin)
|
|
85
|
+
|
|
86
|
+
Transcripts get garbage-collected, so collection has to happen without you remembering. The plugin registers a `SessionEnd` hook that archives each session as it closes:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
/plugin marketplace add tripleaceme/agentabacus
|
|
90
|
+
/plugin install agentabacus@agentabacus
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The CLI must be on your `PATH` (`pipx install agentabacus`). No daemon, no cron entry.
|
|
94
|
+
|
|
95
|
+
## Commands
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
agentabacus report --since 30d --by model # or: source project branch day effort speed thread
|
|
99
|
+
agentabacus top --limit 10 # most expensive sessions
|
|
100
|
+
agentabacus cache # read share and the 1h/5m write split, priced
|
|
101
|
+
agentabacus tools # tool-call volume and error rate
|
|
102
|
+
agentabacus doctor # health + pricing gaps
|
|
103
|
+
agentabacus export --format parquet # hand the tables to dbt / Metabase
|
|
104
|
+
agentabacus sql "select ..." # the schema is yours
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`--by thread` splits main-loop spend from subagent spend — the number most tools can't show you at all.
|
|
108
|
+
|
|
109
|
+
## Where the data lives
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
~/.agentabacus/agentabacus.duckdb # the archive: everything, all time
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Override with `AGENTABACUS_HOME`. The collector is incremental: it records a byte offset per file and re-reads nothing, so a repeat run over a 350 MB corpus costs one `stat()` per file.
|
|
116
|
+
|
|
117
|
+
**This matters more than it sounds.** Claude Code garbage-collects old transcripts. Project directories with a `memory/` folder and zero `.jsonl` files are what that looks like afterwards — that history is gone permanently. Once cleanup runs, this database is the only copy. `agentabacus` is an archive with a dashboard on top, not a dashboard.
|
|
118
|
+
|
|
119
|
+
## Privacy
|
|
120
|
+
|
|
121
|
+
Prompt and response bodies **never enter the pipeline**. The `prompts` table stores a SHA-256 and a character count; there is no column for the text. That's a schema property, not a filter you have to trust — "does this leak my code?" is answerable by reading `schema.py`.
|
|
122
|
+
|
|
123
|
+
Nothing is uploaded anywhere. There is no telemetry.
|
|
124
|
+
|
|
125
|
+
## Pricing
|
|
126
|
+
|
|
127
|
+
`src/agentabacus/data/pricing.csv` — effective-dated, one row per model per speed tier:
|
|
128
|
+
|
|
129
|
+
```csv
|
|
130
|
+
model_id,speed,valid_from,valid_to,input_per_mtok,output_per_mtok,cache_read_per_mtok,cache_write_5m_per_mtok,cache_write_1h_per_mtok,source_note
|
|
131
|
+
claude-opus-5,standard,2020-01-01,,5.00,25.00,0.50,6.25,10.00,anthropic list price
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Cost is computed as **tokens × price-at-event-timestamp**, via the `turns_costed` view. Joining against a "current price" table would silently reprice last quarter's sessions.
|
|
135
|
+
|
|
136
|
+
`agentabacus doctor` lists any model seen in your data that has no pricing row — that's the alarm for "a new model shipped and the table is stale", which is otherwise a silent undercount.
|
|
137
|
+
|
|
138
|
+
**Adding a model is a one-line CSV edit.** Dates currently use an early `valid_from` so historical sessions price at today's rate; real effective dates are welcome as PRs.
|
|
139
|
+
|
|
140
|
+
## Contributing an adapter
|
|
141
|
+
|
|
142
|
+
One module exposing `parse(path, kind, start_offset) -> Batch`, a walker in `discovery.py`, one line in `adapters/__init__.py`. See `adapters/claude_code.py` for the reference and `adapters/codex.py` for the minimal template.
|
|
143
|
+
|
|
144
|
+
**The rule: be a tolerant parser.** These formats are undocumented and change without notice. Route on known shapes, count what you skipped, never raise — a vendor's routine release must not become a crash for every user. Strictness belongs in `schema.py`, not at the edges.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python tests/test_dedupe.py # pins the dedupe contract, the TTL split, and torn-line handling
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Status
|
|
151
|
+
|
|
152
|
+
| Source | State |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Claude Code | verified against real transcripts |
|
|
155
|
+
| Codex CLI | **shape-agnostic, unverified** — needs someone with real rollout files |
|
|
156
|
+
| Gemini CLI, Cursor, Aider, Cline | not yet written |
|
|
157
|
+
|
|
158
|
+
## Roadmap
|
|
159
|
+
|
|
160
|
+
- Edit-survival metric from `file-history-snapshot.trackedFileBackups` (pre-edit backups are already in the transcript, so no git join is needed for Claude Code)
|
|
161
|
+
- More adapters
|
|
162
|
+
- `agentabacus dash` — local static dashboard
|
|
163
|
+
- **Teams**: warehouse sinks (Postgres/Snowflake/BigQuery), redaction policy in version control, a GitHub Action for rollups
|
|
164
|
+
- **`dbt_agentabacus`**: staging models over the parquet export, pricing as a seed, tests as drift detection
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# agentabacus
|
|
2
|
+
|
|
3
|
+
**Local-first analytics for AI coding agents.** Every agent CLI writes session logs to your disk in its own format. Nothing reads all of them. `agentabacus` normalizes them into one schema and answers: what did this cost, which model actually finishes the work, and where is the spend going?
|
|
4
|
+
|
|
5
|
+
No server. No account. No network calls. It reads files that are already on your machine and writes one DuckDB file.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uvx agentabacus collect # read new log data into the archive
|
|
9
|
+
uvx agentabacus report # cost and tokens, last 30 days
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Why this exists
|
|
15
|
+
|
|
16
|
+
Three things make the naive version of this tool wrong, and all three are handled here.
|
|
17
|
+
|
|
18
|
+
### 1. Summing usage per log line overcounts by 2–3×
|
|
19
|
+
|
|
20
|
+
Claude Code writes **one JSONL line per content block** — thinking, text, each `tool_use` — and every one of those lines repeats the **full usage of the parent API response**. Measured on a real session: 16 assistant lines, 6 actual requests.
|
|
21
|
+
|
|
22
|
+
| | naive per-line sum | deduped by `requestId` | overcount |
|
|
23
|
+
|---|---:|---:|---:|
|
|
24
|
+
| input | 5,273 | 1,759 | 3.0× |
|
|
25
|
+
| output | 18,555 | 7,861 | 2.4× |
|
|
26
|
+
| cache read | 712,283 | 264,865 | 2.7× |
|
|
27
|
+
| cache write | 61,219 | 25,647 | 2.4× |
|
|
28
|
+
|
|
29
|
+
The multiplier depends on how many content blocks a response happened to emit, so it can't be corrected after the fact with a constant. `agentabacus` keys the `turns` table on `request_id` and merges with `MAX()`.
|
|
30
|
+
|
|
31
|
+
### 2. Cache writes are not one number
|
|
32
|
+
|
|
33
|
+
A **1-hour** TTL cache write bills at **2×** base input. A **5-minute** write bills at **1.25×**. A cache read bills at **0.1×**. Claude Code records the split (`cache_creation.ephemeral_1h_input_tokens` vs `ephemeral_5m_input_tokens`); collapsing them into a single `cache_creation_input_tokens` figure misprices exactly the long sessions where cache tokens accumulate.
|
|
34
|
+
|
|
35
|
+
### 3. Subagent transcripts live in separate files
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
~/.claude/projects/<slug>/<uuid>.jsonl # main transcript
|
|
39
|
+
~/.claude/projects/<slug>/<uuid>/subagents/agent-*.jsonl # plain subagent
|
|
40
|
+
~/.claude/projects/<slug>/<uuid>/subagents/workflows/wf_*/agent-*.jsonl # workflow subagent
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Two things bite here. A `projects/*/*.jsonl` glob — the obvious one — misses every subagent file. And a `*/subagents/*.jsonl` glob still misses the **workflow** subagents one level deeper, which on a machine that runs workflows are the *majority* (measured: 80 of 127). Discovery has to recurse.
|
|
44
|
+
|
|
45
|
+
Subagent files carry the **parent's** `sessionId` plus their own `agentId`, so the thread is what separates them, not the session. `agentabacus report --by thread` splits main-loop from subagent spend — a number no other tool surfaces.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uvx agentabacus report # zero-install trial
|
|
53
|
+
pipx install agentabacus # permanent CLI
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
agentabacus doctor # what's discoverable, what's collected, what has no price
|
|
60
|
+
agentabacus collect # incremental; safe to run repeatedly
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Collect automatically (Claude Code plugin)
|
|
64
|
+
|
|
65
|
+
Transcripts get garbage-collected, so collection has to happen without you remembering. The plugin registers a `SessionEnd` hook that archives each session as it closes:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
/plugin marketplace add tripleaceme/agentabacus
|
|
69
|
+
/plugin install agentabacus@agentabacus
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The CLI must be on your `PATH` (`pipx install agentabacus`). No daemon, no cron entry.
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
agentabacus report --since 30d --by model # or: source project branch day effort speed thread
|
|
78
|
+
agentabacus top --limit 10 # most expensive sessions
|
|
79
|
+
agentabacus cache # read share and the 1h/5m write split, priced
|
|
80
|
+
agentabacus tools # tool-call volume and error rate
|
|
81
|
+
agentabacus doctor # health + pricing gaps
|
|
82
|
+
agentabacus export --format parquet # hand the tables to dbt / Metabase
|
|
83
|
+
agentabacus sql "select ..." # the schema is yours
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`--by thread` splits main-loop spend from subagent spend — the number most tools can't show you at all.
|
|
87
|
+
|
|
88
|
+
## Where the data lives
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
~/.agentabacus/agentabacus.duckdb # the archive: everything, all time
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Override with `AGENTABACUS_HOME`. The collector is incremental: it records a byte offset per file and re-reads nothing, so a repeat run over a 350 MB corpus costs one `stat()` per file.
|
|
95
|
+
|
|
96
|
+
**This matters more than it sounds.** Claude Code garbage-collects old transcripts. Project directories with a `memory/` folder and zero `.jsonl` files are what that looks like afterwards — that history is gone permanently. Once cleanup runs, this database is the only copy. `agentabacus` is an archive with a dashboard on top, not a dashboard.
|
|
97
|
+
|
|
98
|
+
## Privacy
|
|
99
|
+
|
|
100
|
+
Prompt and response bodies **never enter the pipeline**. The `prompts` table stores a SHA-256 and a character count; there is no column for the text. That's a schema property, not a filter you have to trust — "does this leak my code?" is answerable by reading `schema.py`.
|
|
101
|
+
|
|
102
|
+
Nothing is uploaded anywhere. There is no telemetry.
|
|
103
|
+
|
|
104
|
+
## Pricing
|
|
105
|
+
|
|
106
|
+
`src/agentabacus/data/pricing.csv` — effective-dated, one row per model per speed tier:
|
|
107
|
+
|
|
108
|
+
```csv
|
|
109
|
+
model_id,speed,valid_from,valid_to,input_per_mtok,output_per_mtok,cache_read_per_mtok,cache_write_5m_per_mtok,cache_write_1h_per_mtok,source_note
|
|
110
|
+
claude-opus-5,standard,2020-01-01,,5.00,25.00,0.50,6.25,10.00,anthropic list price
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Cost is computed as **tokens × price-at-event-timestamp**, via the `turns_costed` view. Joining against a "current price" table would silently reprice last quarter's sessions.
|
|
114
|
+
|
|
115
|
+
`agentabacus doctor` lists any model seen in your data that has no pricing row — that's the alarm for "a new model shipped and the table is stale", which is otherwise a silent undercount.
|
|
116
|
+
|
|
117
|
+
**Adding a model is a one-line CSV edit.** Dates currently use an early `valid_from` so historical sessions price at today's rate; real effective dates are welcome as PRs.
|
|
118
|
+
|
|
119
|
+
## Contributing an adapter
|
|
120
|
+
|
|
121
|
+
One module exposing `parse(path, kind, start_offset) -> Batch`, a walker in `discovery.py`, one line in `adapters/__init__.py`. See `adapters/claude_code.py` for the reference and `adapters/codex.py` for the minimal template.
|
|
122
|
+
|
|
123
|
+
**The rule: be a tolerant parser.** These formats are undocumented and change without notice. Route on known shapes, count what you skipped, never raise — a vendor's routine release must not become a crash for every user. Strictness belongs in `schema.py`, not at the edges.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
python tests/test_dedupe.py # pins the dedupe contract, the TTL split, and torn-line handling
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Status
|
|
130
|
+
|
|
131
|
+
| Source | State |
|
|
132
|
+
|---|---|
|
|
133
|
+
| Claude Code | verified against real transcripts |
|
|
134
|
+
| Codex CLI | **shape-agnostic, unverified** — needs someone with real rollout files |
|
|
135
|
+
| Gemini CLI, Cursor, Aider, Cline | not yet written |
|
|
136
|
+
|
|
137
|
+
## Roadmap
|
|
138
|
+
|
|
139
|
+
- Edit-survival metric from `file-history-snapshot.trackedFileBackups` (pre-edit backups are already in the transcript, so no git join is needed for Claude Code)
|
|
140
|
+
- More adapters
|
|
141
|
+
- `agentabacus dash` — local static dashboard
|
|
142
|
+
- **Teams**: warehouse sinks (Postgres/Snowflake/BigQuery), redaction policy in version control, a GitHub Action for rollups
|
|
143
|
+
- **`dbt_agentabacus`**: staging models over the parquet export, pricing as a seed, tests as drift detection
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentabacus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Archives this session's usage to your local agentabacus database when the session ends.",
|
|
5
|
+
"author": { "name": "Ayoade Adegbite" },
|
|
6
|
+
"homepage": "https://github.com/tripleaceme/agentabacus",
|
|
7
|
+
"license": "MIT"
|
|
8
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# agentabacus — Claude Code plugin
|
|
2
|
+
|
|
3
|
+
Runs `agentabacus collect` when a session ends, so usage is archived **before**
|
|
4
|
+
Claude Code's cleanup can delete the transcript.
|
|
5
|
+
|
|
6
|
+
## Why a plugin rather than a cron job
|
|
7
|
+
|
|
8
|
+
Transcripts are garbage-collected. A directory with a `memory/` folder and no
|
|
9
|
+
`.jsonl` files is what a project looks like after cleanup has run — that history
|
|
10
|
+
is unrecoverable. Collection therefore has to happen without the user
|
|
11
|
+
remembering to run it.
|
|
12
|
+
|
|
13
|
+
A `SessionEnd` hook is the cheapest way to guarantee that:
|
|
14
|
+
|
|
15
|
+
- fires exactly when the data is freshest, long before cleanup
|
|
16
|
+
- incremental by construction — one session's new bytes, milliseconds of work
|
|
17
|
+
- no daemon, no cron entry, no background process to explain
|
|
18
|
+
- installs from inside the tool people already have open
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pipx install agentabacus # the CLI must be on PATH
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
/plugin marketplace add tripleaceme/agentabacus
|
|
28
|
+
/plugin install agentabacus
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then `agentabacus report` any time.
|
|
32
|
+
|
|
33
|
+
## Notes
|
|
34
|
+
|
|
35
|
+
- The hook is `|| true`: a collection failure must never interfere with your
|
|
36
|
+
session ending.
|
|
37
|
+
- It writes only to `~/.agentabacus/agentabacus.duckdb` (or `$AGENTABACUS_HOME`).
|
|
38
|
+
Nothing is uploaded.
|
|
39
|
+
- Other agent CLIs (Codex, Gemini) aren't covered by this hook — for those,
|
|
40
|
+
run `agentabacus collect` on a timer or before reporting.
|