sticky-note-cli 3.1.41 → 3.1.43

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
@@ -1071,10 +1071,11 @@ async function cmdInit() {
1071
1071
  print(" ⏭️ .gitignore already configured");
1072
1072
  }
1073
1073
 
1074
- // Add .gitattributes merge strategy
1074
+ // Add .gitattributes merge strategy for both thread data and config
1075
1075
  const gitattrsPath = path.join(process.cwd(), ".gitattributes");
1076
1076
  const oldMergeRule = ".sticky-note/sticky-note.json merge=union";
1077
1077
  const mergeRule = ".sticky-note/sticky-note.json merge=sticky-note";
1078
+ const configMergeRule = ".sticky-note/sticky-note-config.json merge=sticky-note";
1078
1079
  let gitattrsContent = "";
1079
1080
  if (fs.existsSync(gitattrsPath)) {
1080
1081
  gitattrsContent = fs.readFileSync(gitattrsPath, "utf-8");
@@ -1089,10 +1090,18 @@ async function cmdInit() {
1089
1090
  ? `\n# Sticky Note - JSON-aware thread merge\n${mergeRule}\n`
1090
1091
  : `\n\n# Sticky Note - JSON-aware thread merge\n${mergeRule}\n`;
1091
1092
  fs.appendFileSync(gitattrsPath, addition);
1093
+ gitattrsContent += addition;
1092
1094
  print(" [OK] .gitattributes updated (merge=sticky-note)");
1093
1095
  } else {
1094
1096
  print(" ⏭️ .gitattributes already configured");
1095
1097
  }
1098
+ // Also protect sticky-note-config.json — merge conflicts here silently break
1099
+ // cloud transport and hook loading for the entire team.
1100
+ if (!gitattrsContent.includes(configMergeRule)) {
1101
+ const addition = `${configMergeRule}\n`;
1102
+ fs.appendFileSync(gitattrsPath, addition);
1103
+ print(" [OK] .gitattributes: sticky-note-config.json protected from merge conflicts");
1104
+ }
1096
1105
 
1097
1106
  // Install merge driver script and git config
1098
1107
  const mergeDriverSrc = path.join(TEMPLATES_DIR, "merge-driver.js");
@@ -1452,14 +1461,25 @@ async function cmdUpdate() {
1452
1461
  }
1453
1462
  }
1454
1463
 
