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.
@@ -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
- appendAuditLine(entry);
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
- appendAuditLine({
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 memory = loadJson(getMemoryPath(), { version: "2", threads: [] });
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(getMemoryPath(), memory);
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
- output += scoringBlock;
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
- try {
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
- appendAuditLine({
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
- try {
116
- main();
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
- const narrative = buildNarrativeFromAudit(auditData);
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) => p.substring(0, 300));
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
- appendAuditLine(auditEntry);
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
- isOverlapWarned,
65
- markOverlapWarned,
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
- // ── Overlap deny via COPILOT_LOADER_PID (Copilot CLI only) ──
141
- // Each Copilot CLI session has a unique COPILOT_LOADER_PID env var.
142
- // On the first tool call, if overlaps exist, deny with the warning.
143
- // The warned state is keyed by PID so concurrent sessions don't interfere.
144
-
145
- const { execSync } = require("child_process");
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 result = execSync("git diff --name-only", {
162
- encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
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
- for (const f of result.trim().split(/\r?\n/)) {
165
- if (f.trim()) files.add(f.trim());
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 (_) { /* ignore */ }
168
- return files;
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
- const output = outputParts.join("\n\n");
246
+ let output = outputParts.join("\n\n");
252
247
 
253
248
  // Audit the injection
254
249
  try {
255
- appendAuditLine({
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
- try {
278
- main();
279
- } catch (_) {
280
- _safeExit();
281
- }
278
+ main().catch(() => _safeExit());
282
279
  }