sticky-note-cli 3.0.0 → 3.1.1

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/bin/mcp-server.js CHANGED
@@ -3,9 +3,10 @@
3
3
  * sticky-note MCP server — stdio-based
4
4
  *
5
5
  * Implements Model Context Protocol (JSON-RPC 2.0 over stdin/stdout)
6
- * with 8 tools for AI assistants:
6
+ * with 9 tools for AI assistants:
7
7
  *
8
8
  * get_session_context(id) — full thread payload by ID
9
+ * get_full_transcript(id) — full verbatim session transcript(s) for a thread
9
10
  * get_stuck_threads() — all stuck threads
10
11
  * search_threads(query) — keyword search across threads
11
12
  * get_audit_trail(...) — query per-user JSONL audit logs
@@ -23,6 +24,7 @@
23
24
 
24
25
  const fs = require("fs");
25
26
  const path = require("path");
27
+ const { execFileSync } = require("child_process");
26
28
 
27
29
  // ──────────────────────────────────────────────
28
30
  // Project root resolution
@@ -36,8 +38,113 @@ for (let i = 0; i < args.length; i++) {
36
38
  }
37
39
  }
38
40
 
41
+ let _cachedStickyDir = null;
39
42
  function stickyDir() {
40
- return path.join(PROJECT_ROOT, ".sticky-note");
43
+ if (_cachedStickyDir) return _cachedStickyDir;
44
+
45
+ // V3 default storage lives at <git-dir>/sticky-note (the sticky-note/data
46
+ // orphan branch's local working copy), not in the project's working tree.
47
+ // Try that first; fall back to the legacy in-tree .sticky-note/ for repos
48
+ // that haven't migrated (or aren't git repos at all).
49
+ try {
50
+ const gitDir = execFileSync("git", ["rev-parse", "--absolute-git-dir"], {
51
+ encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"], cwd: PROJECT_ROOT,
52
+ }).trim();
53
+ const dataBranchDir = path.join(gitDir, "sticky-note");
54
+ if (fs.existsSync(path.join(dataBranchDir, "sticky-note.json"))) {
55
+ _cachedStickyDir = dataBranchDir;
56
+ return _cachedStickyDir;
57
+ }
58
+ } catch (_) {
59
+ // not a git repo, or git unavailable — fall through to legacy path
60
+ }
61
+
62
+ _cachedStickyDir = path.join(PROJECT_ROOT, ".sticky-note");
63
+ return _cachedStickyDir;
64
+ }
65
+
66
+ function getTranscriptPath(threadId) {
67
+ return path.join(stickyDir(), "transcripts", threadId + ".jsonl");
68
+ }
69
+
70
+ // ──────────────────────────────────────────────
71
+ // Cloud config (V3 optional backend)
72
+ // ──────────────────────────────────────────────
73
+
74
+ function readEnvSticky() {
75
+ const envPath = path.join(PROJECT_ROOT, ".env.sticky");
76
+ const result = {};
77
+ if (!fs.existsSync(envPath)) return result;
78
+ for (const line of fs.readFileSync(envPath, "utf-8").split(/\r?\n/)) {
79
+ const trimmed = line.trim();
80
+ if (trimmed.startsWith("STICKY_URL=")) result.STICKY_URL = trimmed.slice("STICKY_URL=".length);
81
+ if (trimmed.startsWith("STICKY_API_KEY=")) result.STICKY_API_KEY = trimmed.slice("STICKY_API_KEY=".length);
82
+ }
83
+ return result;
84
+ }
85
+
86
+ function getCloudConfig() {
87
+ const envFile = readEnvSticky();
88
+ return {
89
+ url: process.env.STICKY_URL || envFile.STICKY_URL || "",
90
+ apiKey: process.env.STICKY_API_KEY || envFile.STICKY_API_KEY || "",
91
+ };
92
+ }
93
+
94
+ function getProjectName() {
95
+ try {
96
+ const remote = execFileSync("git", ["remote", "get-url", "origin"], {
97
+ encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"], cwd: PROJECT_ROOT,
98
+ }).trim();
99
+ const match = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
100
+ if (match) return match[1];
101
+ } catch (_) {}
102
+ return "default";
103
+ }
104
+
105
+ let _cloudThreads = null;
106
+ let _cloudPresence = null;
107
+ let _cloudPresenceMap = null;
108
+
109
+ function buildPresenceMap(presence) {
110
+ const map = {};
111
+ for (const entry of presence) {
112
+ if (entry.user) map[entry.user] = { active_files: entry.active_files || [], last_seen: entry.last_seen };
113
+ }
114
+ return map;
115
+ }
116
+
117
+ async function initCloudCache() {
118
+ const { url, apiKey } = getCloudConfig();
119
+ if (!url) return;
120
+
121
+ const headers = { "X-Sticky-Project": getProjectName() };
122
+ if (apiKey) headers["X-Sticky-API-Key"] = apiKey;
123
+
124
+ function timedFetch(endpoint) {
125
+ const controller = new AbortController();
126
+ const t = setTimeout(() => controller.abort(), 5000);
127
+ return fetch(`${url}${endpoint}`, { headers, signal: controller.signal })
128
+ .finally(() => clearTimeout(t));
129
+ }
130
+
131
+ try {
132
+ const [threadsRes, presenceRes] = await Promise.all([
133
+ timedFetch("/threads"),
134
+ timedFetch("/presence"),
135
+ ]);
136
+ if (threadsRes.ok) {
137
+ const data = await threadsRes.json();
138
+ _cloudThreads = data.threads || [];
139
+ }
140
+ if (presenceRes.ok) {
141
+ const data = await presenceRes.json();
142
+ _cloudPresence = data.presence || [];
143
+ _cloudPresenceMap = buildPresenceMap(_cloudPresence);
144
+ }
145
+ } catch (_) {
146
+ process.stderr.write("[sticky-note] Cloud unreachable — using local data\n");
147
+ }
41
148
  }
