@swestash/agent-lens 0.15.0 → 0.17.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { existsSync as existsSync10, writeFileSync as writeFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync11, writeFileSync as writeFileSync3 } from "node:fs";
5
5
  import { join as join12 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7
7
 
@@ -812,9 +812,9 @@ CREATE INDEX IF NOT EXISTS idx_findings_category ON findings(category);
812
812
  CREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule_id);
813
813
  CREATE INDEX IF NOT EXISTS idx_findings_tool_call ON findings(tool_call_id);
814
814
 
815
- -- File modifications derived from tool calls (ADR-022). One row per (tool_call, file): today
816
- -- Edit/Write/NotebookEdit are 1:1, but the hash id leaves room for multi-file sources (e.g. a
817
- -- future Bash-write heuristic) without a schema break. Failed calls (status='error') are excluded
815
+ -- File modifications derived from tool calls (ADR-022). One row per (tool_call, file):
816
+ -- Edit/Write/NotebookEdit are 1:1; a Bash call can write several files, which the hash id already
817
+ -- accommodated without a schema break. Failed calls (status='error') are excluded
818
818
  -- at derivation \u2014 they didn't change the file. Re-runnable like findings: incremental runs DELETE
819
819
  -- the dirty sessions' rows and re-INSERT (delete-then-insert). Populated by ingest's
820
820
  -- deriveFileChanges (filechanges.ts).
