sticky-note-cli 2.9.1 → 3.0.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.
- package/README.md +5 -1
- package/bin/cli.js +508 -37
- package/bin/mcp-server.js +3 -3
- package/package.json +4 -1
- package/templates/CLAUDE.md +1 -0
- package/templates/copilot-instructions.md +1 -0
- package/templates/gitignore-additions.txt +4 -0
- package/templates/hooks/data-branch.js +256 -0
- package/templates/hooks/inject-context.js +14 -1
- package/templates/hooks/on-stop.js +10 -2
- package/templates/hooks/session-end.js +55 -0
- package/templates/hooks/session-start.js +208 -3
- package/templates/hooks/sticky-utils.js +112 -2
- package/templates/hooks.json +12 -12
- package/templates/settings.json +7 -7
- package/templates/sticky-note-config.json +2 -1
- package/templates/sticky-note.json +5 -5
package/bin/mcp-server.js
CHANGED
|
@@ -234,7 +234,7 @@ function toolGetPresence() {
|
|
|
234
234
|
const active = entries
|
|
235
235
|
.filter(([_user, data]) => {
|
|
236
236
|
const lastSeen = new Date(data.last_seen || 0);
|
|
237
|
-
return now - lastSeen <
|
|
237
|
+
return now - lastSeen < 60 * 60 * 1000;
|
|
238
238
|
})
|
|
239
239
|
.map(([user, data]) => ({
|
|
240
240
|
user,
|
|
@@ -298,7 +298,7 @@ function toolCheckOverlaps(params) {
|
|
|
298
298
|
for (const [user, data] of Object.entries(presence)) {
|
|
299
299
|
if (user === currentUser) continue;
|
|
300
300
|
const lastSeen = new Date(data.last_seen || 0);
|
|
301
|
-
if (now - lastSeen >
|
|
301
|
+
if (now - lastSeen > 60 * 60 * 1000) continue;
|
|
302
302
|
|
|
303
303
|
const presenceFiles = (data.active_files || []).map((f) =>
|
|
304
304
|
f.replace(/\\/g, "/")
|
|
@@ -576,7 +576,7 @@ const TOOLS = {
|
|
|
576
576
|
},
|
|
577
577
|
get_presence: {
|
|
578
578
|
description:
|
|
579
|
-
"Get current developer presence — who is actively working and on which files. Only shows users seen in the last
|
|
579
|
+
"Get current developer presence — who is actively working and on which files. Only shows users seen in the last hour.",
|
|
580
580
|
inputSchema: {
|
|
581
581
|
type: "object",
|
|
582
582
|
properties: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sticky-note-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
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": {
|
|
@@ -32,5 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "node test/smoke.test.js"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"sticky-note-cli": "^2.9.1"
|
|
35
38
|
}
|
|
36
39
|
}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -159,6 +159,7 @@ Example format:
|
|
|
159
159
|
- `.sticky-note/.sticky-head` — transient HEAD snapshot
|
|
160
160
|
- `.sticky-note/.sticky-injected` — transient injection tracking (V2.5)
|
|
161
161
|
- `.sticky-note/.sticky-active-resume` — transient active resume marker (V2.5)
|
|
162
|
+
- `.sticky-note/.sticky-banner-shown` — transient startup banner dedup
|
|
162
163
|
|
|
163
164
|
When a session ends or the user asks to commit, **always include
|
|
164
165
|
`sticky-note.json` and the `audit/` and `presence/` directories**
|
|
@@ -162,6 +162,7 @@ Example format:
|
|
|
162
162
|
- `.sticky-note/.sticky-head` — transient HEAD snapshot
|
|
163
163
|
- `.sticky-note/.sticky-injected` — transient injection tracking (V2.5)
|
|
164
164
|
- `.sticky-note/.sticky-active-resume` — transient active resume marker (V2.5)
|
|
165
|
+
- `.sticky-note/.sticky-banner-shown` — transient startup banner dedup
|
|
165
166
|
|
|
166
167
|
When a session ends or the user asks to commit, **always include
|
|
167
168
|
`sticky-note.json` and the `audit/` and `presence/` directories**
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# Sticky Note - data moved to sticky-note/data branch (.git/sticky-note/ cache)
|
|
2
|
+
.sticky-note/
|
|
3
|
+
|
|
1
4
|
# Sticky Note - local overrides
|
|
2
5
|
.claude/settings.local.json
|
|
3
6
|
|
|
@@ -8,6 +11,7 @@
|
|
|
8
11
|
.sticky-note/.sticky-injected
|
|
9
12
|
.sticky-note/.sticky-active-resume
|
|
10
13
|
.sticky-note/.sticky-checkpoint
|
|
14
|
+
.sticky-note/.sticky-banner-shown
|
|
11
15
|
|
|
12
16
|
# Sticky Note - debug log
|
|
13
17
|
.sticky-note/hook-debug.log
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* data-branch.js — Git plumbing for the sticky-note/data orphan branch.
|
|
5
|
+
*
|
|
6
|
+
* Reads/writes files via git plumbing (hash-object, update-index, write-tree,
|
|
7
|
+
* commit-tree, update-ref). No working-tree checkout is ever performed.
|
|
8
|
+
* All operations are synchronous; callers receive result objects on failure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const os = require("os");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const crypto = require("crypto");
|
|
15
|
+
const { execFileSync } = require("child_process");
|
|
16
|
+
|
|
17
|
+
const DATA_BRANCH = "sticky-note/data";
|
|
18
|
+
const DATA_REF = "refs/heads/" + DATA_BRANCH;
|
|
19
|
+
|
|
20
|
+
const GIT_OPTS = { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] };
|
|
21
|
+
|
|
22
|
+
// ── Git helpers ───────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function getDefaultRemote() {
|
|
25
|
+
try {
|
|
26
|
+
const out = execFileSync("git", ["remote"], {
|
|
27
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
}).trim();
|
|
29
|
+
return out.split(/\r?\n/)[0] || null;
|
|
30
|
+
} catch (_) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Returns file content from a git ref as a string, or null if not found. */
|
|
36
|
+
function readFileFromBranch(ref, filePath) {
|
|
37
|
+
try {
|
|
38
|
+
return execFileSync("git", ["show", ref + ":" + filePath], GIT_OPTS);
|
|
39
|
+
} catch (_) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Commit { relativePath: content } to a branch using git plumbing.
|
|
46
|
+
* Does not touch the working tree or the main index. Returns the new commit SHA.
|
|
47
|
+
*/
|
|
48
|
+
function commitFilesToBranch(branchName, fileMap) {
|
|
49
|
+
const branchRef = "refs/heads/" + branchName;
|
|
50
|
+
const tmpIndex = path.join(
|
|
51
|
+
os.tmpdir(),
|
|
52
|
+
"sticky-idx-" + process.pid + "-" + crypto.randomBytes(4).toString("hex")
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
let parentSha = null;
|
|
57
|
+
try {
|
|
58
|
+
parentSha = execFileSync("git", ["rev-parse", "--verify", branchRef], {
|
|
59
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
60
|
+
}).trim();
|
|
61
|
+
} catch (_) {
|
|
62
|
+
// Branch doesn't exist yet — first commit creates it as orphan
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const indexEnv = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
66
|
+
|
|
67
|
+
if (parentSha) {
|
|
68
|
+
execFileSync("git", ["read-tree", parentSha], {
|
|
69
|
+
timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
70
|
+
env: indexEnv,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const [filePath, content] of Object.entries(fileMap)) {
|
|
75
|
+
const blobSha = execFileSync("git", ["hash-object", "-w", "--stdin"], {
|
|
76
|
+
input: content,
|
|
77
|
+
encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
}).trim();
|
|
79
|
+
|
|
80
|
+
execFileSync(
|
|
81
|
+
"git",
|
|
82
|
+
["update-index", "--add", "--cacheinfo", "100644," + blobSha + "," + filePath],
|
|
83
|
+
{ timeout: 5000, stdio: ["pipe", "pipe", "pipe"], env: indexEnv }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const treeSha = execFileSync("git", ["write-tree"], {
|
|
88
|
+
encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
+
env: indexEnv,
|
|
90
|
+
}).trim();
|
|
91
|
+
|
|
92
|
+
const commitArgs = ["commit-tree", treeSha, "-m", "chore(sticky-note): sync thread data"];
|
|
93
|
+
if (parentSha) {
|
|
94
|
+
commitArgs.push("-p", parentSha);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const commitSha = execFileSync("git", commitArgs, GIT_OPTS).trim();
|
|
98
|
+
|
|
99
|
+
execFileSync("git", ["update-ref", branchRef, commitSha], {
|
|
100
|
+
timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return commitSha;
|
|
104
|
+
} finally {
|
|
105
|
+
try { fs.unlinkSync(tmpIndex); } catch (_) {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Fetch ─────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Fetch the remote data branch into a local remote-tracking ref.
|
|
113
|
+
* Returns { ok, sha, error, remoteRef }.
|
|
114
|
+
*/
|
|
115
|
+
function fetchDataBranch(remote, branchName) {
|
|
116
|
+
branchName = branchName || DATA_BRANCH;
|
|
117
|
+
remote = remote || getDefaultRemote();
|
|
118
|
+
if (!remote) return { ok: false, sha: null, error: "no remote configured", remoteRef: null };
|
|
119
|
+
|
|
120
|
+
const remoteRef = "refs/remotes/" + remote + "/" + branchName;
|
|
121
|
+
try {
|
|
122
|
+
execFileSync(
|
|
123
|
+
"git",
|
|
124
|
+
["fetch", remote, branchName + ":" + remoteRef],
|
|
125
|
+
{ timeout: 10000, stdio: ["pipe", "pipe", "pipe"] }
|
|
126
|
+
);
|
|
127
|
+
let sha = null;
|
|
128
|
+
try {
|
|
129
|
+
sha = execFileSync("git", ["rev-parse", "--verify", remoteRef], {
|
|
130
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
131
|
+
}).trim();
|
|
132
|
+
} catch (_) {}
|
|
133
|
+
return { ok: true, sha, error: null, remoteRef };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return { ok: false, sha: null, error: err.message, remoteRef };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Push with retry ────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Push the local data branch to the remote, retrying on rejection.
|
|
143
|
+
* On non-fast-forward rejection, fetches and merges before retrying.
|
|
144
|
+
* Returns { ok, error }.
|
|
145
|
+
*/
|
|
146
|
+
function pushDataBranch(remote, branchName, maxRetries, localMemPath, loadJsonFn, saveJsonFn) {
|
|
147
|
+
branchName = branchName || DATA_BRANCH;
|
|
148
|
+
remote = remote || getDefaultRemote();
|
|
149
|
+
maxRetries = maxRetries || 3;
|
|
150
|
+
if (!remote) return { ok: false, error: "no remote configured" };
|
|
151
|
+
|
|
152
|
+
const pushSpec = DATA_REF + ":" + DATA_REF;
|
|
153
|
+
|
|
154
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
155
|
+
try {
|
|
156
|
+
execFileSync("git", ["push", remote, pushSpec], {
|
|
157
|
+
timeout: 30000, stdio: ["pipe", "pipe", "pipe"],
|
|
158
|
+
});
|
|
159
|
+
return { ok: true, error: null };
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (attempt === maxRetries) {
|
|
162
|
+
return { ok: false, error: err.message };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fetch, merge, and re-commit so the next push attempt advances the ref
|
|
166
|
+
try {
|
|
167
|
+
const fetchResult = fetchDataBranch(remote, branchName);
|
|
168
|
+
if (fetchResult.ok && fetchResult.remoteRef && localMemPath) {
|
|
169
|
+
const remoteContent = readFileFromBranch(fetchResult.remoteRef, "sticky-note.json");
|
|
170
|
+
if (remoteContent) {
|
|
171
|
+
mergeAndSaveFromRemote(localMemPath, remoteContent, loadJsonFn, saveJsonFn);
|
|
172
|
+
const mergedContent = fs.readFileSync(localMemPath, "utf-8");
|
|
173
|
+
commitFilesToBranch(branchName, { "sticky-note.json": mergedContent });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (_) {}
|
|
177
|
+
|
|
178
|
+
// Brief exponential backoff (busy-wait — synchronous context)
|
|
179
|
+
const end = Date.now() + Math.pow(2, attempt) * 300;
|
|
180
|
+
while (Date.now() < end) {}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { ok: false, error: "max retries exceeded" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Thread merge ──────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
function _mostRecentTimestamp(thread) {
|
|
189
|
+
return thread.last_activity_at || thread.updated_at || thread.created_at || "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Merge two thread arrays, preferring the more-recently-active copy
|
|
194
|
+
* when both contain the same thread ID.
|
|
195
|
+
*/
|
|
196
|
+
function mergeThreadArrays(localThreads, remoteThreads) {
|
|
197
|
+
const merged = new Map();
|
|
198
|
+
|
|
199
|
+
for (const t of (localThreads || [])) {
|
|
200
|
+
if (t && t.id) merged.set(t.id, t);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const t of (remoteThreads || [])) {
|
|
204
|
+
if (!t || !t.id) continue;
|
|
205
|
+
const existing = merged.get(t.id);
|
|
206
|
+
if (!existing || _mostRecentTimestamp(t) > _mostRecentTimestamp(existing)) {
|
|
207
|
+
merged.set(t.id, t);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return Array.from(merged.values());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const EMPTY_MEMORY = { version: "2", project: "", threads: [] };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Merge remote sticky-note.json content into the local memory file.
|
|
218
|
+
* Uses loadJsonFn/saveJsonFn when provided, otherwise reads/writes directly.
|
|
219
|
+
*/
|
|
220
|
+
function mergeAndSaveFromRemote(localMemPath, remoteContent, loadJsonFn, saveJsonFn) {
|
|
221
|
+
let remoteMemory;
|
|
222
|
+
try {
|
|
223
|
+
remoteMemory = JSON.parse(remoteContent);
|
|
224
|
+
} catch (_) {
|
|
225
|
+
return; // corrupt remote — skip
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const localMemory = loadJsonFn
|
|
229
|
+
? loadJsonFn(localMemPath, { ...EMPTY_MEMORY })
|
|
230
|
+
: { ...EMPTY_MEMORY };
|
|
231
|
+
|
|
232
|
+
const localThreads = Array.isArray(localMemory.threads) ? localMemory.threads.filter(Boolean) : [];
|
|
233
|
+
const remoteThreads = Array.isArray(remoteMemory.threads) ? remoteMemory.threads.filter(Boolean) : [];
|
|
234
|
+
localMemory.threads = mergeThreadArrays(localThreads, remoteThreads);
|
|
235
|
+
|
|
236
|
+
if (saveJsonFn) {
|
|
237
|
+
saveJsonFn(localMemPath, localMemory);
|
|
238
|
+
} else {
|
|
239
|
+
fs.mkdirSync(path.dirname(localMemPath), { recursive: true });
|
|
240
|
+
fs.writeFileSync(localMemPath, JSON.stringify(localMemory, null, 2) + "\n", "utf-8");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Exports ───────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
DATA_BRANCH,
|
|
248
|
+
DATA_REF,
|
|
249
|
+
getDefaultRemote,
|
|
250
|
+
readFileFromBranch,
|
|
251
|
+
commitFilesToBranch,
|
|
252
|
+
fetchDataBranch,
|
|
253
|
+
pushDataBranch,
|
|
254
|
+
mergeThreadArrays,
|
|
255
|
+
mergeAndSaveFromRemote,
|
|
256
|
+
};
|
|
@@ -39,10 +39,20 @@ function _safeExit() {
|
|
|
39
39
|
let utils;
|
|
40
40
|
try {
|
|
41
41
|
utils = require("./sticky-utils.js");
|
|
42
|
-
} catch (
|
|
42
|
+
} catch (err) {
|
|
43
|
+
try {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`[STICKY-NOTE] UserPromptSubmit hook error: failed to load sticky-utils.js — ${err.message}\n` +
|
|
46
|
+
`[STICKY-NOTE] hint: re-run \`npx sticky-note init\` to restore .claude/hooks/.\n`
|
|
47
|
+
);
|
|
48
|
+
} catch (_) { /* ignore */ }
|
|
43
49
|
_safeExit();
|
|
44
50
|
}
|
|
45
51
|
|
|
52
|
+
// Issue #13: also self-heal from this hook (Copilot CLI doesn't fire
|
|
53
|
+
// SessionStart, so inject-context may be the first hook to run successfully).
|
|
54
|
+
try { utils.selfHealHookPaths && utils.selfHealHookPaths(); } catch (_) { /* non-fatal */ }
|
|
55
|
+
|
|
46
56
|
let gitNotes;
|
|
47
57
|
try {
|
|
48
58
|
gitNotes = require("./sticky-git-notes.js");
|
|
@@ -626,6 +636,9 @@ if (require.main === module) {
|
|
|
626
636
|
try {
|
|
627
637
|
main();
|
|
628
638
|
} catch (exc) {
|
|
639
|
+
try {
|
|
640
|
+
utils.reportHookError("UserPromptSubmit", exc);
|
|
641
|
+
} catch (_) { /* ignore */ }
|
|
629
642
|
try {
|
|
630
643
|
appendAuditLine({
|
|
631
644
|
type: "inject_result",
|
|
@@ -22,7 +22,13 @@ function safeExit() {
|
|
|
22
22
|
let utils;
|
|
23
23
|
try {
|
|
24
24
|
utils = require("./sticky-utils.js");
|
|
25
|
-
} catch (
|
|
25
|
+
} catch (err) {
|
|
26
|
+
try {
|
|
27
|
+
process.stderr.write(
|
|
28
|
+
`[STICKY-NOTE] Stop hook error: failed to load sticky-utils.js — ${err.message}\n` +
|
|
29
|
+
`[STICKY-NOTE] hint: re-run \`npx sticky-note init\` to restore .claude/hooks/.\n`
|
|
30
|
+
);
|
|
31
|
+
} catch (_) { /* ignore */ }
|
|
26
32
|
safeExit();
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -285,6 +291,8 @@ async function main() {
|
|
|
285
291
|
}
|
|
286
292
|
|
|
287
293
|
main().catch((err) => {
|
|
288
|
-
try { utils.
|
|
294
|
+
try { utils.reportHookError("Stop", err); } catch (_) {
|
|
295
|
+
try { utils.logHookError("on-stop", err); } catch (_) {}
|
|
296
|
+
}
|
|
289
297
|
safeExit();
|
|
290
298
|
});
|
|
@@ -29,9 +29,14 @@ try {
|
|
|
29
29
|
_safeExit();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
let dataBranch = null;
|
|
33
|
+
try { dataBranch = require("./data-branch.js"); } catch (_) {}
|
|
34
|
+
|
|
32
35
|
const {
|
|
33
36
|
getMemoryPath,
|
|
34
37
|
getConfigPath,
|
|
38
|
+
getAuditDir,
|
|
39
|
+
getPresenceDir,
|
|
35
40
|
getUserPresencePath,
|
|
36
41
|
getAllAuditPaths,
|
|
37
42
|
loadJson,
|
|
@@ -724,6 +729,53 @@ function clearPresence(user) {
|
|
|
724
729
|
}
|
|
725
730
|
}
|
|
726
731
|
|
|
732
|
+
// ── Data-branch commit/push ───────────────────────────────
|
|
733
|
+
|
|
734
|
+
/** Collect files from a directory matching an extension into fileMap. */
|
|
735
|
+
function _collectDirFiles(dir, ext, prefix, fileMap) {
|
|
736
|
+
if (!dir || !fs.existsSync(dir)) return;
|
|
737
|
+
for (const file of fs.readdirSync(dir)) {
|
|
738
|
+
if (file.endsWith(ext)) {
|
|
739
|
+
fileMap[prefix + file] = fs.readFileSync(path.join(dir, file), "utf-8");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function commitAndPushDataBranch() {
|
|
745
|
+
if (!dataBranch) return;
|
|
746
|
+
try {
|
|
747
|
+
const fileMap = {};
|
|
748
|
+
const memPath = getMemoryPath();
|
|
749
|
+
if (fs.existsSync(memPath)) {
|
|
750
|
+
fileMap["sticky-note.json"] = fs.readFileSync(memPath, "utf-8");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
_collectDirFiles(getAuditDir(), ".jsonl", "audit/", fileMap);
|
|
754
|
+
_collectDirFiles(getPresenceDir(), ".json", "presence/", fileMap);
|
|
755
|
+
|
|
756
|
+
if (Object.keys(fileMap).length === 0) return;
|
|
757
|
+
|
|
758
|
+
dataBranch.commitFilesToBranch(dataBranch.DATA_BRANCH, fileMap);
|
|
759
|
+
|
|
760
|
+
const remote = dataBranch.getDefaultRemote();
|
|
761
|
+
if (remote) {
|
|
762
|
+
const pushResult = dataBranch.pushDataBranch(
|
|
763
|
+
remote, dataBranch.DATA_BRANCH, 3, getMemoryPath(), loadJson, saveJson
|
|
764
|
+
);
|
|
765
|
+
if (!pushResult.ok) {
|
|
766
|
+
process.stderr.write(
|
|
767
|
+
"[STICKY-NOTE] warning: failed to push data branch — " + pushResult.error + "\n" +
|
|
768
|
+
"[STICKY-NOTE] your data is safe locally in .git/sticky-note/\n"
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
} catch (err) {
|
|
773
|
+
process.stderr.write(
|
|
774
|
+
"[STICKY-NOTE] warning: data branch sync failed — " + err.message + "\n"
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
727
779
|
// ── Main ──────────────────────────────────────────────────
|
|
728
780
|
|
|
729
781
|
function main() {
|
|
@@ -946,6 +998,9 @@ function main() {
|
|
|
946
998
|
}
|
|
947
999
|
}
|
|
948
1000
|
|
|
1001
|
+
// Commit current data to sticky-note/data branch and push
|
|
1002
|
+
commitAndPushDataBranch();
|
|
1003
|
+
|
|
949
1004
|
const fileCount = filesTouched.length;
|
|
950
1005
|
const commitCount = commitShas.length;
|
|
951
1006
|
const statusLabel = isCopilotCli ? "updated" : "closed";
|