sticky-note-cli 3.1.14 → 3.1.16

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 +61 -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 +10 -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
@@ -11,6 +11,8 @@
11
11
  * GET /threads/:id — get thread
12
12
  * PUT /threads/:id — update thread
13
13
  * DELETE /threads/:id — tombstone thread
14
+ * POST /threads/:id/events — append event batch (local session push)
15
+ * GET /threads/:id/events — retrieve all stored events
14
16
  * GET /audit — query audit (?file=, ?user=, ?since=, ?tool=)
15
17
  * POST /audit — append audit record
16
18
  * GET /presence — list active developers
@@ -21,6 +23,7 @@
21
23
  */
22
24
 
23
25
  import * as adapter from "./adapters/cf-kv.js";
26
+ import { MCP_TOOL_DEFINITIONS, handleMcpTool, _commitThreadToGit, EVENT_TYPES } from "./mcp-tools.js";
24
27
 
25
28
  // ── Helpers ──────────────────────────────────────────────
26
29
 
@@ -51,6 +54,14 @@ function matchRoute(method, pathname) {
51
54
  // Thread routes
52
55
  if (method === "POST" && pathname === "/threads") return { handler: "createThread" };
53
56
  if (method === "GET" && pathname === "/threads") return { handler: "listThreads" };
57
+ if (method === "POST" && pathname.match(/^\/threads\/[^/]+\/events$/)) {
58
+ const id = pathname.split("/")[2];
59
+ return { handler: "appendThreadEvents", id };
60
+ }
61
+ if (method === "GET" && pathname.match(/^\/threads\/[^/]+\/events$/)) {
62
+ const id = pathname.split("/")[2];
63
+ return { handler: "getThreadEvents", id };
64
+ }
54
65
  if (method === "GET" && pathname.startsWith("/threads/")) {
55
66
  return { handler: "getThread", id: pathname.slice("/threads/".length) };
56
67
  }
@@ -76,6 +87,10 @@ function matchRoute(method, pathname) {
76
87
  if (method === "GET" && pathname === "/config") return { handler: "getConfig" };
77
88
  if (method === "PUT" && pathname === "/config") return { handler: "putConfig" };
78
89
 
90
+ // MCP routes
91
+ if (method === "GET" && pathname === "/mcp") return { handler: "mcpListTools" };
92
+ if (method === "POST" && pathname === "/mcp") return { handler: "mcpCallTool" };
93
+
79
94
  // Health
80
95
  if (method === "GET" && pathname === "/health") return { handler: "health" };
81
96
 
@@ -126,6 +141,20 @@ const handlers = {
126
141
  return json({ deleted: id });
127
142
  },
128
143
 
144
+ // ── Thread Events ──
145
+
146
+ async appendThreadEvents(request, kv, project, { id }) {
147
+ const body = await request.json().catch(() => ({}));
148
+ const events = Array.isArray(body.events) ? body.events : [];
149
+ await adapter.appendEvents(kv, project, id, events);
150
+ return json({ ok: true, appended: events.length });
151
+ },
152
+
153
+ async getThreadEvents(_request, kv, project, { id }) {
154
+ const events = await adapter.getEvents(kv, project, id);
155
+ return json({ events });
156
+ },
157
+
129
158
  // ── Audit ──
130
159
 
131
160
  async queryAudit(request, kv, project) {
@@ -165,6 +194,25 @@ const handlers = {
165
194
  return json({ deleted: user });
166
195
  },
167
196
 
197
+ // ── MCP ──
198
+
199
+ async mcpListTools() {
200
+ return json({ tools: MCP_TOOL_DEFINITIONS });
201
+ },
202
+
203
+ async mcpCallTool(request, _kv, project, _route, env) {
204
+ const body = await request.json().catch(() => ({}));
205
+ const toolName = body.tool || body.name;
206
+ const toolInput = body.input || body.arguments || {};
207
+ if (!toolName) return error("tool name required");
208
+ try {
209
+ const result = await handleMcpTool(toolName, toolInput, env, project, getOrCreateDO);
210
+ return json({ content: [{ type: "text", text: JSON.stringify(result) }] });
211
+ } catch (err) {
212
+ return error(err.message, 400);
213
+ }
214
+ },
215
+
168
216
  // ── Config ──
169
217
 
170
218
  async getConfig(_request, kv, project) {
@@ -180,6 +228,13 @@ const handlers = {
180
228
  },
181
229
  };
182
230
 
231
+ // ── Durable Object helper ────────────────────────────────
232
+
233
+ function getOrCreateDO(env, threadId) {
234
+ const id = env.STICKY_THREAD.idFromName(threadId);
235
+ return env.STICKY_THREAD.get(id);
236
+ }
237
+
183
238
  // ── Worker entry point ───────────────────────────────────
184
239
 
185
240
  export default {
@@ -213,7 +268,7 @@ export default {
213
268
 
214
269
  try {
215
270
  const handler = handlers[route.handler];
216
- const response = await handler(request, kv, project, route);
271
+ const response = await handler(request, kv, project, route, env);
217
272
 
218
273
  // Add CORS headers to all responses
219
274
  const headers = new Headers(response.headers);
@@ -226,4 +281,70 @@ export default {
226
281
  return error(`Internal error: ${err.message}`, 500);
227
282
  }
228
283
  },
284
+
285
+ async scheduled(event, env, ctx) {
286
+ ctx.waitUntil(runCrashRecovery(env));
287
+ },
229
288
  };
289
+
290
+ // ── Crash recovery (cron) ────────────────────────────────
291
+
292
+ async function runCrashRecovery(env) {
293
+ const projects = ["default"]; // TODO: discover from KV index if multi-project
294
+ const staleThresholdMs = 30 * 60 * 1000; // 30 minutes
295
+ const now = Date.now();
296
+
297
+ for (const project of projects) {
298
+ const threadsListKey = `${project}:threads_index`;
299
+ const threadIds = await env.STICKY_KV.get(threadsListKey, { type: "json" })
300
+ .catch(() => []) || [];
301
+
302
+ const metas = await Promise.all(
303
+ threadIds.map(id =>
304
+ env.STICKY_KV.get(`${project}:thread:${id}`, { type: "json" }).catch(() => null)
305
+ )
306
+ );
307
+ for (let i = 0; i < threadIds.length; i++) {
308
+ const meta = metas[i];
309
+ const threadId = threadIds[i];
310
+ if (!meta || meta.status !== "open") continue;
311
+
312
+ const lastActivity = new Date(meta.last_activity_at || meta.created_at).getTime();
313
+ if (now - lastActivity < staleThresholdMs) continue;
314
+
315
+ // Track last_recovery_attempt to avoid hammering
316
+ const lastAttempt = meta._last_recovery_attempt || 0;
317
+ if (now - lastAttempt < 5 * 60 * 1000) continue; // skip if tried < 5min ago
318
+
319
+ // Thread is stale — flush DO buffer and mark as stale
320
+ try {
321
+ const do_stub = getOrCreateDO(env, threadId);
322
+ const recovery_event = {
323
+ ts: new Date().toISOString(), type: EVENT_TYPES.SESSION_CLOSE, session_id: null,
324
+ data: { narrative: "[recovered by cron]", work_type: "unknown", handoff_summary: "", failed_approaches: [], files_touched: [] },
325
+ };
326
+
327
+ meta._last_recovery_attempt = now;
328
+ await env.STICKY_KV.put(metaKey, JSON.stringify(meta));
329
+
330
+ await do_stub.fetch(new Request("http://do/do/close", {
331
+ method: "POST",
332
+ headers: { "Content-Type": "application/json" },
333
+ body: JSON.stringify({ close_event: recovery_event }),
334
+ }));
335
+
336
+ meta.status = "stale";
337
+ meta.last_activity_at = new Date().toISOString();
338
+ await env.STICKY_KV.put(metaKey, JSON.stringify(meta));
339
+
340
+ // GitHub commit for stale thread — commit the already-written stale metadata directly
341
+ await _commitThreadToGit(env, project, threadId, getOrCreateDO).catch(() => {});
342
+ } catch (_) {
343
+ // best-effort — log to console for observability
344
+ console.error(`[sticky-note] crash recovery failed for thread ${threadId}`);
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ export { StickyThread } from "./sticky-thread-do.js";
@@ -5,6 +5,23 @@ compatibility_date = "2024-09-01"
5
5
  [vars]
6
6
  ENVIRONMENT = "production"
7
7
 
8
+ # Required secrets (set via `wrangler secret put`):
9
+ # STICKY_API_KEY — API key for X-Sticky-API-Key auth
10
+ # GITHUB_PAT — Fine-grained PAT with Contents read+write on the target repo
11
+ # GITHUB_REPO — Target repo in "owner/repo" format (e.g. "acme/frontend")
12
+
8
13
  [[kv_namespaces]]
9
14
  binding = "STICKY_KV"
10
15
  id = "PLACEHOLDER_KV_NAMESPACE_ID"
16
+
17
+ [durable_objects]
18
+ bindings = [
19
+ { name = "STICKY_THREAD", class_name = "StickyThread" }
20
+ ]
21
+
22
+ [[migrations]]
23
+ tag = "v1"
24
+ new_classes = ["StickyThread"]
25
+
26
+ [triggers]
27
+ crons = ["*/5 * * * *"]
@@ -1,5 +1,6 @@
1
1
  # Sticky Note - local overrides (never commit these)
2
2
  .claude/settings.local.json
3
+ .claude/worktrees/
3
4
  .env.sticky
4
5
 
5
6
  # Sticky Note - transient session/resume signals
@@ -14,6 +15,15 @@
14
15
  # Sticky Note - debug log
15
16
  .sticky-note/hook-debug.log
16
17
  .sticky-note/.sticky-debug.jsonl
18
+ .sticky-note/.sticky-errors.log
19
+
20
+ # Sticky Note - runtime internals (never commit)
21
+ .sticky-note/.sticky-syncing
22
+ .sticky-note/.sticky-backups/
23
+ .sticky-note/.env-provision-hash
24
+ .sticky-note/.overlap-warned
25
+ .sticky-note/.overlap-pending
26
+ .sticky-note/.overlap-dismissed
17
27
 
18
28
  # Sticky Note - legacy single files (migrated to per-user dirs)
19
29
  .sticky-note/sticky-note-audit.jsonl
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * event-writer.js — Shared Event Schema and Builder (V4)
5
+ *
6
+ * Provides event type constants, a builder for structured events,
7
+ * and sanitizers for tool args and results.
8
+ * Imported by track-work.js, on-error.js, inject-context.js,
9
+ * session-start.js, and session-end.js.
10
+ */
11
+
12
+ const crypto = require("crypto");
13
+
14
+ const MAX_RESULT_BYTES = 10 * 1024; // 10 KB cap on tool results
15
+ const MAX_ARG_INLINE_BYTES = 1 * 1024; // 1 KB: keep verbatim below this
16
+
17
+ const EVENT_TYPES = {
18
+ SESSION_OPEN: "session_open",
19
+ USER_PROMPT: "user_prompt",
20
+ AI_THINKING: "ai_thinking",
21
+ AI_RESPONSE: "ai_response",
22
+ TOOL_CALL: "tool_call",
23
+ TOOL_RESULT: "tool_result",
24
+ TOOL_ERROR: "tool_error",
25
+ TOOL_DENIED: "tool_denied",
26
+ CONTEXT_COMPRESSED: "context_compressed",
27
+ GIT_COMMIT: "git_commit",
28
+ SUBAGENT_SPAWN: "subagent_spawn",
29
+ SUBAGENT_RESULT: "subagent_result",
30
+ CHECKPOINT: "checkpoint",
31
+ SESSION_CLOSE: "session_close",
32
+ };
33
+
34
+ const WRITE_TOOLS = new Set([
35
+ "Edit", "edit", "Write", "write", "MultiEdit", "multi_edit",
36
+ ]);
37
+
38
+ /**
39
+ * Build a structured event object.
40
+ * @param {string} type - One of EVENT_TYPES values
41
+ * @param {object} data - Event-specific payload
42
+ * @param {string} sessionId - Session ID for correlation
43
+ * @returns {{ ts: string, type: string, session_id: string, data: object }}
44
+ */
45
+ function buildEvent(type, data, sessionId) {
46
+ return {
47
+ ts: new Date().toISOString(),
48
+ type,
49
+ session_id: sessionId || null,
50
+ data: data || {},
51
+ };
52
+ }
53
+
54
+ function _sha256hex(str) {
55
+ return crypto.createHash("sha256").update(str).digest("hex").slice(0, 16);
56
+ }
57
+
58
+ function _refOf(str) {
59
+ return { _hash: _sha256hex(str), _len: str.length };
60
+ }
61
+
62
+ /**
63
+ * Sanitize tool call args before storing.
64
+ * - Non-write tools: returned unchanged.
65
+ * - Write tools: large string fields (old_string, new_string, content) are
66
+ * replaced with a { _hash, _len } reference if they exceed MAX_ARG_INLINE_BYTES.
67
+ * Short strings are kept verbatim.
68
+ */
69
+ function sanitizeToolArgs(toolName, args) {
70
+ if (!args || typeof args !== "object" || Array.isArray(args)) return args;
71
+ if (!WRITE_TOOLS.has(toolName)) return args;
72
+
73
+ const out = { ...args };
74
+
75
+ for (const field of ["old_string", "new_string"]) {
76
+ if (typeof out[field] === "string" &&
77
+ Buffer.byteLength(out[field], "utf-8") > MAX_ARG_INLINE_BYTES) {
78
+ out[field + "_ref"] = _refOf(out[field]);
79
+ delete out[field];
80
+ }
81
+ }
82
+
83
+ if (typeof out.content === "string" &&
84
+ Buffer.byteLength(out.content, "utf-8") > MAX_ARG_INLINE_BYTES) {
85
+ out.content_ref = _refOf(out.content);
86
+ delete out.content;
87
+ }
88
+
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * Cap a tool result string to MAX_RESULT_BYTES.
94
+ * Non-string values are returned unchanged.
95
+ */
96
+ function capResult(result) {
97
+ if (typeof result !== "string") return result;
98
+ const bytes = Buffer.byteLength(result, "utf-8");
99
+ if (bytes <= MAX_RESULT_BYTES) return result;
100
+ const cutoff = Math.floor(MAX_RESULT_BYTES * 0.95);
101
+ return result.slice(0, cutoff) + `...[truncated, ${bytes} bytes total]`;
102
+ }
103
+
104
+ module.exports = { EVENT_TYPES, WRITE_TOOLS, buildEvent, sanitizeToolArgs, capResult, MAX_RESULT_BYTES, MAX_ARG_INLINE_BYTES };
@@ -410,18 +410,23 @@ async function main() {
410
410
  }
411
411
  }
412
412
 
413
- // Audit the user prompt
414
- try {
415
- const promptAudit = {
416
- type: "user_prompt",
417
- user: getUser(),
418
- ts: new Date().toISOString(),
419
- session_id: sessionId,
420
- prompt: prompt.substring(0, 500),
421
- };
422
- appendAuditLineBoth(promptAudit, cloud);
423
- } catch (_) {
424
- // ignore
413
+ // Capture user_prompt event for AI blame
414
+ let eventWriter = null;
415
+ try { eventWriter = require("./event-writer.js"); } catch (_) {}
416
+
417
+ if (eventWriter) {
418
+ try {
419
+ const promptEntry = eventWriter.buildEvent(
420
+ eventWriter.EVENT_TYPES.USER_PROMPT,
421
+ { content: prompt }, // verbatim, no truncation
422
+ sessionId
423
+ );
424
+ // Keep legacy `prompt` field for backward compat with existing audit queries
425
+ promptEntry.prompt = prompt.substring(0, 500);
426
+ appendAuditLineBoth(promptEntry, cloud);
427
+ } catch (_) {
428
+ // ignore
429
+ }
425
430
  }
426
431
 
427
432
  // Auto-checkpoint: tag subsequent edits with what the user asked for
@@ -8,8 +8,6 @@
8
8
  * Appends JSONL audit line.
9
9
  */
10
10
 
11
- const crypto = require("crypto");
12
-
13
11
  function _safeExit() {
14
12
  try {
15
13
  process.stdout.write(JSON.stringify({ output: "" }) + "\n");
@@ -39,6 +37,20 @@ try {
39
37
  _safeExit();
40
38
  }
41
39
 
40
+ let eventWriter = null;
41
+ try { eventWriter = require("./event-writer.js"); } catch (_) {}
42
+
43
+ function _isDenial(hookInput) {
44
+ if (hookInput.blocked === true) return true;
45
+ const msg = (hookInput.error || hookInput.message || "").toLowerCase();
46
+ return (
47
+ msg.includes("blocked by the user") ||
48
+ msg.includes("user rejected") ||
49
+ msg.includes("permission denied by user") ||
50
+ msg.includes("tool was blocked")
51
+ );
52
+ }
53
+
42
54
  async function main() {
43
55
  let hookInput = {};
44
56
  try {
@@ -113,6 +125,38 @@ async function main() {
113
125
  };
114
126
  appendAuditLineBoth(auditEntry, cloud);
115
127
 
128
+ // Write structured event for AI blame
129
+ if (eventWriter) {
130
+ try {
131
+ const isDenial = _isDenial(hookInput);
132
+ if (isDenial) {
133
+ const denialEvent = eventWriter.buildEvent(
134
+ eventWriter.EVENT_TYPES.TOOL_DENIED,
135
+ {
136
+ tool: toolName,
137
+ args: eventWriter.sanitizeToolArgs(toolName, hookInput.tool_input || {}),
138
+ reason: errorMsg,
139
+ },
140
+ sessionId
141
+ );
142
+ appendAuditLineBoth(denialEvent, cloud);
143
+ } else {
144
+ const errorEvent = eventWriter.buildEvent(
145
+ eventWriter.EVENT_TYPES.TOOL_ERROR,
146
+ {
147
+ tool: toolName,
148
+ args: eventWriter.sanitizeToolArgs(toolName, hookInput.tool_input || {}),
149
+ error: errorMsg,
150
+ },
151
+ sessionId
152
+ );
153
+ appendAuditLineBoth(errorEvent, cloud);
154
+ }
155
+ } catch (_) {
156
+ // best-effort
157
+ }
158
+ }
159
+
116
160
  saveMemoryMerged(memoryPath, memory);
117
161
  if (cloud) {
118
162
  const threadToSync = existing || threads[threads.length - 1];