sticky-note-cli 2.9.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -10
- package/bin/cli.js +1153 -174
- package/bin/mcp-server.js +218 -36
- package/package.json +1 -1
- package/templates/CLAUDE.md +23 -0
- package/templates/gitignore-additions.txt +3 -0
- package/templates/hooks/data-branch.js +256 -0
- package/templates/hooks/inject-context.js +26 -12
- package/templates/hooks/on-error.js +22 -9
- package/templates/hooks/on-stop.js +12 -7
- package/templates/hooks/pre-tool-use.js +49 -52
- package/templates/hooks/session-end.js +190 -21
- package/templates/hooks/session-start.js +81 -9
- package/templates/hooks/sticky-codex.ps1 +20 -4
- package/templates/hooks/sticky-codex.sh +14 -5
- package/templates/hooks/sticky-utils.js +335 -29
- package/templates/hooks/track-work.js +27 -13
- package/templates/hooks.json +47 -47
- package/templates/settings.json +81 -81
- package/templates/sticky-note-config.json +2 -0
- package/templates/sticky-note-install.yml +80 -0
- package/templates/sticky-note.json +5 -5
|
@@ -62,8 +62,9 @@ try {
|
|
|
62
62
|
const {
|
|
63
63
|
getMemoryPath, loadJson, saveJson, saveMemoryMerged, getUser, getBranch,
|
|
64
64
|
getResumeThreadId, findThreadById, getSessionId,
|
|
65
|
-
appendAuditLine, detectTool, getConfigPath,
|
|
65
|
+
appendAuditLine, appendAuditLineBoth, detectTool, getConfigPath,
|
|
66
66
|
isThreadInjected, markThreadInjected, normalizeSep,
|
|
67
|
+
useCloud, cloudReadThreads, cloudWriteThread,
|
|
67
68
|
} = utils;
|
|
68
69
|
|
|
69
70
|
// ── Git helpers ───────────────────────────────────────────
|
|
@@ -367,7 +368,7 @@ function formatThread(thread, detailed) {
|
|
|
367
368
|
|
|
368
369
|
// ── Main ──────────────────────────────────────────────────
|
|
369
370
|
|
|
370
|
-
function main() {
|
|
371
|
+
async function main() {
|
|
371
372
|
let hookInput = {};
|
|
372
373
|
try {
|
|
373
374
|
const raw = require("fs").readFileSync(0, "utf-8");
|
|
@@ -385,6 +386,7 @@ function main() {
|
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
const sessionId = getSessionId(hookInput);
|
|
389
|
+
const cloud = useCloud();
|
|
388
390
|
|
|
389
391
|
function _auditInject(result, threadsScored, threadsInjected, topScores, error) {
|
|
390
392
|
threadsScored = threadsScored || 0;
|
|
@@ -402,7 +404,7 @@ function main() {
|
|
|
402
404
|
};
|
|
403
405
|
if (topScores) entry.top_scores = topScores;
|
|
404
406
|
if (error) entry.error = String(error).substring(0, 200);
|
|
405
|
-
|
|
407
|
+
appendAuditLineBoth(entry, cloud);
|
|
406
408
|
} catch (_) {
|
|
407
409
|
// ignore
|
|
408
410
|
}
|
|
@@ -410,13 +412,14 @@ function main() {
|
|
|
410
412
|
|
|
411
413
|
// Audit the user prompt
|
|
412
414
|
try {
|
|
413
|
-
|
|
415
|
+
const promptAudit = {
|
|
414
416
|
type: "user_prompt",
|
|
415
417
|
user: getUser(),
|
|
416
418
|
ts: new Date().toISOString(),
|
|
417
419
|
session_id: sessionId,
|
|
418
420
|
prompt: prompt.substring(0, 500),
|
|
419
|
-
}
|
|
421
|
+
};
|
|
422
|
+
appendAuditLineBoth(promptAudit, cloud);
|
|
420
423
|
} catch (_) {
|
|
421
424
|
// ignore
|
|
422
425
|
}
|
|
@@ -436,7 +439,12 @@ function main() {
|
|
|
436
439
|
}
|
|
437
440
|
}
|
|
438
441
|
|
|
439
|
-
const
|
|
442
|
+
const memoryPath = getMemoryPath();
|
|
443
|
+
const memory = loadJson(memoryPath, { version: "2", threads: [] });
|
|
444
|
+
if (cloud) {
|
|
445
|
+
const cloudThreads = await cloudReadThreads();
|
|
446
|
+
if (cloudThreads) memory.threads = cloudThreads;
|
|
447
|
+
}
|
|
440
448
|
const threads = (memory.threads || []).filter(Boolean);
|
|
441
449
|
|
|
442
450
|
const live = threads.filter(
|
|
@@ -504,7 +512,13 @@ function main() {
|
|
|
504
512
|
|
|
505
513
|
if (memoryDirty) {
|
|
506
514
|
try {
|
|
507
|
-
saveMemoryMerged(
|
|
515
|
+
saveMemoryMerged(memoryPath, memory);
|
|
516
|
+
if (cloud && resumeThreadId) {
|
|
517
|
+
const resumed = findThreadById(threads, resumeThreadId);
|
|
518
|
+
if (resumed) {
|
|
519
|
+
cloudWriteThread(resumed).catch(() => {});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
508
522
|
} catch (_) {
|
|
509
523
|
// ignore
|
|
510
524
|
}
|
|
@@ -610,7 +624,9 @@ function main() {
|
|
|
610
624
|
outputLines.unshift(header);
|
|
611
625
|
|
|
612
626
|
let output = outputLines.join("\n").trim();
|
|
613
|
-
|
|
627
|
+
if (process.env.STICKY_DEBUG) {
|
|
628
|
+
output += scoringBlock;
|
|
629
|
+
}
|
|
614
630
|
|
|
615
631
|
// Prepend overlap warning if other users' threads touch the same files
|
|
616
632
|
const overlapWarning = detectAndFormatOverlaps(threads, currentUser);
|
|
@@ -633,9 +649,7 @@ function main() {
|
|
|
633
649
|
// ── Entry point ───────────────────────────────────────────
|
|
634
650
|
|
|
635
651
|
if (require.main === module) {
|
|
636
|
-
|
|
637
|
-
main();
|
|
638
|
-
} catch (exc) {
|
|
652
|
+
main().catch((exc) => {
|
|
639
653
|
try {
|
|
640
654
|
utils.reportHookError("UserPromptSubmit", exc);
|
|
641
655
|
} catch (_) { /* ignore */ }
|
|
@@ -650,5 +664,5 @@ if (require.main === module) {
|
|
|
650
664
|
// ignore
|
|
651
665
|
}
|
|
652
666
|
_safeExit();
|
|
653
|
-
}
|
|
667
|
+
});
|
|
654
668
|
}
|
|
@@ -19,7 +19,7 @@ function _safeExit() {
|
|
|
19
19
|
process.exit(0);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
let getMemoryPath, loadJson, saveJson, saveMemoryMerged, appendAuditLine, getUser, detectTool, getSessionId;
|
|
22
|
+
let getMemoryPath, loadJson, saveJson, saveMemoryMerged, appendAuditLine, appendAuditLineBoth, getUser, detectTool, getSessionId, useCloud, cloudReadThreads, cloudWriteThread;
|
|
23
23
|
try {
|
|
24
24
|
({
|
|
25
25
|
getMemoryPath,
|
|
@@ -27,15 +27,19 @@ try {
|
|
|
27
27
|
saveJson,
|
|
28
28
|
saveMemoryMerged,
|
|
29
29
|
appendAuditLine,
|
|
30
|
+
appendAuditLineBoth,
|
|
30
31
|
getUser,
|
|
31
32
|
detectTool,
|
|
32
33
|
getSessionId,
|
|
34
|
+
useCloud,
|
|
35
|
+
cloudReadThreads,
|
|
36
|
+
cloudWriteThread,
|
|
33
37
|
} = require("./sticky-utils.js"));
|
|
34
38
|
} catch (_) {
|
|
35
39
|
_safeExit();
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function main() {
|
|
42
|
+
async function main() {
|
|
39
43
|
let hookInput = {};
|
|
40
44
|
try {
|
|
41
45
|
if (!process.stdin.isTTY) {
|
|
@@ -57,9 +61,15 @@ function main() {
|
|
|
57
61
|
const user = getUser();
|
|
58
62
|
const now = new Date().toISOString();
|
|
59
63
|
|
|
64
|
+
const cloud = useCloud();
|
|
60
65
|
const memoryPath = getMemoryPath();
|
|
61
66
|
const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
|
|
62
67
|
|
|
68
|
+
if (cloud) {
|
|
69
|
+
const cloudThreads = await cloudReadThreads();
|
|
70
|
+
if (cloudThreads) memory.threads = cloudThreads;
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
if (!Array.isArray(memory.threads)) {
|
|
64
74
|
memory.threads = [];
|
|
65
75
|
}
|
|
@@ -93,16 +103,21 @@ function main() {
|
|
|
93
103
|
// thread now. Creating standalone "stuck" threads from tool failures produced
|
|
94
104
|
// garbage entries (tool="Bash"/"Glob", no branch, no context).
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
const auditEntry = {
|
|
97
107
|
type: "error",
|
|
98
108
|
user: user,
|
|
99
109
|
ts: now,
|
|
100
110
|
session_id: sessionId,
|
|
101
111
|
error: errorMsg,
|
|
102
112
|
tool: toolName,
|
|
103
|
-
}
|
|
113
|
+
};
|
|
114
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
104
115
|
|
|
105
116
|
saveMemoryMerged(memoryPath, memory);
|
|
117
|
+
if (cloud) {
|
|
118
|
+
const threadToSync = existing || threads[threads.length - 1];
|
|
119
|
+
cloudWriteThread(threadToSync).catch(() => {});
|
|
120
|
+
}
|
|
106
121
|
const statusMsg = `[STICKY-NOTE] Marked thread as STUCK - ${errorMsg.substring(0, 80)}`;
|
|
107
122
|
try {
|
|
108
123
|
process.stdout.write(JSON.stringify({ output: statusMsg }) + "\n");
|
|
@@ -112,9 +127,7 @@ function main() {
|
|
|
112
127
|
process.exit(0);
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} catch (err) {
|
|
118
|
-
try { utils.logHookError("on-error", err); } catch (_) {}
|
|
130
|
+
main().catch((err) => {
|
|
131
|
+
try { logHookError("on-error", err); } catch (_) {}
|
|
119
132
|
_safeExit();
|
|
120
|
-
}
|
|
133
|
+
});
|
|
@@ -34,10 +34,12 @@ try {
|
|
|
34
34
|
|
|
35
35
|
const {
|
|
36
36
|
getMemoryPath,
|
|
37
|
+
getConfigPath,
|
|
37
38
|
loadJson,
|
|
38
39
|
saveJson,
|
|
39
40
|
saveMemoryMerged,
|
|
40
41
|
appendAuditLine,
|
|
42
|
+
appendAuditLineBoth,
|
|
41
43
|
getUser,
|
|
42
44
|
getBranch,
|
|
43
45
|
getSessionId,
|
|
@@ -45,10 +47,10 @@ const {
|
|
|
45
47
|
extractSessionFromAudit,
|
|
46
48
|
readHeadSha,
|
|
47
49
|
normalizeSep,
|
|
50
|
+
redactSecrets,
|
|
48
51
|
useCloud,
|
|
49
52
|
cloudReadThreads,
|
|
50
53
|
cloudWriteThread,
|
|
51
|
-
cloudAppendAudit,
|
|
52
54
|
} = utils;
|
|
53
55
|
|
|
54
56
|
// Files that belong to sticky-note internals — exclude from files_touched.
|
|
@@ -204,6 +206,8 @@ async function main() {
|
|
|
204
206
|
const cloud = useCloud();
|
|
205
207
|
const memoryPath = getMemoryPath();
|
|
206
208
|
const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
|
|
209
|
+
const config = loadJson(getConfigPath(), {});
|
|
210
|
+
const shouldRedact = config.redact_transcripts !== false;
|
|
207
211
|
|
|
208
212
|
if (cloud) {
|
|
209
213
|
const cloudThreads = await cloudReadThreads();
|
|
@@ -216,7 +220,8 @@ async function main() {
|
|
|
216
220
|
const auditFiles = auditData.files || [];
|
|
217
221
|
const gitFiles = getGitFilesTouched();
|
|
218
222
|
const allFiles = [...new Set([...auditFiles, ...gitFiles])];
|
|
219
|
-
|
|
223
|
+
let narrative = buildNarrativeFromAudit(auditData);
|
|
224
|
+
if (shouldRedact) narrative = redactSecrets(narrative);
|
|
220
225
|
const workType = classifyWorkType(auditData);
|
|
221
226
|
|
|
222
227
|
const threads = (memory.threads || []).filter(Boolean);
|
|
@@ -243,7 +248,10 @@ async function main() {
|
|
|
243
248
|
}
|
|
244
249
|
thread.tool_calls = prevCalls;
|
|
245
250
|
// Store prompts if not already present
|
|
246
|
-
const storedPrompts = (auditData.prompts || []).slice(0, 20).map((p) =>
|
|
251
|
+
const storedPrompts = (auditData.prompts || []).slice(0, 20).map((p) => {
|
|
252
|
+
const trimmed = p.substring(0, 300);
|
|
253
|
+
return shouldRedact ? redactSecrets(trimmed) : trimmed;
|
|
254
|
+
});
|
|
247
255
|
if (storedPrompts.length && !(thread.prompts || []).length) {
|
|
248
256
|
thread.prompts = storedPrompts;
|
|
249
257
|
}
|
|
@@ -267,10 +275,7 @@ async function main() {
|
|
|
267
275
|
session_id: sessionId,
|
|
268
276
|
reason: reason ? reason.substring(0, 200) : "stop_checkpoint",
|
|
269
277
|
};
|
|
270
|
-
|
|
271
|
-
if (cloud) {
|
|
272
|
-
cloudAppendAudit(auditEntry).catch(() => {});
|
|
273
|
-
}
|
|
278
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
274
279
|
|
|
275
280
|
saveMemoryMerged(memoryPath, memory);
|
|
276
281
|
if (cloud) {
|
|
@@ -59,10 +59,11 @@ const {
|
|
|
59
59
|
getSessionId,
|
|
60
60
|
getUser,
|
|
61
61
|
appendAuditLine,
|
|
62
|
+
appendAuditLineBoth,
|
|
62
63
|
isThreadInjected,
|
|
63
64
|
markThreadInjected,
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
useCloud,
|
|
66
|
+
cloudReadPresence,
|
|
66
67
|
getMemoryPath,
|
|
67
68
|
loadJson,
|
|
68
69
|
normalizeSep,
|
|
@@ -137,47 +138,41 @@ function formatThreadForInjection(threadData, file) {
|
|
|
137
138
|
return lines.join("\n");
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
function _getRecentlyModifiedFiles() {
|
|
148
|
-
const files = new Set();
|
|
149
|
-
for (const target of ["HEAD~5", "HEAD~1"]) {
|
|
150
|
-
try {
|
|
151
|
-
const result = execSync(`git diff --name-only ${target}`, {
|
|
152
|
-
encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
153
|
-
});
|
|
154
|
-
for (const f of result.trim().split(/\r?\n/)) {
|
|
155
|
-
if (f.trim()) files.add(f.trim());
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
} catch (_) { /* target doesn't exist */ }
|
|
159
|
-
}
|
|
141
|
+
let _presenceCache = null;
|
|
142
|
+
let _presenceCacheTs = 0;
|
|
143
|
+
const PRESENCE_CACHE_TTL_MS = 60000;
|
|
144
|
+
|
|
145
|
+
async function _checkConflict(filePath) {
|
|
146
|
+
if (!filePath || !useCloud()) return "";
|
|
160
147
|
try {
|
|
161
|
-
const
|
|
162
|
-
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
if (!_presenceCache || (now - _presenceCacheTs) > PRESENCE_CACHE_TTL_MS) {
|
|
150
|
+
_presenceCache = await cloudReadPresence();
|
|
151
|
+
_presenceCacheTs = now;
|
|
152
|
+
}
|
|
153
|
+
const presence = _presenceCache;
|
|
154
|
+
if (!presence) return "";
|
|
155
|
+
const currentUser = getUser();
|
|
156
|
+
const conflicting = presence.filter(p => {
|
|
157
|
+
if (!p.user || p.user === currentUser) return false;
|
|
158
|
+
return (p.active_files || []).some(f =>
|
|
159
|
+
f === filePath || filePath.endsWith(f) || f.endsWith(filePath)
|
|
160
|
+
);
|
|
163
161
|
});
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
if (conflicting.length > 0) {
|
|
163
|
+
const names = conflicting.map(p => p.user).join(", ");
|
|
164
|
+
return `⚠️ [STICKY-NOTE] Conflict warning: ${names} also editing ${filePath}`;
|
|
166
165
|
}
|
|
167
|
-
} catch (_) {
|
|
168
|
-
|
|
166
|
+
} catch (_) {
|
|
167
|
+
// ignore
|
|
168
|
+
}
|
|
169
|
+
return "";
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function _checkOverlapsAndDeny() {
|
|
172
|
-
// Overlap alerting is now handled by the MCP server's check_overlaps tool.
|
|
173
|
-
// This deny-based approach was unreliable (deny reason consumed silently).
|
|
174
|
-
// Kept as no-op to avoid breaking the call site.
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
172
|
|
|
178
173
|
// ── Main ──────────────────────────────────────────────────
|
|
179
174
|
|
|
180
|
-
function main() {
|
|
175
|
+
async function main() {
|
|
181
176
|
let hookInput = {};
|
|
182
177
|
try {
|
|
183
178
|
const raw = require("fs").readFileSync(0, "utf-8");
|
|
@@ -189,6 +184,7 @@ function main() {
|
|
|
189
184
|
}
|
|
190
185
|
|
|
191
186
|
const sessionId = getSessionId(hookInput);
|
|
187
|
+
const cloud = useCloud();
|
|
192
188
|
|
|
193
189
|
// ── Auto-sync before git pull/merge/rebase ──
|
|
194
190
|
// Prevents "local changes would be overwritten" errors for .sticky-note/ files
|
|
@@ -201,14 +197,6 @@ function main() {
|
|
|
201
197
|
}
|
|
202
198
|
}
|
|
203
199
|
|
|
204
|
-
// ── Overlap deny gate (Copilot CLI only) ──
|
|
205
|
-
// Detect overlaps and deny on first tool call, keyed by COPILOT_LOADER_PID.
|
|
206
|
-
const denyResult = _checkOverlapsAndDeny();
|
|
207
|
-
if (denyResult) {
|
|
208
|
-
process.stdout.write(JSON.stringify(denyResult) + "\n");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
200
|
// Extract file path from tool input
|
|
213
201
|
const filePath = extractFilePath(hookInput);
|
|
214
202
|
if (!filePath) {
|
|
@@ -216,7 +204,6 @@ function main() {
|
|
|
216
204
|
return;
|
|
217
205
|
}
|
|
218
206
|
|
|
219
|
-
// Run attribution engine: get threads for this file with line ranges
|
|
220
207
|
let fileAttr;
|
|
221
208
|
try {
|
|
222
209
|
fileAttr = attribution.getFileAttribution(filePath);
|
|
@@ -226,6 +213,10 @@ function main() {
|
|
|
226
213
|
}
|
|
227
214
|
|
|
228
215
|
if (!fileAttr || !fileAttr.threads || fileAttr.threads.length === 0) {
|
|
216
|
+
if (cloud) {
|
|
217
|
+
const warning = await _checkConflict(filePath);
|
|
218
|
+
if (warning) { _emit(warning); return; }
|
|
219
|
+
}
|
|
229
220
|
_emit("");
|
|
230
221
|
return;
|
|
231
222
|
}
|
|
@@ -236,6 +227,10 @@ function main() {
|
|
|
236
227
|
);
|
|
237
228
|
|
|
238
229
|
if (newThreads.length === 0) {
|
|
230
|
+
if (cloud) {
|
|
231
|
+
const warning = await _checkConflict(filePath);
|
|
232
|
+
if (warning) { _emit(warning); return; }
|
|
233
|
+
}
|
|
239
234
|
_emit("");
|
|
240
235
|
return;
|
|
241
236
|
}
|
|
@@ -248,11 +243,11 @@ function main() {
|
|
|
248
243
|
markThreadInjected(threadId, sessionId);
|
|
249
244
|
}
|
|
250
245
|
|
|
251
|
-
|
|
246
|
+
let output = outputParts.join("\n\n");
|
|
252
247
|
|
|
253
248
|
// Audit the injection
|
|
254
249
|
try {
|
|
255
|
-
|
|
250
|
+
const auditEntry = {
|
|
256
251
|
type: "lazy_inject",
|
|
257
252
|
user: getUser(),
|
|
258
253
|
ts: new Date().toISOString(),
|
|
@@ -263,20 +258,22 @@ function main() {
|
|
|
263
258
|
const id = t.thread ? t.thread.id : t.id;
|
|
264
259
|
return (id || "").substring(0, 8);
|
|
265
260
|
}),
|
|
266
|
-
}
|
|
261
|
+
};
|
|
262
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
267
263
|
} catch (_) {
|
|
268
264
|
// ignore
|
|
269
265
|
}
|
|
270
266
|
|
|
267
|
+
if (cloud) {
|
|
268
|
+
const warning = await _checkConflict(filePath);
|
|
269
|
+
if (warning) output += "\n\n" + warning;
|
|
270
|
+
}
|
|
271
|
+
|
|
271
272
|
_emit(output);
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
// ── Entry point ───────────────────────────────────────────
|
|
275
276
|
|
|
276
277
|
if (require.main === module) {
|
|
277
|
-
|
|
278
|
-
main();
|
|
279
|
-
} catch (_) {
|
|
280
|
-
_safeExit();
|
|
281
|
-
}
|
|
278
|
+
main().catch(() => _safeExit());
|
|
282
279
|
}
|