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/README.md +94 -10
- package/bin/cli.js +850 -167
- package/bin/mcp-server.js +218 -36
- package/package.json +5 -5
- package/templates/CLAUDE.md +23 -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 +138 -24
- package/templates/hooks/session-start.js +33 -9
- package/templates/hooks/sticky-codex.ps1 +20 -4
- package/templates/hooks/sticky-codex.sh +14 -5
- package/templates/hooks/sticky-utils.js +306 -20
- package/templates/hooks/track-work.js +27 -13
- package/templates/settings.json +7 -7
- package/templates/sticky-note-config.json +2 -0
- package/templates/sticky-note-install.yml +80 -0
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
927
|
+
(async () => {
|
|
928
|
+
await initCloudCache();
|
|
750
929
|
|
|
751
|
-
|
|
752
|
-
process.stdin.on("data", (chunk) => {
|
|
753
|
-
buffer += chunk;
|
|
930
|
+
let buffer = "";
|
|
754
931
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
buffer = buffer.slice(newlineIdx + 1);
|
|
932
|
+
process.stdin.setEncoding("utf-8");
|
|
933
|
+
process.stdin.on("data", (chunk) => {
|
|
934
|
+
buffer += chunk;
|
|
759
935
|
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
784
|
-
});
|
|
963
|
+
process.stdin.on("end", () => {
|
|
964
|
+
process.exit(0);
|
|
965
|
+
});
|
|
785
966
|
|
|
786
|
-
process.on("uncaughtException", (err) => {
|
|
787
|
-
|
|
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.
|
|
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": "
|
|
8
|
-
"sticky-note-cli": "
|
|
9
|
-
"sticky-note-mcp": "
|
|
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/",
|
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
-
|
|
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) {
|