@swestash/agent-lens 0.15.0 → 0.16.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.
@@ -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);
@@ -6491,8 +6590,8 @@ cli.command("export <sessionId>", "Write a session to a shareable Markdown file
6491
6590
  cli.command(
6492
6591
  "service <action> [target]",
6493
6592
  "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);
6593
+ ).option("--times <hours>", "Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)").action((action, target2, opts) => {
6594
+ const targets = parseTargets(target2);
6496
6595
  const hours = action === "install" ? parseHours(opts.times) : void 0;
6497
6596
  runService(action, { cliEntry: fileURLToPath4(import.meta.url), hours, targets });
6498
6597
  });