sticky-note-cli 2.5.2 → 2.5.4
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/cli.js +1 -1
- package/package.json +1 -1
- package/templates/hooks/on-stop.js +175 -13
- package/templates/hooks/sticky-codex.sh +71 -71
package/bin/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ const { execSync, execFileSync } = require("child_process");
|
|
|
21
21
|
// Constants
|
|
22
22
|
// ──────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
const VERSION = "
|
|
24
|
+
const VERSION = require("../package.json").version;
|
|
25
25
|
|
|
26
26
|
const HOOK_FILES = [
|
|
27
27
|
"sticky-utils.js",
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const crypto = require("crypto");
|
|
11
|
+
const { execFileSync } = require("child_process");
|
|
11
12
|
|
|
12
13
|
function safeExit() {
|
|
13
14
|
try {
|
|
@@ -35,8 +36,117 @@ const {
|
|
|
35
36
|
getBranch,
|
|
36
37
|
getSessionId,
|
|
37
38
|
detectTool,
|
|
39
|
+
extractSessionFromAudit,
|
|
40
|
+
readHeadSha,
|
|
41
|
+
normalizeSep,
|
|
42
|
+
useCloud,
|
|
43
|
+
cloudReadThreads,
|
|
44
|
+
cloudWriteThread,
|
|
45
|
+
cloudAppendAudit,
|
|
38
46
|
} = utils;
|
|
39
47
|
|
|
48
|
+
// Files that belong to sticky-note internals — exclude from files_touched.
|
|
49
|
+
const STICKY_FILES_PREFIX = [
|
|
50
|
+
".sticky-note/sticky-note.json",
|
|
51
|
+
".sticky-note/sticky-note-audit.jsonl",
|
|
52
|
+
".sticky-note/audit/",
|
|
53
|
+
".sticky-note/presence/",
|
|
54
|
+
".sticky-note/.sticky-session",
|
|
55
|
+
".sticky-note/.sticky-head",
|
|
56
|
+
".sticky-note/.sticky-resume",
|
|
57
|
+
".sticky-note/.sticky-debug.jsonl",
|
|
58
|
+
".sticky-note/sticky-note-config.json",
|
|
59
|
+
".sticky-note/.sticky-presence.json",
|
|
60
|
+
".sticky-note/.sticky-injected",
|
|
61
|
+
".sticky-note/.sticky-active-resume",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Collect files changed since session start using git diff.
|
|
66
|
+
* Mirrors getGitFilesTouched() from session-end.js.
|
|
67
|
+
*/
|
|
68
|
+
function getGitFilesTouched() {
|
|
69
|
+
const files = new Set();
|
|
70
|
+
const savedSha = readHeadSha();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (savedSha && /^[0-9a-f]+$/i.test(savedSha)) {
|
|
74
|
+
const result = execFileSync("git", ["diff", "--name-only", savedSha, "HEAD"], {
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
timeout: 5000,
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
});
|
|
79
|
+
for (const f of result.trim().split(/\r?\n/)) {
|
|
80
|
+
const trimmed = f.trim();
|
|
81
|
+
if (trimmed) files.add(trimmed);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const unstaged = execFileSync("git", ["diff", "--name-only"], {
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
timeout: 5000,
|
|
88
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
+
});
|
|
90
|
+
for (const f of unstaged.trim().split(/\r?\n/)) {
|
|
91
|
+
const trimmed = f.trim();
|
|
92
|
+
if (trimmed) files.add(trimmed);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const staged = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
96
|
+
encoding: "utf-8",
|
|
97
|
+
timeout: 5000,
|
|
98
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
99
|
+
});
|
|
100
|
+
for (const f of staged.trim().split(/\r?\n/)) {
|
|
101
|
+
const trimmed = f.trim();
|
|
102
|
+
if (trimmed) files.add(trimmed);
|
|
103
|
+
}
|
|
104
|
+
} catch (_) {
|
|
105
|
+
// ignore — git may not be available
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return Array.from(files).filter((f) => {
|
|
109
|
+
const normalized = normalizeSep(f);
|
|
110
|
+
return !STICKY_FILES_PREFIX.some((p) => normalized === p || normalized.startsWith(p));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build a narrative string from audit trail prompts.
|
|
116
|
+
* Mirrors the audit-fallback path from session-end.js extractNarrative().
|
|
117
|
+
*/
|
|
118
|
+
function buildNarrativeFromAudit(auditData) {
|
|
119
|
+
const prompts = auditData.prompts || [];
|
|
120
|
+
if (!prompts.length) return "";
|
|
121
|
+
if (prompts.length === 1) return prompts[0].substring(0, 300).trim();
|
|
122
|
+
let summary = prompts[0].substring(0, 150).trim();
|
|
123
|
+
if (prompts.length > 2) {
|
|
124
|
+
summary += ` ... (${prompts.length} prompts total) ... ${prompts[prompts.length - 1].substring(0, 100).trim()}`;
|
|
125
|
+
} else {
|
|
126
|
+
summary += ` -> ${prompts[prompts.length - 1].substring(0, 100).trim()}`;
|
|
127
|
+
}
|
|
128
|
+
return summary.substring(0, 300);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Classify work type from audit tool counts (lightweight version of
|
|
133
|
+
* analyzeSessionActivities from session-end.js).
|
|
134
|
+
*/
|
|
135
|
+
function classifyWorkType(auditData) {
|
|
136
|
+
const tools = auditData.tools || {};
|
|
137
|
+
const files = auditData.files || [];
|
|
138
|
+
const editLike = new Set(["edit", "create", "write", "Edit", "Write", "Create", "MultiEdit", "multi_edit"]);
|
|
139
|
+
let editCount = 0;
|
|
140
|
+
for (const [t, c] of Object.entries(tools)) {
|
|
141
|
+
if (editLike.has(t)) editCount += c;
|
|
142
|
+
}
|
|
143
|
+
const readCount = tools["Read"] || tools["read"] || 0;
|
|
144
|
+
if (editCount > 0) return "feature-development";
|
|
145
|
+
if (readCount > 0 && editCount === 0) return "code-review";
|
|
146
|
+
if (files.length > 0) return "general";
|
|
147
|
+
return "general";
|
|
148
|
+
}
|
|
149
|
+
|
|
40
150
|
function buildHandoffSummary(thread, reason) {
|
|
41
151
|
const parts = [];
|
|
42
152
|
const files = thread.files_touched || [];
|
|
@@ -67,7 +177,7 @@ function buildHandoffSummary(thread, reason) {
|
|
|
67
177
|
return parts.length ? parts.join(" | ") : "Session stopped -- no summary available";
|
|
68
178
|
}
|
|
69
179
|
|
|
70
|
-
function main() {
|
|
180
|
+
async function main() {
|
|
71
181
|
let hookInput = {};
|
|
72
182
|
try {
|
|
73
183
|
if (!process.stdin.isTTY) {
|
|
@@ -85,15 +195,53 @@ function main() {
|
|
|
85
195
|
const now = new Date().toISOString();
|
|
86
196
|
const reason = hookInput.reason || "";
|
|
87
197
|
|
|
198
|
+
const cloud = useCloud();
|
|
88
199
|
const memoryPath = getMemoryPath();
|
|
89
200
|
const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
|
|
90
201
|
|
|
202
|
+
if (cloud) {
|
|
203
|
+
const cloudThreads = await cloudReadThreads();
|
|
204
|
+
if (cloudThreads) memory.threads = cloudThreads;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Extract session data from audit trail and git (the key fix —
|
|
208
|
+
// previously this hook created bare threads with no context).
|
|
209
|
+
const auditData = extractSessionFromAudit(sessionId);
|
|
210
|
+
const auditFiles = auditData.files || [];
|
|
211
|
+
const gitFiles = getGitFilesTouched();
|
|
212
|
+
const allFiles = [...new Set([...auditFiles, ...gitFiles])];
|
|
213
|
+
const narrative = buildNarrativeFromAudit(auditData);
|
|
214
|
+
const workType = classifyWorkType(auditData);
|
|
215
|
+
|
|
91
216
|
const threads = memory.threads || [];
|
|
92
217
|
let found = false;
|
|
93
218
|
|
|
94
219
|
for (const thread of threads) {
|
|
95
220
|
if (thread.session_id === sessionId && (thread.status === "open" || thread.status === "stuck")) {
|
|
96
221
|
thread.last_activity_at = now;
|
|
222
|
+
// Enrich with extracted data
|
|
223
|
+
if (allFiles.length) {
|
|
224
|
+
thread.files_touched = [...new Set([...(thread.files_touched || []), ...allFiles])];
|
|
225
|
+
}
|
|
226
|
+
if (narrative && !thread.narrative) {
|
|
227
|
+
thread.narrative = narrative;
|
|
228
|
+
}
|
|
229
|
+
if (workType !== "general" || !thread.work_type) {
|
|
230
|
+
thread.work_type = workType;
|
|
231
|
+
}
|
|
232
|
+
// Merge audit tool counts
|
|
233
|
+
const auditTools = auditData.tools || {};
|
|
234
|
+
const prevCalls = thread.tool_calls || {};
|
|
235
|
+
for (const [tn, cnt] of Object.entries(auditTools)) {
|
|
236
|
+
prevCalls[tn] = (prevCalls[tn] || 0) + cnt;
|
|
237
|
+
}
|
|
238
|
+
thread.tool_calls = prevCalls;
|
|
239
|
+
// Store prompts if not already present
|
|
240
|
+
const storedPrompts = (auditData.prompts || []).slice(0, 20).map((p) => p.substring(0, 300));
|
|
241
|
+
if (storedPrompts.length && !(thread.prompts || []).length) {
|
|
242
|
+
thread.prompts = storedPrompts;
|
|
243
|
+
}
|
|
244
|
+
|
|
97
245
|
thread.handoff_summary = buildHandoffSummary(thread, reason);
|
|
98
246
|
if (reason) {
|
|
99
247
|
thread.last_note = reason.substring(0, 200);
|
|
@@ -104,6 +252,7 @@ function main() {
|
|
|
104
252
|
}
|
|
105
253
|
|
|
106
254
|
if (!found && sessionId !== "unknown") {
|
|
255
|
+
const storedPrompts = (auditData.prompts || []).slice(0, 20).map((p) => p.substring(0, 300));
|
|
107
256
|
const thread = {
|
|
108
257
|
id: crypto.randomUUID(),
|
|
109
258
|
user,
|
|
@@ -113,29 +262,46 @@ function main() {
|
|
|
113
262
|
created_at: now,
|
|
114
263
|
closed_at: now,
|
|
115
264
|
last_activity_at: now,
|
|
116
|
-
files_touched:
|
|
117
|
-
last_note: reason ? reason.substring(0, 200) : "Session stopped",
|
|
118
|
-
narrative
|
|
265
|
+
files_touched: allFiles,
|
|
266
|
+
last_note: reason ? reason.substring(0, 200) : (narrative || "Session stopped"),
|
|
267
|
+
narrative,
|
|
119
268
|
failed_approaches: [],
|
|
120
|
-
handoff_summary:
|
|
269
|
+
handoff_summary: "",
|
|
121
270
|
related_session_ids: [],
|
|
122
271
|
tool: detectTool(hookInput),
|
|
123
272
|
session_id: sessionId,
|
|
273
|
+
work_type: workType,
|
|
274
|
+
tool_calls: auditData.tools || {},
|
|
275
|
+
prompts: storedPrompts,
|
|
276
|
+
contributors: [user],
|
|
277
|
+
resume_history: [],
|
|
124
278
|
};
|
|
279
|
+
thread.handoff_summary = buildHandoffSummary(thread, reason);
|
|
125
280
|
threads.push(thread);
|
|
126
281
|
}
|
|
127
282
|
|
|
128
|
-
|
|
283
|
+
const auditEntry = {
|
|
129
284
|
type: "stop",
|
|
130
285
|
user,
|
|
131
286
|
ts: now,
|
|
132
287
|
session_id: sessionId,
|
|
133
288
|
reason: reason ? reason.substring(0, 200) : "stop_checkpoint",
|
|
134
|
-
}
|
|
289
|
+
};
|
|
290
|
+
appendAuditLine(auditEntry);
|
|
291
|
+
if (cloud) {
|
|
292
|
+
cloudAppendAudit(auditEntry).catch(() => {});
|
|
293
|
+
}
|
|
135
294
|
|
|
136
295
|
saveMemoryMerged(memoryPath, memory);
|
|
296
|
+
if (cloud) {
|
|
297
|
+
const threadToSync = threads.find(t => t.session_id === sessionId);
|
|
298
|
+
if (threadToSync) {
|
|
299
|
+
cloudWriteThread(threadToSync).catch(() => {});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
137
302
|
const reasonStr = reason ? reason.substring(0, 60) : "checkpoint";
|
|
138
|
-
const
|
|
303
|
+
const fileCount = allFiles.length;
|
|
304
|
+
const statusMsg = `[STICKY-NOTE] Session stopped - handoff saved (${reasonStr}, ${fileCount} file${fileCount !== 1 ? "s" : ""})`;
|
|
139
305
|
try {
|
|
140
306
|
process.stdout.write(JSON.stringify({ output: statusMsg }) + "\n");
|
|
141
307
|
} catch (_) {
|
|
@@ -144,8 +310,4 @@ function main() {
|
|
|
144
310
|
process.exit(0);
|
|
145
311
|
}
|
|
146
312
|
|
|
147
|
-
|
|
148
|
-
main();
|
|
149
|
-
} catch (_) {
|
|
150
|
-
safeExit();
|
|
151
|
-
}
|
|
313
|
+
main().catch(() => safeExit());
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# sticky-codex.sh — Codex wrapper for Sticky Note V2
|
|
3
|
-
#
|
|
4
|
-
# Captures Codex stdout/stderr to a temp session log.
|
|
5
|
-
# On exit: calls session-end.js with the transcript for
|
|
6
|
-
# narrative + failed_approaches extraction.
|
|
7
|
-
#
|
|
8
|
-
# Usage:
|
|
9
|
-
# sticky-codex [codex args...]
|
|
10
|
-
#
|
|
11
|
-
# Setup:
|
|
12
|
-
# npx sticky-note init --codex
|
|
13
|
-
# alias sticky-codex="/path/to/.claude/hooks/sticky-codex.sh"
|
|
14
|
-
#
|
|
15
|
-
# Limitation: Context is printed to terminal after Codex exits,
|
|
16
|
-
# not injected into the Codex window. Per-prompt surfacing not available.
|
|
17
|
-
|
|
18
|
-
set -euo pipefail
|
|
19
|
-
|
|
20
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
-
STICKY_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)/.sticky-note"
|
|
22
|
-
SESSION_ID="codex-$(date +%s)-$$"
|
|
23
|
-
TRANSCRIPT_FILE=$(mktemp "${TMPDIR:-/tmp}/sticky-codex-XXXXXX.log")
|
|
24
|
-
USER="${USER:-${USERNAME:-unknown}}"
|
|
25
|
-
|
|
26
|
-
# Ensure .sticky-note directory exists
|
|
27
|
-
mkdir -p "$STICKY_DIR"
|
|
28
|
-
|
|
29
|
-
# Print context from sticky-note before starting Codex
|
|
30
|
-
echo ""
|
|
31
|
-
echo "📌 Sticky Note — injecting context for Codex session $SESSION_ID"
|
|
32
|
-
echo ""
|
|
33
|
-
|
|
34
|
-
if [ -f "$STICKY_DIR/sticky-note.json" ]; then
|
|
35
|
-
node "$SCRIPT_DIR/session-start.js" <<EOF 2>/dev/null || true
|
|
36
|
-
{"session_id": "$SESSION_ID"}
|
|
37
|
-
EOF
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
echo "─────────────────────────────────────────────"
|
|
41
|
-
echo ""
|
|
42
|
-
|
|
43
|
-
# Run Codex, capturing output
|
|
44
|
-
codex "$@" 2>&1 | tee "$TRANSCRIPT_FILE"
|
|
45
|
-
CODEX_EXIT=$?
|
|
46
|
-
|
|
47
|
-
echo ""
|
|
48
|
-
echo "─────────────────────────────────────────────"
|
|
49
|
-
echo "📌 Sticky Note — processing Codex session..."
|
|
50
|
-
|
|
51
|
-
# Parse transcript for narrative + failed_approaches
|
|
52
|
-
PARSED=$(node "$SCRIPT_DIR/parse-transcript.js" "$TRANSCRIPT_FILE" 2>/dev/null || echo '{"narrative":"","failed_approaches":[]}')
|
|
53
|
-
|
|
54
|
-
# Call session-end.js with the transcript info + parsed data
|
|
55
|
-
node "$SCRIPT_DIR/session-end.js" <<EOF 2>/dev/null || true
|
|
56
|
-
{
|
|
57
|
-
"session_id": "$SESSION_ID",
|
|
58
|
-
"transcript_path": "$TRANSCRIPT_FILE",
|
|
59
|
-
"parsed_transcript": ${PARSED},
|
|
60
|
-
"hook_event_name": "sessionEnd",
|
|
61
|
-
"reason": "codex_exit"
|
|
62
|
-
}
|
|
63
|
-
EOF
|
|
64
|
-
|
|
65
|
-
# Cleanup transcript
|
|
66
|
-
rm -f "$TRANSCRIPT_FILE"
|
|
67
|
-
|
|
68
|
-
echo "[OK] Session $SESSION_ID recorded."
|
|
69
|
-
echo ""
|
|
70
|
-
|
|
71
|
-
exit $CODEX_EXIT
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sticky-codex.sh — Codex wrapper for Sticky Note V2
|
|
3
|
+
#
|
|
4
|
+
# Captures Codex stdout/stderr to a temp session log.
|
|
5
|
+
# On exit: calls session-end.js with the transcript for
|
|
6
|
+
# narrative + failed_approaches extraction.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# sticky-codex [codex args...]
|
|
10
|
+
#
|
|
11
|
+
# Setup:
|
|
12
|
+
# npx sticky-note init --codex
|
|
13
|
+
# alias sticky-codex="/path/to/.claude/hooks/sticky-codex.sh"
|
|
14
|
+
#
|
|
15
|
+
# Limitation: Context is printed to terminal after Codex exits,
|
|
16
|
+
# not injected into the Codex window. Per-prompt surfacing not available.
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
STICKY_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)/.sticky-note"
|
|
22
|
+
SESSION_ID="codex-$(date +%s)-$$"
|
|
23
|
+
TRANSCRIPT_FILE=$(mktemp "${TMPDIR:-/tmp}/sticky-codex-XXXXXX.log")
|
|
24
|
+
USER="${USER:-${USERNAME:-unknown}}"
|
|
25
|
+
|
|
26
|
+
# Ensure .sticky-note directory exists
|
|
27
|
+
mkdir -p "$STICKY_DIR"
|
|
28
|
+
|
|
29
|
+
# Print context from sticky-note before starting Codex
|
|
30
|
+
echo ""
|
|
31
|
+
echo "📌 Sticky Note — injecting context for Codex session $SESSION_ID"
|
|
32
|
+
echo ""
|
|
33
|
+
|
|
34
|
+
if [ -f "$STICKY_DIR/sticky-note.json" ]; then
|
|
35
|
+
node "$SCRIPT_DIR/session-start.js" <<EOF 2>/dev/null || true
|
|
36
|
+
{"session_id": "$SESSION_ID"}
|
|
37
|
+
EOF
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "─────────────────────────────────────────────"
|
|
41
|
+
echo ""
|
|
42
|
+
|
|
43
|
+
# Run Codex, capturing output
|
|
44
|
+
codex "$@" 2>&1 | tee "$TRANSCRIPT_FILE"
|
|
45
|
+
CODEX_EXIT=$?
|
|
46
|
+
|
|
47
|
+
echo ""
|
|
48
|
+
echo "─────────────────────────────────────────────"
|
|
49
|
+
echo "📌 Sticky Note — processing Codex session..."
|
|
50
|
+
|
|
51
|
+
# Parse transcript for narrative + failed_approaches
|
|
52
|
+
PARSED=$(node "$SCRIPT_DIR/parse-transcript.js" "$TRANSCRIPT_FILE" 2>/dev/null || echo '{"narrative":"","failed_approaches":[]}')
|
|
53
|
+
|
|
54
|
+
# Call session-end.js with the transcript info + parsed data
|
|
55
|
+
node "$SCRIPT_DIR/session-end.js" <<EOF 2>/dev/null || true
|
|
56
|
+
{
|
|
57
|
+
"session_id": "$SESSION_ID",
|
|
58
|
+
"transcript_path": "$TRANSCRIPT_FILE",
|
|
59
|
+
"parsed_transcript": ${PARSED},
|
|
60
|
+
"hook_event_name": "sessionEnd",
|
|
61
|
+
"reason": "codex_exit"
|
|
62
|
+
}
|
|
63
|
+
EOF
|
|
64
|
+
|
|
65
|
+
# Cleanup transcript
|
|
66
|
+
rm -f "$TRANSCRIPT_FILE"
|
|
67
|
+
|
|
68
|
+
echo "[OK] Session $SESSION_ID recorded."
|
|
69
|
+
echo ""
|
|
70
|
+
|
|
71
|
+
exit $CODEX_EXIT
|