nomnomtokens 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -27
- package/dist/index.js +1207 -129
- 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/fe9c123e-ca03-4d26-9bfd-e56ed456cf65.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 +695 -217
- 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/b80e2306-16ec-4868-89f8-b713594d0c00.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) {
|
|
@@ -5278,13 +5303,6 @@ function forecastLimit(snapshots, now = Date.now()) {
|
|
|
5278
5303
|
samples: current.length
|
|
5279
5304
|
};
|
|
5280
5305
|
}
|
|
5281
|
-
function moodFor(usedPct) {
|
|
5282
|
-
if (usedPct < 5) return "hungry";
|
|
5283
|
-
if (usedPct < 50) return "content";
|
|
5284
|
-
if (usedPct < 80) return "full";
|
|
5285
|
-
if (usedPct < 100) return "stuffed";
|
|
5286
|
-
return "overstuffed";
|
|
5287
|
-
}
|
|
5288
5306
|
|
|
5289
5307
|
// packages/core/src/export-format.ts
|
|
5290
5308
|
var CSV_HEADERS = [
|
|
@@ -5293,6 +5311,7 @@ var CSV_HEADERS = [
|
|
|
5293
5311
|
"kind",
|
|
5294
5312
|
"session_id",
|
|
5295
5313
|
"project",
|
|
5314
|
+
"client",
|
|
5296
5315
|
"scope_hash",
|
|
5297
5316
|
"model",
|
|
5298
5317
|
"cost_usd",
|
|
@@ -5318,6 +5337,7 @@ function formatExportCsv(rows) {
|
|
|
5318
5337
|
csvCell(r.kind),
|
|
5319
5338
|
csvCell(r.sessionId),
|
|
5320
5339
|
csvCell(r.project),
|
|
5340
|
+
csvCell(r.client),
|
|
5321
5341
|
csvCell(r.scopeHash),
|
|
5322
5342
|
csvCell(r.model),
|
|
5323
5343
|
csvCell(r.costUsd),
|
|
@@ -5340,6 +5360,7 @@ function formatExportJson(rows) {
|
|
|
5340
5360
|
kind: r.kind,
|
|
5341
5361
|
sessionId: r.sessionId,
|
|
5342
5362
|
project: r.project,
|
|
5363
|
+
client: r.client,
|
|
5343
5364
|
scopeHash: r.scopeHash,
|
|
5344
5365
|
model: r.model,
|
|
5345
5366
|
costUsd: r.costUsd,
|
|
@@ -5352,6 +5373,95 @@ function formatExportJson(rows) {
|
|
|
5352
5373
|
})), null, 2)}
|
|
5353
5374
|
`;
|
|
5354
5375
|
}
|
|
5376
|
+
var NNT_ARCHIVE_VERSION = 1;
|
|
5377
|
+
function formatNntArchive(archive) {
|
|
5378
|
+
return `${JSON.stringify({ ...archive, version: NNT_ARCHIVE_VERSION }, null, 2)}
|
|
5379
|
+
`;
|
|
5380
|
+
}
|
|
5381
|
+
function numbersOnly(meta) {
|
|
5382
|
+
if (!meta || typeof meta !== "object") return {};
|
|
5383
|
+
const out = {};
|
|
5384
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
5385
|
+
if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
5386
|
+
}
|
|
5387
|
+
return out;
|
|
5388
|
+
}
|
|
5389
|
+
function qtyOnly(qty) {
|
|
5390
|
+
if (!qty || typeof qty !== "object") return {};
|
|
5391
|
+
const out = {};
|
|
5392
|
+
for (const [k, v] of Object.entries(qty)) {
|
|
5393
|
+
if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
5394
|
+
}
|
|
5395
|
+
return out;
|
|
5396
|
+
}
|
|
5397
|
+
function looksLikeNntArchive(text2) {
|
|
5398
|
+
const trimmed = text2.trimStart();
|
|
5399
|
+
if (!trimmed.startsWith("{")) return false;
|
|
5400
|
+
try {
|
|
5401
|
+
const parsed = JSON.parse(text2);
|
|
5402
|
+
return parsed.version === NNT_ARCHIVE_VERSION && Array.isArray(parsed.events);
|
|
5403
|
+
} catch {
|
|
5404
|
+
return false;
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
function parseNntArchive(text2) {
|
|
5408
|
+
let parsed;
|
|
5409
|
+
try {
|
|
5410
|
+
parsed = JSON.parse(text2);
|
|
5411
|
+
} catch {
|
|
5412
|
+
throw new Error("nnt archive is not valid JSON");
|
|
5413
|
+
}
|
|
5414
|
+
const data = parsed;
|
|
5415
|
+
if (data.version !== NNT_ARCHIVE_VERSION || !Array.isArray(data.events)) {
|
|
5416
|
+
throw new Error("not an nnt archive (expected { version: 1, events })");
|
|
5417
|
+
}
|
|
5418
|
+
const records = [];
|
|
5419
|
+
for (const raw of data.events) {
|
|
5420
|
+
if (!raw || typeof raw.id !== "string") continue;
|
|
5421
|
+
const qty = qtyOnly(raw.qty);
|
|
5422
|
+
const event = {
|
|
5423
|
+
id: raw.id,
|
|
5424
|
+
ts: typeof raw.ts === "number" ? raw.ts : 0,
|
|
5425
|
+
provider: typeof raw.provider === "string" ? raw.provider : "nnt",
|
|
5426
|
+
kind: typeof raw.kind === "string" ? raw.kind : "tokens",
|
|
5427
|
+
sessionId: typeof raw.sessionId === "string" ? raw.sessionId : null,
|
|
5428
|
+
scopeHash: typeof raw.scopeHash === "string" ? raw.scopeHash : "unknown",
|
|
5429
|
+
unitLabel: typeof raw.unitLabel === "string" ? raw.unitLabel : null,
|
|
5430
|
+
qty,
|
|
5431
|
+
costUsd: typeof raw.costUsd === "number" ? raw.costUsd : null,
|
|
5432
|
+
meta: numbersOnly(raw.meta)
|
|
5433
|
+
};
|
|
5434
|
+
if (!Number.isFinite(event.ts) || event.ts <= 0) continue;
|
|
5435
|
+
void totalTokens(qty);
|
|
5436
|
+
records.push({ type: "event", event });
|
|
5437
|
+
}
|
|
5438
|
+
for (const raw of data.limits ?? []) {
|
|
5439
|
+
if (!raw || typeof raw.ts !== "number" || typeof raw.provider !== "string") continue;
|
|
5440
|
+
records.push({
|
|
5441
|
+
type: "limit",
|
|
5442
|
+
limit: {
|
|
5443
|
+
ts: raw.ts,
|
|
5444
|
+
provider: raw.provider,
|
|
5445
|
+
window: typeof raw.window === "string" ? raw.window : "7d",
|
|
5446
|
+
usedPct: typeof raw.usedPct === "number" ? raw.usedPct : 0,
|
|
5447
|
+
resetsAt: typeof raw.resetsAt === "number" ? raw.resetsAt : null
|
|
5448
|
+
}
|
|
5449
|
+
});
|
|
5450
|
+
}
|
|
5451
|
+
for (const raw of data.scopes ?? []) {
|
|
5452
|
+
if (!raw || typeof raw.scopeHash !== "string" || typeof raw.label !== "string") continue;
|
|
5453
|
+
records.push({
|
|
5454
|
+
type: "scope",
|
|
5455
|
+
scopeHash: raw.scopeHash,
|
|
5456
|
+
label: raw.label,
|
|
5457
|
+
provider: typeof raw.provider === "string" ? raw.provider : "nnt",
|
|
5458
|
+
lastSeen: typeof raw.lastSeen === "number" ? raw.lastSeen : void 0,
|
|
5459
|
+
client: typeof raw.client === "string" ? raw.client : null,
|
|
5460
|
+
labelLocked: Boolean(raw.labelLocked)
|
|
5461
|
+
});
|
|
5462
|
+
}
|
|
5463
|
+
return records;
|
|
5464
|
+
}
|
|
5355
5465
|
|
|
5356
5466
|
// packages/core/src/prices-file.ts
|
|
5357
5467
|
function parsePricesFile(text2) {
|
|
@@ -5453,6 +5563,368 @@ function evaluateAlerts(config, input) {
|
|
|
5453
5563
|
return hits;
|
|
5454
5564
|
}
|
|
5455
5565
|
|
|
5566
|
+
// packages/core/src/audit.ts
|
|
5567
|
+
function share(part, whole) {
|
|
5568
|
+
if (whole <= 0) return null;
|
|
5569
|
+
return part / whole;
|
|
5570
|
+
}
|
|
5571
|
+
function isFrontierModel(label) {
|
|
5572
|
+
if (!label) return false;
|
|
5573
|
+
const l = label.toLowerCase();
|
|
5574
|
+
return l.includes("fable") || l.includes("opus");
|
|
5575
|
+
}
|
|
5576
|
+
function buildAuditReport(input) {
|
|
5577
|
+
const modelLimit = input.modelLimit ?? 5;
|
|
5578
|
+
const sessionLimit = input.sessionLimit ?? 5;
|
|
5579
|
+
const totalCost = input.totals.costUsd;
|
|
5580
|
+
const tagged = input.sidechain.taggedEvents > 0;
|
|
5581
|
+
const models = [...input.models].sort((a, b) => b.costUsd - a.costUsd).slice(0, modelLimit).map((m) => ({
|
|
5582
|
+
unitLabel: m.unitLabel,
|
|
5583
|
+
costUsd: m.costUsd,
|
|
5584
|
+
share: share(m.costUsd, totalCost) ?? 0
|
|
5585
|
+
}));
|
|
5586
|
+
const topSessions = input.sessions.slice(0, sessionLimit).map((s) => ({
|
|
5587
|
+
sessionId: s.sessionId,
|
|
5588
|
+
label: s.label,
|
|
5589
|
+
costUsd: s.costUsd,
|
|
5590
|
+
sidechainShare: s.sidechainTagged > 0 ? share(s.sidechainCostUsd, s.costUsd) : null
|
|
5591
|
+
}));
|
|
5592
|
+
const hitRate = cacheHitRate({
|
|
5593
|
+
cacheRead: input.totals.qtyCacheRead,
|
|
5594
|
+
in: input.totals.qtyIn
|
|
5595
|
+
});
|
|
5596
|
+
const sidechainShare = tagged ? share(input.sidechain.costUsd, totalCost) : null;
|
|
5597
|
+
const report = {
|
|
5598
|
+
range: input.range,
|
|
5599
|
+
totals: {
|
|
5600
|
+
costUsd: input.totals.costUsd,
|
|
5601
|
+
tokens: input.totals.tokens,
|
|
5602
|
+
events: input.totals.events
|
|
5603
|
+
},
|
|
5604
|
+
cache: {
|
|
5605
|
+
hitRate,
|
|
5606
|
+
readTokens: input.totals.qtyCacheRead,
|
|
5607
|
+
freshInTokens: input.totals.qtyIn
|
|
5608
|
+
},
|
|
5609
|
+
sidechain: {
|
|
5610
|
+
tagged,
|
|
5611
|
+
costUsd: input.sidechain.costUsd,
|
|
5612
|
+
tokens: input.sidechain.tokens,
|
|
5613
|
+
shareOfCost: sidechainShare
|
|
5614
|
+
},
|
|
5615
|
+
models,
|
|
5616
|
+
topSessions,
|
|
5617
|
+
coldResumes: (input.coldResumes ?? []).slice(0, sessionLimit),
|
|
5618
|
+
tips: []
|
|
5619
|
+
};
|
|
5620
|
+
report.tips = buildTips(report);
|
|
5621
|
+
return report;
|
|
5622
|
+
}
|
|
5623
|
+
function formatGap(ms) {
|
|
5624
|
+
const hours = Math.round(ms / 36e5);
|
|
5625
|
+
if (hours < 1) return `${Math.max(1, Math.round(ms / 6e4))}m`;
|
|
5626
|
+
return `${hours}h`;
|
|
5627
|
+
}
|
|
5628
|
+
function buildTips(report) {
|
|
5629
|
+
const tips = [];
|
|
5630
|
+
const cold = report.coldResumes[0];
|
|
5631
|
+
if (cold) {
|
|
5632
|
+
const name = cold.label ?? cold.sessionId.slice(0, 12);
|
|
5633
|
+
const dollars = cold.costUsd < 0.01 ? `$${cold.costUsd.toFixed(4)}` : `$${cold.costUsd.toFixed(2)}`;
|
|
5634
|
+
tips.push(
|
|
5635
|
+
`Session ${name} went cold after ${formatGap(cold.gapMs)}; the next turn cost ${dollars} in cache writes \u2014 /clear is cheaper than resume.`
|
|
5636
|
+
);
|
|
5637
|
+
}
|
|
5638
|
+
if (report.sidechain.tagged && report.sidechain.shareOfCost !== null && report.sidechain.shareOfCost >= 0.3) {
|
|
5639
|
+
const pct2 = Math.round(report.sidechain.shareOfCost * 100);
|
|
5640
|
+
tips.push(
|
|
5641
|
+
`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.`
|
|
5642
|
+
);
|
|
5643
|
+
}
|
|
5644
|
+
if (report.cache.hitRate !== null && report.cache.hitRate >= 0.85 && report.cache.readTokens > report.cache.freshInTokens) {
|
|
5645
|
+
tips.push(
|
|
5646
|
+
`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.`
|
|
5647
|
+
);
|
|
5648
|
+
}
|
|
5649
|
+
const top = report.models[0];
|
|
5650
|
+
if (top && isFrontierModel(top.unitLabel) && top.share >= 0.5 && report.totals.costUsd > 0) {
|
|
5651
|
+
const name = top.unitLabel ?? "frontier";
|
|
5652
|
+
tips.push(
|
|
5653
|
+
`${name} is ${Math.round(top.share * 100)}% of spend \u2014 reserve it for hard tasks and default the rest to a cheaper model.`
|
|
5654
|
+
);
|
|
5655
|
+
}
|
|
5656
|
+
return tips.slice(0, 3);
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
// packages/core/src/cold-resume.ts
|
|
5660
|
+
var COLD_GAP_MS = 60 * 60 * 1e3;
|
|
5661
|
+
var MIN_CACHE_WRITE = 2e3;
|
|
5662
|
+
function cacheWriteTokens(qty) {
|
|
5663
|
+
return (qty.cacheCreate ?? 0) + (qty.cacheCreate1h ?? 0);
|
|
5664
|
+
}
|
|
5665
|
+
function median(values) {
|
|
5666
|
+
if (values.length === 0) return 0;
|
|
5667
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5668
|
+
const mid = Math.floor(sorted.length / 2);
|
|
5669
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
5670
|
+
}
|
|
5671
|
+
function detectColdResumes(sessionId, label, turns) {
|
|
5672
|
+
if (turns.length < 2) return [];
|
|
5673
|
+
const sorted = [...turns].sort((a, b) => a.ts - b.ts);
|
|
5674
|
+
const writes = sorted.map((t) => cacheWriteTokens(t.qty));
|
|
5675
|
+
if (!writes.some((w) => w > 0)) return [];
|
|
5676
|
+
const medianWrite = median(writes.filter((w) => w > 0));
|
|
5677
|
+
const out = [];
|
|
5678
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
5679
|
+
const prev = sorted[i - 1];
|
|
5680
|
+
const cur = sorted[i];
|
|
5681
|
+
const gapMs = cur.ts - prev.ts;
|
|
5682
|
+
if (gapMs < COLD_GAP_MS) continue;
|
|
5683
|
+
const write = cacheWriteTokens(cur.qty);
|
|
5684
|
+
if (write < MIN_CACHE_WRITE) continue;
|
|
5685
|
+
const inputish = write + (cur.qty.in ?? 0) + (cur.qty.cacheRead ?? 0);
|
|
5686
|
+
const writeShare = inputish > 0 ? write / inputish : 0;
|
|
5687
|
+
const aboveMedian = medianWrite > 0 ? write > medianWrite * 1.5 : true;
|
|
5688
|
+
if (!aboveMedian && writeShare < 0.5) continue;
|
|
5689
|
+
out.push({
|
|
5690
|
+
sessionId,
|
|
5691
|
+
label,
|
|
5692
|
+
ts: cur.ts,
|
|
5693
|
+
gapMs,
|
|
5694
|
+
costUsd: cur.costUsd ?? 0,
|
|
5695
|
+
cacheWriteTokens: write
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
return out;
|
|
5699
|
+
}
|
|
5700
|
+
function collectColdResumes(sessions, limit = 5) {
|
|
5701
|
+
return sessions.flatMap((s) => detectColdResumes(s.sessionId, s.label, s.turns)).sort((a, b) => b.costUsd - a.costUsd).slice(0, limit);
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
// packages/core/src/verdict.ts
|
|
5705
|
+
var PLAN_ESTIMATES = {
|
|
5706
|
+
asOf: "2026-09-02"};
|
|
5707
|
+
function latestPct(snapshots) {
|
|
5708
|
+
if (snapshots.length === 0) return null;
|
|
5709
|
+
const current = currentWindowSnapshots(snapshots);
|
|
5710
|
+
const last = current[current.length - 1];
|
|
5711
|
+
return last ? last.usedPct : null;
|
|
5712
|
+
}
|
|
5713
|
+
function elapsedWeekFraction(snapshots, now) {
|
|
5714
|
+
const current = currentWindowSnapshots(snapshots);
|
|
5715
|
+
const last = current[current.length - 1];
|
|
5716
|
+
if (!last) return null;
|
|
5717
|
+
const weekMs = 7 * 24 * 36e5;
|
|
5718
|
+
if (last.resetsAt && last.resetsAt > now) {
|
|
5719
|
+
const remaining = last.resetsAt - now;
|
|
5720
|
+
const elapsed = weekMs - remaining;
|
|
5721
|
+
if (elapsed <= 0) return 0.01;
|
|
5722
|
+
return Math.min(1, Math.max(0.01, elapsed / weekMs));
|
|
5723
|
+
}
|
|
5724
|
+
if (current.length < 2) return null;
|
|
5725
|
+
const span = last.ts - current[0].ts;
|
|
5726
|
+
return Math.min(1, Math.max(0.01, span / weekMs));
|
|
5727
|
+
}
|
|
5728
|
+
function buildVerdict(input) {
|
|
5729
|
+
const now = input.now ?? Date.now();
|
|
5730
|
+
const weeklyUsedPct = latestPct(input.weekly);
|
|
5731
|
+
const fiveHourUsedPct = latestPct(input.fiveHour);
|
|
5732
|
+
const fc7 = forecastLimit(input.weekly, now);
|
|
5733
|
+
const fc5 = forecastLimit(input.fiveHour, now);
|
|
5734
|
+
const caveats = [
|
|
5735
|
+
`Plan multipliers are estimates as of ${PLAN_ESTIMATES.asOf}, not Anthropic's bill.`,
|
|
5736
|
+
'"20x" multiplies the 5-hour window; weekly headroom grows much less (two Max 5x accounts often beat one 20x).',
|
|
5737
|
+
"Local used% is this machine only \u2014 other devices and claude.ai are invisible."
|
|
5738
|
+
];
|
|
5739
|
+
if (weeklyUsedPct === null) {
|
|
5740
|
+
return {
|
|
5741
|
+
recommended: "unknown",
|
|
5742
|
+
weeklyBound: false,
|
|
5743
|
+
weeklyUsedPct: null,
|
|
5744
|
+
fiveHourUsedPct,
|
|
5745
|
+
weeklyCostUsd: input.weeklyCostUsd,
|
|
5746
|
+
two5xVs20x: {
|
|
5747
|
+
two5xWins: false,
|
|
5748
|
+
reason: "No 7-day limit snapshots yet \u2014 run `nnt init` so the status line records weekly fill."
|
|
5749
|
+
},
|
|
5750
|
+
estimatesAsOf: PLAN_ESTIMATES.asOf,
|
|
5751
|
+
caveats
|
|
5752
|
+
};
|
|
5753
|
+
}
|
|
5754
|
+
const weeklyBound = fiveHourUsedPct === null || weeklyUsedPct >= fiveHourUsedPct || Boolean(fc7?.willExhaustBeforeReset && !fc5?.willExhaustBeforeReset);
|
|
5755
|
+
const elapsed = elapsedWeekFraction(input.weekly, now);
|
|
5756
|
+
const pace = elapsed ? weeklyUsedPct / elapsed : weeklyUsedPct;
|
|
5757
|
+
let recommended;
|
|
5758
|
+
if (pace >= 180 || weeklyUsedPct >= 98) {
|
|
5759
|
+
recommended = "above-20x";
|
|
5760
|
+
} else if (weeklyBound && (weeklyUsedPct >= 85 || fc7?.willExhaustBeforeReset)) {
|
|
5761
|
+
recommended = weeklyUsedPct >= 92 ? "above-20x" : "max20x";
|
|
5762
|
+
} else if ((fiveHourUsedPct ?? 0) >= 90 || fc5?.willExhaustBeforeReset) {
|
|
5763
|
+
recommended = "max20x";
|
|
5764
|
+
} else if ((fiveHourUsedPct ?? 0) >= 50 || weeklyUsedPct >= 40) {
|
|
5765
|
+
recommended = "max5x";
|
|
5766
|
+
} else {
|
|
5767
|
+
recommended = "pro";
|
|
5768
|
+
}
|
|
5769
|
+
const two5xWins = weeklyBound;
|
|
5770
|
+
const two5xVs20x = {
|
|
5771
|
+
two5xWins,
|
|
5772
|
+
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."
|
|
5773
|
+
};
|
|
5774
|
+
return {
|
|
5775
|
+
recommended,
|
|
5776
|
+
weeklyBound,
|
|
5777
|
+
weeklyUsedPct,
|
|
5778
|
+
fiveHourUsedPct,
|
|
5779
|
+
weeklyCostUsd: input.weeklyCostUsd,
|
|
5780
|
+
two5xVs20x,
|
|
5781
|
+
estimatesAsOf: PLAN_ESTIMATES.asOf,
|
|
5782
|
+
caveats
|
|
5783
|
+
};
|
|
5784
|
+
}
|
|
5785
|
+
|
|
5786
|
+
// packages/core/src/weigh.ts
|
|
5787
|
+
var WEIGH_CLEAN_SHARE = 0.8;
|
|
5788
|
+
var WEIGH_MIN_CLEAN = 3;
|
|
5789
|
+
var CAVEATS = [
|
|
5790
|
+
"Points per dollar use the list price stored now, not Anthropic's undocumented meter unit.",
|
|
5791
|
+
"A gap counts for a model only when that model is at least 80% of priced cost.",
|
|
5792
|
+
"Local used% is this machine only \u2014 other devices and claude.ai sit in the percent and not in the log."
|
|
5793
|
+
];
|
|
5794
|
+
function quantile(sorted, p) {
|
|
5795
|
+
if (sorted.length === 1) return sorted[0];
|
|
5796
|
+
const idx = (sorted.length - 1) * p;
|
|
5797
|
+
const lo = Math.floor(idx);
|
|
5798
|
+
const hi = Math.ceil(idx);
|
|
5799
|
+
if (lo === hi) return sorted[lo];
|
|
5800
|
+
const a = sorted[lo];
|
|
5801
|
+
const b = sorted[hi];
|
|
5802
|
+
return a + (b - a) * (idx - lo);
|
|
5803
|
+
}
|
|
5804
|
+
function band(rates) {
|
|
5805
|
+
const sorted = [...rates].sort((a, b) => a - b);
|
|
5806
|
+
return {
|
|
5807
|
+
p25: quantile(sorted, 0.25),
|
|
5808
|
+
median: quantile(sorted, 0.5),
|
|
5809
|
+
p75: quantile(sorted, 0.75)
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
function priced(models) {
|
|
5813
|
+
return models.filter((m) => m.costUsd > 0);
|
|
5814
|
+
}
|
|
5815
|
+
function pairRisingSnapshots(snapshots) {
|
|
5816
|
+
const current = currentWindowSnapshots(snapshots);
|
|
5817
|
+
const pairs = [];
|
|
5818
|
+
for (let i = 1; i < current.length; i++) {
|
|
5819
|
+
const prev = current[i - 1];
|
|
5820
|
+
const next = current[i];
|
|
5821
|
+
const deltaPct = next.usedPct - prev.usedPct;
|
|
5822
|
+
if (deltaPct <= 0) continue;
|
|
5823
|
+
pairs.push({ from: prev.ts, to: next.ts, deltaPct });
|
|
5824
|
+
}
|
|
5825
|
+
return pairs;
|
|
5826
|
+
}
|
|
5827
|
+
function buildWeigh(gaps) {
|
|
5828
|
+
let risingPct = 0;
|
|
5829
|
+
let risingUsd = 0;
|
|
5830
|
+
let risingIntervals = 0;
|
|
5831
|
+
let cleanIntervals = 0;
|
|
5832
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
5833
|
+
for (const gap of gaps) {
|
|
5834
|
+
if (gap.deltaPct <= 0) continue;
|
|
5835
|
+
const models2 = priced(gap.models);
|
|
5836
|
+
const total = models2.reduce((s, m) => s + m.costUsd, 0);
|
|
5837
|
+
if (total <= 0) continue;
|
|
5838
|
+
risingIntervals += 1;
|
|
5839
|
+
risingPct += gap.deltaPct;
|
|
5840
|
+
risingUsd += total;
|
|
5841
|
+
const rate = gap.deltaPct / total;
|
|
5842
|
+
const dominant = models2.reduce((a, b) => b.costUsd > a.costUsd ? b : a);
|
|
5843
|
+
if (dominant.costUsd + 1e-9 < total * WEIGH_CLEAN_SHARE) continue;
|
|
5844
|
+
cleanIntervals += 1;
|
|
5845
|
+
const key = dominant.unitLabel ?? "";
|
|
5846
|
+
const row = byLabel.get(key) ?? { label: dominant.unitLabel, rates: [] };
|
|
5847
|
+
row.rates.push(rate);
|
|
5848
|
+
byLabel.set(key, row);
|
|
5849
|
+
}
|
|
5850
|
+
const models = [...byLabel.values()].map((row) => {
|
|
5851
|
+
const ok = row.rates.length >= WEIGH_MIN_CLEAN;
|
|
5852
|
+
return {
|
|
5853
|
+
unitLabel: row.label,
|
|
5854
|
+
ppPerUsd: ok ? band(row.rates) : null,
|
|
5855
|
+
cleanIntervals: row.rates.length,
|
|
5856
|
+
status: ok ? "ok" : "unknown"
|
|
5857
|
+
};
|
|
5858
|
+
}).sort((a, b) => {
|
|
5859
|
+
if (a.status !== b.status) return a.status === "ok" ? -1 : 1;
|
|
5860
|
+
return (b.ppPerUsd?.median ?? 0) - (a.ppPerUsd?.median ?? 0);
|
|
5861
|
+
});
|
|
5862
|
+
return {
|
|
5863
|
+
window: "7d",
|
|
5864
|
+
provider: "claude-code",
|
|
5865
|
+
blendedPpPerUsd: risingUsd > 0 ? risingPct / risingUsd : null,
|
|
5866
|
+
risingIntervals,
|
|
5867
|
+
cleanIntervals,
|
|
5868
|
+
models,
|
|
5869
|
+
caveats: CAVEATS
|
|
5870
|
+
};
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
// packages/core/src/otel.ts
|
|
5874
|
+
function attr(key, value) {
|
|
5875
|
+
return { key, value: { stringValue: value } };
|
|
5876
|
+
}
|
|
5877
|
+
function nano(ts) {
|
|
5878
|
+
return `${BigInt(Math.max(0, Math.round(ts))) * 1000000n}`;
|
|
5879
|
+
}
|
|
5880
|
+
function eventsToOtlp(events2, opts = {}) {
|
|
5881
|
+
const costPoints = [];
|
|
5882
|
+
const tokenPoints = [];
|
|
5883
|
+
for (const e of events2) {
|
|
5884
|
+
const attributes = [
|
|
5885
|
+
attr("provider", e.provider),
|
|
5886
|
+
attr("unit_label", e.unitLabel ?? "unknown"),
|
|
5887
|
+
attr("scope_hash", e.scopeHash)
|
|
5888
|
+
];
|
|
5889
|
+
if (opts.includeLabels && e.label) attributes.push(attr("project", e.label));
|
|
5890
|
+
const t = nano(e.ts);
|
|
5891
|
+
if (e.costUsd !== null && Number.isFinite(e.costUsd)) {
|
|
5892
|
+
costPoints.push({ asDouble: e.costUsd, timeUnixNano: t, attributes });
|
|
5893
|
+
}
|
|
5894
|
+
tokenPoints.push({ asDouble: e.tokens, timeUnixNano: t, attributes });
|
|
5895
|
+
}
|
|
5896
|
+
return {
|
|
5897
|
+
resourceMetrics: [{
|
|
5898
|
+
resource: {
|
|
5899
|
+
attributes: [attr("service.name", opts.serviceName ?? "nomnomtokens")]
|
|
5900
|
+
},
|
|
5901
|
+
scopeMetrics: [{
|
|
5902
|
+
scope: { name: "nomnomtokens" },
|
|
5903
|
+
metrics: [
|
|
5904
|
+
{
|
|
5905
|
+
name: "nnt.cost_usd",
|
|
5906
|
+
unit: "USD",
|
|
5907
|
+
sum: {
|
|
5908
|
+
dataPoints: costPoints,
|
|
5909
|
+
aggregationTemporality: 2,
|
|
5910
|
+
isMonotonic: true
|
|
5911
|
+
}
|
|
5912
|
+
},
|
|
5913
|
+
{
|
|
5914
|
+
name: "nnt.tokens",
|
|
5915
|
+
unit: "1",
|
|
5916
|
+
sum: {
|
|
5917
|
+
dataPoints: tokenPoints,
|
|
5918
|
+
aggregationTemporality: 2,
|
|
5919
|
+
isMonotonic: true
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
]
|
|
5923
|
+
}]
|
|
5924
|
+
}]
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5456
5928
|
// packages/db/src/repo.ts
|
|
5457
5929
|
var Repo = class {
|
|
5458
5930
|
constructor(sqlite) {
|
|
@@ -5474,11 +5946,13 @@ var Repo = class {
|
|
|
5474
5946
|
WHERE excluded.qty_total >= events.qty_total
|
|
5475
5947
|
`);
|
|
5476
5948
|
this.upsertScope = sqlite.prepare(`
|
|
5477
|
-
INSERT INTO scopes (scope_hash, label, provider, last_seen)
|
|
5478
|
-
VALUES (@scopeHash, @label, @provider, @lastSeen)
|
|
5949
|
+
INSERT INTO scopes (scope_hash, label, provider, last_seen, client, label_locked)
|
|
5950
|
+
VALUES (@scopeHash, @label, @provider, @lastSeen, @client, @labelLocked)
|
|
5479
5951
|
ON CONFLICT(scope_hash) DO UPDATE SET
|
|
5480
|
-
label = excluded.label,
|
|
5481
|
-
last_seen = MAX(scopes.last_seen, excluded.last_seen)
|
|
5952
|
+
label = CASE WHEN scopes.label_locked = 1 THEN scopes.label ELSE excluded.label END,
|
|
5953
|
+
last_seen = MAX(scopes.last_seen, excluded.last_seen),
|
|
5954
|
+
client = COALESCE(excluded.client, scopes.client),
|
|
5955
|
+
label_locked = MAX(scopes.label_locked, excluded.label_locked)
|
|
5482
5956
|
`);
|
|
5483
5957
|
this.insertLimit = sqlite.prepare(`
|
|
5484
5958
|
INSERT INTO limits (ts, provider, window, used_pct, resets_at)
|
|
@@ -5537,7 +6011,9 @@ var Repo = class {
|
|
|
5537
6011
|
scopeHash: rec.scopeHash,
|
|
5538
6012
|
label: rec.label,
|
|
5539
6013
|
provider: rec.provider,
|
|
5540
|
-
lastSeen: Date.now()
|
|
6014
|
+
lastSeen: rec.lastSeen ?? Date.now(),
|
|
6015
|
+
client: rec.client ?? null,
|
|
6016
|
+
labelLocked: rec.labelLocked ? 1 : 0
|
|
5541
6017
|
});
|
|
5542
6018
|
result.scopes += 1;
|
|
5543
6019
|
}
|
|
@@ -5569,6 +6045,24 @@ var Repo = class {
|
|
|
5569
6045
|
eventCount() {
|
|
5570
6046
|
return this.sqlite.prepare("SELECT COUNT(*) AS c FROM events").pluck().get();
|
|
5571
6047
|
}
|
|
6048
|
+
/**
|
|
6049
|
+
* Rename a project or attach a client tag. Locks the label so a later scan
|
|
6050
|
+
* cannot revert it to the folder basename.
|
|
6051
|
+
*/
|
|
6052
|
+
updateScope(scopeHash2, patch) {
|
|
6053
|
+
const row = this.sqlite.prepare(
|
|
6054
|
+
"SELECT label, client FROM scopes WHERE scope_hash = ?"
|
|
6055
|
+
).get(scopeHash2);
|
|
6056
|
+
if (!row) return false;
|
|
6057
|
+
const label = patch.label !== void 0 ? patch.label.trim() || row.label : row.label;
|
|
6058
|
+
const client = patch.client !== void 0 ? patch.client?.trim() || null : row.client;
|
|
6059
|
+
this.sqlite.prepare(`
|
|
6060
|
+
UPDATE scopes
|
|
6061
|
+
SET label = @label, client = @client, label_locked = 1
|
|
6062
|
+
WHERE scope_hash = @scopeHash
|
|
6063
|
+
`).run({ scopeHash: scopeHash2, label, client });
|
|
6064
|
+
return true;
|
|
6065
|
+
}
|
|
5572
6066
|
/**
|
|
5573
6067
|
* Collapse Cursor Auto-mode placeholders (`default`, `default,default,…`)
|
|
5574
6068
|
* into a null unit_label so the UI shows a single "unknown" bucket.
|
|
@@ -5630,6 +6124,12 @@ function buildWhere(f, prefix = "e") {
|
|
|
5630
6124
|
});
|
|
5631
6125
|
clauses.push(`${prefix}.scope_hash IN (${names.join(", ")})`);
|
|
5632
6126
|
}
|
|
6127
|
+
if (f.client) {
|
|
6128
|
+
clauses.push(
|
|
6129
|
+
`${prefix}.scope_hash IN (SELECT scope_hash FROM scopes WHERE client = @client)`
|
|
6130
|
+
);
|
|
6131
|
+
params.client = f.client;
|
|
6132
|
+
}
|
|
5633
6133
|
return { sql: clauses.length ? `WHERE ${clauses.join(" AND ")}` : "", params };
|
|
5634
6134
|
}
|
|
5635
6135
|
var BUCKET = {
|
|
@@ -5695,7 +6195,7 @@ var Queries = class {
|
|
|
5695
6195
|
byScope(f = {}) {
|
|
5696
6196
|
const w = buildWhere(f);
|
|
5697
6197
|
return this.sqlite.prepare(`
|
|
5698
|
-
SELECT e.scope_hash AS scopeHash, s.label AS label, s.last_seen AS lastSeen, ${TOTALS}
|
|
6198
|
+
SELECT e.scope_hash AS scopeHash, s.label AS label, s.client AS client, s.last_seen AS lastSeen, ${TOTALS}
|
|
5699
6199
|
FROM events e
|
|
5700
6200
|
LEFT JOIN scopes s ON s.scope_hash = e.scope_hash
|
|
5701
6201
|
${w.sql}
|
|
@@ -5784,11 +6284,89 @@ var Queries = class {
|
|
|
5784
6284
|
limitWindows() {
|
|
5785
6285
|
return this.sqlite.prepare("SELECT DISTINCT provider, window FROM limits ORDER BY provider, window").all();
|
|
5786
6286
|
}
|
|
6287
|
+
/** Latest snapshot per (provider, window) — statusline Codex headroom. */
|
|
6288
|
+
latestLimits() {
|
|
6289
|
+
return this.sqlite.prepare(`
|
|
6290
|
+
SELECT l.ts, l.provider, l.window, l.used_pct AS usedPct, l.resets_at AS resetsAt
|
|
6291
|
+
FROM limits l
|
|
6292
|
+
INNER JOIN (
|
|
6293
|
+
SELECT provider, window, MAX(ts) AS ts
|
|
6294
|
+
FROM limits
|
|
6295
|
+
GROUP BY provider, window
|
|
6296
|
+
) latest ON latest.provider = l.provider AND latest.window = l.window AND latest.ts = l.ts
|
|
6297
|
+
ORDER BY l.provider, l.window
|
|
6298
|
+
`).all();
|
|
6299
|
+
}
|
|
6300
|
+
coldResumesFor(sessions, limit = 5) {
|
|
6301
|
+
return collectColdResumes(
|
|
6302
|
+
sessions.map((s) => ({
|
|
6303
|
+
sessionId: s.sessionId,
|
|
6304
|
+
label: s.label,
|
|
6305
|
+
turns: this.sessionEvents(s.sessionId)
|
|
6306
|
+
})),
|
|
6307
|
+
limit
|
|
6308
|
+
);
|
|
6309
|
+
}
|
|
5787
6310
|
scopeLabels() {
|
|
5788
6311
|
return this.sqlite.prepare(`
|
|
5789
|
-
SELECT scope_hash AS scopeHash, label, provider, last_seen AS lastSeen
|
|
6312
|
+
SELECT scope_hash AS scopeHash, label, provider, last_seen AS lastSeen,
|
|
6313
|
+
client, label_locked AS labelLocked
|
|
5790
6314
|
FROM scopes ORDER BY last_seen DESC
|
|
5791
|
-
`).all()
|
|
6315
|
+
`).all().map((row) => {
|
|
6316
|
+
const r = row;
|
|
6317
|
+
return { ...r, labelLocked: r.labelLocked === 1 };
|
|
6318
|
+
});
|
|
6319
|
+
}
|
|
6320
|
+
/**
|
|
6321
|
+
* Spend tagged as a Claude Code sidechain (subagent) turn.
|
|
6322
|
+
*
|
|
6323
|
+
* `taggedEvents` counts rows that have any `meta.sidechain` key — parents omit
|
|
6324
|
+
* it entirely, so zero tagged events means "this corpus has no sidechain
|
|
6325
|
+
* signal" (Cursor/Codex), not "0% subagents".
|
|
6326
|
+
*/
|
|
6327
|
+
sidechainTotals(f = {}) {
|
|
6328
|
+
const w = buildWhere(f);
|
|
6329
|
+
const sidechainClause = `json_extract(e.meta_json, '$.sidechain') = 1`;
|
|
6330
|
+
const where = w.sql ? `${w.sql} AND ${sidechainClause}` : `WHERE ${sidechainClause}`;
|
|
6331
|
+
const totals = this.sqlite.prepare(`SELECT ${TOTALS} FROM events e ${where}`).get(w.params);
|
|
6332
|
+
const tagged = this.sqlite.prepare(`
|
|
6333
|
+
SELECT COUNT(*) AS n
|
|
6334
|
+
FROM events e ${w.sql}${w.sql ? " AND" : "WHERE"}
|
|
6335
|
+
json_extract(e.meta_json, '$.sidechain') IS NOT NULL
|
|
6336
|
+
`).get(w.params);
|
|
6337
|
+
return { ...totals, taggedEvents: tagged.n };
|
|
6338
|
+
}
|
|
6339
|
+
/**
|
|
6340
|
+
* Top sessions by cost, with the sidechain cost share when any turns are tagged.
|
|
6341
|
+
* Used by the audit report — not a general-purpose sessions list.
|
|
6342
|
+
*/
|
|
6343
|
+
sessionsAudit(f = {}, limit = 5) {
|
|
6344
|
+
const w = buildWhere(f);
|
|
6345
|
+
const params = { ...w.params, limit };
|
|
6346
|
+
const rows = this.sqlite.prepare(`
|
|
6347
|
+
SELECT
|
|
6348
|
+
e.session_id AS sessionId,
|
|
6349
|
+
MIN(e.scope_hash) AS scopeHash,
|
|
6350
|
+
COALESCE(SUM(e.cost_usd), 0) AS costUsd,
|
|
6351
|
+
COALESCE(SUM(e.qty_total), 0) AS tokens,
|
|
6352
|
+
COALESCE(SUM(
|
|
6353
|
+
CASE WHEN json_extract(e.meta_json, '$.sidechain') = 1
|
|
6354
|
+
THEN e.cost_usd ELSE 0 END
|
|
6355
|
+
), 0) AS sidechainCostUsd,
|
|
6356
|
+
SUM(
|
|
6357
|
+
CASE WHEN json_extract(e.meta_json, '$.sidechain') IS NOT NULL
|
|
6358
|
+
THEN 1 ELSE 0 END
|
|
6359
|
+
) AS sidechainTagged
|
|
6360
|
+
FROM events e
|
|
6361
|
+
${w.sql}${w.sql ? " AND" : "WHERE"} e.session_id IS NOT NULL
|
|
6362
|
+
GROUP BY e.session_id
|
|
6363
|
+
ORDER BY costUsd DESC
|
|
6364
|
+
LIMIT @limit
|
|
6365
|
+
`).all(params);
|
|
6366
|
+
return rows.map((r) => {
|
|
6367
|
+
const scope = this.sqlite.prepare("SELECT label FROM scopes WHERE scope_hash = ?").pluck().get(r.scopeHash);
|
|
6368
|
+
return { ...r, label: scope ?? null };
|
|
6369
|
+
});
|
|
5792
6370
|
}
|
|
5793
6371
|
/**
|
|
5794
6372
|
* Lines the agent changed while earning the spend in `f`.
|
|
@@ -5827,6 +6405,7 @@ var Queries = class {
|
|
|
5827
6405
|
e.session_id AS sessionId,
|
|
5828
6406
|
e.scope_hash AS scopeHash,
|
|
5829
6407
|
s.label AS project,
|
|
6408
|
+
s.client AS client,
|
|
5830
6409
|
e.unit_label AS model,
|
|
5831
6410
|
e.cost_usd AS costUsd,
|
|
5832
6411
|
e.qty_total AS tokens,
|
|
@@ -5848,6 +6427,7 @@ var Queries = class {
|
|
|
5848
6427
|
sessionId: r.sessionId,
|
|
5849
6428
|
scopeHash: r.scopeHash,
|
|
5850
6429
|
project: r.project,
|
|
6430
|
+
client: r.client,
|
|
5851
6431
|
model: r.model,
|
|
5852
6432
|
costUsd: r.costUsd,
|
|
5853
6433
|
tokens: r.tokens,
|
|
@@ -5860,6 +6440,28 @@ var Queries = class {
|
|
|
5860
6440
|
};
|
|
5861
6441
|
});
|
|
5862
6442
|
}
|
|
6443
|
+
exportSpendEvents(limit = 1e5) {
|
|
6444
|
+
return this.sqlite.prepare(`
|
|
6445
|
+
SELECT id, ts, provider, kind, session_id AS sessionId, scope_hash AS scopeHash,
|
|
6446
|
+
unit_label AS unitLabel, qty_json AS qtyJson, cost_usd AS costUsd,
|
|
6447
|
+
meta_json AS metaJson
|
|
6448
|
+
FROM events ORDER BY ts ASC LIMIT ?
|
|
6449
|
+
`).all(limit).map((row) => {
|
|
6450
|
+
const r = row;
|
|
6451
|
+
return {
|
|
6452
|
+
id: r.id,
|
|
6453
|
+
ts: r.ts,
|
|
6454
|
+
provider: r.provider,
|
|
6455
|
+
kind: r.kind,
|
|
6456
|
+
sessionId: r.sessionId,
|
|
6457
|
+
scopeHash: r.scopeHash,
|
|
6458
|
+
unitLabel: r.unitLabel,
|
|
6459
|
+
qty: JSON.parse(r.qtyJson),
|
|
6460
|
+
costUsd: r.costUsd,
|
|
6461
|
+
meta: JSON.parse(r.metaJson)
|
|
6462
|
+
};
|
|
6463
|
+
});
|
|
6464
|
+
}
|
|
5863
6465
|
};
|
|
5864
6466
|
function defaultPricesPath() {
|
|
5865
6467
|
return join(defaultDataDir(), "prices.json");
|
|
@@ -5952,6 +6554,9 @@ function duration(ms) {
|
|
|
5952
6554
|
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
5953
6555
|
return `${(ms / 1e3).toFixed(1)}s`;
|
|
5954
6556
|
}
|
|
6557
|
+
function pct(n) {
|
|
6558
|
+
return n === null || n === void 0 ? "\u2014" : `${n.toFixed(1)}%`;
|
|
6559
|
+
}
|
|
5955
6560
|
function untilReset(ms, now = Date.now()) {
|
|
5956
6561
|
if (ms === null || ms === void 0 || !Number.isFinite(ms)) return null;
|
|
5957
6562
|
const delta = ms - now;
|
|
@@ -6049,6 +6654,108 @@ function desktopNotify(hits) {
|
|
|
6049
6654
|
}
|
|
6050
6655
|
}
|
|
6051
6656
|
|
|
6657
|
+
// packages/cli/src/commands/audit.ts
|
|
6658
|
+
function rangeFromDays(days, now = Date.now()) {
|
|
6659
|
+
const to = now;
|
|
6660
|
+
const start = new Date(now);
|
|
6661
|
+
start.setHours(0, 0, 0, 0);
|
|
6662
|
+
start.setDate(start.getDate() - (days - 1));
|
|
6663
|
+
return { from: start.getTime(), to };
|
|
6664
|
+
}
|
|
6665
|
+
function formatGap2(ms) {
|
|
6666
|
+
const hours = Math.round(ms / 36e5);
|
|
6667
|
+
if (hours < 1) return `${Math.max(1, Math.round(ms / 6e4))}m`;
|
|
6668
|
+
return `${hours}h`;
|
|
6669
|
+
}
|
|
6670
|
+
function auditCommand(opts = {}) {
|
|
6671
|
+
const days = opts.days && opts.days > 0 ? opts.days : 7;
|
|
6672
|
+
const { from, to } = rangeFromDays(days);
|
|
6673
|
+
const { sqlite, path: dbPath } = openDb(opts.db);
|
|
6674
|
+
const q = new Queries(sqlite);
|
|
6675
|
+
const filters = { from, to, kind: "tokens" };
|
|
6676
|
+
const totals = q.totals(filters);
|
|
6677
|
+
const sidechain = q.sidechainTotals(filters);
|
|
6678
|
+
const models = q.byUnitLabel(filters);
|
|
6679
|
+
const sessions = q.sessionsAudit(filters, 5);
|
|
6680
|
+
const coldResumes = q.coldResumesFor(sessions);
|
|
6681
|
+
sqlite.close();
|
|
6682
|
+
const report = buildAuditReport({
|
|
6683
|
+
range: { from, to },
|
|
6684
|
+
totals: {
|
|
6685
|
+
costUsd: totals.costUsd,
|
|
6686
|
+
tokens: totals.tokens,
|
|
6687
|
+
events: totals.events,
|
|
6688
|
+
qtyIn: totals.qtyIn,
|
|
6689
|
+
qtyCacheRead: totals.qtyCacheRead
|
|
6690
|
+
},
|
|
6691
|
+
sidechain: {
|
|
6692
|
+
taggedEvents: sidechain.taggedEvents,
|
|
6693
|
+
costUsd: sidechain.costUsd,
|
|
6694
|
+
tokens: sidechain.tokens
|
|
6695
|
+
},
|
|
6696
|
+
models: models.map((m) => ({ unitLabel: m.unitLabel, costUsd: m.costUsd })),
|
|
6697
|
+
sessions: sessions.map((s) => ({
|
|
6698
|
+
sessionId: s.sessionId,
|
|
6699
|
+
label: s.label,
|
|
6700
|
+
costUsd: s.costUsd,
|
|
6701
|
+
sidechainCostUsd: s.sidechainCostUsd,
|
|
6702
|
+
sidechainTagged: s.sidechainTagged
|
|
6703
|
+
})),
|
|
6704
|
+
coldResumes
|
|
6705
|
+
});
|
|
6706
|
+
if (opts.json) {
|
|
6707
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
6708
|
+
`);
|
|
6709
|
+
return;
|
|
6710
|
+
}
|
|
6711
|
+
console.log(c.bold(`Audit \xB7 last ${days}d`));
|
|
6712
|
+
console.log(c.dim(`from ${dbPath}`));
|
|
6713
|
+
console.log();
|
|
6714
|
+
console.log(` cost ${usd(report.totals.costUsd)}`);
|
|
6715
|
+
console.log(` tokens ${compactNumber(report.totals.tokens)}`);
|
|
6716
|
+
console.log(` events ${report.totals.events}`);
|
|
6717
|
+
console.log(` cache hit ${pct(report.cache.hitRate !== null ? report.cache.hitRate * 100 : null)}`);
|
|
6718
|
+
console.log(
|
|
6719
|
+
` 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)`}`
|
|
6720
|
+
);
|
|
6721
|
+
console.log();
|
|
6722
|
+
if (report.models.length) {
|
|
6723
|
+
console.log(c.bold("Models"));
|
|
6724
|
+
for (const m of report.models.slice(0, 5)) {
|
|
6725
|
+
const label = (m.unitLabel ?? "unknown").padEnd(28);
|
|
6726
|
+
console.log(` ${label} ${pct(m.share * 100).padStart(5)} ${usd(m.costUsd)}`);
|
|
6727
|
+
}
|
|
6728
|
+
console.log();
|
|
6729
|
+
}
|
|
6730
|
+
if (report.topSessions.length) {
|
|
6731
|
+
console.log(c.bold("Top sessions"));
|
|
6732
|
+
for (const s of report.topSessions) {
|
|
6733
|
+
const name = (s.label ?? s.sessionId.slice(0, 12)).padEnd(28);
|
|
6734
|
+
const sc = s.sidechainShare !== null ? ` \xB7 sub ${pct(s.sidechainShare * 100)}` : "";
|
|
6735
|
+
console.log(` ${name} ${usd(s.costUsd)}${sc}`);
|
|
6736
|
+
}
|
|
6737
|
+
console.log();
|
|
6738
|
+
}
|
|
6739
|
+
if (report.coldResumes.length) {
|
|
6740
|
+
console.log(c.bold("Cold resumes"));
|
|
6741
|
+
for (const hit of report.coldResumes) {
|
|
6742
|
+
const name = (hit.label ?? hit.sessionId.slice(0, 12)).padEnd(28);
|
|
6743
|
+
console.log(` ${name} ${usd(hit.costUsd)} after ${formatGap2(hit.gapMs)} idle`);
|
|
6744
|
+
}
|
|
6745
|
+
console.log();
|
|
6746
|
+
}
|
|
6747
|
+
if (report.tips.length) {
|
|
6748
|
+
console.log(c.bold("Tips"));
|
|
6749
|
+
for (const tip of report.tips) {
|
|
6750
|
+
console.log(` \u2022 ${tip}`);
|
|
6751
|
+
}
|
|
6752
|
+
console.log();
|
|
6753
|
+
}
|
|
6754
|
+
if (!opts.quiet && report.totals.events === 0) {
|
|
6755
|
+
console.log(c.dim("No token events in this range. Run `nnt scan`, then try again."));
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6052
6759
|
// packages/adapters/src/claude-code/jsonl.ts
|
|
6053
6760
|
function num(v) {
|
|
6054
6761
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
@@ -7126,6 +7833,57 @@ async function detectAdapters(opts = {}) {
|
|
|
7126
7833
|
}
|
|
7127
7834
|
|
|
7128
7835
|
// packages/cli/src/commands/doctor.ts
|
|
7836
|
+
var DEFAULT_CLEANUP_DAYS = 30;
|
|
7837
|
+
function readCleanupPeriodDays(settingsPath2) {
|
|
7838
|
+
if (!existsSync(settingsPath2)) return DEFAULT_CLEANUP_DAYS;
|
|
7839
|
+
try {
|
|
7840
|
+
const parsed = JSON.parse(readFileSync(settingsPath2, "utf8"));
|
|
7841
|
+
const n = parsed.cleanupPeriodDays;
|
|
7842
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : DEFAULT_CLEANUP_DAYS;
|
|
7843
|
+
} catch {
|
|
7844
|
+
return DEFAULT_CLEANUP_DAYS;
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
function listJsonlFiles(root) {
|
|
7848
|
+
if (!existsSync(root)) return [];
|
|
7849
|
+
const out = [];
|
|
7850
|
+
const stack = [root];
|
|
7851
|
+
while (stack.length > 0) {
|
|
7852
|
+
const dir = stack.pop();
|
|
7853
|
+
let entries;
|
|
7854
|
+
try {
|
|
7855
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
7856
|
+
} catch {
|
|
7857
|
+
continue;
|
|
7858
|
+
}
|
|
7859
|
+
for (const entry of entries) {
|
|
7860
|
+
const path = join(dir, entry.name);
|
|
7861
|
+
if (entry.isDirectory()) stack.push(path);
|
|
7862
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(path);
|
|
7863
|
+
}
|
|
7864
|
+
}
|
|
7865
|
+
return out;
|
|
7866
|
+
}
|
|
7867
|
+
function oldestMtime(paths) {
|
|
7868
|
+
let min = null;
|
|
7869
|
+
for (const path of paths) {
|
|
7870
|
+
try {
|
|
7871
|
+
const t = statSync(path).mtimeMs;
|
|
7872
|
+
if (min === null || t < min) min = t;
|
|
7873
|
+
} catch {
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
return min;
|
|
7877
|
+
}
|
|
7878
|
+
function transcriptRetentionWarning(opts) {
|
|
7879
|
+
if (opts.storeFirst === null) return null;
|
|
7880
|
+
const now = opts.now ?? Date.now();
|
|
7881
|
+
const cutoff = now - opts.cleanupDays * 864e5;
|
|
7882
|
+
const storeOlderThanRetention = opts.storeFirst < cutoff;
|
|
7883
|
+
const transcriptsAlreadyGone = opts.oldestJsonlMtime !== null && opts.oldestJsonlMtime > opts.storeFirst + 864e5;
|
|
7884
|
+
if (!storeOlderThanRetention && !transcriptsAlreadyGone) return null;
|
|
7885
|
+
return `store keeps history from ${new Date(opts.storeFirst).toISOString().slice(0, 10)}; transcripts will not (cleanupPeriodDays=${opts.cleanupDays})`;
|
|
7886
|
+
}
|
|
7129
7887
|
function check(ok, label, detail) {
|
|
7130
7888
|
const mark = ok ? c.green("\u2714") : c.yellow("\u2717");
|
|
7131
7889
|
console.log(`${mark} ${label}${detail ? c.dim(` ${detail}`) : ""}`);
|
|
@@ -7134,7 +7892,7 @@ async function doctor(opts = {}) {
|
|
|
7134
7892
|
console.log(c.bold("nomnomtokens doctor"));
|
|
7135
7893
|
console.log();
|
|
7136
7894
|
console.log(c.bold("Sources"));
|
|
7137
|
-
const projects = claudeProjectsDir();
|
|
7895
|
+
const projects = opts.claudeProjects ?? claudeProjectsDir();
|
|
7138
7896
|
check(existsSync(projects), "Claude Code transcripts", projects);
|
|
7139
7897
|
const cursorDb = cursorStateDbPath();
|
|
7140
7898
|
check(existsSync(cursorDb), "Cursor state database", cursorDb);
|
|
@@ -7144,7 +7902,7 @@ async function doctor(opts = {}) {
|
|
|
7144
7902
|
check(adapters.length > 0, `${adapters.length} adapter(s) detected`, adapters.map((a) => a.name).join(", ") || "none");
|
|
7145
7903
|
console.log();
|
|
7146
7904
|
console.log(c.bold("Status line"));
|
|
7147
|
-
const settingsFile = join(homedir(), ".claude", "settings.json");
|
|
7905
|
+
const settingsFile = opts.claudeSettings ?? join(homedir(), ".claude", "settings.json");
|
|
7148
7906
|
let statusLineCommand;
|
|
7149
7907
|
if (existsSync(settingsFile)) {
|
|
7150
7908
|
try {
|
|
@@ -7176,6 +7934,14 @@ async function doctor(opts = {}) {
|
|
|
7176
7934
|
check(false, `${totals.unpricedEvents} events could not be priced`, "unknown model id \u2014 see packages/core/src/pricing.ts");
|
|
7177
7935
|
}
|
|
7178
7936
|
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");
|
|
7937
|
+
const cleanupDays = readCleanupPeriodDays(settingsFile);
|
|
7938
|
+
const jsonl = listJsonlFiles(projects);
|
|
7939
|
+
const warning = transcriptRetentionWarning({
|
|
7940
|
+
storeFirst: bounds.first,
|
|
7941
|
+
oldestJsonlMtime: oldestMtime(jsonl),
|
|
7942
|
+
cleanupDays
|
|
7943
|
+
});
|
|
7944
|
+
check(!warning, "transcript retention", warning ?? `cleanupPeriodDays ${cleanupDays}`);
|
|
7179
7945
|
sqlite.close();
|
|
7180
7946
|
}
|
|
7181
7947
|
console.log();
|
|
@@ -7199,23 +7965,48 @@ function rangeFrom(range, now = Date.now()) {
|
|
|
7199
7965
|
}
|
|
7200
7966
|
}
|
|
7201
7967
|
function exportCommand(opts = {}) {
|
|
7202
|
-
const format = opts.format === "json"
|
|
7968
|
+
const format = opts.format === "json" || opts.format === "nnt" ? opts.format : "csv";
|
|
7203
7969
|
const { sqlite, path: dbPath } = openDb(opts.db);
|
|
7204
7970
|
const q = new Queries(sqlite);
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7971
|
+
let body;
|
|
7972
|
+
let count;
|
|
7973
|
+
if (format === "nnt") {
|
|
7974
|
+
const scopes2 = q.scopeLabels();
|
|
7975
|
+
body = formatNntArchive({
|
|
7976
|
+
version: 1,
|
|
7977
|
+
events: q.exportSpendEvents(),
|
|
7978
|
+
limits: q.limitSnapshots(),
|
|
7979
|
+
scopes: scopes2.map((s) => ({
|
|
7980
|
+
scopeHash: s.scopeHash,
|
|
7981
|
+
label: s.label,
|
|
7982
|
+
provider: s.provider,
|
|
7983
|
+
lastSeen: s.lastSeen,
|
|
7984
|
+
client: s.client,
|
|
7985
|
+
labelLocked: s.labelLocked
|
|
7986
|
+
}))
|
|
7987
|
+
});
|
|
7988
|
+
count = q.bounds().events;
|
|
7989
|
+
if (!opts.quiet) {
|
|
7990
|
+
console.error(c.dim("nnt archive includes local project labels. Treat the file as the whole store."));
|
|
7991
|
+
}
|
|
7992
|
+
} else {
|
|
7993
|
+
const from = rangeFrom(opts.range ?? "30d");
|
|
7994
|
+
const rows = q.exportEvents({
|
|
7995
|
+
from,
|
|
7996
|
+
kind: "tokens",
|
|
7997
|
+
provider: opts.provider ? [opts.provider] : void 0,
|
|
7998
|
+
client: opts.client
|
|
7999
|
+
});
|
|
8000
|
+
body = format === "json" ? formatExportJson(rows) : formatExportCsv(rows);
|
|
8001
|
+
count = rows.length;
|
|
8002
|
+
}
|
|
7211
8003
|
sqlite.close();
|
|
7212
|
-
const body = format === "json" ? formatExportJson(rows) : formatExportCsv(rows);
|
|
7213
8004
|
if (opts.out) {
|
|
7214
8005
|
const dest = resolve(opts.out);
|
|
7215
8006
|
writeFileSync(dest, body, "utf8");
|
|
7216
8007
|
if (!opts.quiet) {
|
|
7217
8008
|
console.log(
|
|
7218
|
-
`${c.cyan("export")} ${c.bold(String(
|
|
8009
|
+
`${c.cyan("export")} ${c.bold(String(count))} events \u2192 ${dest}`
|
|
7219
8010
|
);
|
|
7220
8011
|
console.log(c.dim(`from ${dbPath}`));
|
|
7221
8012
|
}
|
|
@@ -7223,7 +8014,7 @@ function exportCommand(opts = {}) {
|
|
|
7223
8014
|
}
|
|
7224
8015
|
process.stdout.write(body);
|
|
7225
8016
|
if (!opts.quiet && process.stdout.isTTY) {
|
|
7226
|
-
console.error(c.dim(`${compactNumber(
|
|
8017
|
+
console.error(c.dim(`${compactNumber(count)} events \xB7 ${format}`));
|
|
7227
8018
|
}
|
|
7228
8019
|
}
|
|
7229
8020
|
async function importCsvCommand(opts) {
|
|
@@ -7274,6 +8065,47 @@ async function importCsvCommand(opts) {
|
|
|
7274
8065
|
console.log(c.dim(`${compactNumber(before + added)} events in ${dbPath}`));
|
|
7275
8066
|
}
|
|
7276
8067
|
}
|
|
8068
|
+
|
|
8069
|
+
// packages/cli/src/commands/import.ts
|
|
8070
|
+
async function importCommand(opts) {
|
|
8071
|
+
const path = resolve(opts.file);
|
|
8072
|
+
let text2;
|
|
8073
|
+
try {
|
|
8074
|
+
text2 = readFileSync(path, "utf8");
|
|
8075
|
+
} catch (err) {
|
|
8076
|
+
console.error(c.red(`Cannot read ${path}`));
|
|
8077
|
+
console.error(c.dim(err instanceof Error ? err.message : String(err)));
|
|
8078
|
+
process.exitCode = 1;
|
|
8079
|
+
return;
|
|
8080
|
+
}
|
|
8081
|
+
if (looksLikeNntArchive(text2)) {
|
|
8082
|
+
let records;
|
|
8083
|
+
try {
|
|
8084
|
+
records = parseNntArchive(text2);
|
|
8085
|
+
} catch (err) {
|
|
8086
|
+
console.error(c.red(err instanceof Error ? err.message : String(err)));
|
|
8087
|
+
process.exitCode = 1;
|
|
8088
|
+
return;
|
|
8089
|
+
}
|
|
8090
|
+
const { sqlite, path: dbPath } = openDb(opts.db);
|
|
8091
|
+
const repo = new Repo(sqlite);
|
|
8092
|
+
const before = repo.eventCount();
|
|
8093
|
+
const result = repo.ingest(records);
|
|
8094
|
+
const added = repo.eventCount() - before;
|
|
8095
|
+
sqlite.close();
|
|
8096
|
+
if (!opts.quiet) {
|
|
8097
|
+
console.log(
|
|
8098
|
+
`${c.cyan("nnt")} ${c.bold(String(added))} new ` + c.dim(
|
|
8099
|
+
`${result.events} events, ${result.duplicates} already known, ${result.limits} limits, ${result.scopes} scopes`
|
|
8100
|
+
)
|
|
8101
|
+
);
|
|
8102
|
+
console.log(c.dim(`${compactNumber(before + added)} events in ${dbPath}`));
|
|
8103
|
+
console.log(c.dim("Archive includes local project labels and client tags. scan_state is not imported."));
|
|
8104
|
+
}
|
|
8105
|
+
return;
|
|
8106
|
+
}
|
|
8107
|
+
await importCsvCommand(opts);
|
|
8108
|
+
}
|
|
7277
8109
|
function settingsPath(override) {
|
|
7278
8110
|
return override ?? join(homedir(), ".claude", "settings.json");
|
|
7279
8111
|
}
|
|
@@ -7330,74 +8162,6 @@ function init(opts = {}) {
|
|
|
7330
8162
|
console.log("Restart Claude Code, then run `nnt serve` to open the dashboard.");
|
|
7331
8163
|
console.log(c.dim("Limits and live updates only start flowing once the hook has fired once."));
|
|
7332
8164
|
}
|
|
7333
|
-
async function loadFromSource(from) {
|
|
7334
|
-
let text2;
|
|
7335
|
-
if (/^https?:\/\//i.test(from)) {
|
|
7336
|
-
const res = await fetch(from);
|
|
7337
|
-
if (!res.ok) throw new Error(`fetch ${from}: HTTP ${res.status}`);
|
|
7338
|
-
text2 = await res.text();
|
|
7339
|
-
} else {
|
|
7340
|
-
text2 = readFileSync(resolve(from), "utf8");
|
|
7341
|
-
}
|
|
7342
|
-
const file = parsePricesFile(text2);
|
|
7343
|
-
return {
|
|
7344
|
-
...file,
|
|
7345
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7346
|
-
source: from
|
|
7347
|
-
};
|
|
7348
|
-
}
|
|
7349
|
-
function pricesShow() {
|
|
7350
|
-
const { file, path } = loadPriceTable();
|
|
7351
|
-
const overrideCount = file ? Object.keys(file.models).length : 0;
|
|
7352
|
-
console.log(c.bold("Price table"));
|
|
7353
|
-
console.log(` file ${existsLabel(path, file !== null)}`);
|
|
7354
|
-
if (file?.updatedAt) console.log(` updated ${file.updatedAt}`);
|
|
7355
|
-
if (file?.source) console.log(` source ${file.source}`);
|
|
7356
|
-
console.log(` bundled ${Object.keys(PRICES).length} models`);
|
|
7357
|
-
console.log(` overrides ${overrideCount} model(s) in file`);
|
|
7358
|
-
console.log();
|
|
7359
|
-
console.log(c.dim("Sample (effective USD / 1M tokens):"));
|
|
7360
|
-
for (const id of ["claude-sonnet-4-5", "claude-opus-4-6", "gpt-5.6", "gemini-3-flash"]) {
|
|
7361
|
-
const key = resolveModelKey(id);
|
|
7362
|
-
const p = key ? file?.models[key] ?? PRICES[key] : null;
|
|
7363
|
-
console.log(` ${id.padEnd(22)} ${formatPrice(p)}`);
|
|
7364
|
-
}
|
|
7365
|
-
console.log();
|
|
7366
|
-
console.log(c.dim("Edit the file, or run `nnt prices refresh` / `nnt prices refresh --from <url|path>`."));
|
|
7367
|
-
}
|
|
7368
|
-
function existsLabel(path, ok) {
|
|
7369
|
-
return ok ? path : `${path} ${c.dim("(missing \u2014 using bundled)")}`;
|
|
7370
|
-
}
|
|
7371
|
-
function formatPrice(p) {
|
|
7372
|
-
if (!p) return c.dim("unpriced");
|
|
7373
|
-
return `in $${p.input} out $${p.output}`;
|
|
7374
|
-
}
|
|
7375
|
-
async function pricesRefresh(opts = {}) {
|
|
7376
|
-
const path = defaultPricesPath();
|
|
7377
|
-
let file;
|
|
7378
|
-
if (opts.from) {
|
|
7379
|
-
try {
|
|
7380
|
-
file = await loadFromSource(opts.from);
|
|
7381
|
-
} catch (err) {
|
|
7382
|
-
console.error(c.red(err instanceof Error ? err.message : String(err)));
|
|
7383
|
-
process.exitCode = 1;
|
|
7384
|
-
return;
|
|
7385
|
-
}
|
|
7386
|
-
file = {
|
|
7387
|
-
...file,
|
|
7388
|
-
models: { ...PRICES, ...file.models }
|
|
7389
|
-
};
|
|
7390
|
-
savePricesFile(file, path);
|
|
7391
|
-
} else {
|
|
7392
|
-
file = writeBundledPrices(path);
|
|
7393
|
-
}
|
|
7394
|
-
if (!opts.quiet) {
|
|
7395
|
-
console.log(
|
|
7396
|
-
`${c.cyan("prices")} wrote ${c.bold(String(Object.keys(file.models).length))} models \u2192 ${path}`
|
|
7397
|
-
);
|
|
7398
|
-
if (file.source) console.log(c.dim(`source ${file.source}`));
|
|
7399
|
-
}
|
|
7400
|
-
}
|
|
7401
8165
|
|
|
7402
8166
|
// packages/cli/src/commands/scan.ts
|
|
7403
8167
|
async function scan(opts = {}) {
|
|
@@ -7478,15 +8242,142 @@ async function scan(opts = {}) {
|
|
|
7478
8242
|
if (opts.watch) {
|
|
7479
8243
|
console.log(c.dim("\nWatching for changes. Ctrl-C to stop."));
|
|
7480
8244
|
const stops = adapters.filter((a) => a.watch).map((a) => a.watch((rec) => repo.ingest([rec])));
|
|
7481
|
-
await new Promise((
|
|
8245
|
+
await new Promise((resolve7) => {
|
|
7482
8246
|
process.on("SIGINT", () => {
|
|
7483
8247
|
stops.forEach((stop) => stop());
|
|
7484
|
-
|
|
8248
|
+
resolve7();
|
|
7485
8249
|
});
|
|
7486
8250
|
});
|
|
7487
8251
|
}
|
|
7488
8252
|
sqlite.close();
|
|
7489
8253
|
}
|
|
8254
|
+
|
|
8255
|
+
// packages/cli/src/commands/otel.ts
|
|
8256
|
+
function otelEvents(q, includeLabels) {
|
|
8257
|
+
const labels = new Map(q.scopeLabels().map((s) => [s.scopeHash, s.label]));
|
|
8258
|
+
return q.exportSpendEvents().map((e) => {
|
|
8259
|
+
const row = {
|
|
8260
|
+
ts: e.ts,
|
|
8261
|
+
provider: e.provider,
|
|
8262
|
+
unitLabel: e.unitLabel,
|
|
8263
|
+
scopeHash: e.scopeHash,
|
|
8264
|
+
costUsd: e.costUsd,
|
|
8265
|
+
tokens: totalTokens(e.qty)
|
|
8266
|
+
};
|
|
8267
|
+
if (includeLabels) row.label = labels.get(e.scopeHash) ?? null;
|
|
8268
|
+
return row;
|
|
8269
|
+
});
|
|
8270
|
+
}
|
|
8271
|
+
async function postOtlp(endpoint, body) {
|
|
8272
|
+
const url = endpoint.replace(/\/$/, "");
|
|
8273
|
+
const target = url.endsWith("/v1/metrics") ? url : `${url}/v1/metrics`;
|
|
8274
|
+
const res = await fetch(target, {
|
|
8275
|
+
method: "POST",
|
|
8276
|
+
headers: { "content-type": "application/json" },
|
|
8277
|
+
body: JSON.stringify(body)
|
|
8278
|
+
});
|
|
8279
|
+
if (!res.ok) {
|
|
8280
|
+
const text2 = await res.text().catch(() => "");
|
|
8281
|
+
throw new Error(`OTLP POST ${res.status} ${text2.slice(0, 200)}`);
|
|
8282
|
+
}
|
|
8283
|
+
}
|
|
8284
|
+
async function otelCommand(opts) {
|
|
8285
|
+
const endpoint = opts.endpoint.trim();
|
|
8286
|
+
if (!endpoint) {
|
|
8287
|
+
console.error(c.red("nnt otel requires --endpoint"));
|
|
8288
|
+
process.exitCode = 1;
|
|
8289
|
+
return;
|
|
8290
|
+
}
|
|
8291
|
+
if (opts.scan || opts.watch) {
|
|
8292
|
+
await scan({ db: opts.db, quiet: true });
|
|
8293
|
+
}
|
|
8294
|
+
const { sqlite, path: dbPath } = openDb(opts.db);
|
|
8295
|
+
const q = new Queries(sqlite);
|
|
8296
|
+
const events2 = otelEvents(q, Boolean(opts.includeLabels));
|
|
8297
|
+
sqlite.close();
|
|
8298
|
+
const body = eventsToOtlp(events2, { includeLabels: opts.includeLabels });
|
|
8299
|
+
try {
|
|
8300
|
+
await postOtlp(endpoint, body);
|
|
8301
|
+
} catch (err) {
|
|
8302
|
+
console.error(c.red(err instanceof Error ? err.message : String(err)));
|
|
8303
|
+
process.exitCode = 1;
|
|
8304
|
+
return;
|
|
8305
|
+
}
|
|
8306
|
+
if (!opts.quiet) {
|
|
8307
|
+
console.log(
|
|
8308
|
+
`${c.cyan("otel")} ${c.bold(String(events2.length))} events \u2192 ${endpoint}`
|
|
8309
|
+
);
|
|
8310
|
+
console.log(c.dim(`from ${dbPath} \xB7 numbers and hashes only`));
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
async function loadFromSource(from) {
|
|
8314
|
+
let text2;
|
|
8315
|
+
if (/^https?:\/\//i.test(from)) {
|
|
8316
|
+
const res = await fetch(from);
|
|
8317
|
+
if (!res.ok) throw new Error(`fetch ${from}: HTTP ${res.status}`);
|
|
8318
|
+
text2 = await res.text();
|
|
8319
|
+
} else {
|
|
8320
|
+
text2 = readFileSync(resolve(from), "utf8");
|
|
8321
|
+
}
|
|
8322
|
+
const file = parsePricesFile(text2);
|
|
8323
|
+
return {
|
|
8324
|
+
...file,
|
|
8325
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8326
|
+
source: from
|
|
8327
|
+
};
|
|
8328
|
+
}
|
|
8329
|
+
function pricesShow() {
|
|
8330
|
+
const { file, path } = loadPriceTable();
|
|
8331
|
+
const overrideCount = file ? Object.keys(file.models).length : 0;
|
|
8332
|
+
console.log(c.bold("Price table"));
|
|
8333
|
+
console.log(` file ${existsLabel(path, file !== null)}`);
|
|
8334
|
+
if (file?.updatedAt) console.log(` updated ${file.updatedAt}`);
|
|
8335
|
+
if (file?.source) console.log(` source ${file.source}`);
|
|
8336
|
+
console.log(` bundled ${Object.keys(PRICES).length} models`);
|
|
8337
|
+
console.log(` overrides ${overrideCount} model(s) in file`);
|
|
8338
|
+
console.log();
|
|
8339
|
+
console.log(c.dim("Sample (effective USD / 1M tokens):"));
|
|
8340
|
+
for (const id of ["claude-sonnet-4-5", "claude-opus-4-6", "gpt-5.6", "gemini-3-flash"]) {
|
|
8341
|
+
const key = resolveModelKey(id);
|
|
8342
|
+
const p = key ? file?.models[key] ?? PRICES[key] : null;
|
|
8343
|
+
console.log(` ${id.padEnd(22)} ${formatPrice(p)}`);
|
|
8344
|
+
}
|
|
8345
|
+
console.log();
|
|
8346
|
+
console.log(c.dim("Edit the file, or run `nnt prices refresh` / `nnt prices refresh --from <url|path>`."));
|
|
8347
|
+
}
|
|
8348
|
+
function existsLabel(path, ok) {
|
|
8349
|
+
return ok ? path : `${path} ${c.dim("(missing \u2014 using bundled)")}`;
|
|
8350
|
+
}
|
|
8351
|
+
function formatPrice(p) {
|
|
8352
|
+
if (!p) return c.dim("unpriced");
|
|
8353
|
+
return `in $${p.input} out $${p.output}`;
|
|
8354
|
+
}
|
|
8355
|
+
async function pricesRefresh(opts = {}) {
|
|
8356
|
+
const path = defaultPricesPath();
|
|
8357
|
+
let file;
|
|
8358
|
+
if (opts.from) {
|
|
8359
|
+
try {
|
|
8360
|
+
file = await loadFromSource(opts.from);
|
|
8361
|
+
} catch (err) {
|
|
8362
|
+
console.error(c.red(err instanceof Error ? err.message : String(err)));
|
|
8363
|
+
process.exitCode = 1;
|
|
8364
|
+
return;
|
|
8365
|
+
}
|
|
8366
|
+
file = {
|
|
8367
|
+
...file,
|
|
8368
|
+
models: { ...PRICES, ...file.models }
|
|
8369
|
+
};
|
|
8370
|
+
savePricesFile(file, path);
|
|
8371
|
+
} else {
|
|
8372
|
+
file = writeBundledPrices(path);
|
|
8373
|
+
}
|
|
8374
|
+
if (!opts.quiet) {
|
|
8375
|
+
console.log(
|
|
8376
|
+
`${c.cyan("prices")} wrote ${c.bold(String(Object.keys(file.models).length))} models \u2192 ${path}`
|
|
8377
|
+
);
|
|
8378
|
+
if (file.source) console.log(c.dim(`source ${file.source}`));
|
|
8379
|
+
}
|
|
8380
|
+
}
|
|
7490
8381
|
var here = dirname(fileURLToPath(import.meta.url));
|
|
7491
8382
|
function findBuiltServer() {
|
|
7492
8383
|
let dir = here;
|
|
@@ -7544,24 +8435,73 @@ async function serve(opts = {}) {
|
|
|
7544
8435
|
});
|
|
7545
8436
|
}
|
|
7546
8437
|
|
|
7547
|
-
// packages/cli/src/
|
|
7548
|
-
var
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
}
|
|
7555
|
-
function limitSegment(label, usedPct, resetsAtSeconds) {
|
|
8438
|
+
// packages/cli/src/statusline-render.ts
|
|
8439
|
+
var CLAUDE_WEEKLY_HINT = 80;
|
|
8440
|
+
var CODEX_HEADROOM = 50;
|
|
8441
|
+
var ESC2 = "\x1B";
|
|
8442
|
+
function paintLabel(label) {
|
|
8443
|
+
if (process.env.NO_COLOR !== void 0) return label;
|
|
8444
|
+
return `${ESC2}[36m${label}${ESC2}[0m`;
|
|
8445
|
+
}
|
|
8446
|
+
function limitSegment(label, usedPct, resetsAtSeconds, now) {
|
|
7556
8447
|
const resetsAt = typeof resetsAtSeconds === "number" && Number.isFinite(resetsAtSeconds) ? resetsAtSeconds * 1e3 : null;
|
|
7557
|
-
const left = untilReset(resetsAt);
|
|
7558
|
-
|
|
8448
|
+
const left = untilReset(resetsAt, now);
|
|
8449
|
+
const head = `${paintLabel(label)} ${Math.round(usedPct)}%`;
|
|
8450
|
+
return left ? `${head} ${left}` : head;
|
|
8451
|
+
}
|
|
8452
|
+
function resolveWindow(livePct, liveResetsAtSeconds, cached, now) {
|
|
8453
|
+
if (livePct !== null) {
|
|
8454
|
+
return { pct: livePct, resetsAtSeconds: liveResetsAtSeconds };
|
|
8455
|
+
}
|
|
8456
|
+
if (cached && cached.resetsAt !== null && cached.resetsAt > now) {
|
|
8457
|
+
return { pct: cached.usedPct, resetsAtSeconds: cached.resetsAt / 1e3 };
|
|
8458
|
+
}
|
|
8459
|
+
return { pct: null };
|
|
8460
|
+
}
|
|
8461
|
+
function buildStatuslineParts(view) {
|
|
8462
|
+
const now = view.now;
|
|
8463
|
+
const five = view.fiveHour;
|
|
8464
|
+
const seven = view.sevenDay;
|
|
8465
|
+
const segments = [];
|
|
8466
|
+
if (seven !== null) segments.push(limitSegment("7d", seven, view.sevenDayResetsAt, now));
|
|
8467
|
+
if (five !== null) segments.push(limitSegment("5h", five, view.fiveHourResetsAt, now));
|
|
8468
|
+
if (view.ctx !== null) segments.push(`${paintLabel("ctx")}. ${Math.round(view.ctx)}%`);
|
|
8469
|
+
const model = view.model?.trim();
|
|
8470
|
+
if (model) segments.push(paintLabel(model));
|
|
8471
|
+
const codex = view.codexSevenDay;
|
|
8472
|
+
if (seven !== null && seven >= CLAUDE_WEEKLY_HINT && typeof codex === "number" && Number.isFinite(codex) && codex <= CODEX_HEADROOM) {
|
|
8473
|
+
segments.push(`${paintLabel("codex")}: ${Math.round(codex)}%`);
|
|
8474
|
+
}
|
|
8475
|
+
return segments.length === 0 ? "Limits:" : `Limits: ${segments.join(" | ")}`;
|
|
7559
8476
|
}
|
|
8477
|
+
|
|
8478
|
+
// packages/cli/src/commands/statusline.ts
|
|
8479
|
+
var CODEX_FRESH_MS = 24 * 36e5;
|
|
7560
8480
|
async function readStdin() {
|
|
7561
8481
|
const chunks = [];
|
|
7562
8482
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7563
8483
|
return Buffer.concat(chunks).toString("utf8");
|
|
7564
8484
|
}
|
|
8485
|
+
function readStoredLimits(db, now = Date.now()) {
|
|
8486
|
+
const empty = { codexSevenDay: null, claudeFive: null, claudeSeven: null };
|
|
8487
|
+
try {
|
|
8488
|
+
const { sqlite } = openDb(db);
|
|
8489
|
+
const latest = new Queries(sqlite).latestLimits();
|
|
8490
|
+
sqlite.close();
|
|
8491
|
+
const claude = (window) => {
|
|
8492
|
+
const row = latest.find((l) => l.provider === "claude-code" && l.window === window);
|
|
8493
|
+
return row ? { usedPct: row.usedPct, resetsAt: row.resetsAt } : null;
|
|
8494
|
+
};
|
|
8495
|
+
const codex = latest.find((l) => l.provider === "codex" && l.window === "7d");
|
|
8496
|
+
return {
|
|
8497
|
+
codexSevenDay: codex && now - codex.ts <= CODEX_FRESH_MS ? codex.usedPct : null,
|
|
8498
|
+
claudeFive: claude("5h"),
|
|
8499
|
+
claudeSeven: claude("7d")
|
|
8500
|
+
};
|
|
8501
|
+
} catch {
|
|
8502
|
+
return empty;
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
7565
8505
|
async function statusline(opts = {}) {
|
|
7566
8506
|
let payload;
|
|
7567
8507
|
try {
|
|
@@ -7571,19 +8511,33 @@ async function statusline(opts = {}) {
|
|
|
7571
8511
|
return;
|
|
7572
8512
|
}
|
|
7573
8513
|
const limits2 = payload.rate_limits;
|
|
7574
|
-
const
|
|
7575
|
-
const
|
|
7576
|
-
const
|
|
8514
|
+
const now = Date.now();
|
|
8515
|
+
const stored = readStoredLimits(opts.db, now);
|
|
8516
|
+
const five = resolveWindow(
|
|
8517
|
+
limits2?.five_hour?.used_percentage ?? null,
|
|
8518
|
+
limits2?.five_hour?.resets_at,
|
|
8519
|
+
stored.claudeFive,
|
|
8520
|
+
now
|
|
8521
|
+
);
|
|
8522
|
+
const seven = resolveWindow(
|
|
8523
|
+
limits2?.seven_day?.used_percentage ?? null,
|
|
8524
|
+
limits2?.seven_day?.resets_at,
|
|
8525
|
+
stored.claudeSeven,
|
|
8526
|
+
now
|
|
8527
|
+
);
|
|
7577
8528
|
const ctx = payload.context_window?.used_percentage ?? null;
|
|
7578
|
-
const
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
8529
|
+
const model = payload.model?.display_name?.trim() || payload.model?.id?.trim() || null;
|
|
8530
|
+
const line = buildStatuslineParts({
|
|
8531
|
+
fiveHour: five.pct,
|
|
8532
|
+
sevenDay: seven.pct,
|
|
8533
|
+
fiveHourResetsAt: five.resetsAtSeconds,
|
|
8534
|
+
sevenDayResetsAt: seven.resetsAtSeconds,
|
|
8535
|
+
ctx,
|
|
8536
|
+
model,
|
|
8537
|
+
codexSevenDay: stored.codexSevenDay,
|
|
8538
|
+
now
|
|
8539
|
+
});
|
|
8540
|
+
process.stdout.write(`${line}
|
|
7587
8541
|
`);
|
|
7588
8542
|
if (opts.dryRun) return;
|
|
7589
8543
|
try {
|
|
@@ -7595,6 +8549,94 @@ async function statusline(opts = {}) {
|
|
|
7595
8549
|
}
|
|
7596
8550
|
}
|
|
7597
8551
|
|
|
8552
|
+
// packages/cli/src/commands/verdict.ts
|
|
8553
|
+
function verdictCommand(opts = {}) {
|
|
8554
|
+
const now = Date.now();
|
|
8555
|
+
const weekFrom = now - 7 * 24 * 36e5;
|
|
8556
|
+
const { sqlite, path: dbPath } = openDb(opts.db);
|
|
8557
|
+
const q = new Queries(sqlite);
|
|
8558
|
+
const weekly = q.limitSnapshots("claude-code", "7d", weekFrom);
|
|
8559
|
+
const fiveHour = q.limitSnapshots("claude-code", "5h", weekFrom);
|
|
8560
|
+
const weeklyCostUsd = q.totals({ from: weekFrom, to: now, kind: "tokens", provider: ["claude-code"] }).costUsd;
|
|
8561
|
+
sqlite.close();
|
|
8562
|
+
const report = buildVerdict({ weekly, fiveHour, weeklyCostUsd, now });
|
|
8563
|
+
if (opts.json) {
|
|
8564
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
8565
|
+
`);
|
|
8566
|
+
return;
|
|
8567
|
+
}
|
|
8568
|
+
console.log(c.bold("Verdict"));
|
|
8569
|
+
console.log(c.dim(`from ${dbPath}`));
|
|
8570
|
+
console.log();
|
|
8571
|
+
console.log(` recommended ${c.bold(report.recommended)}`);
|
|
8572
|
+
console.log(` weekly bound ${report.weeklyBound ? "yes" : "no"}`);
|
|
8573
|
+
console.log(` 7d used ${pct(report.weeklyUsedPct)}`);
|
|
8574
|
+
console.log(` 5h used ${pct(report.fiveHourUsedPct)}`);
|
|
8575
|
+
console.log(` week $ ${usd(report.weeklyCostUsd)} ${c.dim("(list price, this machine)")}`);
|
|
8576
|
+
console.log();
|
|
8577
|
+
console.log(c.bold("Two Max 5x vs one 20x"));
|
|
8578
|
+
console.log(` ${report.two5xVs20x.two5xWins ? c.yellow("two 5x wins") : "20x wins the 5h burst"}`);
|
|
8579
|
+
console.log(` ${c.dim(report.two5xVs20x.reason)}`);
|
|
8580
|
+
console.log();
|
|
8581
|
+
console.log(c.bold("Caveats"));
|
|
8582
|
+
for (const line of report.caveats) {
|
|
8583
|
+
console.log(` \u2022 ${line}`);
|
|
8584
|
+
}
|
|
8585
|
+
}
|
|
8586
|
+
|
|
8587
|
+
// packages/cli/src/commands/weigh.ts
|
|
8588
|
+
function weighCommand(opts = {}) {
|
|
8589
|
+
const { sqlite, path: dbPath } = openDb(opts.db);
|
|
8590
|
+
const q = new Queries(sqlite);
|
|
8591
|
+
const snaps = q.limitSnapshots("claude-code", "7d");
|
|
8592
|
+
const gaps = pairRisingSnapshots(snaps).map((p) => ({
|
|
8593
|
+
deltaPct: p.deltaPct,
|
|
8594
|
+
models: q.byUnitLabel({
|
|
8595
|
+
from: p.from,
|
|
8596
|
+
to: p.to,
|
|
8597
|
+
provider: ["claude-code"],
|
|
8598
|
+
kind: "tokens"
|
|
8599
|
+
}).filter((row) => row.costUsd > 0).map((row) => ({ unitLabel: row.unitLabel, costUsd: row.costUsd }))
|
|
8600
|
+
}));
|
|
8601
|
+
sqlite.close();
|
|
8602
|
+
const report = buildWeigh(gaps);
|
|
8603
|
+
if (opts.json) {
|
|
8604
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
8605
|
+
`);
|
|
8606
|
+
return;
|
|
8607
|
+
}
|
|
8608
|
+
printWeigh(report, dbPath);
|
|
8609
|
+
}
|
|
8610
|
+
function printWeigh(report, dbPath) {
|
|
8611
|
+
console.log(c.bold("Weigh"));
|
|
8612
|
+
console.log(c.dim(`from ${dbPath}`));
|
|
8613
|
+
console.log();
|
|
8614
|
+
console.log(` window ${report.provider} ${report.window}`);
|
|
8615
|
+
console.log(` blended ${pp(report.blendedPpPerUsd)} ${c.dim("per list-price dollar, all rising gaps")}`);
|
|
8616
|
+
console.log(` gaps ${report.cleanIntervals} clean / ${report.risingIntervals} rising`);
|
|
8617
|
+
console.log();
|
|
8618
|
+
console.log(c.bold("Models"));
|
|
8619
|
+
if (report.models.length === 0) {
|
|
8620
|
+
console.log(c.dim(" no priced gaps in the current 7d window \u2014 run `nnt init` so the status line records weekly fill"));
|
|
8621
|
+
}
|
|
8622
|
+
for (const model of report.models) console.log(modelLine(model));
|
|
8623
|
+
console.log();
|
|
8624
|
+
console.log(c.bold("Caveats"));
|
|
8625
|
+
for (const line of report.caveats) console.log(` \u2022 ${line}`);
|
|
8626
|
+
}
|
|
8627
|
+
function modelLine(model) {
|
|
8628
|
+
const name = model.unitLabel ?? "(unlabeled)";
|
|
8629
|
+
if (model.status === "unknown" || !model.ppPerUsd) {
|
|
8630
|
+
return ` ${name} ${c.yellow("unknown")} ${c.dim(`need 3 clean gaps, have ${model.cleanIntervals}`)}`;
|
|
8631
|
+
}
|
|
8632
|
+
const { p25, median: median2, p75 } = model.ppPerUsd;
|
|
8633
|
+
return ` ${name} ${pp(median2)} ${c.dim(`p25 ${pp(p25)} p75 ${pp(p75)} ${model.cleanIntervals} gaps`)}`;
|
|
8634
|
+
}
|
|
8635
|
+
function pp(n) {
|
|
8636
|
+
if (n === null) return "\u2014";
|
|
8637
|
+
return `${n.toFixed(2)} pp/$`;
|
|
8638
|
+
}
|
|
8639
|
+
|
|
7598
8640
|
// packages/cli/src/index.ts
|
|
7599
8641
|
function packageVersion() {
|
|
7600
8642
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -7611,7 +8653,7 @@ function packageVersion() {
|
|
|
7611
8653
|
return "0.0.0";
|
|
7612
8654
|
}
|
|
7613
8655
|
var program = new Command();
|
|
7614
|
-
program.name("nnt").description("nom nom nom \u2014 your
|
|
8656
|
+
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)");
|
|
7615
8657
|
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) => {
|
|
7616
8658
|
await scan({ ...opts, db: program.opts().db });
|
|
7617
8659
|
});
|
|
@@ -7632,8 +8674,8 @@ program.command("statusline").description("read Claude Code session JSON on stdi
|
|
|
7632
8674
|
program.command("doctor").description("check sources, hook wiring and the local store").action(async () => {
|
|
7633
8675
|
await doctor({ db: program.opts().db });
|
|
7634
8676
|
});
|
|
7635
|
-
program.command("import <file>").description("import a
|
|
7636
|
-
await
|
|
8677
|
+
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) => {
|
|
8678
|
+
await importCommand({
|
|
7637
8679
|
file,
|
|
7638
8680
|
provider: opts.provider,
|
|
7639
8681
|
kind: opts.kind,
|
|
@@ -7641,14 +8683,15 @@ program.command("import <file>").description("import a CSV billing export or spr
|
|
|
7641
8683
|
db: program.opts().db
|
|
7642
8684
|
});
|
|
7643
8685
|
});
|
|
7644
|
-
program.command("export").description("write filtered events as CSV
|
|
7645
|
-
const format = opts.format === "json"
|
|
8686
|
+
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) => {
|
|
8687
|
+
const format = opts.format === "json" || opts.format === "nnt" ? opts.format : "csv";
|
|
7646
8688
|
const range = ["24h", "7d", "30d", "90d", "all"].includes(opts.range) ? opts.range : "30d";
|
|
7647
8689
|
exportCommand({
|
|
7648
8690
|
format,
|
|
7649
8691
|
out: opts.out,
|
|
7650
8692
|
range,
|
|
7651
8693
|
provider: opts.provider,
|
|
8694
|
+
client: opts.client,
|
|
7652
8695
|
quiet: opts.quiet,
|
|
7653
8696
|
db: program.opts().db
|
|
7654
8697
|
});
|
|
@@ -7670,6 +8713,37 @@ alerts.command("check").description("evaluate thresholds; exit 2 if any are firi
|
|
|
7670
8713
|
db: program.opts().db
|
|
7671
8714
|
});
|
|
7672
8715
|
});
|
|
8716
|
+
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) => {
|
|
8717
|
+
auditCommand({
|
|
8718
|
+
days: opts.days,
|
|
8719
|
+
json: opts.json,
|
|
8720
|
+
quiet: opts.quiet,
|
|
8721
|
+
db: program.opts().db
|
|
8722
|
+
});
|
|
8723
|
+
});
|
|
8724
|
+
program.command("verdict").description("which Claude plan your weekly fill actually needs (Pro / 5x / 20x)").option("--json", "print the verdict as JSON").action((opts) => {
|
|
8725
|
+
verdictCommand({
|
|
8726
|
+
json: opts.json,
|
|
8727
|
+
db: program.opts().db
|
|
8728
|
+
});
|
|
8729
|
+
});
|
|
8730
|
+
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) => {
|
|
8731
|
+
weighCommand({
|
|
8732
|
+
json: opts.json,
|
|
8733
|
+
db: program.opts().db
|
|
8734
|
+
});
|
|
8735
|
+
});
|
|
8736
|
+
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) => {
|
|
8737
|
+
await otelCommand({
|
|
8738
|
+
endpoint: opts.endpoint,
|
|
8739
|
+
once: opts.once,
|
|
8740
|
+
watch: opts.watch,
|
|
8741
|
+
scan: opts.scan,
|
|
8742
|
+
includeLabels: opts.includeLabels,
|
|
8743
|
+
quiet: opts.quiet,
|
|
8744
|
+
db: program.opts().db
|
|
8745
|
+
});
|
|
8746
|
+
});
|
|
7673
8747
|
var COMMANDS = /* @__PURE__ */ new Set([
|
|
7674
8748
|
"scan",
|
|
7675
8749
|
"serve",
|
|
@@ -7680,6 +8754,10 @@ var COMMANDS = /* @__PURE__ */ new Set([
|
|
|
7680
8754
|
"export",
|
|
7681
8755
|
"prices",
|
|
7682
8756
|
"alerts",
|
|
8757
|
+
"audit",
|
|
8758
|
+
"verdict",
|
|
8759
|
+
"weigh",
|
|
8760
|
+
"otel",
|
|
7683
8761
|
"help"
|
|
7684
8762
|
]);
|
|
7685
8763
|
var META_FLAGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version"]);
|