nomnomtokens 0.3.0 → 0.5.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.
Files changed (120) hide show
  1. package/README.md +48 -24
  2. package/dist/index.js +1198 -107
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/web/nitro.json +1 -1
  6. package/web/public/_nuxt/AuWqCQTg.js +1 -0
  7. package/web/public/_nuxt/{W3KL_OjU.js → B1SHP0x7.js} +1 -1
  8. package/web/public/_nuxt/{DQEnwY-5.js → B4lg_KdO.js} +1 -1
  9. package/web/public/_nuxt/B8pNcdCn.js +1 -0
  10. package/web/public/_nuxt/{COwSpxrx.js → BL80uXr2.js} +1 -1
  11. package/web/public/_nuxt/{B2aHje2L.js → BOu13FsZ.js} +1 -1
  12. package/web/public/_nuxt/{CvIugsJe2.js → BYf03To42.js} +1 -1
  13. package/web/public/_nuxt/{CkRZsevx.js → B_fydnl_.js} +1 -1
  14. package/web/public/_nuxt/{DnTwjl0_2.js → BboXpc0v2.js} +2 -2
  15. package/web/public/_nuxt/{CfBMjq8x.js → Bg-w_mbM.js} +1 -1
  16. package/web/public/_nuxt/{B1jhn8zK.js → BsIJiDwC.js} +1 -1
  17. package/web/public/_nuxt/BsfWsX6E.js +1 -0
  18. package/web/public/_nuxt/{C9MiT_Wm.js → BuKW47N9.js} +2 -2
  19. package/web/public/_nuxt/{4C_a23zt.js → BvB3OU0z.js} +1 -1
  20. package/web/public/_nuxt/CLdT3CQk.js +1 -0
  21. package/web/public/_nuxt/{D9j6aFUy.js → Car0xtsn.js} +1 -1
  22. package/web/public/_nuxt/Ch1ysvEi.js +1 -0
  23. package/web/public/_nuxt/{BOKlFKrX.js → Ck5ddq-9.js} +1 -1
  24. package/web/public/_nuxt/{Cq1Sq3Go.js → CsVo4EXo.js} +1 -1
  25. package/web/public/_nuxt/CzaLG4_B2.js +1 -0
  26. package/web/public/_nuxt/{C51OzH6V.js → D9MZNmok.js} +1 -1
  27. package/web/public/_nuxt/{D9Mxq5ER.js → DQ6vgNw-.js} +1 -1
  28. package/web/public/_nuxt/{D49wVVwm2.js → DZ0oUjlX2.js} +1 -1
  29. package/web/public/_nuxt/DbTv-m36.js +1 -0
  30. package/web/public/_nuxt/{D4C6J8YV.js → DdHo_MGy.js} +1 -1
  31. package/web/public/_nuxt/{m3-Vaz25.js → DmteO8Ul.js} +1 -1
  32. package/web/public/_nuxt/DyoXCutE.js +1 -0
  33. package/web/public/_nuxt/{DPnm6zS2.js → DyvBBld_2.js} +1 -1
  34. package/web/public/_nuxt/{CIKwfDBl.js → PaV2v2OF.js} +1 -1
  35. package/web/public/_nuxt/{BMSLzLlT.js → RjmsMzM7.js} +1 -1
  36. package/web/public/_nuxt/builds/latest.json +1 -1
  37. package/web/public/_nuxt/builds/meta/2bf417d4-fc60-4108-8624-e631e05f5a46.json +1 -0
  38. package/web/public/_nuxt/entry.CE8-9Sal.css +2 -0
  39. package/web/public/_nuxt/{Dy30RUCD2.js → hPGLTkZo2.js} +1 -1
  40. package/web/public/_nuxt/{Bprokln-.js → jeqgGzjf.js} +1 -1
  41. package/web/public/_nuxt/n29w62AT.js +1 -0
  42. package/web/public/_nuxt/{CDzUujek.js → tShBBc0S.js} +2 -2
  43. package/web/server/chunks/_/filters.mjs +1 -0
  44. package/web/server/chunks/_/filters.mjs.map +1 -1
  45. package/web/server/chunks/build/AuditCard-CgGiEb_r.mjs +290 -0
  46. package/web/server/chunks/build/AuditCard-CgGiEb_r.mjs.map +1 -0
  47. package/web/server/chunks/build/{CardContent-C_IMqI8K.mjs → CardDescription-Bi5t5iXj.mjs} +30 -30
  48. package/web/server/chunks/build/CardDescription-Bi5t5iXj.mjs.map +1 -0
  49. package/web/server/chunks/build/{FilterBar-BlEsJ5dy.mjs → FilterBar-BYBOdTrl.mjs} +2 -2
  50. package/web/server/chunks/build/{FilterBar-BlEsJ5dy.mjs.map → FilterBar-BYBOdTrl.mjs.map} +1 -1
  51. package/web/server/chunks/build/{Sparkline-CaCRq45J.mjs → Sparkline-B3kazZhU.mjs} +2 -2
  52. package/web/server/chunks/build/{Sparkline-CaCRq45J.mjs.map → Sparkline-B3kazZhU.mjs.map} +1 -1
  53. package/web/server/chunks/build/{_id_-BvccBwzH.mjs → _id_-DPUt6OzN.mjs} +4 -4
  54. package/web/server/chunks/build/{_id_-BvccBwzH.mjs.map → _id_-DPUt6OzN.mjs.map} +1 -1
  55. package/web/server/chunks/build/asyncData-CMa0z9Yw.mjs +2 -2
  56. package/web/server/chunks/build/audit-BPAlwhzn.mjs +1180 -0
  57. package/web/server/chunks/build/audit-BPAlwhzn.mjs.map +1 -0
  58. package/web/server/chunks/build/{default-BokiYQNv.mjs → default-CvQF0-O_.mjs} +46 -22
  59. package/web/server/chunks/build/default-CvQF0-O_.mjs.map +1 -0
  60. package/web/server/chunks/build/export-CwC19yOz.mjs +232 -0
  61. package/web/server/chunks/build/export-CwC19yOz.mjs.map +1 -0
  62. package/web/server/chunks/build/fetch-DJ9NNbFP.mjs +1 -1
  63. package/web/server/chunks/build/{import-CO3NEu0P.mjs → import-BmoVtcGR.mjs} +31 -15
  64. package/web/server/chunks/build/import-BmoVtcGR.mjs.map +1 -0
  65. package/web/server/chunks/build/{limits-CX9zBSqw.mjs → limits-C21_a18j.mjs} +5 -5
  66. package/web/server/chunks/build/{limits-CX9zBSqw.mjs.map → limits-C21_a18j.mjs.map} +1 -1
  67. package/web/server/chunks/build/nuxt-link-mYiLFsJs.mjs +2 -2
  68. package/web/server/chunks/build/{pages-DyXqJyPq.mjs → pages-CJpczmoA.mjs} +81 -11
  69. package/web/server/chunks/build/pages-CJpczmoA.mjs.map +1 -0
  70. package/web/server/chunks/build/{providers-Ca7keu2G.mjs → providers-BBVw9CDb.mjs} +4 -4
  71. package/web/server/chunks/build/{providers-Ca7keu2G.mjs.map → providers-BBVw9CDb.mjs.map} +1 -1
  72. package/web/server/chunks/build/{scopes-E5z80S19.mjs → scopes-DpYnOAM9.mjs} +203 -62
  73. package/web/server/chunks/build/scopes-DpYnOAM9.mjs.map +1 -0
  74. package/web/server/chunks/build/{sessions-CESRiCOS.mjs → sessions-B-Y_ifp1.mjs} +5 -5
  75. package/web/server/chunks/build/{sessions-CESRiCOS.mjs.map → sessions-B-Y_ifp1.mjs.map} +1 -1
  76. package/web/server/chunks/build/{timeline-d27OlbZN.mjs → timeline-8kK28WQB.mjs} +5 -5
  77. package/web/server/chunks/build/{timeline-d27OlbZN.mjs.map → timeline-8kK28WQB.mjs.map} +1 -1
  78. package/web/server/chunks/build/useFormat-NkyUqeik.mjs +1 -1
  79. package/web/server/chunks/build/{useStats-CmYzpdbE.mjs → useStats-BQQJIKQO.mjs} +7 -5
  80. package/web/server/chunks/build/{useStats-CmYzpdbE.mjs.map → useStats-BQQJIKQO.mjs.map} +1 -1
  81. package/web/server/chunks/nitro/nitro.mjs +692 -214
  82. package/web/server/chunks/nitro/nitro.mjs.map +1 -1
  83. package/web/server/chunks/routes/api/export.get.mjs +22 -3
  84. package/web/server/chunks/routes/api/export.get.mjs.map +1 -1
  85. package/web/server/chunks/routes/api/import/csv.post.mjs +20 -6
  86. package/web/server/chunks/routes/api/import/csv.post.mjs.map +1 -1
  87. package/web/server/chunks/routes/api/ingest.post.mjs +1 -1
  88. package/web/server/chunks/routes/api/meta.get.mjs +1 -1
  89. package/web/server/chunks/routes/api/scopes/_hash_.patch.mjs +35 -0
  90. package/web/server/chunks/routes/api/scopes/_hash_.patch.mjs.map +1 -0
  91. package/web/server/chunks/routes/api/stats/audit.get.mjs +60 -0
  92. package/web/server/chunks/routes/api/stats/audit.get.mjs.map +1 -0
  93. package/web/server/chunks/routes/api/stats/limits.get.mjs +1 -1
  94. package/web/server/chunks/routes/api/stats/providers.get.mjs +1 -1
  95. package/web/server/chunks/routes/api/stats/scopes.get.mjs +1 -1
  96. package/web/server/chunks/routes/api/stats/session/_id_.get.mjs +1 -1
  97. package/web/server/chunks/routes/api/stats/summary.get.mjs +1 -1
  98. package/web/server/chunks/routes/api/stats/verdict.get.mjs +35 -0
  99. package/web/server/chunks/routes/api/stats/verdict.get.mjs.map +1 -0
  100. package/web/server/chunks/routes/renderer.mjs +2 -2
  101. package/web/server/chunks/virtual/entry.mjs +17 -12
  102. package/web/server/chunks/virtual/precomputed.mjs +1 -1
  103. package/web/server/chunks/virtual/precomputed.mjs.map +1 -1
  104. package/web/server/index.mjs +1 -1
  105. package/web/public/_nuxt/Bia8fJFj.js +0 -1
  106. package/web/public/_nuxt/CIHIOSzn.js +0 -1
  107. package/web/public/_nuxt/CZSbzIi-.js +0 -1
  108. package/web/public/_nuxt/DMgad8mU.js +0 -1
  109. package/web/public/_nuxt/DhJW7zb9.js +0 -1
  110. package/web/public/_nuxt/OU1MVgXQ.js +0 -1
  111. package/web/public/_nuxt/builds/meta/e90c8a2d-9e50-4b58-8019-6b1bf4c8fc6d.json +0 -1
  112. package/web/public/_nuxt/cbxnBwm6.js +0 -1
  113. package/web/public/_nuxt/entry.MbqNBIeH.css +0 -2
  114. package/web/server/chunks/build/CardContent-C_IMqI8K.mjs.map +0 -1
  115. package/web/server/chunks/build/default-BokiYQNv.mjs.map +0 -1
  116. package/web/server/chunks/build/export-BhfwmSMl.mjs +0 -184
  117. package/web/server/chunks/build/export-BhfwmSMl.mjs.map +0 -1
  118. package/web/server/chunks/build/import-CO3NEu0P.mjs.map +0 -1
  119. package/web/server/chunks/build/pages-DyXqJyPq.mjs.map +0 -1
  120. package/web/server/chunks/build/scopes-E5z80S19.mjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync, mkdirSync, copyFileSync, writeFileSync, statSync, createReadStream, readdirSync } from 'fs';
