@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.
Files changed (101) hide show
  1. package/CONTRIBUTING.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/Refresh & Open Dashboard.command +22 -0
  5. package/SECURITY.md +42 -0
  6. package/bin/tokenflow.js +1342 -0
  7. package/docs/architecture.md +193 -0
  8. package/docs/cli.md +390 -0
  9. package/docs/configuration.md +281 -0
  10. package/docs/creating-provider.md +262 -0
  11. package/docs/data-model.md +213 -0
  12. package/docs/getting-started.md +266 -0
  13. package/docs/live-mode.md +199 -0
  14. package/docs/media/architecture-hero.svg +86 -0
  15. package/docs/media/cost-editorial-dark.png +0 -0
  16. package/docs/media/health-terminal-light.png +0 -0
  17. package/docs/media/menubar-dark.png +0 -0
  18. package/docs/media/menubar-light.png +0 -0
  19. package/docs/media/models-terminal-dark.png +0 -0
  20. package/docs/media/overview-aurora-dark.png +0 -0
  21. package/docs/media/time-aurora-light.png +0 -0
  22. package/docs/providers.md +309 -0
  23. package/docs/skill.md +64 -0
  24. package/docs/troubleshooting.md +207 -0
  25. package/examples/config.example.yaml +92 -0
  26. package/examples/demo-data/README.md +38 -0
  27. package/examples/demo-data/sample-usage.csv +11 -0
  28. package/package.json +74 -0
  29. package/scripts/build-dmg.sh +33 -0
  30. package/scripts/build-menubar-app.sh +67 -0
  31. package/scripts/lint.js +111 -0
  32. package/scripts/validate-install.js +140 -0
  33. package/skills/tokenflow/SKILL.md +392 -0
  34. package/skills/tokenflow/examples/config.yaml +92 -0
  35. package/skills/tokenflow/examples/generic-mapping.json +26 -0
  36. package/skills/tokenflow/examples/session-transcript.md +191 -0
  37. package/skills/tokenflow/providers/adapter-template.js +135 -0
  38. package/skills/tokenflow/providers/detection-matrix.md +142 -0
  39. package/skills/tokenflow/schemas/config.schema.json +107 -0
  40. package/skills/tokenflow/schemas/normalized-record.json +63 -0
  41. package/src/analytics/aggregate.js +247 -0
  42. package/src/analytics/anomalies.js +222 -0
  43. package/src/analytics/capacity.js +278 -0
  44. package/src/analytics/comparison.js +96 -0
  45. package/src/analytics/dimensions.js +230 -0
  46. package/src/analytics/efficiency.js +138 -0
  47. package/src/analytics/forecast.js +202 -0
  48. package/src/analytics/index.js +327 -0
  49. package/src/analytics/insights.js +283 -0
  50. package/src/analytics/milestones.js +91 -0
  51. package/src/analytics/peak.js +106 -0
  52. package/src/analytics/productivity.js +166 -0
  53. package/src/analytics/token-usage.js +267 -0
  54. package/src/commands/diagnostics.js +88 -0
  55. package/src/commands/digest.js +155 -0
  56. package/src/commands/models-compare.js +96 -0
  57. package/src/core/budget.js +142 -0
  58. package/src/core/bundle.js +191 -0
  59. package/src/core/config.js +202 -0
  60. package/src/core/delivery.js +109 -0
  61. package/src/core/geo.js +99 -0
  62. package/src/core/ingest.js +457 -0
  63. package/src/core/interface-map.js +55 -0
  64. package/src/core/jsonl.js +124 -0
  65. package/src/core/live-status.js +417 -0
  66. package/src/core/model-map.js +157 -0
  67. package/src/core/notify.js +83 -0
  68. package/src/core/pricing.js +288 -0
  69. package/src/core/prompt-analytics.js +127 -0
  70. package/src/core/registry.js +107 -0
  71. package/src/core/restore.js +261 -0
  72. package/src/core/schedule.js +120 -0
  73. package/src/core/schema.js +316 -0
  74. package/src/core/sqlite.js +96 -0
  75. package/src/core/store.js +493 -0
  76. package/src/core/sync.js +151 -0
  77. package/src/core/units.js +147 -0
  78. package/src/core/validate.js +123 -0
  79. package/src/core/watch.js +287 -0
  80. package/src/core/yaml.js +209 -0
  81. package/src/export/bundler.js +107 -0
  82. package/src/export/csv.js +100 -0
  83. package/src/export/html-snapshot.js +101 -0
  84. package/src/export/menubar.js +158 -0
  85. package/src/index.js +18 -0
  86. package/src/providers/anthropic/index.js +294 -0
  87. package/src/providers/cline/index.js +120 -0
  88. package/src/providers/cursor/index.js +143 -0
  89. package/src/providers/generic/index.js +268 -0
  90. package/src/providers/git/index.js +188 -0
  91. package/src/providers/headroom/index.js +114 -0
  92. package/src/providers/hermes/index.js +299 -0
  93. package/src/providers/mock/index.js +117 -0
  94. package/src/providers/openai/index.js +370 -0
  95. package/src/providers/opencode/index.js +245 -0
  96. package/src/sdk.js +46 -0
  97. package/src/server/server.js +264 -0
  98. package/src/ui/app.js +2473 -0
  99. package/src/ui/charts.js +925 -0
  100. package/src/ui/index.html +42 -0
  101. package/src/ui/styles.css +644 -0
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npm run validate` — a self-check an agent or a new contributor can run to
4
+ * confirm the install is sound before blaming the data.
5
+ *
6
+ * It verifies the runtime, the config directory, every adapter's detect(), the
7
+ * store's internal consistency, and that the analytics layer agrees with the
8
+ * stored facts. It never writes anything.
9
+ */
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import { loadProviders, listProviders } from '../src/core/registry.js';
13
+ import { loadConfig, paths } from '../src/core/config.js';
14
+ import { Store } from '../src/core/store.js';
15
+ import { buildBundle } from '../src/core/bundle.js';
16
+ import { computeView, indexCube, filterCube, sumRows } from '../src/analytics/index.js';
17
+ import { validateUsage } from '../src/core/validate.js';
18
+ import { decodeRecord } from '../src/core/store.js';
19
+
20
+ const checks = [];
21
+ const add = (name, ok, detail = '') => checks.push({ name, ok, detail });
22
+
23
+ // ---- runtime ---------------------------------------------------------------
24
+ const [maj, min] = process.version.slice(1).split('.').map(Number);
25
+ add('Node >= 22.5 (node:sqlite, built-in test runner)', maj > 22 || (maj === 22 && min >= 5), process.version);
26
+ let sqliteOk = false;
27
+ try {
28
+ const { sqliteAvailable } = await import('../src/core/sqlite.js');
29
+ sqliteOk = sqliteAvailable();
30
+ } catch { /* module unavailable on this runtime */ }
31
+ add('node:sqlite available (SQLite-backed sources)', sqliteOk, sqliteOk ? '' : 'SQLite adapters will report as unavailable');
32
+
33
+ // ---- config ----------------------------------------------------------------
34
+ const p = paths();
35
+ let cfg = null;
36
+ try {
37
+ cfg = loadConfig();
38
+ add('config parses', true, p.configYaml);
39
+ } catch (err) {
40
+ add('config parses', false, err.message);
41
+ }
42
+ add('data home writable', canWrite(p.root), p.root);
43
+
44
+ // ---- adapters --------------------------------------------------------------
45
+ await loadProviders();
46
+ const provs = listProviders();
47
+ add('providers load', provs.length > 0, `${provs.length} registered: ${provs.map((x) => x.id).join(', ')}`);
48
+ let detected = 0;
49
+ for (const pr of provs) {
50
+ let det;
51
+ try {
52
+ det = await pr.detect({ config: cfg || {}, home: os.homedir() });
53
+ add(` detect(${pr.id})`, true, det?.available ? `available — ${det.detail || ''}` : `not available — ${det?.detail || ''}`);
54
+ if (det?.available && pr.id !== 'mock') detected++;
55
+ } catch (err) {
56
+ add(` detect(${pr.id})`, false, err.message);
57
+ }
58
+ }
59
+ add('at least one real source detected', detected > 0 || !!process.env.TOKENFLOW_DEMO,
60
+ detected ? `${detected} source(s)` : 'none — run `tokenflow demo` or `tokenflow import <file>`');
61
+
62
+ // ---- store consistency -----------------------------------------------------
63
+ const store = new Store();
64
+ const cube = store.cube();
65
+ const bundle = buildBundle();
66
+ add('cube loads', Array.isArray(cube.rows), `${cube.rows.length} rows`);
67
+
68
+ // ---- live layer ------------------------------------------------------------
69
+ try {
70
+ const { normalizeLimits } = await import('../src/analytics/capacity.js');
71
+ const { limits, invalid } = normalizeLimits(cfg?.limits || []);
72
+ add('limits config valid', invalid.length === 0,
73
+ invalid.length ? invalid.map((x) => `${x.id ?? `#${x.index}`}: ${x.errors.join('; ')}`).join(' | ')
74
+ : `${limits.length} limit(s) declared`);
75
+ const statusFile = p.status;
76
+ const st = fs.existsSync(statusFile)
77
+ ? JSON.parse(fs.readFileSync(statusFile, 'utf8'))
78
+ : null;
79
+ add('live status file readable', st === null || (st && typeof st === 'object' && !!st.schema),
80
+ st === null ? 'not present — run `tokenflow watch` or `watch --once` to create it'
81
+ : `schema ${st.schema}, generatedAt ${st.generatedAt}`);
82
+ } catch (err) {
83
+ add('live layer checks', false, err.message);
84
+ }
85
+
86
+ if (cube.rows.length) {
87
+ // Sum the request-level facts and compare with the cube. If these disagree,
88
+ // the aggregates are stale and `tokenflow compact` should be run.
89
+ let recTotal = 0;
90
+ let recCount = 0;
91
+ let invalid = 0;
92
+ store.scanRecords((o) => {
93
+ recCount++;
94
+ const r = decodeRecord(o);
95
+ if (recCount <= 5000 && !validateUsage(r).ok) invalid++;
96
+ for (const k of ['in', 'ou', 'cr', 'cw']) recTotal += o[k] || 0;
97
+ });
98
+ const ix = indexCube(cube);
99
+ const cubeTotal = sumRows(filterCube(ix, { includeOverlay: true }), ix).total;
100
+ const drift = recTotal ? Math.abs(cubeTotal - recTotal) / recTotal : 0;
101
+ add('cube agrees with stored records', drift < 1e-9,
102
+ recCount ? `records ${recTotal.toLocaleString()} vs cube ${cubeTotal.toLocaleString()}${drift ? ` (drift ${(drift * 100).toFixed(4)}% — run "tokenflow compact")` : ''}` : 'no raw records kept');
103
+ add('stored records validate', invalid === 0, invalid ? `${invalid} invalid in the first 5000` : `${Math.min(recCount, 5000)} checked`);
104
+
105
+ const v = computeView(bundle, {});
106
+ const seriesTotal = v.daily.reduce((a, d) => a + d.total, 0);
107
+ add('daily series sums to the headline total', seriesTotal === v.totals.total,
108
+ `${seriesTotal.toLocaleString()} vs ${v.totals.total.toLocaleString()}`);
109
+ const dimTotal = v.dimensions.providers.reduce((a, x) => a + x.total, 0);
110
+ add('provider breakdown sums to the headline total', dimTotal === v.totals.total,
111
+ `${dimTotal.toLocaleString()} vs ${v.totals.total.toLocaleString()}`);
112
+ const comp = v.composition.input + v.composition.output + v.composition.cacheRead + v.composition.cacheWrite;
113
+ add('composition is exhaustive and non-overlapping', comp === v.totals.total,
114
+ `${comp.toLocaleString()} vs ${v.totals.total.toLocaleString()}`);
115
+ add('no superseded records pending compaction', (store.state.stale || []).length === 0,
116
+ (store.state.stale || []).length ? `${store.state.stale.length} stale generation(s) — run "tokenflow compact"` : '');
117
+ } else {
118
+ add('data present', false, 'no usage ingested yet — run `tokenflow refresh`');
119
+ }
120
+
121
+ // ---- report ----------------------------------------------------------------
122
+ const pad = (s, n) => (String(s).length >= n ? String(s) : String(s) + ' '.repeat(n - String(s).length));
123
+ console.log('');
124
+ let failed = 0;
125
+ for (const c of checks) {
126
+ if (!c.ok) failed++;
127
+ console.log(` ${c.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} ${pad(c.name, 48)} \x1b[2m${c.detail}\x1b[0m`);
128
+ }
129
+ console.log(`\n ${failed ? `\x1b[31m${failed} check(s) failed\x1b[0m` : '\x1b[32mall checks passed\x1b[0m'}\n`);
130
+ process.exit(failed ? 1 : 0);
131
+
132
+ function canWrite(dir) {
133
+ try {
134
+ fs.mkdirSync(dir, { recursive: true });
135
+ fs.accessSync(dir, fs.constants.W_OK);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
@@ -0,0 +1,392 @@
1
+ ---
2
+ name: tokenflow
3
+ description: Install, configure, extend or debug the Tokenflow — a local-first analytics platform for AI token usage across Claude Code, Codex, Cline, Cursor, gateways and generic exports. Use when the user wants to see their own AI token usage, connect a new usage source, add a provider adapter, import a usage export, configure pricing, or when usage numbers look wrong.
4
+ ---
5
+
6
+ # Tokenflow
7
+
8
+ A local-first, provider-agnostic analytics platform for AI token usage. Zero dependencies, Node
9
+ 22.5+. Your job is to get it running against whatever AI tools this machine actually has, and to
10
+ be honest about what the data can and cannot support.
11
+
12
+ ```
13
+ Clone → Configure → Connect sources → Refresh → Dashboard
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Read this first: the six invariants
19
+
20
+ Every mistake worth making here is a violation of one of these. They are stated with the failure
21
+ each one prevents, because you will meet cases the rules don't enumerate.
22
+
23
+ **1. Missing is not zero.** A token field is `number | null`. `null` = "this source does not
24
+ report it"; `0` = "it reported zero". Never write `|| 0` on a token field (the linter fails the
25
+ build). *Prevents:* Cline logs sessions with no token counts at all — zero-filling them would drag
26
+ every average toward zero and invent thousands of free requests.
27
+
28
+ **2. Cache tokens are not input tokens.** Four mutually exclusive billable buckets sum to the
29
+ total: fresh `input`, `cache_read`, `cache_write`, `output`. `cache_refresh` ⊆ `cache_write` and
30
+ `reasoning` ⊆ `output` are *subsets* and are never added again. *Prevents:* double counting every
31
+ cached prompt token — which on cache-heavy agent usage is >90% of all traffic.
32
+
33
+ **3. Vendors disagree about `input_tokens`.** Anthropic reports it *excluding* cache; OpenAI
34
+ reports it *including* `cached_input_tokens`. An adapter for an OpenAI-convention source must
35
+ subtract. *Prevents:* the same double count as (2), arriving through a different door.
36
+
37
+ **4. Streaming logs re-report the same usage.** A Claude Code transcript writes one assistant
38
+ message several times with a growing `output_tokens`; a Codex rollout re-reports a turn's context
39
+ as it grows. Take the **maximum of each monotonic run**, never the sum. *Prevents:* a measured
40
+ **45x** inflation — one real day read as 82.8 B tokens instead of 1.8 B, and one 2.5-minute
41
+ subagent session claiming 7.5 B tokens across 49,739 events for 266 actual turns.
42
+
43
+ **5. A gateway is not a vendor, and its log is not extra usage.** A proxy (`model_provider:
44
+ "headroom"`) is recorded as `gateway`; the vendor comes from the model name. Proxy records are
45
+ `measurement: overlay` and excluded from token totals. *Prevents:* counting the same request
46
+ twice — once from the client, once from the proxy — while still keeping the one thing only the
47
+ gateway knows: a *measured* dollar cost.
48
+
49
+ **6. Interface is never inferred from the model.** It comes only from an explicit surface field
50
+ (`entrypoint`, `originator`, `source`, an IDE marker). No signal → `Unknown`. *Prevents:*
51
+ "it's a Claude model so it must be Claude Desktop".
52
+
53
+ **7. A price needs a source, and a tier is a multiplier.** Every built-in rate names where it was
54
+ fetched from and when. Service tier is a *price multiplier* applied per request — OpenAI's Fast
55
+ mode (renamed from "priority" on 2026-07-30) is **4x** standard, Anthropic's Batch is 0.5x — and
56
+ the long-TTL cache write tier is billed above the 5-minute one. *Prevents:* under-reporting a
57
+ Fast-mode-heavy workload by up to 4x, and quoting a confident total built on rates nobody can
58
+ check. Long-context premium tiers are knowingly **not** applied, so the estimate is a documented
59
+ under-count rather than an unexplained one.
60
+
61
+ And one framing rule: **token usage is not productivity.** The Productivity section reports
62
+ activity *proxies*, and correlations with an independent work signal, with `n` stated. Never
63
+ present a token count as an achievement.
64
+
65
+ ---
66
+
67
+ ## Task: install and configure on this machine
68
+
69
+ ### 1. Check the runtime
70
+
71
+ ```bash
72
+ node -v # must be >= 22.5 (node:sqlite, node:test)
73
+ ```
74
+
75
+ There are no dependencies to install. `npm install` is a no-op.
76
+
77
+ ### 2. Find out what this machine actually has
78
+
79
+ Don't guess — probe. See `providers/detection-matrix.md` for locations per tool and OS. The
80
+ high-value checks:
81
+
82
+ ```bash
83
+ ls -d ~/.claude ~/.claude-* ~/.config/claude 2>/dev/null # Claude Code (several homes are common)
84
+ ls -d ~/.codex/sessions 2>/dev/null # Codex CLI/IDE/Desktop
85
+ ls -d ~/.cline/data/sessions 2>/dev/null # Cline
86
+ ls ~/.cursor/ai-tracking/ai-code-tracking.db 2>/dev/null # Cursor activity
87
+ ls ~/.headroom/savings_events.jsonl 2>/dev/null # local gateway
88
+ ```
89
+
90
+ Then let the tool confirm it:
91
+
92
+ ```bash
93
+ node bin/tokenflow.js setup
94
+ node bin/tokenflow.js providers
95
+ ```
96
+
97
+ Two things to notice, and to report to the user:
98
+
99
+ - **A user may have several Claude Code homes** (`~/.claude-work`, `~/.claude-personal`). The
100
+ adapter auto-discovers any `~/.claude*` containing `projects/`. If they keep one elsewhere, add
101
+ it to `sources.anthropic.paths`.
102
+ - **A tool being absent is a normal outcome**, not an error. Say which were found and which
103
+ weren't, and why.
104
+
105
+ ### 3. Handle non-standard locations
106
+
107
+ ```yaml
108
+ # ~/.tokenflow/config.yaml
109
+ providers: [anthropic, openai, cline, cursor, headroom]
110
+ sources:
111
+ anthropic:
112
+ paths: ["/custom/claude-home", "~/.claude-work"]
113
+ openai:
114
+ paths: ["/custom/codex-home"]
115
+ cursor:
116
+ db: "~/.cursor/ai-tracking/ai-code-tracking.db"
117
+ timezone: "Asia/Kolkata" # dates and hours are resolved in this zone at ingest
118
+ ui:
119
+ defaultRange: all
120
+ defaultFrom: "2026-03-14" # optional floor for the default view
121
+ ```
122
+
123
+ A full worked example is in `examples/config.yaml`. `config.json` is also accepted.
124
+
125
+ ### 4. Ingest — in chunks if your shell is short-lived
126
+
127
+ ```bash
128
+ node bin/tokenflow.js refresh --budget 30 # stops cleanly, saves state
129
+ node bin/tokenflow.js refresh --budget 30 # continues at the byte offset
130
+ ```
131
+
132
+ Repeat until the report no longer says "time budget reached". This is the right pattern in a
133
+ sandbox, a CI step, or any 45-second shell: a multi-gigabyte first ingest completes across several
134
+ calls with no partial writes. A first pass on ~5,000 session files typically reads several GB in
135
+ under a minute; the second refresh is ~2 seconds because unchanged files are skipped without
136
+ being read.
137
+
138
+ ### 5. Verify before you report
139
+
140
+ ```bash
141
+ npm run validate # runtime, adapters, and cube↔records agreement
142
+ node bin/tokenflow.js status
143
+ ```
144
+
145
+ `validate` is the one that matters: it re-sums the stored facts and compares them with the
146
+ aggregates. If they've drifted it tells you to run `tokenflow compact`.
147
+
148
+ **Then sanity-check the shape of the result.** If one day is 10–100x every other day, treat it as
149
+ suspect and investigate before presenting it (see *Task: numbers look wrong*). Reporting a
150
+ plausible-looking wrong number is worse than reporting that you're unsure.
151
+
152
+ ### 6. Serve it
153
+
154
+ ```bash
155
+ node bin/tokenflow.js up # refresh + rebuild the offline file + serve (one command)
156
+ node bin/tokenflow.js dashboard # http://127.0.0.1:7799, loopback only
157
+ node bin/tokenflow.js export --html # one self-contained offline file
158
+ ```
159
+
160
+ Use `--html` when the user can't reach a localhost port (remote shell, container, sandbox) — it
161
+ inlines the CSS, the analytics and chart code, and the data, and opens from `file://`.
162
+
163
+ Tell the user how to come back to it, because this is the question they always ask next:
164
+
165
+ - **`npm start`** (or double-click `Refresh & Open Dashboard.command` on macOS) refreshes,
166
+ rebuilds `tokenflow-dashboard.html`, and opens the live dashboard.
167
+ - The offline file cannot re-read logs — it is a file. It reports its own age and probes loopback
168
+ for a live dashboard; if one is running it offers to hand over and refresh there.
169
+ - For hands-off freshness, schedule `tokenflow up --no-serve` (launchd/systemd/cron — the plist
170
+ and cron line are in `docs/cli.md`).
171
+
172
+ If the user wants a particular look, set it in config rather than editing CSS:
173
+
174
+ ```yaml
175
+ ui:
176
+ skin: aurora # aurora | terminal | editorial
177
+ mode: dark # dark | light
178
+ ```
179
+
180
+ Never hand-edit the series colours to match a skin. They belong to the mode, they were validated
181
+ for colour-blind separation against every skin's surface, and a "nicer" hue that fails that gate
182
+ makes two adjacent stack segments indistinguishable for ~8% of male readers.
183
+
184
+ ---
185
+
186
+ ## Task: connect a source with no adapter
187
+
188
+ Two routes. Prefer **import** if the tool can export; write an **adapter** if it writes a log
189
+ continuously.
190
+
191
+ ### Route A — generic import
192
+
193
+ ```bash
194
+ node bin/tokenflow.js import ~/Downloads/usage.csv --dry-run
195
+ ```
196
+
197
+ With no `--field` flags it prints the columns, infers a mapping, and previews five normalized
198
+ rows. Check the preview: `n/a` means that field is unmapped and will be stored as not-available,
199
+ which is correct if the source truly lacks it and wrong if you just didn't map it.
200
+
201
+ ```bash
202
+ node bin/tokenflow.js import ~/Downloads/usage.csv \
203
+ --field timestamp=created_at \
204
+ --field model=model \
205
+ --field input_tokens=prompt_tokens \
206
+ --field output_tokens=completion_tokens \
207
+ --field cache_read_tokens=cached_tokens \
208
+ --field estimated_cost=cost_usd \
209
+ --default client=openrouter --default interface=API
210
+ ```
211
+
212
+ The mapping is saved and reused on every later refresh. Supports CSV/TSV/JSON/JSONL/SQLite; see
213
+ `examples/generic-mapping.json`.
214
+
215
+ **Before mapping, determine whether the source's input column includes cached tokens.** If it
216
+ does, you cannot fix that with a mapping — write an adapter (Route B) so the subtraction happens.
217
+
218
+ ### Route B — write an adapter
219
+
220
+ Start from `providers/adapter-template.js`. Then, in order:
221
+
222
+ 1. **Read a real sample first.** `head -3 <logfile> | python3 -m json.tool`. Identify: the
223
+ timestamp, the model, every token field, the session id, and any *surface* field.
224
+ 2. **Work out the token convention.** Does the input field include cached tokens? Is there a
225
+ separate cache-write? Is `reasoning` inside `output`? Write what you concluded in the adapter's
226
+ header comment — that header is the most valuable part of an adapter.
227
+ 3. **Check for re-reported usage.** Group the events of one logical call and look at the series.
228
+ Monotonically growing → snapshots, take the max. Genuinely disjoint → sum. Getting this
229
+ backwards is invariant 4, and it is the expensive mistake.
230
+ 4. **Always resume from `ref.start` and return `offset`.** Otherwise every refresh re-emits the
231
+ whole file.
232
+ 5. **Emit `interfaceSignals: [...]`**, never a guessed `interface`.
233
+
234
+ Install to `$TOKENFLOW_HOME/providers/<id>.js` (loaded automatically, shadows a built-in with the
235
+ same id) or to `src/providers/<id>/index.js` with a fixture and tests.
236
+
237
+ **Write these two tests.** The first proves the semantics; the second catches the expensive bug:
238
+
239
+ ```js
240
+ test('token semantics', async () => {
241
+ const { records } = await ingestFixtureAsync(mine, 'mine.jsonl');
242
+ assert.equal(records[0].input_tokens, 6000); // fresh, excludes cache
243
+ assert.equal(records[0].cache_write_tokens, null); // unreported stays null
244
+ for (const r of records) assert.ok(validateUsage(r).ok);
245
+ });
246
+
247
+ test('re-reading the same bytes emits nothing new', async () => {
248
+ const first = await ingestFixtureAsync(mine, 'mine.jsonl');
249
+ const again = await ingestFixtureAsync(mine, 'mine.jsonl', {
250
+ start: first.result.offset, state: first.state,
251
+ });
252
+ assert.equal(again.records.length, 0);
253
+ });
254
+ ```
255
+
256
+ `schemas/normalized-record.json` is the authoritative field list.
257
+
258
+ ---
259
+
260
+ ## Task: numbers look wrong
261
+
262
+ Work down this list; the causes are ordered by how often they're the answer.
263
+
264
+ ### Too high
265
+
266
+ 1. **Re-reported usage in an adapter** (invariant 4). Symptom: one day or session dwarfs the rest
267
+ by 10–100x. Check `metadata.token_count_events` / `metadata.usage_segments` in the Data
268
+ Explorer — a record reconstructed from hundreds of events is suspect if your adapter summed
269
+ them. Reproduce from the raw source with a throwaway script that computes both the naive sum
270
+ and the sum-of-run-maxima; the gap is the inflation.
271
+ 2. **Overlay double counting** (invariant 5). Is "Include gateway overlay" on? It adds proxy
272
+ records to client records for the same traffic. Off by default.
273
+ 3. **Cache read counted as input** (invariants 2, 3). `input + cache_read` should be the prompt
274
+ side with no overlap.
275
+
276
+ **Cross-check against something independent** — a gateway log, a billing page, a second tool.
277
+ Two sources agreeing is worth far more than one source being plausible. When the Codex
278
+ reconstruction in this repo was validated, the corrected figure (1.80 B input for a day) matched a
279
+ gateway's own billing log (~2.1 B lifetime) while the naive reading (82.8 B) did not.
280
+
281
+ ### Too low / missing
282
+
283
+ 1. **The date filter.** `ui.defaultFrom` sets a floor for the *default view*; the data is still
284
+ in the store and in `--all` exports.
285
+ 2. **"Active days" means token-active days.** Days where only a no-token source (Cline, Cursor,
286
+ git) was active are counted separately as "activity-only".
287
+ 3. **Per-adapter coverage.** Data health → Sources lists each adapter's real window. A source that
288
+ started logging in July does not cover March.
289
+ 4. **The source pruned its own logs.** Claude Code defaults to ~30 days. Nothing can recover data
290
+ the source deleted. Say so plainly.
291
+
292
+ ### Cost is blank
293
+
294
+ By design — no rate, no number, never a `$0`. `tokenflow pricing` lists models by volume with
295
+ their pricing status. Add rates in USD per million tokens:
296
+
297
+ ```bash
298
+ node bin/tokenflow.js pricing --sources # what is already known, and from where
299
+ node bin/tokenflow.js pricing --set "some-model=5,25,0.5,6.25" # in,out,cacheRead,cacheWrite
300
+ node bin/tokenflow.js refresh --full
301
+ ```
302
+
303
+ Check `--sources` first: the built-in table already covers the current Anthropic, OpenAI,
304
+ DeepSeek, Z.ai and Google line-ups from their own published pages. If a model is genuinely
305
+ missing, fetch the vendor's own pricing page — not an aggregator — and add the rate. If you cannot
306
+ source it, leave it unpriced and tell the user which models are missing: a made-up rate silently
307
+ corrupts every cost figure on the page, and unlike a blank it cannot be spotted.
308
+
309
+ Do **not** bake a service-tier multiplier into a rate. The tier is recorded per request and the
310
+ multiplier is applied automatically.
311
+
312
+ ### Aggregates disagree with the records
313
+
314
+ ```bash
315
+ npm run validate
316
+ node bin/tokenflow.js compact # rewrites shards, rebuilds aggregates; always safe
317
+ ```
318
+
319
+ ---
320
+
321
+ ## Task: explain a number
322
+
323
+ Be precise about the denominator, and about what the number does not claim.
324
+
325
+ | Metric | Formula | What it does NOT mean |
326
+ |---|---|---|
327
+ | Output / input | `out / in` (in = **fresh** input) | not "efficiency" — with a cache-heavy agent `in` is tiny, so this is large and near-meaningless alone |
328
+ | Output / prompt sent | `out / (in + cr + cw)` | the honest prompt-heavy vs output-heavy measure |
329
+ | Cache hit rate | `cr / (in + cr)` | not cache *savings* — that needs prices |
330
+ | Cache / total | `(cr + cw) / total` | share of token activity, not of cost |
331
+ | Avg / active day | total ÷ days with **measured tokens** | not ÷ calendar days, and not ÷ activity-only days |
332
+ | Peak day | argmax over the filtered slice | may be an artefact — check it against the raw source before celebrating it |
333
+ | Estimated cost | Σ tokens × configured rate | an estimate from a price table, covering only priced requests; the coverage % is always shown |
334
+ | Measured cost | a gateway's own `cost_usd` | covers only gateway-routed traffic, so it is not comparable to a total |
335
+ | Correlation r | Pearson over overlapping days | a correlation, never causation, and never a productivity claim |
336
+
337
+ If a metric is unavailable, explain *why* rather than showing zero. The dashboard is built to do
338
+ this; match its tone.
339
+
340
+ ---
341
+
342
+ ## Command reference
343
+
344
+ ```bash
345
+ node bin/tokenflow.js setup # detect and write config
346
+ node bin/tokenflow.js providers # what's connected, and why not
347
+ node bin/tokenflow.js provider add|remove <id>
348
+ node bin/tokenflow.js refresh [--full] [--provider x] [--budget 30] [--strict]
349
+ node bin/tokenflow.js status [--json]
350
+ node bin/tokenflow.js dashboard [--port n] [--no-open]
351
+ node bin/tokenflow.js up [--budget 60] [--no-serve] [--no-snapshot]
352
+ node bin/tokenflow.js restore <full-export.csv> --yes # rebuild a store from an export
353
+ node bin/tokenflow.js export --csv [--all] | --html
354
+ node bin/tokenflow.js pricing [--set "model=in,out[,cr[,cw]]"]
355
+ node bin/tokenflow.js import <file> [--field a=b] [--dry-run]
356
+ node bin/tokenflow.js config show|path|export|import
357
+ node bin/tokenflow.js doctor | validate | compact | reset --yes
358
+ node bin/tokenflow.js demo
359
+ npm test && npm run lint && npm run validate
360
+ ```
361
+
362
+ `--json` on `refresh`, `status` and `providers` for scripting.
363
+
364
+ ---
365
+
366
+ ## Working style for this project
367
+
368
+ - **Verify, don't assert.** Run `npm run validate` and read the shape of the data before
369
+ reporting. If a number surprises you, it is more likely a bug than a discovery.
370
+ - **Say what you couldn't determine.** "Cline reports no token counts, so its 22 sessions appear
371
+ in activity metrics only" is a better answer than a total that quietly includes zeros.
372
+ - **Never invent a rate, a vendor, or an interface.** Unknown is a valid, supported value
373
+ throughout the schema, and the UI is designed to display it.
374
+ - **Prefer fixing the adapter over patching the analytics.** If a number is wrong, it is almost
375
+ always wrong at ingest. The analytics layer is unit-tested against its formulas.
376
+ - **Nothing leaves the machine.** There is no HTTP client in this codebase. Don't add one, and
377
+ don't paste session contents anywhere.
378
+
379
+ ## Reference files
380
+
381
+ | File | Use |
382
+ |---|---|
383
+ | `providers/detection-matrix.md` | where each tool stores usage, per OS |
384
+ | `providers/adapter-template.js` | commented starting point for a new adapter |
385
+ | `schemas/normalized-record.json` | authoritative field list + missing-value contract |
386
+ | `schemas/config.schema.json` | every config option |
387
+ | `examples/config.yaml` | a fully worked multi-source configuration |
388
+ | `examples/generic-mapping.json` | a saved import mapping |
389
+ | `examples/session-transcript.md` | the awkward real-world log shapes, annotated |
390
+
391
+ Project docs: `docs/data-model.md`, `docs/providers.md`, `docs/creating-provider.md`,
392
+ `docs/architecture.md`, `docs/troubleshooting.md`.
@@ -0,0 +1,92 @@
1
+ # tokenflow — a fully worked configuration
2
+ # Lives at ~/.tokenflow/config.yaml (or $TOKENFLOW_HOME/config.yaml).
3
+ # config.json is also accepted if you prefer JSON.
4
+
5
+ version: 1
6
+
7
+ # Dates and hours are resolved in this zone AT INGEST and then stored, so "my
8
+ # peak hour" means your local peak hour. null = this machine's zone.
9
+ timezone: Asia/Kolkata
10
+
11
+ # Stamped onto every record. Present so a future team deployment can aggregate
12
+ # across machines without a schema change; unused in single-user mode.
13
+ identity:
14
+ user: vshah
15
+ machine: my-laptop
16
+ team: null
17
+
18
+ # Enabled adapters. An empty list means "everything detected".
19
+ providers:
20
+ - anthropic
21
+ - openai
22
+ - cline
23
+ - cursor
24
+ - headroom
25
+ - git
26
+
27
+ sources:
28
+ # Several Claude Code homes is normal — one per account. Listing them here
29
+ # skips auto-discovery entirely.
30
+ anthropic:
31
+ paths:
32
+ - "~/.claude"
33
+ - "~/.claude-work"
34
+
35
+ # $CODEX_HOME is respected; this overrides it.
36
+ openai:
37
+ paths:
38
+ - "~/.codex"
39
+
40
+ # Reports sessions but NO token counts, so every token field is null and the
41
+ # records are measurement:activity.
42
+ cline:
43
+ path: "~/.cline/data/sessions"
44
+
45
+ # SQLite; needs Node >= 22.5. Read from a temp snapshot so a live editor is
46
+ # never disturbed.
47
+ cursor:
48
+ db: "~/.cursor/ai-tracking/ai-code-tracking.db"
49
+
50
+ # A local gateway. measurement:overlay — excluded from token totals by
51
+ # default (it would double count the client adapter) but it carries the only
52
+ # MEASURED cost available anywhere.
53
+ headroom:
54
+ path: "~/.headroom"
55
+
56
+ # The independent work signal for the Productivity correlations.
57
+ git:
58
+ scanRoots:
59
+ - "~/code"
60
+ - "~/work"
61
+ autoFromUsage: true # also use cwds seen in ingested usage records
62
+ author: "you@example.com" # optional: only your own commits
63
+ since: "2026-01-01" # optional
64
+
65
+ store:
66
+ keepRaw: true # false still builds the cube; only the Data
67
+ rawRetentionDays: null # Explorer and full CSV export need the shards
68
+
69
+ analytics:
70
+ includeOverlaySources: false # leave false; see the headroom note above
71
+ minSessionGapMinutes: 30 # only used for sources with no session id
72
+
73
+ # Prepended to the built-in model -> vendor ruleset, so an internal alias maps
74
+ # to the right vendor instead of falling through to "unknown".
75
+ modelMappings:
76
+ - match: "^acme-llm-"
77
+ provider: acme
78
+ label: "Acme AI"
79
+ - match: "^internal-glm"
80
+ provider: zai
81
+ label: "Z.ai / GLM"
82
+
83
+ # Interface is never inferred from the model. If you know a client's surface and
84
+ # the source doesn't record one, say so here.
85
+ interfaceOverrides:
86
+ my-batch-runner: "API"
87
+ team-slackbot: "Web"
88
+
89
+ ui:
90
+ theme: dark
91
+ defaultRange: all
92
+ defaultFrom: "2026-03-14" # floor for the DEFAULT VIEW only
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "openrouter-export",
3
+ "format": "csv",
4
+ "files": ["~/Downloads/openrouter-activity-*.csv"],
5
+ "timestampFormat": "iso",
6
+ "measurement": "primary",
7
+ "idField": "generation_id",
8
+ "defaults": {
9
+ "client": "openrouter",
10
+ "application": "OpenRouter",
11
+ "interface": "API"
12
+ },
13
+ "fields": {
14
+ "timestamp": "created_at",
15
+ "model": "model",
16
+ "input_tokens": "tokens_prompt",
17
+ "output_tokens": "tokens_completion",
18
+ "cache_read_tokens": "cached_tokens",
19
+ "estimated_cost": "cost_usd",
20
+ "session_id": "generation_id",
21
+ "project": "app_name"
22
+ },
23
+ "metadata": {
24
+ "note": "Saved to $TOKENFLOW_HOME/mappings/openrouter-export.json and reused on every refresh. Unmapped token fields (cache_write, reasoning) stay null, not 0. `estimated_cost` from the source is recorded as cost_basis:measured, so it is never overwritten by a price-table estimate. If this export's input column INCLUDED cached tokens, a mapping could not fix that — an adapter would be needed to subtract."
25
+ }
26
+ }