@swestash/agent-lens 0.7.0 → 0.8.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,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync8 } from "fs";
5
- import { dirname as dirname6, join as join12 } from "path";
4
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
5
+ import { dirname as dirname6, join as join13 } from "path";
6
6
  import { fileURLToPath as fileURLToPath3 } from "url";
7
7
 
8
8
  // ../../node_modules/.pnpm/cac@6.7.14/node_modules/cac/dist/index.mjs
@@ -947,6 +947,245 @@ var SECURITY_CATEGORIES = [
947
947
  }
948
948
  ];
949
949
 
950
+ // ../core/dist/secrets.js
951
+ var LEAK_PATTERNS = [
952
+ { name: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
953
+ { name: "home-path-with-user", re: /\/(?:home|Users)\/(?!user\b)[A-Za-z0-9._-]+/ },
954
+ { name: "ipv4", re: /\b\d{1,3}(?:\.\d{1,3}){3}\b/ },
955
+ { name: "url", re: /https?:\/\/[^\s"']+/ },
956
+ { name: "aws-key", re: /AKIA[0-9A-Z]{16}/ },
957
+ { name: "bearer/token", re: /\b(?:sk|pk|ghp|gho|xox[abp])[-_][A-Za-z0-9]{8,}/ }
958
+ ];
959
+ var SECRET_PATTERNS = [
960
+ { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g, label: "PRIVATE-KEY" },
961
+ { re: /\bAKIA[0-9A-Z]{16}\b/g, label: "AWS-KEY" },
962
+ { re: /\bASIA[0-9A-Z]{16}\b/g, label: "AWS-KEY" },
963
+ { re: /\bgithub_pat_[A-Za-z0-9_]{20,}/g, label: "GITHUB-TOKEN" },
964
+ { re: /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g, label: "GITHUB-TOKEN" },
965
+ { re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, label: "SLACK-TOKEN" },
966
+ { re: /\bxapp-[0-9A-Za-z-]{10,}/g, label: "SLACK-TOKEN" },
967
+ { re: /\bnpm_[A-Za-z0-9]{30,}/g, label: "NPM-TOKEN" },
968
+ { re: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: "GOOGLE-API-KEY" },
969
+ { re: /\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,}\b/g, label: "STRIPE-KEY" },
970
+ { re: /\bsk-(?:proj|ant|or|svcacct)-[A-Za-z0-9_-]{20,}/g, label: "API-KEY" },
971
+ { re: /\bsk-[A-Za-z0-9]{20,}\b/g, label: "API-KEY" },
972
+ { re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g, label: "JWT" }
973
+ ];
974
+ function maskUrlCredentials(text) {
975
+ return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/\s:@"'`]+:[^/\s@"'`]+@/gi, "$1[REDACTED-AUTH]@").replace(/([?&](?:access_?token|api[_-]?key|apikey|token|secret|password|pwd|client[_-]?secret|auth|signature|sig)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
976
+ }
977
+ var ENV_ASSIGN = /\b([A-Za-z][A-Za-z0-9_]*_(?:TOKEN|SECRET|KEY|PASSWORD|PASSWD|PWD|PW|CREDENTIALS?)|PASSWORD|PASSWD|SECRET|TOKEN)=(["']?)(?!\$)[^\s"'`&;|<>()]+/gi;
978
+ function maskEnvAssignments(text) {
979
+ return text.replace(ENV_ASSIGN, (_m, name, q) => `${name}=${q}[REDACTED]`);
980
+ }
981
+ var PII_PATTERNS = [
982
+ { re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, label: "EMAIL" },
983
+ { re: /\b\d{1,3}(?:\.\d{1,3}){3}\b/g, label: "IP" }
984
+ ];
985
+ var HOME_USER = /\/(home|Users)\/[^/\s"'`]+/g;
986
+ var ENCODED_HOME_USER = /(?<![A-Za-z0-9])(-(?:home|Users)-)[A-Za-z0-9._]+(?=-)/g;
987
+ var WINDOWS_USER = /([A-Za-z]:\\Users\\)[^\\/\s"'`]+/gi;
988
+ function maskSecrets(text) {
989
+ let out = maskUrlCredentials(text);
990
+ out = maskEnvAssignments(out);
991
+ for (const { re, label } of SECRET_PATTERNS)
992
+ out = out.replace(re, `[${label}]`);
993
+ for (const { re, label } of PII_PATTERNS)
994
+ out = out.replace(re, `[${label}]`);
995
+ out = out.replace(HOME_USER, (_m, root) => `/${root}/user`);
996
+ out = out.replace(ENCODED_HOME_USER, (_m, root) => `${root}user`);
997
+ out = out.replace(WINDOWS_USER, (_m, root) => `${root}user`);
998
+ return out;
999
+ }
1000
+ var SHARE_LEAK = [
1001
+ ...SECRET_PATTERNS.map(({ re, label }) => ({ name: label, re: new RegExp(re.source) })),
1002
+ { name: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
1003
+ { name: "home-path-with-user", re: /\/(?:home|Users)\/(?!user\b)[A-Za-z0-9._-]+/ },
1004
+ { name: "encoded-home-with-user", re: /(?<![A-Za-z0-9])-(?:home|Users)-(?!user-)[A-Za-z0-9._]+-/ },
1005
+ { name: "url-credentials", re: /:\/\/[^/\s:@"'`]+:[^/\s@"'`]+@/ }
1006
+ ];
1007
+ var USER_STOPLIST = /* @__PURE__ */ new Set([
1008
+ "user",
1009
+ "users",
1010
+ "home",
1011
+ "root",
1012
+ "admin",
1013
+ "ubuntu",
1014
+ "runner",
1015
+ "node",
1016
+ "app",
1017
+ "apps",
1018
+ "data",
1019
+ "tmp",
1020
+ "shared",
1021
+ "public",
1022
+ "guest",
1023
+ "vagrant",
1024
+ "ec2-user",
1025
+ "docker"
1026
+ ]);
1027
+ var HOME_OWNER = /(?:\/(?:home|Users)\/|-(?:home|Users)-|[A-Za-z]:\\Users\\)([A-Za-z0-9._]{3,})/g;
1028
+ function deriveHomeUsers(texts) {
1029
+ const found = /* @__PURE__ */ new Set();
1030
+ for (const t of texts) {
1031
+ if (!t)
1032
+ continue;
1033
+ const re = new RegExp(HOME_OWNER.source, "g");
1034
+ let m;
1035
+ while (m = re.exec(t)) {
1036
+ const u = m[1];
1037
+ if (u.length >= 3 && !USER_STOPLIST.has(u.toLowerCase()))
1038
+ found.add(u);
1039
+ }
1040
+ }
1041
+ return [...found];
1042
+ }
1043
+ function scrubUsernames(text, users) {
1044
+ let out = text;
1045
+ for (const u of users) {
1046
+ const esc = u.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
1047
+ out = out.replace(new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, "g"), "[USER]");
1048
+ }
1049
+ return out;
1050
+ }
1051
+
1052
+ // ../core/dist/redact-export.js
1053
+ import { createHash } from "crypto";
1054
+
1055
+ // ../core/dist/markdown.js
1056
+ function fence(s) {
1057
+ return s.length > 2e3 ? s.slice(0, 2e3) + "\n\u2026 (truncated)" : s;
1058
+ }
1059
+ function sessionToMarkdown(s, events) {
1060
+ const out = [];
1061
+ out.push(`# ${s.title || "Session " + s.id.slice(0, 8)}`);
1062
+ out.push("");
1063
+ const meta = [
1064
+ ["Session", s.id],
1065
+ ["Source", s.source],
1066
+ ["Project", s.project],
1067
+ ["Model", s.model],
1068
+ ["Started", s.started_at],
1069
+ ["Ended", s.ended_at]
1070
+ ].filter(([, v]) => v);
1071
+ for (const [k, v] of meta)
1072
+ out.push(`- **${k}:** ${v}`);
1073
+ out.push("");
1074
+ out.push("---");
1075
+ out.push("");
1076
+ for (const e of events) {
1077
+ const who = e.role || e.type;
1078
+ const ts = e.timestamp ? ` _(${e.timestamp})_` : "";
1079
+ if (e.thinking) {
1080
+ out.push(`### \u{1F9E0} thinking${ts}`);
1081
+ out.push("");
1082
+ out.push(fence(e.thinking));
1083
+ out.push("");
1084
+ }
1085
+ if (e.text) {
1086
+ const icon = who === "user" ? "\u{1F464}" : who === "assistant" ? "\u{1F916}" : "\u2699\uFE0F";
1087
+ out.push(`### ${icon} ${who}${ts}`);
1088
+ out.push("");
1089
+ out.push(fence(e.text));
1090
+ out.push("");
1091
+ }
1092
+ for (const tc of e.toolCalls) {
1093
+ const label = tc.skill_name ? `Skill: ${tc.skill_name}` : tc.agent_type ? `${tc.tool_name} \u2192 ${tc.agent_type}` : tc.tool_name;
1094
+ const status = tc.status ? ` _(${tc.status})_` : "";
1095
+ out.push(`> \u{1F527} **${label}**${status}`);
1096
+ if (tc.input_json && tc.input_json !== "{}") {
1097
+ out.push("```json");
1098
+ out.push(fence(tc.input_json));
1099
+ out.push("```");
1100
+ }
1101
+ out.push("");
1102
+ }
1103
+ }
1104
+ return out.join("\n");
1105
+ }
1106
+
1107
+ // ../core/dist/redact-export.js
1108
+ function hash(s) {
1109
+ return createHash("sha256").update(s).digest("hex").slice(0, 4);
1110
+ }
1111
+ function structurePath(p) {
1112
+ if (!p)
1113
+ return p;
1114
+ const abs = p.startsWith("/");
1115
+ const segs = p.split("/").filter(Boolean);
1116
+ const out = segs.map((seg, i) => {
1117
+ if (segs[i - 1] === "home" || segs[i - 1] === "Users")
1118
+ return "user";
1119
+ if (seg === "home" || seg === "Users")
1120
+ return seg;
1121
+ const last = i === segs.length - 1;
1122
+ const dot = last ? seg.lastIndexOf(".") : -1;
1123
+ return dot > 0 ? `file-${hash(seg)}${seg.slice(dot)}` : `dir-${hash(seg)}`;
1124
+ });
1125
+ return (abs ? "/" : "") + out.join("/");
1126
+ }
1127
+ function scrub(s) {
1128
+ return s && s.length > 0 ? "[redacted]" : s;
1129
+ }
1130
+ function redactSession(session, events, level) {
1131
+ const users = deriveHomeUsers([
1132
+ session.title,
1133
+ session.project,
1134
+ session.source,
1135
+ ...events.flatMap((e) => [e.text, e.thinking, ...e.toolCalls.map((t) => t.input_json)])
1136
+ ]);
1137
+ const unuser = (s) => s == null ? s : scrubUsernames(s, users);
1138
+ if (level === "structure") {
1139
+ return {
1140
+ session: { ...session, title: session.title ? "Redacted session" : session.title, project: structurePath(session.project), source: unuser(session.source) },
1141
+ events: events.map((e) => ({
1142
+ ...e,
1143
+ text: scrub(e.text),
1144
+ thinking: scrub(e.thinking),
1145
+ toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: t.input_json ? "{}" : t.input_json }))
1146
+ }))
1147
+ };
1148
+ }
1149
+ const mask = (s) => s == null ? s : scrubUsernames(maskSecrets(s), users);
1150
+ return {
1151
+ session: { ...session, title: mask(session.title), project: mask(session.project), source: mask(session.source) },
1152
+ events: events.map((e) => ({
1153
+ ...e,
1154
+ text: mask(e.text),
1155
+ thinking: mask(e.thinking),
1156
+ toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: mask(t.input_json) }))
1157
+ }))
1158
+ };
1159
+ }
1160
+ function sweep(md, patterns) {
1161
+ let out = md;
1162
+ let caught = false;
1163
+ for (const { re } of patterns) {
1164
+ const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
1165
+ if (g.test(out)) {
1166
+ caught = true;
1167
+ out = out.replace(g, "[REDACTED]");
1168
+ }
1169
+ }
1170
+ return { md: out, caught };
1171
+ }
1172
+ function disclaimer(level) {
1173
+ return `> \u{1F512} **Redacted export** (level: ${level}) \u2014 best-effort automatic masking of secrets, tokens, emails, IPs, and usernames. Not a guarantee; review before sharing. Generated locally by Agent Lens \u2014 never uploaded.`;
1174
+ }
1175
+ function exportMarkdown(session, events, opts = {}) {
1176
+ const level = opts.level ?? "secrets";
1177
+ if (level === "off") {
1178
+ return { markdown: sessionToMarkdown(session, events), residualLeak: false, level: "off" };
1179
+ }
1180
+ const redacted = redactSession(session, events, level);
1181
+ const rendered = sessionToMarkdown(redacted.session, redacted.events);
1182
+ const scanSet = level === "structure" ? LEAK_PATTERNS : SHARE_LEAK;
1183
+ const { md, caught } = sweep(rendered, scanSet);
1184
+ return { markdown: `${disclaimer(level)}
1185
+
1186
+ ${md}`, residualLeak: caught, level };
1187
+ }
1188
+
950
1189
  // ../core/dist/errors.js
951
1190
  var ERROR_CLASSIFIER_VERSION = 1;
952
1191
  var REJECTION_TYPES = /* @__PURE__ */ new Set(["user-rejected", "guardrail-blocked"]);
@@ -1010,58 +1249,6 @@ function costForUsage(model, u) {
1010
1249
  return (u.input_tokens * r.input + u.output_tokens * r.output + u.cache_creation_input_tokens * r.cacheWrite + u.cache_read_input_tokens * r.cacheRead) / M;
1011
1250
  }
1012
1251
 
1013
- // ../core/dist/markdown.js
1014
- function fence(s) {
1015
- return s.length > 2e3 ? s.slice(0, 2e3) + "\n\u2026 (truncated)" : s;
1016
- }
1017
- function sessionToMarkdown(s, events) {
1018
- const out = [];
1019
- out.push(`# ${s.title || "Session " + s.id.slice(0, 8)}`);
1020
- out.push("");
1021
- const meta = [
1022
- ["Session", s.id],
1023
- ["Source", s.source],
1024
- ["Project", s.project],
1025
- ["Model", s.model],
1026
- ["Started", s.started_at],
1027
- ["Ended", s.ended_at]
1028
- ].filter(([, v]) => v);
1029
- for (const [k, v] of meta)
1030
- out.push(`- **${k}:** ${v}`);
1031
- out.push("");
1032
- out.push("---");
1033
- out.push("");
1034
- for (const e of events) {
1035
- const who = e.role || e.type;
1036
- const ts = e.timestamp ? ` _(${e.timestamp})_` : "";
1037
- if (e.thinking) {
1038
- out.push(`### \u{1F9E0} thinking${ts}`);
1039
- out.push("");
1040
- out.push(fence(e.thinking));
1041
- out.push("");
1042
- }
1043
- if (e.text) {
1044
- const icon = who === "user" ? "\u{1F464}" : who === "assistant" ? "\u{1F916}" : "\u2699\uFE0F";
1045
- out.push(`### ${icon} ${who}${ts}`);
1046
- out.push("");
1047
- out.push(fence(e.text));
1048
- out.push("");
1049
- }
1050
- for (const tc of e.toolCalls) {
1051
- const label = tc.skill_name ? `Skill: ${tc.skill_name}` : tc.agent_type ? `${tc.tool_name} \u2192 ${tc.agent_type}` : tc.tool_name;
1052
- const status = tc.status ? ` _(${tc.status})_` : "";
1053
- out.push(`> \u{1F527} **${label}**${status}`);
1054
- if (tc.input_json && tc.input_json !== "{}") {
1055
- out.push("```json");
1056
- out.push(fence(tc.input_json));
1057
- out.push("```");
1058
- }
1059
- out.push("");
1060
- }
1061
- }
1062
- return out.join("\n");
1063
- }
1064
-
1065
1252
  // ../core/dist/rawjson.js
1066
1253
  import { gzipSync, gunzipSync } from "zlib";
1067
1254
  function packRaw(json) {
@@ -1152,14 +1339,72 @@ function resolveWebDist(fromDir, repoRoot = findRepoRoot(fromDir)) {
1152
1339
  return installed;
1153
1340
  }
1154
1341
 
1155
- // ../core/dist/sources.js
1342
+ // ../core/dist/config.js
1156
1343
  import { readFileSync as readFileSync2 } from "fs";
1157
- function readConfig() {
1344
+ import { join as join2 } from "path";
1345
+ var DEFAULT_PORT = 4477;
1346
+ var DEFAULT_HOST = "127.0.0.1";
1347
+ var DEFAULT_DB_NAME = "agent-lens.db";
1348
+ function readConfigFile() {
1158
1349
  const repoRoot = findRepoRoot();
1159
1350
  const dataDir = resolveDataDir(repoRoot);
1160
1351
  const file = resolveConfigFile(repoRoot, dataDir);
1161
1352
  return file ? JSON.parse(readFileSync2(file, "utf8")) : null;
1162
1353
  }
1354
+ function resolveDbPath(override, cfg = readConfigFile()) {
1355
+ if (override)
1356
+ return { path: override, origin: "flag" };
1357
+ if (process.env.AGENT_LENS_DB)
1358
+ return { path: process.env.AGENT_LENS_DB, origin: "env" };
1359
+ if (isSet(cfg?.db))
1360
+ return { path: expandPath(String(cfg.db).trim()), origin: "file" };
1361
+ return { path: join2(resolveDataDir(findRepoRoot()), DEFAULT_DB_NAME), origin: "default" };
1362
+ }
1363
+ function validatePort(value, source) {
1364
+ const n = typeof value === "number" ? value : Number(String(value).trim());
1365
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
1366
+ throw new Error(`agent-lens: invalid port from ${source}: '${value}' (expected an integer 1-65535)`);
1367
+ }
1368
+ return n;
1369
+ }
1370
+ function isSet(v) {
1371
+ return v !== void 0 && v !== null && String(v).trim() !== "";
1372
+ }
1373
+ function resolveServerConfig(overrides = {}, cfg = readConfigFile()) {
1374
+ let port;
1375
+ let portOrigin;
1376
+ if (isSet(overrides.port)) {
1377
+ port = validatePort(overrides.port, "--port");
1378
+ portOrigin = "flag";
1379
+ } else if (isSet(process.env.AGENT_LENS_PORT)) {
1380
+ port = validatePort(process.env.AGENT_LENS_PORT, "AGENT_LENS_PORT");
1381
+ portOrigin = "env";
1382
+ } else if (isSet(cfg?.server?.port)) {
1383
+ port = validatePort(cfg.server.port, "server.port (config file)");
1384
+ portOrigin = "file";
1385
+ } else {
1386
+ port = DEFAULT_PORT;
1387
+ portOrigin = "default";
1388
+ }
1389
+ let host;
1390
+ let hostOrigin;
1391
+ if (isSet(overrides.host)) {
1392
+ host = String(overrides.host).trim();
1393
+ hostOrigin = "flag";
1394
+ } else if (isSet(process.env.AGENT_LENS_HOST)) {
1395
+ host = process.env.AGENT_LENS_HOST.trim();
1396
+ hostOrigin = "env";
1397
+ } else if (isSet(cfg?.server?.host)) {
1398
+ host = String(cfg.server.host).trim();
1399
+ hostOrigin = "file";
1400
+ } else {
1401
+ host = DEFAULT_HOST;
1402
+ hostOrigin = "default";
1403
+ }
1404
+ return { port, host, portOrigin, hostOrigin };
1405
+ }
1406
+
1407
+ // ../core/dist/sources.js
1163
1408
  function loadSources() {
1164
1409
  if (process.env.CLAUDE_DIR) {
1165
1410
  return [
@@ -1170,7 +1415,7 @@ function loadSources() {
1170
1415
  }
1171
1416
  ];
1172
1417
  }
1173
- const cfg = readConfig() ?? { sources: [{ label: "default", agent: "claude-code", configDir: "$HOME/.claude" }] };
1418
+ const cfg = readConfigFile() ?? { sources: [{ label: "default", agent: "claude-code", configDir: "$HOME/.claude" }] };
1174
1419
  const seen = /* @__PURE__ */ new Set();
1175
1420
  const out = [];
1176
1421
  for (const raw of cfg.sources || []) {
@@ -1190,7 +1435,7 @@ function loadExcludes() {
1190
1435
  const fromEnv = (process.env.AGENT_LENS_EXCLUDE || "").split(",");
1191
1436
  let fromCfg = [];
1192
1437
  if (!process.env.CLAUDE_DIR) {
1193
- const cfg = readConfig();
1438
+ const cfg = readConfigFile();
1194
1439
  if (cfg && Array.isArray(cfg.exclude))
1195
1440
  fromCfg = cfg.exclude;
1196
1441
  }
@@ -1213,7 +1458,7 @@ function encodeProjectPath(p) {
1213
1458
 
1214
1459
  // ../core/dist/collect.js
1215
1460
  import { appendFileSync, closeSync, copyFileSync, existsSync as existsSync2, mkdirSync, openSync, readSync, readdirSync, statSync, unlinkSync, utimesSync, writeSync, chmodSync } from "fs";
1216
- import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
1461
+ import { basename, dirname as dirname2, join as join3, relative, sep } from "path";
1217
1462
  import { platform as platform2 } from "os";
1218
1463
  var POSIX = platform2() !== "win32";
1219
1464
  var DIR_MODE = 448;
@@ -1293,19 +1538,19 @@ function filesEqual(a, b) {
1293
1538
  return prefixEquals(a, b, sa.size);
1294
1539
  }
1295
1540
  function latestSnapshot(archive, relParts) {
1296
- const versRoot = join2(archive, ".versions");
1541
+ const versRoot = join3(archive, ".versions");
1297
1542
  if (!existsSync2(versRoot))
1298
1543
  return null;
1299
1544
  const stamps = readdirSync(versRoot).sort();
1300
1545
  for (let i = stamps.length - 1; i >= 0; i--) {
1301
- const p = join2(versRoot, stamps[i], ...relParts);
1546
+ const p = join3(versRoot, stamps[i], ...relParts);
1302
1547
  if (existsSync2(p))
1303
1548
  return p;
1304
1549
  }
1305
1550
  return null;
1306
1551
  }
1307
1552
  function snapshot(absPath, relParts, versionsDir, stats) {
1308
- const dst = join2(versionsDir, ...relParts);
1553
+ const dst = join3(versionsDir, ...relParts);
1309
1554
  ensureDir(dirname2(dst));
1310
1555
  copyFileSync(absPath, dst);
1311
1556
  syncMeta(dst, statSync(absPath));
@@ -1313,7 +1558,7 @@ function snapshot(absPath, relParts, versionsDir, stats) {
1313
1558
  }
1314
1559
  function walkCollectable(dir, out, inWorkflows = false, inToolResults = false) {
1315
1560
  for (const e of readdirSync(dir, { withFileTypes: true })) {
1316
- const p = join2(dir, e.name);
1561
+ const p = join3(dir, e.name);
1317
1562
  if (e.isDirectory()) {
1318
1563
  walkCollectable(p, out, inWorkflows || e.name === "workflows", inToolResults || e.name === "tool-results");
1319
1564
  } else if (e.isFile() && (e.name.endsWith(".jsonl") || inWorkflows || inToolResults || e.name.endsWith(".meta.json"))) {
@@ -1354,7 +1599,7 @@ function removeCredentials(dir) {
1354
1599
  if (!existsSync2(dir))
1355
1600
  return;
1356
1601
  for (const e of readdirSync(dir, { withFileTypes: true })) {
1357
- const p = join2(dir, e.name);
1602
+ const p = join3(dir, e.name);
1358
1603
  if (e.isDirectory())
1359
1604
  removeCredentials(p);
1360
1605
  else if (e.name === ".credentials.json") {
@@ -1367,19 +1612,19 @@ function removeCredentials(dir) {
1367
1612
  }
1368
1613
  function collectSource(source, archiveBase, excludedDirs, runTs, stats, log) {
1369
1614
  const { label, configDir } = source;
1370
- const archive = join2(archiveBase, label);
1371
- const versionsDir = join2(archive, ".versions", runTs);
1615
+ const archive = join3(archiveBase, label);
1616
+ const versionsDir = join3(archive, ".versions", runTs);
1372
1617
  if (!existsSync2(configDir)) {
1373
1618
  log(`agent-lens: [${label}] source not found: ${configDir} (skipping)`);
1374
1619
  return;
1375
1620
  }
1376
- ensureDir(join2(archive, "projects"));
1377
- ensureDir(join2(archive, "settings"));
1621
+ ensureDir(join3(archive, "projects"));
1622
+ ensureDir(join3(archive, "settings"));
1378
1623
  const files = [];
1379
- const projectsDir = join2(configDir, "projects");
1624
+ const projectsDir = join3(configDir, "projects");
1380
1625
  if (existsSync2(projectsDir))
1381
1626
  walkCollectable(projectsDir, files);
1382
- const historySrc = join2(configDir, "history.jsonl");
1627
+ const historySrc = join3(configDir, "history.jsonl");
1383
1628
  if (existsSync2(historySrc))
1384
1629
  files.push(historySrc);
1385
1630
  const before = { scanned: stats.scanned, snapshots: stats.snapshots };
@@ -1392,13 +1637,13 @@ function collectSource(source, archiveBase, excludedDirs, runTs, stats, log) {
1392
1637
  if (excludedDirs.some((enc) => relPosix.startsWith(`projects/${enc}/`)))
1393
1638
  continue;
1394
1639
  stats.scanned++;
1395
- syncFile(src, join2(archive, ...relParts), relParts, archive, versionsDir, stats, base.endsWith(".jsonl"));
1640
+ syncFile(src, join3(archive, ...relParts), relParts, archive, versionsDir, stats, base.endsWith(".jsonl"));
1396
1641
  }
1397
1642
  for (const f of ["settings.json", "settings.local.json"]) {
1398
- const s = join2(configDir, f);
1643
+ const s = join3(configDir, f);
1399
1644
  if (!existsSync2(s))
1400
1645
  continue;
1401
- const a = join2(archive, "settings", f);
1646
+ const a = join3(archive, "settings", f);
1402
1647
  if (existsSync2(a) && !filesEqual(s, a))
1403
1648
  snapshot(a, ["settings", f], versionsDir, stats);
1404
1649
  ensureDir(dirname2(a));
@@ -1411,13 +1656,13 @@ function collectSource(source, archiveBase, excludedDirs, runTs, stats, log) {
1411
1656
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} run=${runTs} source=${label} scanned=${scanned} snapshots=${snapshots} archive=${archive}`;
1412
1657
  log(`agent-lens: ${line}`);
1413
1658
  try {
1414
- appendFileSync(join2(archiveBase, ".collect.log"), line + "\n");
1659
+ appendFileSync(join3(archiveBase, ".collect.log"), line + "\n");
1415
1660
  } catch {
1416
1661
  }
1417
1662
  }
1418
1663
  function collectAll(opts = {}) {
1419
1664
  const log = opts.log ?? ((m) => console.log(m));
1420
- const archiveBase = opts.archiveBase ?? join2(resolveDataDir(findRepoRoot()), "archive");
1665
+ const archiveBase = opts.archiveBase ?? join3(resolveDataDir(findRepoRoot()), "archive");
1421
1666
  const sources = opts.sources ?? loadSources();
1422
1667
  const excludedDirs = (opts.excludes ?? loadExcludes()).map(encodeProjectPath);
1423
1668
  const runTs = opts.runTimestamp ?? collectTimestamp();
@@ -1478,7 +1723,7 @@ function acquireLock(lockPath, staleMs = 6 * 60 * 60 * 1e3) {
1478
1723
  import { spawnSync } from "child_process";
1479
1724
  import { mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1480
1725
  import { homedir as homedir2, platform as platform3 } from "os";
1481
- import { join as join3 } from "path";
1726
+ import { join as join4 } from "path";
1482
1727
  var DEFAULT_HOURS = [9, 13, 17, 21];
1483
1728
  var SYSTEMD_COLLECTOR_TIMER = "agent-lens-collect.timer";
1484
1729
  var SYSTEMD_COLLECTOR_SERVICE = "agent-lens-collect.service";
@@ -1504,10 +1749,11 @@ function parseHours(spec) {
1504
1749
  }
1505
1750
  function serverEnv() {
1506
1751
  const env = {};
1507
- if (process.env.AGENT_LENS_PORT)
1508
- env.AGENT_LENS_PORT = process.env.AGENT_LENS_PORT;
1509
- if (process.env.AGENT_LENS_HOST)
1510
- env.AGENT_LENS_HOST = process.env.AGENT_LENS_HOST;
1752
+ const { port, host, portOrigin, hostOrigin } = resolveServerConfig();
1753
+ if (portOrigin !== "default")
1754
+ env.AGENT_LENS_PORT = String(port);
1755
+ if (hostOrigin !== "default")
1756
+ env.AGENT_LENS_HOST = host;
1511
1757
  return env;
1512
1758
  }
1513
1759
  function onCalendarHours(hours) {
@@ -1550,8 +1796,11 @@ After=network.target
1550
1796
  Type=simple
1551
1797
  # Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.
1552
1798
  ExecStart=${node} ${cli2} serve
1553
- ${envLines}# Long-running: restart if it crashes.
1554
- Restart=on-failure
1799
+ ${envLines}# Long-running: always come back, including after a bare SIGTERM. systemd counts SIGTERM as a
1800
+ # clean exit, so Restart=on-failure would leave the server dead after a stray "pkill -f ... serve"
1801
+ # aimed at an ad-hoc test server \u2014 the unit's ExecStart matches that pattern too. Matches the
1802
+ # launchd KeepAlive=true behaviour. An explicit "systemctl --user stop" still stops it for good.
1803
+ Restart=always
1555
1804
  RestartSec=5
1556
1805
  Nice=10
1557
1806
 
@@ -1617,7 +1866,7 @@ function have(cmd) {
1617
1866
  return r.status === 0 || r.error === void 0;
1618
1867
  }
1619
1868
  function systemdUnitDir() {
1620
- return join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "systemd", "user");
1869
+ return join4(process.env.XDG_CONFIG_HOME || join4(homedir2(), ".config"), "systemd", "user");
1621
1870
  }
1622
1871
  function linuxInstall(targets, node, cli2, hours) {
1623
1872
  if (!have("systemctl")) {
@@ -1628,11 +1877,11 @@ function linuxInstall(targets, node, cli2, hours) {
1628
1877
  const dir = systemdUnitDir();
1629
1878
  mkdirSync3(dir, { recursive: true });
1630
1879
  if (targets.includes("collector")) {
1631
- writeFileSync2(join3(dir, SYSTEMD_COLLECTOR_SERVICE), systemdCollectorService(node, cli2));
1632
- writeFileSync2(join3(dir, SYSTEMD_COLLECTOR_TIMER), systemdCollectorTimer(hours));
1880
+ writeFileSync2(join4(dir, SYSTEMD_COLLECTOR_SERVICE), systemdCollectorService(node, cli2));
1881
+ writeFileSync2(join4(dir, SYSTEMD_COLLECTOR_TIMER), systemdCollectorTimer(hours));
1633
1882
  }
1634
1883
  if (targets.includes("server")) {
1635
- writeFileSync2(join3(dir, SYSTEMD_SERVER_SERVICE), systemdServerService(node, cli2, serverEnv()));
1884
+ writeFileSync2(join4(dir, SYSTEMD_SERVER_SERVICE), systemdServerService(node, cli2, serverEnv()));
1636
1885
  }
1637
1886
  sh("systemctl", ["--user", "daemon-reload"]);
1638
1887
  if (targets.includes("collector")) {
@@ -1656,12 +1905,12 @@ function linuxUninstall(targets) {
1656
1905
  if (targets.includes("collector")) {
1657
1906
  sh("systemctl", ["--user", "disable", "--now", SYSTEMD_COLLECTOR_TIMER], true);
1658
1907
  for (const u of [SYSTEMD_COLLECTOR_TIMER, SYSTEMD_COLLECTOR_SERVICE])
1659
- rmSync(join3(dir, u), { force: true });
1908
+ rmSync(join4(dir, u), { force: true });
1660
1909
  sh("systemctl", ["--user", "reset-failed", SYSTEMD_COLLECTOR_SERVICE], true);
1661
1910
  }
1662
1911
  if (targets.includes("server")) {
1663
1912
  sh("systemctl", ["--user", "disable", "--now", SYSTEMD_SERVER_SERVICE], true);
1664
- rmSync(join3(dir, SYSTEMD_SERVER_SERVICE), { force: true });
1913
+ rmSync(join4(dir, SYSTEMD_SERVER_SERVICE), { force: true });
1665
1914
  sh("systemctl", ["--user", "reset-failed", SYSTEMD_SERVER_SERVICE], true);
1666
1915
  }
1667
1916
  sh("systemctl", ["--user", "daemon-reload"]);
@@ -1676,7 +1925,7 @@ function linuxStatus(targets) {
1676
1925
  sh("systemctl", ["--user", "status", SYSTEMD_SERVER_SERVICE, "--no-pager"]);
1677
1926
  }
1678
1927
  function launchdPlistPath(label) {
1679
- return join3(homedir2(), "Library", "LaunchAgents", `${label}.plist`);
1928
+ return join4(homedir2(), "Library", "LaunchAgents", `${label}.plist`);
1680
1929
  }
1681
1930
  function macBootstrap(plist) {
1682
1931
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
@@ -1690,18 +1939,18 @@ function macBootout(label, plist) {
1690
1939
  rmSync(plist, { force: true });
1691
1940
  }
1692
1941
  function macInstall(targets, node, cli2, hours) {
1693
- const agents = join3(homedir2(), "Library", "LaunchAgents");
1942
+ const agents = join4(homedir2(), "Library", "LaunchAgents");
1694
1943
  mkdirSync3(agents, { recursive: true });
1695
1944
  const dataDir = resolveDataDir(findRepoRoot());
1696
1945
  if (targets.includes("collector")) {
1697
1946
  const plist = launchdPlistPath(LAUNCHD_COLLECTOR_LABEL);
1698
- writeFileSync2(plist, launchdCollectorPlist(node, cli2, hours, join3(dataDir, "schedule.log")));
1947
+ writeFileSync2(plist, launchdCollectorPlist(node, cli2, hours, join4(dataDir, "schedule.log")));
1699
1948
  macBootstrap(plist);
1700
1949
  console.log(`agent-lens service: installed collector agent ${LAUNCHD_COLLECTOR_LABEL} (hours ${hours.join(",")})`);
1701
1950
  }
1702
1951
  if (targets.includes("server")) {
1703
1952
  const plist = launchdPlistPath(LAUNCHD_SERVER_LABEL);
1704
- writeFileSync2(plist, launchdServerPlist(node, cli2, join3(dataDir, "server.log"), serverEnv()));
1953
+ writeFileSync2(plist, launchdServerPlist(node, cli2, join4(dataDir, "server.log"), serverEnv()));
1705
1954
  macBootstrap(plist);
1706
1955
  console.log(`agent-lens service: installed server agent ${LAUNCHD_SERVER_LABEL}`);
1707
1956
  }
@@ -1792,7 +2041,7 @@ function runService(action, params) {
1792
2041
 
1793
2042
  // ../ingest/dist/run.js
1794
2043
  import { readFileSync as readFileSync7, statSync as statSync5, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
1795
- import { dirname as dirname4, join as join8 } from "path";
2044
+ import { dirname as dirname4, join as join9 } from "path";
1796
2045
 
1797
2046
  // ../ingest/dist/db.js
1798
2047
  import Database from "better-sqlite3";
@@ -1848,7 +2097,7 @@ function resetSchema(db) {
1848
2097
 
1849
2098
  // ../ingest/dist/adapters/claude-code.js
1850
2099
  import { readdirSync as readdirSync2 } from "fs";
1851
- import { basename as basename2, join as join4 } from "path";
2100
+ import { basename as basename2, join as join5 } from "path";
1852
2101
  function flattenText(content) {
1853
2102
  if (content == null)
1854
2103
  return null;
@@ -1913,13 +2162,13 @@ function asString(v) {
1913
2162
  function asInt(v) {
1914
2163
  return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : 0;
1915
2164
  }
1916
- function parentSessionFromPath(path) {
1917
- const parts = path.split("/");
2165
+ function parentSessionFromPath(path2) {
2166
+ const parts = path2.split("/");
1918
2167
  const i = parts.indexOf("subagents");
1919
2168
  return i > 0 ? parts[i - 1] : null;
1920
2169
  }
1921
- function workflowRunFromPath(path) {
1922
- const parts = path.split("/");
2170
+ function workflowRunFromPath(path2) {
2171
+ const parts = path2.split("/");
1923
2172
  const i = parts.indexOf("workflows");
1924
2173
  return i >= 0 && i + 1 < parts.length ? parts[i + 1] : null;
1925
2174
  }
@@ -1931,7 +2180,7 @@ function recurseJsonl(dir, out) {
1931
2180
  return;
1932
2181
  }
1933
2182
  for (const e of entries) {
1934
- const p = join4(dir, e.name);
2183
+ const p = join5(dir, e.name);
1935
2184
  if (e.isDirectory())
1936
2185
  recurseJsonl(p, out);
1937
2186
  else if (e.isFile() && e.name.endsWith(".jsonl"))
@@ -1947,27 +2196,27 @@ var ClaudeCodeAdapter = class {
1947
2196
  const addFrom = (projectsDir, isVersion) => {
1948
2197
  const found = [];
1949
2198
  recurseJsonl(projectsDir, found);
1950
- for (const path of found) {
1951
- const parts = path.split("/");
1952
- const sessionId = basename2(path, ".jsonl");
2199
+ for (const path2 of found) {
2200
+ const parts = path2.split("/");
2201
+ const sessionId = basename2(path2, ".jsonl");
1953
2202
  const encodedDir = parts[parts.length - 2] ?? "";
1954
2203
  files.push({
1955
- path,
2204
+ path: path2,
1956
2205
  sessionId,
1957
2206
  encodedDir,
1958
2207
  isVersion,
1959
2208
  sourceId,
1960
- parentSessionId: parentSessionFromPath(path),
1961
- workflowRunId: workflowRunFromPath(path)
2209
+ parentSessionId: parentSessionFromPath(path2),
2210
+ workflowRunId: workflowRunFromPath(path2)
1962
2211
  });
1963
2212
  }
1964
2213
  };
1965
- addFrom(join4(archiveRoot, "projects"), false);
1966
- const versionsRoot = join4(archiveRoot, ".versions");
2214
+ addFrom(join5(archiveRoot, "projects"), false);
2215
+ const versionsRoot = join5(archiveRoot, ".versions");
1967
2216
  try {
1968
2217
  for (const ts of readdirSync2(versionsRoot, { withFileTypes: true })) {
1969
2218
  if (ts.isDirectory())
1970
- addFrom(join4(versionsRoot, ts.name, "projects"), true);
2219
+ addFrom(join5(versionsRoot, ts.name, "projects"), true);
1971
2220
  }
1972
2221
  } catch {
1973
2222
  }
@@ -2354,14 +2603,15 @@ function classify(db, dirty) {
2354
2603
  }
2355
2604
 
2356
2605
  // ../ingest/dist/detect.js
2357
- import { createHash } from "crypto";
2358
- var DETECTOR_VERSION = 7;
2606
+ import { createHash as createHash2 } from "crypto";
2607
+ import { posix as path } from "path";
2608
+ var DETECTOR_VERSION = 8;
2359
2609
  function evidence(s, max = 200) {
2360
2610
  const one = s.replace(/\s+/g, " ").trim();
2361
2611
  return one.length > max ? one.slice(0, max - 1) + "\u2026" : one;
2362
2612
  }
2363
2613
  function findingId(toolCallId, ruleId) {
2364
- return createHash("sha1").update(toolCallId).update("\0").update(ruleId).digest("hex").slice(0, 16);
2614
+ return createHash2("sha1").update(toolCallId).update("\0").update(ruleId).digest("hex").slice(0, 16);
2365
2615
  }
2366
2616
  var SYSTEM_PATH = /(^|\s)(\/etc\/|\/usr\/|\/bin\/|\/sbin\/|\/boot\/|\/var\/|\/root\/|~\/\.ssh\/|~\/\.aws\/)/;
2367
2617
  var HOME_OR_ROOT_TARGET = /(\s|^)(~\/?|\/|\/\*|\$HOME\/?|\*)(\s|$)/;
@@ -2395,8 +2645,8 @@ function execGeneratedScript(cmd) {
2395
2645
  const e = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2396
2646
  const interp = new RegExp(`\\b(?:sh|bash|zsh|ksh|dash|source)\\s+["']?${e}\\b|(?:^|[\\n;|&(])\\s*\\.\\s+["']?${e}\\b`);
2397
2647
  const hasPath = /^(\.\/|\/|~\/)/.test(t);
2398
- const path = hasPath ? new RegExp(`(?:^|[\\n;|&(])\\s*${e}(?:\\s|$|[;&|<>])`) : new RegExp(`(?:^|[\\n;|&(])\\s*\\.\\/${e}\\b`);
2399
- if (interp.test(cmd) || path.test(cmd))
2648
+ const path2 = hasPath ? new RegExp(`(?:^|[\\n;|&(])\\s*${e}(?:\\s|$|[;&|<>])`) : new RegExp(`(?:^|[\\n;|&(])\\s*\\.\\/${e}\\b`);
2649
+ if (interp.test(cmd) || path2.test(cmd))
2400
2650
  return t;
2401
2651
  }
2402
2652
  return null;
@@ -2410,14 +2660,21 @@ function isAgentOwnedPath(p, configDirs) {
2410
2660
  const tmp = process.env.TMPDIR;
2411
2661
  return !!tmp && p.startsWith(tmp.replace(/\/$/, "") + "/");
2412
2662
  }
2413
- function outsideProject(filePath, projectPath, configDirs) {
2414
- if (!filePath || !filePath.startsWith("/"))
2415
- return false;
2416
- if (isAgentOwnedPath(filePath, configDirs))
2417
- return false;
2663
+ function outsideProjectPath(filePath, projectPath, configDirs) {
2664
+ if (!filePath)
2665
+ return null;
2666
+ let abs = filePath;
2667
+ if (!filePath.startsWith("/")) {
2668
+ if (!projectPath || !filePath.includes("../"))
2669
+ return null;
2670
+ abs = path.normalize(projectPath.replace(/\/$/, "") + "/" + filePath);
2671
+ }
2672
+ if (isAgentOwnedPath(abs, configDirs))
2673
+ return null;
2418
2674
  if (!projectPath)
2419
- return false;
2420
- return filePath !== projectPath && !filePath.startsWith(projectPath.replace(/\/$/, "") + "/");
2675
+ return null;
2676
+ const root = projectPath.replace(/\/$/, "");
2677
+ return abs !== root && !abs.startsWith(root + "/") ? abs : null;
2421
2678
  }
2422
2679
  var SCOPE_RANK = { unknown: 0, loopback: 1, internal: 2, external: 3 };
2423
2680
  function netTargetScope(rawCommand) {
@@ -2697,6 +2954,34 @@ var RULES = [
2697
2954
  return { evidence: evidence(ctx.command) };
2698
2955
  }
2699
2956
  },
2957
+ {
2958
+ id: "obfuscated.decode_exec",
2959
+ category: "privilege-bypass",
2960
+ framework_ref: "OWASP LLM06",
2961
+ title: "Decodes and executes obfuscated content (base64 -d | sh)",
2962
+ tools: ["Bash"],
2963
+ baseSeverity: "high",
2964
+ test(ctx) {
2965
+ const c = ctx.commandBare;
2966
+ if (!c)
2967
+ return null;
2968
+ const decoder = /\b(base64\s+(?:-d|-D|--decode)|xxd\s+-r|openssl\s+(?:base64\s+-d|enc\b[^|]*\s-d\b)|uudecode)\b/i.exec(c);
2969
+ if (!decoder)
2970
+ return null;
2971
+ const downstream = c.slice(decoder.index + decoder[0].length);
2972
+ const piped = /\|\s*(?:sudo\s+)?(sh|bash|zsh|dash|ksh|python\d?|node|perl|ruby)\b([^|;&\n]*)/i.exec(downstream);
2973
+ if (!piped)
2974
+ return null;
2975
+ const interp = piped[1].toLowerCase();
2976
+ const args = piped[2] || "";
2977
+ if (/^(?:node|python|perl|ruby)/.test(interp) && /(?:^|\s)-(?:e|c|m|p)\b|(?:^|\s)--(?:eval|command|module|print)\b/.test(args))
2978
+ return null;
2979
+ const sudo = /\|\s*sudo\b/i.test(downstream);
2980
+ const d = decoder[0].toLowerCase();
2981
+ const decoderName = d.startsWith("base64") ? "base64" : d.startsWith("xxd") ? "xxd" : d.startsWith("openssl") ? "openssl" : "uudecode";
2982
+ return { evidence: evidence(ctx.command), severity: sudo ? "critical" : "high", mods: { decoder: decoderName, interpreter: interp } };
2983
+ }
2984
+ },
2700
2985
  {
2701
2986
  id: "privilege.sudo",
2702
2987
  category: "privilege-bypass",
@@ -2770,10 +3055,16 @@ var RULES = [
2770
3055
  // writing to /tmp, scratch dirs, adjacent repos is routine; only a system path (below) is high
2771
3056
  test(ctx) {
2772
3057
  const f = ctx.filePath;
2773
- if (!outsideProject(f, ctx.projectPath, ctx.ownedConfigDirs))
3058
+ const abs = outsideProjectPath(f, ctx.projectPath, ctx.ownedConfigDirs);
3059
+ if (!abs)
2774
3060
  return null;
2775
- const system = !!f && SYSTEM_PATH.test(f);
2776
- return { evidence: evidence(f), severity: system ? "high" : "low", mods: { project: ctx.projectPath, system_path: system } };
3061
+ const system = SYSTEM_PATH.test(abs);
3062
+ const traversal = !!f && !f.startsWith("/") && f.includes("../");
3063
+ return {
3064
+ evidence: evidence(f),
3065
+ severity: system ? "high" : "low",
3066
+ mods: { project: ctx.projectPath, system_path: system, ...traversal ? { traversal: true } : {} }
3067
+ };
2777
3068
  }
2778
3069
  }
2779
3070
  ];
@@ -2896,7 +3187,7 @@ function classifyErrors(db, dirty) {
2896
3187
  }
2897
3188
 
2898
3189
  // ../ingest/dist/pipeline.js
2899
- import { createHash as createHash2 } from "crypto";
3190
+ import { createHash as createHash3 } from "crypto";
2900
3191
  function newStats() {
2901
3192
  return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
2902
3193
  }
@@ -2934,7 +3225,7 @@ function skillSummary(body) {
2934
3225
  return null;
2935
3226
  }
2936
3227
  function skillVersionId(name, body) {
2937
- return createHash2("sha1").update(name).update("\0").update(body).digest("hex").slice(0, 16);
3228
+ return createHash3("sha1").update(name).update("\0").update(body).digest("hex").slice(0, 16);
2938
3229
  }
2939
3230
  function pruneExcluded(db, excludedPaths) {
2940
3231
  if (!excludedPaths.length)
@@ -3316,7 +3607,7 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
3316
3607
  }
3317
3608
  let projectId = null;
3318
3609
  if (sessionMeta.cwd) {
3319
- projectId = createHash2("sha1").update(`${adapter.agentId}\0${sessionMeta.cwd}`).digest("hex").slice(0, 16);
3610
+ projectId = createHash3("sha1").update(`${adapter.agentId}\0${sessionMeta.cwd}`).digest("hex").slice(0, 16);
3320
3611
  stmts.upsertProject.run({
3321
3612
  id: projectId,
3322
3613
  agent_id: adapter.agentId,
@@ -3352,10 +3643,10 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
3352
3643
 
3353
3644
  // ../ingest/dist/workflows.js
3354
3645
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3355
- import { basename as basename3, join as join5 } from "path";
3646
+ import { basename as basename3, join as join6 } from "path";
3356
3647
 
3357
3648
  // ../ingest/dist/redact.js
3358
- import { createHash as createHash3, randomBytes } from "crypto";
3649
+ import { createHash as createHash4, randomBytes } from "crypto";
3359
3650
  function parseExcludes(csv) {
3360
3651
  return (csv ?? "").split(",").map((s) => s.trim()).filter(Boolean).map(encodeProjectPath);
3361
3652
  }
@@ -3364,17 +3655,17 @@ function isExcludedArchivePath(filePath, excluded) {
3364
3655
  }
3365
3656
 
3366
3657
  // ../ingest/dist/fileread.js
3367
- import { createHash as createHash4 } from "crypto";
3658
+ import { createHash as createHash5 } from "crypto";
3368
3659
  import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
3369
3660
  import { StringDecoder } from "string_decoder";
3370
3661
  var STREAM_THRESHOLD = 8 * 1024 * 1024;
3371
3662
  var CHUNK2 = 64 * 1024;
3372
3663
  function sha256(buf) {
3373
- return createHash4("sha256").update(buf).digest("hex");
3664
+ return createHash5("sha256").update(buf).digest("hex");
3374
3665
  }
3375
- function sha256File(path) {
3376
- const fd = openSync2(path, "r");
3377
- const h = createHash4("sha256");
3666
+ function sha256File(path2) {
3667
+ const fd = openSync2(path2, "r");
3668
+ const h = createHash5("sha256");
3378
3669
  const buf = Buffer.allocUnsafe(CHUNK2);
3379
3670
  try {
3380
3671
  let n;
@@ -3385,8 +3676,8 @@ function sha256File(path) {
3385
3676
  }
3386
3677
  return h.digest("hex");
3387
3678
  }
3388
- function* streamLines(path) {
3389
- const fd = openSync2(path, "r");
3679
+ function* streamLines(path2) {
3680
+ const fd = openSync2(path2, "r");
3390
3681
  const decoder = new StringDecoder("utf8");
3391
3682
  const buf = Buffer.allocUnsafe(CHUNK2);
3392
3683
  let leftover = "";
@@ -3420,7 +3711,7 @@ function walkSidecars(dir, inWorkflows, out) {
3420
3711
  return;
3421
3712
  }
3422
3713
  for (const e of entries) {
3423
- const p = join5(dir, e.name);
3714
+ const p = join6(dir, e.name);
3424
3715
  if (e.isDirectory())
3425
3716
  walkSidecars(p, inWorkflows || e.name === "workflows", out);
3426
3717
  else if (e.isFile() && inWorkflows && /^wf_.+\.json$/.test(e.name))
@@ -3429,17 +3720,17 @@ function walkSidecars(dir, inWorkflows, out) {
3429
3720
  }
3430
3721
  function discoverSidecars(sourceArchiveDir) {
3431
3722
  const out = [];
3432
- walkSidecars(join5(sourceArchiveDir, "projects"), false, out);
3723
+ walkSidecars(join6(sourceArchiveDir, "projects"), false, out);
3433
3724
  try {
3434
- for (const ts of readdirSync3(join5(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3725
+ for (const ts of readdirSync3(join6(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3435
3726
  if (ts.isDirectory())
3436
- walkSidecars(join5(sourceArchiveDir, ts.name, "projects"), false, out);
3727
+ walkSidecars(join6(sourceArchiveDir, ts.name, "projects"), false, out);
3437
3728
  } catch {
3438
3729
  }
3439
3730
  return out;
3440
3731
  }
3441
- function sessionFromPath(path) {
3442
- const parts = path.split("/");
3732
+ function sessionFromPath(path2) {
3733
+ const parts = path2.split("/");
3443
3734
  const i = parts.indexOf("workflows");
3444
3735
  return i > 0 ? parts[i - 1] : null;
3445
3736
  }
@@ -3486,25 +3777,25 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
3486
3777
  total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
3487
3778
  duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
3488
3779
  ingested_at=excluded.ingested_at`);
3489
- for (const path of paths) {
3780
+ for (const path2 of paths) {
3490
3781
  stats.files++;
3491
- const st = statSync2(path);
3782
+ const st = statSync2(path2);
3492
3783
  const mtimeMs = Math.trunc(st.mtimeMs);
3493
- const prev = full ? void 0 : getState.get(path);
3784
+ const prev = full ? void 0 : getState.get(path2);
3494
3785
  if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
3495
3786
  stats.skipped++;
3496
3787
  continue;
3497
3788
  }
3498
3789
  let buf;
3499
3790
  try {
3500
- buf = readFileSync4(path);
3791
+ buf = readFileSync4(path2);
3501
3792
  } catch {
3502
3793
  stats.malformed++;
3503
3794
  continue;
3504
3795
  }
3505
- const hash = sha256(buf);
3506
- if (prev && prev.sha256 === hash) {
3507
- setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now2 });
3796
+ const hash2 = sha256(buf);
3797
+ if (prev && prev.sha256 === hash2) {
3798
+ setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3508
3799
  stats.skipped++;
3509
3800
  continue;
3510
3801
  }
@@ -3519,14 +3810,14 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
3519
3810
  stats.malformed++;
3520
3811
  continue;
3521
3812
  }
3522
- const runId = strOrNull(d.runId) ?? basename3(path, ".json");
3813
+ const runId = strOrNull(d.runId) ?? basename3(path2, ".json");
3523
3814
  const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
3524
3815
  const durationMs2 = intOrNull(d.durationMs);
3525
3816
  const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
3526
3817
  upsert.run({
3527
3818
  run_id: runId,
3528
3819
  source_id: sourceId,
3529
- session_id: sessionFromPath(path),
3820
+ session_id: sessionFromPath(path2),
3530
3821
  task_id: strOrNull(d.taskId),
3531
3822
  workflow_name: strOrNull(d.workflowName),
3532
3823
  status: strOrNull(d.status),
@@ -3544,14 +3835,14 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
3544
3835
  ended_at: endedAt,
3545
3836
  ingested_at: now2
3546
3837
  });
3547
- setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now2 });
3838
+ setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3548
3839
  stats.upserted++;
3549
3840
  }
3550
3841
  }
3551
3842
 
3552
3843
  // ../ingest/dist/meta.js
3553
3844
  import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
3554
- import { basename as basename4, join as join6 } from "path";
3845
+ import { basename as basename4, join as join7 } from "path";
3555
3846
  function newMetaStats() {
3556
3847
  return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
3557
3848
  }
@@ -3563,7 +3854,7 @@ function walkMeta(dir, inSubagents, out) {
3563
3854
  return;
3564
3855
  }
3565
3856
  for (const e of entries) {
3566
- const p = join6(dir, e.name);
3857
+ const p = join7(dir, e.name);
3567
3858
  if (e.isDirectory())
3568
3859
  walkMeta(p, inSubagents || e.name === "subagents", out);
3569
3860
  else if (e.isFile() && inSubagents && /^agent-.+\.meta\.json$/.test(e.name))
@@ -3572,11 +3863,11 @@ function walkMeta(dir, inSubagents, out) {
3572
3863
  }
3573
3864
  function discoverMeta(sourceArchiveDir) {
3574
3865
  const out = [];
3575
- walkMeta(join6(sourceArchiveDir, "projects"), false, out);
3866
+ walkMeta(join7(sourceArchiveDir, "projects"), false, out);
3576
3867
  try {
3577
- for (const ts of readdirSync4(join6(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3868
+ for (const ts of readdirSync4(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3578
3869
  if (ts.isDirectory())
3579
- walkMeta(join6(sourceArchiveDir, ts.name, "projects"), false, out);
3870
+ walkMeta(join7(sourceArchiveDir, ts.name, "projects"), false, out);
3580
3871
  } catch {
3581
3872
  }
3582
3873
  return out;
@@ -3600,25 +3891,25 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
3600
3891
  source_id=excluded.source_id, agent_type=excluded.agent_type,
3601
3892
  agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
3602
3893
  tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
3603
- for (const path of paths) {
3894
+ for (const path2 of paths) {
3604
3895
  stats.files++;
3605
- const st = statSync3(path);
3896
+ const st = statSync3(path2);
3606
3897
  const mtimeMs = Math.trunc(st.mtimeMs);
3607
- const prev = full ? void 0 : getState.get(path);
3898
+ const prev = full ? void 0 : getState.get(path2);
3608
3899
  if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
3609
3900
  stats.skipped++;
3610
3901
  continue;
3611
3902
  }
3612
3903
  let buf;
3613
3904
  try {
3614
- buf = readFileSync5(path);
3905
+ buf = readFileSync5(path2);
3615
3906
  } catch {
3616
3907
  stats.malformed++;
3617
3908
  continue;
3618
3909
  }
3619
- const hash = sha256(buf);
3620
- if (prev && prev.sha256 === hash) {
3621
- setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now2 });
3910
+ const hash2 = sha256(buf);
3911
+ if (prev && prev.sha256 === hash2) {
3912
+ setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3622
3913
  stats.skipped++;
3623
3914
  continue;
3624
3915
  }
@@ -3633,7 +3924,7 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
3633
3924
  stats.malformed++;
3634
3925
  continue;
3635
3926
  }
3636
- const sessionId = basename4(path, ".meta.json");
3927
+ const sessionId = basename4(path2, ".meta.json");
3637
3928
  upsert.run({
3638
3929
  session_id: sessionId,
3639
3930
  source_id: sourceId,
@@ -3643,14 +3934,14 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
3643
3934
  tool_use_id: strOrNull2(d.toolUseId),
3644
3935
  ingested_at: now2
3645
3936
  });
3646
- setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now2 });
3937
+ setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3647
3938
  stats.upserted++;
3648
3939
  }
3649
3940
  }
3650
3941
 
3651
3942
  // ../ingest/dist/toolResults.js
3652
3943
  import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
3653
- import { basename as basename5, join as join7 } from "path";
3944
+ import { basename as basename5, join as join8 } from "path";
3654
3945
  function newToolResultStats() {
3655
3946
  return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
3656
3947
  }
@@ -3662,7 +3953,7 @@ function walkResults(dir, inResults, out) {
3662
3953
  return;
3663
3954
  }
3664
3955
  for (const e of entries) {
3665
- const p = join7(dir, e.name);
3956
+ const p = join8(dir, e.name);
3666
3957
  if (e.isDirectory())
3667
3958
  walkResults(p, inResults || e.name === "tool-results", out);
3668
3959
  else if (e.isFile() && inResults && e.name.endsWith(".txt"))
@@ -3671,17 +3962,17 @@ function walkResults(dir, inResults, out) {
3671
3962
  }
3672
3963
  function discoverResults(sourceArchiveDir) {
3673
3964
  const out = [];
3674
- walkResults(join7(sourceArchiveDir, "projects"), false, out);
3965
+ walkResults(join8(sourceArchiveDir, "projects"), false, out);
3675
3966
  try {
3676
- for (const ts of readdirSync5(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3967
+ for (const ts of readdirSync5(join8(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3677
3968
  if (ts.isDirectory())
3678
- walkResults(join7(sourceArchiveDir, ts.name, "projects"), false, out);
3969
+ walkResults(join8(sourceArchiveDir, ts.name, "projects"), false, out);
3679
3970
  } catch {
3680
3971
  }
3681
3972
  return out;
3682
3973
  }
3683
- function sessionFromPath2(path) {
3684
- const parts = path.split("/");
3974
+ function sessionFromPath2(path2) {
3975
+ const parts = path2.split("/");
3685
3976
  const i = parts.indexOf("tool-results");
3686
3977
  return i > 0 ? parts[i - 1] : null;
3687
3978
  }
@@ -3698,40 +3989,40 @@ function ingestToolResults(db, sourceArchiveDir, excludedDirs, now2, stats, full
3698
3989
  VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
3699
3990
  ON CONFLICT(session_id, name) DO UPDATE SET
3700
3991
  path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`);
3701
- for (const path of paths) {
3702
- const sessionId = sessionFromPath2(path);
3992
+ for (const path2 of paths) {
3993
+ const sessionId = sessionFromPath2(path2);
3703
3994
  if (!sessionId)
3704
3995
  continue;
3705
3996
  stats.files++;
3706
- const st = statSync4(path);
3997
+ const st = statSync4(path2);
3707
3998
  const mtimeMs = Math.trunc(st.mtimeMs);
3708
- const prev = full ? void 0 : getState.get(path);
3999
+ const prev = full ? void 0 : getState.get(path2);
3709
4000
  if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
3710
4001
  stats.skipped++;
3711
4002
  continue;
3712
4003
  }
3713
4004
  let buf;
3714
4005
  try {
3715
- buf = readFileSync6(path);
4006
+ buf = readFileSync6(path2);
3716
4007
  } catch {
3717
4008
  stats.malformed++;
3718
4009
  continue;
3719
4010
  }
3720
- const hash = sha256(buf);
3721
- if (prev && prev.sha256 === hash) {
3722
- setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now2 });
4011
+ const hash2 = sha256(buf);
4012
+ if (prev && prev.sha256 === hash2) {
4013
+ setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3723
4014
  stats.skipped++;
3724
4015
  continue;
3725
4016
  }
3726
4017
  upsert.run({
3727
4018
  session_id: sessionId,
3728
- name: basename5(path, ".txt"),
3729
- path,
4019
+ name: basename5(path2, ".txt"),
4020
+ path: path2,
3730
4021
  bytes: st.size,
3731
4022
  text: buf.toString("utf8"),
3732
4023
  ingested_at: now2
3733
4024
  });
3734
- setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now2 });
4025
+ setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3735
4026
  stats.upserted++;
3736
4027
  }
3737
4028
  }
@@ -3752,8 +4043,8 @@ function parseIngestArgs(argv) {
3752
4043
  function runIngest(argv = process.argv.slice(2)) {
3753
4044
  const args = parseIngestArgs(argv);
3754
4045
  const dataDir = resolveDataDir(findRepoRoot());
3755
- const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE || join8(dataDir, "archive");
3756
- const dbPath = args.db || process.env.AGENT_LENS_DB || join8(dataDir, "agent-lens.db");
4046
+ const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE || join9(dataDir, "archive");
4047
+ const { path: dbPath } = resolveDbPath(args.db);
3757
4048
  if (!existsSync4(archiveRoot)) {
3758
4049
  console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
3759
4050
  process.exit(1);
@@ -3791,7 +4082,7 @@ function runIngest(argv = process.argv.slice(2)) {
3791
4082
  }
3792
4083
  stmts.insAgent.run(adapter.agentId, adapter.agentName);
3793
4084
  stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
3794
- let files = adapter.discover(join8(archiveRoot, source.label), source.label);
4085
+ let files = adapter.discover(join9(archiveRoot, source.label), source.label);
3795
4086
  if (excludedDirs.length)
3796
4087
  files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
3797
4088
  files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
@@ -3806,13 +4097,13 @@ function runIngest(argv = process.argv.slice(2)) {
3806
4097
  }
3807
4098
  const small = st.size <= STREAM_THRESHOLD;
3808
4099
  const buf = small ? readFileSync7(file.path) : null;
3809
- const hash = small ? sha256(buf) : sha256File(file.path);
3810
- if (prev && prev.sha256 === hash) {
4100
+ const hash2 = small ? sha256(buf) : sha256File(file.path);
4101
+ if (prev && prev.sha256 === hash2) {
3811
4102
  stmts.setState.run({
3812
4103
  file_path: file.path,
3813
4104
  size: st.size,
3814
4105
  mtime_ms: mtimeMs,
3815
- sha256: hash,
4106
+ sha256: hash2,
3816
4107
  events_ingested: prev.events_ingested,
3817
4108
  ingested_at: now2
3818
4109
  });
@@ -3820,12 +4111,12 @@ function runIngest(argv = process.argv.slice(2)) {
3820
4111
  continue;
3821
4112
  }
3822
4113
  const lines = small ? buf.toString("utf8").split("\n") : streamLines(file.path);
3823
- ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash }, now2, stats);
4114
+ ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: hash2 }, now2, stats);
3824
4115
  dirty.add(file.sessionId);
3825
4116
  }
3826
- ingestWorkflowResults(db, join8(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
3827
- ingestSubagentMeta(db, join8(archiveRoot, source.label), source.label, excludedDirs, now2, metaStats, args.full);
3828
- ingestToolResults(db, join8(archiveRoot, source.label), excludedDirs, now2, trStats, args.full);
4117
+ ingestWorkflowResults(db, join9(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
4118
+ ingestSubagentMeta(db, join9(archiveRoot, source.label), source.label, excludedDirs, now2, metaStats, args.full);
4119
+ ingestToolResults(db, join9(archiveRoot, source.label), excludedDirs, now2, trStats, args.full);
3829
4120
  }
3830
4121
  const expanded = rebuildDerived(db, args.full ? null : dirty);
3831
4122
  const classified = classify(db, args.full ? null : expanded);
@@ -3863,8 +4154,7 @@ function runMetrics(argv = process.argv.slice(2)) {
3863
4154
  for (let i = 0; i < argv.length; i++)
3864
4155
  if (argv[i] === "--db")
3865
4156
  dbArg = argv[++i] ?? "";
3866
- const dataDir = resolveDataDir(findRepoRoot());
3867
- const dbPath = dbArg || process.env.AGENT_LENS_DB || join8(dataDir, "agent-lens.db");
4157
+ const { path: dbPath } = resolveDbPath(dbArg);
3868
4158
  if (!existsSync4(dbPath)) {
3869
4159
  console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);
3870
4160
  process.exit(1);
@@ -3879,7 +4169,7 @@ function runMetrics(argv = process.argv.slice(2)) {
3879
4169
 
3880
4170
  // ../server/dist/run.js
3881
4171
  import { existsSync as existsSync7 } from "fs";
3882
- import { dirname as dirname5, join as join10 } from "path";
4172
+ import { dirname as dirname5, join as join11 } from "path";
3883
4173
  import { fileURLToPath as fileURLToPath2 } from "url";
3884
4174
 
3885
4175
  // ../server/dist/db.js
@@ -4462,6 +4752,34 @@ import { existsSync as existsSync6 } from "fs";
4462
4752
  import Fastify from "fastify";
4463
4753
  import fastifyStatic from "@fastify/static";
4464
4754
 
4755
+ // ../server/dist/export.js
4756
+ function parseRedactionLevel(raw) {
4757
+ return raw === "off" ? "off" : raw === "structure" ? "structure" : "secrets";
4758
+ }
4759
+ function renderSessionExport(db, id, level) {
4760
+ const result = getSession(db, id);
4761
+ if (!result)
4762
+ return null;
4763
+ const s = result.session;
4764
+ const events = result.events.map((e) => ({
4765
+ type: e.type,
4766
+ role: e.role,
4767
+ timestamp: e.timestamp,
4768
+ text: e.text,
4769
+ thinking: e.thinking,
4770
+ toolCalls: e.toolCalls.map((t) => ({
4771
+ tool_name: t.tool_name,
4772
+ skill_name: t.skill_name,
4773
+ agent_type: t.agent_type,
4774
+ input_json: t.input_json,
4775
+ status: t.status
4776
+ }))
4777
+ }));
4778
+ const { markdown } = exportMarkdown({ id: s.id, title: s.title, source: s.source_id, project: s.project_path, model: null, started_at: s.started_at, ended_at: s.ended_at }, events, { level });
4779
+ const suffix = level === "off" ? "" : ".redacted";
4780
+ return { markdown, filename: `session-${id.slice(0, 8)}${suffix}.md` };
4781
+ }
4782
+
4465
4783
  // ../server/dist/dashboard.js
4466
4784
  function sessionWhere(f) {
4467
4785
  const where = [];
@@ -4707,7 +5025,7 @@ function dashboardBreakdowns(db, f) {
4707
5025
 
4708
5026
  // ../server/dist/refresh.js
4709
5027
  import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
4710
- import { join as join9 } from "path";
5028
+ import { join as join10 } from "path";
4711
5029
  function originAllowed(origin) {
4712
5030
  if (!origin)
4713
5031
  return true;
@@ -4718,14 +5036,22 @@ function originAllowed(origin) {
4718
5036
  return false;
4719
5037
  }
4720
5038
  }
5039
+ function writeBlocked(headers) {
5040
+ const site = headers["sec-fetch-site"];
5041
+ const s = Array.isArray(site) ? site[0] : site;
5042
+ if (typeof s === "string" && s !== "same-origin" && s !== "none")
5043
+ return true;
5044
+ const origin = headers["origin"];
5045
+ return !originAllowed(Array.isArray(origin) ? origin[0] : origin);
5046
+ }
4721
5047
  function runRefresh() {
4722
5048
  const dataDir = resolveDataDir(findRepoRoot());
4723
- const lock = acquireLock(join9(dataDir, ".agent-lens.lock"));
5049
+ const lock = acquireLock(join10(dataDir, ".agent-lens.lock"));
4724
5050
  if (!lock)
4725
5051
  return null;
4726
5052
  try {
4727
5053
  const collected = collectAll();
4728
- const archiveRoot = process.env.AGENT_LENS_ARCHIVE || join9(dataDir, "archive");
5054
+ const archiveRoot = process.env.AGENT_LENS_ARCHIVE || join10(dataDir, "archive");
4729
5055
  if (!existsSync5(archiveRoot))
4730
5056
  mkdirSync5(archiveRoot, { recursive: true });
4731
5057
  runIngest([]);
@@ -4823,8 +5149,24 @@ function setPref(db, key, value) {
4823
5149
  }
4824
5150
 
4825
5151
  // ../server/dist/app.js
5152
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
4826
5153
  async function createApp(db, opts = {}) {
4827
5154
  const app = Fastify({ logger: false });
5155
+ if (opts.enforceLoopbackHost !== false) {
5156
+ app.addHook("onRequest", async (req, reply) => {
5157
+ const host = (req.headers.host ?? "").replace(/:\d+$/, "");
5158
+ if (!LOOPBACK_HOSTS.has(host)) {
5159
+ return reply.code(403).send({ error: { code: "FORBIDDEN_HOST", message: "non-loopback Host rejected" } });
5160
+ }
5161
+ });
5162
+ }
5163
+ app.addHook("onSend", async (_req, reply, payload) => {
5164
+ reply.header("X-Frame-Options", "DENY");
5165
+ reply.header("X-Content-Type-Options", "nosniff");
5166
+ reply.header("Referrer-Policy", "no-referrer");
5167
+ reply.header("Content-Security-Policy", "frame-ancestors 'none'");
5168
+ return payload;
5169
+ });
4828
5170
  let triageDb = null;
4829
5171
  if (opts.triageDbPath) {
4830
5172
  triageDb = openTriage(opts.triageDbPath);
@@ -4833,7 +5175,7 @@ async function createApp(db, opts = {}) {
4833
5175
  app.addHook("onClose", async () => triageDb?.close());
4834
5176
  }
4835
5177
  const guardWrite = (req, reply) => {
4836
- if (!originAllowed(req.headers.origin)) {
5178
+ if (writeBlocked(req.headers)) {
4837
5179
  reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin write blocked" } });
4838
5180
  return false;
4839
5181
  }
@@ -4848,14 +5190,15 @@ async function createApp(db, opts = {}) {
4848
5190
  return { ok: true, last_ingested: lastIngested(db), schema_version: s.db_version, schema_stale: s.stale };
4849
5191
  });
4850
5192
  app.post("/api/refresh", async (req, reply) => {
4851
- if (!originAllowed(req.headers.origin)) {
5193
+ if (writeBlocked(req.headers)) {
4852
5194
  return reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin refresh blocked" } });
4853
5195
  }
4854
5196
  let result;
4855
5197
  try {
4856
5198
  result = runRefresh();
4857
5199
  } catch (e) {
4858
- return reply.code(500).send({ error: { code: "REFRESH_FAILED", message: String(e?.message ?? e) } });
5200
+ console.error("agent-lens-server: refresh failed:", e);
5201
+ return reply.code(500).send({ error: { code: "REFRESH_FAILED", message: "refresh failed; see server logs" } });
4859
5202
  }
4860
5203
  if (!result) {
4861
5204
  return reply.code(409).send({ error: { code: "REFRESH_IN_PROGRESS", message: "a collect/ingest run is already in progress" } });
@@ -5006,34 +5349,11 @@ async function createApp(db, opts = {}) {
5006
5349
  });
5007
5350
  app.get("/api/sessions/:id/export.md", async (req, reply) => {
5008
5351
  const { id } = req.params;
5009
- const result = getSession(db, id);
5010
- if (!result)
5352
+ const level = parseRedactionLevel(req.query.redact);
5353
+ const out = renderSessionExport(db, id, level);
5354
+ if (!out)
5011
5355
  return reply.code(404).send({ error: "not found" });
5012
- const s = result.session;
5013
- const events = result.events.map((e) => ({
5014
- type: e.type,
5015
- role: e.role,
5016
- timestamp: e.timestamp,
5017
- text: e.text,
5018
- thinking: e.thinking,
5019
- toolCalls: e.toolCalls.map((t) => ({
5020
- tool_name: t.tool_name,
5021
- skill_name: t.skill_name,
5022
- agent_type: t.agent_type,
5023
- input_json: t.input_json,
5024
- status: t.status
5025
- }))
5026
- }));
5027
- const md = sessionToMarkdown({
5028
- id: s.id,
5029
- title: s.title,
5030
- source: s.source_id,
5031
- project: s.project_path,
5032
- model: null,
5033
- started_at: s.started_at,
5034
- ended_at: s.ended_at
5035
- }, events);
5036
- reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="session-${id.slice(0, 8)}.md"`).send(md);
5356
+ reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="${out.filename}"`).send(out.markdown);
5037
5357
  });
5038
5358
  if (opts.webDist && existsSync6(opts.webDist)) {
5039
5359
  await app.register(fastifyStatic, { root: opts.webDist });
@@ -5047,12 +5367,11 @@ async function createApp(db, opts = {}) {
5047
5367
  }
5048
5368
 
5049
5369
  // ../server/dist/run.js
5050
- async function startServer() {
5370
+ async function startServer(overrides = {}) {
5051
5371
  const here = dirname5(fileURLToPath2(import.meta.url));
5052
5372
  const repoRoot = findRepoRoot(here);
5053
- const dbPath = process.env.AGENT_LENS_DB || join10(resolveDataDir(repoRoot), "agent-lens.db");
5054
- const host = process.env.AGENT_LENS_HOST || "127.0.0.1";
5055
- const port = Number(process.env.AGENT_LENS_PORT || 4477);
5373
+ const { path: dbPath } = resolveDbPath(overrides.db);
5374
+ const { host, port } = resolveServerConfig({ port: overrides.port, host: overrides.host });
5056
5375
  const webDist = resolveWebDist(here, repoRoot);
5057
5376
  if (host !== "127.0.0.1" && host !== "localhost" && !process.env.AGENT_LENS_ALLOW_NONLOCAL) {
5058
5377
  console.error(`agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`);
@@ -5062,9 +5381,10 @@ async function startServer() {
5062
5381
  console.error(`agent-lens-server: db not found: ${dbPath} (run ingest first)`);
5063
5382
  process.exit(1);
5064
5383
  }
5065
- const triageDbPath = process.env.AGENT_LENS_TRIAGE_DB || join10(dirname5(dbPath), "triage.db");
5384
+ const triageDbPath = process.env.AGENT_LENS_TRIAGE_DB || join11(dirname5(dbPath), "triage.db");
5066
5385
  const db = openReadonly(dbPath);
5067
- const app = await createApp(db, { webDist, triageDbPath });
5386
+ const enforceLoopbackHost = host === "127.0.0.1" || host === "localhost";
5387
+ const app = await createApp(db, { webDist, triageDbPath, enforceLoopbackHost });
5068
5388
  try {
5069
5389
  await app.listen({ host, port });
5070
5390
  console.log(`agent-lens-server: http://${host}:${port} (db: ${dbPath})`);
@@ -5078,11 +5398,11 @@ async function startServer() {
5078
5398
 
5079
5399
  // src/watch.ts
5080
5400
  import { existsSync as existsSync8 } from "fs";
5081
- import { join as join11 } from "path";
5401
+ import { join as join12 } from "path";
5082
5402
  import chokidar from "chokidar";
5083
5403
  async function runWatch(opts = {}) {
5084
5404
  const sources = loadSources();
5085
- const lockPath = join11(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5405
+ const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5086
5406
  let running = false;
5087
5407
  let pending = false;
5088
5408
  const cycle = () => {
@@ -5115,7 +5435,7 @@ async function runWatch(opts = {}) {
5115
5435
  }
5116
5436
  };
5117
5437
  cycle();
5118
- const watchPaths = sources.map((s) => join11(s.configDir, "projects")).filter((p) => existsSync8(p));
5438
+ const watchPaths = sources.map((s) => join12(s.configDir, "projects")).filter((p) => existsSync8(p));
5119
5439
  if (watchPaths.length === 0) {
5120
5440
  console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
5121
5441
  }
@@ -5143,14 +5463,14 @@ async function runWatch(opts = {}) {
5143
5463
  // src/index.ts
5144
5464
  function version() {
5145
5465
  try {
5146
- const pkg = JSON.parse(readFileSync8(join12(dirname6(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
5466
+ const pkg = JSON.parse(readFileSync8(join13(dirname6(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
5147
5467
  return pkg.version ?? "0.0.0";
5148
5468
  } catch {
5149
5469
  return "0.0.0";
5150
5470
  }
5151
5471
  }
5152
5472
  function withLock(fn) {
5153
- const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5473
+ const lockPath = join13(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5154
5474
  const lock = acquireLock(lockPath);
5155
5475
  if (!lock) {
5156
5476
  console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");
@@ -5172,22 +5492,46 @@ cli.command("collect", "Copy new session traces into the local archive").option(
5172
5492
  if (opts.thenIngest) runIngest([]);
5173
5493
  });
5174
5494
  });
5175
- cli.command("ingest", "Build/update the SQLite projection from the archive").option("--full", "Ignore incremental state and re-read every file").option("--db <path>", "SQLite DB path").option("--archive <path>", "Archive directory").action((opts) => {
5495
+ cli.command("ingest", "Build/update the SQLite projection from the archive").option("--full", "Ignore incremental state and re-read every file").option("--db <path>", "SQLite DB path (overrides AGENT_LENS_DB / config db)").option("--archive <path>", "Archive directory").action((opts) => {
5176
5496
  const argv = [];
5177
5497
  if (opts.full) argv.push("--full");
5178
5498
  if (opts.db) argv.push("--db", opts.db);
5179
5499
  if (opts.archive) argv.push("--archive", opts.archive);
5180
5500
  withLock(() => runIngest(argv));
5181
5501
  });
5182
- cli.command("serve", "Serve the web UI + read-only API on 127.0.0.1").action(async () => {
5183
- await startServer();
5502
+ cli.command("serve", "Serve the web UI + read-only API on 127.0.0.1").option("--port <n>", "HTTP port (overrides AGENT_LENS_PORT / config server.port; default 4477)").option("--host <host>", "Bind host (overrides AGENT_LENS_HOST / config server.host; loopback only unless AGENT_LENS_ALLOW_NONLOCAL=1)").option("--db <path>", "SQLite DB path (overrides AGENT_LENS_DB / config db)").action(async (opts) => {
5503
+ try {
5504
+ await startServer({ port: opts.port, host: opts.host, db: opts.db });
5505
+ } catch (e) {
5506
+ console.error(e instanceof Error ? e.message : String(e));
5507
+ process.exit(1);
5508
+ }
5184
5509
  });
5185
5510
  cli.command("watch", "Watch sources and collect + ingest on change (resident process)").option("--interval <sec>", "Also run on a fixed interval (seconds)").option("--poll", "Use polling \u2014 for network filesystems where fs events don't fire").action(async (opts) => {
5186
5511
  await runWatch({ intervalSec: opts.interval ? Number(opts.interval) : void 0, poll: !!opts.poll });
5187
5512
  });
5188
- cli.command("metrics", "Re-run classification over an already-ingested DB").option("--db <path>", "SQLite DB path").action((opts) => {
5513
+ cli.command("metrics", "Re-run classification over an already-ingested DB").option("--db <path>", "SQLite DB path (overrides AGENT_LENS_DB / config db)").action((opts) => {
5189
5514
  runMetrics(opts.db ? ["--db", opts.db] : []);
5190
5515
  });
5516
+ cli.command("export <sessionId>", "Write a session to a shareable Markdown file (redacted by default)").option("--out <file>", "Write to this file (default: stdout)").option("--level <level>", "Redaction level: secrets (default) | structure").option("--no-redact", "Verbatim, UNREDACTED output (opt-out)").option("--db <path>", "SQLite DB path (overrides AGENT_LENS_DB / config db)").action((sessionId, opts) => {
5517
+ const { path: dbPath } = resolveDbPath(opts.db);
5518
+ if (!existsSync9(dbPath)) {
5519
+ console.error(`agent-lens: db not found: ${dbPath} (run ingest first)`);
5520
+ process.exit(1);
5521
+ }
5522
+ const level = opts.redact === false ? "off" : parseRedactionLevel(opts.level);
5523
+ const out = renderSessionExport(openReadonly(dbPath), sessionId, level);
5524
+ if (!out) {
5525
+ console.error(`agent-lens: session not found: ${sessionId}`);
5526
+ process.exit(1);
5527
+ }
5528
+ if (opts.out) {
5529
+ writeFileSync3(opts.out, out.markdown);
5530
+ console.error(`agent-lens: wrote ${out.filename} \u2192 ${opts.out} (redaction: ${level})`);
5531
+ } else {
5532
+ process.stdout.write(out.markdown);
5533
+ }
5534
+ });
5191
5535
  cli.command(
5192
5536
  "service <action> [target]",
5193
5537
  "Install/uninstall/status OS services \u2014 action: install|uninstall|status; target: collector|server|all (default all)"
@@ -5196,6 +5540,39 @@ cli.command(
5196
5540
  const hours = action === "install" ? parseHours(opts.times) : void 0;
5197
5541
  runService(action, { cliEntry: fileURLToPath3(import.meta.url), hours, targets });
5198
5542
  });
5543
+ cli.command("config", "Show the effective configuration (sources, paths, server) and where each value came from").action(() => {
5544
+ const repoRoot = findRepoRoot();
5545
+ const dataDir = resolveDataDir(repoRoot);
5546
+ const configFile = resolveConfigFile(repoRoot, dataDir);
5547
+ let server;
5548
+ try {
5549
+ server = resolveServerConfig();
5550
+ } catch (e) {
5551
+ console.error(e instanceof Error ? e.message : String(e));
5552
+ process.exit(1);
5553
+ }
5554
+ const { port, host, portOrigin, hostOrigin } = server;
5555
+ const { path: dbPath, origin: dbOrigin } = resolveDbPath();
5556
+ const archive = process.env.AGENT_LENS_ARCHIVE || join13(dataDir, "archive");
5557
+ const triageDb = process.env.AGENT_LENS_TRIAGE_DB || join13(dirname6(dbPath), "triage.db");
5558
+ const keepDays = process.env.AGENT_LENS_VERSIONS_KEEP_DAYS || "90";
5559
+ const keepOrigin = process.env.AGENT_LENS_VERSIONS_KEEP_DAYS ? "env" : "default";
5560
+ console.log("agent-lens config (precedence: flag > env > config file > default)\n");
5561
+ console.log("Paths");
5562
+ console.log(` config file ${configFile ?? "(none \u2014 using built-in default source)"}`);
5563
+ console.log(` data dir ${dataDir}${process.env.AGENT_LENS_DATA ? " [env]" : ""}`);
5564
+ console.log(` archive ${archive}${process.env.AGENT_LENS_ARCHIVE ? " [env]" : ""}`);
5565
+ console.log(` db ${dbPath} [${dbOrigin}]`);
5566
+ console.log(` triage db ${triageDb}${process.env.AGENT_LENS_TRIAGE_DB ? " [env]" : ""}`);
5567
+ console.log("\nServer");
5568
+ console.log(` host ${host} [${hostOrigin}]`);
5569
+ console.log(` port ${port} [${portOrigin}]`);
5570
+ console.log(` non-local bind ${process.env.AGENT_LENS_ALLOW_NONLOCAL ? "allowed (AGENT_LENS_ALLOW_NONLOCAL)" : "blocked (loopback only)"}`);
5571
+ console.log("\nRetention");
5572
+ console.log(` .versions keep ${keepDays} day(s) [${keepOrigin}]`);
5573
+ console.log("\nSources");
5574
+ for (const s of loadSources()) console.log(` ${s.label.padEnd(14)} ${s.configDir} (${s.agent})`);
5575
+ });
5199
5576
  cli.help();
5200
5577
  cli.version(version());
5201
5578
  cli.parse();