sticky-note-cli 2.5.0 → 2.5.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 +42 -43
- package/bin/cli.js +60 -5
- package/package.json +1 -1
- package/templates/hooks/inject-context.js +2 -2
- package/templates/hooks/on-error.js +3 -2
- package/templates/hooks/on-stop.js +2 -1
- package/templates/hooks/session-end.js +2 -1
- package/templates/hooks/session-start.js +3 -2
- package/templates/hooks/sticky-utils.js +26 -0
- package/templates/merge-driver.js +89 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="https://www.npmjs.com/package/sticky-note"><img src="https://img.shields.io/npm/v/sticky-note.svg?style=flat-square&color=f59e0b" alt="npm version" /></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/sticky-note-cli"><img src="https://img.shields.io/npm/v/sticky-note-cli.svg?style=flat-square&color=f59e0b" alt="npm version" /></a>
|
|
13
13
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="MIT License" /></a>
|
|
14
14
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D16-brightgreen.svg?style=flat-square" alt="Node.js >= 16" /></a>
|
|
15
15
|
<a href="#supported-tools"><img src="https://img.shields.io/badge/works%20with-Claude%20%C2%B7%20Copilot%20%C2%B7%20Codex-8b5cf6?style=flat-square" alt="Works with Claude, Copilot, Codex" /></a>
|
|
@@ -30,24 +30,23 @@
|
|
|
30
30
|
|
|
31
31
|
## The Problem
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
I have seen many agent-to-agent to orchestration systems and frameworks, but I still like working with other people.
|
|
34
|
+
Other people that ALSO use agents to develop. I couldn't get my agent to understand what my friends agents were doing.
|
|
35
|
+
So we built Sticky Note.
|
|
36
36
|
|
|
37
37
|
## The Solution
|
|
38
38
|
|
|
39
39
|
Sticky Note captures what happened in each AI session (files touched, status,
|
|
40
40
|
narrative, failed approaches) and surfaces it to teammates automatically on
|
|
41
|
-
their next session
|
|
42
|
-
Just shared files in your repo.
|
|
41
|
+
their next session by relevance.
|
|
43
42
|
|
|
44
43
|
---
|
|
45
44
|
|
|
46
45
|
## What's New in V2.5
|
|
47
46
|
|
|
48
47
|
- **Smart injection (two-tier)**: Stuck threads injected eagerly at session start; all other threads injected lazily when you first touch a file they authored — via built-in git blame attribution
|
|
49
|
-
- **Thread resume (local)**: `npx sticky-note resume-thread --query "auth fix" --user alice` — natural language thread discovery with text similarity + file attribution ranking
|
|
50
|
-
- **Built-in attribution engine**: `npx sticky-note get-line-attribution --file src/auth.ts` — maps file lines to commit SHAs via git blame, resolves to threads with line ranges. No external dependencies.
|
|
48
|
+
- **Thread resume (local)**: `npx sticky-note-cli resume-thread --query "auth fix" --user alice` — natural language thread discovery with text similarity + file attribution ranking
|
|
49
|
+
- **Built-in attribution engine**: `npx sticky-note-cli get-line-attribution --file src/auth.ts` — maps file lines to commit SHAs via git blame, resolves to threads with line ranges. No external dependencies.
|
|
51
50
|
- **Git Notes storage**: Session attribution stored in `refs/notes/sticky-note` — survives rebase/amend
|
|
52
51
|
- **Three-tier SHA resolution**: Git Notes → Audit JSONL → File+date heuristic — attribution never breaks
|
|
53
52
|
- **PreToolUse hook**: New hook fires before each tool call for lazy injection with line-range detail
|
|
@@ -59,7 +58,7 @@ Just shared files in your repo.
|
|
|
59
58
|
- **Relevance scoring**: Context injected based on file overlap, branch match, and recency
|
|
60
59
|
- **Richer threads**: Narrative summaries, failed approaches, work type, activities
|
|
61
60
|
- **Tombstone expiry**: Old threads are automatically cleaned up via `gc`
|
|
62
|
-
- **Presence tracking**: See who's currently active with `npx sticky-note who`
|
|
61
|
+
- **Presence tracking**: See who's currently active with `npx sticky-note-cli who`
|
|
63
62
|
- **Codex support**: Wrapper script for post-session capture
|
|
64
63
|
- **Separate config**: Team settings in `sticky-note-config.json`
|
|
65
64
|
|
|
@@ -70,7 +69,7 @@ Just shared files in your repo.
|
|
|
70
69
|
### 1. Install
|
|
71
70
|
|
|
72
71
|
```bash
|
|
73
|
-
npx sticky-note init
|
|
72
|
+
npx sticky-note-cli init
|
|
74
73
|
```
|
|
75
74
|
|
|
76
75
|
This runs an interactive setup that:
|
|
@@ -111,7 +110,7 @@ that's always available as a static file.
|
|
|
111
110
|
|
|
112
111
|
### 1. Static Instruction Files (always available)
|
|
113
112
|
|
|
114
|
-
`npx sticky-note init` deploys AI instruction files that teach each tool
|
|
113
|
+
`npx sticky-note-cli init` deploys AI instruction files that teach each tool
|
|
115
114
|
how to interact with Sticky Note:
|
|
116
115
|
|
|
117
116
|
| File | Consumed by | Installed to |
|
|
@@ -121,7 +120,7 @@ how to interact with Sticky Note:
|
|
|
121
120
|
|
|
122
121
|
These files contain thread field references, status icons, resume
|
|
123
122
|
instructions, display formats, and query examples. They're wrapped in
|
|
124
|
-
`<!-- sticky-note:start/end -->` markers so `npx sticky-note update` can
|
|
123
|
+
`<!-- sticky-note:start/end -->` markers so `npx sticky-note-cli update` can
|
|
125
124
|
refresh them without overwriting your own content.
|
|
126
125
|
|
|
127
126
|
### 2. Session Start Hook (`session-start.js`)
|
|
@@ -169,13 +168,13 @@ Runs **before each tool call**. Lazy injection tier:
|
|
|
169
168
|
|
|
170
169
|
No external dependencies — uses built-in git blame.
|
|
171
170
|
|
|
172
|
-
### 4. Resume Flow (`npx sticky-note resume <id>`)
|
|
171
|
+
### 4. Resume Flow (`npx sticky-note-cli resume <id>`)
|
|
173
172
|
|
|
174
173
|
A manual trigger that enables **cross-tool handoff** (e.g., Claude Code →
|
|
175
174
|
Copilot CLI):
|
|
176
175
|
|
|
177
176
|
```
|
|
178
|
-
npx sticky-note resume <thread-id>
|
|
177
|
+
npx sticky-note-cli resume <thread-id>
|
|
179
178
|
```
|
|
180
179
|
|
|
181
180
|
1. Writes the thread UUID to a `.sticky-resume` signal file.
|
|
@@ -259,7 +258,7 @@ the thread's `handoff_summary` field.
|
|
|
259
258
|
open → stale (auto, after stale_days with no activity)
|
|
260
259
|
open → closed (on session end)
|
|
261
260
|
stuck → closed (on session end or manual)
|
|
262
|
-
closed → open (via `npx sticky-note resume <id>`)
|
|
261
|
+
closed → open (via `npx sticky-note-cli resume <id>`)
|
|
263
262
|
closed → expired (auto, tombstoned by gc after stale_days)
|
|
264
263
|
```
|
|
265
264
|
|
|
@@ -311,34 +310,34 @@ CLAUDE.md # AI instructions for Claude Code
|
|
|
311
310
|
## CLI Commands
|
|
312
311
|
|
|
313
312
|
```bash
|
|
314
|
-
npx sticky-note init # Interactive setup
|
|
315
|
-
npx sticky-note init --codex # Setup with Codex wrapper
|
|
316
|
-
npx sticky-note update # Update hook scripts (preserves data)
|
|
317
|
-
npx sticky-note status # Diagnostic report (includes attribution health)
|
|
318
|
-
npx sticky-note threads # List threads with status icons
|
|
319
|
-
npx sticky-note resume # List resumable threads
|
|
320
|
-
npx sticky-note resume <id> # Resume a previous thread
|
|
321
|
-
npx sticky-note resume --clear # Cancel active resume
|
|
322
|
-
npx sticky-note resume-thread # Smart resume: --query, --user, --file (V2.5)
|
|
323
|
-
npx sticky-note audit # Query merged audit trail (all users)
|
|
324
|
-
npx sticky-note who # Show active and recent team members
|
|
325
|
-
npx sticky-note switch <branch> # Safe branch switch (auto-stashes data)
|
|
326
|
-
npx sticky-note gc # Tombstone expired threads
|
|
327
|
-
npx sticky-note reset # Wipe all threads (--force, --keep-audit)
|
|
328
|
-
npx sticky-note get-line-attribution # File→thread attribution with line ranges (V2.5)
|
|
329
|
-
npx sticky-note checkpoint # Set work-topic checkpoint for attribution (V2.5)
|
|
330
|
-
npx sticky-note --version # Show version
|
|
331
|
-
npx sticky-note --help # Show help
|
|
313
|
+
npx sticky-note-cli init # Interactive setup
|
|
314
|
+
npx sticky-note-cli init --codex # Setup with Codex wrapper
|
|
315
|
+
npx sticky-note-cli update # Update hook scripts (preserves data)
|
|
316
|
+
npx sticky-note-cli status # Diagnostic report (includes attribution health)
|
|
317
|
+
npx sticky-note-cli threads # List threads with status icons
|
|
318
|
+
npx sticky-note-cli resume # List resumable threads
|
|
319
|
+
npx sticky-note-cli resume <id> # Resume a previous thread
|
|
320
|
+
npx sticky-note-cli resume --clear # Cancel active resume
|
|
321
|
+
npx sticky-note-cli resume-thread # Smart resume: --query, --user, --file (V2.5)
|
|
322
|
+
npx sticky-note-cli audit # Query merged audit trail (all users)
|
|
323
|
+
npx sticky-note-cli who # Show active and recent team members
|
|
324
|
+
npx sticky-note-cli switch <branch> # Safe branch switch (auto-stashes data)
|
|
325
|
+
npx sticky-note-cli gc # Tombstone expired threads
|
|
326
|
+
npx sticky-note-cli reset # Wipe all threads (--force, --keep-audit)
|
|
327
|
+
npx sticky-note-cli get-line-attribution # File→thread attribution with line ranges (V2.5)
|
|
328
|
+
npx sticky-note-cli checkpoint # Set work-topic checkpoint for attribution (V2.5)
|
|
329
|
+
npx sticky-note-cli --version # Show version
|
|
330
|
+
npx sticky-note-cli --help # Show help
|
|
332
331
|
```
|
|
333
332
|
|
|
334
333
|
### Audit Filters
|
|
335
334
|
|
|
336
335
|
```bash
|
|
337
|
-
npx sticky-note audit --user alice
|
|
338
|
-
npx sticky-note audit --file src/auth.ts
|
|
339
|
-
npx sticky-note audit --since 2025-01-01
|
|
340
|
-
npx sticky-note audit --session abc-123
|
|
341
|
-
npx sticky-note audit --limit 100
|
|
336
|
+
npx sticky-note-cli audit --user alice
|
|
337
|
+
npx sticky-note-cli audit --file src/auth.ts
|
|
338
|
+
npx sticky-note-cli audit --since 2025-01-01
|
|
339
|
+
npx sticky-note-cli audit --session abc-123
|
|
340
|
+
npx sticky-note-cli audit --limit 100
|
|
342
341
|
```
|
|
343
342
|
|
|
344
343
|
---
|
|
@@ -388,7 +387,7 @@ All tools call the same JavaScript hooks and share the same data files.
|
|
|
388
387
|
|
|
389
388
|
## Concurrent Usage & Merge Strategy
|
|
390
389
|
|
|
391
|
-
`npx sticky-note init` adds a `.gitattributes` rule:
|
|
390
|
+
`npx sticky-note-cli init` adds a `.gitattributes` rule:
|
|
392
391
|
|
|
393
392
|
```
|
|
394
393
|
.sticky-note/sticky-note.json merge=union
|
|
@@ -412,9 +411,9 @@ Use one of these approaches:
|
|
|
412
411
|
|
|
413
412
|
```bash
|
|
414
413
|
# Recommended: sticky-note CLI wrapper
|
|
415
|
-
npx sticky-note switch <branch>
|
|
414
|
+
npx sticky-note-cli switch <branch>
|
|
416
415
|
|
|
417
|
-
# Or: git alias (set up by npx sticky-note init)
|
|
416
|
+
# Or: git alias (set up by npx sticky-note-cli init)
|
|
418
417
|
git sw <branch>
|
|
419
418
|
```
|
|
420
419
|
|
|
@@ -434,13 +433,13 @@ A: `merge=union` in `.gitattributes` handles most cases automatically.
|
|
|
434
433
|
|
|
435
434
|
**Q: Can I close a thread manually?**
|
|
436
435
|
A: Edit `sticky-note.json` and change the thread's `status` to `"closed"`,
|
|
437
|
-
or run `npx sticky-note gc` to tombstone expired threads.
|
|
436
|
+
or run `npx sticky-note-cli gc` to tombstone expired threads.
|
|
438
437
|
|
|
439
438
|
**Q: Does this work offline?**
|
|
440
439
|
A: Yes. Everything is local until you `git push`.
|
|
441
440
|
|
|
442
441
|
**Q: How do I set up Codex?**
|
|
443
|
-
A: Run `npx sticky-note init --codex`, then alias the wrapper:
|
|
442
|
+
A: Run `npx sticky-note-cli init --codex`, then alias the wrapper:
|
|
444
443
|
`alias sticky-codex=".claude/hooks/sticky-codex.sh"`
|
|
445
444
|
|
|
446
445
|
---
|
package/bin/cli.js
CHANGED
|
@@ -537,21 +537,44 @@ async function cmdInit() {
|
|
|
537
537
|
|
|
538
538
|
// Add .gitattributes merge strategy
|
|
539
539
|
const gitattrsPath = path.join(process.cwd(), ".gitattributes");
|
|
540
|
-
const
|
|
540
|
+
const oldMergeRule = ".sticky-note/sticky-note.json merge=union";
|
|
541
|
+
const mergeRule = ".sticky-note/sticky-note.json merge=sticky-note";
|
|
541
542
|
let gitattrsContent = "";
|
|
542
543
|
if (fs.existsSync(gitattrsPath)) {
|
|
543
544
|
gitattrsContent = fs.readFileSync(gitattrsPath, "utf-8");
|
|
544
545
|
}
|
|
545
|
-
|
|
546
|
+
// Upgrade from merge=union to custom merge driver
|
|
547
|
+
if (gitattrsContent.includes(oldMergeRule)) {
|
|
548
|
+
gitattrsContent = gitattrsContent.replace(oldMergeRule, mergeRule);
|
|
549
|
+
fs.writeFileSync(gitattrsPath, gitattrsContent, "utf-8");
|
|
550
|
+
print(" [OK] .gitattributes upgraded (merge=union -> merge=sticky-note)");
|
|
551
|
+
} else if (!gitattrsContent.includes(mergeRule)) {
|
|
546
552
|
const addition = gitattrsContent.endsWith("\n") || gitattrsContent === ""
|
|
547
|
-
? `\n# Sticky Note -
|
|
548
|
-
: `\n\n# Sticky Note -
|
|
553
|
+
? `\n# Sticky Note - JSON-aware thread merge\n${mergeRule}\n`
|
|
554
|
+
: `\n\n# Sticky Note - JSON-aware thread merge\n${mergeRule}\n`;
|
|
549
555
|
fs.appendFileSync(gitattrsPath, addition);
|
|
550
|
-
print(" [OK] .gitattributes updated (merge=
|
|
556
|
+
print(" [OK] .gitattributes updated (merge=sticky-note)");
|
|
551
557
|
} else {
|
|
552
558
|
print(" ⏭️ .gitattributes already configured");
|
|
553
559
|
}
|
|
554
560
|
|
|
561
|
+
// Install merge driver script and git config
|
|
562
|
+
const mergeDriverSrc = path.join(TEMPLATES_DIR, "merge-driver.js");
|
|
563
|
+
const mergeDriverDest = path.join(stickyDir, "merge-driver.js");
|
|
564
|
+
copyFile(mergeDriverSrc, mergeDriverDest);
|
|
565
|
+
print(" [OK] .sticky-note/merge-driver.js installed");
|
|
566
|
+
try {
|
|
567
|
+
execFileSync("git", ["config", "merge.sticky-note.name", "Sticky Note thread merge"], {
|
|
568
|
+
cwd: process.cwd(), stdio: ["pipe", "pipe", "pipe"],
|
|
569
|
+
});
|
|
570
|
+
execFileSync("git", ["config", "merge.sticky-note.driver", "node .sticky-note/merge-driver.js %O %A %B"], {
|
|
571
|
+
cwd: process.cwd(), stdio: ["pipe", "pipe", "pipe"],
|
|
572
|
+
});
|
|
573
|
+
print(" [OK] git merge driver configured (merge.sticky-note)");
|
|
574
|
+
} catch (_) {
|
|
575
|
+
print(" ⏭️ Could not configure git merge driver (non-fatal)");
|
|
576
|
+
}
|
|
577
|
+
|
|
555
578
|
// Add git aliases for safe branch switching
|
|
556
579
|
try {
|
|
557
580
|
const swAlias = '!f() { npx sticky-note switch "$@"; }; f';
|
|
@@ -668,6 +691,38 @@ function cmdUpdate() {
|
|
|
668
691
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
669
692
|
}
|
|
670
693
|
|
|
694
|
+
// Update merge driver
|
|
695
|
+
const mergeDriverSrc = path.join(TEMPLATES_DIR, "merge-driver.js");
|
|
696
|
+
const mergeDriverDest = path.join(stickyDir, "merge-driver.js");
|
|
697
|
+
copyFile(mergeDriverSrc, mergeDriverDest);
|
|
698
|
+
print(" [OK] .sticky-note/merge-driver.js updated");
|
|
699
|
+
|
|
700
|
+
// Upgrade .gitattributes from merge=union to custom driver
|
|
701
|
+
const gitattrsPath = path.join(process.cwd(), ".gitattributes");
|
|
702
|
+
if (fs.existsSync(gitattrsPath)) {
|
|
703
|
+
let gitattrsContent = fs.readFileSync(gitattrsPath, "utf-8");
|
|
704
|
+
const oldRule = ".sticky-note/sticky-note.json merge=union";
|
|
705
|
+
const newRule = ".sticky-note/sticky-note.json merge=sticky-note";
|
|
706
|
+
if (gitattrsContent.includes(oldRule)) {
|
|
707
|
+
gitattrsContent = gitattrsContent.replace(oldRule, newRule);
|
|
708
|
+
fs.writeFileSync(gitattrsPath, gitattrsContent, "utf-8");
|
|
709
|
+
print(" [OK] .gitattributes upgraded (merge=union -> merge=sticky-note)");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Ensure git merge driver config is set
|
|
714
|
+
try {
|
|
715
|
+
execFileSync("git", ["config", "merge.sticky-note.name", "Sticky Note thread merge"], {
|
|
716
|
+
cwd: process.cwd(), stdio: ["pipe", "pipe", "pipe"],
|
|
717
|
+
});
|
|
718
|
+
execFileSync("git", ["config", "merge.sticky-note.driver", "node .sticky-note/merge-driver.js %O %A %B"], {
|
|
719
|
+
cwd: process.cwd(), stdio: ["pipe", "pipe", "pipe"],
|
|
720
|
+
});
|
|
721
|
+
print(" [OK] git merge driver configured (merge.sticky-note)");
|
|
722
|
+
} catch (_) {
|
|
723
|
+
// non-fatal
|
|
724
|
+
}
|
|
725
|
+
|
|
671
726
|
print(`\n ✨ Scripts updated to v${VERSION}`);
|
|
672
727
|
|
|
673
728
|
// Update instruction files (section-based: only replaces between markers)
|
package/package.json
CHANGED
|
@@ -50,7 +50,7 @@ try {
|
|
|
50
50
|
gitNotes = null;
|
|
51
51
|
}
|
|
52
52
|
const {
|
|
53
|
-
getMemoryPath, loadJson, saveJson, getUser, getBranch,
|
|
53
|
+
getMemoryPath, loadJson, saveJson, saveMemoryMerged, getUser, getBranch,
|
|
54
54
|
getResumeThreadId, findThreadById, getSessionId,
|
|
55
55
|
appendAuditLine, detectTool, getConfigPath,
|
|
56
56
|
isThreadInjected, markThreadInjected, normalizeSep,
|
|
@@ -391,7 +391,7 @@ function main() {
|
|
|
391
391
|
|
|
392
392
|
if (memoryDirty) {
|
|
393
393
|
try {
|
|
394
|
-
|
|
394
|
+
saveMemoryMerged(getMemoryPath(), memory);
|
|
395
395
|
} catch (_) {
|
|
396
396
|
// ignore
|
|
397
397
|
}
|
|
@@ -19,12 +19,13 @@ function _safeExit() {
|
|
|
19
19
|
process.exit(0);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
let getMemoryPath, loadJson, saveJson, appendAuditLine, getUser, detectTool, getSessionId;
|
|
22
|
+
let getMemoryPath, loadJson, saveJson, saveMemoryMerged, appendAuditLine, getUser, detectTool, getSessionId;
|
|
23
23
|
try {
|
|
24
24
|
({
|
|
25
25
|
getMemoryPath,
|
|
26
26
|
loadJson,
|
|
27
27
|
saveJson,
|
|
28
|
+
saveMemoryMerged,
|
|
28
29
|
appendAuditLine,
|
|
29
30
|
getUser,
|
|
30
31
|
detectTool,
|
|
@@ -118,7 +119,7 @@ function main() {
|
|
|
118
119
|
tool: toolName,
|
|
119
120
|
});
|
|
120
121
|
|
|
121
|
-
|
|
122
|
+
saveMemoryMerged(memoryPath, memory);
|
|
122
123
|
const statusMsg = `[STICKY-NOTE] Marked thread as STUCK - ${errorMsg.substring(0, 80)}`;
|
|
123
124
|
try {
|
|
124
125
|
process.stdout.write(JSON.stringify({ output: statusMsg }) + "\n");
|
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
getMemoryPath,
|
|
30
30
|
loadJson,
|
|
31
31
|
saveJson,
|
|
32
|
+
saveMemoryMerged,
|
|
32
33
|
appendAuditLine,
|
|
33
34
|
getUser,
|
|
34
35
|
getBranch,
|
|
@@ -132,7 +133,7 @@ function main() {
|
|
|
132
133
|
reason: reason ? reason.substring(0, 200) : "stop_checkpoint",
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
saveMemoryMerged(memoryPath, memory);
|
|
136
137
|
const reasonStr = reason ? reason.substring(0, 60) : "checkpoint";
|
|
137
138
|
const statusMsg = `[STICKY-NOTE] Session stopped - handoff saved (${reasonStr})`;
|
|
138
139
|
try {
|
|
@@ -36,6 +36,7 @@ const {
|
|
|
36
36
|
getAllAuditPaths,
|
|
37
37
|
loadJson,
|
|
38
38
|
saveJson,
|
|
39
|
+
saveMemoryMerged,
|
|
39
40
|
appendAuditLine,
|
|
40
41
|
getUser,
|
|
41
42
|
getBranch,
|
|
@@ -932,7 +933,7 @@ function main() {
|
|
|
932
933
|
clearHeadFile();
|
|
933
934
|
clearInjectedSet();
|
|
934
935
|
}
|
|
935
|
-
|
|
936
|
+
saveMemoryMerged(memoryPath, memory);
|
|
936
937
|
|
|
937
938
|
const fileCount = filesTouched.length;
|
|
938
939
|
const commitCount = commitShas.length;
|
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
migrateAuditAndPresence,
|
|
35
35
|
loadJson,
|
|
36
36
|
saveJson,
|
|
37
|
+
saveMemoryMerged,
|
|
37
38
|
appendAuditLine,
|
|
38
39
|
getUser,
|
|
39
40
|
getSessionId,
|
|
@@ -270,7 +271,7 @@ function main() {
|
|
|
270
271
|
resumed_from: prevSession,
|
|
271
272
|
});
|
|
272
273
|
resumedThread.resume_chain = chain;
|
|
273
|
-
|
|
274
|
+
saveMemoryMerged(memoryPath, memory);
|
|
274
275
|
}
|
|
275
276
|
}
|
|
276
277
|
|
|
@@ -293,7 +294,7 @@ function main() {
|
|
|
293
294
|
session_id: sessionId,
|
|
294
295
|
});
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
saveMemoryMerged(memoryPath, memory);
|
|
297
298
|
|
|
298
299
|
// ── Assemble output ───────────────────────────────────
|
|
299
300
|
const parts = [];
|
|
@@ -205,6 +205,31 @@ function saveJson(filePath, data) {
|
|
|
205
205
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Merge-on-write for the memory file. Re-reads the file from disk before
|
|
210
|
+
* writing and preserves any threads (by ID) that exist on disk but not in
|
|
211
|
+
* the in-memory copy. This prevents concurrent sessions or un-pulled commits
|
|
212
|
+
* from silently dropping other users' threads.
|
|
213
|
+
*/
|
|
214
|
+
function saveMemoryMerged(memoryPath, memory) {
|
|
215
|
+
try {
|
|
216
|
+
const onDisk = loadJson(memoryPath, { threads: [] });
|
|
217
|
+
const onDiskThreads = Array.isArray(onDisk.threads) ? onDisk.threads : [];
|
|
218
|
+
const inMemoryThreads = Array.isArray(memory.threads) ? memory.threads : [];
|
|
219
|
+
const inMemoryIds = new Set(inMemoryThreads.map((t) => t.id));
|
|
220
|
+
|
|
221
|
+
for (const diskThread of onDiskThreads) {
|
|
222
|
+
if (diskThread.id && !inMemoryIds.has(diskThread.id)) {
|
|
223
|
+
memory.threads.push(diskThread);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (_) {
|
|
227
|
+
// If re-read fails, proceed with what we have
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
saveJson(memoryPath, memory);
|
|
231
|
+
}
|
|
232
|
+
|
|
208
233
|
function appendAuditLine(entry) {
|
|
209
234
|
const filePath = getUserAuditPath();
|
|
210
235
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -618,6 +643,7 @@ module.exports = {
|
|
|
618
643
|
clearResumeSignal,
|
|
619
644
|
loadJson,
|
|
620
645
|
saveJson,
|
|
646
|
+
saveMemoryMerged,
|
|
621
647
|
appendAuditLine,
|
|
622
648
|
getUser,
|
|
623
649
|
detectTool,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git custom merge driver for sticky-note.json
|
|
4
|
+
*
|
|
5
|
+
* Merges threads by ID instead of line-based union merge.
|
|
6
|
+
* For threads present in both sides, the version with the latest
|
|
7
|
+
* last_activity_at wins. Threads unique to either side are preserved.
|
|
8
|
+
*
|
|
9
|
+
* Usage (called by git automatically):
|
|
10
|
+
* node .sticky-note/merge-driver.js %O %A %B
|
|
11
|
+
*
|
|
12
|
+
* %O = ancestor, %A = ours (result written here), %B = theirs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
|
|
17
|
+
function loadJson(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
20
|
+
} catch (_) {
|
|
21
|
+
return { version: "2", project: "", threads: [] };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getTimestamp(thread) {
|
|
26
|
+
const ts = thread.last_activity_at || thread.closed_at || thread.created_at || "";
|
|
27
|
+
if (!ts) return 0;
|
|
28
|
+
const t = new Date(ts).getTime();
|
|
29
|
+
return isNaN(t) ? 0 : t;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mergeThreads(ancestor, ours, theirs) {
|
|
33
|
+
const ancestorMap = new Map((ancestor.threads || []).map((t) => [t.id, t]));
|
|
34
|
+
const oursMap = new Map((ours.threads || []).map((t) => [t.id, t]));
|
|
35
|
+
const theirsMap = new Map((theirs.threads || []).map((t) => [t.id, t]));
|
|
36
|
+
|
|
37
|
+
const allIds = new Set([...oursMap.keys(), ...theirsMap.keys()]);
|
|
38
|
+
const merged = [];
|
|
39
|
+
|
|
40
|
+
for (const id of allIds) {
|
|
41
|
+
const inOurs = oursMap.has(id);
|
|
42
|
+
const inTheirs = theirsMap.has(id);
|
|
43
|
+
const inAncestor = ancestorMap.has(id);
|
|
44
|
+
|
|
45
|
+
if (inOurs && inTheirs) {
|
|
46
|
+
// Both sides have it — pick the most recently active version
|
|
47
|
+
const o = oursMap.get(id);
|
|
48
|
+
const t = theirsMap.get(id);
|
|
49
|
+
merged.push(getTimestamp(o) >= getTimestamp(t) ? o : t);
|
|
50
|
+
} else if (inOurs && !inTheirs) {
|
|
51
|
+
// Only in ours — keep unless theirs deliberately removed it
|
|
52
|
+
if (inAncestor) {
|
|
53
|
+
// Was in ancestor, theirs removed it — respect the deletion
|
|
54
|
+
} else {
|
|
55
|
+
merged.push(oursMap.get(id));
|
|
56
|
+
}
|
|
57
|
+
} else if (!inOurs && inTheirs) {
|
|
58
|
+
// Only in theirs — keep unless ours deliberately removed it
|
|
59
|
+
if (inAncestor) {
|
|
60
|
+
// Was in ancestor, ours removed it — respect the deletion
|
|
61
|
+
} else {
|
|
62
|
+
merged.push(theirsMap.get(id));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Main ──────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const [ancestorPath, oursPath, theirsPath] = process.argv.slice(2);
|
|
73
|
+
|
|
74
|
+
if (!ancestorPath || !oursPath || !theirsPath) {
|
|
75
|
+
process.stderr.write("Usage: merge-driver.js <ancestor> <ours> <theirs>\n");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ancestor = loadJson(ancestorPath);
|
|
80
|
+
const ours = loadJson(oursPath);
|
|
81
|
+
const theirs = loadJson(theirsPath);
|
|
82
|
+
|
|
83
|
+
const mergedThreads = mergeThreads(ancestor, ours, theirs);
|
|
84
|
+
|
|
85
|
+
// Use ours as the base for non-thread fields (version, project, etc.)
|
|
86
|
+
const result = { ...ours, threads: mergedThreads };
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(oursPath, JSON.stringify(result, null, 2) + "\n", "utf-8");
|
|
89
|
+
process.exit(0);
|