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
|
@@ -33,13 +33,82 @@ function _safeExit() {
|
|
|
33
33
|
process.exit(0);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Issue #12: warn (non-blocking) when the running CLI is below the project's
|
|
37
|
+
// configured min_version. Returns "" when nothing to say. Also writes a
|
|
38
|
+
// stderr banner so the user sees it directly even if the hook output is
|
|
39
|
+
// swallowed by the host tool.
|
|
40
|
+
function _parseSemver(v) {
|
|
41
|
+
if (typeof v !== "string") return null;
|
|
42
|
+
const m = v.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
return [Number(m[1]), Number(m[2]), Number(m[3] || 0)];
|
|
45
|
+
}
|
|
46
|
+
function _cmpSemver(a, b) {
|
|
47
|
+
const pa = _parseSemver(a), pb = _parseSemver(b);
|
|
48
|
+
if (!pa || !pb) return 0;
|
|
49
|
+
for (let i = 0; i < 3; i++) {
|
|
50
|
+
if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
function _installedCliVersion() {
|
|
55
|
+
// Try the package.json shipped alongside this hook script's CLI.
|
|
56
|
+
try {
|
|
57
|
+
const candidates = [
|
|
58
|
+
path.join(__dirname, "..", "..", "package.json"),
|
|
59
|
+
path.join(__dirname, "..", "..", "..", "sticky-note-cli", "package.json"),
|
|
60
|
+
];
|
|
61
|
+
for (const p of candidates) {
|
|
62
|
+
try {
|
|
63
|
+
const pkg = JSON.parse(require("fs").readFileSync(p, "utf-8"));
|
|
64
|
+
if (pkg && pkg.name === "sticky-note-cli" && pkg.version) return pkg.version;
|
|
65
|
+
} catch (_) { /* try next */ }
|
|
66
|
+
}
|
|
67
|
+
} catch (_) { /* ignore */ }
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function formatVersionWarning(config) {
|
|
71
|
+
try {
|
|
72
|
+
const min = config && config.min_version;
|
|
73
|
+
if (!min) return "";
|
|
74
|
+
const installed = _installedCliVersion();
|
|
75
|
+
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.`;
|
|
81
|
+
try { process.stderr.write(msg + "\n"); } catch (_) { /* non-fatal */ }
|
|
82
|
+
return msg;
|
|
83
|
+
} catch (_) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
36
88
|
let utils;
|
|
37
89
|
try {
|
|
38
90
|
utils = require("./sticky-utils.js");
|
|
39
|
-
} catch (
|
|
91
|
+
} catch (err) {
|
|
92
|
+
try {
|
|
93
|
+
process.stderr.write(
|
|
94
|
+
`[STICKY-NOTE] session-start hook error: failed to load sticky-utils.js — ${err.message}\n` +
|
|
95
|
+
`[STICKY-NOTE] hint: re-run \`npx sticky-note init\` to restore .claude/hooks/.\n`
|
|
96
|
+
);
|
|
97
|
+
} catch (_) { /* ignore */ }
|
|
40
98
|
_safeExit();
|
|
41
99
|
}
|
|
42
100
|
|
|
101
|
+
// Issue #13: self-heal stale absolute hook paths in .claude/settings.json
|
|
102
|
+
// before doing anything else, so the next session loads cleanly even if some
|
|
103
|
+
// other hook (e.g. on-stop, inject-context) was pointing at a path that no
|
|
104
|
+
// longer exists. Failures here are non-fatal — we still want the rest of
|
|
105
|
+
// session-start to run.
|
|
106
|
+
try { utils.selfHealHookPaths && utils.selfHealHookPaths(); } catch (_) { /* non-fatal */ }
|
|
107
|
+
|
|
108
|
+
// Non-fatal: data-branch sync is disabled if the module is missing.
|
|
109
|
+
let dataBranch = null;
|
|
110
|
+
try { dataBranch = require("./data-branch.js"); } catch (_) {}
|
|
111
|
+
|
|
43
112
|
const {
|
|
44
113
|
getMemoryPath,
|
|
45
114
|
getConfigPath,
|
|
@@ -121,6 +190,59 @@ function autoCloseCopilotCliThreads(memory, autoCloseHours) {
|
|
|
121
190
|
|
|
122
191
|
// ── Formatting helpers ────────────────────────────────────
|
|
123
192
|
|
|
193
|
+
function formatStuckBanner(threads) {
|
|
194
|
+
const stuck = (threads || []).filter((t) => t && t.status === "stuck");
|
|
195
|
+
if (stuck.length === 0) return "";
|
|
196
|
+
|
|
197
|
+
const R = "\x1b[0m";
|
|
198
|
+
const Y = "\x1b[33m";
|
|
199
|
+
const BY = "\x1b[1;33m";
|
|
200
|
+
const BR = "\x1b[1;31m";
|
|
201
|
+
const B = "\x1b[1m";
|
|
202
|
+
const C = "\x1b[36m";
|
|
203
|
+
const D = "\x1b[2m";
|
|
204
|
+
const bar = "━".repeat(52);
|
|
205
|
+
|
|
206
|
+
const lines = [
|
|
207
|
+
"",
|
|
208
|
+
`${Y}${bar}${R}`,
|
|
209
|
+
`${BY} ⚠️ STUCK THREADS${R}`,
|
|
210
|
+
`${Y}${bar}${R}`,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const t of stuck.slice(0, 5)) {
|
|
214
|
+
const user = t.user || t.author || "unknown";
|
|
215
|
+
const branch = t.branch ? ` ${D}·${R} ${C}${t.branch}${R}` : "";
|
|
216
|
+
const files = (t.files_touched || []).slice(0, 3).join(", ");
|
|
217
|
+
const narrative = t.narrative || t.last_note || "";
|
|
218
|
+
const snip =
|
|
219
|
+
narrative.length > 80 ? narrative.substring(0, 80) + "…" : narrative;
|
|
220
|
+
const threadId = (t.id || "").substring(0, 8);
|
|
221
|
+
const failed = t.failed_approaches || [];
|
|
222
|
+
|
|
223
|
+
lines.push(
|
|
224
|
+
`${Y} ┃${R} ${BR}[STUCK]${R} ${B}${user}${R}${branch}`
|
|
225
|
+
);
|
|
226
|
+
if (files) lines.push(`${Y} ┃${R} Files: ${files}`);
|
|
227
|
+
if (snip) lines.push(`${Y} ┃${R} ${D}"${snip}"${R}`);
|
|
228
|
+
if (failed.length > 0) {
|
|
229
|
+
lines.push(
|
|
230
|
+
`${Y} ┃${R} ${BR}⚠️ ${failed.length} failed approach(es)${R}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
lines.push(
|
|
234
|
+
`${Y} ┃${R} → Resume: ${C}npx sticky-note resume ${threadId}${R}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (stuck.length > 5) {
|
|
239
|
+
lines.push(`${Y} ┃${R} ... and ${stuck.length - 5} more`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
lines.push(`${Y}${bar}${R}`, "");
|
|
243
|
+
return lines.join("\n") + "\n";
|
|
244
|
+
}
|
|
245
|
+
|
|
124
246
|
function formatThreadsForInjection(threads, maxThreads, maxTokens) {
|
|
125
247
|
maxThreads = maxThreads || 10;
|
|
126
248
|
maxTokens = maxTokens || 500;
|
|
@@ -251,7 +373,7 @@ function formatPresence(presenceData, threads) {
|
|
|
251
373
|
try {
|
|
252
374
|
const ts = new Date(lastSeen).getTime();
|
|
253
375
|
if (isNaN(ts)) continue;
|
|
254
|
-
if (now - ts <
|
|
376
|
+
if (now - ts < 60 * 60 * 1000) {
|
|
255
377
|
const files = (info.active_files || []).slice(0, 3).join(", ");
|
|
256
378
|
const thread = userThreads[user];
|
|
257
379
|
let line = `- **${user}**`;
|
|
@@ -761,6 +883,47 @@ function ensureEnvironmentProvisioned() {
|
|
|
761
883
|
if (debug) process.stderr.write("[sticky-note] environment provisioning complete\n");
|
|
762
884
|
}
|
|
763
885
|
|
|
886
|
+
// ── Data branch fetch-and-merge ───────────────────────────
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Fetches the remote sticky-note/data branch and merges into local memory.
|
|
890
|
+
* Also reads from the LOCAL data branch (fresh clones, offline, tests).
|
|
891
|
+
* Non-blocking — the session continues with local data on any failure.
|
|
892
|
+
*/
|
|
893
|
+
function fetchAndMergeDataBranch() {
|
|
894
|
+
if (!dataBranch) return;
|
|
895
|
+
|
|
896
|
+
const memPath = getMemoryPath();
|
|
897
|
+
const mergeFrom = (ref) => {
|
|
898
|
+
const content = dataBranch.readFileFromBranch(ref, "sticky-note.json");
|
|
899
|
+
if (content) {
|
|
900
|
+
dataBranch.mergeAndSaveFromRemote(memPath, content, loadJson, saveJson);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
const remote = dataBranch.getDefaultRemote();
|
|
906
|
+
|
|
907
|
+
if (remote) {
|
|
908
|
+
const fetchResult = dataBranch.fetchDataBranch(remote);
|
|
909
|
+
if (!fetchResult.ok) {
|
|
910
|
+
process.stderr.write(
|
|
911
|
+
"[STICKY-NOTE] offline — using local thread cache (" +
|
|
912
|
+
(fetchResult.error || "fetch failed") + ")\n"
|
|
913
|
+
);
|
|
914
|
+
} else if (fetchResult.remoteRef) {
|
|
915
|
+
mergeFrom(fetchResult.remoteRef);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
mergeFrom(dataBranch.DATA_REF);
|
|
920
|
+
} catch (err) {
|
|
921
|
+
process.stderr.write(
|
|
922
|
+
"[STICKY-NOTE] offline — using local thread cache (" + err.message + ")\n"
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
764
927
|
// ── Main ──────────────────────────────────────────────────
|
|
765
928
|
|
|
766
929
|
function main() {
|
|
@@ -774,6 +937,9 @@ function main() {
|
|
|
774
937
|
hookInput = {};
|
|
775
938
|
}
|
|
776
939
|
|
|
940
|
+
// Fetch remote data branch and merge into local cache — before reading memory
|
|
941
|
+
fetchAndMergeDataBranch();
|
|
942
|
+
|
|
777
943
|
let sessionId = getSessionId(hookInput);
|
|
778
944
|
const aiTool = detectTool(hookInput);
|
|
779
945
|
const isCopilotCli = aiTool === "copilot-cli" || process.argv.includes("--copilot-cli") || !!process.env.COPILOT_CLI;
|
|
@@ -911,6 +1077,7 @@ function main() {
|
|
|
911
1077
|
const overlapContext = formatOverlapWarnings(
|
|
912
1078
|
(memory.threads || []).filter(Boolean), getUser(), memory
|
|
913
1079
|
);
|
|
1080
|
+
const versionWarning = formatVersionWarning(config);
|
|
914
1081
|
|
|
915
1082
|
// V2.5: Mark eagerly-injected stuck threads so PreToolUse won't re-inject.
|
|
916
1083
|
// Skip for Copilot CLI — its sessionStart output is not surfaced to the AI,
|
|
@@ -930,6 +1097,41 @@ function main() {
|
|
|
930
1097
|
|
|
931
1098
|
saveMemoryMerged(memoryPath, memory);
|
|
932
1099
|
|
|
1100
|
+
// ── Stuck threads startup banner ────────────────────────
|
|
1101
|
+
// Write a visible banner to stderr so the user sees stuck threads
|
|
1102
|
+
// immediately when the session starts, before any AI interaction.
|
|
1103
|
+
const stuckThreads = (memory.threads || [])
|
|
1104
|
+
.filter((t) => t && t.status === "stuck");
|
|
1105
|
+
if (stuckThreads.length > 0) {
|
|
1106
|
+
const fs = require("fs");
|
|
1107
|
+
const bannerShownPath = path.join(
|
|
1108
|
+
path.dirname(memoryPath),
|
|
1109
|
+
".sticky-banner-shown"
|
|
1110
|
+
);
|
|
1111
|
+
let showBanner = true;
|
|
1112
|
+
|
|
1113
|
+
// Copilot CLI fires sessionStart per-turn — only show once per session.
|
|
1114
|
+
if (isCopilotCli) {
|
|
1115
|
+
try {
|
|
1116
|
+
const shown = fs.readFileSync(bannerShownPath, "utf-8").trim();
|
|
1117
|
+
if (shown === sessionId) showBanner = false;
|
|
1118
|
+
} catch (_) {
|
|
1119
|
+
/* file doesn't exist yet */
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (showBanner) {
|
|
1124
|
+
process.stderr.write(formatStuckBanner(stuckThreads));
|
|
1125
|
+
if (isCopilotCli) {
|
|
1126
|
+
try {
|
|
1127
|
+
fs.writeFileSync(bannerShownPath, sessionId, "utf-8");
|
|
1128
|
+
} catch (_) {
|
|
1129
|
+
/* non-fatal */
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
933
1135
|
// ── Assemble output ───────────────────────────────────
|
|
934
1136
|
const parts = [];
|
|
935
1137
|
|
|
@@ -1013,6 +1215,7 @@ function main() {
|
|
|
1013
1215
|
}
|
|
1014
1216
|
|
|
1015
1217
|
// Overlap warning comes first — most urgent signal
|
|
1218
|
+
if (versionWarning) parts.push(versionWarning);
|
|
1016
1219
|
if (overlapContext) parts.push(overlapContext);
|
|
1017
1220
|
|
|
1018
1221
|
if (threadContext) parts.push(threadContext);
|
|
@@ -1028,6 +1231,8 @@ function main() {
|
|
|
1028
1231
|
try {
|
|
1029
1232
|
main();
|
|
1030
1233
|
} catch (err) {
|
|
1031
|
-
try { utils.
|
|
1234
|
+
try { utils.reportHookError("session-start", err); } catch (_) {
|
|
1235
|
+
try { utils.logHookError("session-start", err); } catch (_) {}
|
|
1236
|
+
}
|
|
1032
1237
|
_safeExit();
|
|
1033
1238
|
}
|
|
@@ -38,8 +38,16 @@ const FILE_PATH_PATTERN = /[\w./\\-]+\.\w{1,10}/g;
|
|
|
38
38
|
// ── Path helpers ──────────────────────────────────────────
|
|
39
39
|
|
|
40
40
|
function _stickyDir() {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
try {
|
|
42
|
+
const gitDir = execFileSync("git", ["rev-parse", "--absolute-git-dir"], {
|
|
43
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
44
|
+
}).trim();
|
|
45
|
+
return path.resolve(gitDir, "sticky-note");
|
|
46
|
+
} catch (_) {
|
|
47
|
+
// Fallback for non-git environments (e.g., unit tests without git init)
|
|
48
|
+
const scriptDir = path.dirname(path.resolve(__filename));
|
|
49
|
+
return path.join(scriptDir, "..", "..", ".sticky-note");
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
// ── Error logging ─────────────────────────────────────────
|
|
@@ -57,6 +65,101 @@ function logHookError(hookName, error) {
|
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
// Issue #14: Surface hook failures to stderr so the host tool's generic
|
|
69
|
+
// "<HookName> hook error" message has actionable detail next to it. Earlier
|
|
70
|
+
// versions silently swallowed `require()` failures and top-level exceptions,
|
|
71
|
+
// which made stale-path issues (#13) and other crashes impossible to diagnose
|
|
72
|
+
// — particularly under UserPromptSubmit, which Claude Code reports without
|
|
73
|
+
// a stack trace. This helper prefixes every line with `[STICKY-NOTE]` so the
|
|
74
|
+
// user can spot it in mixed hook output.
|
|
75
|
+
function reportHookError(hookName, error, hint) {
|
|
76
|
+
const msg = error instanceof Error ? (error.stack || error.message) : String(error);
|
|
77
|
+
const lines = [`[STICKY-NOTE] ${hookName} hook error: ${msg}`];
|
|
78
|
+
if (hint) lines.push(`[STICKY-NOTE] hint: ${hint}`);
|
|
79
|
+
try { process.stderr.write(lines.join("\n") + "\n"); } catch (_) { /* ignore */ }
|
|
80
|
+
try { logHookError(hookName, error); } catch (_) { /* ignore */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Issue #13: When a teammate copies `.claude/settings.json` from another
|
|
84
|
+
// machine (or runs an older sticky-note-cli that wrote absolute paths), the
|
|
85
|
+
// hook commands point at non-existent absolute paths and every hook fails
|
|
86
|
+
// with MODULE_NOT_FOUND. Re-running `npx sticky-note init` fixes it, but
|
|
87
|
+
// users typically don't know that — they just see "<Hook> hook error" each
|
|
88
|
+
// session. This helper detects and rewrites those stale commands in-place
|
|
89
|
+
// the first time any hook (e.g. session-start, inject-context) successfully
|
|
90
|
+
// loads, so the next session starts clean.
|
|
91
|
+
//
|
|
92
|
+
// The rewritten command uses `npx sticky-note run-hook <basename>` which walks
|
|
93
|
+
// up the directory tree to find `.claude/hooks/<basename>.js`, so it works even
|
|
94
|
+
// when Claude Code is launched from a subdirectory of the project root.
|
|
95
|
+
// We extract `<basename>` from the existing command so we never invent hook
|
|
96
|
+
// scripts that don't exist.
|
|
97
|
+
//
|
|
98
|
+
// Returns the list of (hookEvent, oldCommand, newCommand) tuples that were
|
|
99
|
+
// rewritten, or [] if nothing was stale.
|
|
100
|
+
function selfHealHookPaths(settingsPath) {
|
|
101
|
+
const result = [];
|
|
102
|
+
try {
|
|
103
|
+
if (!settingsPath) {
|
|
104
|
+
settingsPath = path.join(process.cwd(), ".claude", "settings.json");
|
|
105
|
+
}
|
|
106
|
+
if (!fs.existsSync(settingsPath)) return result;
|
|
107
|
+
|
|
108
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
109
|
+
let settings;
|
|
110
|
+
try { settings = JSON.parse(raw); } catch (_) { return result; }
|
|
111
|
+
if (!settings || !settings.hooks) return result;
|
|
112
|
+
|
|
113
|
+
// Two stale patterns:
|
|
114
|
+
// 1. Absolute paths (written by older sticky-note-cli or copied from another machine)
|
|
115
|
+
// 2. Old relative form `node ".claude/hooks/foo.js"` — fails when Claude Code
|
|
116
|
+
// launches from a subdirectory, because the path resolves relative to the
|
|
117
|
+
// launch cwd, not the directory containing .claude/settings.json.
|
|
118
|
+
const STALE_ABSOLUTE = /["'][A-Za-z]:[\\/]|["']\/(?:Users|home|root|tmp)\/|\$\(git rev-parse/;
|
|
119
|
+
const STALE_RELATIVE = /^node\s+["']\.claude[\\/]hooks[\\/][^"']+\.js["']\s*$/;
|
|
120
|
+
// Match the script basename in either quoted or bare form.
|
|
121
|
+
const BASENAME = /([A-Za-z0-9_.-]+)\.(?:js|sh|cjs|mjs)(?:["'`\s]|$)/;
|
|
122
|
+
|
|
123
|
+
let changed = false;
|
|
124
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
125
|
+
if (!Array.isArray(entries)) continue;
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
for (const h of (entry && entry.hooks) || []) {
|
|
128
|
+
const cmd = h && h.command;
|
|
129
|
+
if (typeof cmd !== "string") continue;
|
|
130
|
+
const isStale = STALE_ABSOLUTE.test(cmd) || STALE_RELATIVE.test(cmd);
|
|
131
|
+
if (!isStale) continue;
|
|
132
|
+
const m = cmd.match(BASENAME);
|
|
133
|
+
if (!m) continue;
|
|
134
|
+
const basename = m[1];
|
|
135
|
+
const newCmd = `npx sticky-note run-hook ${basename}`;
|
|
136
|
+
result.push({ event, oldCommand: cmd, newCommand: newCmd });
|
|
137
|
+
h.command = newCmd;
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!changed) return result;
|
|
144
|
+
|
|
145
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
146
|
+
try {
|
|
147
|
+
const summary =
|
|
148
|
+
`[STICKY-NOTE] ⚠️ Detected ${result.length} stale hook path(s) in ` +
|
|
149
|
+
`${path.relative(process.cwd(), settingsPath) || settingsPath} — rewrote to ` +
|
|
150
|
+
`"npx sticky-note run-hook <name>" so hooks work from any subdirectory. ` +
|
|
151
|
+
`Commit the change to share the fix with your team.`;
|
|
152
|
+
process.stderr.write(summary + "\n");
|
|
153
|
+
for (const { event, oldCommand, newCommand } of result.slice(0, 5)) {
|
|
154
|
+
process.stderr.write(`[STICKY-NOTE] ${event}: ${oldCommand} → ${newCommand}\n`);
|
|
155
|
+
}
|
|
156
|
+
} catch (_) { /* non-fatal */ }
|
|
157
|
+
} catch (err) {
|
|
158
|
+
try { logHookError("selfHealHookPaths", err); } catch (_) { /* ignore */ }
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
60
163
|
const _paths = {
|
|
61
164
|
memory: "sticky-note.json",
|
|
62
165
|
config: "sticky-note-config.json",
|
|
@@ -947,6 +1050,8 @@ module.exports = {
|
|
|
947
1050
|
RETRY_PATTERNS,
|
|
948
1051
|
FILE_PATH_PATTERN,
|
|
949
1052
|
logHookError,
|
|
1053
|
+
reportHookError,
|
|
1054
|
+
selfHealHookPaths,
|
|
950
1055
|
getMemoryPath,
|
|
951
1056
|
getConfigPath,
|
|
952
1057
|
getAuditPath,
|
|
@@ -1018,5 +1123,10 @@ module.exports = {
|
|
|
1018
1123
|
rotateBackup,
|
|
1019
1124
|
acquireLock,
|
|
1020
1125
|
releaseLock,
|
|
1126
|
+
// Cloud sync stubs (feature not yet implemented — on-stop.js uses these)
|
|
1127
|
+
useCloud: () => false,
|
|
1128
|
+
cloudReadThreads: async () => null,
|
|
1129
|
+
cloudWriteThread: async () => {},
|
|
1130
|
+
cloudAppendAudit: async () => {},
|
|
1021
1131
|
};
|
|
1022
1132
|
// tier1 test
|
package/templates/hooks.json
CHANGED
|
@@ -4,43 +4,43 @@
|
|
|
4
4
|
"sessionStart": [
|
|
5
5
|
{
|
|
6
6
|
"type": "command",
|
|
7
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
8
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
7
|
+
"bash": "COPILOT_CLI=1 node \".claude/hooks/session-start.js\" --copilot-cli",
|
|
8
|
+
"powershell": "$env:COPILOT_CLI='1'; node \".claude/hooks/session-start.js\" --copilot-cli"
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"sessionEnd": [
|
|
12
12
|
{
|
|
13
13
|
"type": "command",
|
|
14
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
15
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
14
|
+
"bash": "COPILOT_CLI=1 node \".claude/hooks/session-end.js\" --copilot-cli",
|
|
15
|
+
"powershell": "$env:COPILOT_CLI='1'; node \".claude/hooks/session-end.js\" --copilot-cli"
|
|
16
16
|
}
|
|
17
17
|
],
|
|
18
18
|
"userPromptSubmitted": [
|
|
19
19
|
{
|
|
20
20
|
"type": "command",
|
|
21
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
22
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
21
|
+
"bash": "COPILOT_CLI=1 node \".claude/hooks/inject-context.js\" --copilot-cli",
|
|
22
|
+
"powershell": "$env:COPILOT_CLI='1'; node \".claude/hooks/inject-context.js\" --copilot-cli"
|
|
23
23
|
}
|
|
24
24
|
],
|
|
25
25
|
"preToolUse": [
|
|
26
26
|
{
|
|
27
27
|
"type": "command",
|
|
28
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
29
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
28
|
+
"bash": "COPILOT_CLI=1 node \".claude/hooks/pre-tool-use.js\" --copilot-cli",
|
|
29
|
+
"powershell": "$env:COPILOT_CLI='1'; node \".claude/hooks/pre-tool-use.js\" --copilot-cli"
|
|
30
30
|
}
|
|
31
31
|
],
|
|
32
32
|
"postToolUse": [
|
|
33
33
|
{
|
|
34
34
|
"type": "command",
|
|
35
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
36
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
35
|
+
"bash": "COPILOT_CLI=1 node \".claude/hooks/track-work.js\" --copilot-cli",
|
|
36
|
+
"powershell": "$env:COPILOT_CLI='1'; node \".claude/hooks/track-work.js\" --copilot-cli"
|
|
37
37
|
}
|
|
38
38
|
],
|
|
39
39
|
"errorOccurred": [
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
43
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
42
|
+
"bash": "COPILOT_CLI=1 node \".claude/hooks/on-error.js\" --copilot-cli",
|
|
43
|
+
"powershell": "$env:COPILOT_CLI='1'; node \".claude/hooks/on-error.js\" --copilot-cli"
|
|
44
44
|
}
|
|
45
45
|
]
|
|
46
46
|
}
|
package/templates/settings.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "
|
|
9
|
+
"command": "npx sticky-note run-hook session-start"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "
|
|
20
|
+
"command": "npx sticky-note run-hook session-end"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"hooks": [
|
|
29
29
|
{
|
|
30
30
|
"type": "command",
|
|
31
|
-
"command": "
|
|
31
|
+
"command": "npx sticky-note run-hook inject-context"
|
|
32
32
|
}
|
|
33
33
|
]
|
|
34
34
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"hooks": [
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "
|
|
42
|
+
"command": "npx sticky-note run-hook pre-tool-use"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"hooks": [
|
|
51
51
|
{
|
|
52
52
|
"type": "command",
|
|
53
|
-
"command": "
|
|
53
|
+
"command": "npx sticky-note run-hook track-work"
|
|
54
54
|
}
|
|
55
55
|
]
|
|
56
56
|
}
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"hooks": [
|
|
62
62
|
{
|
|
63
63
|
"type": "command",
|
|
64
|
-
"command": "
|
|
64
|
+
"command": "npx sticky-note run-hook on-stop"
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
67
|
}
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"hooks": [
|
|
73
73
|
{
|
|
74
74
|
"type": "command",
|
|
75
|
-
"command": "
|
|
75
|
+
"command": "npx sticky-note run-hook on-error"
|
|
76
76
|
}
|
|
77
77
|
]
|
|
78
78
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "2",
|
|
3
|
-
"project": "",
|
|
4
|
-
"threads": []
|
|
5
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"version": "2",
|
|
3
|
+
"project": "",
|
|
4
|
+
"threads": []
|
|
5
|
+
}
|