@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 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 engineer, per model, with **accurate prompt-cache token splitting**.
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 engineer, per model,
21
- R&D-vs-COGS). It does not charge engineers and it does not cut anyone off.
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 an engineer runs Claude Code, this connector captures the token usage
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 engineer.
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
- engineer's email.
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 engineer email, automatically, and the background meter restarts itself at
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 engineer@example.com · in=3210 out=287 · server=$0.0232 · cc=$0.0236 event=9f0c2a71-...
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-engineer attribution is automatic
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 engineer's logged-in Claude account email.
187
- There is no manual email config. Each engineer loads the telemetry settings
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 engineer signs in with an org-managed seat or an interactive
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
- engineer's sign-in doesn't surface an email, the connector doesn't drop the
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 engineer's identity: it's 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 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.
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 engineer's name on all of it).
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-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
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 engineer email
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 engineers (technical appendix)
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
- " engineer email shows up depends on how you sign in (org API key vs ChatGPT",
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.5.4".length > 0) {
956
- return "0.5.4";
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-engineer attribution: the engineer's email IS the customer id (or the
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. Wire telemetry into your Claude/Codex",
1824
- " config (with your OK), and start the background meter.",
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 engineer email is attributed to a no-identity placeholder",
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
- " engineer's name."
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.5.4",
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 engineer, per model, with accurate prompt-cache token splitting and automatic billable tool-call tracking.",
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"