@swestash/agent-lens 0.10.0 → 0.11.1

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.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync10, writeFileSync as writeFileSync3 } from "fs";
5
- import { join as join12 } from "path";
6
- import { fileURLToPath as fileURLToPath4 } from "url";
4
+ import { existsSync as existsSync10, writeFileSync as writeFileSync3 } from "node:fs";
5
+ import { join as join12 } from "node:path";
6
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
7
7
 
8
8
  // ../../node_modules/.pnpm/cac@6.7.14/node_modules/cac/dist/index.mjs
9
9
  import { EventEmitter } from "events";
@@ -921,6 +921,29 @@ END;
921
921
  `
922
922
  );
923
923
 
924
+ // ../core/dist/sqlite.js
925
+ function runNamed(stmt, row) {
926
+ return stmt.run(row);
927
+ }
928
+ var savepointSeq = 0;
929
+ function transaction(db, fn) {
930
+ return (...args) => {
931
+ const savepoint = db.isTransaction ? `agent_lens_sp_${savepointSeq++}` : "";
932
+ db.exec(savepoint ? `SAVEPOINT ${savepoint}` : "BEGIN");
933
+ try {
934
+ const result = fn(...args);
935
+ db.exec(savepoint ? `RELEASE ${savepoint}` : "COMMIT");
936
+ return result;
937
+ } catch (err) {
938
+ try {
939
+ db.exec(savepoint ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : "ROLLBACK");
940
+ } catch {
941
+ }
942
+ throw err;
943
+ }
944
+ };
945
+ }
946
+
924
947
  // ../core/dist/security.js
925
948
  var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
926
949
  function severityRank(s) {
@@ -1076,7 +1099,7 @@ function scrubUsernames(text, users) {
1076
1099
  }
1077
1100
 
1078
1101
  // ../core/dist/redact-export.js
1079
- import { createHash } from "crypto";
1102
+ import { createHash } from "node:crypto";
1080
1103
 
1081
1104
  // ../core/dist/markdown.js
1082
1105
  function truncate(s) {
@@ -1242,6 +1265,15 @@ function classifyToolError(resultSummary) {
1242
1265
  }
1243
1266
 
1244
1267
  // ../core/dist/pricing.js
1268
+ var SYNTHETIC_MODEL = "<synthetic>";
1269
+ function unpricedModels(models) {
1270
+ const out = /* @__PURE__ */ new Set();
1271
+ for (const m of models) {
1272
+ if (m && m !== SYNTHETIC_MODEL && !rateForModel(m))
1273
+ out.add(m);
1274
+ }
1275
+ return [...out].sort();
1276
+ }
1245
1277
  var PRICE_TABLE = {
1246
1278
  // Opus 4.0 / 4.1 launched at $15/$75; Opus 4.5 cut the Opus tier to $5/$25 and 4.6/4.7/4.8 kept it.
1247
1279
  // The bare 'claude-opus-4' fallback stays at the legacy rate for 4.0/4.1; 4.5+ override below.
@@ -1250,17 +1282,68 @@ var PRICE_TABLE = {
1250
1282
  "claude-opus-4-6": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1251
1283
  "claude-opus-4-7": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1252
1284
  "claude-opus-4-8": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1285
+ "claude-opus-5": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1253
1286
  "claude-fable-5": { input: 10, output: 50, cacheWrite: 12.5, cacheRead: 1 },
1254
1287
  "claude-sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
1288
+ // Sonnet 5 list price. An introductory $2/$10 rate runs through 2026-08-31; we bill the list
1289
+ // price so the estimate doesn't silently change under a date, matching the "list price" label.
1290
+ "claude-sonnet-5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
1255
1291
  "claude-haiku-4": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
1256
1292
  "claude-3-5-haiku": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },
1257
1293
  "claude-3-5-sonnet": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 }
1258
1294
  };
1295
+ var CACHE_WRITE_MULTIPLIER = 1.25;
1296
+ var CACHE_READ_MULTIPLIER = 0.1;
1297
+ function toRate(v) {
1298
+ if (v === null || typeof v !== "object" || Array.isArray(v))
1299
+ return null;
1300
+ const o = v;
1301
+ const num = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : null;
1302
+ const input = num(o.input);
1303
+ const output = num(o.output);
1304
+ if (input === null || output === null)
1305
+ return null;
1306
+ if (o.cacheWrite !== void 0 && num(o.cacheWrite) === null)
1307
+ return null;
1308
+ if (o.cacheRead !== void 0 && num(o.cacheRead) === null)
1309
+ return null;
1310
+ return {
1311
+ input,
1312
+ output,
1313
+ cacheWrite: num(o.cacheWrite) ?? input * CACHE_WRITE_MULTIPLIER,
1314
+ cacheRead: num(o.cacheRead) ?? input * CACHE_READ_MULTIPLIER
1315
+ };
1316
+ }
1317
+ function mergePricing(base, overrides) {
1318
+ const table = { ...base };
1319
+ const invalid = [];
1320
+ const applied = [];
1321
+ if (overrides === null || typeof overrides !== "object" || Array.isArray(overrides)) {
1322
+ return { table, invalid, applied };
1323
+ }
1324
+ for (const [prefix, value] of Object.entries(overrides)) {
1325
+ const rate = toRate(value);
1326
+ if (!prefix.trim() || !rate)
1327
+ invalid.push(prefix);
1328
+ else {
1329
+ table[prefix] = rate;
1330
+ applied.push(prefix);
1331
+ }
1332
+ }
1333
+ return { table, invalid: invalid.sort(), applied: applied.sort() };
1334
+ }
1335
+ var activeTable = PRICE_TABLE;
1336
+ function usePricing(table) {
1337
+ activeTable = table;
1338
+ }
1339
+ function activePricing() {
1340
+ return activeTable;
1341
+ }
1259
1342
  function rateForModel(model) {
1260
1343
  if (!model)
1261
1344
  return null;
1262
1345
  let best = null;
1263
- for (const [prefix, rate] of Object.entries(PRICE_TABLE)) {
1346
+ for (const [prefix, rate] of Object.entries(activeTable)) {
1264
1347
  if (model.startsWith(prefix) && (!best || prefix.length > best.len)) {
1265
1348
  best = { len: prefix.length, rate };
1266
1349
  }
@@ -1276,25 +1359,24 @@ function costForUsage(model, u) {
1276
1359
  }
1277
1360
 
1278
1361
  // ../core/dist/rawjson.js
1279
- import { gzipSync, gunzipSync } from "zlib";
1362
+ import { gzipSync, gunzipSync } from "node:zlib";
1280
1363
  function packRaw(json) {
1281
1364
  return gzipSync(Buffer.from(json, "utf8"));
1282
1365
  }
1283
1366
  function unpackRaw(value) {
1284
- if (Buffer.isBuffer(value)) {
1285
- if (value.length >= 2 && value[0] === 31 && value[1] === 139) {
1286
- return gunzipSync(value).toString("utf8");
1287
- }
1288
- return value.toString("utf8");
1367
+ if (typeof value === "string")
1368
+ return value;
1369
+ if (value.length >= 2 && value[0] === 31 && value[1] === 139) {
1370
+ return gunzipSync(value).toString("utf8");
1289
1371
  }
1290
- return value;
1372
+ return Buffer.from(value).toString("utf8");
1291
1373
  }
1292
1374
 
1293
1375
  // ../core/dist/paths.js
1294
- import { existsSync, readFileSync } from "fs";
1295
- import { dirname, join, resolve } from "path";
1296
- import { fileURLToPath } from "url";
1297
- import { homedir, platform } from "os";
1376
+ import { existsSync, readFileSync } from "node:fs";
1377
+ import { dirname, join, resolve } from "node:path";
1378
+ import { fileURLToPath } from "node:url";
1379
+ import { homedir, platform } from "node:os";
1298
1380
  function expandPath(p) {
1299
1381
  if (!p)
1300
1382
  return p;
@@ -1372,8 +1454,8 @@ function resolveWebDist(fromDir, repoRoot = findRepoRoot(fromDir)) {
1372
1454
  }
1373
1455
 
1374
1456
  // ../core/dist/config.js
1375
- import { readFileSync as readFileSync2 } from "fs";
1376
- import { join as join2 } from "path";
1457
+ import { readFileSync as readFileSync2 } from "node:fs";
1458
+ import { join as join2 } from "node:path";
1377
1459
  var DEFAULT_PORT = 4477;
1378
1460
  var DEFAULT_HOST = "127.0.0.1";
1379
1461
  var DEFAULT_DB_NAME = "agent-lens.db";
@@ -1383,6 +1465,18 @@ function resolveRetention() {
1383
1465
  const parsed = raw ? Number(raw) : NaN;
1384
1466
  return Number.isFinite(parsed) && parsed >= 0 ? { keepDays: parsed, origin: "env" } : { keepDays: DEFAULT_VERSIONS_KEEP_DAYS, origin: "default" };
1385
1467
  }
1468
+ function resolvePricing(cfg = readConfigFile()) {
1469
+ const { table, invalid, applied } = mergePricing(PRICE_TABLE, cfg?.pricing);
1470
+ return { table, origin: applied.length ? "file" : "default", applied, invalid };
1471
+ }
1472
+ function installPricing(cfg) {
1473
+ const resolved = cfg === void 0 ? resolvePricing() : resolvePricing(cfg);
1474
+ usePricing(resolved.table);
1475
+ if (resolved.invalid.length) {
1476
+ console.error(`agent-lens: ignoring ${resolved.invalid.length} malformed pricing override(s): ${resolved.invalid.join(", ")} (each entry needs numeric input + output; built-in rates kept)`);
1477
+ }
1478
+ return resolved;
1479
+ }
1386
1480
  function readConfigFile() {
1387
1481
  const repoRoot = findRepoRoot();
1388
1482
  const dataDir = resolveDataDir(repoRoot);
@@ -1443,10 +1537,10 @@ function resolveServerConfig(overrides = {}, cfg = readConfigFile()) {
1443
1537
  }
1444
1538
 
1445
1539
  // ../core/dist/version.js
1446
- import { spawnSync } from "child_process";
1447
- import { readFileSync as readFileSync3 } from "fs";
1448
- import { dirname as dirname2, join as join3, parse } from "path";
1449
- import { fileURLToPath as fileURLToPath2 } from "url";
1540
+ import { spawnSync } from "node:child_process";
1541
+ import { readFileSync as readFileSync3 } from "node:fs";
1542
+ import { dirname as dirname2, join as join3, parse } from "node:path";
1543
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1450
1544
  var moduleDir2 = dirname2(fileURLToPath2(import.meta.url));
1451
1545
  function isPlaceholder(v) {
1452
1546
  return v.startsWith("0.0.0");
@@ -1548,9 +1642,9 @@ function encodeProjectPath(p) {
1548
1642
  }
1549
1643
 
1550
1644
  // ../core/dist/collect.js
1551
- import { appendFileSync, closeSync, copyFileSync, existsSync as existsSync2, mkdirSync, openSync, readSync, readdirSync, statSync, unlinkSync, utimesSync, writeSync, chmodSync } from "fs";
1552
- import { basename, dirname as dirname3, join as join4, relative, sep } from "path";
1553
- import { platform as platform2 } from "os";
1645
+ import { appendFileSync, closeSync, copyFileSync, existsSync as existsSync2, mkdirSync, openSync, readSync, readdirSync, statSync, unlinkSync, utimesSync, writeSync, chmodSync } from "node:fs";
1646
+ import { basename, dirname as dirname3, join as join4, relative, sep } from "node:path";
1647
+ import { platform as platform2 } from "node:os";
1554
1648
  var POSIX = platform2() !== "win32";
1555
1649
  var DIR_MODE = 448;
1556
1650
  var FILE_MODE = 384;
@@ -1769,9 +1863,9 @@ function collectAll(opts = {}) {
1769
1863
  }
1770
1864
 
1771
1865
  // ../core/dist/lock.js
1772
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1773
- import { dirname as dirname4 } from "path";
1774
- import { hostname } from "os";
1866
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
1867
+ import { dirname as dirname4 } from "node:path";
1868
+ import { hostname } from "node:os";
1775
1869
  function isAlive(pid) {
1776
1870
  if (!pid)
1777
1871
  return false;
@@ -1811,10 +1905,10 @@ function acquireLock(lockPath, staleMs = 6 * 60 * 60 * 1e3) {
1811
1905
  }
1812
1906
 
1813
1907
  // ../core/dist/service.js
1814
- import { spawnSync as spawnSync2 } from "child_process";
1815
- import { mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1816
- import { homedir as homedir2, platform as platform3, userInfo } from "os";
1817
- import { join as join5 } from "path";
1908
+ import { spawnSync as spawnSync2 } from "node:child_process";
1909
+ import { mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
1910
+ import { homedir as homedir2, platform as platform3, userInfo } from "node:os";
1911
+ import { join as join5 } from "node:path";
1818
1912
  var DEFAULT_HOURS = [9, 13, 17, 21];
1819
1913
  function currentUser() {
1820
1914
  try {
@@ -2139,15 +2233,15 @@ function runService(action, params) {
2139
2233
  }
2140
2234
 
2141
2235
  // ../ingest/dist/run.js
2142
- import { readFileSync as readFileSync6, statSync as statSync3, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
2143
- import { dirname as dirname6, join as join9 } from "path";
2236
+ import { readFileSync as readFileSync6, statSync as statSync3, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "node:fs";
2237
+ import { dirname as dirname6, join as join9 } from "node:path";
2144
2238
 
2145
2239
  // ../ingest/dist/db.js
2146
- import Database from "better-sqlite3";
2240
+ import { DatabaseSync } from "node:sqlite";
2147
2241
  function openRaw(file) {
2148
- const db = new Database(file);
2149
- db.pragma("journal_mode = WAL");
2150
- db.pragma("foreign_keys = ON");
2242
+ const db = new DatabaseSync(file);
2243
+ db.exec("PRAGMA journal_mode = WAL");
2244
+ db.exec("PRAGMA foreign_keys = ON");
2151
2245
  return db;
2152
2246
  }
2153
2247
  function readSchemaVersion(db) {
@@ -2167,37 +2261,40 @@ function openDb(file) {
2167
2261
  return db;
2168
2262
  }
2169
2263
  function resetSchema(db) {
2170
- db.pragma("foreign_keys = OFF");
2171
- db.exec(`
2172
- DROP TRIGGER IF EXISTS events_au;
2173
- DROP TRIGGER IF EXISTS events_ad;
2174
- DROP TRIGGER IF EXISTS events_ai;
2175
- DROP TABLE IF EXISTS events_fts;
2176
- DROP TABLE IF EXISTS file_changes;
2177
- DROP TABLE IF EXISTS findings;
2178
- DROP TABLE IF EXISTS classifications;
2179
- DROP TABLE IF EXISTS token_usage;
2180
- DROP TABLE IF EXISTS tool_calls;
2181
- DROP TABLE IF EXISTS skills;
2182
- DROP TABLE IF EXISTS events;
2183
- DROP TABLE IF EXISTS turns;
2184
- DROP TABLE IF EXISTS sessions;
2185
- DROP TABLE IF EXISTS projects;
2186
- DROP TABLE IF EXISTS sources;
2187
- DROP TABLE IF EXISTS agents;
2188
- DROP TABLE IF EXISTS workflow_results;
2189
- DROP TABLE IF EXISTS session_meta;
2190
- DROP TABLE IF EXISTS tool_results;
2191
- DROP TABLE IF EXISTS ingest_state;
2192
- DROP TABLE IF EXISTS meta;
2193
- `);
2194
- applySchema(db);
2195
- db.pragma("foreign_keys = ON");
2264
+ db.exec("PRAGMA foreign_keys = OFF");
2265
+ try {
2266
+ db.exec(`
2267
+ DROP TRIGGER IF EXISTS events_au;
2268
+ DROP TRIGGER IF EXISTS events_ad;
2269
+ DROP TRIGGER IF EXISTS events_ai;
2270
+ DROP TABLE IF EXISTS events_fts;
2271
+ DROP TABLE IF EXISTS file_changes;
2272
+ DROP TABLE IF EXISTS findings;
2273
+ DROP TABLE IF EXISTS classifications;
2274
+ DROP TABLE IF EXISTS token_usage;
2275
+ DROP TABLE IF EXISTS tool_calls;
2276
+ DROP TABLE IF EXISTS skills;
2277
+ DROP TABLE IF EXISTS events;
2278
+ DROP TABLE IF EXISTS turns;
2279
+ DROP TABLE IF EXISTS sessions;
2280
+ DROP TABLE IF EXISTS projects;
2281
+ DROP TABLE IF EXISTS sources;
2282
+ DROP TABLE IF EXISTS agents;
2283
+ DROP TABLE IF EXISTS workflow_results;
2284
+ DROP TABLE IF EXISTS session_meta;
2285
+ DROP TABLE IF EXISTS tool_results;
2286
+ DROP TABLE IF EXISTS ingest_state;
2287
+ DROP TABLE IF EXISTS meta;
2288
+ `);
2289
+ applySchema(db);
2290
+ } finally {
2291
+ db.exec("PRAGMA foreign_keys = ON");
2292
+ }
2196
2293
  }
2197
2294
 
2198
2295
  // ../ingest/dist/adapters/claude-code.js
2199
- import { readdirSync as readdirSync2 } from "fs";
2200
- import { basename as basename2, join as join6 } from "path";
2296
+ import { readdirSync as readdirSync2 } from "node:fs";
2297
+ import { basename as basename2, join as join6 } from "node:path";
2201
2298
  function flattenText(content) {
2202
2299
  if (content == null)
2203
2300
  return null;
@@ -2458,7 +2555,7 @@ function createDirtySet(db, name, ids) {
2458
2555
  db.exec(`DROP TABLE IF EXISTS ${name}`);
2459
2556
  db.exec(`CREATE TEMP TABLE ${name} (id TEXT PRIMARY KEY)`);
2460
2557
  const ins = db.prepare(`INSERT OR IGNORE INTO ${name} (id) VALUES (?)`);
2461
- db.transaction((xs) => {
2558
+ transaction(db, (xs) => {
2462
2559
  for (const id of xs)
2463
2560
  ins.run(id);
2464
2561
  })(ids);
@@ -2658,7 +2755,7 @@ function classify(db, dirty) {
2658
2755
  category=excluded.category, complexity_score=excluded.complexity_score,
2659
2756
  complexity_band=excluded.complexity_band, signals_json=excluded.signals_json,
2660
2757
  classifier_version=excluded.classifier_version`);
