@swestash/agent-lens 0.8.3 → 0.9.0
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 +311 -54
- 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
|
@@ -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
|
|
@@ -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,107 @@ 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/pipeline.js
|
|
3327
|
+
import { createHash as createHash4 } from "crypto";
|
|
3205
3328
|
function newStats() {
|
|
3206
3329
|
return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
|
|
3207
3330
|
}
|
|
@@ -3239,7 +3362,7 @@ function skillSummary(body) {
|
|
|
3239
3362
|
return null;
|
|
3240
3363
|
}
|
|
3241
3364
|
function skillVersionId(name, body) {
|
|
3242
|
-
return
|
|
3365
|
+
return createHash4("sha1").update(name).update("\0").update(body).digest("hex").slice(0, 16);
|
|
3243
3366
|
}
|
|
3244
3367
|
function pruneExcluded(db, excludedPaths) {
|
|
3245
3368
|
if (!excludedPaths.length)
|
|
@@ -3274,6 +3397,7 @@ function pruneExcluded(db, excludedPaths) {
|
|
|
3274
3397
|
db.pragma("foreign_keys = OFF");
|
|
3275
3398
|
db.transaction(() => {
|
|
3276
3399
|
db.exec("DELETE FROM findings WHERE session_id IN (SELECT id FROM _prune)");
|
|
3400
|
+
db.exec("DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _prune)");
|
|
3277
3401
|
for (const t of ["token_usage", "tool_calls", "turns", "events"])
|
|
3278
3402
|
db.exec(`DELETE FROM ${t} WHERE session_id IN (SELECT id FROM _prune)`);
|
|
3279
3403
|
db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id IN (SELECT id FROM _prune)");
|
|
@@ -3507,6 +3631,7 @@ function rebuildDerived(db, dirty) {
|
|
|
3507
3631
|
db.exec(`DELETE FROM sessions WHERE event_count = 0${andId}`);
|
|
3508
3632
|
db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id NOT IN (SELECT id FROM sessions)");
|
|
3509
3633
|
db.exec("DELETE FROM findings WHERE session_id NOT IN (SELECT id FROM sessions)");
|
|
3634
|
+
db.exec("DELETE FROM file_changes WHERE session_id NOT IN (SELECT id FROM sessions)");
|
|
3510
3635
|
if (!incremental) {
|
|
3511
3636
|
db.exec("DELETE FROM skills WHERE id NOT IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)");
|
|
3512
3637
|
}
|
|
@@ -3621,7 +3746,7 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
|
3621
3746
|
}
|
|
3622
3747
|
let projectId = null;
|
|
3623
3748
|
if (sessionMeta.cwd) {
|
|
3624
|
-
projectId =
|
|
3749
|
+
projectId = createHash4("sha1").update(`${adapter.agentId}\0${sessionMeta.cwd}`).digest("hex").slice(0, 16);
|
|
3625
3750
|
stmts.upsertProject.run({
|
|
3626
3751
|
id: projectId,
|
|
3627
3752
|
agent_id: adapter.agentId,
|
|
@@ -3660,7 +3785,7 @@ import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as
|
|
|
3660
3785
|
import { basename as basename3, join as join6 } from "path";
|
|
3661
3786
|
|
|
3662
3787
|
// ../ingest/dist/redact.js
|
|
3663
|
-
import { createHash as
|
|
3788
|
+
import { createHash as createHash5, randomBytes } from "crypto";
|
|
3664
3789
|
function parseExcludes(csv) {
|
|
3665
3790
|
return (csv ?? "").split(",").map((s) => s.trim()).filter(Boolean).map(encodeProjectPath);
|
|
3666
3791
|
}
|
|
@@ -3669,17 +3794,17 @@ function isExcludedArchivePath(filePath, excluded) {
|
|
|
3669
3794
|
}
|
|
3670
3795
|
|
|
3671
3796
|
// ../ingest/dist/fileread.js
|
|
3672
|
-
import { createHash as
|
|
3797
|
+
import { createHash as createHash6 } from "crypto";
|
|
3673
3798
|
import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
|
|
3674
3799
|
import { StringDecoder } from "string_decoder";
|
|
3675
3800
|
var STREAM_THRESHOLD = 8 * 1024 * 1024;
|
|
3676
3801
|
var CHUNK2 = 64 * 1024;
|
|
3677
3802
|
function sha256(buf) {
|
|
3678
|
-
return
|
|
3803
|
+
return createHash6("sha256").update(buf).digest("hex");
|
|
3679
3804
|
}
|
|
3680
|
-
function sha256File(
|
|
3681
|
-
const fd = openSync2(
|
|
3682
|
-
const h =
|
|
3805
|
+
function sha256File(path3) {
|
|
3806
|
+
const fd = openSync2(path3, "r");
|
|
3807
|
+
const h = createHash6("sha256");
|
|
3683
3808
|
const buf = Buffer.allocUnsafe(CHUNK2);
|
|
3684
3809
|
try {
|
|
3685
3810
|
let n;
|
|
@@ -3690,8 +3815,8 @@ function sha256File(path2) {
|
|
|
3690
3815
|
}
|
|
3691
3816
|
return h.digest("hex");
|
|
3692
3817
|
}
|
|
3693
|
-
function* streamLines(
|
|
3694
|
-
const fd = openSync2(
|
|
3818
|
+
function* streamLines(path3) {
|
|
3819
|
+
const fd = openSync2(path3, "r");
|
|
3695
3820
|
const decoder = new StringDecoder("utf8");
|
|
3696
3821
|
const buf = Buffer.allocUnsafe(CHUNK2);
|
|
3697
3822
|
let leftover = "";
|
|
@@ -3743,8 +3868,8 @@ function discoverSidecars(sourceArchiveDir) {
|
|
|
3743
3868
|
}
|
|
3744
3869
|
return out;
|
|
3745
3870
|
}
|
|
3746
|
-
function sessionFromPath(
|
|
3747
|
-
const parts =
|
|
3871
|
+
function sessionFromPath(path3) {
|
|
3872
|
+
const parts = path3.split("/");
|
|
3748
3873
|
const i = parts.indexOf("workflows");
|
|
3749
3874
|
return i > 0 ? parts[i - 1] : null;
|
|
3750
3875
|
}
|
|
@@ -3791,25 +3916,25 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3791
3916
|
total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
|
|
3792
3917
|
duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
|
|
3793
3918
|
ingested_at=excluded.ingested_at`);
|
|
3794
|
-
for (const
|
|
3919
|
+
for (const path3 of paths) {
|
|
3795
3920
|
stats.files++;
|
|
3796
|
-
const st = statSync2(
|
|
3921
|
+
const st = statSync2(path3);
|
|
3797
3922
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3798
|
-
const prev = full ? void 0 : getState.get(
|
|
3923
|
+
const prev = full ? void 0 : getState.get(path3);
|
|
3799
3924
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3800
3925
|
stats.skipped++;
|
|
3801
3926
|
continue;
|
|
3802
3927
|
}
|
|
3803
3928
|
let buf;
|
|
3804
3929
|
try {
|
|
3805
|
-
buf = readFileSync4(
|
|
3930
|
+
buf = readFileSync4(path3);
|
|
3806
3931
|
} catch {
|
|
3807
3932
|
stats.malformed++;
|
|
3808
3933
|
continue;
|
|
3809
3934
|
}
|
|
3810
3935
|
const hash2 = sha256(buf);
|
|
3811
3936
|
if (prev && prev.sha256 === hash2) {
|
|
3812
|
-
setState.run({ file_path:
|
|
3937
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3813
3938
|
stats.skipped++;
|
|
3814
3939
|
continue;
|
|
3815
3940
|
}
|
|
@@ -3824,14 +3949,14 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3824
3949
|
stats.malformed++;
|
|
3825
3950
|
continue;
|
|
3826
3951
|
}
|
|
3827
|
-
const runId = strOrNull(d.runId) ?? basename3(
|
|
3952
|
+
const runId = strOrNull(d.runId) ?? basename3(path3, ".json");
|
|
3828
3953
|
const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
|
|
3829
3954
|
const durationMs2 = intOrNull(d.durationMs);
|
|
3830
3955
|
const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
|
|
3831
3956
|
upsert.run({
|
|
3832
3957
|
run_id: runId,
|
|
3833
3958
|
source_id: sourceId,
|
|
3834
|
-
session_id: sessionFromPath(
|
|
3959
|
+
session_id: sessionFromPath(path3),
|
|
3835
3960
|
task_id: strOrNull(d.taskId),
|
|
3836
3961
|
workflow_name: strOrNull(d.workflowName),
|
|
3837
3962
|
status: strOrNull(d.status),
|
|
@@ -3849,7 +3974,7 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3849
3974
|
ended_at: endedAt,
|
|
3850
3975
|
ingested_at: now2
|
|
3851
3976
|
});
|
|
3852
|
-
setState.run({ file_path:
|
|
3977
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3853
3978
|
stats.upserted++;
|
|
3854
3979
|
}
|
|
3855
3980
|
}
|
|
@@ -3905,25 +4030,25 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3905
4030
|
source_id=excluded.source_id, agent_type=excluded.agent_type,
|
|
3906
4031
|
agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
|
|
3907
4032
|
tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
|
|
3908
|
-
for (const
|
|
4033
|
+
for (const path3 of paths) {
|
|
3909
4034
|
stats.files++;
|
|
3910
|
-
const st = statSync3(
|
|
4035
|
+
const st = statSync3(path3);
|
|
3911
4036
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3912
|
-
const prev = full ? void 0 : getState.get(
|
|
4037
|
+
const prev = full ? void 0 : getState.get(path3);
|
|
3913
4038
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3914
4039
|
stats.skipped++;
|
|
3915
4040
|
continue;
|
|
3916
4041
|
}
|
|
3917
4042
|
let buf;
|
|
3918
4043
|
try {
|
|
3919
|
-
buf = readFileSync5(
|
|
4044
|
+
buf = readFileSync5(path3);
|
|
3920
4045
|
} catch {
|
|
3921
4046
|
stats.malformed++;
|
|
3922
4047
|
continue;
|
|
3923
4048
|
}
|
|
3924
4049
|
const hash2 = sha256(buf);
|
|
3925
4050
|
if (prev && prev.sha256 === hash2) {
|
|
3926
|
-
setState.run({ file_path:
|
|
4051
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3927
4052
|
stats.skipped++;
|
|
3928
4053
|
continue;
|
|
3929
4054
|
}
|
|
@@ -3938,7 +4063,7 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3938
4063
|
stats.malformed++;
|
|
3939
4064
|
continue;
|
|
3940
4065
|
}
|
|
3941
|
-
const sessionId = basename4(
|
|
4066
|
+
const sessionId = basename4(path3, ".meta.json");
|
|
3942
4067
|
upsert.run({
|
|
3943
4068
|
session_id: sessionId,
|
|
3944
4069
|
source_id: sourceId,
|
|
@@ -3948,7 +4073,7 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3948
4073
|
tool_use_id: strOrNull2(d.toolUseId),
|
|
3949
4074
|
ingested_at: now2
|
|
3950
4075
|
});
|
|
3951
|
-
setState.run({ file_path:
|
|
4076
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3952
4077
|
stats.upserted++;
|
|
3953
4078
|
}
|
|
3954
4079
|
}
|
|
@@ -3985,8 +4110,8 @@ function discoverResults(sourceArchiveDir) {
|
|
|
3985
4110
|
}
|
|
3986
4111
|
return out;
|
|
3987
4112
|
}
|
|
3988
|
-
function sessionFromPath2(
|
|
3989
|
-
const parts =
|
|
4113
|
+
function sessionFromPath2(path3) {
|
|
4114
|
+
const parts = path3.split("/");
|
|
3990
4115
|
const i = parts.indexOf("tool-results");
|
|
3991
4116
|
return i > 0 ? parts[i - 1] : null;
|
|
3992
4117
|
}
|
|
@@ -4003,40 +4128,40 @@ function ingestToolResults(db, sourceArchiveDir, excludedDirs, now2, stats, full
|
|
|
4003
4128
|
VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
|
|
4004
4129
|
ON CONFLICT(session_id, name) DO UPDATE SET
|
|
4005
4130
|
path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`);
|
|
4006
|
-
for (const
|
|
4007
|
-
const sessionId = sessionFromPath2(
|
|
4131
|
+
for (const path3 of paths) {
|
|
4132
|
+
const sessionId = sessionFromPath2(path3);
|
|
4008
4133
|
if (!sessionId)
|
|
4009
4134
|
continue;
|
|
4010
4135
|
stats.files++;
|
|
4011
|
-
const st = statSync4(
|
|
4136
|
+
const st = statSync4(path3);
|
|
4012
4137
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
4013
|
-
const prev = full ? void 0 : getState.get(
|
|
4138
|
+
const prev = full ? void 0 : getState.get(path3);
|
|
4014
4139
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
4015
4140
|
stats.skipped++;
|
|
4016
4141
|
continue;
|
|
4017
4142
|
}
|
|
4018
4143
|
let buf;
|
|
4019
4144
|
try {
|
|
4020
|
-
buf = readFileSync6(
|
|
4145
|
+
buf = readFileSync6(path3);
|
|
4021
4146
|
} catch {
|
|
4022
4147
|
stats.malformed++;
|
|
4023
4148
|
continue;
|
|
4024
4149
|
}
|
|
4025
4150
|
const hash2 = sha256(buf);
|
|
4026
4151
|
if (prev && prev.sha256 === hash2) {
|
|
4027
|
-
setState.run({ file_path:
|
|
4152
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
4028
4153
|
stats.skipped++;
|
|
4029
4154
|
continue;
|
|
4030
4155
|
}
|
|
4031
4156
|
upsert.run({
|
|
4032
4157
|
session_id: sessionId,
|
|
4033
|
-
name: basename5(
|
|
4034
|
-
path:
|
|
4158
|
+
name: basename5(path3, ".txt"),
|
|
4159
|
+
path: path3,
|
|
4035
4160
|
bytes: st.size,
|
|
4036
4161
|
text: buf.toString("utf8"),
|
|
4037
4162
|
ingested_at: now2
|
|
4038
4163
|
});
|
|
4039
|
-
setState.run({ file_path:
|
|
4164
|
+
setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
4040
4165
|
stats.upserted++;
|
|
4041
4166
|
}
|
|
4042
4167
|
}
|
|
@@ -4135,6 +4260,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4135
4260
|
const classified = classify(db, args.full ? null : expanded);
|
|
4136
4261
|
const detected = detect(db, args.full ? null : expanded);
|
|
4137
4262
|
classifyErrors(db, args.full ? null : expanded);
|
|
4263
|
+
const fileChanges = deriveFileChanges(db, args.full ? null : expanded);
|
|
4138
4264
|
const count = (sql) => db.prepare(sql).get().n;
|
|
4139
4265
|
const sessions = count("SELECT COUNT(*) n FROM sessions");
|
|
4140
4266
|
const turns = count("SELECT COUNT(*) n FROM turns");
|
|
@@ -4160,6 +4286,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4160
4286
|
session_meta=${metaRows} (upserted=${metaStats.upserted} skipped=${metaStats.skipped} malformed=${metaStats.malformed})
|
|
4161
4287
|
tool_results=${trRows} (upserted=${trStats.upserted} skipped=${trStats.skipped} malformed=${trStats.malformed})
|
|
4162
4288
|
findings=${detected.count} (${sevLine})
|
|
4289
|
+
file_changes=${fileChanges.count}
|
|
4163
4290
|
tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
|
|
4164
4291
|
}
|
|
4165
4292
|
function runMetrics(argv = process.argv.slice(2)) {
|
|
@@ -4176,8 +4303,9 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
4176
4303
|
const r = classify(db);
|
|
4177
4304
|
const d = detect(db);
|
|
4178
4305
|
const e = classifyErrors(db);
|
|
4306
|
+
const fc = deriveFileChanges(db);
|
|
4179
4307
|
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}`);
|
|
4308
|
+
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
4309
|
}
|
|
4182
4310
|
|
|
4183
4311
|
// ../server/dist/run.js
|
|
@@ -4508,7 +4636,13 @@ function getSession(db, id) {
|
|
|
4508
4636
|
session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
|
|
4509
4637
|
session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
|
|
4510
4638
|
session.tool_failure_count = session.tool_error_count - session.tool_rejection_count;
|
|
4511
|
-
|
|
4639
|
+
let fileChanges = [];
|
|
4640
|
+
if (tableExists(db, "file_changes")) {
|
|
4641
|
+
fileChanges = db.prepare(`SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
|
|
4642
|
+
fc.lines_added, fc.lines_removed, fc.timestamp
|
|
4643
|
+
FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`).all(id);
|
|
4644
|
+
}
|
|
4645
|
+
return { session, turns, events, classification, parent, children, workflow_runs, findings: sessionFindings, file_changes: fileChanges };
|
|
4512
4646
|
}
|
|
4513
4647
|
function xmlTag(text, tag) {
|
|
4514
4648
|
return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
|
|
@@ -4644,6 +4778,107 @@ function listSkills(db, f = {}) {
|
|
|
4644
4778
|
r.sources = r.sources ? String(r.sources).split(",").filter(Boolean) : [];
|
|
4645
4779
|
return rows;
|
|
4646
4780
|
}
|
|
4781
|
+
function listFiles(db, f = {}) {
|
|
4782
|
+
if (!tableExists(db, "file_changes"))
|
|
4783
|
+
return { total: 0, files: [] };
|
|
4784
|
+
const where = [];
|
|
4785
|
+
const params = [];
|
|
4786
|
+
if (f.q && f.q.trim())
|
|
4787
|
+
where.push("fc.file_path LIKE ?"), params.push(`%${f.q.trim()}%`);
|
|
4788
|
+
if (f.project)
|
|
4789
|
+
where.push("fc.project_id = ?"), params.push(f.project);
|
|
4790
|
+
if (f.source)
|
|
4791
|
+
where.push("EXISTS (SELECT 1 FROM sessions s WHERE s.id = fc.session_id AND s.source_id = ?)"), params.push(f.source);
|
|
4792
|
+
const whereSql = where.length ? ` WHERE ${where.join(" AND ")}` : "";
|
|
4793
|
+
const grouped = `SELECT fc.project_id, fc.file_path,
|
|
4794
|
+
COUNT(DISTINCT fc.session_id) AS sessions,
|
|
4795
|
+
COUNT(*) AS changes,
|
|
4796
|
+
SUM(fc.lines_added) AS lines_added,
|
|
4797
|
+
SUM(fc.lines_removed) AS lines_removed,
|
|
4798
|
+
MIN(fc.timestamp) AS first_ts,
|
|
4799
|
+
MAX(fc.timestamp) AS last_ts
|
|
4800
|
+
FROM file_changes fc${whereSql}
|
|
4801
|
+
GROUP BY fc.project_id, fc.file_path`;
|
|
4802
|
+
const total = db.prepare(`SELECT COUNT(*) n FROM (${grouped})`).get(...params).n;
|
|
4803
|
+
const ORDER = {
|
|
4804
|
+
last_ts: "last_ts",
|
|
4805
|
+
first_ts: "first_ts",
|
|
4806
|
+
changes: "changes",
|
|
4807
|
+
sessions: "sessions",
|
|
4808
|
+
path: "file_path"
|
|
4809
|
+
};
|
|
4810
|
+
const col = ORDER[f.sort ?? "last_ts"] ?? "last_ts";
|
|
4811
|
+
const dir = f.dir === "asc" ? "ASC" : "DESC";
|
|
4812
|
+
const limit = Math.min(f.limit ?? 50, 200);
|
|
4813
|
+
const offset = f.offset ?? 0;
|
|
4814
|
+
const files = db.prepare(`SELECT g.*, p.path AS project_path FROM (${grouped}) g
|
|
4815
|
+
LEFT JOIN projects p ON p.id = g.project_id
|
|
4816
|
+
ORDER BY ${col} ${dir}, g.file_path ASC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
4817
|
+
return { total, files };
|
|
4818
|
+
}
|
|
4819
|
+
function getFileTimeline(db, filePath, projectId) {
|
|
4820
|
+
if (!tableExists(db, "file_changes"))
|
|
4821
|
+
return null;
|
|
4822
|
+
const params = [filePath];
|
|
4823
|
+
let scope = "fc.file_path = ?";
|
|
4824
|
+
if (projectId)
|
|
4825
|
+
scope += " AND fc.project_id = ?", params.push(projectId);
|
|
4826
|
+
const rows = db.prepare(`SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,
|
|
4827
|
+
fc.lines_removed, fc.timestamp, fc.project_id,
|
|
4828
|
+
s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,
|
|
4829
|
+
p.path AS project_path
|
|
4830
|
+
FROM file_changes fc
|
|
4831
|
+
JOIN sessions s ON s.id = fc.session_id
|
|
4832
|
+
LEFT JOIN turns t ON t.id = fc.turn_id
|
|
4833
|
+
LEFT JOIN projects p ON p.id = fc.project_id
|
|
4834
|
+
WHERE ${scope} ORDER BY fc.timestamp`).all(...params);
|
|
4835
|
+
if (!rows.length)
|
|
4836
|
+
return null;
|
|
4837
|
+
const cat = tableExists(db, "classifications") ? db.prepare("SELECT category FROM classifications WHERE scope = 'session' AND target_id = ?") : null;
|
|
4838
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
4839
|
+
for (const r of rows) {
|
|
4840
|
+
let g = bySession.get(r.session_id);
|
|
4841
|
+
if (!g) {
|
|
4842
|
+
g = {
|
|
4843
|
+
session_id: r.session_id,
|
|
4844
|
+
title: r.ai_title || r.slug || null,
|
|
4845
|
+
source_id: r.source_id,
|
|
4846
|
+
started_at: r.started_at,
|
|
4847
|
+
category: cat ? cat.get(r.session_id)?.category ?? null : null,
|
|
4848
|
+
changes: []
|
|
4849
|
+
};
|
|
4850
|
+
bySession.set(r.session_id, g);
|
|
4851
|
+
}
|
|
4852
|
+
g.changes.push({
|
|
4853
|
+
id: r.id,
|
|
4854
|
+
turn_id: r.turn_id,
|
|
4855
|
+
turn_seq: r.turn_seq,
|
|
4856
|
+
prompt_preview: r.prompt_preview,
|
|
4857
|
+
event_uuid: r.event_uuid,
|
|
4858
|
+
tool_name: r.tool_name,
|
|
4859
|
+
lines_added: r.lines_added,
|
|
4860
|
+
lines_removed: r.lines_removed,
|
|
4861
|
+
timestamp: r.timestamp
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
const sessions = [...bySession.values()].sort((a, b) => {
|
|
4865
|
+
const la = a.changes[a.changes.length - 1].timestamp ?? "";
|
|
4866
|
+
const lb = b.changes[b.changes.length - 1].timestamp ?? "";
|
|
4867
|
+
return la < lb ? 1 : la > lb ? -1 : 0;
|
|
4868
|
+
});
|
|
4869
|
+
return {
|
|
4870
|
+
file_path: filePath,
|
|
4871
|
+
project_id: rows[0].project_id ?? null,
|
|
4872
|
+
project_path: rows[0].project_path ?? null,
|
|
4873
|
+
sessions_count: sessions.length,
|
|
4874
|
+
changes_count: rows.length,
|
|
4875
|
+
lines_added: rows.reduce((a, r) => a + (r.lines_added ?? 0), 0),
|
|
4876
|
+
lines_removed: rows.reduce((a, r) => a + (r.lines_removed ?? 0), 0),
|
|
4877
|
+
first_ts: rows[0].timestamp ?? null,
|
|
4878
|
+
last_ts: rows[rows.length - 1].timestamp ?? null,
|
|
4879
|
+
sessions
|
|
4880
|
+
};
|
|
4881
|
+
}
|
|
4647
4882
|
function getSkill(db, name) {
|
|
4648
4883
|
const versions = db.prepare(`SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,
|
|
4649
4884
|
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count
|
|
@@ -5360,6 +5595,28 @@ async function createApp(db, opts = {}) {
|
|
|
5360
5595
|
return reply.code(404).send({ error: "not found" });
|
|
5361
5596
|
return result;
|
|
5362
5597
|
});
|
|
5598
|
+
app.get("/api/files", async (req) => {
|
|
5599
|
+
const q = req.query;
|
|
5600
|
+
const sort = ["last_ts", "first_ts", "changes", "sessions", "path"].find((s) => s === q.sort);
|
|
5601
|
+
return listFiles(db, {
|
|
5602
|
+
q: q.q,
|
|
5603
|
+
source: q.source,
|
|
5604
|
+
project: q.project,
|
|
5605
|
+
sort,
|
|
5606
|
+
dir: q.dir === "asc" ? "asc" : "desc",
|
|
5607
|
+
limit: Math.min(Number(q.limit) || 50, 200),
|
|
5608
|
+
offset: Number(q.offset) || 0
|
|
5609
|
+
});
|
|
5610
|
+
});
|
|
5611
|
+
app.get("/api/file", async (req, reply) => {
|
|
5612
|
+
const q = req.query;
|
|
5613
|
+
if (!q.path)
|
|
5614
|
+
return reply.code(400).send({ error: "missing path" });
|
|
5615
|
+
const result = getFileTimeline(db, q.path, q.project || void 0);
|
|
5616
|
+
if (!result)
|
|
5617
|
+
return reply.code(404).send({ error: "not found" });
|
|
5618
|
+
return result;
|
|
5619
|
+
});
|
|
5363
5620
|
app.get("/api/sessions/:id/export.md", async (req, reply) => {
|
|
5364
5621
|
const { id } = req.params;
|
|
5365
5622
|
const level = parseRedactionLevel(req.query.redact);
|