42
149
 
43
150
  // ──────────────────────────────────────────────
@@ -69,6 +176,7 @@ function getConfig() {
69
176
  }
70
177
 
71
178
  function getThreads() {
179
+ if (_cloudThreads !== null) return _cloudThreads;
72
180
  return getMemory().threads || [];
73
181
  }
74
182
 
@@ -77,10 +185,11 @@ function getThreads() {
77
185
  * Returns { username: { active_files, last_seen } }
78
186
  */
79
187
  function getAllPresence() {
188
+ if (_cloudPresenceMap !== null) return _cloudPresenceMap;
189
+
80
190
  const presenceDir = path.join(stickyDir(), "presence");
81
191
  const result = {};
82
192
  if (!fs.existsSync(presenceDir)) return result;
83
-
84
193
  for (const file of fs.readdirSync(presenceDir)) {
85
194
  if (!file.endsWith(".json")) continue;
86
195
  const user = file.replace(".json", "");
@@ -156,6 +265,60 @@ function toolGetSessionContext(params) {
156
265
  return { thread };
157
266
  }
158
267
 
268
+ /**
269
+ * Return the full verbatim transcript(s) captured for a thread — one entry
270
+ * per contributing session (a resumed thread may have several). Secrets
271
+ * are redacted before storage (see redactSecrets in sticky-utils.js) unless
272
+ * a team has disabled redact_transcripts.
273
+ */
274
+ function toolGetFullTranscript(params) {
275
+ const { id } = params;
276
+ if (!id) {
277
+ return { error: "Missing required parameter: id" };
278
+ }
279
+
280
+ const threads = getThreads();
281
+ const thread = threads.find((t) => t.id === id || t.id.startsWith(id) || t.session_id === id);
282
+ if (!thread) {
283
+ return { error: `No thread found with id: ${id}` };
284
+ }
285
+
286
+ const filePath = getTranscriptPath(thread.id);
287
+ if (!fs.existsSync(filePath)) {
288
+ return {
289
+ thread_id: thread.id,
290
+ transcript_captured: !!thread.transcript_captured,
291
+ sessions: [],
292
+ note: "No transcript captured for this thread — it may predate transcript capture, or capture_transcripts is disabled for this team.",
293
+ };
294
+ }
295
+
296
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter((l) => l.trim());
297
+ const sessions = [];
298
+ for (const line of lines) {
299
+ let entry;
300
+ try {
301
+ entry = JSON.parse(line);
302
+ } catch {
303
+ continue;
304
+ }
305
+ sessions.push({
306
+ session_id: entry.session_id,
307
+ tool: entry.tool,
308
+ user: entry.user,
309
+ captured_at: entry.captured_at,
310
+ redacted: !!entry.redacted,
311
+ transcript: entry.transcript,
312
+ });
313
+ }
314
+
315
+ return {
316
+ thread_id: thread.id,
317
+ session_count: sessions.length,
318
+ sessions,
319
+ };
320
+ }
321
+
159
322
  function toolGetStuckThreads() {
160
323
  const threads = getThreads();
161
324
  const stuck = threads.filter((t) => t.status === "stuck");
@@ -515,6 +678,21 @@ const TOOLS = {
515
678
  },
516
679
  handler: toolGetSessionContext,
517
680
  },
681
+ get_full_transcript: {
682
+ description:
683
+ "Get the full verbatim transcript(s) captured for a thread by ID — one entry per contributing session. Secrets are redacted before storage unless a team disabled redact_transcripts. Returns an empty sessions array if no transcript was captured (e.g. capture_transcripts is off, or the thread predates this feature).",
684
+ inputSchema: {
685
+ type: "object",
686
+ properties: {
687
+ id: {
688
+ type: "string",
689
+ description: "Thread ID or session ID to look up",
690
+ },
691
+ },
692
+ required: ["id"],
693
+ },
694
+ handler: toolGetFullTranscript,
695
+ },
518
696
  get_stuck_threads: {
519
697
  description:
520
698
  "Get all threads with status 'stuck'. Returns full payloads including failed approaches and last notes. Call at session start to check for team blockers.",
@@ -634,7 +812,7 @@ const TOOLS = {
634
812
 
635
813
  const SERVER_INFO = {
636
814
  name: "sticky-note",
637
- version: "3.0.0",
815
+ version: "3.1.0",
638
816
  };
639
817
 
640
818
  const CAPABILITIES = {
@@ -746,43 +924,47 @@ function handleRequest(msg) {
746
924
  // stdio transport
747
925
  // ──────────────────────────────────────────────
748
926
 
749
- let buffer = "";
927
+ (async () => {
928
+ await initCloudCache();
750
929
 
751
- process.stdin.setEncoding("utf-8");
752
- process.stdin.on("data", (chunk) => {
753
- buffer += chunk;
930
+ let buffer = "";
754
931
 
755
- let newlineIdx;
756
- while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
757
- const line = buffer.slice(0, newlineIdx).trim();
758
- buffer = buffer.slice(newlineIdx + 1);
932
+ process.stdin.setEncoding("utf-8");
933
+ process.stdin.on("data", (chunk) => {
934
+ buffer += chunk;
759
935
 
760
- if (!line) continue;
936
+ let newlineIdx;
937
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
938
+ const line = buffer.slice(0, newlineIdx).trim();
939
+ buffer = buffer.slice(newlineIdx + 1);
761
940
 
762
- let msg;
763
- try {
764
- msg = JSON.parse(line);
765
- } catch {
766
- const errResp = {
767
- jsonrpc: "2.0",
768
- id: null,
769
- error: { code: -32700, message: "Parse error" },
770
- };
771
- process.stdout.write(JSON.stringify(errResp) + "\n");
772
- continue;
773
- }
941
+ if (!line) continue;
774
942
 
775
- const response = handleRequest(msg);
776
- if (response) {
777
- process.stdout.write(JSON.stringify(response) + "\n");
943
+ let msg;
944
+ try {
945
+ msg = JSON.parse(line);
946
+ } catch {
947
+ const errResp = {
948
+ jsonrpc: "2.0",
949
+ id: null,
950
+ error: { code: -32700, message: "Parse error" },
951
+ };
952
+ process.stdout.write(JSON.stringify(errResp) + "\n");
953
+ continue;
954
+ }
955
+
956
+ const response = handleRequest(msg);
957
+ if (response) {
958
+ process.stdout.write(JSON.stringify(response) + "\n");
959
+ }
778
960
  }
779
- }
780
- });
961
+ });
781
962
 
782
- process.stdin.on("end", () => {
783
- process.exit(0);
784
- });
963
+ process.stdin.on("end", () => {
964
+ process.exit(0);
965
+ });
785
966
 
786
- process.on("uncaughtException", (err) => {
787
- process.stderr.write(`sticky-note MCP server error: ${err.message}\n`);
788
- });
967
+ process.on("uncaughtException", (err) => {
968
+ process.stderr.write(`sticky-note MCP server error: ${err.message}\n`);
969
+ });
970
+ })();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Human-to-human handoff for AI coding assistants. Git-backed shared memory for Claude Code and Copilot CLI.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
- "sticky-note": "./bin/cli.js",
8
- "sticky-note-cli": "./bin/cli.js",
9
- "sticky-note-mcp": "./bin/mcp-server.js"
7
+ "sticky-note": "bin/cli.js",
8
+ "sticky-note-cli": "bin/cli.js",
9
+ "sticky-note-mcp": "bin/mcp-server.js"
10
10
  },
11
11
  "keywords": [
12
12
  "ai",
@@ -21,7 +21,7 @@
21
21
  "homepage": "https://github.com/BandaruDheeraj/sticky-note#readme",
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "https://github.com/BandaruDheeraj/sticky-note.git"
24
+ "url": "git+https://github.com/BandaruDheeraj/sticky-note.git"
25
25
  },
26
26
  "files": [
27
27
  "bin/",
@@ -33,6 +33,7 @@ cat .sticky-note/sticky-note.json
33
33
  | `resumed_by` | Most recent user to resume this thread (V2.5) |
34
34
  | `resumed_at` | ISO timestamp of most recent resume (V2.5) |
35
35
  | `resume_history` | Full resume chain: user, timestamp, session (V2.5) |
36
+ | `transcript_captured` | Whether a full verbatim session transcript exists for this thread (V3.1) |
36
37
 
37
38
  ### Status icons
38
39
 
@@ -107,6 +108,24 @@ approaches, prompts). Read and present this context to the user immediately —
107
108
  do NOT tell them to start a new session. The inject-context hook will also
108
109
  pick up the resumed thread on subsequent prompts.
109
110
 
111
+ ### Full transcript capture (V3.1)
112
+
113
+ By default, each session's complete verbatim transcript is captured — not
114
+ just the narrative summary — keyed by thread ID and stored alongside audit
115
+ logs and presence data. Secrets are redacted before anything is written
116
+ (best-effort pattern matching, not a guarantee).
117
+
118
+ - A thread's `transcript_captured` field is `true` once at least one
119
+ session's transcript has been persisted.
120
+ - Retrieve it with `npx sticky-note transcript <thread-id>` (add `--raw` for
121
+ the untouched JSONL) or the `get_full_transcript(id)` MCP tool.
122
+ - A team can turn this off with `"capture_transcripts": false` in
123
+ `sticky-note-config.json` — e.g. for a repo where sessions routinely
124
+ touch especially sensitive material.
125
+ - When a user asks "what did we actually discuss in that session" (as
126
+ opposed to "what was the summary"), prefer the transcript over
127
+ `narrative`/`prompts` — those are truncated summaries, not the full record.
128
+
110
129
  ### File attribution (V2.5)
111
130
 
112
131
  To see which threads are related to a specific file:
@@ -245,6 +264,10 @@ You have access to a `sticky-note` MCP server. Use it:
245
264
  4. **For audit history**: Call `get_audit_trail(file)` to understand who
246
265
  changed a file and when.
247
266
 
267
+ 5. **For the full conversation, not just the summary**: Call
268
+ `get_full_transcript(id)` to get the verbatim session transcript(s) for
269
+ a thread, when `narrative` or `prompts` aren't enough detail.
270
+
248
271
  These tools are the primary way sticky-note communicates with you.
249
272
  Hook-injected context supplements but does not replace MCP tool calls.
250
273
 
@@ -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) {