@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.
- package/dist/agent-lens.js +621 -244
- package/dist/agent-lens.js.map +1 -1
- package/package.json +2 -2
- package/web/assets/{CopyButton--e68pO3S.js → CopyButton-CQA_09tY.js} +1 -1
- package/web/assets/{Dashboard-Ce5pBLCZ.js → Dashboard-CtAZNySe.js} +1 -1
- package/web/assets/{SecurityView-C2cpbxxV.js → SecurityView-Dg0hnxA-.js} +1 -1
- package/web/assets/SessionView-Ddk_CWjP.js +12 -0
- package/web/assets/{SkillView-ByxQC40e.js → SkillView-BfEFS2DU.js} +1 -1
- package/web/assets/{SkillsView-D_CwXBGT.js → SkillsView-CG8HFyyk.js} +1 -1
- package/web/assets/{WorkflowView-Ddmg3evU.js → WorkflowView-B6_1UcYD.js} +1 -1
- package/web/assets/{index-BTQM3Zlg.js → index-B9PbFrlb.js} +2 -2
- package/web/assets/index-Dt08Kre7.css +1 -0
- package/web/assets/{index--T4VdWmP.js → index-ksYuAXX3.js} +1 -1
- package/web/assets/{severity-B-8BLd1n.js → severity-CZIEtWx1.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/SessionView-Q6Ec76B9.js +0 -12
- package/web/assets/index-DuZ7WdnZ.css +0 -1
package/dist/agent-lens.js
CHANGED
|
@@ -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
|
|
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/
|
|
1342
|
+
// ../core/dist/config.js
|
|
1156
1343
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1157
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1371
|
-
const versionsDir =
|
|
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(
|
|
1377
|
-
ensureDir(
|
|
1621
|
+
ensureDir(join3(archive, "projects"));
|
|
1622
|
+
ensureDir(join3(archive, "settings"));
|
|
1378
1623
|
const files = [];
|
|
1379
|
-
const projectsDir =
|
|
1624
|
+
const projectsDir = join3(configDir, "projects");
|
|
1380
1625
|
if (existsSync2(projectsDir))
|
|
1381
1626
|
walkCollectable(projectsDir, files);
|
|
1382
|
-
const historySrc =
|
|
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,
|
|
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 =
|
|
1643
|
+
const s = join3(configDir, f);
|
|
1399
1644
|
if (!existsSync2(s))
|
|
1400
1645
|
continue;
|
|
1401
|
-
const a =
|
|
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(
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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:
|
|
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
|
|
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(
|
|
1632
|
-
writeFileSync2(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
1917
|
-
const parts =
|
|
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(
|
|
1922
|
-
const parts =
|
|
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 =
|
|
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
|
|
1951
|
-
const parts =
|
|
1952
|
-
const sessionId = basename2(
|
|
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(
|
|
1961
|
-
workflowRunId: workflowRunFromPath(
|
|
2209
|
+
parentSessionId: parentSessionFromPath(path2),
|
|
2210
|
+
workflowRunId: workflowRunFromPath(path2)
|
|
1962
2211
|
});
|
|
1963
2212
|
}
|
|
1964
2213
|
};
|
|
1965
|
-
addFrom(
|
|
1966
|
-
const versionsRoot =
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
2399
|
-
if (interp.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
|
|
2414
|
-
if (!filePath
|
|
2415
|
-
return
|
|
2416
|
-
|
|
2417
|
-
|
|
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
|
|
2420
|
-
|
|
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
|
-
|
|
3058
|
+
const abs = outsideProjectPath(f, ctx.projectPath, ctx.ownedConfigDirs);
|
|
3059
|
+
if (!abs)
|
|
2774
3060
|
return null;
|
|
2775
|
-
const system =
|
|
2776
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
3646
|
+
import { basename as basename3, join as join6 } from "path";
|
|
3356
3647
|
|
|
3357
3648
|
// ../ingest/dist/redact.js
|
|
3358
|
-
import { createHash as
|
|
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
|
|
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
|
|
3664
|
+
return createHash5("sha256").update(buf).digest("hex");
|
|
3374
3665
|
}
|
|
3375
|
-
function sha256File(
|
|
3376
|
-
const fd = openSync2(
|
|
3377
|
-
const h =
|
|
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(
|
|
3389
|
-
const fd = openSync2(
|
|
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 =
|
|
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(
|
|
3723
|
+
walkSidecars(join6(sourceArchiveDir, "projects"), false, out);
|
|
3433
3724
|
try {
|
|
3434
|
-
for (const ts of readdirSync3(
|
|
3725
|
+
for (const ts of readdirSync3(join6(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3435
3726
|
if (ts.isDirectory())
|
|
3436
|
-
walkSidecars(
|
|
3727
|
+
walkSidecars(join6(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
3437
3728
|
} catch {
|
|
3438
3729
|
}
|
|
3439
3730
|
return out;
|
|
3440
3731
|
}
|
|
3441
|
-
function sessionFromPath(
|
|
3442
|
-
const parts =
|
|
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
|
|
3780
|
+
for (const path2 of paths) {
|
|
3490
3781
|
stats.files++;
|
|
3491
|
-
const st = statSync2(
|
|
3782
|
+
const st = statSync2(path2);
|
|
3492
3783
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3493
|
-
const prev = full ? void 0 : getState.get(
|
|
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(
|
|
3791
|
+
buf = readFileSync4(path2);
|
|
3501
3792
|
} catch {
|
|
3502
3793
|
stats.malformed++;
|
|
3503
3794
|
continue;
|
|
3504
3795
|
}
|
|
3505
|
-
const
|
|
3506
|
-
if (prev && prev.sha256 ===
|
|
3507
|
-
setState.run({ file_path:
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
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(
|
|
3866
|
+
walkMeta(join7(sourceArchiveDir, "projects"), false, out);
|
|
3576
3867
|
try {
|
|
3577
|
-
for (const ts of readdirSync4(
|
|
3868
|
+
for (const ts of readdirSync4(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3578
3869
|
if (ts.isDirectory())
|
|
3579
|
-
walkMeta(
|
|
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
|
|
3894
|
+
for (const path2 of paths) {
|
|
3604
3895
|
stats.files++;
|
|
3605
|
-
const st = statSync3(
|
|
3896
|
+
const st = statSync3(path2);
|
|
3606
3897
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3607
|
-
const prev = full ? void 0 : getState.get(
|
|
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(
|
|
3905
|
+
buf = readFileSync5(path2);
|
|
3615
3906
|
} catch {
|
|
3616
3907
|
stats.malformed++;
|
|
3617
3908
|
continue;
|
|
3618
3909
|
}
|
|
3619
|
-
const
|
|
3620
|
-
if (prev && prev.sha256 ===
|
|
3621
|
-
setState.run({ file_path:
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
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(
|
|
3965
|
+
walkResults(join8(sourceArchiveDir, "projects"), false, out);
|
|
3675
3966
|
try {
|
|
3676
|
-
for (const ts of readdirSync5(
|
|
3967
|
+
for (const ts of readdirSync5(join8(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3677
3968
|
if (ts.isDirectory())
|
|
3678
|
-
walkResults(
|
|
3969
|
+
walkResults(join8(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
3679
3970
|
} catch {
|
|
3680
3971
|
}
|
|
3681
3972
|
return out;
|
|
3682
3973
|
}
|
|
3683
|
-
function sessionFromPath2(
|
|
3684
|
-
const parts =
|
|
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
|
|
3702
|
-
const sessionId = sessionFromPath2(
|
|
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(
|
|
3997
|
+
const st = statSync4(path2);
|
|
3707
3998
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3708
|
-
const prev = full ? void 0 : getState.get(
|
|
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(
|
|
4006
|
+
buf = readFileSync6(path2);
|
|
3716
4007
|
} catch {
|
|
3717
4008
|
stats.malformed++;
|
|
3718
4009
|
continue;
|
|
3719
4010
|
}
|
|
3720
|
-
const
|
|
3721
|
-
if (prev && prev.sha256 ===
|
|
3722
|
-
setState.run({ file_path:
|
|
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(
|
|
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:
|
|
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 ||
|
|
3756
|
-
const
|
|
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(
|
|
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
|
|
3810
|
-
if (prev && prev.sha256 ===
|
|
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:
|
|
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,
|
|
3827
|
-
ingestSubagentMeta(db,
|
|
3828
|
-
ingestToolResults(db,
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 ||
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
5010
|
-
|
|
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
|
-
|
|
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 =
|
|
5054
|
-
const host =
|
|
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 ||
|
|
5384
|
+
const triageDbPath = process.env.AGENT_LENS_TRIAGE_DB || join11(dirname5(dbPath), "triage.db");
|
|
5066
5385
|
const db = openReadonly(dbPath);
|
|
5067
|
-
const
|
|
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
|
|
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 =
|
|
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) =>
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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();
|