2661
- const tx = db.transaction(() => {
2758
+ const tx = transaction(db, () => {
2662
2759
  for (const sess of sessions) {
2663
2760
  const toolCounts = toolMix.get(sess.id) ?? {};
2664
2761
  const skills = skillMix.get(sess.id) ?? {};
@@ -2729,8 +2826,8 @@ function classify(db, dirty) {
2729
2826
  }
2730
2827
 
2731
2828
  // ../ingest/dist/detect.js
2732
- import { createHash as createHash2 } from "crypto";
2733
- import { posix as path } from "path";
2829
+ import { createHash as createHash2 } from "node:crypto";
2830
+ import { posix as path } from "node:path";
2734
2831
  var DETECTOR_VERSION = 8;
2735
2832
  function evidence(s, max = 200) {
2736
2833
  const one = s.replace(/\s+/g, " ").trim();
@@ -3195,7 +3292,7 @@ function detect(db, dirty) {
3195
3292
  VALUES (@id, @session_id, @tool_call_id, @event_uuid, @turn_id, @rule_id, @category, @framework_ref, @severity, @title, @evidence, @signals_json, @detector_version)
3196
3293
  ON CONFLICT(id) DO NOTHING`);
3197
3294
  const delScope = incremental ? "DELETE FROM findings WHERE session_id IN (SELECT id FROM _dirty_sec)" : "DELETE FROM findings";
3198
- const tx = db.transaction(() => {
3295
+ const tx = transaction(db, () => {
3199
3296
  db.exec(delScope);
3200
3297
  for (const row of rows) {
3201
3298
  const ctx = buildContext(row, projPath.get(row.session_id) ?? null, ownedConfigDirs);
@@ -3255,7 +3352,7 @@ function classifyErrors(db, dirty) {
3255
3352
  createDirtySet(db, "_dirty_err", dirty);
3256
3353
  const rows = db.prepare(`SELECT id, result_summary FROM tool_calls WHERE status = 'error'${scope}`).all();
3257
3354
  const update = db.prepare("UPDATE tool_calls SET error_type = ? WHERE id = ?");
3258
- const tx = db.transaction(() => {
3355
+ const tx = transaction(db, () => {
3259
3356
  db.exec(`UPDATE tool_calls SET error_type = NULL WHERE error_type IS NOT NULL${scope}`);
3260
3357
  for (const r of rows)
3261
3358
  update.run(classifyToolError(r.result_summary).type, r.id);
@@ -3268,8 +3365,8 @@ function classifyErrors(db, dirty) {
3268
3365
  }
3269
3366
 
3270
3367
  // ../ingest/dist/filechanges.js
3271
- import { createHash as createHash3 } from "crypto";
3272
- import { posix as path2 } from "path";
3368
+ import { createHash as createHash3 } from "node:crypto";
3369
+ import { posix as path2 } from "node:path";
3273
3370
  var FILECHANGES_VERSION = 1;
3274
3371
  var FILE_TOOLS = ["Edit", "Write", "NotebookEdit"];
3275
3372
  function fileChangeId(toolCallId, filePath) {
@@ -3301,7 +3398,7 @@ function deriveFileChanges(db, dirty) {
3301
3398
  db.exec("DROP TABLE IF EXISTS _dirty_fc");
3302
3399
  db.exec("CREATE TEMP TABLE _dirty_fc (id TEXT PRIMARY KEY)");
3303
3400
  const ins = db.prepare("INSERT OR IGNORE INTO _dirty_fc (id) VALUES (?)");
3304
- db.transaction((ids) => {
3401
+ transaction(db, (ids) => {
3305
3402
  for (const id of ids)
3306
3403
  ins.run(id);
3307
3404
  })(dirty);
@@ -3318,7 +3415,7 @@ function deriveFileChanges(db, dirty) {
3318
3415
  VALUES (@id, @tool_call_id, @session_id, @turn_id, @event_uuid, @project_id, @file_path, @tool_name, @lines_added, @lines_removed, @timestamp, @derive_version)
3319
3416
  ON CONFLICT(id) DO NOTHING`);
3320
3417
  const delScope = incremental ? "DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _dirty_fc)" : "DELETE FROM file_changes";
3321
- const tx = db.transaction(() => {
3418
+ const tx = transaction(db, () => {
3322
3419
  db.exec(delScope);
3323
3420
  for (const row of rows) {
3324
3421
  if (row.status === "error")
@@ -3367,10 +3464,10 @@ function deriveFileChanges(db, dirty) {
3367
3464
  }
3368
3465
 
3369
3466
  // ../ingest/dist/canonicalize.js
3370
- import { existsSync as existsSync4 } from "fs";
3371
- import { homedir as homedir3 } from "os";
3372
- import { createHash as createHash4 } from "crypto";
3373
- import { dirname as dirname5, join as join7 } from "path";
3467
+ import { existsSync as existsSync4 } from "node:fs";
3468
+ import { homedir as homedir3 } from "node:os";
3469
+ import { createHash as createHash4 } from "node:crypto";
3470
+ import { dirname as dirname5, join as join7 } from "node:path";
3374
3471
  function projectId(agentId, path3) {
3375
3472
  return createHash4("sha1").update(`${agentId}\0${path3}`).digest("hex").slice(0, 16);
3376
3473
  }
@@ -3426,7 +3523,7 @@ function canonicalizeProjects(db, opts = {}) {
3426
3523
  const repointSessions = db.prepare("UPDATE sessions SET project_id = ? WHERE project_id = ?");
3427
3524
  const repointFileChanges = db.prepare("UPDATE file_changes SET project_id = ? WHERE project_id = ?");
3428
3525
  const touched = /* @__PURE__ */ new Set();
3429
- db.transaction(() => {
3526
+ transaction(db, () => {
3430
3527
  for (const r of rows) {
3431
3528
  const target = canon(r.path);
3432
3529
  if (target === strip(r.path))
@@ -3450,12 +3547,12 @@ function canonicalizeProjects(db, opts = {}) {
3450
3547
  for (const id of touched)
3451
3548
  refresh.run(id);
3452
3549
  })();
3453
- const removed = db.prepare("DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)").run().changes;
3550
+ const removed = Number(db.prepare("DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)").run().changes);
3454
3551
  return { merged, removed };
3455
3552
  }
3456
3553
 
3457
3554
  // ../ingest/dist/pipeline.js
3458
- import { createHash as createHash5 } from "crypto";
3555
+ import { createHash as createHash5 } from "node:crypto";
3459
3556
  function newStats() {
3460
3557
  return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
3461
3558
  }
@@ -3519,20 +3616,23 @@ function pruneExcluded(db, excludedPaths) {
3519
3616
  return 0;
3520
3617
  }
3521
3618
  const files = db.prepare("SELECT DISTINCT source_file f FROM events WHERE session_id IN (SELECT id FROM _prune) AND source_file IS NOT NULL").all().map((r) => r.f);
3522
- db.pragma("foreign_keys = OFF");
3523
- db.transaction(() => {
3524
- db.exec("DELETE FROM findings WHERE session_id IN (SELECT id FROM _prune)");
3525
- db.exec("DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _prune)");
3526
- for (const t of ["token_usage", "tool_calls", "turns", "events"])
3527
- db.exec(`DELETE FROM ${t} WHERE session_id IN (SELECT id FROM _prune)`);
3528
- db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id IN (SELECT id FROM _prune)");
3529
- db.exec("DELETE FROM sessions WHERE id IN (SELECT id FROM _prune)");
3530
- const delState = db.prepare("DELETE FROM ingest_state WHERE file_path = ?");
3531
- for (const f of files)
3532
- delState.run(f);
3533
- db.exec("DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)");
3534
- })();
3535
- db.pragma("foreign_keys = ON");
3619
+ db.exec("PRAGMA foreign_keys = OFF");
3620
+ try {
3621
+ transaction(db, () => {
3622
+ db.exec("DELETE FROM findings WHERE session_id IN (SELECT id FROM _prune)");
3623
+ db.exec("DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _prune)");
3624
+ for (const t of ["token_usage", "tool_calls", "turns", "events"])
3625
+ db.exec(`DELETE FROM ${t} WHERE session_id IN (SELECT id FROM _prune)`);
3626
+ db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id IN (SELECT id FROM _prune)");
3627
+ db.exec("DELETE FROM sessions WHERE id IN (SELECT id FROM _prune)");
3628
+ const delState = db.prepare("DELETE FROM ingest_state WHERE file_path = ?");
3629
+ for (const f of files)
3630
+ delState.run(f);
3631
+ db.exec("DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)");
3632
+ })();
3633
+ } finally {
3634
+ db.exec("PRAGMA foreign_keys = ON");
3635
+ }
3536
3636
  db.exec("DROP TABLE IF EXISTS _prune");
3537
3637
  return total;
3538
3638
  }
@@ -3614,12 +3714,12 @@ function rebuildTurns(db, scope, sessionIds) {
3614
3714
  const insTurn = db.prepare(`INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)
3615
3715
  VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`);
3616
3716
  const updEventTurn = db.prepare("UPDATE events SET turn_id = ? WHERE uuid = ?");
3617
- const tx = db.transaction(() => {
3717
+ const tx = transaction(db, () => {
3618
3718
  for (const { id } of sessionIds) {
3619
3719
  const rows = selEvents.all(id);
3620
3720
  const { turns, eventTurn } = buildTurns(id, rows);
3621
3721
  for (const t of turns)
3622
- insTurn.run(t);
3722
+ runNamed(insTurn, t);
3623
3723
  for (const [uuid, turnId] of eventTurn)
3624
3724
  updEventTurn.run(turnId, uuid);
3625
3725
  }
@@ -3638,7 +3738,7 @@ function linkSkillVersions(db, sessionIds) {
3638
3738
  first_seen = MIN(first_seen, excluded.first_seen),
3639
3739
  base_dir = excluded.base_dir`);
3640
3740
  const linkCall = db.prepare("UPDATE tool_calls SET skill_id = ? WHERE id = ?");
3641
- const linkTx = db.transaction(() => {
3741
+ const linkTx = transaction(db, () => {
3642
3742
  for (const { id: sid } of sessionIds) {
3643
3743
  const calls = selSkillCalls.all(sid);
3644
3744
  if (!calls.length)
@@ -3836,7 +3936,7 @@ function prepareStatements(db) {
3836
3936
  function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
3837
3937
  const sessionMeta = {};
3838
3938
  let eventsInFile = 0;
3839
- const tx = db.transaction(() => {
3939
+ const tx = transaction(db, () => {
3840
3940
  stmts.insSessionStub.run(file.sessionId, adapter.agentId, file.sourceId);
3841
3941
  let seq = 0;
3842
3942
  for (const line of lines) {
@@ -3860,10 +3960,10 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
3860
3960
  }
3861
3961
  }
3862
3962
  if (parsed.tokenUsage)
3863
- stmts.insTokens.run(parsed.tokenUsage);
3963
+ runNamed(stmts.insTokens, parsed.tokenUsage);
3864
3964
  if (parsed.toolCalls)
3865
3965
  for (const tc of parsed.toolCalls)
3866
- stmts.insTool.run(tc);
3966
+ runNamed(stmts.insTool, tc);
3867
3967
  if (parsed.toolResults)
3868
3968
  for (const tr of parsed.toolResults)
3869
3969
  stmts.patchTool.run({
@@ -3917,14 +4017,13 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
3917
4017
  }
3918
4018
 
3919
4019
  // ../ingest/dist/workflows.js
3920
- import { basename as basename3 } from "path";
4020
+ import { basename as basename3 } from "node:path";
3921
4021
 
3922
4022
  // ../ingest/dist/sidecar.js
3923
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
3924
- import { join as join8 } from "path";
4023
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
4024
+ import { join as join8 } from "node:path";
3925
4025
 
3926
4026
  // ../ingest/dist/redact.js
3927
- import { createHash as createHash6, randomBytes } from "crypto";
3928
4027
  function parseExcludes(csv) {
3929
4028
  return (csv ?? "").split(",").map((s) => s.trim()).filter(Boolean).map(encodeProjectPath);
3930
4029
  }
@@ -3933,17 +4032,17 @@ function isExcludedArchivePath(filePath, excluded) {
3933
4032
  }
3934
4033
 
3935
4034
  // ../ingest/dist/fileread.js
3936
- import { createHash as createHash7 } from "crypto";
3937
- import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
3938
- import { StringDecoder } from "string_decoder";
4035
+ import { createHash as createHash6 } from "node:crypto";
4036
+ import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "node:fs";
4037
+ import { StringDecoder } from "node:string_decoder";
3939
4038
  var STREAM_THRESHOLD = 8 * 1024 * 1024;
3940
4039
  var CHUNK2 = 64 * 1024;
3941
4040
  function sha256(buf) {
3942
- return createHash7("sha256").update(buf).digest("hex");
4041
+ return createHash6("sha256").update(buf).digest("hex");
3943
4042
  }
3944
4043
  function sha256File(path3) {
3945
4044
  const fd = openSync2(path3, "r");
3946
- const h = createHash7("sha256");
4045
+ const h = createHash6("sha256");
3947
4046
  const buf = Buffer.allocUnsafe(CHUNK2);
3948
4047
  try {
3949
4048
  let n;
@@ -4136,7 +4235,7 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
4136
4235
  }
4137
4236
 
4138
4237
  // ../ingest/dist/meta.js
4139
- import { basename as basename4 } from "path";
4238
+ import { basename as basename4 } from "node:path";
4140
4239
  var newMetaStats = newSidecarStats;
4141
4240
  function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2, stats, full) {
4142
4241
  const upsert = db.prepare(`INSERT INTO session_meta
@@ -4175,7 +4274,7 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
4175
4274
  }
4176
4275
 
4177
4276
  // ../ingest/dist/toolResults.js
4178
- import { basename as basename5 } from "path";
4277
+ import { basename as basename5 } from "node:path";
4179
4278
  var newToolResultStats = newSidecarStats;
4180
4279
  function ingestToolResults(db, sourceArchiveDir, excludedDirs, now2, stats, full) {
4181
4280
  const upsert = db.prepare(`INSERT INTO tool_results (session_id, name, path, bytes, text, ingested_at)
@@ -4235,10 +4334,16 @@ function printIngestReport(db, dbPath, r) {
4235
4334
  const usageRows = db.prepare("SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage").all();
4236
4335
  let cost = 0;
4237
4336
  let totalTokens = 0;
4337
+ const unpriced2 = /* @__PURE__ */ new Map();
4238
4338
  for (const u of usageRows) {
4239
4339
  cost += costForUsage(u.model, u);
4240
4340
  totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
4341
+ if (u.model && u.model !== SYNTHETIC_MODEL && !rateForModel(u.model)) {
4342
+ unpriced2.set(u.model, (unpriced2.get(u.model) ?? 0) + 1);
4343
+ }
4241
4344
  }
4345
+ const unpricedLine = unpriced2.size ? `
4346
+ unpriced=${unpriced2.size} model(s): ` + [...unpriced2].map(([m, n]) => `${m} (${n} records)`).join(", ") + ` \u2014 cost above excludes them; add rates under "pricing" in agent-lens.config.json` : "";
4242
4347
  const wfRuns = count("SELECT COUNT(*) n FROM workflow_results");
4243
4348
  const metaRows = count("SELECT COUNT(*) n FROM session_meta");
4244
4349
  const trRows = count("SELECT COUNT(*) n FROM tool_results");
@@ -4253,12 +4358,13 @@ function printIngestReport(db, dbPath, r) {
4253
4358
  tool_results=${trRows} (upserted=${r.trStats.upserted} skipped=${r.trStats.skipped} malformed=${r.trStats.malformed})
4254
4359
  findings=${r.detected.count} (${sevLine})
4255
4360
  file_changes=${r.fileChanges.count}
4256
- tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
4361
+ tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}` + unpricedLine);
4257
4362
  }
4258
4363
  function runIngest(argv = process.argv.slice(2)) {
4259
4364
  const args = parseIngestArgs(argv);
4260
4365
  const archiveRoot = args.archive || resolveArchiveDir();
4261
4366
  const { path: dbPath } = resolveDbPath(args.db);
4367
+ installPricing();
4262
4368
  if (!existsSync5(archiveRoot)) {
4263
4369
  console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
4264
4370
  process.exit(1);
@@ -4359,22 +4465,23 @@ function runMetrics(argv = process.argv.slice(2)) {
4359
4465
  }
4360
4466
 
4361
4467
  // ../server/dist/run.js
4362
- import { existsSync as existsSync8 } from "fs";
4363
- import { dirname as dirname7 } from "path";
4364
- import { fileURLToPath as fileURLToPath3 } from "url";
4468
+ import { existsSync as existsSync8 } from "node:fs";
4469
+ import { dirname as dirname7 } from "node:path";
4470
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
4365
4471
 
4366
4472
  // ../server/dist/db.js
4367
- import Database2 from "better-sqlite3";
4473
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
4368
4474
 
4369
4475
  // ../server/dist/sql-util.js
4476
+ var bind = (params) => params;
4370
4477
  function tableExists(db, name) {
4371
4478
  return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
4372
4479
  }
4373
4480
  function queryAll(db, sql, ...params) {
4374
- return db.prepare(sql).all(...params);
4481
+ return db.prepare(sql).all(...bind(params));
4375
4482
  }
4376
4483
  function queryGet(db, sql, ...params) {
4377
- return db.prepare(sql).get(...params);
4484
+ return db.prepare(sql).get(...bind(params));
4378
4485
  }
4379
4486
  function pushDateRange(clauses, params, col, from, to) {
4380
4487
  if (from) {
@@ -4432,6 +4539,9 @@ function splitOf(rows) {
4432
4539
  function tokensOf(split) {
4433
4540
  return split.input + split.output + split.cache_creation + split.cache_read;
4434
4541
  }
4542
+ function unpricedOf(rows) {
4543
+ return unpricedModels(rows.filter((u) => u.i || u.o || u.cw || u.cr).map((u) => u.model));
4544
+ }
4435
4545
  function costOf(rows) {
4436
4546
  let cost = 0;
4437
4547
  for (const u of rows) {
@@ -4447,8 +4557,8 @@ function costOf(rows) {
4447
4557
 
4448
4558
  // ../server/dist/db.js
4449
4559
  function openReadonly(file) {
4450
- const db = new Database2(file, { readonly: true, fileMustExist: true });
4451
- db.pragma("query_only = ON");
4560
+ const db = new DatabaseSync2(file, { readOnly: true });
4561
+ db.exec("PRAGMA query_only = ON");
4452
4562
  return db;
4453
4563
  }
4454
4564
  function schemaStatus(db) {
@@ -4496,6 +4606,32 @@ function listModels(db) {
4496
4606
  function toFtsQuery(raw) {
4497
4607
  return raw.trim().split(/\s+/).filter(Boolean).map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
4498
4608
  }
4609
+ var TOKENS_SORT_SQL = `(SELECT COALESCE(SUM(t.input_tokens + t.output_tokens
4610
+ + t.cache_creation_input_tokens + t.cache_read_input_tokens), 0)
4611
+ FROM token_usage t WHERE t.session_id = s.id)`;
4612
+ var SECURITY_SORT_SQL = `(CASE worst_severity
4613
+ WHEN 'critical' THEN 5 WHEN 'high' THEN 4 WHEN 'medium' THEN 3 WHEN 'low' THEN 2 WHEN 'info' THEN 1
4614
+ ELSE 0 END) * 1000000 + finding_count`;
4615
+ var COST_SORT_SQL = `(SELECT COALESCE(SUM((tu.input_tokens * mr.i_rate + tu.output_tokens * mr.o_rate
4616
+ + tu.cache_creation_input_tokens * mr.cw_rate + tu.cache_read_input_tokens * mr.cr_rate) / 1000000.0), 0)
4617
+ FROM token_usage tu LEFT JOIN model_rate mr ON mr.model IS tu.model
4618
+ WHERE tu.session_id = s.id)`;
4619
+ function ratesCte() {
4620
+ const entries = Object.entries(activePricing());
4621
+ return {
4622
+ // `model_rate` resolves the prefix match ONCE per distinct model id, not once per token_usage row.
4623
+ // Inlining the lookup into the cost expression instead re-ran it for every usage row of every
4624
+ // candidate session — ~35× slower on a real corpus, which would have made this "fix" a regression.
4625
+ sql: `WITH rates(prefix, i_rate, o_rate, cw_rate, cr_rate) AS (VALUES ${entries.map(() => "(?,?,?,?,?)").join(",")}),
4626
+ model_rate AS (
4627
+ SELECT m.model, r.i_rate, r.o_rate, r.cw_rate, r.cr_rate
4628
+ FROM (SELECT DISTINCT model FROM token_usage) m
4629
+ LEFT JOIN rates r ON r.prefix = (
4630
+ SELECT p.prefix FROM rates p WHERE substr(m.model, 1, length(p.prefix)) = p.prefix
4631
+ ORDER BY length(p.prefix) DESC LIMIT 1)) `,
4632
+ params: entries.flatMap(([prefix, r]) => [prefix, r.input, r.output, r.cacheWrite, r.cacheRead])
4633
+ };
4634
+ }
4499
4635
  function listSessions(db, f) {
4500
4636
  const where = [];
4501
4637
  const params = [];
@@ -4540,41 +4676,19 @@ function listSessions(db, f) {
4540
4676
  LIMIT 1) AS worst_severity
4541
4677
  FROM sessions s LEFT JOIN projects p ON p.id = s.project_id
4542
4678
  ${whereSql}`;
4543
- const NATIVE_ORDER = {
4679
+ const ORDER = {
4544
4680
  started: "s.started_at",
4545
4681
  title: "COALESCE(s.ai_title, s.slug)",
4546
4682
  turns: "s.turn_count",
4547
- duration: "s.duration_ms"
4683
+ duration: "s.duration_ms",
4684
+ errors: "tool_error_count",
4685
+ tokens: TOKENS_SORT_SQL,
4686
+ cost: COST_SORT_SQL,
4687
+ security: SECURITY_SORT_SQL
4548
4688
  };
4549
4689
  const sortKey = f.sort ?? "started";
4550
- const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
4551
- let sessions;
4552
- if (JS_SORT.has(sortKey)) {
4553
- const all = attachSessionCost(db, queryAll(db, baseSelect, ...params));
4554
- const sign = f.dir === "asc" ? 1 : -1;
4555
- const metric = (r) => {
4556
- switch (sortKey) {
4557
- case "tokens":
4558
- return r.tokens;
4559
- case "cost":
4560
- return r.cost;
4561
- case "errors":
4562
- return r.tool_error_count ?? 0;
4563
- // Security: rank by worst severity (info=0 … critical=4; none = -1), finding count as tiebreak.
4564
- case "security":
4565
- return (severityRank(r.worst_severity ?? "") + 1) * 1e6 + (r.finding_count ?? 0);
4566
- default:
4567
- return 0;
4568
- }
4569
- };
4570
- all.sort((a, b) => {
4571
- const c = (metric(a) - metric(b)) * sign;
4572
- return c !== 0 ? c : String(a.id).localeCompare(String(b.id));
4573
- });
4574
- sessions = all.slice(f.offset, f.offset + f.limit);
4575
- } else {
4576
- sessions = attachSessionCost(db, queryAll(db, `${baseSelect} ORDER BY ${orderBy(NATIVE_ORDER, sortKey, "started", f.dir)}, s.id ASC LIMIT ? OFFSET ?`, ...params, f.limit, f.offset));
4577
- }
4690
+ const rates = sortKey === "cost" ? ratesCte() : null;
4691
+ const sessions = attachSessionCost(db, queryAll(db, `${rates?.sql ?? ""}${baseSelect} ORDER BY ${orderBy(ORDER, sortKey, "started", f.dir)}, s.id ASC LIMIT ? OFFSET ?`, ...rates?.params ?? [], ...params, f.limit, f.offset));
4578
4692
  return { total, sessions };
4579
4693
  }
4580
4694
  function attachSessionCost(db, rows) {
@@ -4594,7 +4708,14 @@ function attachSessionCost(db, rows) {
4594
4708
  return rows.map(({ ai_title, slug, ...r }) => {
4595
4709
  const u = usageBySession.get(r.id) ?? [];
4596
4710
  const split = splitOf(u);
4597
- return { ...r, tokens: tokensOf(split), token_split: split, cost: costOf(u), title: ai_title || slug || null };
4711
+ return {
4712
+ ...r,
4713
+ tokens: tokensOf(split),
4714
+ token_split: split,
4715
+ cost: costOf(u),
4716
+ unpriced_models: unpricedOf(u),
4717
+ title: ai_title || slug || null
4718
+ };
4598
4719
  });
4599
4720
  }
4600
4721
  function loadToolCalls(db, id) {
@@ -4739,6 +4860,7 @@ function withSessionTotals(db, session, id, toolRows) {
4739
4860
  tokens: tokensOf(split),
4740
4861
  token_split: split,
4741
4862
  cost: costOf(usage),
4863
+ unpriced_models: unpricedOf(usage),
4742
4864
  title: session.ai_title || session.slug || null,
4743
4865
  tool_call_count: toolRows.length,
4744
4866
  tool_error_count,
@@ -5129,7 +5251,7 @@ function securitySummary(db) {
5129
5251
  }
5130
5252
 
5131
5253
  // ../server/dist/app.js
5132
- import { existsSync as existsSync7 } from "fs";
5254
+ import { existsSync as existsSync7 } from "node:fs";
5133
5255
  import Fastify from "fastify";
5134
5256
  import fastifyStatic from "@fastify/static";
5135
5257
 
@@ -5187,11 +5309,18 @@ var usageForCost = (u) => ({
5187
5309
  cache_creation_input_tokens: u.cw ?? 0,
5188
5310
  cache_read_input_tokens: u.cr ?? 0
5189
5311
  });
5190
- function pct(sorted, p) {
5191
- if (sorted.length === 0)
5192
- return 0;
5193
- const rank = Math.ceil(p / 100 * sorted.length) - 1;
5194
- return sorted[Math.min(Math.max(rank, 0), sorted.length - 1)];
5312
+ function percentiles(db, values, params) {
5313
+ const at = (p) => `(SELECT COALESCE((SELECT v FROM vals ORDER BY v
5314
+ LIMIT 1 OFFSET MAX(0, CAST(ceil(${p} / 100.0 * (SELECT c FROM n)) AS INTEGER) - 1)), 0))`;
5315
+ const row = queryGet(
5316
+ db,
5317
+ // MATERIALIZED matters: the three selects below each reference `vals`, and without the hint
5318
+ // SQLite inlines the CTE and re-runs the underlying scan once per percentile.
5319
+ `WITH vals AS MATERIALIZED (${values}), n AS (SELECT COUNT(*) c FROM vals)
5320
+ SELECT ${at(50)} AS p50, ${at(95)} AS p95, (SELECT c FROM n) AS count`,
5321
+ ...params
5322
+ );
5323
+ return row ?? { p50: 0, p95: 0, count: 0 };
5195
5324
  }
5196
5325
  function workflowWhere(f) {
5197
5326
  const where = [];
@@ -5250,18 +5379,18 @@ function dashboardOverview(db, f) {
5250
5379
  GROUP BY t.model`, ...w.params);
5251
5380
  const tokens = zeroSplit();
5252
5381
  let cost = 0;
5253
- const unpriced = [];
5382
+ const unpriced2 = [];
5254
5383
  for (const u of usage) {
5255
5384
  addUsage(tokens, u);
5256
5385
  cost += costForUsage(u.model, usageForCost(u));
5257
5386
  if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr))
5258
- unpriced.push(u.model);
5387
+ unpriced2.push(u.model);
5259
5388
  }
5260
5389
  const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
5261
- const durs = queryAll(db, `SELECT tn.duration_ms d FROM turns tn JOIN sessions s ON s.id = tn.session_id
5262
- ${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL ORDER BY tn.duration_ms`, ...w.params).map((r) => r.d);
5263
- const sessDurs = queryAll(db, `SELECT s.duration_ms d FROM sessions s
5264
- ${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL ORDER BY s.duration_ms`, ...w.params).map((r) => r.d);
5390
+ const turnDur = percentiles(db, `SELECT tn.duration_ms v FROM turns tn JOIN sessions s ON s.id = tn.session_id
5391
+ ${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL`, w.params);
5392
+ const sessDur = percentiles(db, `SELECT s.duration_ms v FROM sessions s
5393
+ ${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL`, w.params);
5265
5394
  return {
5266
5395
  range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },
5267
5396
  sessions: counts.sessions ?? 0,
@@ -5275,9 +5404,9 @@ function dashboardOverview(db, f) {
5275
5404
  total_tokens: totalTokens,
5276
5405
  cache_read_ratio: totalTokens ? tokens.cache_read / totalTokens : 0,
5277
5406
  cost: Number(cost.toFixed(4)),
5278
- unpriced_models: [...new Set(unpriced)].sort(),
5279
- turn_duration_ms: { p50: pct(durs, 50), p95: pct(durs, 95), count: durs.length },
5280
- session_duration_ms: { p50: pct(sessDurs, 50), p95: pct(sessDurs, 95), count: sessDurs.length },
5407
+ unpriced_models: [...new Set(unpriced2)].sort(),
5408
+ turn_duration_ms: turnDur,
5409
+ session_duration_ms: sessDur,
5281
5410
  workflows: workflowAgg(db, f)
5282
5411
  };
5283
5412
  }
@@ -5442,8 +5571,8 @@ function dashboardBreakdowns(db, f) {
5442
5571
  }
5443
5572
 
5444
5573
  // ../server/dist/refresh.js
5445
- import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
5446
- import { join as join10 } from "path";
5574
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
5575
+ import { join as join10 } from "node:path";
5447
5576
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
5448
5577
  function originAllowed(origin) {
5449
5578
  if (!origin)
@@ -5480,7 +5609,7 @@ function runRefresh() {
5480
5609
  }
5481
5610
 
5482
5611
  // ../server/dist/triage.js
5483
- import Database3 from "better-sqlite3";
5612
+ import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
5484
5613
  var TRIAGE_SCHEMA_SQL = (
5485
5614
  /* sql */
5486
5615
  `
@@ -5508,8 +5637,8 @@ CREATE TABLE IF NOT EXISTS muted_rules (
5508
5637
  `
5509
5638
  );
5510
5639
  function openTriage(file) {
5511
- const db = new Database3(file);
5512
- db.pragma("journal_mode = WAL");
5640
+ const db = new DatabaseSync3(file);
5641
+ db.exec("PRAGMA journal_mode = WAL");
5513
5642
  db.exec(TRIAGE_SCHEMA_SQL);
5514
5643
  return db;
5515
5644
  }
@@ -5518,7 +5647,7 @@ function dismiss(db, ids, note) {
5518
5647
  const stmt = db.prepare(`INSERT INTO dismissed_findings (finding_id, note, dismissed_at) VALUES (?, ?, ?)
5519
5648
  ON CONFLICT(finding_id) DO UPDATE SET note = excluded.note, dismissed_at = excluded.dismissed_at`);
5520
5649
  const ts = now();
5521
- const tx = db.transaction((list) => {
5650
+ const tx = transaction(db, (list) => {
5522
5651
  for (const id of list)
5523
5652
  stmt.run(id, note ?? null, ts);
5524
5653
  });
@@ -5528,9 +5657,9 @@ function dismiss(db, ids, note) {
5528
5657
  function reopen(db, ids) {
5529
5658
  const stmt = db.prepare("DELETE FROM dismissed_findings WHERE finding_id = ?");
5530
5659
  let n = 0;
5531
- const tx = db.transaction((list) => {
5660
+ const tx = transaction(db, (list) => {
5532
5661
  for (const id of list)
5533
- n += stmt.run(id).changes;
5662
+ n += Number(stmt.run(id).changes);
5534
5663
  });
5535
5664
  tx(ids);
5536
5665
  return n;
@@ -5540,14 +5669,14 @@ function muteRule(db, ruleId, scope = "global", scopeId = "", note) {
5540
5669
  ON CONFLICT(rule_id, scope, scope_id) DO UPDATE SET note = excluded.note, muted_at = excluded.muted_at`).run(ruleId, scope, scope === "global" ? "" : scopeId, note ?? null, now());
5541
5670
  }
5542
5671
  function unmute(db, ruleId, scope = "global", scopeId = "") {
5543
- return db.prepare("DELETE FROM muted_rules WHERE rule_id = ? AND scope = ? AND scope_id = ?").run(ruleId, scope, scope === "global" ? "" : scopeId).changes;
5672
+ return Number(db.prepare("DELETE FROM muted_rules WHERE rule_id = ? AND scope = ? AND scope_id = ?").run(ruleId, scope, scope === "global" ? "" : scopeId).changes);
5544
5673
  }
5545
5674
  function listMutes(db) {
5546
5675
  return db.prepare("SELECT rule_id, scope, scope_id, note, muted_at FROM muted_rules ORDER BY muted_at DESC").all();
5547
5676
  }
5548
5677
 
5549
5678
  // ../server/dist/about.js
5550
- import { statSync as statSync4 } from "fs";
5679
+ import { statSync as statSync4 } from "node:fs";
5551
5680
  function storage(db, dbPath) {
5552
5681
  const row = tableExists(db, "ingest_state") ? queryGet(db, "SELECT COUNT(*) files, SUM(size) bytes, MAX(ingested_at) last FROM ingest_state") : void 0;
5553
5682
  let db_bytes = null;
@@ -5564,6 +5693,13 @@ function storage(db, dbPath) {
5564
5693
  last_ingested: row?.last ?? null
5565
5694
  };
5566
5695
  }
5696
+ function unpriced(db) {
5697
+ if (!tableExists(db, "token_usage"))
5698
+ return [];
5699
+ const rows = queryAll(db, `SELECT DISTINCT model FROM token_usage
5700
+ WHERE input_tokens + output_tokens + cache_creation_input_tokens + cache_read_input_tokens > 0`);
5701
+ return unpricedModels(rows.map((r) => r.model));
5702
+ }
5567
5703
  function engineVersion(db, table, sql, expected) {
5568
5704
  const in_data = tableExists(db, table) ? queryAll(db, sql).map((r) => r.v).filter((v) => Number.isFinite(v)) : [];
5569
5705
  return { expected, in_data, stale: in_data.some((v) => v < expected) };
@@ -5594,6 +5730,13 @@ function about(db, ctx) {
5594
5730
  },
5595
5731
  server: { host: ctx.host, port: ctx.port, loopback_only: ctx.loopbackOnly },
5596
5732
  retention: { versions_keep_days: retention.keepDays, origin: retention.origin },
5733
+ pricing: {
5734
+ origin: ctx.pricing.origin,
5735
+ models: Object.keys(ctx.pricing.table).length,
5736
+ applied: ctx.pricing.applied,
5737
+ invalid: ctx.pricing.invalid,
5738
+ unpriced: unpriced(db)
5739
+ },
5597
5740
  sources: loadSources().map((s) => ({ label: s.label, agent: s.agent, config_dir: s.configDir })),
5598
5741
  // storage() already carries last_ingested from the same MAX(ingested_at) it aggregates over —
5599
5742
  // reusing it avoids a second query AND keeps the tableExists guard that lastIngested() lacks.
@@ -5886,6 +6029,7 @@ async function startServer(overrides = {}) {
5886
6029
  const repoRoot = findRepoRoot(here);
5887
6030
  const { path: dbPath, origin: dbOrigin } = resolveDbPath(overrides.db);
5888
6031
  const { host, port } = resolveServerConfig({ port: overrides.port, host: overrides.host });
6032
+ const pricing = installPricing();
5889
6033
  const webDist = resolveWebDist(here, repoRoot);
5890
6034
  if (host !== "127.0.0.1" && host !== "localhost" && !process.env.AGENT_LENS_ALLOW_NONLOCAL) {
5891
6035
  console.error(`agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`);
@@ -5902,7 +6046,7 @@ async function startServer(overrides = {}) {
5902
6046
  webDist,
5903
6047
  triageDbPath,
5904
6048
  enforceLoopbackHost,
5905
- about: { db: { path: dbPath, origin: dbOrigin }, host, port, loopbackOnly: enforceLoopbackHost, repoRoot }
6049
+ about: { db: { path: dbPath, origin: dbOrigin }, host, port, loopbackOnly: enforceLoopbackHost, repoRoot, pricing }
5906
6050
  });
5907
6051
  try {
5908
6052
  await app.listen({ host, port });
@@ -5916,8 +6060,8 @@ async function startServer(overrides = {}) {
5916
6060
  }
5917
6061
 
5918
6062
  // src/watch.ts
5919
- import { existsSync as existsSync9 } from "fs";
5920
- import { join as join11 } from "path";
6063
+ import { existsSync as existsSync9 } from "node:fs";
6064
+ import { join as join11 } from "node:path";
5921
6065
  import chokidar from "chokidar";
5922
6066
  async function runWatch(opts = {}) {
5923
6067
  const sources = loadSources();
@@ -6070,6 +6214,7 @@ cli.command("config", "Show the effective configuration (sources, paths, server)
6070
6214
  const archive = resolveArchiveDir(repoRoot);
6071
6215
  const triageDb = triageDbFor(dbPath);
6072
6216
  const { keepDays, origin: keepOrigin } = resolveRetention();
6217
+ const pricing = resolvePricing();
6073
6218
  console.log("agent-lens config (precedence: flag > env > config file > default)\n");
6074
6219
  console.log("Paths");
6075
6220
  console.log(` config file ${configFile ?? "(none \u2014 using built-in default source)"}`);
@@ -6083,6 +6228,10 @@ cli.command("config", "Show the effective configuration (sources, paths, server)
6083
6228
  console.log(` non-local bind ${process.env.AGENT_LENS_ALLOW_NONLOCAL ? "allowed (AGENT_LENS_ALLOW_NONLOCAL)" : "blocked (loopback only)"}`);
6084
6229
  console.log("\nRetention");
6085
6230
  console.log(` .versions keep ${keepDays} day(s) [${keepOrigin}]`);
6231
+ console.log("\nPricing");
6232
+ console.log(` models priced ${Object.keys(pricing.table).length} [${pricing.origin}]`);
6233
+ if (pricing.applied.length) console.log(` overrides ${pricing.applied.join(", ")}`);
6234
+ if (pricing.invalid.length) console.log(` ignored (bad) ${pricing.invalid.join(", ")}`);
6086
6235
  console.log("\nSources");
6087
6236
  for (const s of loadSources()) console.log(` ${s.label.padEnd(14)} ${s.configDir} (${s.agent})`);
6088
6237
  });