1455
- if (fs.existsSync(gitattrsPath)) {
1456
- let gitattrsContent = fs.readFileSync(gitattrsPath, "utf-8");
1464
+ {
1465
+ let gitattrsContent = fs.existsSync(gitattrsPath)
1466
+ ? fs.readFileSync(gitattrsPath, "utf-8") : "";
1457
1467
  const oldRule = ".sticky-note/sticky-note.json merge=union";
1458
1468
  const newRule = ".sticky-note/sticky-note.json merge=sticky-note";
1469
+ const configRule = ".sticky-note/sticky-note-config.json merge=sticky-note";
1459
1470
  if (gitattrsContent.includes(oldRule)) {
1460
1471
  gitattrsContent = gitattrsContent.replace(oldRule, newRule);
1461
1472
  fs.writeFileSync(gitattrsPath, gitattrsContent, "utf-8");
1462
1473
  print(" [OK] .gitattributes upgraded (merge=union -> merge=sticky-note)");
1474
+ } else if (!gitattrsContent.includes(newRule)) {
1475
+ const addition = `\n# Sticky Note - JSON-aware thread merge\n${newRule}\n`;
1476
+ fs.appendFileSync(gitattrsPath, addition);
1477
+ gitattrsContent += addition;
1478
+ print(" [OK] .gitattributes updated (merge=sticky-note)");
1479
+ }
1480
+ if (!gitattrsContent.includes(configRule)) {
1481
+ fs.appendFileSync(gitattrsPath, `${configRule}\n`);
1482
+ print(" [OK] .gitattributes: sticky-note-config.json protected from merge conflicts");
1463
1483
  }
1464
1484
  }
1465
1485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "3.1.41",
3
+ "version": "3.1.43",
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": {
@@ -237,22 +237,28 @@ function pushDataBranch(remote, branchName, maxRetries, localMemPath, loadJsonFn
237
237
  ).trim();
238
238
 
239
239
  // Build merged file map: start with remote files as base, then
240
- // overlay local files so this user's audit/presence are preserved
240
+ // overlay local files so this user's audit/presence are preserved.
241
+ // Capture remote sticky-note.json BEFORE the local overlay so we
242
+ // have the actual remote content to merge (not the local overwrite).
241
243
  const fileMap = {};
242
244
  const localRef = "refs/heads/" + branchName;
245
+ let remoteStickyContent = null;
243
246
  for (const f of listFilesInBranch(fetchResult.remoteRef)) {
244
247
  const content = readFileFromBranch(fetchResult.remoteRef, f);
245
- if (content !== null) fileMap[f] = content;
248
+ if (content !== null) {
249
+ fileMap[f] = content;
250
+ if (f === "sticky-note.json") remoteStickyContent = content;
251
+ }
246
252
  }
247
253
  for (const f of listFilesInBranch(localRef)) {
248
254
  const content = readFileFromBranch(localRef, f);
249
255
  if (content !== null) fileMap[f] = content;
250
256
  }
251
257
 
252
- // Merge sticky-note.json from both sides
253
- if (localMemPath && fileMap["sticky-note.json"]) {
258
+ // Merge sticky-note.json from both sides using captured remote content
259
+ if (localMemPath && remoteStickyContent) {
254
260
  mergeAndSaveFromRemote(
255
- localMemPath, fileMap["sticky-note.json"], loadJsonFn, saveJsonFn
261
+ localMemPath, remoteStickyContent, loadJsonFn, saveJsonFn
256
262
  );
257
263
  fileMap["sticky-note.json"] = fs.readFileSync(localMemPath, "utf-8");
258
264
  }
@@ -263,9 +269,12 @@ function pushDataBranch(remote, branchName, maxRetries, localMemPath, loadJsonFn
263
269
  }
264
270
  } catch (_) {}
265
271
 
266
- // Brief exponential backoff (busy-wait — synchronous context)
267
- const end = Date.now() + Math.pow(2, attempt) * 300;
268
- while (Date.now() < end) {}
272
+ // Brief exponential backoff — use Atomics.wait to block without CPU spin
273
+ try {
274
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.pow(2, attempt) * 300);
275
+ } catch (_) {
276
+ // Atomics.wait unavailable (e.g. SharedArrayBuffer disabled) — skip delay
277
+ }
269
278
  }
270
279
  }
271
280
  return { ok: false, error: "max retries exceeded" };
@@ -313,9 +322,16 @@ function mergeAndSaveFromRemote(localMemPath, remoteContent, loadJsonFn, saveJso
313
322
  return; // corrupt remote — skip
314
323
  }
315
324
 
316
- const localMemory = loadJsonFn
317
- ? loadJsonFn(localMemPath, { ...EMPTY_MEMORY })
318
- : { ...EMPTY_MEMORY };
325
+ let localMemory;
326
+ if (loadJsonFn) {
327
+ localMemory = loadJsonFn(localMemPath, { ...EMPTY_MEMORY });
328
+ } else {
329
+ try {
330
+ localMemory = JSON.parse(fs.readFileSync(localMemPath, "utf-8"));
331
+ } catch (_) {
332
+ localMemory = { ...EMPTY_MEMORY };
333
+ }
334
+ }
319
335
 
320
336
  const localThreads = Array.isArray(localMemory.threads) ? localMemory.threads.filter(Boolean) : [];
321
337
  const remoteThreads = Array.isArray(remoteMemory.threads) ? remoteMemory.threads.filter(Boolean) : [];
@@ -954,7 +954,7 @@ function _collectDirFiles(dir, ext, prefix, fileMap) {
954
954
  }
955
955
 
