sticky-note-cli 3.1.13 → 3.1.15

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.
Files changed (82) hide show
  1. package/README.md +77 -0
  2. package/bin/cli.js +68 -0
  3. package/bin/mcp-server.js +51 -9
  4. package/package.json +1 -1
  5. package/sticky-server/.wrangler/cache/cf.json +1 -0
  6. package/sticky-server/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  7. package/sticky-server/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  8. package/sticky-server/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  9. package/sticky-server/.wrangler/state/v3/do/sticky-note-server-StickyThread/metadata.sqlite +0 -0
  10. package/sticky-server/.wrangler/state/v3/do/sticky-note-server-StickyThread/metadata.sqlite-shm +0 -0
  11. package/sticky-server/.wrangler/state/v3/do/sticky-note-server-StickyThread/metadata.sqlite-wal +0 -0
  12. package/sticky-server/.wrangler/state/v3/kv/PLACEHOLDER_KV_NAMESPACE_ID/blobs/24473f36cce8fd4dcc0287333b336236fef2ffd176b20d9f9861257480f2d2f0000001a0272e56bb +1 -0
  13. package/sticky-server/.wrangler/state/v3/kv/PLACEHOLDER_KV_NAMESPACE_ID/blobs/896e937930a2be286388f1ae1003af2a41171bcdb97993b90e66a5836c4bce88000001a0272e57be +1 -0
  14. package/sticky-server/.wrangler/state/v3/kv/PLACEHOLDER_KV_NAMESPACE_ID/blobs/cb1dad011015782163f59d1e79653f10a354fc026eea8e482fe7417f4c4d7426000001a0272e57b2 +1 -0
  15. package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/d561fc7d90f13b8d408360f0c2d3dad0579c60d59ea97622cecfbe29274283cd.sqlite +0 -0
  16. package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/metadata.sqlite +0 -0
  17. package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/metadata.sqlite-shm +0 -0
  18. package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/metadata.sqlite-wal +0 -0
  19. package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite +0 -0
  20. package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite-shm +0 -0
  21. package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite-wal +0 -0
  22. package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/metadata.sqlite +0 -0
  23. package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/metadata.sqlite-shm +0 -0
  24. package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/metadata.sqlite-wal +0 -0
  25. package/sticky-server/.wrangler/tmp/bundle-BhcBKo/checked-fetch.js +28 -0
  26. package/sticky-server/.wrangler/tmp/bundle-BhcBKo/middleware-insertion-facade.js +11 -0
  27. package/sticky-server/.wrangler/tmp/bundle-BhcBKo/middleware-loader.entry.ts +134 -0
  28. package/sticky-server/.wrangler/tmp/bundle-EyT3Z4/checked-fetch.js +28 -0
  29. package/sticky-server/.wrangler/tmp/bundle-EyT3Z4/middleware-insertion-facade.js +11 -0
  30. package/sticky-server/.wrangler/tmp/bundle-EyT3Z4/middleware-loader.entry.ts +134 -0
  31. package/sticky-server/.wrangler/tmp/bundle-Fenl4S/checked-fetch.js +28 -0
  32. package/sticky-server/.wrangler/tmp/bundle-Fenl4S/middleware-insertion-facade.js +11 -0
  33. package/sticky-server/.wrangler/tmp/bundle-Fenl4S/middleware-loader.entry.ts +134 -0
  34. package/sticky-server/.wrangler/tmp/bundle-GEjBtr/checked-fetch.js +28 -0
  35. package/sticky-server/.wrangler/tmp/bundle-GEjBtr/middleware-insertion-facade.js +11 -0
  36. package/sticky-server/.wrangler/tmp/bundle-GEjBtr/middleware-loader.entry.ts +134 -0
  37. package/sticky-server/.wrangler/tmp/bundle-QVovzR/checked-fetch.js +28 -0
  38. package/sticky-server/.wrangler/tmp/bundle-QVovzR/middleware-insertion-facade.js +11 -0
  39. package/sticky-server/.wrangler/tmp/bundle-QVovzR/middleware-loader.entry.ts +134 -0
  40. package/sticky-server/.wrangler/tmp/bundle-cuweP4/checked-fetch.js +28 -0
  41. package/sticky-server/.wrangler/tmp/bundle-cuweP4/middleware-insertion-facade.js +11 -0
  42. package/sticky-server/.wrangler/tmp/bundle-cuweP4/middleware-loader.entry.ts +134 -0
  43. package/sticky-server/.wrangler/tmp/bundle-jSM6YE/checked-fetch.js +28 -0
  44. package/sticky-server/.wrangler/tmp/bundle-jSM6YE/middleware-insertion-facade.js +11 -0
  45. package/sticky-server/.wrangler/tmp/bundle-jSM6YE/middleware-loader.entry.ts +134 -0
  46. package/sticky-server/.wrangler/tmp/bundle-skMxAR/checked-fetch.js +28 -0
  47. package/sticky-server/.wrangler/tmp/bundle-skMxAR/middleware-insertion-facade.js +11 -0
  48. package/sticky-server/.wrangler/tmp/bundle-skMxAR/middleware-loader.entry.ts +134 -0
  49. package/sticky-server/.wrangler/tmp/bundle-ufHiFB/checked-fetch.js +28 -0
  50. package/sticky-server/.wrangler/tmp/bundle-ufHiFB/middleware-insertion-facade.js +11 -0
  51. package/sticky-server/.wrangler/tmp/bundle-ufHiFB/middleware-loader.entry.ts +134 -0
  52. package/sticky-server/.wrangler/tmp/dev-7URP42/worker.js +1006 -0
  53. package/sticky-server/.wrangler/tmp/dev-7URP42/worker.js.map +8 -0
  54. package/sticky-server/.wrangler/tmp/dev-J7vWKa/worker.js +1006 -0
  55. package/sticky-server/.wrangler/tmp/dev-J7vWKa/worker.js.map +8 -0
  56. package/sticky-server/.wrangler/tmp/dev-QZJJpV/worker.js +579 -0
  57. package/sticky-server/.wrangler/tmp/dev-QZJJpV/worker.js.map +8 -0
  58. package/sticky-server/.wrangler/tmp/dev-ZhpGzw/worker.js +1006 -0
  59. package/sticky-server/.wrangler/tmp/dev-ZhpGzw/worker.js.map +8 -0
  60. package/sticky-server/.wrangler/tmp/dev-Zj4Um8/worker.js +1006 -0
  61. package/sticky-server/.wrangler/tmp/dev-Zj4Um8/worker.js.map +8 -0
  62. package/sticky-server/.wrangler/tmp/dev-cV8SnT/worker.js +1006 -0
  63. package/sticky-server/.wrangler/tmp/dev-cV8SnT/worker.js.map +8 -0
  64. package/sticky-server/.wrangler/tmp/dev-dVJlR1/worker.js +1006 -0
  65. package/sticky-server/.wrangler/tmp/dev-dVJlR1/worker.js.map +8 -0
  66. package/sticky-server/.wrangler/tmp/dev-gZFGI3/worker.js +1006 -0
  67. package/sticky-server/.wrangler/tmp/dev-gZFGI3/worker.js.map +8 -0
  68. package/sticky-server/.wrangler/tmp/dev-rZTsQz/worker.js +1006 -0
  69. package/sticky-server/.wrangler/tmp/dev-rZTsQz/worker.js.map +8 -0
  70. package/sticky-server/adapters/cf-kv.js +60 -0
  71. package/sticky-server/mcp-tools.js +249 -0
  72. package/sticky-server/sticky-thread-do.js +134 -0
  73. package/sticky-server/worker.js +122 -1
  74. package/sticky-server/wrangler.toml +17 -0
  75. package/templates/gitignore-additions.txt +9 -0
  76. package/templates/hooks/event-writer.js +104 -0
  77. package/templates/hooks/inject-context.js +17 -12
  78. package/templates/hooks/on-error.js +46 -2
  79. package/templates/hooks/session-end.js +187 -22
  80. package/templates/hooks/session-start.js +27 -0
  81. package/templates/hooks/sticky-utils.js +12 -3
  82. package/templates/hooks/track-work.js +54 -14
