@vimoxshah/tokenflow 1.1.0
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.
- package/CONTRIBUTING.md +84 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/Refresh & Open Dashboard.command +22 -0
- package/SECURITY.md +42 -0
- package/bin/tokenflow.js +1342 -0
- package/docs/architecture.md +193 -0
- package/docs/cli.md +390 -0
- package/docs/configuration.md +281 -0
- package/docs/creating-provider.md +262 -0
- package/docs/data-model.md +213 -0
- package/docs/getting-started.md +266 -0
- package/docs/live-mode.md +199 -0
- package/docs/media/architecture-hero.svg +86 -0
- package/docs/media/cost-editorial-dark.png +0 -0
- package/docs/media/health-terminal-light.png +0 -0
- package/docs/media/menubar-dark.png +0 -0
- package/docs/media/menubar-light.png +0 -0
- package/docs/media/models-terminal-dark.png +0 -0
- package/docs/media/overview-aurora-dark.png +0 -0
- package/docs/media/time-aurora-light.png +0 -0
- package/docs/providers.md +309 -0
- package/docs/skill.md +64 -0
- package/docs/troubleshooting.md +207 -0
- package/examples/config.example.yaml +92 -0
- package/examples/demo-data/README.md +38 -0
- package/examples/demo-data/sample-usage.csv +11 -0
- package/package.json +74 -0
- package/scripts/build-dmg.sh +33 -0
- package/scripts/build-menubar-app.sh +67 -0
- package/scripts/lint.js +111 -0
- package/scripts/validate-install.js +140 -0
- package/skills/tokenflow/SKILL.md +392 -0
- package/skills/tokenflow/examples/config.yaml +92 -0
- package/skills/tokenflow/examples/generic-mapping.json +26 -0
- package/skills/tokenflow/examples/session-transcript.md +191 -0
- package/skills/tokenflow/providers/adapter-template.js +135 -0
- package/skills/tokenflow/providers/detection-matrix.md +142 -0
- package/skills/tokenflow/schemas/config.schema.json +107 -0
- package/skills/tokenflow/schemas/normalized-record.json +63 -0
- package/src/analytics/aggregate.js +247 -0
- package/src/analytics/anomalies.js +222 -0
- package/src/analytics/capacity.js +278 -0
- package/src/analytics/comparison.js +96 -0
- package/src/analytics/dimensions.js +230 -0
- package/src/analytics/efficiency.js +138 -0
- package/src/analytics/forecast.js +202 -0
- package/src/analytics/index.js +327 -0
- package/src/analytics/insights.js +283 -0
- package/src/analytics/milestones.js +91 -0
- package/src/analytics/peak.js +106 -0
- package/src/analytics/productivity.js +166 -0
- package/src/analytics/token-usage.js +267 -0
- package/src/commands/diagnostics.js +88 -0
- package/src/commands/digest.js +155 -0
- package/src/commands/models-compare.js +96 -0
- package/src/core/budget.js +142 -0
- package/src/core/bundle.js +191 -0
- package/src/core/config.js +202 -0
- package/src/core/delivery.js +109 -0
- package/src/core/geo.js +99 -0
- package/src/core/ingest.js +457 -0
- package/src/core/interface-map.js +55 -0
- package/src/core/jsonl.js +124 -0
- package/src/core/live-status.js +417 -0
- package/src/core/model-map.js +157 -0
- package/src/core/notify.js +83 -0
- package/src/core/pricing.js +288 -0
- package/src/core/prompt-analytics.js +127 -0
- package/src/core/registry.js +107 -0
- package/src/core/restore.js +261 -0
- package/src/core/schedule.js +120 -0
- package/src/core/schema.js +316 -0
- package/src/core/sqlite.js +96 -0
- package/src/core/store.js +493 -0
- package/src/core/sync.js +151 -0
- package/src/core/units.js +147 -0
- package/src/core/validate.js +123 -0
- package/src/core/watch.js +287 -0
- package/src/core/yaml.js +209 -0
- package/src/export/bundler.js +107 -0
- package/src/export/csv.js +100 -0
- package/src/export/html-snapshot.js +101 -0
- package/src/export/menubar.js +158 -0
- package/src/index.js +18 -0
- package/src/providers/anthropic/index.js +294 -0
- package/src/providers/cline/index.js +120 -0
- package/src/providers/cursor/index.js +143 -0
- package/src/providers/generic/index.js +268 -0
- package/src/providers/git/index.js +188 -0
- package/src/providers/headroom/index.js +114 -0
- package/src/providers/hermes/index.js +299 -0
- package/src/providers/mock/index.js +117 -0
- package/src/providers/openai/index.js +370 -0
- package/src/providers/opencode/index.js +245 -0
- package/src/sdk.js +46 -0
- package/src/server/server.js +264 -0
- package/src/ui/app.js +2473 -0
- package/src/ui/charts.js +925 -0
- package/src/ui/index.html +42 -0
- package/src/ui/styles.css +644 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 300" width="100%" role="img" aria-label="TokenFlow architecture: local AI tool logs flow through adapters into a token store, then to dashboard, CLI, and menu bar — nothing leaves the machine.">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="tfh-accent" x1="0" y1="0" x2="1" y2="0">
|
|
4
|
+
<stop offset="0" stop-color="#3987e5"/><stop offset="1" stop-color="#8b5cf6"/>
|
|
5
|
+
</linearGradient>
|
|
6
|
+
<linearGradient id="tfh-card" x1="0" y1="0" x2="0" y2="1">
|
|
7
|
+
<stop offset="0" stop-color="#1c2333"/><stop offset="1" stop-color="#141926"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<style>
|
|
11
|
+
.tfh-title{font:600 13px -apple-system,'Segoe UI',sans-serif;fill:#eef1f7}
|
|
12
|
+
.tfh-sub{font:400 10px -apple-system,'Segoe UI',sans-serif;fill:#98a1b5}
|
|
13
|
+
.tfh-chip{font:500 10px ui-monospace,Menlo,monospace;fill:#c7cede}
|
|
14
|
+
.tfh-num{font:700 20px -apple-system,'Segoe UI',sans-serif;fill:url(#tfh-accent)}
|
|
15
|
+
.tfh-cap{font:400 9.5px -apple-system,'Segoe UI',sans-serif;fill:#98a1b5}
|
|
16
|
+
.card{fill:url(#tfh-card);stroke:#2a3348;rx:12}
|
|
17
|
+
.arrow{stroke:url(#tfh-accent);stroke-width:1.6;fill:none;stroke-dasharray:4 5;opacity:.85}
|
|
18
|
+
</style>
|
|
19
|
+
|
|
20
|
+
<!-- sources -->
|
|
21
|
+
<g>
|
|
22
|
+
<rect class="card" x="16" y="34" width="150" height="232" rx="12"/>
|
|
23
|
+
<text class="tfh-title" x="30" y="56">Your tools' logs</text>
|
|
24
|
+
<text class="tfh-chip" x="30" y="84">~/.claude/*.jsonl</text>
|
|
25
|
+
<text class="tfh-chip" x="30" y="106">~/.codex/sessions</text>
|
|
26
|
+
<text class="tfh-chip" x="30" y="128">opencode.db</text>
|
|
27
|
+
<text class="tfh-chip" x="30" y="150">~/.hermes/state.db</text>
|
|
28
|
+
<text class="tfh-chip" x="30" y="172">~/.cline · ~/.cursor</text>
|
|
29
|
+
<text class="tfh-chip" x="30" y="194">CSV · JSONL · SQLite</text>
|
|
30
|
+
<text class="tfh-cap" x="30" y="230">read-only, incremental,</text>
|
|
31
|
+
<text class="tfh-cap" x="30" y="244">byte-offset resumable</text>
|
|
32
|
+
</g>
|
|
33
|
+
|
|
34
|
+
<!-- engine -->
|
|
35
|
+
<g>
|
|
36
|
+
<rect class="card" x="220" y="34" width="190" height="232" rx="12"/>
|
|
37
|
+
<text class="tfh-title" x="234" y="56">TokenFlow engine</text>
|
|
38
|
+
<text class="tfh-cap" x="234" y="74">zero dependencies · Node 22+</text>
|
|
39
|
+
<text class="tfh-chip" x="234" y="100">adapters → normalize</text>
|
|
40
|
+
<text class="tfh-chip" x="234" y="122">cache ≠ input accounting</text>
|
|
41
|
+
<text class="tfh-chip" x="234" y="144">stream dedup (max-of-run)</text>
|
|
42
|
+
<text class="tfh-chip" x="234" y="166">versioned price table</text>
|
|
43
|
+
<text class="tfh-chip" x="234" y="188">pre-aggregated cube</text>
|
|
44
|
+
<text class="tfh-cap" x="234" y="222">null ≠ 0 · measured ≠ estimated</text>
|
|
45
|
+
<text class="tfh-cap" x="234" y="236">145 tests enforce it</text>
|
|
46
|
+
</g>
|
|
47
|
+
|
|
48
|
+
<!-- surfaces -->
|
|
49
|
+
<g>
|
|
50
|
+
<rect class="card" x="464" y="34" width="180" height="70" rx="12"/>
|
|
51
|
+
<text class="tfh-title" x="478" y="58">Web dashboard</text>
|
|
52
|
+
<text class="tfh-cap" x="478" y="76">12 views · loopback only · themes</text>
|
|
53
|
+
|
|
54
|
+
<rect class="card" x="464" y="118" width="180" height="70" rx="12"/>
|
|
55
|
+
<text class="tfh-title" x="478" y="142">Native menu bar app</text>
|
|
56
|
+
<text class="tfh-cap" x="478" y="160">cost, limits, forecast, source &</text>
|
|
57
|
+
<text class="tfh-cap" x="478" y="173">model breakdown, theme toggle</text>
|
|
58
|
+
|
|
59
|
+
<rect class="card" x="464" y="202" width="180" height="64" rx="12"/>
|
|
60
|
+
<text class="tfh-title" x="478" y="226">CLI + exports</text>
|
|
61
|
+
<text class="tfh-cap" x="478" y="244">refresh · status · watch · CSV/HTML</text>
|
|
62
|
+
</g>
|
|
63
|
+
|
|
64
|
+
<!-- stats column -->
|
|
65
|
+
<g>
|
|
66
|
+
<rect class="card" x="688" y="34" width="176" height="232" rx="12"/>
|
|
67
|
+
<text class="tfh-num" x="702" y="72">15+</text>
|
|
68
|
+
<text class="tfh-cap" x="702" y="88">providers & gateways</text>
|
|
69
|
+
<text class="tfh-num" x="702" y="124">35+</text>
|
|
70
|
+
<text class="tfh-cap" x="702" y="140">models auto-classified</text>
|
|
71
|
+
<text class="tfh-num" x="702" y="176">12</text>
|
|
72
|
+
<text class="tfh-cap" x="702" y="192">analytics views</text>
|
|
73
|
+
<text class="tfh-num" x="702" y="228">0</text>
|
|
74
|
+
<text class="tfh-cap" x="702" y="244">runtime dependencies</text>
|
|
75
|
+
</g>
|
|
76
|
+
|
|
77
|
+
<!-- arrows -->
|
|
78
|
+
<path class="arrow" d="M170 150 H 214"/>
|
|
79
|
+
<path class="arrow" d="M414 120 C 440 110, 442 80, 458 70"/>
|
|
80
|
+
<path class="arrow" d="M414 150 H 458"/>
|
|
81
|
+
<path class="arrow" d="M414 180 C 440 190, 442 220, 458 230"/>
|
|
82
|
+
|
|
83
|
+
<!-- privacy band -->
|
|
84
|
+
<rect x="16" y="278" width="848" height="16" rx="4" fill="#10141d"/>
|
|
85
|
+
<text class="tfh-cap" x="440" y="290" text-anchor="middle">local-first — logs are read in place, normalized on this machine, and never uploaded anywhere</text>
|
|
86
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Providers
|
|
2
|
+
|
|
3
|
+
Every adapter answers three questions honestly: **where it looks**, **what it can report**, and
|
|
4
|
+
**what it cannot know**. The third one matters most — a source that doesn't log token counts must
|
|
5
|
+
say so rather than emit zeros.
|
|
6
|
+
|
|
7
|
+
Enable and disable adapters with:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
tokenflow providers
|
|
11
|
+
tokenflow provider add <id>
|
|
12
|
+
tokenflow provider remove <id>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## `anthropic` — Claude Code / Claude Agent SDK
|
|
18
|
+
|
|
19
|
+
| | |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Reads | `<claude-home>/projects/<slug>/<sessionId>.jsonl` |
|
|
22
|
+
| Homes | `$CLAUDE_CONFIG_DIR`, `~/.claude`, `~/.config/claude`, and any `~/.claude-*` sibling containing `projects/` (people running several accounts keep them side by side) |
|
|
23
|
+
| Measurement | `primary` |
|
|
24
|
+
| Reports | `input`, `output`, `cache_read`, `cache_write`, `cache_refresh` (1h ephemeral), `reasoning` (thinking), session, project (from `cwd`), git branch, interface (from `entrypoint`), sidechain/subagent flag, CLI version, service tier |
|
|
25
|
+
| Cannot know | cost (no rate in the transcript) |
|
|
26
|
+
|
|
27
|
+
### The two traps it handles
|
|
28
|
+
|
|
29
|
+
**Streaming snapshots.** A single assistant message is written to the transcript several times as
|
|
30
|
+
it streams. Every line carries the same `requestId` + `message.id` with a *growing*
|
|
31
|
+
`output_tokens` and constant prompt-side counts. Measured on a real corpus: 2,493 of 5,060 usage
|
|
32
|
+
rows were re-reports. Summing them inflates output by 3–5x. The adapter groups by
|
|
33
|
+
`(requestId, message.id)` and keeps the **per-field maximum**.
|
|
34
|
+
|
|
35
|
+
**Synthetic messages.** Locally generated assistant entries have `requestId: null` and an
|
|
36
|
+
all-zero usage block — they are not API calls. Counting them would add fake zero-token requests
|
|
37
|
+
and skew "requests" and "tokens per request". They are skipped and counted separately as
|
|
38
|
+
`synthetic`.
|
|
39
|
+
|
|
40
|
+
A group can straddle the end of a live file. Rather than defer it (which would silently drop the
|
|
41
|
+
last record of every settled file, forever), it is emitted at EOF and its totals are remembered;
|
|
42
|
+
if the file grows and the same group reappears with larger counts, only the **delta** is emitted.
|
|
43
|
+
|
|
44
|
+
### Configuration
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
sources:
|
|
48
|
+
anthropic:
|
|
49
|
+
paths: ["~/.claude", "~/.claude-work"] # skip auto-discovery
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## `openai` — Codex CLI / Codex IDE / Codex Desktop
|
|
55
|
+
|
|
56
|
+
| | |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Reads | `~/.codex/sessions/**/*.jsonl` and `~/.codex/archived_sessions/**` (`$CODEX_HOME` respected) |
|
|
59
|
+
| Measurement | `primary` |
|
|
60
|
+
| Reports | fresh `input`, `cache_read`, `cache_write`, `output`, `reasoning`, session, thread, turn id, project (from `cwd`), interface (from `source` / `originator`), gateway, reasoning effort, service tier, context window, time-to-first-token, subagent role |
|
|
61
|
+
| Cannot know | cost; `cache_write` on older CLI builds that never emitted the field (recorded as `null`, not `0`) |
|
|
62
|
+
|
|
63
|
+
### Token semantics
|
|
64
|
+
|
|
65
|
+
Codex uses OpenAI's convention where `input_tokens` **includes** `cached_input_tokens`. The
|
|
66
|
+
adapter converts:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
input_tokens = input_tokens − cached_input_tokens (fresh only)
|
|
70
|
+
cache_read_tokens = cached_input_tokens
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### One record per turn, and why the obvious reading is wrong
|
|
74
|
+
|
|
75
|
+
Codex emits a `token_count` event repeatedly while a turn runs, and `last_token_usage` is
|
|
76
|
+
**re-reported as the turn's context grows** — 27k, then 29k, then 30k, all describing the same
|
|
77
|
+
growing conversation. `total_token_usage` is a running **sum** of those re-reports, so it is not a
|
|
78
|
+
usable cumulative counter.
|
|
79
|
+
|
|
80
|
+
On a real corpus that error is enormous: one subagent session claimed **7.5 B tokens in
|
|
81
|
+
2.5 minutes** (49,739 `token_count` events for 266 actual turns), and a single day totalled
|
|
82
|
+
**82.8 B** instead of **1.8 B** — a **45x** inflation.
|
|
83
|
+
|
|
84
|
+
The adapter instead splits each turn's `last_token_usage` series into monotonically
|
|
85
|
+
non-decreasing **runs** (a drop means a context compaction or a new call) and sums each run's
|
|
86
|
+
maximum. This reduces to the identity for simple sessions with one event per turn, and was
|
|
87
|
+
cross-checked against an independent gateway billing log which agreed with the reconstruction
|
|
88
|
+
and not with the naive sum.
|
|
89
|
+
|
|
90
|
+
Each record carries `metadata.token_count_events` and `metadata.usage_segments` so the
|
|
91
|
+
reconstruction is auditable rather than a hidden fudge.
|
|
92
|
+
|
|
93
|
+
### Gateways
|
|
94
|
+
|
|
95
|
+
`model_provider: "headroom"` (or any non-vendor value) is recorded as `gateway`, not as the
|
|
96
|
+
vendor. The vendor comes from the model name.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## `cline` — Cline CLI
|
|
101
|
+
|
|
102
|
+
| | |
|
|
103
|
+
|---|---|
|
|
104
|
+
| Reads | `~/.cline/data/sessions/<id>/<id>.json` (+ `.messages.json` for a message count) |
|
|
105
|
+
| Measurement | `activity` |
|
|
106
|
+
| Reports | session, model (so the vendor is derived — e.g. `deepseek/deepseek-v4-flash` → DeepSeek), provider, duration, status, exit code, project, git branch, team, agent message count |
|
|
107
|
+
| **Cannot know** | **any token count — Cline's session files contain none** |
|
|
108
|
+
|
|
109
|
+
Every token field is `null`. The dashboard counts these sessions in activity metrics and excludes
|
|
110
|
+
them from token totals, rather than adding a session's worth of zeros. `metadata.tokens_reported`
|
|
111
|
+
is `false` so this is visible in the Data Explorer.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## `cursor` — AI code activity
|
|
116
|
+
|
|
117
|
+
| | |
|
|
118
|
+
|---|---|
|
|
119
|
+
| Reads | `~/.cursor/ai-tracking/ai-code-tracking.db` (read-only, via `node:sqlite`) |
|
|
120
|
+
| Measurement | `activity` |
|
|
121
|
+
| Reports | AI-authored code hashes (model, file, extension, conversation, attribution source: `composer` / `tab` / `human`), and per-commit AI vs human line attribution (`linesAdded`, `composerLinesAdded`, `humanLinesAdded`, AI percentage) |
|
|
122
|
+
| Cannot know | token counts (Cursor does not store them locally) |
|
|
123
|
+
|
|
124
|
+
This is the productivity-correlation source: it measures **work output**, which is the thing token
|
|
125
|
+
counts are so often wrongly assumed to prove. `model: "default"` and `NULL` are Cursor's own
|
|
126
|
+
placeholders and are recorded as unknown rather than guessed.
|
|
127
|
+
|
|
128
|
+
The database is snapshotted to a temp directory before reading (with its `-wal`/`-shm` sidecars)
|
|
129
|
+
so a live editor is never disturbed and a hot transaction never yields a stale page.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## `opencode` — OpenCode
|
|
134
|
+
|
|
135
|
+
| | |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Reads | `$XDG_DATA_HOME/opencode/opencode.db` (`~/.local/share/opencode/opencode.db`), via `node:sqlite` |
|
|
138
|
+
| Measurement | `primary` |
|
|
139
|
+
| Reports | per-request tokens: fresh input, cache read, cache write, output, reasoning; model, provider/gateway, session, project, agent mode, duration |
|
|
140
|
+
| Cannot know | git branch (not stored per message); a surface field — interface stays `Unknown` |
|
|
141
|
+
|
|
142
|
+
One record per assistant message. Token semantics were verified against a live corpus: `input`
|
|
143
|
+
is **exclusive** of cache reads (on long sessions `cache.read` exceeds `input`, which is
|
|
144
|
+
impossible under OpenAI's inclusive convention), and the five reported fields are disjoint
|
|
145
|
+
addends of the source's own total.
|
|
146
|
+
|
|
147
|
+
Two things the adapter handles that would otherwise corrupt the numbers:
|
|
148
|
+
|
|
149
|
+
- **Rows are updated in place.** An assistant row is inserted when the request starts and its
|
|
150
|
+
totals are revised as the response finalises — on a real corpus every token-bearing row had
|
|
151
|
+
`time_updated != time_created`. A naive cursor would freeze the first partial snapshot. The
|
|
152
|
+
adapter re-reads a recent window ordered by `time_updated` and emits only the *delta* for a
|
|
153
|
+
message whose totals grew — the same tails mechanism the Anthropic adapter uses for streamed
|
|
154
|
+
snapshots.
|
|
155
|
+
- **Reasoning is sometimes additive.** Most providers report reasoning as a subset of output,
|
|
156
|
+
but some report it on top. Where `reasoning > output` the only reading that satisfies the
|
|
157
|
+
schema's subset invariant without losing tokens is `output += reasoning`.
|
|
158
|
+
|
|
159
|
+
A non-vendor `providerID` (e.g. `opencode`'s own gateway) is recorded as `gateway`; vendor
|
|
160
|
+
slugs route direct. Zero-token assistant rows are aborted requests, not free API calls, and are
|
|
161
|
+
skipped.
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
sources:
|
|
165
|
+
opencode:
|
|
166
|
+
db: "~/.local/share/opencode/opencode.db" # override auto-discovery
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## `hermes` — Hermes agent
|
|
172
|
+
|
|
173
|
+
| | |
|
|
174
|
+
|---|---|
|
|
175
|
+
| Reads | `~/.hermes/state.db` (`$HERMES_HOME` honoured), via `node:sqlite` |
|
|
176
|
+
| Measurement | `primary` |
|
|
177
|
+
| Reports | per-session-per-model tokens: input, cache read, cache write, output, reasoning; billing provider (as gateway), measured cost when recorded, session source (cli/cron/whatsapp/telegram), cwd, git branch/repo, task, call count, duration |
|
|
178
|
+
| Cannot know | per-request granularity — usage is aggregated per session × model; within-group timing |
|
|
179
|
+
|
|
180
|
+
Hermes records its own LLM traffic in `session_model_usage` (one row per session × model ×
|
|
181
|
+
billing provider × task) joined to `sessions`. There is no per-request log — `messages.token_count`
|
|
182
|
+
is unpopulated — so the finest honest granularity is one record per group, timestamped at the
|
|
183
|
+
group's first API call.
|
|
184
|
+
|
|
185
|
+
Token semantics: `input_tokens` is already **exclusive** of cache reads (verified: cache reads
|
|
186
|
+
exceed input many-fold on real sessions). The columns are `NOT NULL DEFAULT 0`, so an
|
|
187
|
+
unreported split and a true zero are indistinguishable in this source; zeros are passed through
|
|
188
|
+
as zeros rather than invented into nulls.
|
|
189
|
+
|
|
190
|
+
Three things worth knowing:
|
|
191
|
+
|
|
192
|
+
- **Rows grow.** A session's usage rows are upserted as it makes more calls. Like the opencode
|
|
193
|
+
adapter, this one re-reads a recent window ordered by `last_seen` and emits only deltas; the
|
|
194
|
+
base record sits at `first_seen`, a delta at the `last_seen` that revealed it.
|
|
195
|
+
- **Gateway vs vendor.** `billing_provider` ("nous", "openrouter", "openai-codex") is routing,
|
|
196
|
+
so it becomes `gateway` — unless it IS the vendor ("anthropic" billing for a claude model is
|
|
197
|
+
a direct call). Unmapped models stay provider `unknown` with their gateway preserved;
|
|
198
|
+
`"default"` is treated as the placeholder it is.
|
|
199
|
+
- **Measured cost wins.** When hermes recorded `actual_cost_usd > 0` that is passed through as
|
|
200
|
+
a measured cost and the price-table estimate stands down. Its `estimated_cost_usd` is ignored:
|
|
201
|
+
two competing estimates would make the Cost page unauditable.
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
sources:
|
|
205
|
+
hermes:
|
|
206
|
+
db: "~/.hermes/state.db" # override auto-discovery
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## `headroom` — local LLM gateway (overlay)
|
|
212
|
+
|
|
213
|
+
| | |
|
|
214
|
+
|---|---|
|
|
215
|
+
| Reads | `~/.headroom/savings_events.jsonl` |
|
|
216
|
+
| Measurement | `overlay` — **excluded from token totals by default** |
|
|
217
|
+
| Reports | **measured** `cost_usd` per request, post-compression prompt tokens actually sent, the compression delta (`before` → `after`), model, client |
|
|
218
|
+
| Cannot know | output tokens, cache split (the savings log has neither) |
|
|
219
|
+
|
|
220
|
+
The proxy sees the same requests the Codex adapter already recorded. Adding both would double
|
|
221
|
+
count every routed token — hence `overlay`. What only the gateway knows is what the request
|
|
222
|
+
actually **cost**, which gives the Cost page an independent cross-check against the price-table
|
|
223
|
+
estimate instead of a single unverifiable number.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## `git` — activity correlation
|
|
228
|
+
|
|
229
|
+
| | |
|
|
230
|
+
|---|---|
|
|
231
|
+
| Reads | `git log --numstat` per repository |
|
|
232
|
+
| Measurement | `activity` |
|
|
233
|
+
| Reports | commits, files changed, insertions, deletions, author, branch, message |
|
|
234
|
+
| Cannot know | anything about tokens |
|
|
235
|
+
|
|
236
|
+
Repositories come from, in order: `sources.git.repos`, `sources.git.scanRoots` (searched for
|
|
237
|
+
`.git`, depth 3), and `autoFromUsage: true` — the working directories already observed in ingested
|
|
238
|
+
usage records, which means zero configuration for the common case where you code where you prompt.
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
sources:
|
|
242
|
+
git:
|
|
243
|
+
scanRoots: ["~/code"]
|
|
244
|
+
author: "you@example.com" # optional
|
|
245
|
+
since: "2026-01-01" # optional
|
|
246
|
+
autoFromUsage: true
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## `generic` — CSV / TSV / JSON / JSONL / SQLite
|
|
252
|
+
|
|
253
|
+
The escape hatch that keeps this project useful for providers nobody has written an adapter for.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
tokenflow import ~/Downloads/openrouter.csv # infers a mapping and previews it
|
|
257
|
+
tokenflow import usage.jsonl --field timestamp=ts --field input_tokens=prompt_tokens
|
|
258
|
+
tokenflow import app.db --format sqlite --table usage --field timestamp=created_at
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The mapping is saved to `$TOKENFLOW_HOME/mappings/<name>.json` and reused automatically:
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"name": "openrouter-export",
|
|
266
|
+
"format": "csv",
|
|
267
|
+
"files": ["~/Downloads/openrouter-*.csv"],
|
|
268
|
+
"timestampFormat": "iso",
|
|
269
|
+
"defaults": { "client": "openrouter", "interface": "API" },
|
|
270
|
+
"fields": {
|
|
271
|
+
"timestamp": "created_at",
|
|
272
|
+
"model": "model",
|
|
273
|
+
"input_tokens": "prompt_tokens",
|
|
274
|
+
"output_tokens": "completion_tokens",
|
|
275
|
+
"cache_read_tokens": "cached_tokens",
|
|
276
|
+
"estimated_cost": "cost_usd",
|
|
277
|
+
"session_id": "generation_id"
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Unmapped token fields stay `null`. A row whose timestamp cannot be parsed is rejected rather than
|
|
283
|
+
defaulted to "now". `timestampFormat` accepts `iso`, `epoch_ms`, `epoch_s`.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## `mock` — demo data
|
|
288
|
+
|
|
289
|
+
Deterministic synthetic usage so a new contributor can run `npm run demo` and see a realistic
|
|
290
|
+
dashboard without connecting anything. Only activates when explicitly asked for
|
|
291
|
+
(`TOKENFLOW_DEMO=1` or `providers: [mock]`), so it can never contaminate a real dataset by
|
|
292
|
+
accident. Every record carries `metadata.demo = true` and `machine: "demo-machine"`, and the
|
|
293
|
+
dashboard shows a persistent banner whenever any of it is in scope.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## What is deliberately not here
|
|
298
|
+
|
|
299
|
+
- **Vendor billing APIs.** They would need credentials and a network client. This project has
|
|
300
|
+
neither by design. If you can export a CSV from a billing console, `tokenflow import` will take it.
|
|
301
|
+
- **Prompt or completion text.** Adapters read counts and metadata and discard content. There is
|
|
302
|
+
nowhere in the schema to put a prompt.
|
|
303
|
+
- **Anything that writes to a source.** Adapters open source files read-only, and SQLite sources
|
|
304
|
+
are read from a temp copy.
|
|
305
|
+
|
|
306
|
+
## Adding your own
|
|
307
|
+
|
|
308
|
+
See [creating-provider.md](creating-provider.md). Drop a file in
|
|
309
|
+
`$TOKENFLOW_HOME/providers/<id>.js` and it is loaded on the next run — no core changes.
|
package/docs/skill.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Agent skill
|
|
2
|
+
|
|
3
|
+
`skills/tokenflow/` is a self-contained skill that teaches an AI coding agent to install,
|
|
4
|
+
configure, extend and debug this project on an unfamiliar machine. The point is that you should be
|
|
5
|
+
able to say:
|
|
6
|
+
|
|
7
|
+
> "Install and configure the Tokenflow for my environment."
|
|
8
|
+
|
|
9
|
+
…and get a working dashboard without explaining anything about the internals.
|
|
10
|
+
|
|
11
|
+
## What the agent can do with it
|
|
12
|
+
|
|
13
|
+
- probe the machine for supported AI tools and report what it found
|
|
14
|
+
- write a correct `config.yaml`, including non-standard source paths
|
|
15
|
+
- run a resumable ingest that fits inside short-lived shells
|
|
16
|
+
- validate the result and explain the data-health numbers
|
|
17
|
+
- add a new provider adapter for an unsupported tool
|
|
18
|
+
- build a generic import mapping from an arbitrary CSV/JSON/SQLite export
|
|
19
|
+
- configure pricing, and explain why cost is blank until it is
|
|
20
|
+
- diagnose inflated or missing numbers using the documented failure modes
|
|
21
|
+
- explain any number on the dashboard, including what it deliberately does not claim
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
skills/tokenflow/
|
|
27
|
+
SKILL.md the instructions the agent reads
|
|
28
|
+
providers/
|
|
29
|
+
detection-matrix.md where each tool stores usage, per OS
|
|
30
|
+
adapter-template.js a commented starting point
|
|
31
|
+
schemas/
|
|
32
|
+
normalized-record.json JSON Schema for the unified record
|
|
33
|
+
config.schema.json JSON Schema for config.yaml
|
|
34
|
+
examples/
|
|
35
|
+
config.yaml a fully worked configuration
|
|
36
|
+
generic-mapping.json a saved import mapping
|
|
37
|
+
session-transcript.md the awkward real-world shapes, annotated
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Using it
|
|
41
|
+
|
|
42
|
+
**Claude Code / Cowork:** copy the folder into `.claude/skills/` (project) or `~/.claude/skills/`
|
|
43
|
+
(global) and ask for it by name. **Any other agent:** point it at `SKILL.md` — it is plain
|
|
44
|
+
Markdown with no tool-specific assumptions.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
> Install and configure the Tokenflow for my environment.
|
|
48
|
+
> Add an adapter for <tool>; its logs are at <path> and look like <sample>.
|
|
49
|
+
> My Aug 15 total looks 40x too high. Diagnose it.
|
|
50
|
+
> Explain what "cache hit rate 94.8%" on my dashboard actually measures.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Design notes
|
|
54
|
+
|
|
55
|
+
The skill front-loads the **invariants**, not the API surface, because those are what an agent
|
|
56
|
+
gets wrong: missing is not zero, cache is not input, a gateway is not a vendor, interface is never
|
|
57
|
+
inferred from the model, streaming logs re-report usage, and tokens are not productivity. Each one
|
|
58
|
+
is stated with the concrete failure it prevents — including the measured 45x inflation that
|
|
59
|
+
motivated the Codex reconstruction — because an agent that knows *why* a rule exists applies it to
|
|
60
|
+
cases the rule didn't enumerate.
|
|
61
|
+
|
|
62
|
+
It also tells the agent to verify rather than assert: run `npm run validate`, cross-check a
|
|
63
|
+
suspicious day against an independent source, and write the "re-reading the same bytes emits
|
|
64
|
+
nothing new" test for any new adapter.
|