956
956
  function commitAndPushDataBranch() {
957
- if (!dataBranch) return;
957
+ if (!dataBranch) return null;
958
958
  try {
959
959
  const fileMap = {};
960
960
  const memPath = getMemoryPath();
@@ -966,7 +966,7 @@ function commitAndPushDataBranch() {
966
966
  _collectDirFiles(getPresenceDir(), ".json", "presence/", fileMap);
967
967
  _collectDirFiles(getTranscriptsDir(), ".jsonl", "transcripts/", fileMap);
968
968
 
969
- if (Object.keys(fileMap).length === 0) return;
969
+ if (Object.keys(fileMap).length === 0) return null;
970
970
 
971
971
  dataBranch.commitFilesToBranch(dataBranch.DATA_BRANCH, fileMap);
972
972
 
@@ -976,16 +976,18 @@ function commitAndPushDataBranch() {
976
976
  remote, dataBranch.DATA_BRANCH, 3, getMemoryPath(), loadJson, saveJson
977
977
  );
978
978
  if (!pushResult.ok) {
979
- process.stderr.write(
980
- "[STICKY-NOTE] warning: failed to push data branch — " + pushResult.error + "\n" +
981
- "[STICKY-NOTE] your data is safe locally in .git/sticky-note/\n"
982
- );
979
+ const hint = pushResult.error && pushResult.error.includes("authentication")
980
+ ? " Add STICKY_PUSH_TOKEN=<github_pat> to .env.sticky to fix."
981
+ : "";
982
+ const msg = `[STICKY-NOTE] ⚠️ data branch push failed — ${pushResult.error}${hint}`;
983
+ logHookError("data-branch-push", new Error(pushResult.error));
984
+ return msg;
983
985
  }
984
986
  }
987
+ return null;
985
988
  } catch (err) {
986
- process.stderr.write(
987
- "[STICKY-NOTE] warning: data branch sync failed — " + err.message + "\n"
988
- );
989
+ logHookError("data-branch-sync", err);
990
+ return `[STICKY-NOTE] ⚠️ data branch sync failed — ${err.message}`;
989
991
  }
990
992
  }
991
993
 
