@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 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 };