@@ -76,9 +76,14 @@ const {
76
76
  syncStickyNote,
77
77
  } = utils;
78
78
 
79
+ // ── AI event writer ───────────────────────────────────────
80
+
81
+ let eventWriter = null;
82
+ try { eventWriter = require("./event-writer.js"); } catch (_) {}
83
+
79
84
  // ── Constants ─────────────────────────────────────────────
80
85
 
81
- const WRITE_TOOLS = new Set([
86
+ const WRITE_TOOLS = (eventWriter && eventWriter.WRITE_TOOLS) || new Set([
82
87
  "Write", "Edit", "MultiEdit",
83
88
  "write", "edit", "multi_edit",
84
89
  ]);
@@ -378,17 +383,140 @@ function extractFailedApproaches(hookInput) {
378
383
  return extractFailedFromEntries(entries);
379
384
  }
380
385
 
386
+ // ── AI event extraction for AI blame ─────────────────────
387
+
388
+ /**
389
+ * Parse Claude Code's transcript JSONL to extract ai_thinking,
390
+ * ai_response, and context_compressed events for the AI blame stream.
391
+ * Returns an array of structured events ordered by their appearance.
392
+ */
393
+ function extractAiEventsFromTranscript(transcriptPath, sessionId) {
394
+ if (!eventWriter) return [];
395
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return [];
396
+
397
+ const events = [];
398
+ let raw;
399
+ try {
400
+ raw = fs.readFileSync(transcriptPath, "utf-8");
401
+ } catch (_) {
402
+ return [];
403
+ }
404
+
405
+ for (const line of raw.split(/\r?\n/)) {
406
+ const trimmed = line.trim();
407
+ if (!trimmed) continue;
408
+ let entry;
409
+ try { entry = JSON.parse(trimmed); } catch (_) { continue; }
410
+
411
+ // Context compression: Claude Code emits entries with a summary field
412
+ // when the context window is compacted.
413
+ if (entry.type === "summary" && entry.summary) {
414
+ events.push(eventWriter.buildEvent(
415
+ eventWriter.EVENT_TYPES.CONTEXT_COMPRESSED,
416
+ {
417
+ summary: String(entry.summary).substring(0, 1000),
418
+ tokens_before: entry.tokens_before || null,
419
+ tokens_after: entry.tokens_after || null,
420
+ },
421
+ sessionId
422
+ ));
423
+ continue;
424
+ }
425
+
426
+ const message = entry.message;
427
+ if (!message || typeof message !== "object") continue;
428
+ const role = message.role || entry.role || "";
429
+ if (role !== "assistant") continue;
430
+
431
+ const contentBlocks = _getContentBlocks(entry);
432
+ if (!Array.isArray(contentBlocks)) continue;
433
+
434
+ for (const block of contentBlocks) {
435
+ if (!block || typeof block !== "object") continue;
436
+
437
+ if (block.type === "thinking" && block.thinking) {
438
+ events.push(eventWriter.buildEvent(
439
+ eventWriter.EVENT_TYPES.AI_THINKING,
440
+ { content: block.thinking },
441
+ sessionId
442
+ ));
443
+ }
444
+
445
+ if (block.type === "text" && block.text && block.text.trim()) {
446
+ events.push(eventWriter.buildEvent(
447
+ eventWriter.EVENT_TYPES.AI_RESPONSE,
448
+ { content: block.text },
449
+ sessionId
450
+ ));
451
+ }
452
+ }
453
+
454
+ // Token usage may appear on the entry itself (Claude Code's format)
455
+ if (entry.usage && eventWriter) {
456
+ const last = events[events.length - 1];
457
+ if (last && last.type === eventWriter.EVENT_TYPES.AI_RESPONSE) {
458
+ last.data.input_tokens = entry.usage.input_tokens || null;
459
+ last.data.output_tokens = entry.usage.output_tokens || null;
460
+ last.data.cache_read_tokens = entry.usage.cache_read_input_tokens || null;
461
+ }
462
+ }
463
+ }
464
+
465
+ return events;
466
+ }
467
+
468
+ /**
469
+ * Collect all events for this session from the audit JSONL files
470
+ * (tool_call, tool_result, tool_error, tool_denied, user_prompt, session_open)
471
+ * plus the provided pre-extracted AI events.
472
+ * Sort by ts. Returns an array ready to push to the Worker.
473
+ *
474
+ * Callers must extract AI events separately (via extractAiEventsFromTranscript)
475
+ * so the same extraction can be reused for local audit writes — avoiding a
476
+ * second transcript parse.
477
+ */
478
+ function collectSessionEvents(sessionId, aiEvents) {
479
+ const events = [];
480
+
481
+ // Gather structured events from audit (written by hooks during the session)
482
+ const typesForBlame = new Set([
483
+ "tool_call", "tool_result", "tool_error", "tool_denied",
484
+ "user_prompt", "session_open", "checkpoint",
485
+ ]);
486
+
487
+ for (const auditPath of getAllAuditPaths()) {
488
+ try {
489
+ const raw = fs.readFileSync(auditPath, "utf-8");
490
+ for (const line of raw.split(/\r?\n/)) {
491
+ const trimmed = line.trim();
492
+ if (!trimmed) continue;
493
+ let entry;
494
+ try { entry = JSON.parse(trimmed); } catch (_) { continue; }
495
+ if (entry.session_id === sessionId && typesForBlame.has(entry.type)) {
496
+ events.push(entry);
497
+ }
498
+ }
499
+ } catch (_) {
500
+ // ignore unreadable audit file
501
+ }
502
+ }
503
+
504
+ events.push(...aiEvents);
505
+
506
+ // Sort by ts ascending
507
+ events.sort((a, b) => {
508
+ if (a.ts < b.ts) return -1;
509
+ if (a.ts > b.ts) return 1;
510
+ return 0;
511
+ });
512
+
513
+ return events;
514
+ }
515
+
381
516
  // ── Transcript capture ────────────────────────────────────
382
- // Persists the full verbatim session transcript (Claude Code's native
383
- // transcript_path JSONL file) into sticky-note/data storage, keyed by
384
- // thread ID rather than commit SHA — a thread's transcript belongs to the
385
- // whole session, which may span zero, one, or many commits. Appended as
386
- // one entry per contributing session so resumed threads accumulate their
387
- // full history rather than overwriting it.
388
- //
389
- // Opt-out: set "capture_transcripts": false in sticky-note-config.json.
390
- // Secrets are redacted (best-effort — see redactSecrets in sticky-utils.js)
391
- // before anything is written to disk or committed.
517
+ // Keyed by thread ID rather than commit SHA — a session may span zero or many
518
+ // commits. Appended (not overwritten) so resumed threads accumulate history.
519
+ // Opt-out: "capture_transcripts": false in sticky-note-config.json.
392
520
 
393
521
  function captureTranscript(hookInput, threadId, sessionId, aiTool, user, now, config) {
394
522
  if (!threadId) return false;
@@ -800,6 +928,19 @@ function clearPresence(user) {
800
928
  try { fs.unlinkSync(presencePath); } catch (_) {}
801
929
  }
802
930
 
931
+ // ── Cloud helpers ─────────────────────────────────────────
932
+
933
+ function _makeCloudHeaders() {
934
+ const { url, apiKey } = getCloudConfig();
935
+ const projectName = getProjectName();
936
+ const headers = {
937
+ "Content-Type": "application/json",
938
+ "X-Sticky-Project": projectName,
939
+ };
940
+ if (apiKey) headers["X-Sticky-API-Key"] = apiKey;
941
+ return { url, headers };
942
+ }
943
+
803
944
  // ── Data-branch commit/push ───────────────────────────────
804
945
 
805
946
  /** Collect files from a directory matching an extension into fileMap. */
@@ -920,7 +1061,7 @@ async function main() {
920
1061
  memory.threads = threads;
921
1062
  let existing = null;
922
1063
 
923
- // V2.5: Check for active resumed thread (set by resume-thread command)
1064
+ // Check for active resumed thread (set by resume-thread command)
924
1065
  const activeResumeId = getActiveResumeThreadId();
925
1066
 
926
1067
  // Check for resumed thread (V2 resume signal or V2.5 active resume)
@@ -952,7 +1093,7 @@ async function main() {
952
1093
  });
953
1094
  }
954
1095
 
955
- // V2.5: Update contributors and resume metadata
1096
+ // Update contributors and resume metadata
956
1097
  const contributors = existing.contributors || [];
957
1098
  if (!contributors.includes(user)) {
958
1099
  contributors.push(user);
@@ -1069,13 +1210,7 @@ async function main() {
1069
1210
  .filter(Boolean)
1070
1211
  .find(e => e.session_id === sessionId);
1071
1212
  if (entry) {
1072
- const { url: stickyUrl, apiKey: stickyApiKey } = getCloudConfig();
1073
- const projectName = getProjectName();
1074
- const headers = {
1075
- "Content-Type": "application/json",
1076
- "X-Sticky-Project": projectName,
1077
- };
1078
- if (stickyApiKey) headers["X-Sticky-API-Key"] = stickyApiKey;
1213
+ const { url: stickyUrl, headers } = _makeCloudHeaders();
1079
1214
  fetch(`${stickyUrl}/transcripts/${existing.id}`, {
1080
1215
  method: "POST",
1081
1216
  headers,
@@ -1086,7 +1221,37 @@ async function main() {
1086
1221
  } catch (_) {} // transcript cloud sync is non-fatal
1087
1222
  }
1088
1223
 
1089
- // V2.5: Capture commit SHAs for attribution engine
1224
+ // Extract AI events once — used for both local audit writes and the Worker push.
1225
+ let aiEvents = [];
1226
+ try {
1227
+ aiEvents = extractAiEventsFromTranscript(hookInput.transcript_path || "", sessionId);
1228
+ for (const ev of aiEvents) {
1229
+ try { appendAuditLineBoth(ev, cloud); } catch (_) {}
1230
+ }
1231
+ } catch (_) {
1232
+ // non-fatal — AI event extraction must never interrupt session-end
1233
+ }
1234
+
1235
+ // Push complete event stream to Worker for team-wide AI blame.
1236
+ // This is a no-op if cloud is not configured or the endpoint doesn't exist yet.
1237
+ if (cloud && existing && existing.id) {
1238
+ try {
1239
+ const allEvents = collectSessionEvents(sessionId, aiEvents);
1240
+ if (allEvents.length > 0) {
1241
+ const { url: stickyUrl, headers } = _makeCloudHeaders();
1242
+ fetch(`${stickyUrl}/threads/${existing.id}/events`, {
1243
+ method: "POST",
1244
+ headers,
1245
+ body: JSON.stringify({ events: allEvents }),
1246
+ signal: AbortSignal.timeout(10000),
1247
+ }).catch(() => {}); // non-fatal
1248
+ }
1249
+ } catch (_) {
1250
+ // non-fatal — Worker endpoint may not exist until Plan 2 ships
1251
+ }
1252
+ }
1253
+
1254
+ // Capture commit SHAs for attribution engine
1090
1255
  const commitShas = getSessionCommitShas();
1091
1256
 
1092
1257
  // Housekeeping
@@ -1106,7 +1271,7 @@ async function main() {
1106
1271
  }
1107
1272
  appendAuditLineBoth(auditEntry, cloud);
1108
1273
 
1109
- // V2.5: Also write individual commit_sha audit entries for bridge lookup
1274
+ // Also write individual commit_sha audit entries for bridge lookup
1110
1275
  for (const sha of commitShas) {
1111
1276
  const commitEntry = {
1112
1277
  type: "commit",
@@ -1248,6 +1248,33 @@ async function main() {
1248
1248
  if (configContext) parts.push(configContext);
1249
1249
  if (presenceContext) parts.push(presenceContext);
1250
1250
 
1251
+ // Write session_open event for AI blame
1252
+ let eventWriter = null;
1253
+ try { eventWriter = require("./event-writer.js"); } catch (_) {}
1254
+ if (eventWriter) {
1255
+ try {
1256
+ const pkg = (() => {
1257
+ try { return require("../../package.json"); } catch (_) { return {}; }
1258
+ })();
1259
+ const openEvent = eventWriter.buildEvent(
1260
+ eventWriter.EVENT_TYPES.SESSION_OPEN,
1261
+ {
1262
+ model: hookInput.model || (hookInput.api_info && hookInput.api_info.model) || null,
1263
+ branch: getBranch(),
1264
+ user: getUser(),
1265
+ sticky_version: pkg.version || null,
1266
+ mcp_servers: (config.mcp_servers || [])
1267
+ .map(s => (typeof s === "object" ? s.name : s))
1268
+ .filter(Boolean),
1269
+ },
1270
+ getSessionId(hookInput)
1271
+ );
1272
+ appendAuditLineBoth(openEvent, cloud);
1273
+ } catch (_) {
1274
+ // best-effort — never break the hook
1275
+ }
1276
+ }
1277
+
1251
1278
  const output = parts.join("\n").trim();
1252
1279
  _emit(output);
1253
1280
  }
@@ -82,10 +82,10 @@ let _cachedStickyDir = null;
82
82
  function _stickyDir() {
83
83
  if (_cachedStickyDir) return _cachedStickyDir;
84
84
  try {
85
- const gitDir = execFileSync("git", ["rev-parse", "--absolute-git-dir"], {
85
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
86
86
  encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
87
87
  }).trim();
88
- _cachedStickyDir = path.resolve(gitDir, "sticky-note");
88
+ _cachedStickyDir = path.join(root, ".sticky-note");
89
89
  return _cachedStickyDir;
90
90
  } catch (_) {
91
91
  // Fallback for non-git environments (e.g., unit tests without git init)
@@ -1341,6 +1341,15 @@ async function cloudFetch(method, endpoint, body) {
1341
1341
  try {
1342
1342
  const resp = await fetch(`${url}${endpoint}`, opts);
1343
1343
  if (!resp.ok) {
1344
+ if (!_cloudWarned) {
1345
+ _cloudWarned = true;
1346
+ const hint = resp.status === 401
1347
+ ? " (check STICKY_API_KEY in .env.sticky)"
1348
+ : ` (HTTP ${resp.status})`;
1349
+ process.stderr.write(
1350
+ `[STICKY-NOTE] Cloud sync failed${hint} — using local/git fallback\n`
1351
+ );
1352
+ }
1344
1353
  return null;
1345
1354
  }
1346
1355
  const text = await resp.text();
@@ -1349,7 +1358,7 @@ async function cloudFetch(method, endpoint, body) {
1349
1358
  if (!_cloudWarned) {
1350
1359
  _cloudWarned = true;
1351
1360
  process.stderr.write(
1352
- "[STICKY-NOTE] Cloud unreachable, using local fallback\n"
1361
+ "[STICKY-NOTE] Cloud unreachable, using local/git fallback\n"
1353
1362
  );
1354
1363
  }
1355
1364
  return null;
@@ -35,6 +35,9 @@ try {
35
35
  gitNotes = null;
36
36
  }
37
37
 
38
+ let eventWriter = null;
39
+ try { eventWriter = require("./event-writer.js"); } catch (_) {}
40
+
38
41
  const {
39
42
  getConfigPath,
40
43
  getMemoryPath,
@@ -53,7 +56,8 @@ const {
53
56
  } = utils;
54
57
 
55
58
  const WRITE_TOOLS = new Set([
56
- "edit", "Edit", "create", "Create", "Write", "write", "MultiEdit", "multi_edit",
59
+ ...(eventWriter && eventWriter.WRITE_TOOLS ? eventWriter.WRITE_TOOLS : ["Edit", "edit", "Write", "write", "MultiEdit", "multi_edit"]),
60
+ "create", "Create",
57
61
  ]);
58
62
 
59
63
  function _debugPath() {
@@ -147,7 +151,7 @@ function autoDetectMcp(toolName) {
147
151
  return null;
148
152
  }
149
153
 
150
- // ── V2.5: Line-level change tracking ─────────────────────
154
+ // ── Line-level change tracking ───────────────────────────
151
155
 
152
156
  /**
153
157
  * After a write tool completes, capture exact line ranges changed.
@@ -257,10 +261,10 @@ function main() {
257
261
  }
258
262
 
259
263
  const user = getUser();
260
- const now = new Date().toISOString();
261
264
  const isWriteTool = WRITE_TOOLS.has(toolName);
262
265
 
263
- // V2.5: Capture line-level changes for write tools
266
+ // Capture line-level changes for write tools before timestamping the entry,
267
+ // so the ts reflects when the audit record is actually written.
264
268
  let lineRanges = null;
265
269
  let checkpoint = null;
266
270
  if (isWriteTool && filePath) {
@@ -270,6 +274,9 @@ function main() {
270
274
  }
271
275
  }
272
276
 
277
+ const now = new Date().toISOString();
278
+
279
+ // Legacy entry — kept for backward compat with existing audit queries
273
280
  const entry = {
274
281
  type: "tool_use",
275
282
  user,
@@ -277,17 +284,51 @@ function main() {
277
284
  tool: toolName,
278
285
  session_id: sessionId,
279
286
  };
280
- if (filePath) {
281
- entry.file = filePath;
282
- }
283
- if (lineRanges) {
284
- entry.lines_changed = lineRanges.map((r) => `${r.start}-${r.end}`);
285
- }
286
- if (checkpoint) {
287
- entry.checkpoint_topic = checkpoint.topic;
288
- }
287
+ if (filePath) entry.file = filePath;
288
+ if (lineRanges) entry.lines_changed = lineRanges.map((r) => `${r.start}-${r.end}`);
289
+ if (checkpoint) entry.checkpoint_topic = checkpoint.topic;
289
290
  appendAuditLineBoth(entry, cloud);
290
291
 
292
+ // New enriched events for AI blame
293
+ if (eventWriter) {
294
+ try {
295
+ const rawArgs = hookInput.tool_input || {};
296
+ const sanitizedArgs = eventWriter.sanitizeToolArgs(toolName, rawArgs);
297
+ const callEvent = eventWriter.buildEvent(
298
+ eventWriter.EVENT_TYPES.TOOL_CALL,
299
+ { tool: toolName, args: sanitizedArgs },
300
+ sessionId
301
+ );
302
+ appendAuditLineBoth(callEvent, cloud);
303
+
304
+ // tool_response may be a string or object
305
+ let rawResult = hookInput.tool_response;
306
+ if (rawResult && typeof rawResult === "object" && "output" in rawResult) {
307
+ rawResult = rawResult.output;
308
+ } else if (rawResult && typeof rawResult === "object") {
309
+ rawResult = JSON.stringify(rawResult);
310
+ }
311
+ const cappedResult = eventWriter.capResult(
312
+ typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult)
313
+ );
314
+ const resultData = {
315
+ tool: toolName,
316
+ result: cappedResult,
317
+ };
318
+ if (lineRanges) {
319
+ resultData.lines_changed = lineRanges.map((r) => `${r.start}-${r.end}`);
320
+ }
321
+ const resultEvent = eventWriter.buildEvent(
322
+ eventWriter.EVENT_TYPES.TOOL_RESULT,
323
+ resultData,
324
+ sessionId
325
+ );
326
+ appendAuditLineBoth(resultEvent, cloud);
327
+ } catch (_) {
328
+ // enrichment is best-effort — never break the hook
329
+ }
330
+ }
331
+
291
332
  updatePresence(user, filePath);
292
333
  if (cloud) {
293
334
  cloudWritePresence({
@@ -298,7 +339,6 @@ function main() {
298
339
  }).catch(() => {});
299
340
  }
300
341
 
301
- // V2.5: Write Git Note for write tools
302
342
  if (isWriteTool && filePath) {
303
343
  writeEditNote(sessionId, user, filePath, lineRanges, checkpoint);
304
344
  }