@@ -1306,7 +1308,9 @@ async function main() {
1306
1308
  if (cloud) {
1307
1309
  const threadToSync = threads.find(t => t.session_id === sessionId);
1308
1310
  if (threadToSync) {
1309
- cloudWriteThread(threadToSync).catch(() => {});
1311
+ cloudWriteThread(threadToSync).catch(err => {
1312
+ logHookError("session-end:cloud-write", err);
1313
+ });
1310
1314
  }
1311
1315
  }
1312
1316
 
@@ -1320,14 +1324,15 @@ async function main() {
1320
1324
  }
1321
1325
 
1322
1326
  // Commit current data to sticky-note/data branch and push
1323
- commitAndPushDataBranch();
1327
+ const dataBranchWarning = commitAndPushDataBranch();
1324
1328
 
1325
1329
  const fileCount = filesTouched.length;
1326
1330
  const commitCount = commitShas.length;
1327
1331
  const statusLabel = isCopilotCli ? "updated" : "closed";
1328
1332
  const statusMsg = `[STICKY-NOTE] Session ${statusLabel} - thread ${isCopilotCli ? "updated" : "created"} (${fileCount} file${fileCount !== 1 ? "s" : ""}${commitCount > 0 ? ", " + commitCount + " commit" + (commitCount !== 1 ? "s" : "") : ""})`;
1333
+ const fullOutput = dataBranchWarning ? `${statusMsg}\n${dataBranchWarning}` : statusMsg;
1329
1334
  try {
1330
- process.stdout.write(JSON.stringify({ output: statusMsg }) + "\n");
1335
+ process.stdout.write(JSON.stringify({ output: fullOutput }) + "\n");
1331
1336
  } catch (_) {
1332
1337
  process.stdout.write('{"output":""}\n');
1333
1338
  }
@@ -69,15 +69,38 @@ function _installedCliVersion() {
69
69
  }
70
70
  function formatVersionWarning(config) {
71
71
  try {
72
- const min = config && config.min_version;
73
- if (!min) return "";
74
72
  const installed = _installedCliVersion();
75
73
  if (!installed) return "";
76
- if (_cmpSemver(installed, min) >= 0) return "";
77
- const msg =
78
- `[STICKY-NOTE] ⚠️ Installed sticky-note-cli v${installed} is below ` +
79
- `this project's min_version v${min}. Run \`npm i -D sticky-note-cli@latest\` ` +
80
- `to update — features and schema may differ until you do.`;
74
+ const warnings = [];
75
+
76
+ // Check 1: installed version is below the project's required minimum.
77
+ const min = config && config.min_version;
78
+ if (min && _cmpSemver(installed, min) < 0) {
79
+ warnings.push(
80
+ `[STICKY-NOTE] ⚠️ sticky-note-cli v${installed} is below this project's ` +
81
+ `min_version v${min}. Run \`npm i -g sticky-note-cli@latest\` to update — ` +
82
+ `features and bug fixes are missing until you do.`
83
+ );
84
+ }
85
+
86
+ // Check 2: hook scripts in .claude/hooks/ are older than the installed CLI.
87
+ // This means the teammate installed a newer CLI but never ran `npx sticky-note update`
88
+ // to refresh the hook files — they're running old code with new expectations.
89
+ const hookVer = config && config.hook_version;
90
+ if (hookVer && _cmpSemver(installed, hookVer) > 0) {
91
+ const iv = _parseSemver(installed), hv = _parseSemver(hookVer);
92
+ // Warn on any minor or major gap — patch-only differences are usually safe.
93
+ if (iv && hv && (iv[0] !== hv[0] || iv[1] !== hv[1])) {
94
+ warnings.push(
95
+ `[STICKY-NOTE] ⚠️ Hook scripts are v${hookVer} but sticky-note-cli v${installed} ` +
96
+ `is installed. Run \`npx sticky-note update\` to get the latest fixes ` +
97
+ `(data branch push, identity, credential handling).`
98
+ );
99
+ }
100
+ }
101
+
102
+ if (warnings.length === 0) return "";
103
+ const msg = warnings.join("\n");
81
104
  try { process.stderr.write(msg + "\n"); } catch (_) { /* non-fatal */ }
82
105
  return msg;
83
106
  } catch (_) {
@@ -450,6 +450,20 @@ function clearResumeSignal() {
450
450
  function loadJson(filePath, defaultVal) {
451
451
  try {
452
452
  const raw = fs.readFileSync(filePath, "utf-8");
453
+ // Detect unresolved git merge conflict markers — JSON.parse would silently
454
+ // swallow the SyntaxError and return the default, hiding the real problem.
455
+ // This breaks useCloud(), thread loading, and config reading with no trace.
456
+ if (raw.includes("<<<<<<<") || raw.includes(">>>>>>>")) {
457
+ logHookError(
458
+ "loadJson",
459
+ new Error(
460
+ `Unresolved git merge conflict in ${filePath}. ` +
461
+ `Resolve the conflict and commit, or restore the file with ` +
462
+ `\`git checkout HEAD -- ${path.relative(process.cwd(), filePath) || filePath}\``
463
+ )
464
+ );
465
+ return defaultVal !== undefined ? defaultVal : {};
466
+ }
453
467
  return JSON.parse(raw);
454
468
  } catch (_) {
455
469
  return defaultVal !== undefined ? defaultVal : {};
@@ -644,7 +658,6 @@ function getUser() {
644
658
  // Prefer git config user.name — consistent across machines regardless of OS login name.
645
659
  // Fall back to OS env vars, then "unknown".
646
660
  try {
647
- const { execFileSync } = require("child_process");
648
661
  const name = execFileSync("git", ["config", "user.name"], {
649
662
  encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
650
663
  }).trim();
@@ -1281,6 +1294,7 @@ function getCloudConfig() {
1281
1294
  return {
1282
1295
  url: process.env.STICKY_URL || envFile.STICKY_URL || "",
1283
1296
  apiKey: process.env.STICKY_API_KEY || envFile.STICKY_API_KEY || "",
1297
+ pushToken: process.env.STICKY_PUSH_TOKEN || envFile.STICKY_PUSH_TOKEN || "",
1284
1298
  };
1285
1299
  }
1286
1300