@@ -826,7 +826,7 @@ CREATE TABLE IF NOT EXISTS file_changes (
826
826
  event_uuid TEXT, -- jump-to-transcript anchor (#ev-<uuid>)
827
827
  project_id TEXT REFERENCES projects(id), -- from the session, for per-project grouping
828
828
  file_path TEXT NOT NULL, -- normalized absolute path
829
- tool_name TEXT NOT NULL, -- Edit | Write | NotebookEdit
829
+ tool_name TEXT NOT NULL, -- Edit | Write | NotebookEdit | Bash
830
830
  lines_added INTEGER, -- newline-count delta (NULL when unknowable)
831
831
  lines_removed INTEGER,
832
832
  timestamp TEXT, -- ISO8601, from the tool call's event
@@ -3427,8 +3427,98 @@ function classifyErrors(db, dirty) {
3427
3427
  // ../ingest/dist/filechanges.js
3428
3428
  import { createHash as createHash3 } from "node:crypto";
3429
3429
  import { posix as path2 } from "node:path";
3430
- var FILECHANGES_VERSION = 1;
3431
- var FILE_TOOLS = ["Edit", "Write", "NotebookEdit"];
3430
+
3431
+ // ../ingest/dist/shellwrites.js
3432
+ var TARGET = `"([^"]+)"|'([^']+)'|([^\\s;|&<>()]+)`;
3433
+ var REDIRECT = new RegExp(`(?:^|[\\s;|&(])\\d?>{1,2}\\s*(?:${TARGET})`, "g");
3434
+ var TEE = new RegExp(`(?:^|[\\s;|&(])tee\\s+(?:-a\\s+)?(?:${TARGET})`, "g");
3435
+ var SED_I = new RegExp(`(?:^|[\\s;|&(])sed\\s+(?:-[a-zA-Z]*i[a-zA-Z]*(?:\\.\\w+)?)\\s+(?:(?:-e\\s+)?(?:'[^']*'|"[^"]*"|\\S+)\\s+)+?(?:${TARGET})`, "g");
3436
+ var CD = new RegExp(`^\\s*cd\\s+(?:${TARGET})`);
3437
+ var HEREDOC = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/g;
3438
+ var REDIRECT_BEFORE_HEREDOC = new RegExp(`\\d?>{1,2}\\s*(?:${TARGET})\\s*$`);
3439
+ function target(m, first) {
3440
+ return m[first] ?? m[first + 1] ?? m[first + 2] ?? null;
3441
+ }
3442
+ function usable(path3) {
3443
+ if (!path3)
3444
+ return false;
3445
+ if (/[$*?`~]/.test(path3))
3446
+ return false;
3447
+ if (path3.startsWith("&"))
3448
+ return false;
3449
+ if (path3 === "/dev/null" || path3.startsWith("/dev/"))
3450
+ return false;
3451
+ return true;
3452
+ }
3453
+ function bodyLines(body) {
3454
+ const trimmed = body.replace(/\n$/, "");
3455
+ return trimmed === "" ? 0 : trimmed.split("\n").length;
3456
+ }
3457
+ function stripHeredocs(command) {
3458
+ const writes = [];
3459
+ let scan = command;
3460
+ HEREDOC.lastIndex = 0;
3461
+ let m;
3462
+ while (m = HEREDOC.exec(scan)) {
3463
+ const delim = m[2];
3464
+ const openerEnd = scan.indexOf("\n", m.index);
3465
+ if (openerEnd === -1)
3466
+ break;
3467
+ const terminator = new RegExp(`^[ \\t]*${delim}[ \\t]*$`, "m");
3468
+ const rest = scan.slice(openerEnd + 1);
3469
+ const end = rest.search(terminator);
3470
+ const bodyEnd = end === -1 ? rest.length : end;
3471
+ const before = scan.slice(0, m.index);
3472
+ const red = before.match(REDIRECT_BEFORE_HEREDOC);
3473
+ if (red) {
3474
+ const p = target(red, 1);
3475
+ if (usable(p))
3476
+ writes.push({ at: red.index ?? m.index, write: { path: p, linesAdded: bodyLines(rest.slice(0, bodyEnd)) } });
3477
+ }
3478
+ const stripEnd = end === -1 ? scan.length : openerEnd + 1 + bodyEnd + delim.length;
3479
+ scan = scan.slice(0, m.index) + " ".repeat(stripEnd - m.index) + scan.slice(stripEnd);
3480
+ HEREDOC.lastIndex = stripEnd;
3481
+ }
3482
+ return { scan, writes };
3483
+ }
3484
+ function parseShellWrites(command) {
3485
+ if (typeof command !== "string" || !command)
3486
+ return { cwd: null, writes: [] };
3487
+ const cdMatch = command.match(CD);
3488
+ const cdPath = cdMatch ? target(cdMatch, 1) : null;
3489
+ const cwd = cdPath && cdPath.startsWith("/") && !/[$*?`~]/.test(cdPath) ? cdPath : null;
3490
+ const { scan, writes: found } = stripHeredocs(command);
3491
+ for (const [re, group] of [
3492
+ [REDIRECT, 1],
3493
+ [TEE, 1],
3494
+ [SED_I, 1]
3495
+ ]) {
3496
+ re.lastIndex = 0;
3497
+ let m;
3498
+ while (m = re.exec(scan)) {
3499
+ const p = target(m, group);
3500
+ if (usable(p))
3501
+ found.push({ at: m.index, write: { path: p, linesAdded: null } });
3502
+ }
3503
+ }
3504
+ const byPath = /* @__PURE__ */ new Map();
3505
+ for (const entry of found) {
3506
+ const prev = byPath.get(entry.write.path);
3507
+ if (!prev) {
3508
+ byPath.set(entry.write.path, entry);
3509
+ continue;
3510
+ }
3511
+ byPath.set(entry.write.path, {
3512
+ at: Math.min(prev.at, entry.at),
3513
+ write: { path: entry.write.path, linesAdded: prev.write.linesAdded ?? entry.write.linesAdded }
3514
+ });
3515
+ }
3516
+ return { cwd, writes: [...byPath.values()].sort((a, b) => a.at - b.at).map((e) => e.write) };
3517
+ }
3518
+
3519
+ // ../ingest/dist/filechanges.js
3520
+ var FILECHANGES_VERSION = 2;
3521
+ var FILE_TOOLS = ["Edit", "Write", "NotebookEdit", "Bash"];
3432
3522
  function fileChangeId(toolCallId, filePath) {
3433
3523
  return createHash3("sha1").update(toolCallId).update("\0").update(filePath).digest("hex").slice(0, 16);
3434
3524
  }
@@ -3489,31 +3579,40 @@ function deriveFileChanges(db, dirty) {
3489
3579
  if (!input)
3490
3580
  continue;
3491
3581
  const p = proj.get(row.session_id);
3492
- const filePath = normalizeFilePath(input.file_path ?? input.notebook_path, p?.path ?? null);
3493
- if (!filePath)
3494
- continue;
3495
- let added = null;
3496
- let removed = null;
3497
- if (row.tool_name === "Edit") {
3498
- added = lineCount(input.new_string);
3499
- removed = lineCount(input.old_string);
3500
- } else if (row.tool_name === "Write") {
3501
- added = lineCount(input.content);
3582
+ const targets = [];
3583
+ if (row.tool_name === "Bash") {
3584
+ const { cwd, writes } = parseShellWrites(input.command);
3585
+ for (const w of writes) {
3586
+ const filePath = normalizeFilePath(w.path, cwd ?? p?.path ?? null);
3587
+ if (filePath)
3588
+ targets.push({ filePath, added: w.linesAdded, removed: null });
3589
+ }
3590
+ } else {
3591
+ const filePath = normalizeFilePath(input.file_path ?? input.notebook_path, p?.path ?? null);
3592
+ if (filePath) {
3593
+ targets.push({
3594
+ filePath,
3595
+ added: row.tool_name === "Edit" ? lineCount(input.new_string) : row.tool_name === "Write" ? lineCount(input.content) : null,
3596
+ removed: row.tool_name === "Edit" ? lineCount(input.old_string) : null
3597
+ });
3598
+ }
3599
+ }
3600
+ for (const t of targets) {
3601
+ insert.run({
3602
+ id: fileChangeId(row.id, t.filePath),
3603
+ tool_call_id: row.id,
3604
+ session_id: row.session_id,
3605
+ turn_id: row.turn_id,
3606
+ event_uuid: row.event_uuid,
3607
+ project_id: p?.id ?? null,
3608
+ file_path: t.filePath,
3609
+ tool_name: row.tool_name,
3610
+ lines_added: t.added,
3611
+ lines_removed: t.removed,
3612
+ timestamp: row.timestamp,
3613
+ derive_version: FILECHANGES_VERSION
3614
+ });
3502
3615
  }
3503
- insert.run({
3504
- id: fileChangeId(row.id, filePath),
3505
- tool_call_id: row.id,
3506
- session_id: row.session_id,
3507
- turn_id: row.turn_id,
3508
- event_uuid: row.event_uuid,
3509
- project_id: p?.id ?? null,
3510
- file_path: filePath,
3511
- tool_name: row.tool_name,
3512
- lines_added: added,
3513
- lines_removed: removed,
3514
- timestamp: row.timestamp,
3515
- derive_version: FILECHANGES_VERSION
3516
- });
3517
3616
  }
3518
3617
  });
3519
3618
  tx();
@@ -3585,15 +3684,15 @@ function canonicalizeProjects(db, opts = {}) {
3585
3684
  const touched = /* @__PURE__ */ new Set();
3586
3685
  transaction(db, () => {
3587
3686
  for (const r of rows) {
3588
- const target = canon(r.path);
3589
- if (target === strip(r.path))
3687
+ const target2 = canon(r.path);
3688
+ if (target2 === strip(r.path))
3590
3689
  continue;
3591
- const targetId = byPath.get(target)?.id ?? projectId(r.agent_id, target);
3690
+ const targetId = byPath.get(target2)?.id ?? projectId(r.agent_id, target2);
3592
3691
  if (targetId === r.id)
3593
3692
  continue;
3594
- if (!byPath.has(target)) {
3595
- upsertProject.run(targetId, r.agent_id, target);
3596
- byPath.set(target, { id: targetId, agent_id: r.agent_id });
3693
+ if (!byPath.has(target2)) {
3694
+ upsertProject.run(targetId, r.agent_id, target2);
3695
+ byPath.set(target2, { id: targetId, agent_id: r.agent_id });
3597
3696
  }
3598
3697
  repointSessions.run(targetId, r.id);
3599
3698
  repointFileChanges.run(targetId, r.id);
@@ -4544,11 +4643,12 @@ function runMetrics(argv = process.argv.slice(2)) {
4544
4643
  }
4545
4644
 
4546
4645
  // ../server/dist/run.js
4547
- import { existsSync as existsSync8 } from "node:fs";
4646
+ import { existsSync as existsSync9 } from "node:fs";
4548
4647
  import { dirname as dirname7 } from "node:path";
4549
4648
  import { fileURLToPath as fileURLToPath3 } from "node:url";
4550
4649
 
4551
4650
  // ../server/dist/db.js
4651
+ import { existsSync as existsSync6 } from "node:fs";
4552
4652
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
4553
4653
 
4554
4654
  // ../server/dist/sql-util.js
@@ -5006,6 +5106,17 @@ function getSession(db, id) {
5006
5106
  file_changes: loadFileChanges(db, id)
5007
5107
  };
5008
5108
  }
5109
+ function sessionLocation(db, id) {
5110
+ const session = queryGet(db, "SELECT source_id FROM sessions WHERE id = ?", id);
5111
+ if (!session)
5112
+ return null;
5113
+ const files = queryAll(db, "SELECT DISTINCT source_file FROM events WHERE session_id = ? AND source_file IS NOT NULL", id).map((r) => r.source_file);
5114
+ if (files.length === 0)
5115
+ return null;
5116
+ const live = files.filter((f) => !f.includes("/.versions/"));
5117
+ const path3 = live.find((f) => f.endsWith(`/${id}.jsonl`)) ?? live[0] ?? files[0];
5118
+ return { session_id: id, path: path3, exists: existsSync6(path3), source_id: session.source_id };
5119
+ }
5009
5120
  function safeJson(s) {
5010
5121
  if (!s)
5011
5122
  return null;
@@ -5369,7 +5480,7 @@ function securitySummary(db) {
5369
5480
  }
5370
5481
 
5371
5482
  // ../server/dist/app.js
5372
- import { existsSync as existsSync7 } from "node:fs";
5483
+ import { existsSync as existsSync8 } from "node:fs";
5373
5484
  import Fastify from "fastify";
5374
5485
  import fastifyStatic from "@fastify/static";
5375
5486
 
@@ -5857,7 +5968,7 @@ function findingsOverTime(db, w, bucket) {
5857
5968
  }
5858
5969
 
5859
5970
  // ../server/dist/refresh.js
5860
- import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
5971
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "node:fs";
5861
5972
  import { join as join10 } from "node:path";
5862
5973
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
5863
5974
  function originAllowed(origin) {
@@ -5885,7 +5996,7 @@ function runRefresh() {
5885
5996
  try {
5886
5997
  const collected = collectAll();
5887
5998
  const archiveRoot = resolveArchiveDir();
5888
- if (!existsSync6(archiveRoot))
5999
+ if (!existsSync7(archiveRoot))
5889
6000
  mkdirSync5(archiveRoot, { recursive: true });
5890
6001
  runIngest([]);
5891
6002
  return { ok: true, collected };
@@ -6187,6 +6298,13 @@ async function createApp(db, opts = {}) {
6187
6298
  return notFound(reply);
6188
6299
  return result;
6189
6300
  });
6301
+ app.get("/api/sessions/:id/location", async (req, reply) => {
6302
+ const { id } = req.params;
6303
+ const result = sessionLocation(db, id);
6304
+ if (!result)
6305
+ return notFound(reply, "no archive path recorded for this session");
6306
+ return result;
6307
+ });
6190
6308
  app.get("/api/workflows/:run_id", async (req, reply) => {
6191
6309
  const { run_id } = req.params;
6192
6310
  const result = getWorkflow(db, run_id);
@@ -6306,7 +6424,7 @@ async function createApp(db, opts = {}) {
6306
6424
  return notFound(reply);
6307
6425
  reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="${out.filename}"`).send(out.markdown);
6308
6426
  });
6309
- if (opts.webDist && existsSync7(opts.webDist)) {
6427
+ if (opts.webDist && existsSync8(opts.webDist)) {
6310
6428
  await app.register(fastifyStatic, { root: opts.webDist });
6311
6429
  app.setNotFoundHandler((req, reply) => {
6312
6430
  if (req.url.startsWith("/api/"))
@@ -6331,7 +6449,7 @@ async function startServer(overrides = {}) {
6331
6449
  console.error(`agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`);
6332
6450
  process.exit(1);
6333
6451
  }
6334
- if (!existsSync8(dbPath)) {
6452
+ if (!existsSync9(dbPath)) {
6335
6453
  console.error(`agent-lens-server: db not found: ${dbPath} (run ingest first)`);
6336
6454
  process.exit(1);
6337
6455
  }
@@ -6347,7 +6465,7 @@ async function startServer(overrides = {}) {
6347
6465
  try {
6348
6466
  await app.listen({ host, port });
6349
6467
  console.log(`agent-lens-server: http://${host}:${port} (db: ${dbPath})`);
6350
- if (!existsSync8(webDist))
6468
+ if (!existsSync9(webDist))
6351
6469
  console.log(" note: web SPA not built \u2014 run `pnpm --filter @agent-lens/web build`");
6352
6470
  } catch (err) {
6353
6471
  console.error(err);
@@ -6356,7 +6474,7 @@ async function startServer(overrides = {}) {
6356
6474
  }
6357
6475
 
6358
6476
  // src/watch.ts
6359
- import { existsSync as existsSync9 } from "node:fs";
6477
+ import { existsSync as existsSync10 } from "node:fs";
6360
6478
  import { join as join11 } from "node:path";
6361
6479
  import chokidar from "chokidar";
6362
6480
  async function runWatch(opts = {}) {
@@ -6394,7 +6512,7 @@ async function runWatch(opts = {}) {
6394
6512
  }
6395
6513
  };
6396
6514
  cycle();
6397
- const watchPaths = sources.map((s) => join11(s.configDir, "projects")).filter((p) => existsSync9(p));
6515
+ const watchPaths = sources.map((s) => join11(s.configDir, "projects")).filter((p) => existsSync10(p));
6398
6516
  if (watchPaths.length === 0) {
6399
6517
  console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
6400
6518
  }
@@ -6471,7 +6589,7 @@ cli.command("metrics", "Re-run classification over an already-ingested DB").opti
6471
6589
  });
6472
6590
  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) => {
6473
6591
  const { path: dbPath } = resolveDbPath(opts.db);
6474
- if (!existsSync10(dbPath)) {
6592
+ if (!existsSync11(dbPath)) {
6475
6593
  console.error(`agent-lens: db not found: ${dbPath} (run ingest first)`);
6476
6594
  process.exit(1);
6477
6595
  }
@@ -6491,8 +6609,8 @@ cli.command("export <sessionId>", "Write a session to a shareable Markdown file
6491
6609
  cli.command(
6492
6610
  "service <action> [target]",
6493
6611
  "Install/uninstall/status OS services \u2014 action: install|uninstall|status; target: collector|server|all (default all)"
6494
- ).option("--times <hours>", "Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)").action((action, target, opts) => {
6495
- const targets = parseTargets(target);
6612
+ ).option("--times <hours>", "Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)").action((action, target2, opts) => {
6613
+ const targets = parseTargets(target2);
6496
6614
  const hours = action === "install" ? parseHours(opts.times) : void 0;
6497
6615
  runService(action, { cliEntry: fileURLToPath4(import.meta.url), hours, targets });
6498
6616
  });