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 CHANGED
@@ -21,7 +21,7 @@ const { execSync, execFileSync } = require("child_process");
21
21
  // Constants
22
22
  // ──────────────────────────────────────────────
23
23
 
24
- const VERSION = "2.5.2";
24
+ const VERSION = require("../package.json").version;
25
25
 
26
26
  const HOOK_FILES = [
27
27
  "sticky-utils.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "2.5.2",
3
+ "version": "2.5.4",
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": {
@@ -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: buildHandoffSummary({}, reason),
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
- appendAuditLine({
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 statusMsg = `[STICKY-NOTE] Session stopped - handoff saved (${reasonStr})`;
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
- try {
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