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.
- package/README.md +77 -0
- package/bin/cli.js +61 -0
- package/bin/mcp-server.js +51 -9
- package/package.json +1 -1
- package/sticky-server/.wrangler/cache/cf.json +1 -0
- package/sticky-server/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/sticky-server/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/sticky-server/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/sticky-server/.wrangler/state/v3/do/sticky-note-server-StickyThread/metadata.sqlite +0 -0
- package/sticky-server/.wrangler/state/v3/do/sticky-note-server-StickyThread/metadata.sqlite-shm +0 -0
- package/sticky-server/.wrangler/state/v3/do/sticky-note-server-StickyThread/metadata.sqlite-wal +0 -0
- package/sticky-server/.wrangler/state/v3/kv/PLACEHOLDER_KV_NAMESPACE_ID/blobs/24473f36cce8fd4dcc0287333b336236fef2ffd176b20d9f9861257480f2d2f0000001a0272e56bb +1 -0
- package/sticky-server/.wrangler/state/v3/kv/PLACEHOLDER_KV_NAMESPACE_ID/blobs/896e937930a2be286388f1ae1003af2a41171bcdb97993b90e66a5836c4bce88000001a0272e57be +1 -0
- package/sticky-server/.wrangler/state/v3/kv/PLACEHOLDER_KV_NAMESPACE_ID/blobs/cb1dad011015782163f59d1e79653f10a354fc026eea8e482fe7417f4c4d7426000001a0272e57b2 +1 -0
- package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/d561fc7d90f13b8d408360f0c2d3dad0579c60d59ea97622cecfbe29274283cd.sqlite +0 -0
- package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/metadata.sqlite +0 -0
- package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/metadata.sqlite-shm +0 -0
- package/sticky-server/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/metadata.sqlite-wal +0 -0
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite +0 -0
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite-shm +0 -0
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite-wal +0 -0
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/metadata.sqlite +0 -0
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/metadata.sqlite-shm +0 -0
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/metadata.sqlite-wal +0 -0
- package/sticky-server/.wrangler/tmp/bundle-BhcBKo/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-BhcBKo/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-BhcBKo/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-EyT3Z4/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-EyT3Z4/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-EyT3Z4/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-Fenl4S/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-Fenl4S/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-Fenl4S/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-GEjBtr/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-GEjBtr/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-GEjBtr/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-QVovzR/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-QVovzR/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-QVovzR/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-cuweP4/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-cuweP4/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-cuweP4/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-jSM6YE/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-jSM6YE/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-jSM6YE/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-skMxAR/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-skMxAR/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-skMxAR/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/bundle-ufHiFB/checked-fetch.js +28 -0
- package/sticky-server/.wrangler/tmp/bundle-ufHiFB/middleware-insertion-facade.js +11 -0
- package/sticky-server/.wrangler/tmp/bundle-ufHiFB/middleware-loader.entry.ts +134 -0
- package/sticky-server/.wrangler/tmp/dev-7URP42/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-7URP42/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-J7vWKa/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-J7vWKa/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-QZJJpV/worker.js +579 -0
- package/sticky-server/.wrangler/tmp/dev-QZJJpV/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-ZhpGzw/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-ZhpGzw/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-Zj4Um8/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-Zj4Um8/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-cV8SnT/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-cV8SnT/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-dVJlR1/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-dVJlR1/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-gZFGI3/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-gZFGI3/worker.js.map +8 -0
- package/sticky-server/.wrangler/tmp/dev-rZTsQz/worker.js +1006 -0
- package/sticky-server/.wrangler/tmp/dev-rZTsQz/worker.js.map +8 -0
- package/sticky-server/adapters/cf-kv.js +60 -0
- package/sticky-server/mcp-tools.js +249 -0
- package/sticky-server/sticky-thread-do.js +134 -0
- package/sticky-server/worker.js +122 -1
- package/sticky-server/wrangler.toml +17 -0
- package/templates/gitignore-additions.txt +10 -0
- package/templates/hooks/event-writer.js +104 -0
- package/templates/hooks/inject-context.js +17 -12
- package/templates/hooks/on-error.js +46 -2
- package/templates/hooks/session-end.js +187 -22
- package/templates/hooks/session-start.js +27 -0
- package/templates/hooks/sticky-utils.js +12 -3
- package/templates/hooks/track-work.js +54 -14
package/sticky-server/worker.js
CHANGED
|
@@ -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
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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];
|