@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.
- package/README.md +5 -3
- package/dist/agent-lens.js +331 -182
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -2
- package/web/404.html +1 -1
- package/web/assets/AboutView-ttDCxp0d.js +1 -0
- package/web/assets/{Dashboard-B9-O6UM7.js → Dashboard-CdjRQBg_.js} +1 -1
- package/web/assets/{FileView-D4iZxHta.js → FileView-0gYNqeF8.js} +1 -1
- package/web/assets/{FilesView-bjBm-f1c.js → FilesView-CMwzNpYU.js} +1 -1
- package/web/assets/{SecurityView-DNgbLbZ2.js → SecurityView-B8QFRIOR.js} +1 -1
- package/web/assets/{SessionView-BCz9AnOz.js → SessionView-C3NBGoRm.js} +8 -8
- package/web/assets/SkillView-ChrzRaaf.js +1 -0
- package/web/assets/{SkillsView-Dvke2ty1.js → SkillsView-BS-Hhx-l.js} +1 -1
- package/web/assets/{WorkflowView-BadGoeGI.js → WorkflowView-BohpjfRN.js} +1 -1
- package/web/assets/{index-CqWARNqS.js → index-0iQ2t2WR.js} +4 -4
- package/web/assets/{index-DxkiGVr3.js → index-DWa5WNaY.js} +1 -1
- package/web/assets/{jsonish-SimOVe9L.js → jsonish-Bvk9aoZ9.js} +1 -1
- package/web/assets/{severity-DJTsjBgl.js → severity-FT-qpBVJ.js} +1 -1
- package/web/index.html +1 -1
- package/web/assets/AboutView-Bngkx739.js +0 -1
- package/web/assets/SkillView-D6egSywk.js +0 -1
package/dist/agent-lens.js
CHANGED
|
@@ -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(
|
|
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 (
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
|
2240
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2147
2241
|
function openRaw(file) {
|
|
2148
|
-
const db = new
|
|
2149
|
-
db.
|
|
2150
|
-
db.
|
|
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.
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
3523
|
-
|
|
3524
|
-
db
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
3963
|
+
runNamed(stmts.insTokens, parsed.tokenUsage);
|
|
3864
3964
|
if (parsed.toolCalls)
|
|
3865
3965
|
for (const tc of parsed.toolCalls)
|
|
3866
|
-
stmts.insTool
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
4451
|
-
db.
|
|
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
|
|
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
|
|
4551
|
-
|
|
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 {
|
|
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
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
const
|
|
5194
|
-
|
|
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
|
|
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
|
-
|
|
5387
|
+
unpriced2.push(u.model);
|
|
5259
5388
|
}
|
|
5260
5389
|
const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
|
|
5261
|
-
const
|
|
5262
|
-
${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL
|
|
5263
|
-
const
|
|
5264
|
-
${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL
|
|
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(
|
|
5279
|
-
turn_duration_ms:
|
|
5280
|
-
session_duration_ms:
|
|
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
|
|
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
|
|
5512
|
-
db.
|
|
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 =
|
|
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 =
|
|
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
|
});
|