2
+ import { readFileSync, existsSync, mkdirSync, copyFileSync, writeFileSync, statSync, readdirSync, createReadStream } from 'fs';
3
3
  import { dirname, join, resolve } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { Command } from 'commander';
@@ -4995,7 +4995,11 @@ var scopes = sqliteTable("scopes", {
4995
4995
  /** human-readable alias — local only, never synced */
4996
4996
  label: text("label").notNull(),
4997
4997
  provider: text("provider").notNull(),
4998
- lastSeen: integer("last_seen").notNull()
4998
+ lastSeen: integer("last_seen").notNull(),
4999
+ /** freelancer/client tag — local only, never attached to events */
5000
+ client: text("client"),
5001
+ /** 1 when the user renamed the label; ingest must not clobber it */
5002
+ labelLocked: integer("label_locked").notNull().default(0)
4999
5003
  });
5000
5004
  var limits = sqliteTable("limits", {
5001
5005
  ts: integer("ts").notNull(),
@@ -5015,7 +5019,7 @@ var scanState = sqliteTable("scan_state", {
5015
5019
  });
5016
5020
 
5017
5021
  // packages/db/src/client.ts
5018
- var SCHEMA_VERSION = 1;
5022
+ var SCHEMA_VERSION = 2;
5019
5023
  function defaultDataDir() {
5020
5024
  return process.env.NOMNOMTOKENS_HOME ?? join(homedir(), ".nomnomtokens");
5021
5025
  }
@@ -5045,7 +5049,9 @@ CREATE TABLE IF NOT EXISTS scopes (
5045
5049
  scope_hash TEXT PRIMARY KEY,
5046
5050
  label TEXT NOT NULL,
5047
5051
  provider TEXT NOT NULL,
5048
- last_seen INTEGER NOT NULL
5052
+ last_seen INTEGER NOT NULL,
5053
+ client TEXT,
5054
+ label_locked INTEGER NOT NULL DEFAULT 0
5049
5055
  );
5050
5056
 
5051
5057
  CREATE TABLE IF NOT EXISTS limits (
@@ -5065,6 +5071,16 @@ CREATE TABLE IF NOT EXISTS scan_state (
5065
5071
  offset INTEGER NOT NULL
5066
5072
  );
5067
5073
  `;
5074
+ function migrate(sqlite, from) {
5075
+ if (from < 2) {
5076
+ const cols = sqlite.pragma("table_info(scopes)");
5077
+ const names = new Set(cols.map((c2) => c2.name));
5078
+ if (!names.has("client")) sqlite.exec("ALTER TABLE scopes ADD COLUMN client TEXT");
5079
+ if (!names.has("label_locked")) {
5080
+ sqlite.exec("ALTER TABLE scopes ADD COLUMN label_locked INTEGER NOT NULL DEFAULT 0");
5081
+ }
5082
+ }
5083
+ }
5068
5084
  function openDb(path = defaultDbPath()) {
5069
5085
  if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
5070
5086
  const sqlite = new Client(path);
@@ -5078,6 +5094,9 @@ function openDb(path = defaultDbPath()) {
5078
5094
  throw new Error(
5079
5095
  `Database at ${path} was written by a newer nomnomtokens (schema v${found}, this build understands v${SCHEMA_VERSION}). Upgrade the CLI.`
5080
5096
  );
5097
+ } else if (found < SCHEMA_VERSION) {
5098
+ migrate(sqlite, found);
5099
+ sqlite.pragma(`user_version = ${SCHEMA_VERSION}`);
5081
5100
  }
5082
5101
  const db = drizzle(sqlite, { schema: sqlite_exports });
5083
5102
  return { db, sqlite, path };
@@ -5229,6 +5248,12 @@ function bucketStart(ts, g) {
5229
5248
  return d.getTime();
5230
5249
  }
5231
5250
  }
5251
+ function cacheHitRate(qty) {
5252
+ const read = qty.cacheRead ?? 0;
5253
+ const fresh = qty.in ?? 0;
5254
+ const denom = read + fresh;
5255
+ return denom === 0 ? null : read / denom;
5256
+ }
5232
5257
 
5233
5258
  // packages/core/src/forecast.ts
5234
5259
  function linearRegression(points) {
@@ -5293,6 +5318,7 @@ var CSV_HEADERS = [
5293
5318
  "kind",
5294
5319
  "session_id",
5295
5320
  "project",
5321
+ "client",
5296
5322
  "scope_hash",
5297
5323
  "model",
5298
5324
  "cost_usd",
@@ -5318,6 +5344,7 @@ function formatExportCsv(rows) {
5318
5344
  csvCell(r.kind),
5319
5345
  csvCell(r.sessionId),
5320
5346
  csvCell(r.project),
5347
+ csvCell(r.client),
5321
5348
  csvCell(r.scopeHash),
5322
5349
  csvCell(r.model),
5323
5350
  csvCell(r.costUsd),
@@ -5340,6 +5367,7 @@ function formatExportJson(rows) {
5340
5367
  kind: r.kind,
5341
5368
  sessionId: r.sessionId,
5342
5369
  project: r.project,
5370
+ client: r.client,
5343
5371
  scopeHash: r.scopeHash,
5344
5372
  model: r.model,
5345
5373
  costUsd: r.costUsd,
@@ -5352,6 +5380,95 @@ function formatExportJson(rows) {
5352
5380
  })), null, 2)}
5353
5381
  `;
5354
5382
  }
5383
+ var NNT_ARCHIVE_VERSION = 1;
5384
+ function formatNntArchive(archive) {
5385
+ return `${JSON.stringify({ ...archive, version: NNT_ARCHIVE_VERSION }, null, 2)}
5386
+ `;
5387
+ }
5388
+ function numbersOnly(meta) {
5389
+ if (!meta || typeof meta !== "object") return {};
5390
+ const out = {};
5391
+ for (const [k, v] of Object.entries(meta)) {
5392
+ if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
5393
+ }
5394
+ return out;
5395
+ }
5396
+ function qtyOnly(qty) {
5397
+ if (!qty || typeof qty !== "object") return {};
5398
+ const out = {};
5399
+ for (const [k, v] of Object.entries(qty)) {
5400
+ if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
5401
+ }
5402
+ return out;
5403
+ }
5404
+ function looksLikeNntArchive(text2) {
5405
+ const trimmed = text2.trimStart();
5406
+ if (!trimmed.startsWith("{")) return false;
5407
+ try {
5408
+ const parsed = JSON.parse(text2);
5409
+ return parsed.version === NNT_ARCHIVE_VERSION && Array.isArray(parsed.events);
5410
+ } catch {
5411
+ return false;
5412
+ }
5413
+ }
5414
+ function parseNntArchive(text2) {
5415
+ let parsed;
5416
+ try {
5417
+ parsed = JSON.parse(text2);
5418
+ } catch {
5419
+ throw new Error("nnt archive is not valid JSON");
5420
+ }
5421
+ const data = parsed;
5422
+ if (data.version !== NNT_ARCHIVE_VERSION || !Array.isArray(data.events)) {
5423
+ throw new Error("not an nnt archive (expected { version: 1, events })");
5424
+ }
5425
+ const records = [];
5426
+ for (const raw of data.events) {
5427
+ if (!raw || typeof raw.id !== "string") continue;
5428
+ const qty = qtyOnly(raw.qty);
5429
+ const event = {
5430
+ id: raw.id,
5431
+ ts: typeof raw.ts === "number" ? raw.ts : 0,
5432
+ provider: typeof raw.provider === "string" ? raw.provider : "nnt",
5433
+ kind: typeof raw.kind === "string" ? raw.kind : "tokens",
5434
+ sessionId: typeof raw.sessionId === "string" ? raw.sessionId : null,
5435
+ scopeHash: typeof raw.scopeHash === "string" ? raw.scopeHash : "unknown",
5436
+ unitLabel: typeof raw.unitLabel === "string" ? raw.unitLabel : null,
5437
+ qty,
5438
+ costUsd: typeof raw.costUsd === "number" ? raw.costUsd : null,
5439
+ meta: numbersOnly(raw.meta)
5440
+ };
5441
+ if (!Number.isFinite(event.ts) || event.ts <= 0) continue;
5442
+ void totalTokens(qty);
5443
+ records.push({ type: "event", event });
5444
+ }
5445
+ for (const raw of data.limits ?? []) {
5446
+ if (!raw || typeof raw.ts !== "number" || typeof raw.provider !== "string") continue;
5447
+ records.push({
5448
+ type: "limit",
5449
+ limit: {
5450
+ ts: raw.ts,
5451
+ provider: raw.provider,
5452
+ window: typeof raw.window === "string" ? raw.window : "7d",
5453
+ usedPct: typeof raw.usedPct === "number" ? raw.usedPct : 0,
5454
+ resetsAt: typeof raw.resetsAt === "number" ? raw.resetsAt : null
5455
+ }
5456
+ });
5457
+ }
5458
+ for (const raw of data.scopes ?? []) {
5459
+ if (!raw || typeof raw.scopeHash !== "string" || typeof raw.label !== "string") continue;
5460
+ records.push({
5461
+ type: "scope",
5462
+ scopeHash: raw.scopeHash,
5463
+ label: raw.label,
5464
+ provider: typeof raw.provider === "string" ? raw.provider : "nnt",
5465
+ lastSeen: typeof raw.lastSeen === "number" ? raw.lastSeen : void 0,
5466
+ client: typeof raw.client === "string" ? raw.client : null,
5467
+ labelLocked: Boolean(raw.labelLocked)
5468
+ });
5469
+ }
5470
+ return records;
5471
+ }
5355
5472
 
5356
5473
  // packages/core/src/prices-file.ts
5357
5474
  function parsePricesFile(text2) {
@@ -5453,6 +5570,368 @@ function evaluateAlerts(config, input) {
5453
5570
  return hits;
5454
5571
  }
5455
5572
 
5573
+ // packages/core/src/audit.ts
5574
+ function share(part, whole) {
5575
+ if (whole <= 0) return null;
5576
+ return part / whole;
5577
+ }
5578
+ function isFrontierModel(label) {
5579
+ if (!label) return false;
5580
+ const l = label.toLowerCase();
5581
+ return l.includes("fable") || l.includes("opus");
5582
+ }
5583
+ function buildAuditReport(input) {
5584
+ const modelLimit = input.modelLimit ?? 5;
5585
+ const sessionLimit = input.sessionLimit ?? 5;
5586
+ const totalCost = input.totals.costUsd;
5587
+ const tagged = input.sidechain.taggedEvents > 0;
5588
+ const models = [...input.models].sort((a, b) => b.costUsd - a.costUsd).slice(0, modelLimit).map((m) => ({
5589
+ unitLabel: m.unitLabel,
5590
+ costUsd: m.costUsd,
5591
+ share: share(m.costUsd, totalCost) ?? 0
5592
+ }));
5593
+ const topSessions = input.sessions.slice(0, sessionLimit).map((s) => ({
5594
+ sessionId: s.sessionId,
5595
+ label: s.label,
5596
+ costUsd: s.costUsd,
5597
+ sidechainShare: s.sidechainTagged > 0 ? share(s.sidechainCostUsd, s.costUsd) : null
5598
+ }));
5599
+ const hitRate = cacheHitRate({
5600
+ cacheRead: input.totals.qtyCacheRead,
5601
+ in: input.totals.qtyIn
5602
+ });
5603
+ const sidechainShare = tagged ? share(input.sidechain.costUsd, totalCost) : null;
5604
+ const report = {
5605
+ range: input.range,
5606
+ totals: {
5607
+ costUsd: input.totals.costUsd,
5608
+ tokens: input.totals.tokens,
5609
+ events: input.totals.events
5610
+ },
5611
+ cache: {
5612
+ hitRate,
5613
+ readTokens: input.totals.qtyCacheRead,
5614
+ freshInTokens: input.totals.qtyIn
5615
+ },
5616
+ sidechain: {
5617
+ tagged,
5618
+ costUsd: input.sidechain.costUsd,
5619
+ tokens: input.sidechain.tokens,
5620
+ shareOfCost: sidechainShare
5621
+ },
5622
+ models,
5623
+ topSessions,
5624
+ coldResumes: (input.coldResumes ?? []).slice(0, sessionLimit),
5625
+ tips: []
5626
+ };
5627
+ report.tips = buildTips(report);
5628
+ return report;
5629
+ }
5630
+ function formatGap(ms) {
5631
+ const hours = Math.round(ms / 36e5);
5632
+ if (hours < 1) return `${Math.max(1, Math.round(ms / 6e4))}m`;
5633
+ return `${hours}h`;
5634
+ }
5635
+ function buildTips(report) {
5636
+ const tips = [];
5637
+ const cold = report.coldResumes[0];
5638
+ if (cold) {
5639
+ const name = cold.label ?? cold.sessionId.slice(0, 12);
5640
+ const dollars = cold.costUsd < 0.01 ? `$${cold.costUsd.toFixed(4)}` : `$${cold.costUsd.toFixed(2)}`;
5641
+ tips.push(
5642
+ `Session ${name} went cold after ${formatGap(cold.gapMs)}; the next turn cost ${dollars} in cache writes \u2014 /clear is cheaper than resume.`
5643
+ );
5644
+ }
5645
+ if (report.sidechain.tagged && report.sidechain.shareOfCost !== null && report.sidechain.shareOfCost >= 0.3) {
5646
+ const pct2 = Math.round(report.sidechain.shareOfCost * 100);
5647
+ tips.push(
5648
+ `Subagents are ${pct2}% of cost in this range \u2014 set CLAUDE_CODE_SUBAGENT_MODEL to sonnet or haiku so Task-tool workers stop inheriting the frontier model.`
5649
+ );
5650
+ }
5651
+ if (report.cache.hitRate !== null && report.cache.hitRate >= 0.85 && report.cache.readTokens > report.cache.freshInTokens) {
5652
+ tips.push(
5653
+ `Cache reads are ${Math.round(report.cache.hitRate * 100)}% of input tokens \u2014 they inflate the token count more than the dollar bill; watch cost, not raw tokens, when pacing limits.`
5654
+ );
5655
+ }
5656
+ const top = report.models[0];
5657
+ if (top && isFrontierModel(top.unitLabel) && top.share >= 0.5 && report.totals.costUsd > 0) {
5658
+ const name = top.unitLabel ?? "frontier";
5659
+ tips.push(
5660
+ `${name} is ${Math.round(top.share * 100)}% of spend \u2014 reserve it for hard tasks and default the rest to a cheaper model.`
5661
+ );
5662
+ }
5663
+ return tips.slice(0, 3);
5664
+ }
5665
+
5666
+ // packages/core/src/cold-resume.ts
5667
+ var COLD_GAP_MS = 60 * 60 * 1e3;
5668
+ var MIN_CACHE_WRITE = 2e3;
5669
+ function cacheWriteTokens(qty) {
5670
+ return (qty.cacheCreate ?? 0) + (qty.cacheCreate1h ?? 0);
5671
+ }
5672
+ function median(values) {
5673
+ if (values.length === 0) return 0;
5674
+ const sorted = [...values].sort((a, b) => a - b);
5675
+ const mid = Math.floor(sorted.length / 2);
5676
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
5677
+ }
5678
+ function detectColdResumes(sessionId, label, turns) {
5679
+ if (turns.length < 2) return [];
5680
+ const sorted = [...turns].sort((a, b) => a.ts - b.ts);
5681
+ const writes = sorted.map((t) => cacheWriteTokens(t.qty));
5682
+ if (!writes.some((w) => w > 0)) return [];
5683
+ const medianWrite = median(writes.filter((w) => w > 0));
5684
+ const out = [];
5685
+ for (let i = 1; i < sorted.length; i++) {
5686
+ const prev = sorted[i - 1];
5687
+ const cur = sorted[i];
5688
+ const gapMs = cur.ts - prev.ts;
5689
+ if (gapMs < COLD_GAP_MS) continue;
5690
+ const write = cacheWriteTokens(cur.qty);
5691
+ if (write < MIN_CACHE_WRITE) continue;
5692
+ const inputish = write + (cur.qty.in ?? 0) + (cur.qty.cacheRead ?? 0);
5693
+ const writeShare = inputish > 0 ? write / inputish : 0;
5694
+ const aboveMedian = medianWrite > 0 ? write > medianWrite * 1.5 : true;
5695
+ if (!aboveMedian && writeShare < 0.5) continue;
5696
+ out.push({
5697
+ sessionId,
5698
+ label,
5699
+ ts: cur.ts,
5700
+ gapMs,
5701
+ costUsd: cur.costUsd ?? 0,
5702
+ cacheWriteTokens: write
5703
+ });
5704
+ }
5705
+ return out;
5706
+ }
5707
+ function collectColdResumes(sessions, limit = 5) {
5708
+ return sessions.flatMap((s) => detectColdResumes(s.sessionId, s.label, s.turns)).sort((a, b) => b.costUsd - a.costUsd).slice(0, limit);
5709
+ }
5710
+
5711
+ // packages/core/src/verdict.ts
5712
+ var PLAN_ESTIMATES = {
5713
+ asOf: "2026-09-02"};
5714
+ function latestPct(snapshots) {
5715
+ if (snapshots.length === 0) return null;
5716
+ const current = currentWindowSnapshots(snapshots);
5717
+ const last = current[current.length - 1];
5718
+ return last ? last.usedPct : null;
5719
+ }
5720
+ function elapsedWeekFraction(snapshots, now) {
5721
+ const current = currentWindowSnapshots(snapshots);
5722
+ const last = current[current.length - 1];
5723
+ if (!last) return null;
5724
+ const weekMs = 7 * 24 * 36e5;
5725
+ if (last.resetsAt && last.resetsAt > now) {
5726
+ const remaining = last.resetsAt - now;
5727
+ const elapsed = weekMs - remaining;
5728
+ if (elapsed <= 0) return 0.01;
5729
+ return Math.min(1, Math.max(0.01, elapsed / weekMs));
5730
+ }
5731
+ if (current.length < 2) return null;
5732
+ const span = last.ts - current[0].ts;
5733
+ return Math.min(1, Math.max(0.01, span / weekMs));
5734
+ }
5735
+ function buildVerdict(input) {
5736
+ const now = input.now ?? Date.now();
5737
+ const weeklyUsedPct = latestPct(input.weekly);
5738
+ const fiveHourUsedPct = latestPct(input.fiveHour);
5739
+ const fc7 = forecastLimit(input.weekly, now);
5740
+ const fc5 = forecastLimit(input.fiveHour, now);
5741
+ const caveats = [
5742
+ `Plan multipliers are estimates as of ${PLAN_ESTIMATES.asOf}, not Anthropic's bill.`,
5743
+ '"20x" multiplies the 5-hour window; weekly headroom grows much less (two Max 5x accounts often beat one 20x).',
5744
+ "Local used% is this machine only \u2014 other devices and claude.ai are invisible."
5745
+ ];
5746
+ if (weeklyUsedPct === null) {
5747
+ return {
5748
+ recommended: "unknown",
5749
+ weeklyBound: false,
5750
+ weeklyUsedPct: null,
5751
+ fiveHourUsedPct,
5752
+ weeklyCostUsd: input.weeklyCostUsd,
5753
+ two5xVs20x: {
5754
+ two5xWins: false,
5755
+ reason: "No 7-day limit snapshots yet \u2014 run `nnt init` so the status line records weekly fill."
5756
+ },
5757
+ estimatesAsOf: PLAN_ESTIMATES.asOf,
5758
+ caveats
5759
+ };
5760
+ }
5761
+ const weeklyBound = fiveHourUsedPct === null || weeklyUsedPct >= fiveHourUsedPct || Boolean(fc7?.willExhaustBeforeReset && !fc5?.willExhaustBeforeReset);
5762
+ const elapsed = elapsedWeekFraction(input.weekly, now);
5763
+ const pace = elapsed ? weeklyUsedPct / elapsed : weeklyUsedPct;
5764
+ let recommended;
5765
+ if (pace >= 180 || weeklyUsedPct >= 98) {
5766
+ recommended = "above-20x";
5767
+ } else if (weeklyBound && (weeklyUsedPct >= 85 || fc7?.willExhaustBeforeReset)) {
5768
+ recommended = weeklyUsedPct >= 92 ? "above-20x" : "max20x";
5769
+ } else if ((fiveHourUsedPct ?? 0) >= 90 || fc5?.willExhaustBeforeReset) {
5770
+ recommended = "max20x";
5771
+ } else if ((fiveHourUsedPct ?? 0) >= 50 || weeklyUsedPct >= 40) {
5772
+ recommended = "max5x";
5773
+ } else {
5774
+ recommended = "pro";
5775
+ }
5776
+ const two5xWins = weeklyBound;
5777
+ const two5xVs20x = {
5778
+ two5xWins,
5779
+ reason: two5xWins ? "The weekly cap is the wall. Two Max 5x accounts roughly double weekly pool; one 20x mostly buys a bigger 5-hour burst." : "The 5-hour window is tighter than weekly, so 20x's 5h multiplier is the lever \u2014 two 5x accounts would not help as much."
5780
+ };
5781
+ return {
5782
+ recommended,
5783
+ weeklyBound,
5784
+ weeklyUsedPct,
5785
+ fiveHourUsedPct,
5786
+ weeklyCostUsd: input.weeklyCostUsd,
5787
+ two5xVs20x,
5788
+ estimatesAsOf: PLAN_ESTIMATES.asOf,
5789
+ caveats
5790
+ };
5791
+ }
5792
+
5793
+ // packages/core/src/weigh.ts
5794
+ var WEIGH_CLEAN_SHARE = 0.8;
5795
+ var WEIGH_MIN_CLEAN = 3;
5796
+ var CAVEATS = [
5797
+ "Points per dollar use the list price stored now, not Anthropic's undocumented meter unit.",
5798
+ "A gap counts for a model only when that model is at least 80% of priced cost.",
5799
+ "Local used% is this machine only \u2014 other devices and claude.ai sit in the percent and not in the log."
5800
+ ];
5801
+ function quantile(sorted, p) {
5802
+ if (sorted.length === 1) return sorted[0];
5803
+ const idx = (sorted.length - 1) * p;
5804
+ const lo = Math.floor(idx);
5805
+ const hi = Math.ceil(idx);
5806
+ if (lo === hi) return sorted[lo];
5807
+ const a = sorted[lo];
5808
+ const b = sorted[hi];
5809
+ return a + (b - a) * (idx - lo);
5810
+ }
5811
+ function band(rates) {
5812
+ const sorted = [...rates].sort((a, b) => a - b);
5813
+ return {
5814
+ p25: quantile(sorted, 0.25),
5815
+ median: quantile(sorted, 0.5),
5816
+ p75: quantile(sorted, 0.75)
5817
+ };
5818
+ }
5819
+ function priced(models) {
5820
+ return models.filter((m) => m.costUsd > 0);
5821
+ }
5822
+ function pairRisingSnapshots(snapshots) {
5823
+ const current = currentWindowSnapshots(snapshots);
5824
+ const pairs = [];
5825
+ for (let i = 1; i < current.length; i++) {
5826
+ const prev = current[i - 1];
5827
+ const next = current[i];
5828
+ const deltaPct = next.usedPct - prev.usedPct;
5829
+ if (deltaPct <= 0) continue;
5830
+ pairs.push({ from: prev.ts, to: next.ts, deltaPct });
5831
+ }
5832
+ return pairs;
5833
+ }
5834
+ function buildWeigh(gaps) {
5835
+ let risingPct = 0;
5836
+ let risingUsd = 0;
5837
+ let risingIntervals = 0;
5838
+ let cleanIntervals = 0;
5839
+ const byLabel = /* @__PURE__ */ new Map();
5840
+ for (const gap of gaps) {
5841
+ if (gap.deltaPct <= 0) continue;
5842
+ const models2 = priced(gap.models);
5843
+ const total = models2.reduce((s, m) => s + m.costUsd, 0);
5844
+ if (total <= 0) continue;
5845
+ risingIntervals += 1;
5846
+ risingPct += gap.deltaPct;
5847
+ risingUsd += total;
5848
+ const rate = gap.deltaPct / total;
5849
+ const dominant = models2.reduce((a, b) => b.costUsd > a.costUsd ? b : a);
5850
+ if (dominant.costUsd + 1e-9 < total * WEIGH_CLEAN_SHARE) continue;
5851
+ cleanIntervals += 1;
5852
+ const key = dominant.unitLabel ?? "";
5853
+ const row = byLabel.get(key) ?? { label: dominant.unitLabel, rates: [] };
5854
+ row.rates.push(rate);
5855
+ byLabel.set(key, row);
5856
+ }
5857
+ const models = [...byLabel.values()].map((row) => {
5858
+ const ok = row.rates.length >= WEIGH_MIN_CLEAN;
5859
+ return {
5860
+ unitLabel: row.label,
5861
+ ppPerUsd: ok ? band(row.rates) : null,
5862
+ cleanIntervals: row.rates.length,
5863
+ status: ok ? "ok" : "unknown"
5864
+ };
5865
+ }).sort((a, b) => {
5866
+ if (a.status !== b.status) return a.status === "ok" ? -1 : 1;
5867
+ return (b.ppPerUsd?.median ?? 0) - (a.ppPerUsd?.median ?? 0);
5868
+ });
5869
+ return {
5870
+ window: "7d",
5871
+ provider: "claude-code",
5872
+ blendedPpPerUsd: risingUsd > 0 ? risingPct / risingUsd : null,
5873
+ risingIntervals,
5874
+ cleanIntervals,
5875
+ models,
5876
+ caveats: CAVEATS
5877
+ };
5878
+ }
5879
+
5880
+ // packages/core/src/otel.ts
5881
+ function attr(key, value) {
5882
+ return { key, value: { stringValue: value } };
5883
+ }
5884
+ function nano(ts) {
5885
+ return `${BigInt(Math.max(0, Math.round(ts))) * 1000000n}`;
5886
+ }
5887
+ function eventsToOtlp(events2, opts = {}) {
5888
+ const costPoints = [];
5889
+ const tokenPoints = [];
5890
+ for (const e of events2) {
5891
+ const attributes = [
5892
+ attr("provider", e.provider),
5893
+ attr("unit_label", e.unitLabel ?? "unknown"),
5894
+ attr("scope_hash", e.scopeHash)
5895
+ ];
5896
+ if (opts.includeLabels && e.label) attributes.push(attr("project", e.label));
5897
+ const t = nano(e.ts);
5898
+ if (e.costUsd !== null && Number.isFinite(e.costUsd)) {
5899
+ costPoints.push({ asDouble: e.costUsd, timeUnixNano: t, attributes });
5900
+ }
5901
+ tokenPoints.push({ asDouble: e.tokens, timeUnixNano: t, attributes });
5902
+ }
5903
+ return {
5904
+ resourceMetrics: [{
5905
+ resource: {
5906
+ attributes: [attr("service.name", opts.serviceName ?? "nomnomtokens")]
5907
+ },
5908
+ scopeMetrics: [{
5909
+ scope: { name: "nomnomtokens" },
5910
+ metrics: [
5911
+ {
5912
+ name: "nnt.cost_usd",
5913
+ unit: "USD",
5914
+ sum: {
5915
+ dataPoints: costPoints,
5916
+ aggregationTemporality: 2,
5917
+ isMonotonic: true
5918
+ }
5919
+ },
5920
+ {
5921
+ name: "nnt.tokens",
5922
+ unit: "1",
5923
+ sum: {
5924
+ dataPoints: tokenPoints,
5925
+ aggregationTemporality: 2,
5926
+ isMonotonic: true
5927
+ }
5928
+ }
5929
+ ]
5930
+ }]
5931
+ }]
5932
+ };
5933
+ }
5934
+
5456
5935
  // packages/db/src/repo.ts
5457
5936
  var Repo = class {
5458
5937
  constructor(sqlite) {
@@ -5474,11 +5953,13 @@ var Repo = class {
5474
5953
  WHERE excluded.qty_total >= events.qty_total
5475
5954
  `);
5476
5955
  this.upsertScope = sqlite.prepare(`
5477
- INSERT INTO scopes (scope_hash, label, provider, last_seen)
5478
- VALUES (@scopeHash, @label, @provider, @lastSeen)
5956
+ INSERT INTO scopes (scope_hash, label, provider, last_seen, client, label_locked)
5957
+ VALUES (@scopeHash, @label, @provider, @lastSeen, @client, @labelLocked)
5479
5958
  ON CONFLICT(scope_hash) DO UPDATE SET
5480
- label = excluded.label,
5481
- last_seen = MAX(scopes.last_seen, excluded.last_seen)
5959
+ label = CASE WHEN scopes.label_locked = 1 THEN scopes.label ELSE excluded.label END,
5960
+ last_seen = MAX(scopes.last_seen, excluded.last_seen),
5961
+ client = COALESCE(excluded.client, scopes.client),
5962
+ label_locked = MAX(scopes.label_locked, excluded.label_locked)
5482
5963
  `);
5483
5964
  this.insertLimit = sqlite.prepare(`
5484
5965
  INSERT INTO limits (ts, provider, window, used_pct, resets_at)
@@ -5537,7 +6018,9 @@ var Repo = class {
5537
6018
  scopeHash: rec.scopeHash,
5538
6019
  label: rec.label,
5539
6020
  provider: rec.provider,
5540
- lastSeen: Date.now()
6021
+ lastSeen: rec.lastSeen ?? Date.now(),
6022
+ client: rec.client ?? null,
6023
+ labelLocked: rec.labelLocked ? 1 : 0
5541
6024
  });
5542
6025
  result.scopes += 1;
5543
6026
  }
@@ -5569,6 +6052,24 @@ var Repo = class {
5569
6052
  eventCount() {
5570
6053
  return this.sqlite.prepare("SELECT COUNT(*) AS c FROM events").pluck().get();
5571
6054
  }
6055
+ /**
6056
+ * Rename a project or attach a client tag. Locks the label so a later scan
6057
+ * cannot revert it to the folder basename.
6058
+ */
6059
+ updateScope(scopeHash2, patch) {
6060
+ const row = this.sqlite.prepare(
6061
+ "SELECT label, client FROM scopes WHERE scope_hash = ?"
6062
+ ).get(scopeHash2);
6063
+ if (!row) return false;
6064
+ const label = patch.label !== void 0 ? patch.label.trim() || row.label : row.label;
6065
+ const client = patch.client !== void 0 ? patch.client?.trim() || null : row.client;
6066
+ this.sqlite.prepare(`
6067
+ UPDATE scopes
6068
+ SET label = @label, client = @client, label_locked = 1
6069
+ WHERE scope_hash = @scopeHash
6070
+ `).run({ scopeHash: scopeHash2, label, client });
6071
+ return true;
6072
+ }
5572
6073
  /**
5573
6074
  * Collapse Cursor Auto-mode placeholders (`default`, `default,default,…`)
5574
6075
  * into a null unit_label so the UI shows a single "unknown" bucket.
@@ -5630,6 +6131,12 @@ function buildWhere(f, prefix = "e") {
5630
6131
  });
5631
6132
  clauses.push(`${prefix}.scope_hash IN (${names.join(", ")})`);
5632
6133
  }
6134
+ if (f.client) {
6135
+ clauses.push(
6136
+ `${prefix}.scope_hash IN (SELECT scope_hash FROM scopes WHERE client = @client)`
6137
+ );
6138
+ params.client = f.client;
6139
+ }
5633
6140
  return { sql: clauses.length ? `WHERE ${clauses.join(" AND ")}` : "", params };
5634
6141
  }
5635
6142
  var BUCKET = {
@@ -5695,7 +6202,7 @@ var Queries = class {
5695
6202
  byScope(f = {}) {
5696
6203
  const w = buildWhere(f);
5697
6204
  return this.sqlite.prepare(`
5698
- SELECT e.scope_hash AS scopeHash, s.label AS label, s.last_seen AS lastSeen, ${TOTALS}
6205
+ SELECT e.scope_hash AS scopeHash, s.label AS label, s.client AS client, s.last_seen AS lastSeen, ${TOTALS}
5699
6206
  FROM events e
5700
6207
  LEFT JOIN scopes s ON s.scope_hash = e.scope_hash
5701
6208
  ${w.sql}
@@ -5784,11 +6291,89 @@ var Queries = class {
5784
6291
  limitWindows() {
5785
6292
  return this.sqlite.prepare("SELECT DISTINCT provider, window FROM limits ORDER BY provider, window").all();
5786
6293
  }
6294
+ /** Latest snapshot per (provider, window) — statusline Codex headroom. */
6295
+ latestLimits() {
6296
+ return this.sqlite.prepare(`
6297
+ SELECT l.ts, l.provider, l.window, l.used_pct AS usedPct, l.resets_at AS resetsAt
6298
+ FROM limits l
6299
+ INNER JOIN (
6300
+ SELECT provider, window, MAX(ts) AS ts
6301
+ FROM limits
6302
+ GROUP BY provider, window
6303
+ ) latest ON latest.provider = l.provider AND latest.window = l.window AND latest.ts = l.ts
6304
+ ORDER BY l.provider, l.window
6305
+ `).all();
6306
+ }
6307
+ coldResumesFor(sessions, limit = 5) {
6308
+ return collectColdResumes(
6309
+ sessions.map((s) => ({
6310
+ sessionId: s.sessionId,
6311
+ label: s.label,
6312
+ turns: this.sessionEvents(s.sessionId)
6313
+ })),
6314
+ limit
6315
+ );
6316
+ }
5787
6317
  scopeLabels() {
5788
6318
  return this.sqlite.prepare(`
5789
- SELECT scope_hash AS scopeHash, label, provider, last_seen AS lastSeen
6319
+ SELECT scope_hash AS scopeHash, label, provider, last_seen AS lastSeen,
6320
+ client, label_locked AS labelLocked
5790
6321
  FROM scopes ORDER BY last_seen DESC
5791
- `).all();
6322
+ `).all().map((row) => {
6323
+ const r = row;
6324
+ return { ...r, labelLocked: r.labelLocked === 1 };
6325
+ });
6326
+ }
6327
+ /**
6328
+ * Spend tagged as a Claude Code sidechain (subagent) turn.
6329
+ *
6330
+ * `taggedEvents` counts rows that have any `meta.sidechain` key — parents omit
6331
+ * it entirely, so zero tagged events means "this corpus has no sidechain
6332
+ * signal" (Cursor/Codex), not "0% subagents".
6333
+ */
6334
+ sidechainTotals(f = {}) {
6335
+ const w = buildWhere(f);
6336
+ const sidechainClause = `json_extract(e.meta_json, '$.sidechain') = 1`;
6337
+ const where = w.sql ? `${w.sql} AND ${sidechainClause}` : `WHERE ${sidechainClause}`;
6338
+ const totals = this.sqlite.prepare(`SELECT ${TOTALS} FROM events e ${where}`).get(w.params);
6339
+ const tagged = this.sqlite.prepare(`
6340
+ SELECT COUNT(*) AS n
6341
+ FROM events e ${w.sql}${w.sql ? " AND" : "WHERE"}
6342
+ json_extract(e.meta_json, '$.sidechain') IS NOT NULL
6343
+ `).get(w.params);
6344
+ return { ...totals, taggedEvents: tagged.n };
6345
+ }
6346
+ /**
6347
+ * Top sessions by cost, with the sidechain cost share when any turns are tagged.
6348
+ * Used by the audit report — not a general-purpose sessions list.
6349
+ */
6350
+ sessionsAudit(f = {}, limit = 5) {
6351
+ const w = buildWhere(f);
6352
+ const params = { ...w.params, limit };
6353
+ const rows = this.sqlite.prepare(`
6354
+ SELECT
6355
+ e.session_id AS sessionId,
6356
+ MIN(e.scope_hash) AS scopeHash,
6357
+ COALESCE(SUM(e.cost_usd), 0) AS costUsd,
6358
+ COALESCE(SUM(e.qty_total), 0) AS tokens,
6359
+ COALESCE(SUM(
6360
+ CASE WHEN json_extract(e.meta_json, '$.sidechain') = 1
6361
+ THEN e.cost_usd ELSE 0 END
6362
+ ), 0) AS sidechainCostUsd,
6363
+ SUM(
6364
+ CASE WHEN json_extract(e.meta_json, '$.sidechain') IS NOT NULL
6365
+ THEN 1 ELSE 0 END
6366
+ ) AS sidechainTagged
6367
+ FROM events e
6368
+ ${w.sql}${w.sql ? " AND" : "WHERE"} e.session_id IS NOT NULL
6369
+ GROUP BY e.session_id
6370
+ ORDER BY costUsd DESC
6371
+ LIMIT @limit
6372
+ `).all(params);
6373
+ return rows.map((r) => {
6374
+ const scope = this.sqlite.prepare("SELECT label FROM scopes WHERE scope_hash = ?").pluck().get(r.scopeHash);
6375
+ return { ...r, label: scope ?? null };
6376
+ });
5792
6377
  }
5793
6378
  /**
5794
6379
  * Lines the agent changed while earning the spend in `f`.
@@ -5827,6 +6412,7 @@ var Queries = class {
5827
6412
  e.session_id AS sessionId,
5828
6413
  e.scope_hash AS scopeHash,
5829
6414
  s.label AS project,
6415
+ s.client AS client,
5830
6416
  e.unit_label AS model,
5831
6417
  e.cost_usd AS costUsd,
5832
6418
  e.qty_total AS tokens,
@@ -5848,6 +6434,7 @@ var Queries = class {
5848
6434
  sessionId: r.sessionId,
5849
6435
  scopeHash: r.scopeHash,
5850
6436
  project: r.project,
6437
+ client: r.client,
5851
6438
  model: r.model,
5852
6439
  costUsd: r.costUsd,
5853
6440
  tokens: r.tokens,
@@ -5860,6 +6447,28 @@ var Queries = class {
5860
6447
  };
5861
6448
  });
5862
6449
  }
6450
+ exportSpendEvents(limit = 1e5) {
6451
+ return this.sqlite.prepare(`
6452
+ SELECT id, ts, provider, kind, session_id AS sessionId, scope_hash AS scopeHash,
6453
+ unit_label AS unitLabel, qty_json AS qtyJson, cost_usd AS costUsd,
6454
+ meta_json AS metaJson
6455
+ FROM events ORDER BY ts ASC LIMIT ?
6456
+ `).all(limit).map((row) => {
6457
+ const r = row;
6458
+ return {
6459
+ id: r.id,
6460
+ ts: r.ts,
6461
+ provider: r.provider,
6462
+ kind: r.kind,
6463
+ sessionId: r.sessionId,
6464
+ scopeHash: r.scopeHash,
6465
+ unitLabel: r.unitLabel,
6466
+ qty: JSON.parse(r.qtyJson),
6467
+ costUsd: r.costUsd,
6468
+ meta: JSON.parse(r.metaJson)
6469
+ };
6470
+ });
6471
+ }
5863
6472
  };
5864
6473
  function defaultPricesPath() {
5865
6474
  return join(defaultDataDir(), "prices.json");
@@ -5952,6 +6561,24 @@ function duration(ms) {
5952
6561
  if (ms < 1e3) return `${Math.round(ms)}ms`;
5953
6562
  return `${(ms / 1e3).toFixed(1)}s`;
5954
6563
  }
6564
+ function pct(n) {
6565
+ return n === null || n === void 0 ? "\u2014" : `${n.toFixed(1)}%`;
6566
+ }
6567
+ function untilReset(ms, now = Date.now()) {
6568
+ if (ms === null || ms === void 0 || !Number.isFinite(ms)) return null;
6569
+ const delta = ms - now;
6570
+ if (delta <= 0) return "now";
6571
+ const minutes = Math.floor(delta / 6e4);
6572
+ if (minutes < 60) return `${Math.max(minutes, 1)}m`;
6573
+ const hours = Math.floor(minutes / 60);
6574
+ if (hours < 24) {
6575
+ const rest2 = minutes % 60;
6576
+ return rest2 === 0 ? `${hours}h` : `${hours}h${rest2}m`;
6577
+ }
6578
+ const days = Math.floor(hours / 24);
6579
+ const rest = hours % 24;
6580
+ return rest === 0 ? `${days}d` : `${days}d${rest}h`;
6581
+ }
5955
6582
  var MASCOT = String.raw`
5956
6583
  (\_/)
5957
6584
  ( o.o ) nom nom nom
@@ -6034,6 +6661,108 @@ function desktopNotify(hits) {
6034
6661
  }
6035
6662
  }
6036
6663
 
6664
+ // packages/cli/src/commands/audit.ts
6665
+ function rangeFromDays(days, now = Date.now()) {
6666
+ const to = now;
6667
+ const start = new Date(now);
6668
+ start.setHours(0, 0, 0, 0);
6669
+ start.setDate(start.getDate() - (days - 1));
6670
+ return { from: start.getTime(), to };
6671
+ }
6672
+ function formatGap2(ms) {
6673
+ const hours = Math.round(ms / 36e5);
6674
+ if (hours < 1) return `${Math.max(1, Math.round(ms / 6e4))}m`;
6675
+ return `${hours}h`;
6676
+ }
6677
+ function auditCommand(opts = {}) {
6678
+ const days = opts.days && opts.days > 0 ? opts.days : 7;
6679
+ const { from, to } = rangeFromDays(days);
6680
+ const { sqlite, path: dbPath } = openDb(opts.db);
6681
+ const q = new Queries(sqlite);
6682
+ const filters = { from, to, kind: "tokens" };
6683
+ const totals = q.totals(filters);
6684
+ const sidechain = q.sidechainTotals(filters);
6685
+ const models = q.byUnitLabel(filters);
6686
+ const sessions = q.sessionsAudit(filters, 5);
6687
+ const coldResumes = q.coldResumesFor(sessions);
6688
+ sqlite.close();
6689
+ const report = buildAuditReport({
6690
+ range: { from, to },
6691
+ totals: {
6692
+ costUsd: totals.costUsd,
6693
+ tokens: totals.tokens,
6694
+ events: totals.events,
6695
+ qtyIn: totals.qtyIn,
6696
+ qtyCacheRead: totals.qtyCacheRead
6697
+ },
6698
+ sidechain: {
6699
+ taggedEvents: sidechain.taggedEvents,
6700
+ costUsd: sidechain.costUsd,
6701
+ tokens: sidechain.tokens
6702
+ },
6703
+ models: models.map((m) => ({ unitLabel: m.unitLabel, costUsd: m.costUsd })),
6704
+ sessions: sessions.map((s) => ({
6705
+ sessionId: s.sessionId,
6706
+ label: s.label,
6707
+ costUsd: s.costUsd,
6708
+ sidechainCostUsd: s.sidechainCostUsd,
6709
+ sidechainTagged: s.sidechainTagged
6710
+ })),
6711
+ coldResumes
6712
+ });
6713
+ if (opts.json) {
6714
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
6715
+ `);
6716
+ return;
6717
+ }
6718
+ console.log(c.bold(`Audit \xB7 last ${days}d`));
6719
+ console.log(c.dim(`from ${dbPath}`));
6720
+ console.log();
6721
+ console.log(` cost ${usd(report.totals.costUsd)}`);
6722
+ console.log(` tokens ${compactNumber(report.totals.tokens)}`);
6723
+ console.log(` events ${report.totals.events}`);
6724
+ console.log(` cache hit ${pct(report.cache.hitRate !== null ? report.cache.hitRate * 100 : null)}`);
6725
+ console.log(
6726
+ ` subagents ${report.sidechain.tagged ? `${pct(report.sidechain.shareOfCost !== null ? report.sidechain.shareOfCost * 100 : null)} \xB7 ${usd(report.sidechain.costUsd)}` : `${c.dim("n/a")} (Claude Code only)`}`
6727
+ );
6728
+ console.log();
6729
+ if (report.models.length) {
6730
+ console.log(c.bold("Models"));
6731
+ for (const m of report.models.slice(0, 5)) {
6732
+ const label = (m.unitLabel ?? "unknown").padEnd(28);
6733
+ console.log(` ${label} ${pct(m.share * 100).padStart(5)} ${usd(m.costUsd)}`);
6734
+ }
6735
+ console.log();
6736
+ }
6737
+ if (report.topSessions.length) {
6738
+ console.log(c.bold("Top sessions"));
6739
+ for (const s of report.topSessions) {
6740
+ const name = (s.label ?? s.sessionId.slice(0, 12)).padEnd(28);
6741
+ const sc = s.sidechainShare !== null ? ` \xB7 sub ${pct(s.sidechainShare * 100)}` : "";
6742
+ console.log(` ${name} ${usd(s.costUsd)}${sc}`);
6743
+ }
6744
+ console.log();
6745
+ }
6746
+ if (report.coldResumes.length) {
6747
+ console.log(c.bold("Cold resumes"));
6748
+ for (const hit of report.coldResumes) {
6749
+ const name = (hit.label ?? hit.sessionId.slice(0, 12)).padEnd(28);
6750
+ console.log(` ${name} ${usd(hit.costUsd)} after ${formatGap2(hit.gapMs)} idle`);
6751
+ }
6752
+ console.log();
6753
+ }
6754
+ if (report.tips.length) {
6755
+ console.log(c.bold("Tips"));
6756
+ for (const tip of report.tips) {
6757
+ console.log(` \u2022 ${tip}`);
6758
+ }
6759
+ console.log();
6760
+ }
6761
+ if (!opts.quiet && report.totals.events === 0) {
6762
+ console.log(c.dim("No token events in this range. Run `nnt scan`, then try again."));
6763
+ }
6764
+ }
6765
+
6037
6766
  // packages/adapters/src/claude-code/jsonl.ts
6038
6767
  function num(v) {
6039
6768
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
@@ -7111,6 +7840,57 @@ async function detectAdapters(opts = {}) {
7111
7840
  }
7112
7841
 
7113
7842
  // packages/cli/src/commands/doctor.ts
7843
+ var DEFAULT_CLEANUP_DAYS = 30;
7844
+ function readCleanupPeriodDays(settingsPath2) {
7845
+ if (!existsSync(settingsPath2)) return DEFAULT_CLEANUP_DAYS;
7846
+ try {
7847
+ const parsed = JSON.parse(readFileSync(settingsPath2, "utf8"));
7848
+ const n = parsed.cleanupPeriodDays;
7849
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : DEFAULT_CLEANUP_DAYS;
7850
+ } catch {
7851
+ return DEFAULT_CLEANUP_DAYS;
7852
+ }
7853
+ }
7854
+ function listJsonlFiles(root) {
7855
+ if (!existsSync(root)) return [];
7856
+ const out = [];
7857
+ const stack = [root];
7858
+ while (stack.length > 0) {
7859
+ const dir = stack.pop();
7860
+ let entries;
7861
+ try {
7862
+ entries = readdirSync(dir, { withFileTypes: true });
7863
+ } catch {
7864
+ continue;
7865
+ }
7866
+ for (const entry of entries) {
7867
+ const path = join(dir, entry.name);
7868
+ if (entry.isDirectory()) stack.push(path);
7869
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(path);
7870
+ }
7871
+ }
7872
+ return out;
7873
+ }
7874
+ function oldestMtime(paths) {
7875
+ let min = null;
7876
+ for (const path of paths) {
7877
+ try {
7878
+ const t = statSync(path).mtimeMs;
7879
+ if (min === null || t < min) min = t;
7880
+ } catch {
7881
+ }
7882
+ }
7883
+ return min;
7884
+ }
7885
+ function transcriptRetentionWarning(opts) {
7886
+ if (opts.storeFirst === null) return null;
7887
+ const now = opts.now ?? Date.now();
7888
+ const cutoff = now - opts.cleanupDays * 864e5;
7889
+ const storeOlderThanRetention = opts.storeFirst < cutoff;
7890
+ const transcriptsAlreadyGone = opts.oldestJsonlMtime !== null && opts.oldestJsonlMtime > opts.storeFirst + 864e5;
7891
+ if (!storeOlderThanRetention && !transcriptsAlreadyGone) return null;
7892
+ return `store keeps history from ${new Date(opts.storeFirst).toISOString().slice(0, 10)}; transcripts will not (cleanupPeriodDays=${opts.cleanupDays})`;
7893
+ }
7114
7894
  function check(ok, label, detail) {
7115
7895
  const mark = ok ? c.green("\u2714") : c.yellow("\u2717");
7116
7896
  console.log(`${mark} ${label}${detail ? c.dim(` ${detail}`) : ""}`);
@@ -7119,7 +7899,7 @@ async function doctor(opts = {}) {
7119
7899
  console.log(c.bold("nomnomtokens doctor"));
7120
7900
  console.log();
7121
7901
  console.log(c.bold("Sources"));
7122
- const projects = claudeProjectsDir();
7902
+ const projects = opts.claudeProjects ?? claudeProjectsDir();
7123
7903
  check(existsSync(projects), "Claude Code transcripts", projects);
7124
7904
  const cursorDb = cursorStateDbPath();
7125
7905
  check(existsSync(cursorDb), "Cursor state database", cursorDb);
@@ -7129,7 +7909,7 @@ async function doctor(opts = {}) {
7129
7909
  check(adapters.length > 0, `${adapters.length} adapter(s) detected`, adapters.map((a) => a.name).join(", ") || "none");
7130
7910
  console.log();
7131
7911
  console.log(c.bold("Status line"));
7132
- const settingsFile = join(homedir(), ".claude", "settings.json");
7912
+ const settingsFile = opts.claudeSettings ?? join(homedir(), ".claude", "settings.json");
7133
7913
  let statusLineCommand;
7134
7914
  if (existsSync(settingsFile)) {
7135
7915
  try {
@@ -7161,6 +7941,14 @@ async function doctor(opts = {}) {
7161
7941
  check(false, `${totals.unpricedEvents} events could not be priced`, "unknown model id \u2014 see packages/core/src/pricing.ts");
7162
7942
  }
7163
7943
  check(windows.length > 0, "limit history", windows.length > 0 ? windows.map((w) => `${w.provider}/${w.window}`).join(", ") : "none yet \u2014 the status line hook has not fired");
7944
+ const cleanupDays = readCleanupPeriodDays(settingsFile);
7945
+ const jsonl = listJsonlFiles(projects);
7946
+ const warning = transcriptRetentionWarning({
7947
+ storeFirst: bounds.first,
7948
+ oldestJsonlMtime: oldestMtime(jsonl),
7949
+ cleanupDays
7950
+ });
7951
+ check(!warning, "transcript retention", warning ?? `cleanupPeriodDays ${cleanupDays}`);
7164
7952
  sqlite.close();
7165
7953
  }
7166
7954
  console.log();
@@ -7184,23 +7972,48 @@ function rangeFrom(range, now = Date.now()) {
7184
7972
  }
7185
7973
  }
7186
7974
  function exportCommand(opts = {}) {
7187
- const format = opts.format === "json" ? "json" : "csv";
7975
+ const format = opts.format === "json" || opts.format === "nnt" ? opts.format : "csv";
7188
7976
  const { sqlite, path: dbPath } = openDb(opts.db);
7189
7977
  const q = new Queries(sqlite);
7190
- const from = rangeFrom(opts.range ?? "30d");
7191
- const rows = q.exportEvents({
7192
- from,
7193
- kind: "tokens",
7194
- provider: opts.provider ? [opts.provider] : void 0
7195
- });
7978
+ let body;
7979
+ let count;
7980
+ if (format === "nnt") {
7981
+ const scopes2 = q.scopeLabels();
7982
+ body = formatNntArchive({
7983
+ version: 1,
7984
+ events: q.exportSpendEvents(),
7985
+ limits: q.limitSnapshots(),
7986
+ scopes: scopes2.map((s) => ({
7987
+ scopeHash: s.scopeHash,
7988
+ label: s.label,
7989
+ provider: s.provider,
7990
+ lastSeen: s.lastSeen,
7991
+ client: s.client,
7992
+ labelLocked: s.labelLocked
7993
+ }))
7994
+ });
7995
+ count = q.bounds().events;
7996
+ if (!opts.quiet) {
7997
+ console.error(c.dim("nnt archive includes local project labels. Treat the file as the whole store."));
7998
+ }
7999
+ } else {
8000
+ const from = rangeFrom(opts.range ?? "30d");
8001
+ const rows = q.exportEvents({
8002
+ from,
8003
+ kind: "tokens",
8004
+ provider: opts.provider ? [opts.provider] : void 0,
8005
+ client: opts.client
8006
+ });
8007
+ body = format === "json" ? formatExportJson(rows) : formatExportCsv(rows);
8008
+ count = rows.length;
8009
+ }
7196
8010
  sqlite.close();
7197
- const body = format === "json" ? formatExportJson(rows) : formatExportCsv(rows);
7198
8011
  if (opts.out) {
7199
8012
  const dest = resolve(opts.out);
7200
8013
  writeFileSync(dest, body, "utf8");
7201
8014
  if (!opts.quiet) {
7202
8015
  console.log(
7203
- `${c.cyan("export")} ${c.bold(String(rows.length))} events \u2192 ${dest}`
8016
+ `${c.cyan("export")} ${c.bold(String(count))} events \u2192 ${dest}`
7204
8017
  );
7205
8018
  console.log(c.dim(`from ${dbPath}`));
7206
8019
  }
@@ -7208,7 +8021,7 @@ function exportCommand(opts = {}) {
7208
8021
  }
7209
8022
  process.stdout.write(body);
7210
8023
  if (!opts.quiet && process.stdout.isTTY) {
7211
- console.error(c.dim(`${compactNumber(rows.length)} events \xB7 ${format}`));
8024
+ console.error(c.dim(`${compactNumber(count)} events \xB7 ${format}`));
7212
8025
  }
7213
8026
  }
7214
8027
  async function importCsvCommand(opts) {
@@ -7259,6 +8072,47 @@ async function importCsvCommand(opts) {
7259
8072
  console.log(c.dim(`${compactNumber(before + added)} events in ${dbPath}`));
7260
8073
  }
7261
8074
  }
8075
+
8076
+ // packages/cli/src/commands/import.ts
8077
+ async function importCommand(opts) {
8078
+ const path = resolve(opts.file);
8079
+ let text2;
8080
+ try {
8081
+ text2 = readFileSync(path, "utf8");
8082
+ } catch (err) {
8083
+ console.error(c.red(`Cannot read ${path}`));
8084
+ console.error(c.dim(err instanceof Error ? err.message : String(err)));
8085
+ process.exitCode = 1;
8086
+ return;
8087
+ }
8088
+ if (looksLikeNntArchive(text2)) {
8089
+ let records;
8090
+ try {
8091
+ records = parseNntArchive(text2);
8092
+ } catch (err) {
8093
+ console.error(c.red(err instanceof Error ? err.message : String(err)));
8094
+ process.exitCode = 1;
8095
+ return;
8096
+ }
8097
+ const { sqlite, path: dbPath } = openDb(opts.db);
8098
+ const repo = new Repo(sqlite);
8099
+ const before = repo.eventCount();
8100
+ const result = repo.ingest(records);
8101
+ const added = repo.eventCount() - before;
8102
+ sqlite.close();
8103
+ if (!opts.quiet) {
8104
+ console.log(
8105
+ `${c.cyan("nnt")} ${c.bold(String(added))} new ` + c.dim(
8106
+ `${result.events} events, ${result.duplicates} already known, ${result.limits} limits, ${result.scopes} scopes`
8107
+ )
8108
+ );
8109
+ console.log(c.dim(`${compactNumber(before + added)} events in ${dbPath}`));
8110
+ console.log(c.dim("Archive includes local project labels and client tags. scan_state is not imported."));
8111
+ }
8112
+ return;
8113
+ }
8114
+ await importCsvCommand(opts);
8115
+ }
7262
8116
  function settingsPath(override) {
7263
8117
  return override ?? join(homedir(), ".claude", "settings.json");
7264
8118
  }
@@ -7315,74 +8169,6 @@ function init(opts = {}) {
7315
8169
  console.log("Restart Claude Code, then run `nnt serve` to open the dashboard.");
7316
8170
  console.log(c.dim("Limits and live updates only start flowing once the hook has fired once."));
7317
8171
  }
7318
- async function loadFromSource(from) {
7319
- let text2;
7320
- if (/^https?:\/\//i.test(from)) {
7321
- const res = await fetch(from);
7322
- if (!res.ok) throw new Error(`fetch ${from}: HTTP ${res.status}`);
7323
- text2 = await res.text();
7324
- } else {
7325
- text2 = readFileSync(resolve(from), "utf8");
7326
- }
7327
- const file = parsePricesFile(text2);
7328
- return {
7329
- ...file,
7330
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7331
- source: from
7332
- };
7333
- }
7334
- function pricesShow() {
7335
- const { file, path } = loadPriceTable();
7336
- const overrideCount = file ? Object.keys(file.models).length : 0;
7337
- console.log(c.bold("Price table"));
7338
- console.log(` file ${existsLabel(path, file !== null)}`);
7339
- if (file?.updatedAt) console.log(` updated ${file.updatedAt}`);
7340
- if (file?.source) console.log(` source ${file.source}`);
7341
- console.log(` bundled ${Object.keys(PRICES).length} models`);
7342
- console.log(` overrides ${overrideCount} model(s) in file`);
7343
- console.log();
7344
- console.log(c.dim("Sample (effective USD / 1M tokens):"));
7345
- for (const id of ["claude-sonnet-4-5", "claude-opus-4-6", "gpt-5.6", "gemini-3-flash"]) {
7346
- const key = resolveModelKey(id);
7347
- const p = key ? file?.models[key] ?? PRICES[key] : null;
7348
- console.log(` ${id.padEnd(22)} ${formatPrice(p)}`);
7349
- }
7350
- console.log();
7351
- console.log(c.dim("Edit the file, or run `nnt prices refresh` / `nnt prices refresh --from <url|path>`."));
7352
- }
7353
- function existsLabel(path, ok) {
7354
- return ok ? path : `${path} ${c.dim("(missing \u2014 using bundled)")}`;
7355
- }
7356
- function formatPrice(p) {
7357
- if (!p) return c.dim("unpriced");
7358
- return `in $${p.input} out $${p.output}`;
7359
- }
7360
- async function pricesRefresh(opts = {}) {
7361
- const path = defaultPricesPath();
7362
- let file;
7363
- if (opts.from) {
7364
- try {
7365
- file = await loadFromSource(opts.from);
7366
- } catch (err) {
7367
- console.error(c.red(err instanceof Error ? err.message : String(err)));
7368
- process.exitCode = 1;
7369
- return;
7370
- }
7371
- file = {
7372
- ...file,
7373
- models: { ...PRICES, ...file.models }
7374
- };
7375
- savePricesFile(file, path);
7376
- } else {
7377
- file = writeBundledPrices(path);
7378
- }
7379
- if (!opts.quiet) {
7380
- console.log(
7381
- `${c.cyan("prices")} wrote ${c.bold(String(Object.keys(file.models).length))} models \u2192 ${path}`
7382
- );
7383
- if (file.source) console.log(c.dim(`source ${file.source}`));
7384
- }
7385
- }
7386
8172
 
7387
8173
  // packages/cli/src/commands/scan.ts
7388
8174
  async function scan(opts = {}) {
@@ -7463,15 +8249,142 @@ async function scan(opts = {}) {
7463
8249
  if (opts.watch) {
7464
8250
  console.log(c.dim("\nWatching for changes. Ctrl-C to stop."));
7465
8251
  const stops = adapters.filter((a) => a.watch).map((a) => a.watch((rec) => repo.ingest([rec])));
7466
- await new Promise((resolve6) => {
8252
+ await new Promise((resolve7) => {
7467
8253
  process.on("SIGINT", () => {
7468
8254
  stops.forEach((stop) => stop());
7469
- resolve6();
8255
+ resolve7();
7470
8256
  });
7471
8257
  });
7472
8258
  }
7473
8259
  sqlite.close();
7474
8260
  }
8261
+
8262
+ // packages/cli/src/commands/otel.ts
8263
+ function otelEvents(q, includeLabels) {
8264
+ const labels = new Map(q.scopeLabels().map((s) => [s.scopeHash, s.label]));
8265
+ return q.exportSpendEvents().map((e) => {
8266
+ const row = {
8267
+ ts: e.ts,
8268
+ provider: e.provider,
8269
+ unitLabel: e.unitLabel,
8270
+ scopeHash: e.scopeHash,
8271
+ costUsd: e.costUsd,
8272
+ tokens: totalTokens(e.qty)
8273
+ };
8274
+ if (includeLabels) row.label = labels.get(e.scopeHash) ?? null;
8275
+ return row;
8276
+ });
8277
+ }
8278
+ async function postOtlp(endpoint, body) {
8279
+ const url = endpoint.replace(/\/$/, "");
8280
+ const target = url.endsWith("/v1/metrics") ? url : `${url}/v1/metrics`;
8281
+ const res = await fetch(target, {
8282
+ method: "POST",
8283
+ headers: { "content-type": "application/json" },
8284
+ body: JSON.stringify(body)
8285
+ });
8286
+ if (!res.ok) {
8287
+ const text2 = await res.text().catch(() => "");
8288
+ throw new Error(`OTLP POST ${res.status} ${text2.slice(0, 200)}`);
8289
+ }
8290
+ }
8291
+ async function otelCommand(opts) {
8292
+ const endpoint = opts.endpoint.trim();
8293
+ if (!endpoint) {
8294
+ console.error(c.red("nnt otel requires --endpoint"));
8295
+ process.exitCode = 1;
8296
+ return;
8297
+ }
8298
+ if (opts.scan || opts.watch) {
8299
+ await scan({ db: opts.db, quiet: true });
8300
+ }
8301
+ const { sqlite, path: dbPath } = openDb(opts.db);
8302
+ const q = new Queries(sqlite);
8303
+ const events2 = otelEvents(q, Boolean(opts.includeLabels));
8304
+ sqlite.close();
8305
+ const body = eventsToOtlp(events2, { includeLabels: opts.includeLabels });
8306
+ try {
8307
+ await postOtlp(endpoint, body);
8308
+ } catch (err) {
8309
+ console.error(c.red(err instanceof Error ? err.message : String(err)));
8310
+ process.exitCode = 1;
8311
+ return;
8312
+ }
8313
+ if (!opts.quiet) {
8314
+ console.log(
8315
+ `${c.cyan("otel")} ${c.bold(String(events2.length))} events \u2192 ${endpoint}`
8316
+ );
8317
+ console.log(c.dim(`from ${dbPath} \xB7 numbers and hashes only`));
8318
+ }
8319
+ }
8320
+ async function loadFromSource(from) {
8321
+ let text2;
8322
+ if (/^https?:\/\//i.test(from)) {
8323
+ const res = await fetch(from);
8324
+ if (!res.ok) throw new Error(`fetch ${from}: HTTP ${res.status}`);
8325
+ text2 = await res.text();
8326
+ } else {
8327
+ text2 = readFileSync(resolve(from), "utf8");
8328
+ }
8329
+ const file = parsePricesFile(text2);
8330
+ return {
8331
+ ...file,
8332
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8333
+ source: from
8334
+ };
8335
+ }
8336
+ function pricesShow() {
8337
+ const { file, path } = loadPriceTable();
8338
+ const overrideCount = file ? Object.keys(file.models).length : 0;
8339
+ console.log(c.bold("Price table"));
8340
+ console.log(` file ${existsLabel(path, file !== null)}`);
8341
+ if (file?.updatedAt) console.log(` updated ${file.updatedAt}`);
8342
+ if (file?.source) console.log(` source ${file.source}`);
8343
+ console.log(` bundled ${Object.keys(PRICES).length} models`);
8344
+ console.log(` overrides ${overrideCount} model(s) in file`);
8345
+ console.log();
8346
+ console.log(c.dim("Sample (effective USD / 1M tokens):"));
8347
+ for (const id of ["claude-sonnet-4-5", "claude-opus-4-6", "gpt-5.6", "gemini-3-flash"]) {
8348
+ const key = resolveModelKey(id);
8349
+ const p = key ? file?.models[key] ?? PRICES[key] : null;
8350
+ console.log(` ${id.padEnd(22)} ${formatPrice(p)}`);
8351
+ }
8352
+ console.log();
8353
+ console.log(c.dim("Edit the file, or run `nnt prices refresh` / `nnt prices refresh --from <url|path>`."));
8354
+ }
8355
+ function existsLabel(path, ok) {
8356
+ return ok ? path : `${path} ${c.dim("(missing \u2014 using bundled)")}`;
8357
+ }
8358
+ function formatPrice(p) {
8359
+ if (!p) return c.dim("unpriced");
8360
+ return `in $${p.input} out $${p.output}`;
8361
+ }
8362
+ async function pricesRefresh(opts = {}) {
8363
+ const path = defaultPricesPath();
8364
+ let file;
8365
+ if (opts.from) {
8366
+ try {
8367
+ file = await loadFromSource(opts.from);
8368
+ } catch (err) {
8369
+ console.error(c.red(err instanceof Error ? err.message : String(err)));
8370
+ process.exitCode = 1;
8371
+ return;
8372
+ }
8373
+ file = {
8374
+ ...file,
8375
+ models: { ...PRICES, ...file.models }
8376
+ };
8377
+ savePricesFile(file, path);
8378
+ } else {
8379
+ file = writeBundledPrices(path);
8380
+ }
8381
+ if (!opts.quiet) {
8382
+ console.log(
8383
+ `${c.cyan("prices")} wrote ${c.bold(String(Object.keys(file.models).length))} models \u2192 ${path}`
8384
+ );
8385
+ if (file.source) console.log(c.dim(`source ${file.source}`));
8386
+ }
8387
+ }
7475
8388
  var here = dirname(fileURLToPath(import.meta.url));
7476
8389
  function findBuiltServer() {
7477
8390
  let dir = here;
@@ -7529,7 +8442,7 @@ async function serve(opts = {}) {
7529
8442
  });
7530
8443
  }
7531
8444
 
7532
- // packages/cli/src/commands/statusline.ts
8445
+ // packages/cli/src/statusline-render.ts
7533
8446
  var MOOD_FACE = {
7534
8447
  hungry: "(\u30FB_\u30FB)",
7535
8448
  content: "(^_^)",
@@ -7537,11 +8450,61 @@ var MOOD_FACE = {
7537
8450
  stuffed: "(>_<)",
7538
8451
  overstuffed: "(x_x)"
7539
8452
  };
8453
+ var CLAUDE_WEEKLY_HINT = 80;
8454
+ var CODEX_HEADROOM = 50;
8455
+ function limitSegment(label, usedPct, resetsAtSeconds, now) {
8456
+ const resetsAt = typeof resetsAtSeconds === "number" && Number.isFinite(resetsAtSeconds) ? resetsAtSeconds * 1e3 : null;
8457
+ const left = untilReset(resetsAt, now);
8458
+ return `${label} ${Math.round(usedPct)}%${left ? ` \xB7${left}` : ""}`;
8459
+ }
8460
+ function buildStatuslineParts(view) {
8461
+ const now = view.now;
8462
+ const five = view.fiveHour;
8463
+ const seven = view.sevenDay;
8464
+ const moodPct = Math.max(five ?? 0, seven ?? 0);
8465
+ const parts = [];
8466
+ parts.push(MOOD_FACE[moodFor(moodPct)] ?? "(^_^)");
8467
+ if (view.cost !== null) parts.push(usd(view.cost));
8468
+ if (view.ctx !== null) parts.push(`ctx ${Math.round(view.ctx)}%`);
8469
+ const fiveSeg = five !== null ? limitSegment("5h", five, view.fiveHourResetsAt, now) : null;
8470
+ const sevenSeg = seven !== null ? limitSegment("7d", seven, view.sevenDayResetsAt, now) : null;
8471
+ const weeklyFirst = seven !== null && (five === null || seven >= five);
8472
+ if (weeklyFirst) {
8473
+ if (sevenSeg) parts.push(sevenSeg);
8474
+ if (fiveSeg) parts.push(fiveSeg);
8475
+ } else {
8476
+ if (fiveSeg) parts.push(fiveSeg);
8477
+ if (sevenSeg) parts.push(sevenSeg);
8478
+ }
8479
+ if (view.lines > 0) parts.push(`${compactNumber(view.lines)} lines`);
8480
+ const codex = view.codexSevenDay;
8481
+ if (seven !== null && seven >= CLAUDE_WEEKLY_HINT && typeof codex === "number" && Number.isFinite(codex) && codex <= CODEX_HEADROOM) {
8482
+ parts.push(`codex 7d ${Math.round(codex)}%`);
8483
+ }
8484
+ return parts;
8485
+ }
8486
+
8487
+ // packages/cli/src/commands/statusline.ts
8488
+ var CODEX_FRESH_MS = 24 * 36e5;
7540
8489
  async function readStdin() {
7541
8490
  const chunks = [];
7542
8491
  for await (const chunk of process.stdin) chunks.push(chunk);
7543
8492
  return Buffer.concat(chunks).toString("utf8");
7544
8493
  }
8494
+ function readCodexWeekly(db) {
8495
+ try {
8496
+ const { sqlite } = openDb(db);
8497
+ const q = new Queries(sqlite);
8498
+ const latest = q.latestLimits();
8499
+ sqlite.close();
8500
+ const row = latest.find((l) => l.provider === "codex" && l.window === "7d");
8501
+ if (!row) return null;
8502
+ if (Date.now() - row.ts > CODEX_FRESH_MS) return null;
8503
+ return row.usedPct;
8504
+ } catch {
8505
+ return null;
8506
+ }
8507
+ }
7545
8508
  async function statusline(opts = {}) {
7546
8509
  let payload;
7547
8510
  try {
@@ -7555,14 +8518,18 @@ async function statusline(opts = {}) {
7555
8518
  const sevenDay = limits2?.seven_day?.used_percentage ?? null;
7556
8519
  const cost = payload.cost?.total_cost_usd ?? null;
7557
8520
  const ctx = payload.context_window?.used_percentage ?? null;
7558
- const parts = [];
7559
- parts.push(MOOD_FACE[moodFor(Math.max(fiveHour ?? 0, sevenDay ?? 0))] ?? "(^_^)");
7560
- if (cost !== null) parts.push(usd(cost));
7561
- if (ctx !== null) parts.push(`ctx ${Math.round(ctx)}%`);
7562
- if (fiveHour !== null) parts.push(`5h ${Math.round(fiveHour)}%`);
7563
- if (sevenDay !== null) parts.push(`7d ${Math.round(sevenDay)}%`);
7564
8521
  const lines = (payload.cost?.total_lines_added ?? 0) + (payload.cost?.total_lines_removed ?? 0);
7565
- if (lines > 0) parts.push(`${compactNumber(lines)} lines`);
8522
+ const codexSevenDay = readCodexWeekly(opts.db);
8523
+ const parts = buildStatuslineParts({
8524
+ fiveHour,
8525
+ sevenDay,
8526
+ fiveHourResetsAt: limits2?.five_hour?.resets_at,
8527
+ sevenDayResetsAt: limits2?.seven_day?.resets_at,
8528
+ cost,
8529
+ ctx,
8530
+ lines,
8531
+ codexSevenDay
8532
+ });
7566
8533
  process.stdout.write(`${parts.join(" ")}
7567
8534
  `);
7568
8535
  if (opts.dryRun) return;
@@ -7575,6 +8542,94 @@ async function statusline(opts = {}) {
7575
8542
  }
7576
8543
  }
7577
8544
 
8545
+ // packages/cli/src/commands/verdict.ts
8546
+ function verdictCommand(opts = {}) {
8547
+ const now = Date.now();
8548
+ const weekFrom = now - 7 * 24 * 36e5;
8549
+ const { sqlite, path: dbPath } = openDb(opts.db);
8550
+ const q = new Queries(sqlite);
8551
+ const weekly = q.limitSnapshots("claude-code", "7d", weekFrom);
8552
+ const fiveHour = q.limitSnapshots("claude-code", "5h", weekFrom);
8553
+ const weeklyCostUsd = q.totals({ from: weekFrom, to: now, kind: "tokens", provider: ["claude-code"] }).costUsd;
8554
+ sqlite.close();
8555
+ const report = buildVerdict({ weekly, fiveHour, weeklyCostUsd, now });
8556
+ if (opts.json) {
8557
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
8558
+ `);
8559
+ return;
8560
+ }
8561
+ console.log(c.bold("Verdict"));
8562
+ console.log(c.dim(`from ${dbPath}`));
8563
+ console.log();
8564
+ console.log(` recommended ${c.bold(report.recommended)}`);
8565
+ console.log(` weekly bound ${report.weeklyBound ? "yes" : "no"}`);
8566
+ console.log(` 7d used ${pct(report.weeklyUsedPct)}`);
8567
+ console.log(` 5h used ${pct(report.fiveHourUsedPct)}`);
8568
+ console.log(` week $ ${usd(report.weeklyCostUsd)} ${c.dim("(list price, this machine)")}`);
8569
+ console.log();
8570
+ console.log(c.bold("Two Max 5x vs one 20x"));
8571
+ console.log(` ${report.two5xVs20x.two5xWins ? c.yellow("two 5x wins") : "20x wins the 5h burst"}`);
8572
+ console.log(` ${c.dim(report.two5xVs20x.reason)}`);
8573
+ console.log();
8574
+ console.log(c.bold("Caveats"));
8575
+ for (const line of report.caveats) {
8576
+ console.log(` \u2022 ${line}`);
8577
+ }
8578
+ }
8579
+
8580
+ // packages/cli/src/commands/weigh.ts
8581
+ function weighCommand(opts = {}) {
8582
+ const { sqlite, path: dbPath } = openDb(opts.db);
8583
+ const q = new Queries(sqlite);
8584
+ const snaps = q.limitSnapshots("claude-code", "7d");
8585
+ const gaps = pairRisingSnapshots(snaps).map((p) => ({
8586
+ deltaPct: p.deltaPct,
8587
+ models: q.byUnitLabel({
8588
+ from: p.from,
8589
+ to: p.to,
8590
+ provider: ["claude-code"],
8591
+ kind: "tokens"
8592
+ }).filter((row) => row.costUsd > 0).map((row) => ({ unitLabel: row.unitLabel, costUsd: row.costUsd }))
8593
+ }));
8594
+ sqlite.close();
8595
+ const report = buildWeigh(gaps);
8596
+ if (opts.json) {
8597
+ process.stdout.write(`${JSON.stringify(report, null, 2)}
8598
+ `);
8599
+ return;
8600
+ }
8601
+ printWeigh(report, dbPath);
8602
+ }
8603
+ function printWeigh(report, dbPath) {
8604
+ console.log(c.bold("Weigh"));
8605
+ console.log(c.dim(`from ${dbPath}`));
8606
+ console.log();
8607
+ console.log(` window ${report.provider} ${report.window}`);
8608
+ console.log(` blended ${pp(report.blendedPpPerUsd)} ${c.dim("per list-price dollar, all rising gaps")}`);
8609
+ console.log(` gaps ${report.cleanIntervals} clean / ${report.risingIntervals} rising`);
8610
+ console.log();
8611
+ console.log(c.bold("Models"));
8612
+ if (report.models.length === 0) {
8613
+ console.log(c.dim(" no priced gaps in the current 7d window \u2014 run `nnt init` so the status line records weekly fill"));
8614
+ }
8615
+ for (const model of report.models) console.log(modelLine(model));
8616
+ console.log();
8617
+ console.log(c.bold("Caveats"));
8618
+ for (const line of report.caveats) console.log(` \u2022 ${line}`);
8619
+ }
8620
+ function modelLine(model) {
8621
+ const name = model.unitLabel ?? "(unlabeled)";
8622
+ if (model.status === "unknown" || !model.ppPerUsd) {
8623
+ return ` ${name} ${c.yellow("unknown")} ${c.dim(`need 3 clean gaps, have ${model.cleanIntervals}`)}`;
8624
+ }
8625
+ const { p25, median: median2, p75 } = model.ppPerUsd;
8626
+ return ` ${name} ${pp(median2)} ${c.dim(`p25 ${pp(p25)} p75 ${pp(p75)} ${model.cleanIntervals} gaps`)}`;
8627
+ }
8628
+ function pp(n) {
8629
+ if (n === null) return "\u2014";
8630
+ return `${n.toFixed(2)} pp/$`;
8631
+ }
8632
+
7578
8633
  // packages/cli/src/index.ts
7579
8634
  function packageVersion() {
7580
8635
  let dir = dirname(fileURLToPath(import.meta.url));
@@ -7591,7 +8646,7 @@ function packageVersion() {
7591
8646
  return "0.0.0";
7592
8647
  }
7593
8648
  var program = new Command();
7594
- program.name("nnt").description("nom nom nom \u2014 your agent is eating tokens. The dashboard shows exactly how many.").version(packageVersion()).option("--db <path>", "database file (default: ~/.nomnomtokens/data.db)");
8649
+ program.name("nnt").description("nom nom nom \u2014 your agents are eating tokens. One local store shows where.").version(packageVersion()).option("--db <path>", "database file (default: ~/.nomnomtokens/data.db)");
7595
8650
  program.command("scan", { isDefault: false }).description("read new records from every detected source into the local store").option("-w, --watch", "stay running and ingest new records as they are written").option("-q, --quiet", "suppress the summary").action(async (opts) => {
7596
8651
  await scan({ ...opts, db: program.opts().db });
7597
8652
  });
@@ -7612,8 +8667,8 @@ program.command("statusline").description("read Claude Code session JSON on stdi
7612
8667
  program.command("doctor").description("check sources, hook wiring and the local store").action(async () => {
7613
8668
  await doctor({ db: program.opts().db });
7614
8669
  });
7615
- program.command("import <file>").description("import a CSV billing export or spreadsheet into the local store").option("--provider <name>", "provider label written on every row", "csv").option("--kind <kind>", "event kind (tokens, minutes, \u2026)", "tokens").option("-q, --quiet", "suppress the summary").action(async (file, opts) => {
7616
- await importCsvCommand({
8670
+ program.command("import <file>").description("import a billing CSV or an nnt archive JSON into the local store").option("--provider <name>", "provider label written on every row", "csv").option("--kind <kind>", "event kind (tokens, minutes, \u2026)", "tokens").option("-q, --quiet", "suppress the summary").action(async (file, opts) => {
8671
+ await importCommand({
7617
8672
  file,
7618
8673
  provider: opts.provider,
7619
8674
  kind: opts.kind,
@@ -7621,14 +8676,15 @@ program.command("import <file>").description("import a CSV billing export or spr
7621
8676
  db: program.opts().db
7622
8677
  });
7623
8678
  });
7624
- program.command("export").description("write filtered events as CSV or JSON (stdout or -o file)").option("--format <fmt>", "csv or json", "csv").option("-o, --out <file>", "write to a file instead of stdout").option("--range <range>", "24h | 7d | 30d | 90d | all", "30d").option("--provider <name>", "filter to one provider").option("-q, --quiet", "suppress the summary on stderr").action((opts) => {
7625
- const format = opts.format === "json" ? "json" : "csv";
8679
+ program.command("export").description("write filtered events as CSV, JSON, or an nnt archive").option("--format <fmt>", "csv, json, or nnt", "csv").option("-o, --out <file>", "write to a file instead of stdout").option("--range <range>", "24h | 7d | 30d | 90d | all", "30d").option("--provider <name>", "filter to one provider").option("--client <name>", "filter to a local client tag").option("-q, --quiet", "suppress the summary on stderr").action((opts) => {
8680
+ const format = opts.format === "json" || opts.format === "nnt" ? opts.format : "csv";
7626
8681
  const range = ["24h", "7d", "30d", "90d", "all"].includes(opts.range) ? opts.range : "30d";
7627
8682
  exportCommand({
7628
8683
  format,
7629
8684
  out: opts.out,
7630
8685
  range,
7631
8686
  provider: opts.provider,
8687
+ client: opts.client,
7632
8688
  quiet: opts.quiet,
7633
8689
  db: program.opts().db
7634
8690
  });
@@ -7650,6 +8706,37 @@ alerts.command("check").description("evaluate thresholds; exit 2 if any are firi
7650
8706
  db: program.opts().db
7651
8707
  });
7652
8708
  });
8709
+ program.command("audit").description("where spend went: cache, subagents, models, and tips").option("--days <n>", "calendar days to include (default 7)", (v) => Number.parseInt(v, 10), 7).option("--json", "print the audit report as JSON").option("-q, --quiet", "suppress empty-range hint").action((opts) => {
8710
+ auditCommand({
8711
+ days: opts.days,
8712
+ json: opts.json,
8713
+ quiet: opts.quiet,
8714
+ db: program.opts().db
8715
+ });
8716
+ });
8717
+ program.command("verdict").description("which Claude plan your weekly fill actually needs (Pro / 5x / 20x)").option("--json", "print the verdict as JSON").action((opts) => {
8718
+ verdictCommand({
8719
+ json: opts.json,
8720
+ db: program.opts().db
8721
+ });
8722
+ });
8723
+ program.command("weigh").description("weekly limit points per list-price dollar, from gaps one model dominates").option("--json", "print the weigh report as JSON").action((opts) => {
8724
+ weighCommand({
8725
+ json: opts.json,
8726
+ db: program.opts().db
8727
+ });
8728
+ });
8729
+ program.command("otel").description("opt-in OTLP/HTTP export of local numbers (never on by default)").requiredOption("--endpoint <url>", "OTLP HTTP base (we POST /v1/metrics)").option("--once", "export current store and exit (default)").option("-w, --watch", "scan once, then export").option("--scan", "run a catch-up scan before exporting").option("--include-labels", "attach local project labels (off by default)").option("-q, --quiet", "suppress the summary").action(async (opts) => {
8730
+ await otelCommand({
8731
+ endpoint: opts.endpoint,
8732
+ once: opts.once,
8733
+ watch: opts.watch,
8734
+ scan: opts.scan,
8735
+ includeLabels: opts.includeLabels,
8736
+ quiet: opts.quiet,
8737
+ db: program.opts().db
8738
+ });
8739
+ });
7653
8740
  var COMMANDS = /* @__PURE__ */ new Set([
7654
8741
  "scan",
7655
8742
  "serve",
@@ -7660,6 +8747,10 @@ var COMMANDS = /* @__PURE__ */ new Set([
7660
8747
  "export",
7661
8748
  "prices",
7662
8749
  "alerts",
8750
+ "audit",
8751
+ "verdict",
8752
+ "weigh",
8753
+ "otel",
7663
8754
  "help"
7664
8755
  ]);
7665
8756
  var META_FLAGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version"]);