@swestash/agent-lens 0.8.3 → 0.9.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 +448 -101
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/{CopyButton-CQA_09tY.js → CopyButton-CLyPzvYe.js} +1 -1
- package/web/assets/{Dashboard-CtAZNySe.js → Dashboard-De60eF84.js} +1 -1
- package/web/assets/FileView-BbQOT43z.js +1 -0
- package/web/assets/FilesView-Ce3W2j5_.js +1 -0
- package/web/assets/{SecurityView-Dg0hnxA-.js → SecurityView-B2czCQsw.js} +1 -1
- package/web/assets/SessionView-CpmvQW2v.js +12 -0
- package/web/assets/{SkillView-BfEFS2DU.js → SkillView-DIxKdBD4.js} +1 -1
- package/web/assets/{SkillsView-CG8HFyyk.js → SkillsView-DF66QBkB.js} +1 -1
- package/web/assets/{WorkflowView-B6_1UcYD.js → WorkflowView-BWB4Z_bX.js} +1 -1
- package/web/assets/{index-B9PbFrlb.js → index-D2UV_n-R.js} +13 -13
- package/web/assets/{index-ksYuAXX3.js → index-bz6hQeaj.js} +1 -1
- package/web/assets/{severity-CZIEtWx1.js → severity-CVXkbfrj.js} +1 -1
- package/web/index.html +1 -1
- package/web/assets/SessionView-Ddk_CWjP.js +0 -12
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 { existsSync as
|
|
5
|
-
import { dirname as
|
|
4
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
5
|
+
import { dirname as dirname7, 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
|
|
@@ -591,7 +591,7 @@ var CAC = class extends EventEmitter {
|
|
|
591
591
|
var cac = (name = "") => new CAC(name);
|
|
592
592
|
|
|
593
593
|
// ../core/dist/schema.js
|
|
594
|
-
var SCHEMA_VERSION =
|
|
594
|
+
var SCHEMA_VERSION = 14;
|
|
595
595
|
var SCHEMA_SQL = (
|
|
596
596
|
/* sql */
|
|
597
597
|
`
|
|
@@ -811,6 +811,29 @@ CREATE INDEX IF NOT EXISTS idx_findings_category ON findings(category);
|
|
|
811
811
|
CREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule_id);
|
|
812
812
|
CREATE INDEX IF NOT EXISTS idx_findings_tool_call ON findings(tool_call_id);
|
|
813
813
|
|
|
814
|
+
-- File modifications derived from tool calls (ADR-022). One row per (tool_call, file): today
|
|
815
|
+
-- Edit/Write/NotebookEdit are 1:1, but the hash id leaves room for multi-file sources (e.g. a
|
|
816
|
+
-- future Bash-write heuristic) without a schema break. Failed calls (status='error') are excluded
|
|
817
|
+
-- at derivation \u2014 they didn't change the file. Re-runnable like findings: incremental runs DELETE
|
|
818
|
+
-- the dirty sessions' rows and re-INSERT (delete-then-insert). Populated by ingest's
|
|
819
|
+
-- deriveFileChanges (filechanges.ts).
|
|
820
|
+
CREATE TABLE IF NOT EXISTS file_changes (
|
|
821
|
+
id TEXT PRIMARY KEY, -- sha1(tool_call_id + NUL + file_path), 16 hex
|
|
822
|
+
tool_call_id TEXT NOT NULL REFERENCES tool_calls(id),
|
|
823
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
824
|
+
turn_id TEXT, -- no FK: rebuildDerived recreates turns (same stance as findings)
|
|
825
|
+
event_uuid TEXT, -- jump-to-transcript anchor (#ev-<uuid>)
|
|
826
|
+
project_id TEXT REFERENCES projects(id), -- from the session, for per-project grouping
|
|
827
|
+
file_path TEXT NOT NULL, -- normalized absolute path
|
|
828
|
+
tool_name TEXT NOT NULL, -- Edit | Write | NotebookEdit
|
|
829
|
+
lines_added INTEGER, -- newline-count delta (NULL when unknowable)
|
|
830
|
+
lines_removed INTEGER,
|
|
831
|
+
timestamp TEXT, -- ISO8601, from the tool call's event
|
|
832
|
+
derive_version INTEGER NOT NULL DEFAULT 1
|
|
833
|
+
);
|
|
834
|
+
CREATE INDEX IF NOT EXISTS idx_file_changes_session ON file_changes(session_id);
|
|
835
|
+
CREATE INDEX IF NOT EXISTS idx_file_changes_file ON file_changes(project_id, file_path, timestamp);
|
|
836
|
+
|
|
814
837
|
-- Workflow-tool run results, ingested from the sidecar JSON the runner writes next to the launching
|
|
815
838
|
-- session (\u2026/projects/<enc>/<sessionId>/workflows/wf_<id>.json). The transcript only carries the
|
|
816
839
|
-- "launched in background" ack for an async run; this sidecar is the authoritative record of how the
|
|
@@ -2054,8 +2077,8 @@ function runService(action, params) {
|
|
|
2054
2077
|
}
|
|
2055
2078
|
|
|
2056
2079
|
// ../ingest/dist/run.js
|
|
2057
|
-
import { readFileSync as readFileSync7, statSync as statSync5, mkdirSync as mkdirSync4, existsSync as
|
|
2058
|
-
import { dirname as
|
|
2080
|
+
import { readFileSync as readFileSync7, statSync as statSync5, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
2081
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
2059
2082
|
|
|
2060
2083
|
// ../ingest/dist/db.js
|
|
2061
2084
|
import Database from "better-sqlite3";
|
|
@@ -2088,6 +2111,7 @@ function resetSchema(db) {
|
|
|
2088
2111
|
DROP TRIGGER IF EXISTS events_ad;
|
|
2089
2112
|
DROP TRIGGER IF EXISTS events_ai;
|
|
2090
2113
|
DROP TABLE IF EXISTS events_fts;
|
|
2114
|
+
DROP TABLE IF EXISTS file_changes;
|
|
2091
2115
|
DROP TABLE IF EXISTS findings;
|
|
2092
2116
|
DROP TABLE IF EXISTS classifications;
|
|
2093
2117
|
DROP TABLE IF EXISTS token_usage;
|
|
@@ -2176,13 +2200,13 @@ function asString(v) {
|
|
|
2176
2200
|
function asInt(v) {
|
|
2177
2201
|
return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : 0;
|
|
2178
2202
|
}
|
|
2179
|
-
function parentSessionFromPath(
|
|
2180
|
-
const parts =
|
|
2203
|
+
function parentSessionFromPath(path3) {
|
|
2204
|
+
const parts = path3.split("/");
|
|
2181
2205
|
const i = parts.indexOf("subagents");
|
|
2182
2206
|
return i > 0 ? parts[i - 1] : null;
|
|
2183
2207
|
}
|
|
2184
|
-
function workflowRunFromPath(
|
|
2185
|
-
const parts =
|
|
2208
|
+
function workflowRunFromPath(path3) {
|
|
2209
|
+
const parts = path3.split("/");
|
|
2186
2210
|
const i = parts.indexOf("workflows");
|
|
2187
2211
|
return i >= 0 && i + 1 < parts.length ? parts[i + 1] : null;
|
|
2188
2212
|
}
|
|
@@ -2210,18 +2234,18 @@ var ClaudeCodeAdapter = class {
|
|
|
2210
2234
|
const addFrom = (projectsDir, isVersion) => {
|
|
2211
2235
|
const found = [];
|
|
2212
2236
|
recurseJsonl(projectsDir, found);
|
|
2213
|
-
for (const
|
|
2214
|
-
const parts =
|
|
2215
|
-
const sessionId = basename2(
|
|
2237
|
+
for (const path3 of found) {
|
|
2238
|
+
const parts = path3.split("/");
|
|
2239
|
+
const sessionId = basename2(path3, ".jsonl");
|
|
2216
2240
|
const encodedDir = parts[parts.length - 2] ?? "";
|
|
2217
2241
|
files.push({
|
|
2218
|
-
path:
|
|
2242
|
+
path: path3,
|
|
2219
2243
|
sessionId,
|
|
2220
2244
|
encodedDir,
|
|
2221
2245
|
isVersion,
|
|
2222
2246
|
sourceId,
|
|
2223
|
-
parentSessionId: parentSessionFromPath(
|
|
2224
|
-
workflowRunId: workflowRunFromPath(
|
|
2247
|
+
parentSessionId: parentSessionFromPath(path3),
|
|
2248
|
+
workflowRunId: workflowRunFromPath(path3)
|
|
2225
2249
|
});
|
|
2226
2250
|
}
|
|
2227
2251
|
};
|
|
@@ -2659,8 +2683,8 @@ function execGeneratedScript(cmd) {
|
|
|
2659
2683
|
const e = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2660
2684
|
const interp = new RegExp(`\\b(?:sh|bash|zsh|ksh|dash|source)\\s+["']?${e}\\b|(?:^|[\\n;|&(])\\s*\\.\\s+["']?${e}\\b`);
|
|
2661
2685
|
const hasPath = /^(\.\/|\/|~\/)/.test(t);
|
|
2662
|
-
const
|
|
2663
|
-
if (interp.test(cmd) ||
|
|
2686
|
+
const path3 = hasPath ? new RegExp(`(?:^|[\\n;|&(])\\s*${e}(?:\\s|$|[;&|<>])`) : new RegExp(`(?:^|[\\n;|&(])\\s*\\.\\/${e}\\b`);
|
|
2687
|
+
if (interp.test(cmd) || path3.test(cmd))
|
|
2664
2688
|
return t;
|
|
2665
2689
|
}
|
|
2666
2690
|
return null;
|
|
@@ -3200,8 +3224,195 @@ function classifyErrors(db, dirty) {
|
|
|
3200
3224
|
return { count, version: ERROR_CLASSIFIER_VERSION };
|
|
3201
3225
|
}
|
|
3202
3226
|
|
|
3203
|
-
// ../ingest/dist/
|
|
3227
|
+
// ../ingest/dist/filechanges.js
|
|
3204
3228
|
import { createHash as createHash3 } from "crypto";
|
|
3229
|
+
import { posix as path2 } from "path";
|
|
3230
|
+
var FILECHANGES_VERSION = 1;
|
|
3231
|
+
var FILE_TOOLS = ["Edit", "Write", "NotebookEdit"];
|
|
3232
|
+
function fileChangeId(toolCallId, filePath) {
|
|
3233
|
+
return createHash3("sha1").update(toolCallId).update("\0").update(filePath).digest("hex").slice(0, 16);
|
|
3234
|
+
}
|
|
3235
|
+
function normalizeFilePath(filePath, projectPath) {
|
|
3236
|
+
if (typeof filePath !== "string" || !filePath)
|
|
3237
|
+
return null;
|
|
3238
|
+
if (filePath.startsWith("/"))
|
|
3239
|
+
return path2.normalize(filePath);
|
|
3240
|
+
if (!projectPath)
|
|
3241
|
+
return null;
|
|
3242
|
+
return path2.normalize(projectPath.replace(/\/$/, "") + "/" + filePath);
|
|
3243
|
+
}
|
|
3244
|
+
function lineCount(s) {
|
|
3245
|
+
if (typeof s !== "string")
|
|
3246
|
+
return null;
|
|
3247
|
+
if (s === "")
|
|
3248
|
+
return 0;
|
|
3249
|
+
let n = 1;
|
|
3250
|
+
for (let i = 0; i < s.length; i++)
|
|
3251
|
+
if (s.charCodeAt(i) === 10)
|
|
3252
|
+
n++;
|
|
3253
|
+
return n;
|
|
3254
|
+
}
|
|
3255
|
+
function deriveFileChanges(db, dirty) {
|
|
3256
|
+
const incremental = dirty != null;
|
|
3257
|
+
if (incremental) {
|
|
3258
|
+
db.exec("DROP TABLE IF EXISTS _dirty_fc");
|
|
3259
|
+
db.exec("CREATE TEMP TABLE _dirty_fc (id TEXT PRIMARY KEY)");
|
|
3260
|
+
const ins = db.prepare("INSERT OR IGNORE INTO _dirty_fc (id) VALUES (?)");
|
|
3261
|
+
db.transaction((ids) => {
|
|
3262
|
+
for (const id of ids)
|
|
3263
|
+
ins.run(id);
|
|
3264
|
+
})(dirty);
|
|
3265
|
+
}
|
|
3266
|
+
const scope = incremental ? " AND tc.session_id IN (SELECT id FROM _dirty_fc)" : "";
|
|
3267
|
+
const proj = /* @__PURE__ */ new Map();
|
|
3268
|
+
for (const r of db.prepare(`SELECT s.id id, p.id pid, p.path path FROM sessions s JOIN projects p ON p.id = s.project_id${incremental ? " WHERE s.id IN (SELECT id FROM _dirty_fc)" : ""}`).all()) {
|
|
3269
|
+
proj.set(r.id, { id: r.pid, path: r.path });
|
|
3270
|
+
}
|
|
3271
|
+
const rows = db.prepare(`SELECT tc.id, tc.session_id, tc.turn_id, tc.event_uuid, tc.tool_name, tc.input_json, tc.status, e.timestamp
|
|
3272
|
+
FROM tool_calls tc LEFT JOIN events e ON e.uuid = tc.event_uuid
|
|
3273
|
+
WHERE tc.tool_name IN (${FILE_TOOLS.map(() => "?").join(",")})${scope}`).all(...FILE_TOOLS);
|
|
3274
|
+
const insert = db.prepare(`INSERT INTO file_changes (id, tool_call_id, session_id, turn_id, event_uuid, project_id, file_path, tool_name, lines_added, lines_removed, timestamp, derive_version)
|
|
3275
|
+
VALUES (@id, @tool_call_id, @session_id, @turn_id, @event_uuid, @project_id, @file_path, @tool_name, @lines_added, @lines_removed, @timestamp, @derive_version)
|
|
3276
|
+
ON CONFLICT(id) DO NOTHING`);
|
|
3277
|
+
const delScope = incremental ? "DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _dirty_fc)" : "DELETE FROM file_changes";
|
|
3278
|
+
const tx = db.transaction(() => {
|
|
3279
|
+
db.exec(delScope);
|
|
3280
|
+
for (const row of rows) {
|
|
3281
|
+
if (row.status === "error")
|
|
3282
|
+
continue;
|
|
3283
|
+
let input = null;
|
|
3284
|
+
try {
|
|
3285
|
+
input = row.input_json ? JSON.parse(row.input_json) : null;
|
|
3286
|
+
} catch {
|
|
3287
|
+
input = null;
|
|
3288
|
+
}
|
|
3289
|
+
if (!input)
|
|
3290
|
+
continue;
|
|
3291
|
+
const p = proj.get(row.session_id);
|
|
3292
|
+
const filePath = normalizeFilePath(input.file_path ?? input.notebook_path, p?.path ?? null);
|
|
3293
|
+
if (!filePath)
|
|
3294
|
+
continue;
|
|
3295
|
+
let added = null;
|
|
3296
|
+
let removed = null;
|
|
3297
|
+
if (row.tool_name === "Edit") {
|
|
3298
|
+
added = lineCount(input.new_string);
|
|
3299
|
+
removed = lineCount(input.old_string);
|
|
3300
|
+
} else if (row.tool_name === "Write") {
|
|
3301
|
+
added = lineCount(input.content);
|
|
3302
|
+
}
|
|
3303
|
+
insert.run({
|
|
3304
|
+
id: fileChangeId(row.id, filePath),
|
|
3305
|
+
tool_call_id: row.id,
|
|
3306
|
+
session_id: row.session_id,
|
|
3307
|
+
turn_id: row.turn_id,
|
|
3308
|
+
event_uuid: row.event_uuid,
|
|
3309
|
+
project_id: p?.id ?? null,
|
|
3310
|
+
file_path: filePath,
|
|
3311
|
+
tool_name: row.tool_name,
|
|
3312
|
+
lines_added: added,
|
|
3313
|
+
lines_removed: removed,
|
|
3314
|
+
timestamp: row.timestamp,
|
|
3315
|
+
derive_version: FILECHANGES_VERSION
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
});
|
|
3319
|
+
tx();
|
|
3320
|
+
if (incremental)
|
|
3321
|
+
db.exec("DROP TABLE IF EXISTS _dirty_fc");
|
|
3322
|
+
const count = db.prepare("SELECT COUNT(*) n FROM file_changes").get().n;
|
|
3323
|
+
return { count, version: FILECHANGES_VERSION };
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
// ../ingest/dist/canonicalize.js
|
|
3327
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3328
|
+
import { homedir as homedir3 } from "os";
|
|
3329
|
+
import { createHash as createHash4 } from "crypto";
|
|
3330
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
3331
|
+
function projectId(agentId, path3) {
|
|
3332
|
+
return createHash4("sha1").update(`${agentId}\0${path3}`).digest("hex").slice(0, 16);
|
|
3333
|
+
}
|
|
3334
|
+
var strip = (p) => p.length > 1 ? p.replace(/\/$/, "") : p;
|
|
3335
|
+
function nearestGitRoot(path3, home) {
|
|
3336
|
+
let cur = strip(path3);
|
|
3337
|
+
while (cur !== home && cur !== "/" && cur.length > 0) {
|
|
3338
|
+
if (existsSync4(join6(cur, ".git")))
|
|
3339
|
+
return cur;
|
|
3340
|
+
const next = dirname4(cur);
|
|
3341
|
+
if (next === cur)
|
|
3342
|
+
break;
|
|
3343
|
+
cur = next;
|
|
3344
|
+
}
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
function canonicalizeProjects(db, opts = {}) {
|
|
3348
|
+
const home = strip(opts.homeDir ?? homedir3());
|
|
3349
|
+
const rows = db.prepare("SELECT id, agent_id, path FROM projects").all();
|
|
3350
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3351
|
+
for (const r of rows)
|
|
3352
|
+
byPath.set(strip(r.path), { id: r.id, agent_id: r.agent_id });
|
|
3353
|
+
const withMains = new Set(db.prepare("SELECT DISTINCT project_id FROM sessions WHERE is_sidechain = 0 AND project_id IS NOT NULL").all().map((r) => r.project_id));
|
|
3354
|
+
const memo = /* @__PURE__ */ new Map();
|
|
3355
|
+
function canon(path3) {
|
|
3356
|
+
const p = strip(path3);
|
|
3357
|
+
const hit = memo.get(p);
|
|
3358
|
+
if (hit)
|
|
3359
|
+
return hit;
|
|
3360
|
+
memo.set(p, p);
|
|
3361
|
+
let out = p;
|
|
3362
|
+
if (p !== home) {
|
|
3363
|
+
const git = nearestGitRoot(p, home);
|
|
3364
|
+
if (git) {
|
|
3365
|
+
out = git;
|
|
3366
|
+
} else if (existsSync4(p) || !withMains.has(byPath.get(p)?.id ?? "")) {
|
|
3367
|
+
for (let cur = dirname4(p); cur !== "/" && cur !== home && cur.length > 0; cur = dirname4(cur)) {
|
|
3368
|
+
if (byPath.has(cur)) {
|
|
3369
|
+
out = canon(cur);
|
|
3370
|
+
break;
|
|
3371
|
+
}
|
|
3372
|
+
if (dirname4(cur) === cur)
|
|
3373
|
+
break;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
memo.set(p, out);
|
|
3378
|
+
return out;
|
|
3379
|
+
}
|
|
3380
|
+
let merged = 0;
|
|
3381
|
+
const upsertProject = db.prepare(`INSERT INTO projects (id, agent_id, path, encoded_dir, first_seen, last_seen)
|
|
3382
|
+
VALUES (?, ?, ?, NULL, NULL, NULL) ON CONFLICT(agent_id, path) DO NOTHING`);
|
|
3383
|
+
const repointSessions = db.prepare("UPDATE sessions SET project_id = ? WHERE project_id = ?");
|
|
3384
|
+
const repointFileChanges = db.prepare("UPDATE file_changes SET project_id = ? WHERE project_id = ?");
|
|
3385
|
+
const touched = /* @__PURE__ */ new Set();
|
|
3386
|
+
db.transaction(() => {
|
|
3387
|
+
for (const r of rows) {
|
|
3388
|
+
const target = canon(r.path);
|
|
3389
|
+
if (target === strip(r.path))
|
|
3390
|
+
continue;
|
|
3391
|
+
const targetId = byPath.get(target)?.id ?? projectId(r.agent_id, target);
|
|
3392
|
+
if (targetId === r.id)
|
|
3393
|
+
continue;
|
|
3394
|
+
if (!byPath.has(target)) {
|
|
3395
|
+
upsertProject.run(targetId, r.agent_id, target);
|
|
3396
|
+
byPath.set(target, { id: targetId, agent_id: r.agent_id });
|
|
3397
|
+
}
|
|
3398
|
+
repointSessions.run(targetId, r.id);
|
|
3399
|
+
repointFileChanges.run(targetId, r.id);
|
|
3400
|
+
touched.add(targetId);
|
|
3401
|
+
merged++;
|
|
3402
|
+
}
|
|
3403
|
+
const refresh = db.prepare(`UPDATE projects SET
|
|
3404
|
+
first_seen = (SELECT MIN(started_at) FROM sessions WHERE project_id = projects.id),
|
|
3405
|
+
last_seen = (SELECT MAX(COALESCE(ended_at, started_at)) FROM sessions WHERE project_id = projects.id)
|
|
3406
|
+
WHERE id = ?`);
|
|
3407
|
+
for (const id of touched)
|
|
3408
|
+
refresh.run(id);
|
|
3409
|
+
})();
|
|
3410
|
+
const removed = db.prepare("DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)").run().changes;
|
|
3411
|
+
return { merged, removed };
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
// ../ingest/dist/pipeline.js
|
|
3415
|
+
import { createHash as createHash5 } from "crypto";
|
|
3205
3416
|
function newStats() {
|
|
3206
3417
|
return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
|
|
3207
3418
|
}
|
|
@@ -3239,7 +3450,7 @@ function skillSummary(body) {
|
|
|
3239
3450
|
return null;
|
|
3240
3451
|
}
|
|
3241
3452
|
function skillVersionId(name, body) {
|
|
3242
|
-
return
|
|
3453
|
+
return createHash5("sha1").update(name).update("\0").update(body).digest("hex").slice(0, 16);
|
|
3243
3454
|
}
|
|
3244
3455
|
function pruneExcluded(db, excludedPaths) {
|
|
3245
3456
|
if (!excludedPaths.length)
|
|
@@ -3274,6 +3485,7 @@ function pruneExcluded(db, excludedPaths) {
|
|
|
3274
3485
|
db.pragma("foreign_keys = OFF");
|
|
3275
3486
|
db.transaction(() => {
|
|
3276
3487
|
db.exec("DELETE FROM findings WHERE session_id IN (SELECT id FROM _prune)");
|
|
3488
|
+
db.exec("DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _prune)");
|
|
3277
3489
|
for (const t of ["token_usage", "tool_calls", "turns", "events"])
|
|
3278
3490
|
db.exec(`DELETE FROM ${t} WHERE session_id IN (SELECT id FROM _prune)`);
|
|
3279
3491
|
db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id IN (SELECT id FROM _prune)");
|
|
@@ -3507,6 +3719,7 @@ function rebuildDerived(db, dirty) {
|
|
|
3507
3719
|
db.exec(`DELETE FROM sessions WHERE event_count = 0${andId}`);
|
|
3508
3720
|
db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id NOT IN (SELECT id FROM sessions)");
|
|
3509
3721
|
db.exec("DELETE FROM findings WHERE session_id NOT IN (SELECT id FROM sessions)");
|
|
3722
|
+
db.exec("DELETE FROM file_changes WHERE session_id NOT IN (SELECT id FROM sessions)");
|
|
3510
3723
|
if (!incremental) {
|
|
3511
3724
|
db.exec("DELETE FROM skills WHERE id NOT IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)");
|
|
3512
3725
|
}
|
|
@@ -3619,11 +3832,11 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
|
3619
3832
|
result_summary: tr.result_summary ?? null
|
|
3620
3833
|
});
|
|
3621
3834
|
}
|
|
3622
|
-
let
|
|
3835
|
+
let projectId2 = null;
|
|
3623
3836
|
if (sessionMeta.cwd) {
|
|
3624
|
-
|
|
3837
|
+
projectId2 = createHash5("sha1").update(`${adapter.agentId}\0${sessionMeta.cwd}`).digest("hex").slice(0, 16);
|
|
3625
3838
|
stmts.upsertProject.run({
|
|
3626
|
-
id:
|
|
3839
|
+
id: projectId2,
|
|
3627
3840
|
agent_id: adapter.agentId,
|
|
3628
3841
|
path: sessionMeta.cwd,
|
|
3629
3842
|
encoded_dir: file.encodedDir || null,
|
|
@@ -3634,7 +3847,7 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
|
3634
3847
|
id: file.sessionId,
|
|
3635
3848
|
agent_id: adapter.agentId,
|
|
3636
3849
|
source_id: file.sourceId,
|
|
3637
|
-
project_id:
|
|
3850
|
+
project_id: projectId2,
|
|
3638
3851
|
slug: sessionMeta.slug ?? null,
|
|
3639
3852
|
ai_title: sessionMeta.ai_title ?? null,
|
|
3640
3853
|
cli_version: sessionMeta.cli_version ?? null,
|
|
@@ -3657,10 +3870,10 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
|
3657
3870
|
|
|
3658
3871
|
// ../ingest/dist/workflows.js
|
|
3659
3872
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
3660
|
-
import { basename as basename3, join as
|
|
3873
|
+
import { basename as basename3, join as join7 } from "path";
|
|
3661
3874
|
|
|
3662
3875
|
// ../ingest/dist/redact.js
|
|
3663
|
-
import { createHash as
|
|
3876
|
+
import { createHash as createHash6, randomBytes } from "crypto";
|
|
3664
3877
|
function parseExcludes(csv) {
|
|
3665
3878
|
return (csv ?? "").split(",").map((s) => s.trim()).filter(Boolean).map(encodeProjectPath);
|
|
3666
3879
|
}
|
|
@@ -3669,17 +3882,17 @@ function isExcludedArchivePath(filePath, excluded) {
|
|
|
3669
3882
|
}
|
|
3670
3883
|
|
|
3671
3884
|
// ../ingest/dist/fileread.js
|
|
3672
|
-
import { createHash as
|
|
3885
|
+
import { createHash as createHash7 } from "crypto";
|
|
3673
3886
|
import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
|
|
3674
3887
|
import { StringDecoder } from "string_decoder";
|
|
3675
3888
|
var STREAM_THRESHOLD = 8 * 1024 * 1024;
|
|
3676
3889
|
var CHUNK2 = 64 * 1024;
|
|
3677
3890
|
function sha256(buf) {
|
|
3678
|
-
return
|
|
3891
|
+
return createHash7("sha256").update(buf).digest("hex");
|
|
3679
3892
|
}
|
|
3680
|
-
function sha256File(
|
|
3681
|
-
const fd = openSync2(
|
|
3682
|
-
const h =
|
|
3893
|
+
function sha256File(path3) {
|
|
3894
|
+
const fd = openSync2(path3, "r");
|
|
3895
|
+
const h = createHash7("sha256");
|
|
3683
3896
|
const buf = Buffer.allocUnsafe(CHUNK2);
|
|
3684
3897
|
try {
|
|
3685
3898
|
let n;
|
|
@@ -3690,8 +3903,8 @@ function sha256File(path2) {
|
|
|
3690
3903
|
}
|
|
3691
3904
|
return h.digest("hex");
|
|
3692
3905
|
}
|
|
3693
|
-
function* streamLines(
|
|
3694
|
-
const fd = openSync2(
|
|
3906
|
+
function* streamLines(path3) {
|
|
3907
|
+
const fd = openSync2(path3, "r");
|
|
3695
3908
|
const decoder = new StringDecoder("utf8");
|
|
3696
3909
|
const buf = Buffer.allocUnsafe(CHUNK2);
|
|
3697
3910
|
let leftover = "";
|
|
@@ -3725,7 +3938,7 @@ function walkSidecars(dir, inWorkflows, out) {
|
|
|
3725
3938
|
return;
|
|
3726
3939
|
}
|
|
3727
3940
|
for (const e of entries) {
|
|
3728
|
-
const p =
|
|
3941
|
+
const p = join7(dir, e.name);
|
|
3729
3942
|
if (e.isDirectory())
|
|
3730
3943
|
walkSidecars(p, inWorkflows || e.name === "workflows", out);
|
|
3731
3944
|
else if (e.isFile() && inWorkflows && /^wf_.+\.json$/.test(e.name))
|
|
@@ -3734,17 +3947,17 @@ function walkSidecars(dir, inWorkflows, out) {
|
|
|
3734
3947
|
}
|
|
3735
3948
|
function discoverSidecars(sourceArchiveDir) {
|
|
3736
3949
|
const out = [];
|
|
3737
|
-
walkSidecars(
|
|
3950
|
+
walkSidecars(join7(sourceArchiveDir, "projects"), false, out);
|
|
3738
3951
|
try {
|
|
3739
|
-
for (const ts of readdirSync3(
|
|
3952
|
+
for (const ts of readdirSync3(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3740
3953
|
if (ts.isDirectory())
|
|
3741
|
-
walkSidecars(
|
|
3954
|
+
walkSidecars(join7(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
3742
3955
|
} catch {
|
|
3743
3956
|
}
|
|
3744
3957
|
return out;
|
|
3745
3958
|
}
|
|
3746
|
-
function sessionFromPath(
|
|
3747
|
-
const parts =
|
|
3959
|
+
function sessionFromPath(path3) {
|
|
3960
|
+
const parts = path3.split("/");
|
|
3748
3961
|
const i = parts.indexOf("workflows");
|
|
3749
3962
|
return i > 0 ? parts[i - 1] : null;
|
|
3750
3963
|
}
|
|
@@ -3791,25 +4004,25 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3791
4004
|
total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
|
|
3792
4005
|
duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
|
|
3793
4006
|
ingested_at=excluded.ingested_at`);
|
|
3794
|
-
for (const
|
|
4007
|
+
for (const path3 of paths) {
|
|
3795
4008
|
stats.files++;
|
|
3796
|
-
const st = statSync2(
|
|
4009
|
+
const st = statSync2(path3);
|
|
3797
4010
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3798
|
-
const prev = full ? void 0 : getState.get(
|
|
4011
|
+
const prev = full ? void 0 : getState.get(path3);
|
|
3799
4012
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3800
4013
|
stats.skipped++;
|
|
3801
4014
|
continue;
|
|
3802
4015
|
}
|
|
3803
4016
|
let buf;
|
|
3804
4017
|
try {
|
|
3805
|
-
buf = readFileSync4(
|
|
4018
|
+
buf = readFileSync4(path3);
|
|
3806
4019
|
} catch {
|
|
3807
4020
|
stats.malformed++;
|
|
3808
4021
|
continue;
|
|
3809
4022
|
}
|
|
3810
4023
|
const hash2 = sha256(buf);
|
|
3811
4024
|
if (prev && prev.sha256 === hash2) {
|
|
3812
|
-
setState.run({ file_path:
|
|
4025
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3813
4026
|
stats.skipped++;
|
|
3814
4027
|
continue;
|
|
3815
4028
|
}
|
|
@@ -3824,14 +4037,14 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3824
4037
|
stats.malformed++;
|
|
3825
4038
|
continue;
|
|
3826
4039
|
}
|
|
3827
|
-
const runId = strOrNull(d.runId) ?? basename3(
|
|
4040
|
+
const runId = strOrNull(d.runId) ?? basename3(path3, ".json");
|
|
3828
4041
|
const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
|
|
3829
4042
|
const durationMs2 = intOrNull(d.durationMs);
|
|
3830
4043
|
const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
|
|
3831
4044
|
upsert.run({
|
|
3832
4045
|
run_id: runId,
|
|
3833
4046
|
source_id: sourceId,
|
|
3834
|
-
session_id: sessionFromPath(
|
|
4047
|
+
session_id: sessionFromPath(path3),
|
|
3835
4048
|
task_id: strOrNull(d.taskId),
|
|
3836
4049
|
workflow_name: strOrNull(d.workflowName),
|
|
3837
4050
|
status: strOrNull(d.status),
|
|
@@ -3849,14 +4062,14 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3849
4062
|
ended_at: endedAt,
|
|
3850
4063
|
ingested_at: now2
|
|
3851
4064
|
});
|
|
3852
|
-
setState.run({ file_path:
|
|
4065
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3853
4066
|
stats.upserted++;
|
|
3854
4067
|
}
|
|
3855
4068
|
}
|
|
3856
4069
|
|
|
3857
4070
|
// ../ingest/dist/meta.js
|
|
3858
4071
|
import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
3859
|
-
import { basename as basename4, join as
|
|
4072
|
+
import { basename as basename4, join as join8 } from "path";
|
|
3860
4073
|
function newMetaStats() {
|
|
3861
4074
|
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
3862
4075
|
}
|
|
@@ -3868,7 +4081,7 @@ function walkMeta(dir, inSubagents, out) {
|
|
|
3868
4081
|
return;
|
|
3869
4082
|
}
|
|
3870
4083
|
for (const e of entries) {
|
|
3871
|
-
const p =
|
|
4084
|
+
const p = join8(dir, e.name);
|
|
3872
4085
|
if (e.isDirectory())
|
|
3873
4086
|
walkMeta(p, inSubagents || e.name === "subagents", out);
|
|
3874
4087
|
else if (e.isFile() && inSubagents && /^agent-.+\.meta\.json$/.test(e.name))
|
|
@@ -3877,11 +4090,11 @@ function walkMeta(dir, inSubagents, out) {
|
|
|
3877
4090
|
}
|
|
3878
4091
|
function discoverMeta(sourceArchiveDir) {
|
|
3879
4092
|
const out = [];
|
|
3880
|
-
walkMeta(
|
|
4093
|
+
walkMeta(join8(sourceArchiveDir, "projects"), false, out);
|
|
3881
4094
|
try {
|
|
3882
|
-
for (const ts of readdirSync4(
|
|
4095
|
+
for (const ts of readdirSync4(join8(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3883
4096
|
if (ts.isDirectory())
|
|
3884
|
-
walkMeta(
|
|
4097
|
+
walkMeta(join8(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
3885
4098
|
} catch {
|
|
3886
4099
|
}
|
|
3887
4100
|
return out;
|
|
@@ -3905,25 +4118,25 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3905
4118
|
source_id=excluded.source_id, agent_type=excluded.agent_type,
|
|
3906
4119
|
agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
|
|
3907
4120
|
tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
|
|
3908
|
-
for (const
|
|
4121
|
+
for (const path3 of paths) {
|
|
3909
4122
|
stats.files++;
|
|
3910
|
-
const st = statSync3(
|
|
4123
|
+
const st = statSync3(path3);
|
|
3911
4124
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3912
|
-
const prev = full ? void 0 : getState.get(
|
|
4125
|
+
const prev = full ? void 0 : getState.get(path3);
|
|
3913
4126
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3914
4127
|
stats.skipped++;
|
|
3915
4128
|
continue;
|
|
3916
4129
|
}
|
|
3917
4130
|
let buf;
|
|
3918
4131
|
try {
|
|
3919
|
-
buf = readFileSync5(
|
|
4132
|
+
buf = readFileSync5(path3);
|
|
3920
4133
|
} catch {
|
|
3921
4134
|
stats.malformed++;
|
|
3922
4135
|
continue;
|
|
3923
4136
|
}
|
|
3924
4137
|
const hash2 = sha256(buf);
|
|
3925
4138
|
if (prev && prev.sha256 === hash2) {
|
|
3926
|
-
setState.run({ file_path:
|
|
4139
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3927
4140
|
stats.skipped++;
|
|
3928
4141
|
continue;
|
|
3929
4142
|
}
|
|
@@ -3938,7 +4151,7 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3938
4151
|
stats.malformed++;
|
|
3939
4152
|
continue;
|
|
3940
4153
|
}
|
|
3941
|
-
const sessionId = basename4(
|
|
4154
|
+
const sessionId = basename4(path3, ".meta.json");
|
|
3942
4155
|
upsert.run({
|
|
3943
4156
|
session_id: sessionId,
|
|
3944
4157
|
source_id: sourceId,
|
|
@@ -3948,14 +4161,14 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3948
4161
|
tool_use_id: strOrNull2(d.toolUseId),
|
|
3949
4162
|
ingested_at: now2
|
|
3950
4163
|
});
|
|
3951
|
-
setState.run({ file_path:
|
|
4164
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3952
4165
|
stats.upserted++;
|
|
3953
4166
|
}
|
|
3954
4167
|
}
|
|
3955
4168
|
|
|
3956
4169
|
// ../ingest/dist/toolResults.js
|
|
3957
4170
|
import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
3958
|
-
import { basename as basename5, join as
|
|
4171
|
+
import { basename as basename5, join as join9 } from "path";
|
|
3959
4172
|
function newToolResultStats() {
|
|
3960
4173
|
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
3961
4174
|
}
|
|
@@ -3967,7 +4180,7 @@ function walkResults(dir, inResults, out) {
|
|
|
3967
4180
|
return;
|
|
3968
4181
|
}
|
|
3969
4182
|
for (const e of entries) {
|
|
3970
|
-
const p =
|
|
4183
|
+
const p = join9(dir, e.name);
|
|
3971
4184
|
if (e.isDirectory())
|
|
3972
4185
|
walkResults(p, inResults || e.name === "tool-results", out);
|
|
3973
4186
|
else if (e.isFile() && inResults && e.name.endsWith(".txt"))
|
|
@@ -3976,17 +4189,17 @@ function walkResults(dir, inResults, out) {
|
|
|
3976
4189
|
}
|
|
3977
4190
|
function discoverResults(sourceArchiveDir) {
|
|
3978
4191
|
const out = [];
|
|
3979
|
-
walkResults(
|
|
4192
|
+
walkResults(join9(sourceArchiveDir, "projects"), false, out);
|
|
3980
4193
|
try {
|
|
3981
|
-
for (const ts of readdirSync5(
|
|
4194
|
+
for (const ts of readdirSync5(join9(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3982
4195
|
if (ts.isDirectory())
|
|
3983
|
-
walkResults(
|
|
4196
|
+
walkResults(join9(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
3984
4197
|
} catch {
|
|
3985
4198
|
}
|
|
3986
4199
|
return out;
|
|
3987
4200
|
}
|
|
3988
|
-
function sessionFromPath2(
|
|
3989
|
-
const parts =
|
|
4201
|
+
function sessionFromPath2(path3) {
|
|
4202
|
+
const parts = path3.split("/");
|
|
3990
4203
|
const i = parts.indexOf("tool-results");
|
|
3991
4204
|
return i > 0 ? parts[i - 1] : null;
|
|
3992
4205
|
}
|
|
@@ -4003,40 +4216,40 @@ function ingestToolResults(db, sourceArchiveDir, excludedDirs, now2, stats, full
|
|
|
4003
4216
|
VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
|
|
4004
4217
|
ON CONFLICT(session_id, name) DO UPDATE SET
|
|
4005
4218
|
path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`);
|
|
4006
|
-
for (const
|
|
4007
|
-
const sessionId = sessionFromPath2(
|
|
4219
|
+
for (const path3 of paths) {
|
|
4220
|
+
const sessionId = sessionFromPath2(path3);
|
|
4008
4221
|
if (!sessionId)
|
|
4009
4222
|
continue;
|
|
4010
4223
|
stats.files++;
|
|
4011
|
-
const st = statSync4(
|
|
4224
|
+
const st = statSync4(path3);
|
|
4012
4225
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
4013
|
-
const prev = full ? void 0 : getState.get(
|
|
4226
|
+
const prev = full ? void 0 : getState.get(path3);
|
|
4014
4227
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
4015
4228
|
stats.skipped++;
|
|
4016
4229
|
continue;
|
|
4017
4230
|
}
|
|
4018
4231
|
let buf;
|
|
4019
4232
|
try {
|
|
4020
|
-
buf = readFileSync6(
|
|
4233
|
+
buf = readFileSync6(path3);
|
|
4021
4234
|
} catch {
|
|
4022
4235
|
stats.malformed++;
|
|
4023
4236
|
continue;
|
|
4024
4237
|
}
|
|
4025
4238
|
const hash2 = sha256(buf);
|
|
4026
4239
|
if (prev && prev.sha256 === hash2) {
|
|
4027
|
-
setState.run({ file_path:
|
|
4240
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
4028
4241
|
stats.skipped++;
|
|
4029
4242
|
continue;
|
|
4030
4243
|
}
|
|
4031
4244
|
upsert.run({
|
|
4032
4245
|
session_id: sessionId,
|
|
4033
|
-
name: basename5(
|
|
4034
|
-
path:
|
|
4246
|
+
name: basename5(path3, ".txt"),
|
|
4247
|
+
path: path3,
|
|
4035
4248
|
bytes: st.size,
|
|
4036
4249
|
text: buf.toString("utf8"),
|
|
4037
4250
|
ingested_at: now2
|
|
4038
4251
|
});
|
|
4039
|
-
setState.run({ file_path:
|
|
4252
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
4040
4253
|
stats.upserted++;
|
|
4041
4254
|
}
|
|
4042
4255
|
}
|
|
@@ -4058,11 +4271,11 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4058
4271
|
const args = parseIngestArgs(argv);
|
|
4059
4272
|
const archiveRoot = args.archive || resolveArchiveDir();
|
|
4060
4273
|
const { path: dbPath } = resolveDbPath(args.db);
|
|
4061
|
-
if (!
|
|
4274
|
+
if (!existsSync5(archiveRoot)) {
|
|
4062
4275
|
console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
|
|
4063
4276
|
process.exit(1);
|
|
4064
4277
|
}
|
|
4065
|
-
mkdirSync4(
|
|
4278
|
+
mkdirSync4(dirname5(dbPath), { recursive: true });
|
|
4066
4279
|
const db = args.full ? openRaw(dbPath) : openDb(dbPath);
|
|
4067
4280
|
if (args.full) {
|
|
4068
4281
|
resetSchema(db);
|
|
@@ -4095,7 +4308,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4095
4308
|
}
|
|
4096
4309
|
stmts.insAgent.run(adapter.agentId, adapter.agentName);
|
|
4097
4310
|
stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
|
|
4098
|
-
let files = adapter.discover(
|
|
4311
|
+
let files = adapter.discover(join10(archiveRoot, source.label), source.label);
|
|
4099
4312
|
if (excludedDirs.length)
|
|
4100
4313
|
files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
|
|
4101
4314
|
files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
|
|
@@ -4127,14 +4340,16 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4127
4340
|
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: hash2 }, now2, stats);
|
|
4128
4341
|
dirty.add(file.sessionId);
|
|
4129
4342
|
}
|
|
4130
|
-
ingestWorkflowResults(db,
|
|
4131
|
-
ingestSubagentMeta(db,
|
|
4132
|
-
ingestToolResults(db,
|
|
4343
|
+
ingestWorkflowResults(db, join10(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
|
|
4344
|
+
ingestSubagentMeta(db, join10(archiveRoot, source.label), source.label, excludedDirs, now2, metaStats, args.full);
|
|
4345
|
+
ingestToolResults(db, join10(archiveRoot, source.label), excludedDirs, now2, trStats, args.full);
|
|
4133
4346
|
}
|
|
4134
4347
|
const expanded = rebuildDerived(db, args.full ? null : dirty);
|
|
4348
|
+
canonicalizeProjects(db);
|
|
4135
4349
|
const classified = classify(db, args.full ? null : expanded);
|
|
4136
4350
|
const detected = detect(db, args.full ? null : expanded);
|
|
4137
4351
|
classifyErrors(db, args.full ? null : expanded);
|
|
4352
|
+
const fileChanges = deriveFileChanges(db, args.full ? null : expanded);
|
|
4138
4353
|
const count = (sql) => db.prepare(sql).get().n;
|
|
4139
4354
|
const sessions = count("SELECT COUNT(*) n FROM sessions");
|
|
4140
4355
|
const turns = count("SELECT COUNT(*) n FROM turns");
|
|
@@ -4160,6 +4375,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4160
4375
|
session_meta=${metaRows} (upserted=${metaStats.upserted} skipped=${metaStats.skipped} malformed=${metaStats.malformed})
|
|
4161
4376
|
tool_results=${trRows} (upserted=${trStats.upserted} skipped=${trStats.skipped} malformed=${trStats.malformed})
|
|
4162
4377
|
findings=${detected.count} (${sevLine})
|
|
4378
|
+
file_changes=${fileChanges.count}
|
|
4163
4379
|
tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
|
|
4164
4380
|
}
|
|
4165
4381
|
function runMetrics(argv = process.argv.slice(2)) {
|
|
@@ -4168,21 +4384,23 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
4168
4384
|
if (argv[i] === "--db")
|
|
4169
4385
|
dbArg = argv[++i] ?? "";
|
|
4170
4386
|
const { path: dbPath } = resolveDbPath(dbArg);
|
|
4171
|
-
if (!
|
|
4387
|
+
if (!existsSync5(dbPath)) {
|
|
4172
4388
|
console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);
|
|
4173
4389
|
process.exit(1);
|
|
4174
4390
|
}
|
|
4175
4391
|
const db = openDb(dbPath);
|
|
4392
|
+
canonicalizeProjects(db);
|
|
4176
4393
|
const r = classify(db);
|
|
4177
4394
|
const d = detect(db);
|
|
4178
4395
|
const e = classifyErrors(db);
|
|
4396
|
+
const fc = deriveFileChanges(db);
|
|
4179
4397
|
db.close();
|
|
4180
|
-
console.log(`agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} findings=${d.count} detector_version=${DETECTOR_VERSION} errors_classified=${e.count} error_classifier_version=${e.version} db=${dbPath}`);
|
|
4398
|
+
console.log(`agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} findings=${d.count} detector_version=${DETECTOR_VERSION} errors_classified=${e.count} error_classifier_version=${e.version} file_changes=${fc.count} filechanges_version=${FILECHANGES_VERSION} db=${dbPath}`);
|
|
4181
4399
|
}
|
|
4182
4400
|
|
|
4183
4401
|
// ../server/dist/run.js
|
|
4184
|
-
import { existsSync as
|
|
4185
|
-
import { dirname as
|
|
4402
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4403
|
+
import { dirname as dirname6 } from "path";
|
|
4186
4404
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4187
4405
|
|
|
4188
4406
|
// ../server/dist/db.js
|
|
@@ -4508,7 +4726,13 @@ function getSession(db, id) {
|
|
|
4508
4726
|
session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
|
|
4509
4727
|
session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
|
|
4510
4728
|
session.tool_failure_count = session.tool_error_count - session.tool_rejection_count;
|
|
4511
|
-
|
|
4729
|
+
let fileChanges = [];
|
|
4730
|
+
if (tableExists(db, "file_changes")) {
|
|
4731
|
+
fileChanges = db.prepare(`SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
|
|
4732
|
+
fc.lines_added, fc.lines_removed, fc.timestamp
|
|
4733
|
+
FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`).all(id);
|
|
4734
|
+
}
|
|
4735
|
+
return { session, turns, events, classification, parent, children, workflow_runs, findings: sessionFindings, file_changes: fileChanges };
|
|
4512
4736
|
}
|
|
4513
4737
|
function xmlTag(text, tag) {
|
|
4514
4738
|
return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
|
|
@@ -4644,6 +4868,107 @@ function listSkills(db, f = {}) {
|
|
|
4644
4868
|
r.sources = r.sources ? String(r.sources).split(",").filter(Boolean) : [];
|
|
4645
4869
|
return rows;
|
|
4646
4870
|
}
|
|
4871
|
+
function listFiles(db, f = {}) {
|
|
4872
|
+
if (!tableExists(db, "file_changes"))
|
|
4873
|
+
return { total: 0, files: [] };
|
|
4874
|
+
const where = [];
|
|
4875
|
+
const params = [];
|
|
4876
|
+
if (f.q && f.q.trim())
|
|
4877
|
+
where.push("fc.file_path LIKE ?"), params.push(`%${f.q.trim()}%`);
|
|
4878
|
+
if (f.project)
|
|
4879
|
+
where.push("fc.project_id = ?"), params.push(f.project);
|
|
4880
|
+
if (f.source)
|
|
4881
|
+
where.push("EXISTS (SELECT 1 FROM sessions s WHERE s.id = fc.session_id AND s.source_id = ?)"), params.push(f.source);
|
|
4882
|
+
const whereSql = where.length ? ` WHERE ${where.join(" AND ")}` : "";
|
|
4883
|
+
const grouped = `SELECT fc.project_id, fc.file_path,
|
|
4884
|
+
COUNT(DISTINCT fc.session_id) AS sessions,
|
|
4885
|
+
COUNT(*) AS changes,
|
|
4886
|
+
SUM(fc.lines_added) AS lines_added,
|
|
4887
|
+
SUM(fc.lines_removed) AS lines_removed,
|
|
4888
|
+
MIN(fc.timestamp) AS first_ts,
|
|
4889
|
+
MAX(fc.timestamp) AS last_ts
|
|
4890
|
+
FROM file_changes fc${whereSql}
|
|
4891
|
+
GROUP BY fc.project_id, fc.file_path`;
|
|
4892
|
+
const total = db.prepare(`SELECT COUNT(*) n FROM (${grouped})`).get(...params).n;
|
|
4893
|
+
const ORDER = {
|
|
4894
|
+
last_ts: "last_ts",
|
|
4895
|
+
first_ts: "first_ts",
|
|
4896
|
+
changes: "changes",
|
|
4897
|
+
sessions: "sessions",
|
|
4898
|
+
path: "file_path"
|
|
4899
|
+
};
|
|
4900
|
+
const col = ORDER[f.sort ?? "last_ts"] ?? "last_ts";
|
|
4901
|
+
const dir = f.dir === "asc" ? "ASC" : "DESC";
|
|
4902
|
+
const limit = Math.min(f.limit ?? 50, 200);
|
|
4903
|
+
const offset = f.offset ?? 0;
|
|
4904
|
+
const files = db.prepare(`SELECT g.*, p.path AS project_path FROM (${grouped}) g
|
|
4905
|
+
LEFT JOIN projects p ON p.id = g.project_id
|
|
4906
|
+
ORDER BY ${col} ${dir}, g.file_path ASC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
4907
|
+
return { total, files };
|
|
4908
|
+
}
|
|
4909
|
+
function getFileTimeline(db, filePath, projectId2) {
|
|
4910
|
+
if (!tableExists(db, "file_changes"))
|
|
4911
|
+
return null;
|
|
4912
|
+
const params = [filePath];
|
|
4913
|
+
let scope = "fc.file_path = ?";
|
|
4914
|
+
if (projectId2)
|
|
4915
|
+
scope += " AND fc.project_id = ?", params.push(projectId2);
|
|
4916
|
+
const rows = db.prepare(`SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,
|
|
4917
|
+
fc.lines_removed, fc.timestamp, fc.project_id,
|
|
4918
|
+
s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,
|
|
4919
|
+
p.path AS project_path
|
|
4920
|
+
FROM file_changes fc
|
|
4921
|
+
JOIN sessions s ON s.id = fc.session_id
|
|
4922
|
+
LEFT JOIN turns t ON t.id = fc.turn_id
|
|
4923
|
+
LEFT JOIN projects p ON p.id = fc.project_id
|
|
4924
|
+
WHERE ${scope} ORDER BY fc.timestamp`).all(...params);
|
|
4925
|
+
if (!rows.length)
|
|
4926
|
+
return null;
|
|
4927
|
+
const cat = tableExists(db, "classifications") ? db.prepare("SELECT category FROM classifications WHERE scope = 'session' AND target_id = ?") : null;
|
|
4928
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
4929
|
+
for (const r of rows) {
|
|
4930
|
+
let g = bySession.get(r.session_id);
|
|
4931
|
+
if (!g) {
|
|
4932
|
+
g = {
|
|
4933
|
+
session_id: r.session_id,
|
|
4934
|
+
title: r.ai_title || r.slug || null,
|
|
4935
|
+
source_id: r.source_id,
|
|
4936
|
+
started_at: r.started_at,
|
|
4937
|
+
category: cat ? cat.get(r.session_id)?.category ?? null : null,
|
|
4938
|
+
changes: []
|
|
4939
|
+
};
|
|
4940
|
+
bySession.set(r.session_id, g);
|
|
4941
|
+
}
|
|
4942
|
+
g.changes.push({
|
|
4943
|
+
id: r.id,
|
|
4944
|
+
turn_id: r.turn_id,
|
|
4945
|
+
turn_seq: r.turn_seq,
|
|
4946
|
+
prompt_preview: r.prompt_preview,
|
|
4947
|
+
event_uuid: r.event_uuid,
|
|
4948
|
+
tool_name: r.tool_name,
|
|
4949
|
+
lines_added: r.lines_added,
|
|
4950
|
+
lines_removed: r.lines_removed,
|
|
4951
|
+
timestamp: r.timestamp
|
|
4952
|
+
});
|
|
4953
|
+
}
|
|
4954
|
+
const sessions = [...bySession.values()].sort((a, b) => {
|
|
4955
|
+
const la = a.changes[a.changes.length - 1].timestamp ?? "";
|
|
4956
|
+
const lb = b.changes[b.changes.length - 1].timestamp ?? "";
|
|
4957
|
+
return la < lb ? 1 : la > lb ? -1 : 0;
|
|
4958
|
+
});
|
|
4959
|
+
return {
|
|
4960
|
+
file_path: filePath,
|
|
4961
|
+
project_id: rows[0].project_id ?? null,
|
|
4962
|
+
project_path: rows[0].project_path ?? null,
|
|
4963
|
+
sessions_count: sessions.length,
|
|
4964
|
+
changes_count: rows.length,
|
|
4965
|
+
lines_added: rows.reduce((a, r) => a + (r.lines_added ?? 0), 0),
|
|
4966
|
+
lines_removed: rows.reduce((a, r) => a + (r.lines_removed ?? 0), 0),
|
|
4967
|
+
first_ts: rows[0].timestamp ?? null,
|
|
4968
|
+
last_ts: rows[rows.length - 1].timestamp ?? null,
|
|
4969
|
+
sessions
|
|
4970
|
+
};
|
|
4971
|
+
}
|
|
4647
4972
|
function getSkill(db, name) {
|
|
4648
4973
|
const versions = db.prepare(`SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,
|
|
4649
4974
|
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count
|
|
@@ -4761,7 +5086,7 @@ function securitySummary(db) {
|
|
|
4761
5086
|
}
|
|
4762
5087
|
|
|
4763
5088
|
// ../server/dist/app.js
|
|
4764
|
-
import { existsSync as
|
|
5089
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4765
5090
|
import Fastify from "fastify";
|
|
4766
5091
|
import fastifyStatic from "@fastify/static";
|
|
4767
5092
|
|
|
@@ -5037,8 +5362,8 @@ function dashboardBreakdowns(db, f) {
|
|
|
5037
5362
|
}
|
|
5038
5363
|
|
|
5039
5364
|
// ../server/dist/refresh.js
|
|
5040
|
-
import { existsSync as
|
|
5041
|
-
import { join as
|
|
5365
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
5366
|
+
import { join as join11 } from "path";
|
|
5042
5367
|
function originAllowed(origin) {
|
|
5043
5368
|
if (!origin)
|
|
5044
5369
|
return true;
|
|
@@ -5059,13 +5384,13 @@ function writeBlocked(headers) {
|
|
|
5059
5384
|
}
|
|
5060
5385
|
function runRefresh() {
|
|
5061
5386
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
5062
|
-
const lock = acquireLock(
|
|
5387
|
+
const lock = acquireLock(join11(dataDir, ".agent-lens.lock"));
|
|
5063
5388
|
if (!lock)
|
|
5064
5389
|
return null;
|
|
5065
5390
|
try {
|
|
5066
5391
|
const collected = collectAll();
|
|
5067
5392
|
const archiveRoot = resolveArchiveDir();
|
|
5068
|
-
if (!
|
|
5393
|
+
if (!existsSync6(archiveRoot))
|
|
5069
5394
|
mkdirSync5(archiveRoot, { recursive: true });
|
|
5070
5395
|
runIngest([]);
|
|
5071
5396
|
return { ok: true, collected };
|
|
@@ -5360,6 +5685,28 @@ async function createApp(db, opts = {}) {
|
|
|
5360
5685
|
return reply.code(404).send({ error: "not found" });
|
|
5361
5686
|
return result;
|
|
5362
5687
|
});
|
|
5688
|
+
app.get("/api/files", async (req) => {
|
|
5689
|
+
const q = req.query;
|
|
5690
|
+
const sort = ["last_ts", "first_ts", "changes", "sessions", "path"].find((s) => s === q.sort);
|
|
5691
|
+
return listFiles(db, {
|
|
5692
|
+
q: q.q,
|
|
5693
|
+
source: q.source,
|
|
5694
|
+
project: q.project,
|
|
5695
|
+
sort,
|
|
5696
|
+
dir: q.dir === "asc" ? "asc" : "desc",
|
|
5697
|
+
limit: Math.min(Number(q.limit) || 50, 200),
|
|
5698
|
+
offset: Number(q.offset) || 0
|
|
5699
|
+
});
|
|
5700
|
+
});
|
|
5701
|
+
app.get("/api/file", async (req, reply) => {
|
|
5702
|
+
const q = req.query;
|
|
5703
|
+
if (!q.path)
|
|
5704
|
+
return reply.code(400).send({ error: "missing path" });
|
|
5705
|
+
const result = getFileTimeline(db, q.path, q.project || void 0);
|
|
5706
|
+
if (!result)
|
|
5707
|
+
return reply.code(404).send({ error: "not found" });
|
|
5708
|
+
return result;
|
|
5709
|
+
});
|
|
5363
5710
|
app.get("/api/sessions/:id/export.md", async (req, reply) => {
|
|
5364
5711
|
const { id } = req.params;
|
|
5365
5712
|
const level = parseRedactionLevel(req.query.redact);
|
|
@@ -5368,7 +5715,7 @@ async function createApp(db, opts = {}) {
|
|
|
5368
5715
|
return reply.code(404).send({ error: "not found" });
|
|
5369
5716
|
reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="${out.filename}"`).send(out.markdown);
|
|
5370
5717
|
});
|
|
5371
|
-
if (opts.webDist &&
|
|
5718
|
+
if (opts.webDist && existsSync7(opts.webDist)) {
|
|
5372
5719
|
await app.register(fastifyStatic, { root: opts.webDist });
|
|
5373
5720
|
app.setNotFoundHandler((req, reply) => {
|
|
5374
5721
|
if (req.url.startsWith("/api/"))
|
|
@@ -5381,7 +5728,7 @@ async function createApp(db, opts = {}) {
|
|
|
5381
5728
|
|
|
5382
5729
|
// ../server/dist/run.js
|
|
5383
5730
|
async function startServer(overrides = {}) {
|
|
5384
|
-
const here =
|
|
5731
|
+
const here = dirname6(fileURLToPath2(import.meta.url));
|
|
5385
5732
|
const repoRoot = findRepoRoot(here);
|
|
5386
5733
|
const { path: dbPath } = resolveDbPath(overrides.db);
|
|
5387
5734
|
const { host, port } = resolveServerConfig({ port: overrides.port, host: overrides.host });
|
|
@@ -5390,7 +5737,7 @@ async function startServer(overrides = {}) {
|
|
|
5390
5737
|
console.error(`agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`);
|
|
5391
5738
|
process.exit(1);
|
|
5392
5739
|
}
|
|
5393
|
-
if (!
|
|
5740
|
+
if (!existsSync8(dbPath)) {
|
|
5394
5741
|
console.error(`agent-lens-server: db not found: ${dbPath} (run ingest first)`);
|
|
5395
5742
|
process.exit(1);
|
|
5396
5743
|
}
|
|
@@ -5401,7 +5748,7 @@ async function startServer(overrides = {}) {
|
|
|
5401
5748
|
try {
|
|
5402
5749
|
await app.listen({ host, port });
|
|
5403
5750
|
console.log(`agent-lens-server: http://${host}:${port} (db: ${dbPath})`);
|
|
5404
|
-
if (!
|
|
5751
|
+
if (!existsSync8(webDist))
|
|
5405
5752
|
console.log(" note: web SPA not built \u2014 run `pnpm --filter @agent-lens/web build`");
|
|
5406
5753
|
} catch (err) {
|
|
5407
5754
|
console.error(err);
|
|
@@ -5410,12 +5757,12 @@ async function startServer(overrides = {}) {
|
|
|
5410
5757
|
}
|
|
5411
5758
|
|
|
5412
5759
|
// src/watch.ts
|
|
5413
|
-
import { existsSync as
|
|
5414
|
-
import { join as
|
|
5760
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5761
|
+
import { join as join12 } from "path";
|
|
5415
5762
|
import chokidar from "chokidar";
|
|
5416
5763
|
async function runWatch(opts = {}) {
|
|
5417
5764
|
const sources = loadSources();
|
|
5418
|
-
const lockPath =
|
|
5765
|
+
const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
5419
5766
|
let running = false;
|
|
5420
5767
|
let pending = false;
|
|
5421
5768
|
const cycle = () => {
|
|
@@ -5448,7 +5795,7 @@ async function runWatch(opts = {}) {
|
|
|
5448
5795
|
}
|
|
5449
5796
|
};
|
|
5450
5797
|
cycle();
|
|
5451
|
-
const watchPaths = sources.map((s) =>
|
|
5798
|
+
const watchPaths = sources.map((s) => join12(s.configDir, "projects")).filter((p) => existsSync9(p));
|
|
5452
5799
|
if (watchPaths.length === 0) {
|
|
5453
5800
|
console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
|
|
5454
5801
|
}
|
|
@@ -5476,14 +5823,14 @@ async function runWatch(opts = {}) {
|
|
|
5476
5823
|
// src/index.ts
|
|
5477
5824
|
function version() {
|
|
5478
5825
|
try {
|
|
5479
|
-
const pkg = JSON.parse(readFileSync8(
|
|
5826
|
+
const pkg = JSON.parse(readFileSync8(join13(dirname7(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
|
|
5480
5827
|
return pkg.version ?? "0.0.0";
|
|
5481
5828
|
} catch {
|
|
5482
5829
|
return "0.0.0";
|
|
5483
5830
|
}
|
|
5484
5831
|
}
|
|
5485
5832
|
function withLock(fn) {
|
|
5486
|
-
const lockPath =
|
|
5833
|
+
const lockPath = join13(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
5487
5834
|
const lock = acquireLock(lockPath);
|
|
5488
5835
|
if (!lock) {
|
|
5489
5836
|
console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");
|
|
@@ -5528,7 +5875,7 @@ cli.command("metrics", "Re-run classification over an already-ingested DB").opti
|
|
|
5528
5875
|
});
|
|
5529
5876
|
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) => {
|
|
5530
5877
|
const { path: dbPath } = resolveDbPath(opts.db);
|
|
5531
|
-
if (!
|
|
5878
|
+
if (!existsSync10(dbPath)) {
|
|
5532
5879
|
console.error(`agent-lens: db not found: ${dbPath} (run ingest first)`);
|
|
5533
5880
|
process.exit(1);
|
|
5534
5881
|
}
|