sticky-note-cli 3.1.42 → 3.1.44

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
@@ -744,6 +744,29 @@ async function cmdInit() {
744
744
  v3Mode ? "y" : "N"
745
745
  );
746
746
  wantsCloud = cloudAnswer.toLowerCase() === "y" || cloudAnswer.toLowerCase() === "yes";
747
+
748
+ // Always ask for STICKY_PUSH_TOKEN — needed for data branch push from hook
749
+ // processes regardless of whether cloud backend is configured.
750
+ print("\n A GitHub Personal Access Token (PAT) is needed so sticky-note can push");
751
+ print(" thread data in the background without requiring interactive credential access.");
752
+ print(" Create one at https://github.com/settings/tokens with 'repo' scope (or");
753
+ print(" 'contents: write' for fine-grained tokens).");
754
+ const pushTokenAnswer = await ask(rl, "Enter your STICKY_PUSH_TOKEN (GitHub PAT, leave blank to skip)", "");
755
+ if (pushTokenAnswer.trim()) {
756
+ const envStickyPath = path.join(process.cwd(), ".env.sticky");
757
+ let envContent = "";
758
+ try { envContent = fs.readFileSync(envStickyPath, "utf-8"); } catch (_) {}
759
+ if (!envContent.endsWith("\n") && envContent.length > 0) envContent += "\n";
760
+ if (!/^STICKY_PUSH_TOKEN=/m.test(envContent)) {
761
+ envContent += `STICKY_PUSH_TOKEN=${pushTokenAnswer.trim()}\n`;
762
+ } else {
763
+ envContent = envContent.replace(/^STICKY_PUSH_TOKEN=.+$/m, `STICKY_PUSH_TOKEN=${pushTokenAnswer.trim()}`);
764
+ }
765
+ fs.writeFileSync(envStickyPath, envContent, "utf-8");
766
+ print(" [OK] STICKY_PUSH_TOKEN written to .env.sticky");
767
+ } else {
768
+ print(" ⏭️ No PAT entered — data branch push will rely on system credential helper");
769
+ }
747
770
  }
748
771
 
749
772
  rl.close();
@@ -877,6 +900,19 @@ async function cmdInit() {
877
900
  print(` wrangler secret put GITHUB_REPO # value: ${repo.trim()}`);
878
901
  }
879
902
  }
903
+ // Also write STICKY_PUSH_TOKEN to .env.sticky so session-end hook can
904
+ // push the data branch from background processes without a credential helper.
905
+ try {
906
+ const envStickyPath = path.join(process.cwd(), ".env.sticky");
907
+ let envContent = "";
908
+ try { envContent = fs.readFileSync(envStickyPath, "utf-8"); } catch (_) {}
909
+ if (!envContent.endsWith("\n") && envContent.length > 0) envContent += "\n";
910
+ if (!/^STICKY_PUSH_TOKEN=/m.test(envContent)) {
911
+ envContent += `STICKY_PUSH_TOKEN=${pat.trim()}\n`;
912
+ fs.writeFileSync(envStickyPath, envContent, "utf-8");
913
+ print(" ✓ STICKY_PUSH_TOKEN written to .env.sticky (data branch push)");
914
+ }
915
+ } catch (_) {}
880
916
  }
881
917
  rl2.close();
882
918
 
@@ -1071,10 +1107,11 @@ async function cmdInit() {
1071
1107
  print(" ⏭️ .gitignore already configured");
1072
1108
  }
1073
1109
 
1074
- // Add .gitattributes merge strategy
1110
+ // Add .gitattributes merge strategy for both thread data and config
1075
1111
  const gitattrsPath = path.join(process.cwd(), ".gitattributes");
1076
1112
  const oldMergeRule = ".sticky-note/sticky-note.json merge=union";
1077
1113
  const mergeRule = ".sticky-note/sticky-note.json merge=sticky-note";
1114
+ const configMergeRule = ".sticky-note/sticky-note-config.json merge=sticky-note";
1078
1115
  let gitattrsContent = "";
1079
1116
  if (fs.existsSync(gitattrsPath)) {
1080
1117
  gitattrsContent = fs.readFileSync(gitattrsPath, "utf-8");
@@ -1089,10 +1126,18 @@ async function cmdInit() {
1089
1126
  ? `\n# Sticky Note - JSON-aware thread merge\n${mergeRule}\n`
1090
1127
  : `\n\n# Sticky Note - JSON-aware thread merge\n${mergeRule}\n`;
1091
1128
  fs.appendFileSync(gitattrsPath, addition);
1129
+ gitattrsContent += addition;
1092
1130
  print(" [OK] .gitattributes updated (merge=sticky-note)");
1093
1131
  } else {
1094
1132
  print(" ⏭️ .gitattributes already configured");
1095
1133
  }
1134
+ // Also protect sticky-note-config.json — merge conflicts here silently break
1135
+ // cloud transport and hook loading for the entire team.
1136
+ if (!gitattrsContent.includes(configMergeRule)) {
1137
+ const addition = `${configMergeRule}\n`;
1138
+ fs.appendFileSync(gitattrsPath, addition);
1139
+ print(" [OK] .gitattributes: sticky-note-config.json protected from merge conflicts");
1140
+ }
1096
1141
 
1097
1142
  // Install merge driver script and git config
1098
1143
  const mergeDriverSrc = path.join(TEMPLATES_DIR, "merge-driver.js");
@@ -1452,14 +1497,25 @@ async function cmdUpdate() {
1452
1497
  }
1453
1498
  }
1454
1499
 
1455
- if (fs.existsSync(gitattrsPath)) {
1456
- let gitattrsContent = fs.readFileSync(gitattrsPath, "utf-8");
1500
+ {
1501
+ let gitattrsContent = fs.existsSync(gitattrsPath)
1502
+ ? fs.readFileSync(gitattrsPath, "utf-8") : "";
1457
1503
  const oldRule = ".sticky-note/sticky-note.json merge=union";
1458
1504
  const newRule = ".sticky-note/sticky-note.json merge=sticky-note";
1505
+ const configRule = ".sticky-note/sticky-note-config.json merge=sticky-note";
1459
1506
  if (gitattrsContent.includes(oldRule)) {
1460
1507
  gitattrsContent = gitattrsContent.replace(oldRule, newRule);
1461
1508
  fs.writeFileSync(gitattrsPath, gitattrsContent, "utf-8");
1462
1509
  print(" [OK] .gitattributes upgraded (merge=union -> merge=sticky-note)");
1510
+ } else if (!gitattrsContent.includes(newRule)) {
1511
+ const addition = `\n# Sticky Note - JSON-aware thread merge\n${newRule}\n`;
1512
+ fs.appendFileSync(gitattrsPath, addition);
1513
+ gitattrsContent += addition;
1514
+ print(" [OK] .gitattributes updated (merge=sticky-note)");
1515
+ }
1516
+ if (!gitattrsContent.includes(configRule)) {
1517
+ fs.appendFileSync(gitattrsPath, `${configRule}\n`);
1518
+ print(" [OK] .gitattributes: sticky-note-config.json protected from merge conflicts");
1463
1519
  }
1464
1520
  }
1465
1521
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "3.1.42",
3
+ "version": "3.1.44",
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": {
@@ -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 : {};