@marginfront/code-cost-clarity 0.5.4
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/README.md +512 -0
- package/dist/cli.d.ts +90 -0
- package/dist/cli.js +2496 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# @marginfront/code-cost-clarity
|
|
2
|
+
|
|
3
|
+
> See your Claude Code **and Codex** spend in MarginFront. One command wires your
|
|
4
|
+
> coding-agent usage telemetry through a local collector and into MarginFront,
|
|
5
|
+
> priced per engineer, per model, with **accurate prompt-cache token splitting**.
|
|
6
|
+
> One package, three modes: Claude Code only, Codex only, or both.
|
|
7
|
+
|
|
8
|
+
> **โ ๏ธ Beta (pilot software).** Under active development - pin a version, expect rough
|
|
9
|
+
> edges, and report issues.
|
|
10
|
+
|
|
11
|
+
> **Use `npx`, not `npm install`.** This is a CLI tool, not a library. Installing it
|
|
12
|
+
> into a project pulls in its dependencies and can surface unrelated audit warnings.
|
|
13
|
+
> Run it with `npx @marginfront/code-cost-clarity@latest init` (`@latest` skips a stale
|
|
14
|
+
> npx cache). The `npm i ...` box on npmjs.com is npm's auto-generated default for every
|
|
15
|
+
> package and is not the intended usage here.
|
|
16
|
+
|
|
17
|
+
**๐ Full documentation:** https://docs.marginfront.com/tools/code-cost-clarity
|
|
18
|
+
|
|
19
|
+
**This is internal cost visibility, not billing and not a spend cap.** The
|
|
20
|
+
installing company watches its own AI coding spend (per engineer, per model,
|
|
21
|
+
R&D-vs-COGS). It does not charge engineers and it does not cut anyone off.
|
|
22
|
+
|
|
23
|
+
**Your LLM keys stay yours.** This tool never reads, needs, or transmits your
|
|
24
|
+
Anthropic or OpenAI API key. The only credential it uses is your MarginFront key
|
|
25
|
+
(the secret `mf_sk_*`), and only to send usage to MarginFront.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What this does (one sentence)
|
|
30
|
+
|
|
31
|
+
Every time an engineer runs Claude Code, this connector captures the token usage
|
|
32
|
+
and sends it to MarginFront as a usage event, so you can see who used what, on
|
|
33
|
+
which model, and how much it cost.
|
|
34
|
+
|
|
35
|
+
## The mental model (read this first)
|
|
36
|
+
|
|
37
|
+
Think of it like a cash-register receipt system:
|
|
38
|
+
|
|
39
|
+
1. **Claude Code** is the register. As it works, it broadcasts receipts
|
|
40
|
+
(OpenTelemetry exports): how many tokens, which model, which engineer.
|
|
41
|
+
2. **The collector** (`otelcol-contrib`, open source) is the catcher. It runs in
|
|
42
|
+
the background, catches the receipts, and writes them to a file.
|
|
43
|
+
3. **The forwarder** (our glue, inside this package) reads those receipts and
|
|
44
|
+
sends each one to MarginFront.
|
|
45
|
+
4. **MarginFront** records the event, prices it, and shows it under the
|
|
46
|
+
engineer's email.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Claude Code (your task)
|
|
50
|
+
| broadcasts usage on a timer (you choose the interval) + once at session end
|
|
51
|
+
v
|
|
52
|
+
Collector (otelcol-contrib, local) --- converts cumulative->delta so we don't double-count
|
|
53
|
+
| writes OTLP/JSON lines to live-otlp.jsonl
|
|
54
|
+
v
|
|
55
|
+
Forwarder (this package, `run`)
|
|
56
|
+
| POST /v1/sdk/usage/record
|
|
57
|
+
v
|
|
58
|
+
MarginFront (cost dashboard)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**How often does it report?** It's _timer-based_, not per-message. Claude Code
|
|
62
|
+
flushes usage on the export interval (`OTEL_METRIC_EXPORT_INTERVAL`) plus a final
|
|
63
|
+
flush when the session ends. The package ships this at **5 minutes**, batched, so
|
|
64
|
+
you get roughly **one event per turn** without a long wait. Lower it (e.g. 60s or 5s)
|
|
65
|
+
for a more live drip, or raise it (e.g. 10 min) to batch harder. The collector's
|
|
66
|
+
delta conversion keeps repeated reports from double-counting.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
**Get your MarginFront key first.** Sign in at app.marginfront.com, go to
|
|
73
|
+
**Developer Zone -> API Keys -> "Create Key Pair"**, and copy the **secret** key
|
|
74
|
+
(`mf_sk_*`). Direct link: https://app.marginfront.com/developer-zone/api-keys
|
|
75
|
+
|
|
76
|
+
Setup is **one step** now. `init` does everything:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx @marginfront/code-cost-clarity init
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`init` walks you through it:
|
|
83
|
+
|
|
84
|
+
1. It downloads the collector (~360 MB, one time) and creates `~/.marginfront-ccc/`.
|
|
85
|
+
2. It **prompts you to paste your MarginFront SECRET key** (`mf_sk_*`) and saves it
|
|
86
|
+
to `~/.marginfront-ccc/.env` for you (mode 600). A publishable key (`mf_pk_*`)
|
|
87
|
+
is rejected on send, so use the secret one.
|
|
88
|
+
3. It shows you **exactly** what it will change in your global config and asks for a
|
|
89
|
+
**`y/N`** (defaults to **No**). See the [Privacy](#privacy-read-this) section for
|
|
90
|
+
the full list. On yes it:
|
|
91
|
+
- adds six telemetry settings to `~/.claude/settings.json`,
|
|
92
|
+
- adds a small `[otel]` block to `~/.codex/config.toml`, and
|
|
93
|
+
- installs a tiny background helper (a macOS LaunchAgent) that runs the meter.
|
|
94
|
+
|
|
95
|
+
Then **just code**:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
claude # or: codex (or open the Claude/Codex desktop apps)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
> **Already running Claude or Codex? Quit and reopen them after `init`** - and start a
|
|
102
|
+
> fresh terminal session. The desktop apps and each terminal session read the telemetry
|
|
103
|
+
> config when they launch, so anything that was already open won't emit until it's reopened.
|
|
104
|
+
|
|
105
|
+
That's it. No `source`, no second terminal. Your spend shows up in MarginFront under
|
|
106
|
+
your engineer email, automatically, and the background meter restarts itself at
|
|
107
|
+
every login. Check on it anytime:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx @marginfront/code-cost-clarity status
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Prefer a live terminal view** instead of the background meter? After `init`, run
|
|
114
|
+
`npx @marginfront/code-cost-clarity run` to stream spend in your terminal (Ctrl-C
|
|
115
|
+
stops it). You'll see a line per turn like:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
[14:22:07] recorded engineer@example.com ยท in=3210 out=287 ยท server=$0.0232 ยท cc=$0.0236 event=9f0c2a71-...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**CI / scripted installs:** add `--no-prompt` to `init` to skip BOTH questions (the
|
|
122
|
+
key paste and the consent prompt) and proceed. It **will** wire your global config
|
|
123
|
+
as listed above. Paste your key into `~/.marginfront-ccc/.env` separately.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## The background meter (always on, no terminal)
|
|
128
|
+
|
|
129
|
+
`init` (and `start`) install the forwarder as a **macOS LaunchAgent**, a small
|
|
130
|
+
background helper macOS manages for you. This is what makes "just code" work:
|
|
131
|
+
|
|
132
|
+
- **It starts at login.** No terminal to keep open, nothing to remember.
|
|
133
|
+
- **It restarts itself if it ever stops** (and survives a reboot).
|
|
134
|
+
- **It needs no key in your shell.** It reads your saved key from
|
|
135
|
+
`~/.marginfront-ccc/.env`, so a fresh login Just Works.
|
|
136
|
+
|
|
137
|
+
Manage it with three commands:
|
|
138
|
+
|
|
139
|
+
| Command | What it does |
|
|
140
|
+
| -------- | ----------------------------------------------------------------------------------------------------------- |
|
|
141
|
+
| `start` | (Re)installs + starts the background meter. `init` already does this; use it to bring it back after `stop`. |
|
|
142
|
+
| `status` | Tells you whether the meter is running, and shows the last lines of its output + error logs. |
|
|
143
|
+
| `stop` | Pauses the meter (the background daemon **and** any foreground `run`). `start` brings it back. |
|
|
144
|
+
|
|
145
|
+
**What gets captured, and what doesn't.** CCC meters the coding agents: it reads the
|
|
146
|
+
`claude_code.*` and `codex.*` telemetry that Claude Code and Codex emit to the local
|
|
147
|
+
collector. `init` wires telemetry into **both** places these agents look, so it captures
|
|
148
|
+
them in the terminal **and** the desktop apps:
|
|
149
|
+
|
|
150
|
+
- **Terminal / CLI** (`claude`, `codex`) and local IDE coding sessions read the env from
|
|
151
|
+
`~/.claude/settings.json` / `~/.codex/config.toml`.
|
|
152
|
+
- **The Claude Code and Codex desktop apps** don't read that env block and never see your
|
|
153
|
+
shell, so `init` also writes the same vars into the macOS **launchd GUI-session env**
|
|
154
|
+
(`launchctl setenv`), which Dock-launched apps inherit. **Quit and reopen a desktop app
|
|
155
|
+
after `init`** so it reads them at launch. (All four surfaces were confirmed capturing live.)
|
|
156
|
+
|
|
157
|
+
It does **not** capture:
|
|
158
|
+
|
|
159
|
+
- **The Claude consumer chat app** (the chat window) - the general assistant, not Claude
|
|
160
|
+
Code, so it never emits the coding-agent telemetry CCC reads (it also runs in a sandbox
|
|
161
|
+
separate from your machine).
|
|
162
|
+
- **Anything in a cloud sandbox or not connected to this device** - a cloud session, or a
|
|
163
|
+
remote / devcontainer / SSH setup where the agent can't reach the local collector at
|
|
164
|
+
`127.0.0.1:4318`. If a surface doesn't run here and emit to the local collector, CCC can't see it.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## The commands
|
|
169
|
+
|
|
170
|
+
| Command | What it does |
|
|
171
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
172
|
+
| `init` | The one setup command. Downloads the collector, prompts you to paste your MarginFront secret key (`mf_sk_*`), then, **after a `y/N` consent prompt**, wires telemetry into `~/.claude/settings.json` + `~/.codex/config.toml` and installs the background meter. Safe to re-run (never re-asks for a saved key, never clobbers your settings). `--no-prompt` skips both questions and proceeds (CI / scripts). |
|
|
173
|
+
| `start` | (Re)installs + starts the background meter (the LaunchAgent). `init` already runs this; use it to bring the meter back after `stop`. |
|
|
174
|
+
| `status` | Is the background meter running? Shows recent activity + the last lines of its error log. |
|
|
175
|
+
| `preview <capture.json>` | Prints the **exact** record it would send for one captured snapshot. Needs **no MarginFront key**, great for a dry run. |
|
|
176
|
+
| `run` | Streams your live spend in **this** terminal instead of the background daemon (Ctrl-C stops it + the collector). Add `--fold-cache` only for an unpriced model (see below). |
|
|
177
|
+
| `stop` | Pauses the meter: the background daemon **and** any foreground `run`. |
|
|
178
|
+
| `uninstall` | Stops everything, **undoes every config change** (the telemetry keys in settings.json, the Codex `[otel]` block, the background helper), and deletes the collector binary + runtime files (reclaims the ~360 MB). Keeps your settings. Add `--purge` to also delete your settings + MarginFront key. |
|
|
179
|
+
| `help`, `version` | The usual. |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Per-engineer attribution is automatic
|
|
184
|
+
|
|
185
|
+
What makes "who spent what" work is `user.email`, and **Claude Code puts it in
|
|
186
|
+
the telemetry on its own**. It's the engineer's logged-in Claude account email.
|
|
187
|
+
There is no manual email config. Each engineer loads the telemetry settings
|
|
188
|
+
and runs Claude Code normally.
|
|
189
|
+
|
|
190
|
+
**Works whether the engineer signs in with an org-managed seat or an interactive
|
|
191
|
+
login.** On org-managed Claude seats the email is stamped for free. If an
|
|
192
|
+
engineer's sign-in doesn't surface an email, the connector doesn't drop the
|
|
193
|
+
usage. It attributes it to a clearly labeled placeholder customer
|
|
194
|
+
(`claude-code-no-identity`) and prints how to fix it (sign in with an org-managed
|
|
195
|
+
seat, or attach a customer mapping). You'll see the placeholder in `preview`/`run`
|
|
196
|
+
output if it ever kicks in.
|
|
197
|
+
|
|
198
|
+
> The MarginFront **API key** is separate from the engineer's identity: it's the
|
|
199
|
+
> connector's own credential for posting to MarginFront. `run` needs it;
|
|
200
|
+
> `preview` does not.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Privacy (read this)
|
|
205
|
+
|
|
206
|
+
This tool is **internal cost visibility**, and being honest about what it turns on
|
|
207
|
+
matters, so here is the full picture in plain English.
|
|
208
|
+
|
|
209
|
+
**Your engineer email travels in the telemetry, in plaintext, on purpose.** That's
|
|
210
|
+
the whole point: per-engineer cost attribution needs to know who ran the turn.
|
|
211
|
+
Claude Code stamps `user.email` (your logged-in account email) onto the usage
|
|
212
|
+
telemetry, the local collector writes it to a file on your machine, and the
|
|
213
|
+
forwarder POSTs it to MarginFront so the spend lands under your name. It is **not**
|
|
214
|
+
hashed or anonymized. If you don't want the email attached, sign in with a login
|
|
215
|
+
that doesn't surface one, and the usage falls back to the `claude-code-no-identity`
|
|
216
|
+
placeholder instead.
|
|
217
|
+
|
|
218
|
+
**Telemetry becomes always-on, globally, for every Claude session.** When you say
|
|
219
|
+
"yes" to the consent prompt, `init` writes six telemetry settings into
|
|
220
|
+
`~/.claude/settings.json`. Both the `claude` command line **and** the Claude Desktop
|
|
221
|
+
app read that file, so from then on **every** Claude session on this computer
|
|
222
|
+
broadcasts usage to the local collector, not just the terminal you set it up in.
|
|
223
|
+
That's deliberate (it's what makes "just code" work and what captures Desktop), but
|
|
224
|
+
it's a real, persistent, machine-wide change, which is exactly why `init` asks first
|
|
225
|
+
and defaults to **No**.
|
|
226
|
+
|
|
227
|
+
**The exact things `init` changes (and nothing else):**
|
|
228
|
+
|
|
229
|
+
1. `~/.claude/settings.json`: adds these six keys to the `"env"` block:
|
|
230
|
+
`CLAUDE_CODE_ENABLE_TELEMETRY`, `OTEL_METRICS_EXPORTER`, `OTEL_LOGS_EXPORTER`,
|
|
231
|
+
`OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_EXPORTER_OTLP_ENDPOINT`,
|
|
232
|
+
`OTEL_METRIC_EXPORT_INTERVAL`. It backs the file up first and **never** overwrites
|
|
233
|
+
a value you'd already set.
|
|
234
|
+
2. `~/.codex/config.toml`: adds one fenced `[otel]` block (or, if you already have
|
|
235
|
+
your own `[otel]`, leaves it alone and prints the block to paste).
|
|
236
|
+
3. `~/Library/LaunchAgents/ai.marginfront.ccc.plist` + a copy of the meter program (the
|
|
237
|
+
background helper).
|
|
238
|
+
|
|
239
|
+
**Your MarginFront secret key is the one thing that never leaves
|
|
240
|
+
`~/.marginfront-ccc/.env`** (mode 600). It is **never** written into
|
|
241
|
+
`settings.json`, `config.toml`, or any telemetry payload.
|
|
242
|
+
|
|
243
|
+
**`uninstall` reverses every one of those changes:**
|
|
244
|
+
|
|
245
|
+
- removes the LaunchAgent and the copied meter program,
|
|
246
|
+
- removes **only** the six telemetry keys it added to `settings.json`, and only if
|
|
247
|
+
the value still matches what it wrote, so anything you changed survives,
|
|
248
|
+
- removes **only** its own fenced `[otel]` block from `config.toml`,
|
|
249
|
+
- deletes the collector binary + runtime files.
|
|
250
|
+
|
|
251
|
+
It keeps your saved key unless you add `--purge`. The one thing it will **not** touch
|
|
252
|
+
is your `~/.zshrc`: if an older (0.4.x) setup left a `source ~/.marginfront-ccc/.env`
|
|
253
|
+
line there, `uninstall` reminds you about it but never edits your shell file silently.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Cache pricing
|
|
258
|
+
|
|
259
|
+
Claude Code reports four kinds of tokens (fresh input, output, cache-read, and
|
|
260
|
+
cache-creation), and Anthropic prices them differently. This connector **splits
|
|
261
|
+
them into their correct typed fields** (`cacheReadTokens` / `cacheWriteTokens`)
|
|
262
|
+
so MarginFront can price each at its own cache rate instead of lumping everything
|
|
263
|
+
into one input number. **That token split is accurate by default.**
|
|
264
|
+
|
|
265
|
+
- **Default (recommended).** Cache tokens go in the typed `cacheReadTokens` /
|
|
266
|
+
`cacheWriteTokens` fields; MarginFront prices each at the model's catalog rate.
|
|
267
|
+
- **`--fold-cache` (emergency round-up only):** for a model MarginFront can't
|
|
268
|
+
price yet, this rolls the cache tokens into billed input at the fresh-input
|
|
269
|
+
rate, pushing the number **up toward reality** so it's never silently low, a
|
|
270
|
+
conservative ceiling. Use it only when a model has no cache price; otherwise
|
|
271
|
+
the default is more accurate.
|
|
272
|
+
|
|
273
|
+
The dollar figure uses your MarginFront catalog's rate for each model. If a model
|
|
274
|
+
isn't priced yet, its usage lands `NEEDS_COST_BACKFILL` (a **visible** gap, never
|
|
275
|
+
a silent $0), and one click in MarginFront prices it **both retroactively and
|
|
276
|
+
going forward**. The raw cache numbers are also kept in `metadata`, and Claude
|
|
277
|
+
Code's own cache-accurate cost is in `metadata.claudeCodeCostUsd` to reconcile
|
|
278
|
+
against.
|
|
279
|
+
|
|
280
|
+
> **Pricing note (catalog/ops):** coding agents use Anthropic's **1-hour** prompt
|
|
281
|
+
> cache, so set your catalog's cache-write rate to the **1-hour** rate (2ร base
|
|
282
|
+
> input), not the 5-minute rate (1.25ร input). Reconciling a live session against
|
|
283
|
+
> `metadata.claudeCodeCostUsd` confirmed the 1-hour rate matches to the cent; the
|
|
284
|
+
> 5-minute rate reads ~30-40% low on a cache-heavy session.
|
|
285
|
+
|
|
286
|
+
> A long-context model id like `claude-opus-4-8[1m]` is normalized to
|
|
287
|
+
> `claude-opus-4.8` to match MarginFront's pricing table. The raw id is kept in
|
|
288
|
+
> `metadata.rawModel`.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Billable tool calls (automatic)
|
|
293
|
+
|
|
294
|
+
Tokens are the bulk of coding-agent spend, but the connector also forwards
|
|
295
|
+
**billable tool calls** (a paid web search, a metered MCP or API tool) as their
|
|
296
|
+
own line items, tagged by tool name. There's **no client config**: MarginFront's
|
|
297
|
+
pricing catalog decides what's billable. A tool with a price row gets priced; any
|
|
298
|
+
other tool lands `NEEDS_COST_BACKFILL` (a visible gap you can price later, never a
|
|
299
|
+
silent charge). Free built-ins (file reads, shell, grep) are never forwarded.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Also capture Codex (second source)
|
|
304
|
+
|
|
305
|
+
The same package captures **Codex** too. Nothing extra to install. Codex reports
|
|
306
|
+
to the **same** local collector. You get three modes for free: Claude Code only,
|
|
307
|
+
Codex only, or **both** (same engineer's name on all of it).
|
|
308
|
+
|
|
309
|
+
**`init` turns it on for you** when you consent, it adds this `[otel]` block to
|
|
310
|
+
your `~/.codex/config.toml` automatically:
|
|
311
|
+
|
|
312
|
+
```toml
|
|
313
|
+
[otel]
|
|
314
|
+
exporter = { otlp-http = { endpoint = "http://127.0.0.1:4318/v1/logs", protocol = "binary" } }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The `exporter` is a **table**, the endpoint lives **inside** it and ends in
|
|
318
|
+
`/v1/logs`, and `protocol` sits in that same table. (A flat `exporter = "otlp-http"`
|
|
319
|
+
with a bare top-level `endpoint` silently exports nothing. Verified.)
|
|
320
|
+
|
|
321
|
+
The **only** case `init` won't write it for you is if you already have your **own**
|
|
322
|
+
`[otel]` settings. It won't rewrite your config; it just prints the block above so
|
|
323
|
+
you can merge it by hand. Either way, then run Codex normally. The background meter
|
|
324
|
+
(and `run`) already watches both sources from the one collector file; there's no
|
|
325
|
+
extra flag. `uninstall` removes only the block it added.
|
|
326
|
+
|
|
327
|
+
### Why Codex's numbers are computed differently
|
|
328
|
+
|
|
329
|
+
Anthropic and OpenAI count tokens differently, and getting this wrong silently
|
|
330
|
+
mis-charges you. For **Codex**, two of the token counts are **already nested
|
|
331
|
+
inside** the others:
|
|
332
|
+
|
|
333
|
+
- `cached_token_count` is **already part of** `input_token_count`.
|
|
334
|
+
- `reasoning_token_count` is **already part of** `output_token_count`.
|
|
335
|
+
|
|
336
|
+
So we do **not** add reasoning on top of output, and we do **not** bill input and
|
|
337
|
+
cached separately as if they were distinct. The accurate split we send is:
|
|
338
|
+
|
|
339
|
+
| MarginFront field | From Codex | Why |
|
|
340
|
+
| ----------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
341
|
+
| `inputTokens` | `input_token_count - cached_token_count` | the fresh, non-cached input only |
|
|
342
|
+
| `cacheReadTokens` | `cached_token_count` | priced at the cheaper cache-read rate |
|
|
343
|
+
| `outputTokens` | `output_token_count` | reasoning is **already inside** this, billed at the output rate, same as OpenAI bills it |
|
|
344
|
+
| _(no cacheWrite)_ | n/a | OpenAI has no cache-**creation** tokens |
|
|
345
|
+
|
|
346
|
+
The raw `reasoning`, `tool`, and `cached` counts are recorded in `metadata` for
|
|
347
|
+
audit; they are never billed twice. (If you ever hit an unpriced Codex model,
|
|
348
|
+
`--fold-cache` still works the same conservative way: it bills the whole input at
|
|
349
|
+
the input rate and drops the cache-read field, so the number reads high, never low.)
|
|
350
|
+
|
|
351
|
+
### Identity by sign-in mode (org seat vs ChatGPT login)
|
|
352
|
+
|
|
353
|
+
Per-engineer attribution rides on `user.email` from Codex's telemetry, same idea
|
|
354
|
+
as Claude Code. Whether that email surfaces depends on how the engineer signs in
|
|
355
|
+
to Codex (an org-managed or API-key sign-in stamps it; some interactive ChatGPT
|
|
356
|
+
logins may not). We never read that OpenAI key; the only thing that matters here is
|
|
357
|
+
whether the telemetry carries an email. If it doesn't surface, the usage isn't
|
|
358
|
+
dropped. It lands under the `codex-no-identity` placeholder with a fix hint,
|
|
359
|
+
exactly like the Claude path.
|
|
360
|
+
|
|
361
|
+
### Edge cases to know
|
|
362
|
+
|
|
363
|
+
- **`gpt-5-codex` pricing row is a precondition.** A full-rate row (including its
|
|
364
|
+
cache rate) must exist in MarginFront's pricing catalog. Until it does, a Codex
|
|
365
|
+
event lands `NEEDS_COST_BACKFILL` (a visible gap, safe), never a silent `$0`.
|
|
366
|
+
This is an ops/catalog step, not something this package can seed.
|
|
367
|
+
- **`codex-auto-review` is excluded.** It's an internal pseudo-model, not a real
|
|
368
|
+
billable model, so we drop it rather than mis-price it.
|
|
369
|
+
- **`codex` and `codex exec` both export the usage logs this package reads**,
|
|
370
|
+
verified against a live session. (Separately, some Codex versions don't emit OTel
|
|
371
|
+
_metrics_, and `codex mcp-server` telemetry has had bugs, but neither is the
|
|
372
|
+
_logs_ stream we use, so neither affects capture here.)
|
|
373
|
+
- **The `[otel]` block above is the verified shape.** If a future Codex version
|
|
374
|
+
changes it, keep the exporter a table pointed at the local collector on `/v1/logs`.
|
|
375
|
+
|
|
376
|
+
> **Confirm the dollar figure once.** The exact Codex dollar figure should be
|
|
377
|
+
> confirmed against one real captured session: that Codex reports per-turn (not
|
|
378
|
+
> session-cumulative) token counts on the log stream, and that your engineer email
|
|
379
|
+
> populates under your sign-in mode. The mapping above is the verified-safe
|
|
380
|
+
> default; the confirmation is a one-session spike, not a blocker to installing.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Confirm it landed (independent read-back)
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
KEY="<your MarginFront API key>"
|
|
388
|
+
curl -s -H "x-api-key: $KEY" \
|
|
389
|
+
"https://api.marginfront.com/v1/events?customerExternalId=you@example.com&limit=5&sortBy=usageDate&sortOrder=desc"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Maintain / shut off
|
|
395
|
+
|
|
396
|
+
- **Collector version is pinned for safety.** The installer downloads one
|
|
397
|
+
known-good, checksum-verified collector release. Moving to a newer collector
|
|
398
|
+
ships in a package update (so the integrity check always has a fingerprint to
|
|
399
|
+
verify against); there is no per-run version override.
|
|
400
|
+
- **Check on it:** `status` says whether the background meter is running and shows
|
|
401
|
+
the last lines of its logs.
|
|
402
|
+
- **Pause temporarily:** `stop` pauses the background meter (and any foreground
|
|
403
|
+
`run`). `start` brings it back; it also comes back on the next login.
|
|
404
|
+
- **Remove it:** `uninstall` stops everything, **undoes every change `init` made** (the
|
|
405
|
+
telemetry keys it added to `~/.claude/settings.json`, its `[otel]` block in
|
|
406
|
+
`~/.codex/config.toml`, and the background helper) and frees the ~360 MB. It keeps
|
|
407
|
+
your MarginFront key; `uninstall --purge` deletes your settings too. The one thing
|
|
408
|
+
it won't touch is `~/.zshrc`: if an older (0.4.x) setup left a
|
|
409
|
+
`source ~/.marginfront-ccc/.env` line there, `uninstall` reminds you but never edits
|
|
410
|
+
your shell file silently. Remove that line by hand if you like.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Troubleshooting
|
|
415
|
+
|
|
416
|
+
- **"No MARGINFRONT_API_KEY found"**: paste your MarginFront key into
|
|
417
|
+
`~/.marginfront-ccc/.env`, or `export MARGINFRONT_API_KEY=...` in the `run`
|
|
418
|
+
shell. `preview` works without one.
|
|
419
|
+
- **HTTP 401/403**: wrong/expired key, or you pasted a **publishable** key
|
|
420
|
+
(`mf_pk_*`) where a **secret** key (`mf_sk_*`) is required. The forwarder POSTs
|
|
421
|
+
usage, and publishable keys are rejected on writes. Pull a fresh secret key at
|
|
422
|
+
`app.marginfront.com` โ Developer Zone โ API Keys โ "Create Key Pair"
|
|
423
|
+
(https://app.marginfront.com/developer-zone/api-keys).
|
|
424
|
+
- **HTTP 422 / validation error**: body-shape mismatch. Run
|
|
425
|
+
`preview <capture.json>` and compare the record.
|
|
426
|
+
- **Collector file stays empty**: telemetry isn't reaching the collector. Most
|
|
427
|
+
often that's because you skipped the consent prompt (so `~/.claude/settings.json`
|
|
428
|
+
was never wired), so re-run `init` and answer `y`, then start a fresh `claude`
|
|
429
|
+
session. (Older manual setups could also be on gRPC/4317; the settings default to
|
|
430
|
+
http/protobuf/4318, the transport that actually works with Claude Code.)
|
|
431
|
+
- **`usageCost` is null**: the normalized model id didn't match the pricing
|
|
432
|
+
table. Tokens are still recorded; the cache-accurate cost is in
|
|
433
|
+
`metadata.claudeCodeCostUsd`.
|
|
434
|
+
- **Numbers ballooning**: the collector's `cumulativetodelta` processor isn't
|
|
435
|
+
running. Re-run `init` to rewrite the collector config.
|
|
436
|
+
- **Seeing `claude-code-no-identity` (or `codex-no-identity`)?** Your login didn't
|
|
437
|
+
surface an email. Use an org-managed Claude seat, or attach a customer mapping.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Security
|
|
442
|
+
|
|
443
|
+
- **This tool never reads, needs, or transmits your Anthropic or OpenAI API
|
|
444
|
+
key.** The only credential it uses is your MarginFront key, and only to send
|
|
445
|
+
usage to MarginFront.
|
|
446
|
+
- Your MarginFront key never lives in this package. It's saved only in
|
|
447
|
+
`~/.marginfront-ccc/.env` (mode 600) on your machine. The forwarder reads it
|
|
448
|
+
from the environment only, never hardcoded, never logged. It is **never** written
|
|
449
|
+
into `~/.claude/settings.json` or `~/.codex/config.toml`. Only the six non-secret
|
|
450
|
+
telemetry settings go there (see [Privacy](#privacy-read-this)).
|
|
451
|
+
- The published package contains only the built code and this README. Captured
|
|
452
|
+
telemetry, the collector binary, and your `.env` are all kept out (and the
|
|
453
|
+
package `.gitignore` guards the source tree too).
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## For engineers (technical appendix)
|
|
458
|
+
|
|
459
|
+
**Input shape:** OTLP/JSON, tree `resourceMetrics[].scopeMetrics[].metrics[]`. Two
|
|
460
|
+
metrics matter: `claude_code.token.usage` (one datapoint per `type` in
|
|
461
|
+
input/output/cacheRead/cacheCreation) and `claude_code.cost.usage` (USD).
|
|
462
|
+
|
|
463
|
+
**Value encoding (handled both ways):** the console exporter emits the value as
|
|
464
|
+
`asInt` (a JSON string); the collector's file exporter emits it as `asDouble` (a
|
|
465
|
+
JSON number).
|
|
466
|
+
|
|
467
|
+
**Grouping key:** `(user.email, model, session.id)` โ one MarginFront record per
|
|
468
|
+
group.
|
|
469
|
+
|
|
470
|
+
**Token mapping:** `input`โ`inputTokens`, `output`โ`outputTokens`,
|
|
471
|
+
`cacheRead`โ`cacheReadTokens`, `cacheCreation`โ`cacheWriteTokens` (Anthropic
|
|
472
|
+
`cache_creation_input_tokens`). With `--fold-cache`, cache tokens are added into
|
|
473
|
+
`inputTokens` instead and the typed fields are omitted (no double count).
|
|
474
|
+
|
|
475
|
+
**Temporality:** Claude Code emits CUMULATIVE counters and ignores the delta
|
|
476
|
+
preference env var. The collector's `cumulativetodelta` processor converts to
|
|
477
|
+
deltas; the forwarder trusts each line is already an increment.
|
|
478
|
+
|
|
479
|
+
**Ingest:** `POST https://api.marginfront.com/v1/sdk/usage/record`, headers
|
|
480
|
+
`Content-Type: application/json` + `x-api-key: <key>` (NOT Bearer). Body envelope
|
|
481
|
+
`{ records: [...] }`. The endpoint auto-creates the customer (by
|
|
482
|
+
`customerExternalId`) and agent (by `agentCode`) on first POST, and resolves your
|
|
483
|
+
org from the MarginFront key (the body can't override it).
|
|
484
|
+
|
|
485
|
+
### Codex source (the second input shape)
|
|
486
|
+
|
|
487
|
+
**Input shape:** OTLP/JSON **logs**, tree `resourceLogs[].scopeLogs[].logRecords[]` (a
|
|
488
|
+
different tree than Claude Code's `resourceMetrics`). The usage event is named
|
|
489
|
+
`codex.sse_event`; we detect it by that name (on the `event.name` attribute or the
|
|
490
|
+
record body) or, failing that, by the presence of any token-count attribute.
|
|
491
|
+
|
|
492
|
+
**Value encoding:** log attribute values arrive as `intValue` (a JSON string per
|
|
493
|
+
the protobuf int64 rule) or `doubleValue` (a JSON number) for token counts, and
|
|
494
|
+
`stringValue` for identity fields. All three are handled.
|
|
495
|
+
|
|
496
|
+
**Token fields:** `input_token_count`, `output_token_count`, `cached_token_count`,
|
|
497
|
+
`reasoning_token_count`, `tool_token_count`, plus `model`, `user.email`, and a
|
|
498
|
+
session id (`conversation.id` / `session.id`). The accurate **nested** mapping is
|
|
499
|
+
in the Codex section above: cached is subtracted from input, reasoning is never
|
|
500
|
+
added to output.
|
|
501
|
+
|
|
502
|
+
**Granularity:** one MarginFront record per `codex.sse_event` (one per turn). No
|
|
503
|
+
`cumulativetodelta` on the logs pipeline (that's a metrics-only processor); each
|
|
504
|
+
Codex log record is already one turn's usage.
|
|
505
|
+
|
|
506
|
+
**Identity:** `agentCode: "codex"`, `signalName: "codex-turn"`,
|
|
507
|
+
`modelProvider: "openai"`, `environment: "development"` (R&D). Records carry
|
|
508
|
+
`metadata.source: "codex"`.
|
|
509
|
+
|
|
510
|
+
**Routing:** both sources write one JSON document per line to the same collector
|
|
511
|
+
file. The forwarder routes each line by which tree it has (`resourceMetrics` โ
|
|
512
|
+
Claude Code, `resourceLogs` โ Codex), so one watcher handles either or both.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
interface WriteCodexConfigResult {
|
|
3
|
+
action: "created" | "appended" | "skipped-existing";
|
|
4
|
+
backupPath: string | null;
|
|
5
|
+
}
|
|
6
|
+
interface RemoveCodexConfigResult {
|
|
7
|
+
action: "removed" | "no-marker" | "missing";
|
|
8
|
+
backupPath: string | null;
|
|
9
|
+
}
|
|
10
|
+
interface WriteClaudeSettingsResult {
|
|
11
|
+
written: string[];
|
|
12
|
+
skipped: string[];
|
|
13
|
+
backupPath: string | null;
|
|
14
|
+
}
|
|
15
|
+
interface RemoveClaudeSettingsResult {
|
|
16
|
+
removed: string[];
|
|
17
|
+
kept: string[];
|
|
18
|
+
backupPath: string | null;
|
|
19
|
+
}
|
|
20
|
+
interface DaemonStatus {
|
|
21
|
+
loaded: boolean;
|
|
22
|
+
running: boolean;
|
|
23
|
+
pid: number | null;
|
|
24
|
+
}
|
|
25
|
+
interface ZshrcSourceLineResult {
|
|
26
|
+
found: boolean;
|
|
27
|
+
matchedLines: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PromptIO {
|
|
31
|
+
input?: NodeJS.ReadableStream;
|
|
32
|
+
output?: NodeJS.WritableStream;
|
|
33
|
+
terminal?: boolean;
|
|
34
|
+
}
|
|
35
|
+
declare function promptYesNo(promptText: string, io?: PromptIO, defaultYes?: boolean): Promise<boolean>;
|
|
36
|
+
interface InitDeps {
|
|
37
|
+
ensureConfigFiles?: () => void;
|
|
38
|
+
ensureCollectorBinary?: () => void;
|
|
39
|
+
loadEnv?: () => Record<string, string>;
|
|
40
|
+
isTTY?: () => boolean;
|
|
41
|
+
promptSecret?: (promptText: string) => Promise<string>;
|
|
42
|
+
writeApiKey?: (value: string) => void;
|
|
43
|
+
hasApiKey?: () => boolean;
|
|
44
|
+
promptConsent?: () => Promise<boolean>;
|
|
45
|
+
writeClaudeTelemetry?: () => WriteClaudeSettingsResult;
|
|
46
|
+
writeCodexConfig?: () => WriteCodexConfigResult;
|
|
47
|
+
codexHasCccBlock?: () => boolean;
|
|
48
|
+
installDaemon?: () => void;
|
|
49
|
+
findZshrcLines?: () => ZshrcSourceLineResult;
|
|
50
|
+
promptRemoveZshrc?: () => Promise<boolean>;
|
|
51
|
+
removeZshrcLines?: () => void;
|
|
52
|
+
log?: (message: string) => void;
|
|
53
|
+
}
|
|
54
|
+
declare function cmdInit(noPrompt: boolean, deps?: InitDeps): Promise<number>;
|
|
55
|
+
interface StartDeps {
|
|
56
|
+
installDaemon?: () => void;
|
|
57
|
+
hasApiKey?: () => boolean;
|
|
58
|
+
isSetUp?: () => boolean;
|
|
59
|
+
queryStatus?: () => DaemonStatus;
|
|
60
|
+
sleep?: (ms: number) => Promise<void>;
|
|
61
|
+
errLogTail?: () => string[];
|
|
62
|
+
log?: (message: string) => void;
|
|
63
|
+
errorLog?: (message: string) => void;
|
|
64
|
+
}
|
|
65
|
+
declare function cmdStart(deps?: StartDeps): Promise<number>;
|
|
66
|
+
declare function runningForwarderPid(pidFilePath: string): number | null;
|
|
67
|
+
declare function cmdRun(foldCache: boolean, _overridePidPath?: string): number | null;
|
|
68
|
+
interface StopDeps {
|
|
69
|
+
isForwarderAlive: () => boolean;
|
|
70
|
+
signalForwarder: (signal: "SIGTERM" | "SIGKILL") => void;
|
|
71
|
+
stopCollector: () => void;
|
|
72
|
+
sleep: (ms: number) => Promise<void>;
|
|
73
|
+
now: () => number;
|
|
74
|
+
log?: (message: string) => void;
|
|
75
|
+
}
|
|
76
|
+
declare function stopForwarderThenStopCollector(deps: StopDeps, pollMs: number, timeoutMs: number): Promise<void>;
|
|
77
|
+
interface UninstallDeps {
|
|
78
|
+
uninstallDaemon?: () => void;
|
|
79
|
+
stopCollector?: () => void;
|
|
80
|
+
removeClaudeTelemetry?: () => RemoveClaudeSettingsResult;
|
|
81
|
+
removeCodexConfig?: () => RemoveCodexConfigResult;
|
|
82
|
+
removeRuntimeFile?: (path: string) => void;
|
|
83
|
+
purgeConfigDir?: () => void;
|
|
84
|
+
findZshrcLines?: () => ZshrcSourceLineResult;
|
|
85
|
+
unsetDesktopEnv?: () => void;
|
|
86
|
+
log?: (message: string) => void;
|
|
87
|
+
}
|
|
88
|
+
declare function cmdUninstall(purge: boolean, deps?: UninstallDeps): number;
|
|
89
|
+
|
|
90
|
+
export { type InitDeps, type PromptIO, type StartDeps, type StopDeps, type UninstallDeps, cmdInit, cmdRun, cmdStart, cmdUninstall, promptYesNo, runningForwarderPid, stopForwarderThenStopCollector };
|