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
|
@@ -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
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
}
|