@marginfront/code-cost-clarity 0.5.4 → 0.6.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/README.md +62 -21
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +134 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> See your Claude Code **and Codex** spend in MarginFront. One command wires your
|
|
4
4
|
> coding-agent usage telemetry through a local collector and into MarginFront,
|
|
5
|
-
> priced per
|
|
5
|
+
> priced per developer, per model, with **accurate prompt-cache token splitting**.
|
|
6
6
|
> One package, three modes: Claude Code only, Codex only, or both.
|
|
7
7
|
|
|
8
8
|
> **⚠️ Beta (pilot software).** Under active development - pin a version, expect rough
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
**📖 Full documentation:** https://docs.marginfront.com/tools/code-cost-clarity
|
|
18
18
|
|
|
19
19
|
**This is internal cost visibility, not billing and not a spend cap.** The
|
|
20
|
-
installing company watches its own AI coding spend (per
|
|
21
|
-
R&D-vs-COGS). It does not charge
|
|
20
|
+
installing company watches its own AI coding spend (per developer, per model,
|
|
21
|
+
R&D-vs-COGS). It does not charge developers and it does not cut anyone off.
|
|
22
22
|
|
|
23
23
|
**Your LLM keys stay yours.** This tool never reads, needs, or transmits your
|
|
24
24
|
Anthropic or OpenAI API key. The only credential it uses is your MarginFront key
|
|
@@ -28,7 +28,7 @@ Anthropic or OpenAI API key. The only credential it uses is your MarginFront key
|
|
|
28
28
|
|
|
29
29
|
## What this does (one sentence)
|
|
30
30
|
|
|
31
|
-
Every time
|
|
31
|
+
Every time a developer runs Claude Code, this connector captures the token usage
|
|
32
32
|
and sends it to MarginFront as a usage event, so you can see who used what, on
|
|
33
33
|
which model, and how much it cost.
|
|
34
34
|
|
|
@@ -37,13 +37,13 @@ which model, and how much it cost.
|
|
|
37
37
|
Think of it like a cash-register receipt system:
|
|
38
38
|
|
|
39
39
|
1. **Claude Code** is the register. As it works, it broadcasts receipts
|
|
40
|
-
(OpenTelemetry exports): how many tokens, which model, which
|
|
40
|
+
(OpenTelemetry exports): how many tokens, which model, which developer.
|
|
41
41
|
2. **The collector** (`otelcol-contrib`, open source) is the catcher. It runs in
|
|
42
42
|
the background, catches the receipts, and writes them to a file.
|
|
43
43
|
3. **The forwarder** (our glue, inside this package) reads those receipts and
|
|
44
44
|
sends each one to MarginFront.
|
|
45
45
|
4. **MarginFront** records the event, prices it, and shows it under the
|
|
46
|
-
|
|
46
|
+
developer's email.
|
|
47
47
|
|
|
48
48
|
```
|
|
49
49
|
Claude Code (your task)
|
|
@@ -103,7 +103,7 @@ claude # or: codex (or open the Claude/Codex desktop apps)
|
|
|
103
103
|
> config when they launch, so anything that was already open won't emit until it's reopened.
|
|
104
104
|
|
|
105
105
|
That's it. No `source`, no second terminal. Your spend shows up in MarginFront under
|
|
106
|
-
your
|
|
106
|
+
your developer email, automatically, and the background meter restarts itself at
|
|
107
107
|
every login. Check on it anytime:
|
|
108
108
|
|
|
109
109
|
```bash
|
|
@@ -115,7 +115,7 @@ npx @marginfront/code-cost-clarity status
|
|
|
115
115
|
stops it). You'll see a line per turn like:
|
|
116
116
|
|
|
117
117
|
```
|
|
118
|
-
[14:22:07] recorded
|
|
118
|
+
[14:22:07] recorded developer@example.com · in=3210 out=287 · server=$0.0232 · cc=$0.0236 event=9f0c2a71-...
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
**CI / scripted installs:** add `--no-prompt` to `init` to skip BOTH questions (the
|
|
@@ -180,34 +180,75 @@ It does **not** capture:
|
|
|
180
180
|
|
|
181
181
|
---
|
|
182
182
|
|
|
183
|
-
## Per-
|
|
183
|
+
## Per-developer attribution is automatic
|
|
184
184
|
|
|
185
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
|
|
187
|
-
There is no manual email config. Each
|
|
186
|
+
the telemetry on its own**. It's the developer's logged-in Claude account email.
|
|
187
|
+
There is no manual email config. Each developer loads the telemetry settings
|
|
188
188
|
and runs Claude Code normally.
|
|
189
189
|
|
|
190
|
-
**Works whether the
|
|
190
|
+
**Works whether the developer signs in with an org-managed seat or an interactive
|
|
191
191
|
login.** On org-managed Claude seats the email is stamped for free. If an
|
|
192
|
-
|
|
192
|
+
developer's sign-in doesn't surface an email, the connector doesn't drop the
|
|
193
193
|
usage. It attributes it to a clearly labeled placeholder customer
|
|
194
194
|
(`claude-code-no-identity`) and prints how to fix it (sign in with an org-managed
|
|
195
195
|
seat, or attach a customer mapping). You'll see the placeholder in `preview`/`run`
|
|
196
196
|
output if it ever kicks in.
|
|
197
197
|
|
|
198
|
-
> The MarginFront **API key** is separate from the
|
|
198
|
+
> The MarginFront **API key** is separate from the developer's identity: it's the
|
|
199
199
|
> connector's own credential for posting to MarginFront. `run` needs it;
|
|
200
200
|
> `preview` does not.
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
|
204
|
+
## Shared one login across the team? Set a per-machine developer (`CCC_DEVELOPER_EMAIL`)
|
|
205
|
+
|
|
206
|
+
Some teams share **one** Claude/Codex login across the whole team. When that
|
|
207
|
+
happens, every developer's telemetry carries the **same** `user.email`, so all the
|
|
208
|
+
AI cost collapses onto one person and per-developer attribution breaks. CCC fixes
|
|
209
|
+
this without any change to how you log in, because **CCC runs locally on each
|
|
210
|
+
laptop** - so each laptop can carry its own developer identity.
|
|
211
|
+
|
|
212
|
+
**During `init` we ask one question:** which developer this machine's AI cost should
|
|
213
|
+
be attributed to. The prompt is pre-filled with your **global git email**
|
|
214
|
+
(`git config --global user.email`), which is usually the right person sitting at
|
|
215
|
+
this laptop. You have three choices:
|
|
216
|
+
|
|
217
|
+
- **Press Enter** to accept the pre-filled email.
|
|
218
|
+
- **Type a different email** to attribute this machine to someone else.
|
|
219
|
+
- **Clear it and leave it blank** to keep the old behavior - auto-detect from
|
|
220
|
+
whatever email the coding-agent login reports.
|
|
221
|
+
|
|
222
|
+
Whatever you choose is saved as **`CCC_DEVELOPER_EMAIL`** in
|
|
223
|
+
`~/.marginfront-ccc/.env` (mode 600, right next to your key). To change it later,
|
|
224
|
+
edit that one line and then `stop` + `start` (or just re-run `init`).
|
|
225
|
+
|
|
226
|
+
**Precedence at send time (highest wins):**
|
|
227
|
+
|
|
228
|
+
1. **`CCC_DEVELOPER_EMAIL`** (this machine's developer) - if set, it wins for **every**
|
|
229
|
+
record this machine sends, Claude **and** Codex, token turns **and** tool calls.
|
|
230
|
+
2. Otherwise the email the coding-agent login reports (`user.email`).
|
|
231
|
+
3. Otherwise the no-identity placeholder (`claude-code-no-identity` /
|
|
232
|
+
`codex-no-identity`).
|
|
233
|
+
|
|
234
|
+
If you **don't** set it (leave it blank), nothing changes - attribution is
|
|
235
|
+
byte-for-byte what it was before this feature: the login's own `user.email`, then
|
|
236
|
+
the placeholder.
|
|
237
|
+
|
|
238
|
+
> This is **internal cost visibility only**. `CCC_DEVELOPER_EMAIL` re-labels which
|
|
239
|
+
> developer the cost shows up under in _your_ MarginFront - it does **not** change
|
|
240
|
+
> who pays or any customer's bill. The `--no-prompt` (CI / scripted) install skips
|
|
241
|
+
> this question and leaves `CCC_DEVELOPER_EMAIL` unset.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
204
245
|
## Privacy (read this)
|
|
205
246
|
|
|
206
247
|
This tool is **internal cost visibility**, and being honest about what it turns on
|
|
207
248
|
matters, so here is the full picture in plain English.
|
|
208
249
|
|
|
209
|
-
**Your
|
|
210
|
-
the whole point: per-
|
|
250
|
+
**Your developer email travels in the telemetry, in plaintext, on purpose.** That's
|
|
251
|
+
the whole point: per-developer cost attribution needs to know who ran the turn.
|
|
211
252
|
Claude Code stamps `user.email` (your logged-in account email) onto the usage
|
|
212
253
|
telemetry, the local collector writes it to a file on your machine, and the
|
|
213
254
|
forwarder POSTs it to MarginFront so the spend lands under your name. It is **not**
|
|
@@ -304,7 +345,7 @@ silent charge). Free built-ins (file reads, shell, grep) are never forwarded.
|
|
|
304
345
|
|
|
305
346
|
The same package captures **Codex** too. Nothing extra to install. Codex reports
|
|
306
347
|
to the **same** local collector. You get three modes for free: Claude Code only,
|
|
307
|
-
Codex only, or **both** (same
|
|
348
|
+
Codex only, or **both** (same developer's name on all of it).
|
|
308
349
|
|
|
309
350
|
**`init` turns it on for you** when you consent, it adds this `[otel]` block to
|
|
310
351
|
your `~/.codex/config.toml` automatically:
|
|
@@ -350,8 +391,8 @@ the input rate and drops the cache-read field, so the number reads high, never l
|
|
|
350
391
|
|
|
351
392
|
### Identity by sign-in mode (org seat vs ChatGPT login)
|
|
352
393
|
|
|
353
|
-
Per-
|
|
354
|
-
as Claude Code. Whether that email surfaces depends on how the
|
|
394
|
+
Per-developer attribution rides on `user.email` from Codex's telemetry, same idea
|
|
395
|
+
as Claude Code. Whether that email surfaces depends on how the developer signs in
|
|
355
396
|
to Codex (an org-managed or API-key sign-in stamps it; some interactive ChatGPT
|
|
356
397
|
logins may not). We never read that OpenAI key; the only thing that matters here is
|
|
357
398
|
whether the telemetry carries an email. If it doesn't surface, the usage isn't
|
|
@@ -375,7 +416,7 @@ exactly like the Claude path.
|
|
|
375
416
|
|
|
376
417
|
> **Confirm the dollar figure once.** The exact Codex dollar figure should be
|
|
377
418
|
> confirmed against one real captured session: that Codex reports per-turn (not
|
|
378
|
-
> session-cumulative) token counts on the log stream, and that your
|
|
419
|
+
> session-cumulative) token counts on the log stream, and that your developer email
|
|
379
420
|
> populates under your sign-in mode. The mapping above is the verified-safe
|
|
380
421
|
> default; the confirmation is a one-session spike, not a blocker to installing.
|
|
381
422
|
|
|
@@ -454,7 +495,7 @@ curl -s -H "x-api-key: $KEY" \
|
|
|
454
495
|
|
|
455
496
|
---
|
|
456
497
|
|
|
457
|
-
## For
|
|
498
|
+
## For developers (technical appendix)
|
|
458
499
|
|
|
459
500
|
**Input shape:** OTLP/JSON, tree `resourceMetrics[].scopeMetrics[].metrics[]`. Two
|
|
460
501
|
metrics matter: `claude_code.token.usage` (one datapoint per `type` in
|
package/dist/cli.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ interface PromptIO {
|
|
|
33
33
|
terminal?: boolean;
|
|
34
34
|
}
|
|
35
35
|
declare function promptYesNo(promptText: string, io?: PromptIO, defaultYes?: boolean): Promise<boolean>;
|
|
36
|
+
declare function promptVisible(promptText: string, defaultValue?: string, io?: PromptIO): Promise<string>;
|
|
36
37
|
interface InitDeps {
|
|
37
38
|
ensureConfigFiles?: () => void;
|
|
38
39
|
ensureCollectorBinary?: () => void;
|
|
@@ -40,6 +41,9 @@ interface InitDeps {
|
|
|
40
41
|
isTTY?: () => boolean;
|
|
41
42
|
promptSecret?: (promptText: string) => Promise<string>;
|
|
42
43
|
writeApiKey?: (value: string) => void;
|
|
44
|
+
gitDefaultEmail?: () => string;
|
|
45
|
+
promptDeveloperEmail?: (defaultValue: string) => Promise<string>;
|
|
46
|
+
writeDeveloperEmail?: (value: string) => void;
|
|
43
47
|
hasApiKey?: () => boolean;
|
|
44
48
|
promptConsent?: () => Promise<boolean>;
|
|
45
49
|
writeClaudeTelemetry?: () => WriteClaudeSettingsResult;
|
|
@@ -87,4 +91,4 @@ interface UninstallDeps {
|
|
|
87
91
|
}
|
|
88
92
|
declare function cmdUninstall(purge: boolean, deps?: UninstallDeps): number;
|
|
89
93
|
|
|
90
|
-
export { type InitDeps, type PromptIO, type StartDeps, type StopDeps, type UninstallDeps, cmdInit, cmdRun, cmdStart, cmdUninstall, promptYesNo, runningForwarderPid, stopForwarderThenStopCollector };
|
|
94
|
+
export { type InitDeps, type PromptIO, type StartDeps, type StopDeps, type UninstallDeps, cmdInit, cmdRun, cmdStart, cmdUninstall, promptVisible, promptYesNo, runningForwarderPid, stopForwarderThenStopCollector };
|
package/dist/cli.js
CHANGED
|
@@ -102,6 +102,21 @@ OTEL_METRIC_EXPORT_INTERVAL=300000
|
|
|
102
102
|
# MarginFront's pricing catalog decides what's billable and at what rate. Add price
|
|
103
103
|
# rows for your paid tools in MarginFront - there's no client-side list to maintain.
|
|
104
104
|
|
|
105
|
+
# WHO this machine's AI cost belongs to (per-developer attribution).
|
|
106
|
+
# WHY THIS EXISTS, plainly: lots of teams share ONE Claude/Codex login, so every
|
|
107
|
+
# developer's usage carries the SAME login email and all the cost piles onto one
|
|
108
|
+
# person. CCC runs locally on each laptop, so naming the developer here splits that
|
|
109
|
+
# shared-login cost back out per developer. This is INTERNAL cost visibility only -
|
|
110
|
+
# it does NOT change anyone's bill.
|
|
111
|
+
#
|
|
112
|
+
# HOW IT'S USED at send time (precedence, highest wins):
|
|
113
|
+
# 1. This CCC_DEVELOPER_EMAIL, when set - it wins for EVERY record from this machine.
|
|
114
|
+
# 2. Otherwise the email the coding-agent login reports (user.email).
|
|
115
|
+
# 3. Otherwise a clearly-labeled no-identity placeholder.
|
|
116
|
+
# Leave it BLANK to keep today's behavior (auto-detect from the login). init fills
|
|
117
|
+
# this in for you, defaulting to your global git user.email - change it anytime.
|
|
118
|
+
CCC_DEVELOPER_EMAIL=
|
|
119
|
+
|
|
105
120
|
# YOUR MarginFront SECRET key (mf_sk_*). Create or copy one at:
|
|
106
121
|
# app.marginfront.com -> Build -> API Keys -> "Create Key Pair"
|
|
107
122
|
# (https://app.marginfront.com/developer-zone/api-keys)
|
|
@@ -205,7 +220,7 @@ function renderCodexConfigInstructions() {
|
|
|
205
220
|
" Codex, or both - from the one collector file; there's no extra flag.",
|
|
206
221
|
"",
|
|
207
222
|
" Note: exact [otel] key names can vary by Codex version, and whether your",
|
|
208
|
-
"
|
|
223
|
+
" developer email shows up depends on how you sign in (org API key vs ChatGPT",
|
|
209
224
|
" login). If the email doesn't surface, usage still lands under the no-identity",
|
|
210
225
|
" placeholder. See the README's Codex section for the full details."
|
|
211
226
|
].join("\n");
|
|
@@ -447,6 +462,32 @@ function writeApiKeyToEnv(value, path = ENV_PATH) {
|
|
|
447
462
|
writeFileSync(path, next, { mode: 384 });
|
|
448
463
|
chmodSync(path, 384);
|
|
449
464
|
}
|
|
465
|
+
function gitGlobalUserEmail() {
|
|
466
|
+
try {
|
|
467
|
+
const out = execFileSync("git", ["config", "--global", "user.email"], {
|
|
468
|
+
encoding: "utf8",
|
|
469
|
+
// Swallow git's own stderr so a "key not set" message can't leak to the user.
|
|
470
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
471
|
+
});
|
|
472
|
+
return out.trim();
|
|
473
|
+
} catch {
|
|
474
|
+
return "";
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function writeDeveloperEmailToEnv(value, path = ENV_PATH) {
|
|
478
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : renderEnvFile();
|
|
479
|
+
const developerLine = /^CCC_DEVELOPER_EMAIL=.*$/m;
|
|
480
|
+
let next;
|
|
481
|
+
if (developerLine.test(current)) {
|
|
482
|
+
next = current.replace(developerLine, () => `CCC_DEVELOPER_EMAIL=${value}`);
|
|
483
|
+
} else {
|
|
484
|
+
const sep = current.endsWith("\n") || current === "" ? "" : "\n";
|
|
485
|
+
next = `${current}${sep}CCC_DEVELOPER_EMAIL=${value}
|
|
486
|
+
`;
|
|
487
|
+
}
|
|
488
|
+
writeFileSync(path, next, { mode: 384 });
|
|
489
|
+
chmodSync(path, 384);
|
|
490
|
+
}
|
|
450
491
|
var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
451
492
|
var CLAUDE_SETTINGS_OWNERSHIP_PATH = join(
|
|
452
493
|
CONFIG_DIR,
|
|
@@ -952,8 +993,8 @@ import { createRequire } from "module";
|
|
|
952
993
|
import process2 from "process";
|
|
953
994
|
var _cccRequire = createRequire(import.meta.url);
|
|
954
995
|
function resolveCccPackageVersion() {
|
|
955
|
-
if ("0.
|
|
956
|
-
return "0.
|
|
996
|
+
if ("0.6.0".length > 0) {
|
|
997
|
+
return "0.6.0";
|
|
957
998
|
}
|
|
958
999
|
try {
|
|
959
1000
|
const pkg = _cccRequire("../package.json");
|
|
@@ -981,6 +1022,9 @@ function resolveIngestUrl(override = process2.env.MARGINFRONT_INGEST_URL) {
|
|
|
981
1022
|
return DEFAULT_INGEST_URL;
|
|
982
1023
|
}
|
|
983
1024
|
var MARGINFRONT_INGEST_URL = resolveIngestUrl();
|
|
1025
|
+
function resolveDeveloperOverride(env) {
|
|
1026
|
+
return (env.CCC_DEVELOPER_EMAIL || "").trim() || void 0;
|
|
1027
|
+
}
|
|
984
1028
|
var AGENT_CODE = "claude-code";
|
|
985
1029
|
var SIGNAL_NAME = "claude-code-turn";
|
|
986
1030
|
var MODEL_PROVIDER = "anthropic";
|
|
@@ -1097,6 +1141,7 @@ function idempotencyKeyFor(rawSourceLine, email, rawModel, sessionId, indexWithi
|
|
|
1097
1141
|
function buildMarginFrontRequestBody(parsedOtlp, options = {}) {
|
|
1098
1142
|
const foldCache = options.foldCache === true;
|
|
1099
1143
|
const fallbackCustomerId = typeof options.fallbackCustomerId === "string" && options.fallbackCustomerId.length > 0 ? options.fallbackCustomerId : null;
|
|
1144
|
+
const developerOverride = options.developerOverride;
|
|
1100
1145
|
const tokenDataPoints = collectDataPointsForMetric(
|
|
1101
1146
|
parsedOtlp,
|
|
1102
1147
|
"claude_code.token.usage"
|
|
@@ -1113,7 +1158,7 @@ ${sessionId}`;
|
|
|
1113
1158
|
}
|
|
1114
1159
|
for (const dataPoint of tokenDataPoints) {
|
|
1115
1160
|
const attributes = attributesToLookup(dataPoint.attributes);
|
|
1116
|
-
const email = attributes["user.email"] || fallbackCustomerId;
|
|
1161
|
+
const email = developerOverride || attributes["user.email"] || fallbackCustomerId;
|
|
1117
1162
|
const rawModel = attributes["model"];
|
|
1118
1163
|
const sessionId = attributes["session.id"];
|
|
1119
1164
|
const tokenType = attributes["type"];
|
|
@@ -1146,7 +1191,7 @@ ${sessionId}`;
|
|
|
1146
1191
|
function findCostForGroup(email, rawModel, sessionId) {
|
|
1147
1192
|
for (const costPoint of costDataPoints) {
|
|
1148
1193
|
const costAttributes = attributesToLookup(costPoint.attributes);
|
|
1149
|
-
const costEmail = costAttributes["user.email"] || fallbackCustomerId;
|
|
1194
|
+
const costEmail = developerOverride || costAttributes["user.email"] || fallbackCustomerId;
|
|
1150
1195
|
if (costEmail === email && costAttributes["model"] === rawModel && costAttributes["session.id"] === sessionId) {
|
|
1151
1196
|
return typeof costPoint.asDouble === "number" ? costPoint.asDouble : null;
|
|
1152
1197
|
}
|
|
@@ -1167,7 +1212,7 @@ ${sessionId}`;
|
|
|
1167
1212
|
group.sessionId
|
|
1168
1213
|
);
|
|
1169
1214
|
const record = {
|
|
1170
|
-
// Per-
|
|
1215
|
+
// Per-developer attribution: the developer's email IS the customer id (or the
|
|
1171
1216
|
// no-identity placeholder).
|
|
1172
1217
|
customerExternalId: group.email,
|
|
1173
1218
|
agentCode: AGENT_CODE,
|
|
@@ -1245,6 +1290,7 @@ function isCodexUsageRecord(lookup, eventName) {
|
|
|
1245
1290
|
function buildCodexRequestBody(parsedOtlp, options = {}) {
|
|
1246
1291
|
const foldCache = options.foldCache === true;
|
|
1247
1292
|
const fallbackCustomerId = typeof options.fallbackCustomerId === "string" && options.fallbackCustomerId.length > 0 ? options.fallbackCustomerId : CODEX_NO_IDENTITY_CUSTOMER;
|
|
1293
|
+
const developerOverride = options.developerOverride;
|
|
1248
1294
|
const records = [];
|
|
1249
1295
|
const resourceLogsList = parsedOtlp?.resourceLogs;
|
|
1250
1296
|
if (!Array.isArray(resourceLogsList)) return { records };
|
|
@@ -1262,7 +1308,7 @@ function buildCodexRequestBody(parsedOtlp, options = {}) {
|
|
|
1262
1308
|
const rawModel = logAttrString(lookup["model"]);
|
|
1263
1309
|
if (!rawModel) continue;
|
|
1264
1310
|
if (CODEX_EXCLUDED_MODELS.has(rawModel.trim().toLowerCase())) continue;
|
|
1265
|
-
const email = logAttrString(lookup["user.email"]) || fallbackCustomerId;
|
|
1311
|
+
const email = developerOverride || logAttrString(lookup["user.email"]) || fallbackCustomerId;
|
|
1266
1312
|
if (!email) continue;
|
|
1267
1313
|
const sessionId = logAttrString(lookup["session.id"]) ?? logAttrString(lookup["conversation.id"]) ?? logAttrString(lookup["session_id"]) ?? null;
|
|
1268
1314
|
const inputCount = logAttrTokenCount(lookup[CODEX_INPUT_TOKEN_KEY]);
|
|
@@ -1327,6 +1373,7 @@ function toolResultSource(eventName) {
|
|
|
1327
1373
|
}
|
|
1328
1374
|
function buildToolUsageRecords(parsedOtlp, options = {}) {
|
|
1329
1375
|
const fallbackCustomerId = typeof options.fallbackCustomerId === "string" && options.fallbackCustomerId.length > 0 ? options.fallbackCustomerId : CODEX_NO_IDENTITY_CUSTOMER;
|
|
1376
|
+
const developerOverride = options.developerOverride;
|
|
1330
1377
|
const records = [];
|
|
1331
1378
|
const resourceLogsList = parsedOtlp?.resourceLogs;
|
|
1332
1379
|
if (!Array.isArray(resourceLogsList)) return { records };
|
|
@@ -1352,7 +1399,7 @@ function buildToolUsageRecords(parsedOtlp, options = {}) {
|
|
|
1352
1399
|
}
|
|
1353
1400
|
if (!toolName) continue;
|
|
1354
1401
|
if (BUILTIN_NONBILLABLE_TOOLS.has(toolName)) continue;
|
|
1355
|
-
const email = logAttrString(lookup["user.email"]) || fallbackCustomerId;
|
|
1402
|
+
const email = developerOverride || logAttrString(lookup["user.email"]) || fallbackCustomerId;
|
|
1356
1403
|
if (!email) continue;
|
|
1357
1404
|
const sessionId = logAttrString(lookup["session.id"]) ?? logAttrString(lookup["conversation.id"]) ?? logAttrString(lookup["session_id"]) ?? null;
|
|
1358
1405
|
const groupKey = `${email}
|
|
@@ -1618,6 +1665,7 @@ function watchAndForward(filePath, options = {}) {
|
|
|
1618
1665
|
const fallbackCustomerId = options.fallbackCustomerId;
|
|
1619
1666
|
const cursorPath = options.cursorPath;
|
|
1620
1667
|
const pidFilePath = options.pidFilePath;
|
|
1668
|
+
const developerOverride = resolveDeveloperOverride(process2.env);
|
|
1621
1669
|
if (!process2.env.MARGINFRONT_API_KEY) {
|
|
1622
1670
|
console.error(
|
|
1623
1671
|
"MARGINFRONT_API_KEY is not set. Set it in your shell before watching: export MARGINFRONT_API_KEY=..."
|
|
@@ -1751,6 +1799,7 @@ function watchAndForward(filePath, options = {}) {
|
|
|
1751
1799
|
const body = buildRequestBodyForLine(parsed, {
|
|
1752
1800
|
foldCache,
|
|
1753
1801
|
fallbackCustomerId,
|
|
1802
|
+
developerOverride,
|
|
1754
1803
|
rawSourceLine: line
|
|
1755
1804
|
});
|
|
1756
1805
|
if (!body.records.length) continue;
|
|
@@ -1820,8 +1869,10 @@ function printHelp() {
|
|
|
1820
1869
|
"",
|
|
1821
1870
|
"Commands:",
|
|
1822
1871
|
" init Set up everything: save your MarginFront secret key (mf_sk_...), not",
|
|
1823
|
-
" your Anthropic or OpenAI key.
|
|
1824
|
-
"
|
|
1872
|
+
" your Anthropic or OpenAI key. Ask which developer THIS machine's AI",
|
|
1873
|
+
" cost belongs to (for teams on a shared Claude/Codex login). Wire",
|
|
1874
|
+
" telemetry into your Claude/Codex config (with your OK), and start",
|
|
1875
|
+
" the background meter.",
|
|
1825
1876
|
" start (Re)start the background meter - starts at login, no terminal to keep open.",
|
|
1826
1877
|
" status Is the background meter running? Shows recent activity + errors.",
|
|
1827
1878
|
" preview <file.json> Show the exact record it would send for a capture. No key needed.",
|
|
@@ -1846,19 +1897,29 @@ function printHelp() {
|
|
|
1846
1897
|
" automatically. No `source`, no second terminal. Check on it with `status` anytime.",
|
|
1847
1898
|
"",
|
|
1848
1899
|
"Prefer a live terminal view instead of the background meter? After init, run:",
|
|
1849
|
-
" npx @marginfront/code-cost-clarity run"
|
|
1900
|
+
" npx @marginfront/code-cost-clarity run",
|
|
1901
|
+
"",
|
|
1902
|
+
"Sharing one Claude/Codex login across a team? (per-developer cost attribution)",
|
|
1903
|
+
" During `init` we ask which developer THIS machine's AI cost belongs to and save it",
|
|
1904
|
+
" as CCC_DEVELOPER_EMAIL in ~/.marginfront-ccc/.env. At send time the order is:",
|
|
1905
|
+
" 1. CCC_DEVELOPER_EMAIL (this machine's developer) - if set, it always wins;",
|
|
1906
|
+
" 2. otherwise the email the coding-agent login reports;",
|
|
1907
|
+
" 3. otherwise a no-identity placeholder.",
|
|
1908
|
+
" Leave it BLANK to keep the old auto-detect behavior. Edit the .env to change it,",
|
|
1909
|
+
" then `stop` and `start` (or just re-run `init`). Internal cost visibility only -",
|
|
1910
|
+
" it does NOT change anyone's bill."
|
|
1850
1911
|
].join("\n")
|
|
1851
1912
|
);
|
|
1852
1913
|
}
|
|
1853
1914
|
function printNoIdentityHint() {
|
|
1854
1915
|
console.log(
|
|
1855
1916
|
[
|
|
1856
|
-
"Note: usage with no
|
|
1917
|
+
"Note: usage with no developer email is attributed to a no-identity placeholder",
|
|
1857
1918
|
` ("${NO_IDENTITY_CUSTOMER}" for Claude Code, "${CODEX_NO_IDENTITY_CUSTOMER}" for Codex).`,
|
|
1858
1919
|
" Why: org-managed seats stamp the email automatically; some interactive logins don't.",
|
|
1859
1920
|
" Fix: sign in with an org-managed seat, or attach your own MarginFront customer",
|
|
1860
1921
|
" mapping. Until then the spend still lands, under the placeholder instead of the",
|
|
1861
|
-
"
|
|
1922
|
+
" developer's name."
|
|
1862
1923
|
].join("\n")
|
|
1863
1924
|
);
|
|
1864
1925
|
}
|
|
@@ -1912,6 +1973,30 @@ function promptYesNo(promptText, io = {}, defaultYes = false) {
|
|
|
1912
1973
|
rl.on("close", () => finish("", false));
|
|
1913
1974
|
});
|
|
1914
1975
|
}
|
|
1976
|
+
function promptVisible(promptText, defaultValue = "", io = {}) {
|
|
1977
|
+
return new Promise((resolve) => {
|
|
1978
|
+
const input = io.input ?? process3.stdin;
|
|
1979
|
+
const output = io.output ?? process3.stdout;
|
|
1980
|
+
const interactive = io.terminal ?? Boolean(input.isTTY);
|
|
1981
|
+
const rl = readline.createInterface({
|
|
1982
|
+
input,
|
|
1983
|
+
output,
|
|
1984
|
+
terminal: interactive
|
|
1985
|
+
});
|
|
1986
|
+
let settled = false;
|
|
1987
|
+
const finish = (answer) => {
|
|
1988
|
+
if (settled) return;
|
|
1989
|
+
settled = true;
|
|
1990
|
+
rl.close();
|
|
1991
|
+
resolve(answer.trim());
|
|
1992
|
+
};
|
|
1993
|
+
output.write(promptText);
|
|
1994
|
+
rl.question("", (answer) => finish(answer));
|
|
1995
|
+
if (interactive && defaultValue) rl.write(defaultValue);
|
|
1996
|
+
rl.on("SIGINT", () => finish(""));
|
|
1997
|
+
rl.on("close", () => finish(""));
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
1915
2000
|
function printConsentDisclosure(log) {
|
|
1916
2001
|
log(
|
|
1917
2002
|
[
|
|
@@ -1987,6 +2072,41 @@ async function cmdInit(noPrompt, deps = {}) {
|
|
|
1987
2072
|
keySaved = true;
|
|
1988
2073
|
}
|
|
1989
2074
|
}
|
|
2075
|
+
const shouldPromptDeveloper = !noPrompt && isTTY();
|
|
2076
|
+
if (shouldPromptDeveloper) {
|
|
2077
|
+
const gitDefault = (deps.gitDefaultEmail ?? (() => gitGlobalUserEmail()))();
|
|
2078
|
+
const savedDeveloperEmail = loadEnv()["CCC_DEVELOPER_EMAIL"];
|
|
2079
|
+
const defaultEmail = savedDeveloperEmail || gitDefault;
|
|
2080
|
+
log(
|
|
2081
|
+
[
|
|
2082
|
+
"",
|
|
2083
|
+
"Who should this machine's AI cost be attributed to?",
|
|
2084
|
+
" If your team shares ONE Claude/Codex login, every developer's usage looks",
|
|
2085
|
+
" like the same person. Naming the developer here splits this laptop's spend",
|
|
2086
|
+
" back out to them. (Internal cost visibility only - it does NOT change anyone's bill.)",
|
|
2087
|
+
"",
|
|
2088
|
+
" - Press Enter to accept the pre-filled email (from your global git config).",
|
|
2089
|
+
" - Type a different email to attribute this machine to someone else.",
|
|
2090
|
+
" - Clear it and leave it BLANK to auto-detect from your coding-agent login",
|
|
2091
|
+
" (today's behavior - usage is tagged with whatever email the login reports).",
|
|
2092
|
+
""
|
|
2093
|
+
].join("\n")
|
|
2094
|
+
);
|
|
2095
|
+
const askDeveloper = deps.promptDeveloperEmail ?? ((defaultValue) => promptVisible(
|
|
2096
|
+
"Developer email for this machine's AI cost: ",
|
|
2097
|
+
defaultValue
|
|
2098
|
+
));
|
|
2099
|
+
const chosenEmail = (await askDeveloper(defaultEmail)).trim();
|
|
2100
|
+
const writeDeveloper = deps.writeDeveloperEmail ?? ((value) => writeDeveloperEmailToEnv(value));
|
|
2101
|
+
writeDeveloper(chosenEmail);
|
|
2102
|
+
if (chosenEmail) {
|
|
2103
|
+
log(`Will attribute this machine's AI cost to ${chosenEmail}.`);
|
|
2104
|
+
} else {
|
|
2105
|
+
log(
|
|
2106
|
+
"Left attribution on auto-detect (this machine's AI cost follows the coding-agent login)."
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
1990
2110
|
printConsentDisclosure(log);
|
|
1991
2111
|
let consented;
|
|
1992
2112
|
if (noPrompt) {
|
|
@@ -2490,6 +2610,7 @@ export {
|
|
|
2490
2610
|
cmdRun,
|
|
2491
2611
|
cmdStart,
|
|
2492
2612
|
cmdUninstall,
|
|
2613
|
+
promptVisible,
|
|
2493
2614
|
promptYesNo,
|
|
2494
2615
|
runningForwarderPid,
|
|
2495
2616
|
stopForwarderThenStopCollector
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marginfront/code-cost-clarity",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "See your Claude Code and Codex spend in MarginFront. One command wires your coding-agent usage telemetry through a local collector into MarginFront, priced per
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "See your Claude Code and Codex spend in MarginFront. One command wires your coding-agent usage telemetry through a local collector into MarginFront, priced per developer, per model, with accurate prompt-cache token splitting and automatic billable tool-call tracking.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"code-cost-clarity": "dist/cli.js"
|