@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,191 @@
|
|
|
1
|
+
# Real log shapes, annotated
|
|
2
|
+
|
|
3
|
+
The awkward parts of real transcripts, with the trap each one sets. Reading this before writing an
|
|
4
|
+
adapter is worth more than reading the schema.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Claude Code — one message written three times
|
|
9
|
+
|
|
10
|
+
```jsonc
|
|
11
|
+
// line 1
|
|
12
|
+
{"type":"assistant","requestId":"req_011Ce…","timestamp":"2026-08-18T10:14:45.100Z",
|
|
13
|
+
"sessionId":"ec553ff4…","cwd":"/Users/you/projects/billing","gitBranch":"main",
|
|
14
|
+
"entrypoint":"cli","version":"2.1.234","isSidechain":false,
|
|
15
|
+
"message":{"id":"msg_011Ce…","model":"claude-haiku-4-5-20251001",
|
|
16
|
+
"usage":{"input_tokens":10,"output_tokens":4,
|
|
17
|
+
"cache_creation_input_tokens":38477,"cache_read_input_tokens":21339,
|
|
18
|
+
"cache_creation":{"ephemeral_1h_input_tokens":38477,"ephemeral_5m_input_tokens":0},
|
|
19
|
+
"output_tokens_details":{"thinking_tokens":2}}}}
|
|
20
|
+
// line 2 — SAME requestId + message.id, output has grown
|
|
21
|
+
{… "usage":{"input_tokens":10,"output_tokens":131,"cache_creation_input_tokens":38477,
|
|
22
|
+
"cache_read_input_tokens":21339, …}}
|
|
23
|
+
// line 3 — SAME again, final
|
|
24
|
+
{… "usage":{"input_tokens":10,"output_tokens":254,"cache_creation_input_tokens":38477,
|
|
25
|
+
"cache_read_input_tokens":21339,"output_tokens_details":{"thinking_tokens":123}}}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Trap:** summing gives `output = 389`. **Correct:** `output = 254` — the maximum. Prompt-side
|
|
29
|
+
counts are constant across the snapshots, which is the tell.
|
|
30
|
+
|
|
31
|
+
Measured on a real corpus: **2,493 of 5,060** usage rows were re-reports. All duplicates were
|
|
32
|
+
within a single file, so a per-file group buffer is enough — no global index required.
|
|
33
|
+
|
|
34
|
+
**Mapping:**
|
|
35
|
+
```
|
|
36
|
+
input_tokens -> input_tokens (already fresh-only)
|
|
37
|
+
cache_read_input_tokens -> cache_read_tokens
|
|
38
|
+
cache_creation_input_tokens -> cache_write_tokens
|
|
39
|
+
cache_creation.ephemeral_1h_… -> cache_refresh_tokens (SUBSET of cache_write)
|
|
40
|
+
output_tokens_details.thinking_tokens -> reasoning_tokens (SUBSET of output)
|
|
41
|
+
entrypoint -> interface signal
|
|
42
|
+
cwd -> project
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. Claude Code — a synthetic entry that is not an API call
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
{"type":"assistant","requestId":null,"uuid":"6e605aee…","sessionId":"eb4cf63e…",
|
|
51
|
+
"message":{"id":"c0dfe116…",
|
|
52
|
+
"usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,
|
|
53
|
+
"cache_read_input_tokens":0}}}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Trap:** it looks like a request. It isn't — no `requestId` and every count is zero. Counting it
|
|
57
|
+
adds fake zero-token requests and drags "tokens per request" down. **Rule:** skip when
|
|
58
|
+
`requestId` is null AND every usage field is zero. On the sampled corpus this was ~60% of lines
|
|
59
|
+
containing `"usage"`.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. Codex — `input_tokens` includes cached tokens
|
|
64
|
+
|
|
65
|
+
```jsonc
|
|
66
|
+
{"timestamp":"2026-08-14T15:32:14.124Z","type":"event_msg","payload":{"type":"token_count",
|
|
67
|
+
"info":{"total_token_usage":{"input_tokens":25243,"cached_input_tokens":4864,
|
|
68
|
+
"cache_write_input_tokens":0,"output_tokens":140,
|
|
69
|
+
"reasoning_output_tokens":82,"total_tokens":25383},
|
|
70
|
+
"last_token_usage":{"input_tokens":25243,"cached_input_tokens":4864,
|
|
71
|
+
"output_tokens":140,"reasoning_output_tokens":82,
|
|
72
|
+
"total_tokens":25383}}}}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Trap:** copying `input_tokens` straight through counts 4,864 tokens twice — once as fresh input,
|
|
76
|
+
once as cache read. **Correct:** `fresh = 25243 − 4864 = 20379`.
|
|
77
|
+
|
|
78
|
+
Note also `cache_write_input_tokens` is **absent** on older sessions. Absent → `null`, not `0`:
|
|
79
|
+
"the CLI didn't report it" and "no cache was written" are different facts.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 4. Codex — the expensive one: re-reported turn usage
|
|
84
|
+
|
|
85
|
+
One turn (`turn_id: 375dc371`), 1,508 `token_count` events, `last_token_usage.input_tokens`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
27531, 29361, 29533, 29900, … 170071, 170257 ← the SAME context, growing
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
and `total_token_usage` climbing to **199,900,281** for that single turn.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
sum of all last_token_usage = 201,152,484 ← what a naive adapter reports
|
|
95
|
+
max of the monotonic run = 244,880 ← the turn's actual context
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Whole-file view: 49,739 `token_count` events for **266** `task_started` turns, and 89,000 events
|
|
99
|
+
sharing two wall-clock seconds — this file is a bulk replay, not 49,739 API calls.
|
|
100
|
+
|
|
101
|
+
**Correct reconstruction:** per turn, split the `last_token_usage` series into monotonically
|
|
102
|
+
non-decreasing **runs** (a drop = a context compaction or a new call — this file had 318
|
|
103
|
+
`context_compacted` events) and sum each run's maximum.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
turn with a compaction: 30000, 5000, 9000 -> 30000 + 9000 = 39000 (2 segments)
|
|
107
|
+
simple turn: 20000, 24000, 30000 -> 30000 (1 segment)
|
|
108
|
+
single-event turn: 100 -> 100 (identity)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Effect on one real day: **82.8 B → 1.8 B**, a 45x correction. Validated against an independent
|
|
112
|
+
gateway billing log (~2.1 B input lifetime for the same model), which agreed with the corrected
|
|
113
|
+
figure and not the naive one.
|
|
114
|
+
|
|
115
|
+
Every emitted record carries `metadata.token_count_events` and `metadata.usage_segments` so the
|
|
116
|
+
reconstruction is auditable.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 5. Codex — a gateway is not a vendor
|
|
121
|
+
|
|
122
|
+
```jsonc
|
|
123
|
+
{"type":"session_meta","payload":{
|
|
124
|
+
"session_id":"019fb754…","id":"01a00510…","cwd":"/Users/you/projects/app",
|
|
125
|
+
"originator":"codex_work_desktop","cli_version":"0.147.0-alpha.6.6",
|
|
126
|
+
"source":{"subagent":{"thread_spawn":{"depth":1,"agent_role":"luna_worker"}}},
|
|
127
|
+
"thread_source":"subagent","model_provider":"headroom"}}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
model_provider: "headroom" -> gateway: "headroom" (a local proxy)
|
|
132
|
+
model: "gpt-5.6-luna" -> provider: "openai" (the vendor, from the MODEL)
|
|
133
|
+
originator: codex_work_desktop -> interface: Desktop App
|
|
134
|
+
thread_source: "subagent" -> category: "subagent"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Trap:** recording `provider: "headroom"` invents a vendor that doesn't exist and hides all the
|
|
138
|
+
OpenAI usage. Note `source` is sometimes a string (`"cli"`, `"vscode"`, `"exec"`) and sometimes a
|
|
139
|
+
nested object — flatten it before using it as an interface signal.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 6. Cline — a source with no token counts at all
|
|
144
|
+
|
|
145
|
+
```jsonc
|
|
146
|
+
{"version":1,"session_id":"1785561978480_iuptv","source":"cli",
|
|
147
|
+
"started_at":"2026-08-01T05:27:23.189Z","ended_at":"2026-08-01T05:27:53.359Z",
|
|
148
|
+
"status":"completed","provider":"cline","model":"deepseek/deepseek-v4-flash",
|
|
149
|
+
"cwd":"/Users/you/projects/pipeline","metadata":{"git":{"branch":"main"}}}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Two traps.** `provider: "cline"` names the **client**, not the vendor — the vendor comes from
|
|
153
|
+
`deepseek/deepseek-v4-flash`. And there is **no usage block anywhere**: every token field must be
|
|
154
|
+
`null` with `measurement: "activity"`. Emitting zeros would add 22 free sessions to the totals and
|
|
155
|
+
pull every average down.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 7. Cursor — activity, not tokens
|
|
160
|
+
|
|
161
|
+
```sql
|
|
162
|
+
CREATE TABLE ai_code_hashes (hash TEXT PRIMARY KEY, source TEXT, fileExtension TEXT,
|
|
163
|
+
fileName TEXT, requestId TEXT, conversationId TEXT, timestamp INTEGER,
|
|
164
|
+
createdAt INTEGER, model TEXT);
|
|
165
|
+
CREATE TABLE scored_commits (commitHash TEXT, branchName TEXT, scoredAt INTEGER,
|
|
166
|
+
linesAdded INT, composerLinesAdded INT, humanLinesAdded INT, v2AiPercentage TEXT, …);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
No token columns exist. This is the **work-output** signal for correlation — the thing token counts
|
|
170
|
+
are so often wrongly assumed to prove. `model: "default"` and `NULL` are Cursor's own placeholders
|
|
171
|
+
and must be recorded as unknown, not guessed. Most `scored_commits` columns are nullable and often
|
|
172
|
+
null: `null` ≠ 0.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 8. A gateway savings log — measured cost, partial tokens
|
|
177
|
+
|
|
178
|
+
```jsonc
|
|
179
|
+
{"v":1,"ts":"2026-08-13T10:05:06.449119+00:00","before":92783,"after":83161,
|
|
180
|
+
"saved":9622,"cost_usd":0.028866,"model":"gpt-5.6-sol","client":"codex","source":"proxy"}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
after -> input_tokens (post-compression: what was actually sent)
|
|
185
|
+
cost_usd -> measured_cost (cost_basis: "measured" — a real bill, not an estimate)
|
|
186
|
+
output, cache split -> null: the savings log has neither
|
|
187
|
+
measurement -> "overlay": the Codex adapter already counted this traffic
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Trap:** treating this as primary usage double counts every routed request. Its unique value is
|
|
191
|
+
that `cost_usd` is *measured*, which is what makes the Cost page's estimate checkable.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <PROVIDER NAME> — <what this tool is>
|
|
3
|
+
*
|
|
4
|
+
* Source: <exact path pattern>
|
|
5
|
+
*
|
|
6
|
+
* ## Token semantics ← FILL THIS IN. It is the most valuable part of the file.
|
|
7
|
+
*
|
|
8
|
+
* <vendor field> -> input_tokens (fresh only? or inclusive of cache?)
|
|
9
|
+
* <vendor field> -> cache_read_tokens
|
|
10
|
+
* <vendor field> -> cache_write_tokens (null if this version never emits it)
|
|
11
|
+
* <vendor field> -> output_tokens
|
|
12
|
+
* <vendor field> -> reasoning_tokens (a SUBSET of output)
|
|
13
|
+
*
|
|
14
|
+
* ## Known quirks
|
|
15
|
+
*
|
|
16
|
+
* - <does it re-report usage as a call streams? then take the max of each
|
|
17
|
+
* monotonic run, never the sum>
|
|
18
|
+
* - <are there synthetic/local entries with no real API call? skip them>
|
|
19
|
+
* - <which fields does it simply not have? those stay null, never 0>
|
|
20
|
+
*/
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import { createProvider, readLines, walk, MEASUREMENT } from 'tokenflow/sdk';
|
|
25
|
+
|
|
26
|
+
const ID = 'my-provider';
|
|
27
|
+
|
|
28
|
+
function root(ctx) {
|
|
29
|
+
const configured = ctx?.config?.sources?.[ID]?.path;
|
|
30
|
+
if (configured) {
|
|
31
|
+
return configured.startsWith('~') ? path.join(os.homedir(), configured.slice(1)) : configured;
|
|
32
|
+
}
|
|
33
|
+
return path.join(ctx?.home || os.homedir(), '.my-tool', 'logs');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default createProvider({
|
|
37
|
+
id: ID,
|
|
38
|
+
name: 'My AI Provider',
|
|
39
|
+
description: 'Per-request token usage from My Tool.',
|
|
40
|
+
// primary = authoritative per-request usage (counted in totals)
|
|
41
|
+
// overlay = a gateway's view of the same traffic (excluded from totals by default)
|
|
42
|
+
// activity = no token accounting at all (activity + correlation only)
|
|
43
|
+
measurement: MEASUREMENT.PRIMARY,
|
|
44
|
+
requires: ['~/.my-tool/logs/*.jsonl'],
|
|
45
|
+
|
|
46
|
+
/** Existence check only. Read no data, write nothing. */
|
|
47
|
+
async detect(ctx) {
|
|
48
|
+
const dir = root(ctx);
|
|
49
|
+
if (!fs.existsSync(dir)) return { available: false, detail: `no ${dir}` };
|
|
50
|
+
return { available: true, detail: dir, paths: [dir] };
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/** Enumerate source files. `key` must be stable — it identifies incremental state. */
|
|
54
|
+
async discover(ctx) {
|
|
55
|
+
const dir = root(ctx);
|
|
56
|
+
return walk(dir, (name) => name.endsWith('.jsonl')).flatMap((p) => {
|
|
57
|
+
let stat;
|
|
58
|
+
try { stat = fs.statSync(p); } catch { return []; } // vanished mid-scan
|
|
59
|
+
if (!stat.size) return [];
|
|
60
|
+
return [{ key: path.relative(dir, p), path: p, stat }];
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async ingestFile(ref, ctx, emit) {
|
|
65
|
+
// `ref.state` is a plain object persisted per file across refreshes. Use it
|
|
66
|
+
// for header context a mid-file resume would otherwise lose.
|
|
67
|
+
const s = ref.state;
|
|
68
|
+
let records = 0;
|
|
69
|
+
let malformed = 0;
|
|
70
|
+
|
|
71
|
+
const res = readLines(
|
|
72
|
+
ref.path,
|
|
73
|
+
(line) => {
|
|
74
|
+
let o;
|
|
75
|
+
try { o = JSON.parse(line); } catch { malformed++; return; }
|
|
76
|
+
|
|
77
|
+
// Header/metadata lines: stash what later records need.
|
|
78
|
+
if (o.type === 'session_start') { s.sessionId = o.session_id; s.cwd = o.cwd; return; }
|
|
79
|
+
if (!o.usage) return;
|
|
80
|
+
|
|
81
|
+
// Skip entries that are not real API calls (no id AND all-zero usage).
|
|
82
|
+
// Counting them adds fake zero-token requests.
|
|
83
|
+
// if (!o.id && !o.usage.prompt_tokens && !o.usage.completion_tokens) return;
|
|
84
|
+
|
|
85
|
+
// If this vendor's input INCLUDES cached tokens, subtract here.
|
|
86
|
+
const inTotal = num(o.usage.prompt_tokens);
|
|
87
|
+
const cached = num(o.usage.cached_tokens);
|
|
88
|
+
const fresh = inTotal === null ? null : cached === null ? inTotal : Math.max(0, inTotal - cached);
|
|
89
|
+
|
|
90
|
+
emit({
|
|
91
|
+
timestamp: o.created_at, // ISO or epoch; the engine normalises
|
|
92
|
+
model: o.model, // the vendor is derived FROM THIS
|
|
93
|
+
input_tokens: fresh,
|
|
94
|
+
cache_read_tokens: cached,
|
|
95
|
+
cache_write_tokens: num(o.usage.cache_write), // null when unreported
|
|
96
|
+
output_tokens: num(o.usage.completion_tokens),
|
|
97
|
+
reasoning_tokens: num(o.usage.reasoning_tokens),
|
|
98
|
+
cache_refresh_tokens: null,
|
|
99
|
+
session_id: s.sessionId || ref.key,
|
|
100
|
+
request_id: o.id || null,
|
|
101
|
+
project: s.cwd ? path.basename(s.cwd) : null,
|
|
102
|
+
repository: s.cwd ? path.basename(s.cwd) : null,
|
|
103
|
+
git_branch: o.branch || null,
|
|
104
|
+
category: 'main',
|
|
105
|
+
client: 'my-tool',
|
|
106
|
+
application: 'My Tool',
|
|
107
|
+
// ORDERED, strongest first. Surface fields only — never the model.
|
|
108
|
+
interfaceSignals: [o.surface, o.client, o.entrypoint],
|
|
109
|
+
// Measured cost from the source, if it has one. Omit otherwise and the
|
|
110
|
+
// engine estimates from the price table (or leaves it null).
|
|
111
|
+
// measured_cost: o.cost_usd,
|
|
112
|
+
metadata: {
|
|
113
|
+
cwd: s.cwd || null,
|
|
114
|
+
version: o.version || null,
|
|
115
|
+
// Record anything that makes your reconstruction auditable:
|
|
116
|
+
// usage_events: n, usage_segments: k,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
records++;
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
start: ref.start, // ALWAYS. This is what makes refresh incremental.
|
|
123
|
+
must: ['"usage"'], // cheap prefilter — skip lines that cannot match
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Return the offset of the last COMPLETE line so the next refresh resumes here.
|
|
128
|
+
return { offset: res.offset, records, malformed };
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** null, never 0, for an absent value. */
|
|
133
|
+
function num(v) {
|
|
134
|
+
return v === undefined || v === null ? null : Number(v);
|
|
135
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Detection matrix
|
|
2
|
+
|
|
3
|
+
Where each tool keeps local usage data, and what can be recovered from it. Probe rather than
|
|
4
|
+
assume — several of these move between versions, and users relocate them.
|
|
5
|
+
|
|
6
|
+
## Claude Code / Claude Agent SDK → adapter `anthropic`
|
|
7
|
+
|
|
8
|
+
| OS | Location |
|
|
9
|
+
|---|---|
|
|
10
|
+
| macOS / Linux | `~/.claude/projects/<slugified-cwd>/<sessionId>.jsonl` |
|
|
11
|
+
| alt homes | `$CLAUDE_CONFIG_DIR` (may be `:`-separated), `~/.config/claude`, any `~/.claude-*` containing `projects/` |
|
|
12
|
+
| Windows | `%USERPROFILE%\.claude\projects\...` |
|
|
13
|
+
|
|
14
|
+
Multiple homes are common (one per account: `~/.claude-work`, `~/.claude-personal`). The adapter
|
|
15
|
+
auto-discovers any `~/.claude*` with a `projects/` directory; add anything else to
|
|
16
|
+
`sources.anthropic.paths`.
|
|
17
|
+
|
|
18
|
+
**Recoverable:** input, output, cache read, cache write, 1h-ephemeral cache refresh, thinking
|
|
19
|
+
tokens, model, session id, request id, `cwd` → project, git branch, `entrypoint` → interface,
|
|
20
|
+
CLI version, service tier, sidechain (subagent) flag.
|
|
21
|
+
|
|
22
|
+
**Not recoverable:** cost.
|
|
23
|
+
|
|
24
|
+
**Retention:** Claude Code prunes transcripts (commonly ~30 days by default). Data already deleted
|
|
25
|
+
cannot be recovered from anywhere.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ls -d ~/.claude ~/.claude-* ~/.config/claude 2>/dev/null
|
|
29
|
+
find ~/.claude*/projects -name '*.jsonl' 2>/dev/null | wc -l
|
|
30
|
+
grep -m1 '"usage"' "$(find ~/.claude*/projects -name '*.jsonl' | head -1)" | head -c 600
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Codex CLI / IDE / Desktop → adapter `openai`
|
|
34
|
+
|
|
35
|
+
| OS | Location |
|
|
36
|
+
|---|---|
|
|
37
|
+
| macOS / Linux | `~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl`, plus `~/.codex/archived_sessions/` |
|
|
38
|
+
| alt home | `$CODEX_HOME` |
|
|
39
|
+
|
|
40
|
+
**Recoverable:** fresh input (after subtracting cached), cached input, cache write (newer builds
|
|
41
|
+
only), output, reasoning, session/thread/turn ids, `cwd` → project, `source`/`originator` →
|
|
42
|
+
interface, `model_provider` → gateway, reasoning effort, service tier, context window,
|
|
43
|
+
time-to-first-token, subagent role.
|
|
44
|
+
|
|
45
|
+
**Not recoverable:** cost.
|
|
46
|
+
|
|
47
|
+
**Two traps** — see invariants 3 and 4 in `SKILL.md`. `input_tokens` **includes**
|
|
48
|
+
`cached_input_tokens`, and `token_count` events re-report a turn's usage as it grows while
|
|
49
|
+
`total_token_usage` is a running sum of those re-reports (unusable as a cumulative counter).
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
find ~/.codex/sessions -name '*.jsonl' | wc -l
|
|
53
|
+
f=$(find ~/.codex/sessions -name '*.jsonl' | tail -1)
|
|
54
|
+
grep -c '"token_count"' "$f"; grep -c '"task_started"' "$f" # ratio >> 1 means re-reporting
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Cline CLI → adapter `cline`
|
|
58
|
+
|
|
59
|
+
`~/.cline/data/sessions/<id>/<id>.json` (+ `<id>.messages.json`).
|
|
60
|
+
|
|
61
|
+
**Recoverable:** session id, `provider`, `model` (so the vendor is derived — `deepseek/...` →
|
|
62
|
+
DeepSeek), start/end, status, exit code, `cwd`, git metadata, team, message counts.
|
|
63
|
+
|
|
64
|
+
**NOT recoverable: any token count.** All token fields must be `null` and the record
|
|
65
|
+
`measurement: activity`.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
ls ~/.cline/data/sessions | wc -l
|
|
69
|
+
cat ~/.cline/data/sessions/*/*[!s].json | head -40
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Cursor → adapter `cursor`
|
|
73
|
+
|
|
74
|
+
`~/.cursor/ai-tracking/ai-code-tracking.db` (SQLite; needs `node:sqlite`, i.e. Node ≥ 22.5).
|
|
75
|
+
|
|
76
|
+
Tables that matter: `ai_code_hashes` (AI-authored code events: model, file, extension,
|
|
77
|
+
conversation, `source` ∈ composer/tab/human) and `scored_commits` (per-commit AI vs human line
|
|
78
|
+
attribution).
|
|
79
|
+
|
|
80
|
+
**Recoverable:** AI edit activity, per-commit attribution, model names as Cursor labels them
|
|
81
|
+
(`claude-4.5-sonnet`, `composer-1`, and the placeholder `default`/`NULL` → unknown).
|
|
82
|
+
|
|
83
|
+
**Not recoverable:** token counts — Cursor does not store them locally.
|
|
84
|
+
|
|
85
|
+
Read from a temp snapshot (with `-wal`/`-shm`) so a live editor is never disturbed.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ls -la ~/.cursor/ai-tracking/
|
|
89
|
+
node -e "const{DatabaseSync}=require('node:sqlite');const d=new DatabaseSync(process.argv[1]);console.log(d.prepare(\"select name from sqlite_master where type='table'\").all())" ~/.cursor/ai-tracking/ai-code-tracking.db
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Local gateways / proxies → adapter `headroom` (or a new overlay adapter)
|
|
93
|
+
|
|
94
|
+
`~/.headroom/savings_events.jsonl`, plus `proxy_savings.json` and a SQLite store.
|
|
95
|
+
|
|
96
|
+
**Recoverable:** measured `cost_usd` per request, post-compression prompt tokens actually sent,
|
|
97
|
+
the compression delta, model, client.
|
|
98
|
+
|
|
99
|
+
**Not recoverable:** output tokens, cache split.
|
|
100
|
+
|
|
101
|
+
Always `measurement: overlay` — the proxy is a second view of traffic a client adapter already
|
|
102
|
+
recorded. Its unique value is the *measured* cost.
|
|
103
|
+
|
|
104
|
+
Any router of this shape (LiteLLM, OpenRouter's local proxy, a custom one) fits the same pattern:
|
|
105
|
+
one overlay adapter, measured cost, excluded from token totals.
|
|
106
|
+
|
|
107
|
+
## Git → adapter `git`
|
|
108
|
+
|
|
109
|
+
Any repository. Needs `git` on `PATH`.
|
|
110
|
+
|
|
111
|
+
**Recoverable:** commits, files changed, insertions, deletions, author, branch, message.
|
|
112
|
+
|
|
113
|
+
Repos come from `sources.git.repos`, `sources.git.scanRoots` (depth 3), or `autoFromUsage: true`
|
|
114
|
+
(the working directories already seen in ingested usage records — zero config when you code where
|
|
115
|
+
you prompt).
|
|
116
|
+
|
|
117
|
+
## Other tools worth probing
|
|
118
|
+
|
|
119
|
+
| Tool | Look at | Typically has token counts? |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| GitHub Copilot CLI | `~/.copilot/logs/` | no |
|
|
122
|
+
| Continue | `~/.continue/dev_data/*.jsonl` | sometimes (`tokensGenerated.jsonl`) |
|
|
123
|
+
| opencode / crush / goose | `~/.opencode`, `~/.crush`, `~/.config/goose` | varies by version |
|
|
124
|
+
| Aider | `.aider.chat.history.md`, `.aider.llm.history` | partial, text-formatted |
|
|
125
|
+
| Zed | `~/.local/share/zed/` | no |
|
|
126
|
+
| Ollama | `~/.ollama/logs/server.log` | prompt/eval counts in the log |
|
|
127
|
+
| OpenAI / Anthropic consoles | CSV export | yes → use `tokenflow import` |
|
|
128
|
+
| LiteLLM | its own DB/logs | yes → an overlay adapter |
|
|
129
|
+
|
|
130
|
+
For anything with an export, prefer `tokenflow import` over an adapter — a saved mapping is less
|
|
131
|
+
code to maintain. Write an adapter when the tool writes a log continuously.
|
|
132
|
+
|
|
133
|
+
## Quick machine sweep
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
for d in .claude .claude-* .codex .cline .cursor .headroom .continue .copilot .opencode .crush .aider .ollama; do
|
|
137
|
+
[ -e "$HOME/$d" ] && printf '%-22s %s\n' "$d" "$(du -sh "$HOME/$d" 2>/dev/null | cut -f1)"
|
|
138
|
+
done
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Size is a useful signal: a directory in the hundreds of MB is almost certainly full of
|
|
142
|
+
transcripts worth ingesting.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://tokenflow/schemas/config.schema.json",
|
|
4
|
+
"title": "tokenflow config.yaml",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"version": { "type": "integer", "const": 1 },
|
|
9
|
+
"timezone": { "type": ["string", "null"], "description": "IANA zone used to resolve date/hour/dow at ingest. null = this machine's zone." },
|
|
10
|
+
"identity": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": false,
|
|
13
|
+
"description": "Stamped onto records so a future team deployment can aggregate across machines.",
|
|
14
|
+
"properties": {
|
|
15
|
+
"user": { "type": ["string", "null"] },
|
|
16
|
+
"machine": { "type": ["string", "null"] },
|
|
17
|
+
"team": { "type": ["string", "null"] }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"providers": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": { "type": "string" },
|
|
23
|
+
"description": "Enabled adapter ids. Empty means 'all detected'."
|
|
24
|
+
},
|
|
25
|
+
"sources": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"description": "Per-adapter options. Unknown keys are passed to the adapter untouched.",
|
|
28
|
+
"additionalProperties": true,
|
|
29
|
+
"properties": {
|
|
30
|
+
"anthropic": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": { "paths": { "type": "array", "items": { "type": "string" }, "description": "Claude Code homes. Overrides auto-discovery." } }
|
|
33
|
+
},
|
|
34
|
+
"openai": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": { "paths": { "type": "array", "items": { "type": "string" }, "description": "Codex homes." } }
|
|
37
|
+
},
|
|
38
|
+
"cline": { "type": "object", "properties": { "path": { "type": "string" } } },
|
|
39
|
+
"cursor": { "type": "object", "properties": { "db": { "type": "string" } } },
|
|
40
|
+
"headroom": { "type": "object", "properties": { "path": { "type": "string" } } },
|
|
41
|
+
"git": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"properties": {
|
|
45
|
+
"repos": { "type": "array", "items": { "type": "string" } },
|
|
46
|
+
"scanRoots": { "type": "array", "items": { "type": "string" }, "description": "Searched for .git, depth 3." },
|
|
47
|
+
"autoFromUsage": { "type": "boolean", "default": true, "description": "Also use working directories seen in ingested usage records." },
|
|
48
|
+
"author": { "type": "string" },
|
|
49
|
+
"since": { "type": "string" }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"generic": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": { "imports": { "type": "array", "items": { "type": "object" }, "description": "Inline mappings; see mappings/*.json." } }
|
|
55
|
+
},
|
|
56
|
+
"mock": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"properties": { "days": { "type": "integer" }, "seed": { "type": "integer" } }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"store": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"additionalProperties": false,
|
|
65
|
+
"properties": {
|
|
66
|
+
"keepRaw": { "type": "boolean", "default": true, "description": "Keep request-level shards. false still builds the cube; only the Data Explorer and full CSV export need them." },
|
|
67
|
+
"rawRetentionDays": { "type": ["integer", "null"], "default": null }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"analytics": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"additionalProperties": false,
|
|
73
|
+
"properties": {
|
|
74
|
+
"includeOverlaySources": { "type": "boolean", "default": false, "description": "Include gateway/proxy records in token totals. Off by default: it double counts." },
|
|
75
|
+
"minSessionGapMinutes": { "type": "integer", "default": 30, "description": "Used only for sources with no session id." }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"modelMappings": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"description": "User rules, PREPENDED to the built-in model->vendor ruleset.",
|
|
81
|
+
"items": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"required": ["match", "provider"],
|
|
84
|
+
"properties": {
|
|
85
|
+
"match": { "type": "string", "description": "Case-insensitive regex against the raw model string." },
|
|
86
|
+
"provider": { "type": "string" },
|
|
87
|
+
"label": { "type": "string" },
|
|
88
|
+
"family": { "type": "string" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"interfaceOverrides": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"description": "client id -> interface. Use when you know a client's surface and the source doesn't say.",
|
|
95
|
+
"additionalProperties": { "type": "string", "enum": ["CLI", "IDE", "Desktop App", "Web", "API", "SDK", "Extension", "Unknown"] }
|
|
96
|
+
},
|
|
97
|
+
"ui": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"additionalProperties": false,
|
|
100
|
+
"properties": {
|
|
101
|
+
"theme": { "type": "string", "enum": ["dark", "light"], "default": "dark" },
|
|
102
|
+
"defaultRange": { "type": "string", "enum": ["all", "7d", "30d", "90d", "mtd", "lastmonth", "today", "yesterday"], "default": "all" },
|
|
103
|
+
"defaultFrom": { "type": ["string", "null"], "description": "Floor for the DEFAULT VIEW only (YYYY-MM-DD). Does not filter the store or --all exports." }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://tokenflow/schemas/normalized-record.json",
|
|
4
|
+
"title": "NormalizedUsageRecord",
|
|
5
|
+
"description": "The unified record every provider adapter emits. MISSING-VALUE CONTRACT: a token field is number|null. null means 'this source does not report it'; 0 means 'it reported zero'. Analytics never coerce null to 0.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "timestamp", "date", "hour", "dow", "provider", "model", "interface", "measurement", "source"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"id": { "type": "string", "description": "Stable dedup key: hash(source, session, request||timestamp, model, seq)." },
|
|
11
|
+
"timestamp": { "type": "string", "format": "date-time", "description": "ISO-8601, UTC." },
|
|
12
|
+
"date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Resolved in the capture timezone at ingest, then stored." },
|
|
13
|
+
"hour": { "type": "integer", "minimum": 0, "maximum": 23, "description": "Capture-timezone hour." },
|
|
14
|
+
"dow": { "type": "integer", "minimum": 0, "maximum": 6, "description": "0=Mon .. 6=Sun, capture timezone." },
|
|
15
|
+
"tz_offset": { "type": ["integer", "null"], "description": "Capture timezone offset in minutes." },
|
|
16
|
+
|
|
17
|
+
"provider": { "type": "string", "description": "Canonical vendor slug derived from the model name. 'unknown' when it cannot be determined — never a guess." },
|
|
18
|
+
"provider_label": { "type": "string" },
|
|
19
|
+
"gateway": { "type": ["string", "null"], "description": "Routing layer (proxy/router) that served the request. NOT the vendor." },
|
|
20
|
+
"model": { "type": "string", "description": "Raw model identifier from the source, preserved verbatim." },
|
|
21
|
+
"model_family": { "type": "string", "description": "Human tier, e.g. 'Claude Opus 5', 'GPT 5.6 (sol)'." },
|
|
22
|
+
|
|
23
|
+
"client": { "type": "string", "description": "Tool that made the call: claude-code | codex | cline | cursor | ..." },
|
|
24
|
+
"application": { "type": "string" },
|
|
25
|
+
"interface": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["CLI", "IDE", "Desktop App", "Web", "API", "SDK", "Extension", "Unknown"],
|
|
28
|
+
"description": "Derived ONLY from an explicit surface field. Never inferred from the model or provider. 'Unknown' when there was no signal."
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"input_tokens": { "type": ["number", "null"], "minimum": 0, "description": "FRESH prompt tokens, EXCLUDING cache read and cache write. Adapters for OpenAI-convention sources must subtract cached_input_tokens." },
|
|
32
|
+
"cache_read_tokens": { "type": ["number", "null"], "minimum": 0, "description": "Prompt tokens served from a prompt cache." },
|
|
33
|
+
"cache_write_tokens": { "type": ["number", "null"], "minimum": 0, "description": "Prompt tokens written into a prompt cache." },
|
|
34
|
+
"output_tokens": { "type": ["number", "null"], "minimum": 0, "description": "Generated tokens." },
|
|
35
|
+
"cache_refresh_tokens": { "type": ["number", "null"], "minimum": 0, "description": "SUBSET of cache_write_tokens (long-TTL / refreshed writes). Never added into a total again." },
|
|
36
|
+
"reasoning_tokens": { "type": ["number", "null"], "minimum": 0, "description": "SUBSET of output_tokens (thinking / reasoning). Never added into a total again." },
|
|
37
|
+
"total_tokens": { "type": ["number", "null"], "minimum": 0, "description": "DERIVED: input + cache_read + cache_write + output. Never trusted from the source." },
|
|
38
|
+
"total_is_partial": { "type": "boolean", "description": "True when at least one billable field was not available." },
|
|
39
|
+
|
|
40
|
+
"session_id": { "type": ["string", "null"] },
|
|
41
|
+
"conversation_id": { "type": ["string", "null"] },
|
|
42
|
+
"request_id": { "type": ["string", "null"] },
|
|
43
|
+
|
|
44
|
+
"project": { "type": ["string", "null"], "description": "Usually the basename of the working directory." },
|
|
45
|
+
"repository": { "type": ["string", "null"] },
|
|
46
|
+
"git_branch": { "type": ["string", "null"] },
|
|
47
|
+
"category": { "type": ["string", "null"], "description": "main | subagent | commit | ai-edit:* | ..." },
|
|
48
|
+
|
|
49
|
+
"estimated_cost": { "type": ["number", "null"], "description": "null when the model has no configured price. NEVER 0 as a stand-in for unknown." },
|
|
50
|
+
"cost_basis": { "type": ["string", "null"], "enum": ["measured", "estimated", null], "description": "'measured' = the source actually billed it; 'estimated' = computed from the price table." },
|
|
51
|
+
|
|
52
|
+
"source": { "type": "string", "description": "Adapter id." },
|
|
53
|
+
"measurement": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"enum": ["primary", "overlay", "activity"],
|
|
56
|
+
"description": "primary = authoritative per-request usage, counted in totals. overlay = a gateway's view of traffic already counted elsewhere, excluded from totals by default. activity = no token accounting at all."
|
|
57
|
+
},
|
|
58
|
+
"user": { "type": ["string", "null"] },
|
|
59
|
+
"machine": { "type": ["string", "null"] },
|
|
60
|
+
"duration_ms": { "type": ["number", "null"] },
|
|
61
|
+
"metadata": { "type": "object", "description": "Source-specific. Never interpreted by analytics. Put audit trails here (event counts, reconstruction segments, cwd, versions)." }
|
|
62
|
+
}
|
|
63
|
+
}
|