sticky-note-cli 2.5.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.
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * on-stop.js — Handoff Summary Generation (V2)
5
+ *
6
+ * Hook: Stop (Claude Code)
7
+ * Generates a structured handoff_summary on the current thread.
8
+ */
9
+
10
+ const crypto = require("crypto");
11
+
12
+ function safeExit() {
13
+ try {
14
+ process.stdout.write(JSON.stringify({ output: "" }) + "\n");
15
+ } catch (_) {
16
+ process.stdout.write('{"output":""}\n');
17
+ }
18
+ process.exit(0);
19
+ }
20
+
21
+ let utils;
22
+ try {
23
+ utils = require("./sticky-utils.js");
24
+ } catch (_) {
25
+ safeExit();
26
+ }
27
+
28
+ const {
29
+ getMemoryPath,
30
+ loadJson,
31
+ saveJson,
32
+ appendAuditLine,
33
+ getUser,
34
+ getBranch,
35
+ getSessionId,
36
+ detectTool,
37
+ } = utils;
38
+
39
+ function buildHandoffSummary(thread, reason) {
40
+ const parts = [];
41
+ const files = thread.files_touched || [];
42
+ const workType = thread.work_type || "general";
43
+ const note = thread.last_note || "";
44
+
45
+ if (files.length) {
46
+ parts.push(`What done: ${workType} on ${files.slice(0, 5).join(", ")}`);
47
+ } else if (note) {
48
+ parts.push(`What done: ${note}`);
49
+ }
50
+
51
+ const failed = thread.failed_approaches || [];
52
+ if (failed.length) {
53
+ const descs = failed.slice(0, 3).map((a) => (a.description || "").substring(0, 60));
54
+ parts.push(`What failed: ${descs.join("; ")}`);
55
+ }
56
+
57
+ const narrative = thread.narrative || "";
58
+ if (narrative) {
59
+ parts.push(`Status: ${narrative.substring(0, 150)}`);
60
+ }
61
+
62
+ if (reason) {
63
+ parts.push(`Next: ${reason.substring(0, 100)}`);
64
+ }
65
+
66
+ return parts.length ? parts.join(" | ") : "Session stopped -- no summary available";
67
+ }
68
+
69
+ function main() {
70
+ let hookInput = {};
71
+ try {
72
+ if (!process.stdin.isTTY) {
73
+ const raw = require("fs").readFileSync(0, "utf-8").trim();
74
+ if (raw) {
75
+ hookInput = JSON.parse(raw);
76
+ }
77
+ }
78
+ } catch (_) {
79
+ hookInput = {};
80
+ }
81
+
82
+ const sessionId = getSessionId(hookInput);
83
+ const user = getUser();
84
+ const now = new Date().toISOString();
85
+ const reason = hookInput.reason || "";
86
+
87
+ const memoryPath = getMemoryPath();
88
+ const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
89
+
90
+ const threads = memory.threads || [];
91
+ let found = false;
92
+
93
+ for (const thread of threads) {
94
+ if (thread.session_id === sessionId && (thread.status === "open" || thread.status === "stuck")) {
95
+ thread.last_activity_at = now;
96
+ thread.handoff_summary = buildHandoffSummary(thread, reason);
97
+ if (reason) {
98
+ thread.last_note = reason.substring(0, 200);
99
+ }
100
+ found = true;
101
+ break;
102
+ }
103
+ }
104
+
105
+ if (!found && sessionId !== "unknown") {
106
+ const thread = {
107
+ id: crypto.randomUUID(),
108
+ user,
109
+ project: memory.project || "",
110
+ status: "closed",
111
+ branch: getBranch(),
112
+ created_at: now,
113
+ closed_at: now,
114
+ last_activity_at: now,
115
+ files_touched: [],
116
+ last_note: reason ? reason.substring(0, 200) : "Session stopped",
117
+ narrative: "",
118
+ failed_approaches: [],
119
+ handoff_summary: buildHandoffSummary({}, reason),
120
+ related_session_ids: [],
121
+ tool: detectTool(hookInput),
122
+ session_id: sessionId,
123
+ };
124
+ threads.push(thread);
125
+ }
126
+
127
+ appendAuditLine({
128
+ type: "stop",
129
+ user,
130
+ ts: now,
131
+ session_id: sessionId,
132
+ reason: reason ? reason.substring(0, 200) : "stop_checkpoint",
133
+ });
134
+
135
+ saveJson(memoryPath, memory);
136
+ const reasonStr = reason ? reason.substring(0, 60) : "checkpoint";
137
+ const statusMsg = `[STICKY-NOTE] Session stopped - handoff saved (${reasonStr})`;
138
+ try {
139
+ process.stdout.write(JSON.stringify({ output: statusMsg }) + "\n");
140
+ } catch (_) {
141
+ process.stdout.write('{"output":""}\n');
142
+ }
143
+ process.exit(0);
144
+ }
145
+
146
+ try {
147
+ main();
148
+ } catch (_) {
149
+ safeExit();
150
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * parse-transcript.js — Standalone Transcript Parser (V2)
5
+ *
6
+ * Extracts narrative + failed_approaches from a session transcript file.
7
+ * Used by sticky-codex.sh to process Codex stdout/stderr logs.
8
+ * Also usable standalone: node parse-transcript.js <transcript_path>
9
+ *
10
+ * Outputs JSON to stdout: {"narrative": "...", "failed_approaches": [...]}
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const {
15
+ parseJsonlFile,
16
+ extractNarrativeFromEntries,
17
+ extractNarrativeFromText,
18
+ extractFailedFromEntries,
19
+ extractFailedFromText,
20
+ } = require("./sticky-utils.js");
21
+
22
+ function parsePlaintextTranscript(filePath) {
23
+ try {
24
+ return fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
25
+ } catch (_) {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ function parseTranscript(filePath) {
31
+ const entries = parseJsonlFile(filePath);
32
+ if (entries.length) {
33
+ return {
34
+ narrative: extractNarrativeFromEntries(entries),
35
+ failed_approaches: extractFailedFromEntries(entries),
36
+ };
37
+ }
38
+
39
+ const lines = parsePlaintextTranscript(filePath);
40
+ if (lines.length) {
41
+ return {
42
+ narrative: extractNarrativeFromText(lines),
43
+ failed_approaches: extractFailedFromText(lines),
44
+ };
45
+ }
46
+
47
+ return { narrative: "", failed_approaches: [] };
48
+ }
49
+
50
+ function main() {
51
+ if (process.argv.length < 3) {
52
+ process.stderr.write("Usage: parse-transcript.js <transcript_path>\n");
53
+ process.exit(1);
54
+ }
55
+
56
+ const transcriptPath = process.argv[2];
57
+ if (!fs.existsSync(transcriptPath)) {
58
+ process.stderr.write(`Warning: transcript file not found: ${transcriptPath}\n`);
59
+ console.log(JSON.stringify({ narrative: "", failed_approaches: [] }));
60
+ return;
61
+ }
62
+
63
+ const result = parseTranscript(transcriptPath);
64
+ console.log(JSON.stringify(result));
65
+ }
66
+
67
+ main();
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * post-rewrite.js — Git post-rewrite hook for Sticky Note
5
+ *
6
+ * Copies Git Notes from old commit SHAs to new SHAs when commits
7
+ * are rewritten via rebase or amend. This keeps session attribution
8
+ * alive through history rewrites.
9
+ *
10
+ * Install: place in .git/hooks/post-rewrite (or use core.hooksPath)
11
+ * Git calls this with: post-rewrite <command>
12
+ * Stdin receives: <old-sha> <new-sha>\n per rewritten commit
13
+ */
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+
18
+ let gitNotes;
19
+ try {
20
+ // Try hooks dir first (deployed)
21
+ gitNotes = require(path.join(__dirname, "..", ".claude", "hooks", "sticky-git-notes.js"));
22
+ } catch (_) {
23
+ try {
24
+ // Try relative to .git/hooks/
25
+ gitNotes = require(path.join(__dirname, "..", "..", ".claude", "hooks", "sticky-git-notes.js"));
26
+ } catch (_) {
27
+ try {
28
+ // Try templates
29
+ gitNotes = require(path.join(__dirname, "..", "..", "templates", "hooks", "sticky-git-notes.js"));
30
+ } catch (_) {
31
+ // Can't load git-notes module — exit silently
32
+ process.exit(0);
33
+ }
34
+ }
35
+ }
36
+
37
+ function main() {
38
+ // Read stdin: "old_sha new_sha\n" pairs
39
+ let stdin = "";
40
+ try {
41
+ stdin = fs.readFileSync(0, "utf-8");
42
+ } catch (_) {
43
+ process.exit(0);
44
+ }
45
+
46
+ if (!stdin.trim()) process.exit(0);
47
+
48
+ const pairs = gitNotes.parsePostRewriteInput(stdin);
49
+ if (pairs.length === 0) process.exit(0);
50
+
51
+ const copied = gitNotes.copyNotesForRewrite(pairs);
52
+
53
+ if (copied > 0 && process.env.STICKY_NOTE_DEBUG) {
54
+ process.stderr.write(`[sticky-note] Copied ${copied} note(s) to rewritten commits\n`);
55
+ }
56
+ }
57
+
58
+ try {
59
+ main();
60
+ } catch (_) {
61
+ // Never fail a git hook
62
+ process.exit(0);
63
+ }
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * pre-tool-use.js — Lazy Injection via PreToolUse Hook (V2.5)
5
+ *
6
+ * Hook: PreToolUse (Claude Code) / preToolUse (Copilot CLI)
7
+ * Fires BEFORE each tool call. If the tool targets a file, runs
8
+ * git blame → attribution engine to find threads that authored
9
+ * lines in that file, and injects them as system context with
10
+ * line-range detail.
11
+ *
12
+ * Dedup: each thread is only injected once per session via the
13
+ * .sticky-injected tracking file.
14
+ *
15
+ * No external dependencies — uses built-in git blame.
16
+ */
17
+
18
+ // ── Output helpers ────────────────────────────────────────
19
+
20
+ function _isCopilotCli() {
21
+ return process.argv.includes("--copilot-cli") || !!process.env.COPILOT_CLI;
22
+ }
23
+
24
+ function _emit(text) {
25
+ if (text === undefined) text = "";
26
+ if (_isCopilotCli()) {
27
+ process.stdout.write(JSON.stringify({ additionalContext: text }) + "\n");
28
+ } else {
29
+ process.stdout.write(JSON.stringify({ output: text }) + "\n");
30
+ }
31
+ }
32
+
33
+ function _safeExit() {
34
+ try {
35
+ _emit("");
36
+ } catch (_) {
37
+ process.stdout.write('{"output": ""}\n');
38
+ }
39
+ process.exit(0);
40
+ }
41
+
42
+ // ── Import dependencies ───────────────────────────────────
43
+
44
+ let utils, attribution;
45
+ try {
46
+ utils = require("./sticky-utils.js");
47
+ } catch (_) {
48
+ _safeExit();
49
+ }
50
+
51
+ try {
52
+ attribution = require("./sticky-attribution.js");
53
+ } catch (_) {
54
+ // Attribution engine not available — skip lazy injection
55
+ _safeExit();
56
+ }
57
+
58
+ const {
59
+ getSessionId,
60
+ getUser,
61
+ appendAuditLine,
62
+ isThreadInjected,
63
+ markThreadInjected,
64
+ } = utils;
65
+
66
+ // ── File path extraction from tool input ──────────────────
67
+
68
+ function extractFilePath(hookInput) {
69
+ const toolInput = hookInput.tool_input || hookInput.input || hookInput.toolArgs || {};
70
+
71
+ if (typeof toolInput === "object" && toolInput !== null) {
72
+ for (const key of ["file_path", "filePath", "path", "file", "filename"]) {
73
+ if (toolInput[key] && typeof toolInput[key] === "string") {
74
+ return toolInput[key];
75
+ }
76
+ }
77
+ if (toolInput.command && typeof toolInput.command === "string") {
78
+ return extractFileFromCommand(toolInput.command);
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ function extractFileFromCommand(command) {
86
+ const tokens = command.split(/\s+/);
87
+ for (const token of tokens) {
88
+ if (token.includes("/") || token.includes("\\")) {
89
+ if (/\.\w{1,10}$/.test(token)) {
90
+ return token;
91
+ }
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+
97
+ // ── Thread formatting for injection ───────────────────────
98
+
99
+ function formatThreadForInjection(threadData, file) {
100
+ const thread = threadData.thread || threadData;
101
+ const lineRanges = threadData.line_ranges || threadData._line_ranges || [];
102
+ const tier = threadData.tier || threadData._tier || "";
103
+
104
+ const user = thread.user || thread.author || "unknown";
105
+ const status = thread.status || "open";
106
+ const statusTag = status === "stuck" ? "[STUCK]" : status === "open" ? "[OPEN]" : "[CLOSED]";
107
+ const branch = thread.branch || "";
108
+ const branchStr = branch ? ` (${branch})` : "";
109
+
110
+ const lineStr = lineRanges.length > 0 ? ` [lines ${lineRanges.join(", ")}]` : "";
111
+
112
+ const lines = [
113
+ `[STICKY-NOTE] ${statusTag} ${user}'s thread on ${file}${lineStr}${branchStr}:`,
114
+ ];
115
+
116
+ if (thread.narrative) {
117
+ lines.push(thread.narrative.substring(0, 200));
118
+ } else if (thread.last_note) {
119
+ lines.push(thread.last_note.substring(0, 150));
120
+ }
121
+
122
+ const failed = thread.failed_approaches || [];
123
+ if (failed.length > 0) {
124
+ lines.push(`[!] ${failed.length} failed approach(es) - check thread ${(thread.id || "").substring(0, 8)} for details`);
125
+ }
126
+
127
+ if (thread.handoff_summary) {
128
+ lines.push(`Handoff: ${thread.handoff_summary.substring(0, 150)}`);
129
+ }
130
+
131
+ return lines.join("\n");
132
+ }
133
+
134
+ // ── Main ──────────────────────────────────────────────────
135
+
136
+ function main() {
137
+ let hookInput = {};
138
+ try {
139
+ const raw = require("fs").readFileSync(0, "utf-8");
140
+ if (raw.trim()) {
141
+ hookInput = JSON.parse(raw);
142
+ }
143
+ } catch (_) {
144
+ hookInput = {};
145
+ }
146
+
147
+ const sessionId = getSessionId(hookInput);
148
+
149
+ // Extract file path from tool input
150
+ const filePath = extractFilePath(hookInput);
151
+ if (!filePath) {
152
+ _emit("");
153
+ return;
154
+ }
155
+
156
+ // Run attribution engine: get threads for this file with line ranges
157
+ let fileAttr;
158
+ try {
159
+ fileAttr = attribution.getFileAttribution(filePath);
160
+ } catch (_) {
161
+ _emit("");
162
+ return;
163
+ }
164
+
165
+ if (!fileAttr || !fileAttr.threads || fileAttr.threads.length === 0) {
166
+ _emit("");
167
+ return;
168
+ }
169
+
170
+ // Filter out already-injected threads
171
+ const newThreads = fileAttr.threads.filter(
172
+ (t) => !isThreadInjected(t.thread ? t.thread.id : t.id)
173
+ );
174
+
175
+ if (newThreads.length === 0) {
176
+ _emit("");
177
+ return;
178
+ }
179
+
180
+ // Build injection output with line-range detail
181
+ const outputParts = [];
182
+ for (const threadData of newThreads) {
183
+ const threadId = threadData.thread ? threadData.thread.id : threadData.id;
184
+ outputParts.push(formatThreadForInjection(threadData, filePath));
185
+ markThreadInjected(threadId, sessionId);
186
+ }
187
+
188
+ const output = outputParts.join("\n\n");
189
+
190
+ // Audit the injection
191
+ try {
192
+ appendAuditLine({
193
+ type: "lazy_inject",
194
+ user: getUser(),
195
+ ts: new Date().toISOString(),
196
+ session_id: sessionId,
197
+ file: filePath,
198
+ threads_injected: newThreads.length,
199
+ thread_ids: newThreads.map((t) => {
200
+ const id = t.thread ? t.thread.id : t.id;
201
+ return (id || "").substring(0, 8);
202
+ }),
203
+ });
204
+ } catch (_) {
205
+ // ignore
206
+ }
207
+
208
+ _emit(output);
209
+ }
210
+
211
+ // ── Entry point ───────────────────────────────────────────
212
+
213
+ if (require.main === module) {
214
+ try {
215
+ main();
216
+ } catch (_) {
217
+ _safeExit();
218
+ }
219
+ }