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.
- package/README.md +48 -24
- package/dist/index.js +1198 -107
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/web/nitro.json +1 -1
- package/web/public/_nuxt/AuWqCQTg.js +1 -0
- package/web/public/_nuxt/{W3KL_OjU.js → B1SHP0x7.js} +1 -1
- package/web/public/_nuxt/{DQEnwY-5.js → B4lg_KdO.js} +1 -1
- package/web/public/_nuxt/B8pNcdCn.js +1 -0
- package/web/public/_nuxt/{COwSpxrx.js → BL80uXr2.js} +1 -1
- package/web/public/_nuxt/{B2aHje2L.js → BOu13FsZ.js} +1 -1
- package/web/public/_nuxt/{CvIugsJe2.js → BYf03To42.js} +1 -1
- package/web/public/_nuxt/{CkRZsevx.js → B_fydnl_.js} +1 -1
- package/web/public/_nuxt/{DnTwjl0_2.js → BboXpc0v2.js} +2 -2
- package/web/public/_nuxt/{CfBMjq8x.js → Bg-w_mbM.js} +1 -1
- package/web/public/_nuxt/{B1jhn8zK.js → BsIJiDwC.js} +1 -1
- package/web/public/_nuxt/BsfWsX6E.js +1 -0
- package/web/public/_nuxt/{C9MiT_Wm.js → BuKW47N9.js} +2 -2
- package/web/public/_nuxt/{4C_a23zt.js → BvB3OU0z.js} +1 -1
- package/web/public/_nuxt/CLdT3CQk.js +1 -0
- package/web/public/_nuxt/{D9j6aFUy.js → Car0xtsn.js} +1 -1
- package/web/public/_nuxt/Ch1ysvEi.js +1 -0
- package/web/public/_nuxt/{BOKlFKrX.js → Ck5ddq-9.js} +1 -1
- package/web/public/_nuxt/{Cq1Sq3Go.js → CsVo4EXo.js} +1 -1
- package/web/public/_nuxt/CzaLG4_B2.js +1 -0
- package/web/public/_nuxt/{C51OzH6V.js → D9MZNmok.js} +1 -1
- package/web/public/_nuxt/{D9Mxq5ER.js → DQ6vgNw-.js} +1 -1
- package/web/public/_nuxt/{D49wVVwm2.js → DZ0oUjlX2.js} +1 -1
- package/web/public/_nuxt/DbTv-m36.js +1 -0
- package/web/public/_nuxt/{D4C6J8YV.js → DdHo_MGy.js} +1 -1
- package/web/public/_nuxt/{m3-Vaz25.js → DmteO8Ul.js} +1 -1
- package/web/public/_nuxt/DyoXCutE.js +1 -0
- package/web/public/_nuxt/{DPnm6zS2.js → DyvBBld_2.js} +1 -1
- package/web/public/_nuxt/{CIKwfDBl.js → PaV2v2OF.js} +1 -1
- package/web/public/_nuxt/{BMSLzLlT.js → RjmsMzM7.js} +1 -1
- package/web/public/_nuxt/builds/latest.json +1 -1
- package/web/public/_nuxt/builds/meta/2bf417d4-fc60-4108-8624-e631e05f5a46.json +1 -0
- package/web/public/_nuxt/entry.CE8-9Sal.css +2 -0
- package/web/public/_nuxt/{Dy30RUCD2.js → hPGLTkZo2.js} +1 -1
- package/web/public/_nuxt/{Bprokln-.js → jeqgGzjf.js} +1 -1
- package/web/public/_nuxt/n29w62AT.js +1 -0
- package/web/public/_nuxt/{CDzUujek.js → tShBBc0S.js} +2 -2
- package/web/server/chunks/_/filters.mjs +1 -0
- package/web/server/chunks/_/filters.mjs.map +1 -1
- package/web/server/chunks/build/AuditCard-CgGiEb_r.mjs +290 -0
- package/web/server/chunks/build/AuditCard-CgGiEb_r.mjs.map +1 -0
- package/web/server/chunks/build/{CardContent-C_IMqI8K.mjs → CardDescription-Bi5t5iXj.mjs} +30 -30
- package/web/server/chunks/build/CardDescription-Bi5t5iXj.mjs.map +1 -0
- package/web/server/chunks/build/{FilterBar-BlEsJ5dy.mjs → FilterBar-BYBOdTrl.mjs} +2 -2
- package/web/server/chunks/build/{FilterBar-BlEsJ5dy.mjs.map → FilterBar-BYBOdTrl.mjs.map} +1 -1
- package/web/server/chunks/build/{Sparkline-CaCRq45J.mjs → Sparkline-B3kazZhU.mjs} +2 -2
- package/web/server/chunks/build/{Sparkline-CaCRq45J.mjs.map → Sparkline-B3kazZhU.mjs.map} +1 -1
- package/web/server/chunks/build/{_id_-BvccBwzH.mjs → _id_-DPUt6OzN.mjs} +4 -4
- package/web/server/chunks/build/{_id_-BvccBwzH.mjs.map → _id_-DPUt6OzN.mjs.map} +1 -1
- package/web/server/chunks/build/asyncData-CMa0z9Yw.mjs +2 -2
- package/web/server/chunks/build/audit-BPAlwhzn.mjs +1180 -0
- package/web/server/chunks/build/audit-BPAlwhzn.mjs.map +1 -0
- package/web/server/chunks/build/{default-BokiYQNv.mjs → default-CvQF0-O_.mjs} +46 -22
- package/web/server/chunks/build/default-CvQF0-O_.mjs.map +1 -0
- package/web/server/chunks/build/export-CwC19yOz.mjs +232 -0
- package/web/server/chunks/build/export-CwC19yOz.mjs.map +1 -0
- package/web/server/chunks/build/fetch-DJ9NNbFP.mjs +1 -1
- package/web/server/chunks/build/{import-CO3NEu0P.mjs → import-BmoVtcGR.mjs} +31 -15
- package/web/server/chunks/build/import-BmoVtcGR.mjs.map +1 -0
- package/web/server/chunks/build/{limits-CX9zBSqw.mjs → limits-C21_a18j.mjs} +5 -5
- package/web/server/chunks/build/{limits-CX9zBSqw.mjs.map → limits-C21_a18j.mjs.map} +1 -1
- package/web/server/chunks/build/nuxt-link-mYiLFsJs.mjs +2 -2
- package/web/server/chunks/build/{pages-DyXqJyPq.mjs → pages-CJpczmoA.mjs} +81 -11
- package/web/server/chunks/build/pages-CJpczmoA.mjs.map +1 -0
- package/web/server/chunks/build/{providers-Ca7keu2G.mjs → providers-BBVw9CDb.mjs} +4 -4
- package/web/server/chunks/build/{providers-Ca7keu2G.mjs.map → providers-BBVw9CDb.mjs.map} +1 -1
- package/web/server/chunks/build/{scopes-E5z80S19.mjs → scopes-DpYnOAM9.mjs} +203 -62
- package/web/server/chunks/build/scopes-DpYnOAM9.mjs.map +1 -0
- package/web/server/chunks/build/{sessions-CESRiCOS.mjs → sessions-B-Y_ifp1.mjs} +5 -5
- package/web/server/chunks/build/{sessions-CESRiCOS.mjs.map → sessions-B-Y_ifp1.mjs.map} +1 -1
- package/web/server/chunks/build/{timeline-d27OlbZN.mjs → timeline-8kK28WQB.mjs} +5 -5
- package/web/server/chunks/build/{timeline-d27OlbZN.mjs.map → timeline-8kK28WQB.mjs.map} +1 -1
- package/web/server/chunks/build/useFormat-NkyUqeik.mjs +1 -1
- package/web/server/chunks/build/{useStats-CmYzpdbE.mjs → useStats-BQQJIKQO.mjs} +7 -5
- package/web/server/chunks/build/{useStats-CmYzpdbE.mjs.map → useStats-BQQJIKQO.mjs.map} +1 -1
- package/web/server/chunks/nitro/nitro.mjs +692 -214
- package/web/server/chunks/nitro/nitro.mjs.map +1 -1
- package/web/server/chunks/routes/api/export.get.mjs +22 -3
- package/web/server/chunks/routes/api/export.get.mjs.map +1 -1
- package/web/server/chunks/routes/api/import/csv.post.mjs +20 -6
- package/web/server/chunks/routes/api/import/csv.post.mjs.map +1 -1
- package/web/server/chunks/routes/api/ingest.post.mjs +1 -1
- package/web/server/chunks/routes/api/meta.get.mjs +1 -1
- package/web/server/chunks/routes/api/scopes/_hash_.patch.mjs +35 -0
- package/web/server/chunks/routes/api/scopes/_hash_.patch.mjs.map +1 -0
- package/web/server/chunks/routes/api/stats/audit.get.mjs +60 -0
- package/web/server/chunks/routes/api/stats/audit.get.mjs.map +1 -0
- package/web/server/chunks/routes/api/stats/limits.get.mjs +1 -1
- package/web/server/chunks/routes/api/stats/providers.get.mjs +1 -1
- package/web/server/chunks/routes/api/stats/scopes.get.mjs +1 -1
- package/web/server/chunks/routes/api/stats/session/_id_.get.mjs +1 -1
- package/web/server/chunks/routes/api/stats/summary.get.mjs +1 -1
- package/web/server/chunks/routes/api/stats/verdict.get.mjs +35 -0
- package/web/server/chunks/routes/api/stats/verdict.get.mjs.map +1 -0
- package/web/server/chunks/routes/renderer.mjs +2 -2
- package/web/server/chunks/virtual/entry.mjs +17 -12
- package/web/server/chunks/virtual/precomputed.mjs +1 -1
- package/web/server/chunks/virtual/precomputed.mjs.map +1 -1
- package/web/server/index.mjs +1 -1
- package/web/public/_nuxt/Bia8fJFj.js +0 -1
- package/web/public/_nuxt/CIHIOSzn.js +0 -1
- package/web/public/_nuxt/CZSbzIi-.js +0 -1
- package/web/public/_nuxt/DMgad8mU.js +0 -1
- package/web/public/_nuxt/DhJW7zb9.js +0 -1
- package/web/public/_nuxt/OU1MVgXQ.js +0 -1
- package/web/public/_nuxt/builds/meta/e90c8a2d-9e50-4b58-8019-6b1bf4c8fc6d.json +0 -1
- package/web/public/_nuxt/cbxnBwm6.js +0 -1
- package/web/public/_nuxt/entry.MbqNBIeH.css +0 -2
- package/web/server/chunks/build/CardContent-C_IMqI8K.mjs.map +0 -1
- package/web/server/chunks/build/default-BokiYQNv.mjs.map +0 -1
- package/web/server/chunks/build/export-BhfwmSMl.mjs +0 -184
- package/web/server/chunks/build/export-BhfwmSMl.mjs.map +0 -1
- package/web/server/chunks/build/import-CO3NEu0P.mjs.map +0 -1
- package/web/server/chunks/build/pages-DyXqJyPq.mjs.map +0 -1
- 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,
|
|
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 =
|
|
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"
|
|
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
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
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(
|
|
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(
|
|
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((
|
|
8252
|
+
await new Promise((resolve7) => {
|
|
7467
8253
|
process.on("SIGINT", () => {
|
|
7468
8254
|
stops.forEach((stop) => stop());
|
|
7469
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
|
7616
|
-
await
|
|
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
|
|
7625
|
-
const format = opts.format === "json"
|
|
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"]);
|