sticky-note-cli 2.9.1 → 2.9.2
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 +155 -23
- 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 +1 -0
- package/templates/hooks/inject-context.js +14 -1
- package/templates/hooks/on-stop.js +10 -2
- package/templates/hooks/session-start.js +160 -3
- package/templates/hooks/sticky-utils.js +90 -0
- package/templates/hooks.json +47 -47
- package/templates/settings.json +81 -81
- package/templates/sticky-note-config.json +2 -1
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
<br/>
|
|
19
19
|
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="docs/images/demo.gif" alt="Sticky Note demo" width="600" />
|
|
22
|
+
</p>
|
|
23
|
+
|
|
20
24
|
<p align="center">
|
|
21
25
|
<img src="https://raw.githubusercontent.com/BandaruDheeraj/sticky-note/main/docs/images/architecture.svg" alt="Architecture diagram" width="800" />
|
|
22
26
|
</p>
|
|
@@ -154,7 +158,7 @@ threads only — non-stuck threads are held for lazy injection.
|
|
|
154
158
|
- **Stuck threads** — All stuck threads are injected eagerly (V2.5).
|
|
155
159
|
- **Team config** — Conventions, MCP servers, and skills from
|
|
156
160
|
`sticky-note-config.json`.
|
|
157
|
-
- **Active presence** — Developers seen in the last
|
|
161
|
+
- **Active presence** — Developers seen in the last hour and the
|
|
158
162
|
files they're working on.
|
|
159
163
|
|
|
160
164
|
The hook also clears the injected-this-session tracking set, snapshots
|
package/bin/cli.js
CHANGED
|
@@ -83,26 +83,108 @@ function readTemplate(name) {
|
|
|
83
83
|
return fs.readFileSync(path.join(TEMPLATES_DIR, name), "utf-8");
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
// Hook commands in templates use relative paths (e.g. ".claude/hooks/foo.js")
|
|
87
|
+
// because Claude Code and Copilot CLI both resolve hook commands relative to
|
|
88
|
+
// the project root. Earlier versions rewrote these to absolute paths at init
|
|
89
|
+
// time, but that path was machine-specific and got committed to settings.json,
|
|
90
|
+
// causing every teammate's hooks to fail silently (issue #11). This is now a
|
|
91
|
+
// no-op, kept only to avoid breaking any external callers of the function.
|
|
92
|
+
function resolveHookPaths(_obj, _rootDir) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Detects hook commands in an existing settings.json that point at an absolute
|
|
97
|
+
// path (likely written by an older sticky-note-cli on another developer's
|
|
98
|
+
// machine). Returns the list of stale command strings, or [] if all clean.
|
|
99
|
+
function findStaleHookPaths(settings) {
|
|
100
|
+
const stale = [];
|
|
101
|
+
const hooks = (settings && settings.hooks) || {};
|
|
93
102
|
for (const entries of Object.values(hooks)) {
|
|
94
103
|
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
for (const h of entry.hooks || []) {
|
|
105
|
+
const cmd = h && h.command;
|
|
106
|
+
if (typeof cmd !== "string") continue;
|
|
107
|
+
// Absolute paths (Unix /Users, /home, /root or Windows C:/, D:/ etc.)
|
|
108
|
+
// inside the hook command are the smoking gun. Match anywhere in the
|
|
109
|
+
// string — the path may be preceded by other tokens (env vars, shell
|
|
110
|
+
// prefixes) and may contain spaces.
|
|
111
|
+
if (
|
|
112
|
+
/["'][A-Za-z]:[\\/]/.test(cmd) ||
|
|
113
|
+
/["']\/(?:Users|home|root|tmp)\//.test(cmd) ||
|
|
114
|
+
cmd.includes("$(git rev-parse")
|
|
115
|
+
) {
|
|
116
|
+
stale.push(cmd);
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
|
-
if (entry.bash) entry.bash = resolve(entry.bash);
|
|
101
|
-
if (entry.powershell) entry.powershell = resolve(entry.powershell);
|
|
102
|
-
if (entry.command) entry.command = resolve(entry.command);
|
|
103
119
|
}
|
|
104
120
|
}
|
|
105
|
-
return
|
|
121
|
+
return stale;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Parse a semver-ish version string ("2.9.1", "2.9.1-beta.1", "v2.9") into
|
|
125
|
+
// [major, minor, patch] numbers. Pre-release suffixes are ignored. Returns
|
|
126
|
+
// null if unparseable.
|
|
127
|
+
function parseSemver(v) {
|
|
128
|
+
if (typeof v !== "string") return null;
|
|
129
|
+
const m = v.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
130
|
+
if (!m) return null;
|
|
131
|
+
return [Number(m[1]), Number(m[2]), Number(m[3] || 0)];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Returns -1 if a < b, 0 if equal, 1 if a > b. Unparseable values sort last.
|
|
135
|
+
function compareSemver(a, b) {
|
|
136
|
+
const pa = parseSemver(a), pb = parseSemver(b);
|
|
137
|
+
if (!pa && !pb) return 0;
|
|
138
|
+
if (!pa) return 1;
|
|
139
|
+
if (!pb) return -1;
|
|
140
|
+
for (let i = 0; i < 3; i++) {
|
|
141
|
+
if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1;
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// When updating an existing config, only bump min_version forward — never
|
|
147
|
+
// downgrade if a teammate has already pinned a newer minimum.
|
|
148
|
+
function bumpMinVersion(existing, current) {
|
|
149
|
+
if (!existing) return current;
|
|
150
|
+
return compareSemver(existing, current) >= 0 ? existing : current;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add sticky-note-cli to the project package.json devDependencies so every
|
|
154
|
+
// teammate gets a pinned working copy on `npm install` (issue #12). No-ops
|
|
155
|
+
// gracefully if there is no package.json or if the entry is already present
|
|
156
|
+
// at >= the current version.
|
|
157
|
+
function pinCliInPackageJson() {
|
|
158
|
+
const pkgPath = path.join(process.cwd(), "package.json");
|
|
159
|
+
if (!fs.existsSync(pkgPath)) {
|
|
160
|
+
print(" ⏭️ No package.json found — skipping devDependency pin");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let pkg;
|
|
164
|
+
try {
|
|
165
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
print(` ⏭️ Could not parse package.json: ${err.message}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
171
|
+
const existing = pkg.devDependencies["sticky-note-cli"];
|
|
172
|
+
const desired = `^${VERSION}`;
|
|
173
|
+
if (existing) {
|
|
174
|
+
const existingClean = String(existing).replace(/^[\^~]/, "");
|
|
175
|
+
if (compareSemver(existingClean, VERSION) >= 0) {
|
|
176
|
+
print(` ⏭️ package.json devDependencies already pin sticky-note-cli ${existing}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
pkg.devDependencies["sticky-note-cli"] = desired;
|
|
181
|
+
// Keep keys sorted for clean diffs
|
|
182
|
+
pkg.devDependencies = Object.fromEntries(
|
|
183
|
+
Object.entries(pkg.devDependencies).sort(([a], [b]) => a.localeCompare(b))
|
|
184
|
+
);
|
|
185
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
186
|
+
print(` [OK] package.json devDependencies: sticky-note-cli ${desired}`);
|
|
187
|
+
print(" Run `npm install` to lock the version for your team.");
|
|
106
188
|
}
|
|
107
189
|
|
|
108
190
|
function makeExecutable(filePath) {
|
|
@@ -654,17 +736,32 @@ async function cmdInit() {
|
|
|
654
736
|
print(` [OK] .claude/hooks/${file}`);
|
|
655
737
|
}
|
|
656
738
|
|
|
657
|
-
// Create settings.json (Claude Code)
|
|
739
|
+
// Create settings.json (Claude Code) — uses relative hook paths so it can
|
|
740
|
+
// be safely committed and shared across teammates (issue #11).
|
|
658
741
|
const settingsTemplate = JSON.parse(readTemplate("settings.json"));
|
|
659
|
-
resolveHookPaths(settingsTemplate, process.cwd());
|
|
660
742
|
const settingsDest = path.join(process.cwd(), ".claude", "settings.json");
|
|
743
|
+
if (fs.existsSync(settingsDest)) {
|
|
744
|
+
const existing = readJsonSafe(settingsDest, {});
|
|
745
|
+
const stale = findStaleHookPaths(existing);
|
|
746
|
+
if (stale.length > 0) {
|
|
747
|
+
print(" ⚠️ .claude/settings.json contains absolute or shell-expanded hook paths:");
|
|
748
|
+
for (const cmd of stale.slice(0, 3)) print(` ${cmd}`);
|
|
749
|
+
print(" Rewriting with relative paths so hooks work for every teammate.");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
661
752
|
fs.writeFileSync(settingsDest, JSON.stringify(settingsTemplate, null, 2) + "\n");
|
|
662
753
|
print(" [OK] .claude/settings.json");
|
|
663
754
|
|
|
664
|
-
// Create hooks.json (Copilot CLI)
|
|
755
|
+
// Create hooks.json (Copilot CLI) — also relative paths.
|
|
665
756
|
const hooksTemplate = JSON.parse(readTemplate("hooks.json"));
|
|
666
|
-
resolveHookPaths(hooksTemplate, process.cwd());
|
|
667
757
|
const hooksDest = path.join(githubHooksDir, "hooks.json");
|
|
758
|
+
if (fs.existsSync(hooksDest)) {
|
|
759
|
+
const existing = readJsonSafe(hooksDest, {});
|
|
760
|
+
const stale = findStaleHookPaths(existing);
|
|
761
|
+
if (stale.length > 0) {
|
|
762
|
+
print(" ⚠️ .github/hooks/hooks.json contains absolute or shell-expanded hook paths — rewriting.");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
668
765
|
fs.writeFileSync(hooksDest, JSON.stringify(hooksTemplate, null, 2) + "\n");
|
|
669
766
|
print(" [OK] .github/hooks/hooks.json");
|
|
670
767
|
|
|
@@ -718,6 +815,7 @@ async function cmdInit() {
|
|
|
718
815
|
configTemplate.stale_days = staleDaysResolved;
|
|
719
816
|
configTemplate.inject_token_budget = injectTokenBudgetResolved;
|
|
720
817
|
configTemplate.hook_version = VERSION;
|
|
818
|
+
configTemplate.min_version = VERSION;
|
|
721
819
|
fs.writeFileSync(configDest, JSON.stringify(configTemplate, null, 2) + "\n");
|
|
722
820
|
} else {
|
|
723
821
|
// Update existing config with new settings
|
|
@@ -728,10 +826,15 @@ async function cmdInit() {
|
|
|
728
826
|
existing.stale_days = staleDaysResolved;
|
|
729
827
|
existing.inject_token_budget = injectTokenBudgetResolved;
|
|
730
828
|
existing.hook_version = VERSION;
|
|
829
|
+
existing.min_version = bumpMinVersion(existing.min_version, VERSION);
|
|
731
830
|
fs.writeFileSync(configDest, JSON.stringify(existing, null, 2) + "\n");
|
|
732
831
|
}
|
|
733
832
|
print(" [OK] .sticky-note/sticky-note-config.json");
|
|
734
833
|
|
|
834
|
+
// Pin sticky-note-cli in the project package.json devDependencies so every
|
|
835
|
+
// teammate gets the same version on `npm install` (issue #12).
|
|
836
|
+
pinCliInPackageJson();
|
|
837
|
+
|
|
735
838
|
// Update .gitignore
|
|
736
839
|
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
737
840
|
const ignoreAdditions = fs
|
|
@@ -998,8 +1101,11 @@ function cmdUpdate() {
|
|
|
998
1101
|
const settingsPath = path.join(process.cwd(), ".claude", "settings.json");
|
|
999
1102
|
if (fs.existsSync(settingsPath)) {
|
|
1000
1103
|
const settingsTemplate = JSON.parse(readTemplate("settings.json"));
|
|
1001
|
-
resolveHookPaths(settingsTemplate, process.cwd());
|
|
1002
1104
|
const existing = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
1105
|
+
const stale = findStaleHookPaths(existing);
|
|
1106
|
+
if (stale.length > 0) {
|
|
1107
|
+
print(" ⚠️ Existing settings.json had absolute hook paths — replacing with relative paths.");
|
|
1108
|
+
}
|
|
1003
1109
|
existing.hooks = settingsTemplate.hooks;
|
|
1004
1110
|
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n");
|
|
1005
1111
|
print(" [OK] .claude/settings.json (hooks updated)");
|
|
@@ -1009,7 +1115,6 @@ function cmdUpdate() {
|
|
|
1009
1115
|
const hooksPath = path.join(process.cwd(), ".github", "hooks", "hooks.json");
|
|
1010
1116
|
if (fs.existsSync(hooksPath)) {
|
|
1011
1117
|
const hooksTemplate = JSON.parse(readTemplate("hooks.json"));
|
|
1012
|
-
resolveHookPaths(hooksTemplate, process.cwd());
|
|
1013
1118
|
fs.writeFileSync(hooksPath, JSON.stringify(hooksTemplate, null, 2) + "\n");
|
|
1014
1119
|
print(" [OK] .github/hooks/hooks.json (updated)");
|
|
1015
1120
|
}
|
|
@@ -1161,6 +1266,33 @@ function cmdStatus() {
|
|
|
1161
1266
|
` ${fs.existsSync(hooksJsonPath) ? "[OK]" : "[ERR]"} .github/hooks/hooks.json`
|
|
1162
1267
|
);
|
|
1163
1268
|
|
|
1269
|
+
// Issue #11 guard: warn about absolute hook paths in shared config files.
|
|
1270
|
+
for (const [label, p] of [
|
|
1271
|
+
[".claude/settings.json", settingsPath],
|
|
1272
|
+
[".github/hooks/hooks.json", hooksJsonPath],
|
|
1273
|
+
]) {
|
|
1274
|
+
if (!fs.existsSync(p)) continue;
|
|
1275
|
+
const data = readJsonSafe(p, {});
|
|
1276
|
+
const stale = findStaleHookPaths(data);
|
|
1277
|
+
if (stale.length > 0) {
|
|
1278
|
+
print(` ⚠️ ${label} has absolute hook paths — hooks will fail for teammates.`);
|
|
1279
|
+
print(" Run `npx sticky-note init` to rewrite with relative paths.");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Issue #12 guard: warn if installed CLI is below the project's min_version.
|
|
1284
|
+
const configPathForVersionCheck = path.join(stickyDir, "sticky-note-config.json");
|
|
1285
|
+
if (fs.existsSync(configPathForVersionCheck)) {
|
|
1286
|
+
const cfg = readJsonSafe(configPathForVersionCheck, {});
|
|
1287
|
+
if (cfg.min_version && compareSemver(VERSION, cfg.min_version) < 0) {
|
|
1288
|
+
print(
|
|
1289
|
+
` ⚠️ Installed sticky-note-cli v${VERSION} is below the project's ` +
|
|
1290
|
+
`min_version v${cfg.min_version}.`
|
|
1291
|
+
);
|
|
1292
|
+
print(" Run `npm i -D sticky-note-cli@latest` to update.");
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1164
1296
|
// Sticky Note data — V2 format
|
|
1165
1297
|
const memoryPath = path.join(stickyDir, "sticky-note.json");
|
|
1166
1298
|
const configPath = path.join(stickyDir, "sticky-note-config.json");
|
|
@@ -1243,7 +1375,7 @@ function cmdStatus() {
|
|
|
1243
1375
|
const lastSeen = info.last_seen || "";
|
|
1244
1376
|
if (lastSeen) {
|
|
1245
1377
|
const ts = new Date(lastSeen).getTime();
|
|
1246
|
-
if (!isNaN(ts) && now - ts <
|
|
1378
|
+
if (!isNaN(ts) && now - ts < 60 * 60 * 1000) {
|
|
1247
1379
|
activeUsers.push(path.basename(f, ".json"));
|
|
1248
1380
|
}
|
|
1249
1381
|
}
|
|
@@ -1687,7 +1819,7 @@ function cmdWho() {
|
|
|
1687
1819
|
for (const [user, info] of Object.entries(data)) {
|
|
1688
1820
|
const lastSeen = info.last_seen || "";
|
|
1689
1821
|
const ts = lastSeen ? new Date(lastSeen).getTime() : 0;
|
|
1690
|
-
const active = !isNaN(ts) && now - ts <
|
|
1822
|
+
const active = !isNaN(ts) && now - ts < 60 * 60 * 1000;
|
|
1691
1823
|
const ago = lastSeen ? _relativeTime(lastSeen) : "unknown";
|
|
1692
1824
|
const files = (info.active_files || []).slice(0, 5).join(", ");
|
|
1693
1825
|
const marker = active ? "[ACTIVE]" : "[IDLE]";
|
|
@@ -1713,7 +1845,7 @@ function cmdWho() {
|
|
|
1713
1845
|
const info = readJsonSafe(path.join(presenceDir, f), {});
|
|
1714
1846
|
const lastSeen = info.last_seen || "";
|
|
1715
1847
|
const ts = lastSeen ? new Date(lastSeen).getTime() : 0;
|
|
1716
|
-
const active = !isNaN(ts) && now - ts <
|
|
1848
|
+
const active = !isNaN(ts) && now - ts < 60 * 60 * 1000;
|
|
1717
1849
|
users.push({ user, info, lastSeen, active });
|
|
1718
1850
|
}
|
|
1719
1851
|
|
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": "2.9.
|
|
3
|
+
"version": "2.9.2",
|
|
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**
|
|
@@ -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
|
});
|
|
@@ -33,13 +33,78 @@ 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
|
+
|
|
43
108
|
const {
|
|
44
109
|
getMemoryPath,
|
|
45
110
|
getConfigPath,
|
|
@@ -121,6 +186,59 @@ function autoCloseCopilotCliThreads(memory, autoCloseHours) {
|
|
|
121
186
|
|
|
122
187
|
// ── Formatting helpers ────────────────────────────────────
|
|
123
188
|
|
|
189
|
+
function formatStuckBanner(threads) {
|
|
190
|
+
const stuck = (threads || []).filter((t) => t && t.status === "stuck");
|
|
191
|
+
if (stuck.length === 0) return "";
|
|
192
|
+
|
|
193
|
+
const R = "\x1b[0m";
|
|
194
|
+
const Y = "\x1b[33m";
|
|
195
|
+
const BY = "\x1b[1;33m";
|
|
196
|
+
const BR = "\x1b[1;31m";
|
|
197
|
+
const B = "\x1b[1m";
|
|
198
|
+
const C = "\x1b[36m";
|
|
199
|
+
const D = "\x1b[2m";
|
|
200
|
+
const bar = "━".repeat(52);
|
|
201
|
+
|
|
202
|
+
const lines = [
|
|
203
|
+
"",
|
|
204
|
+
`${Y}${bar}${R}`,
|
|
205
|
+
`${BY} ⚠️ STUCK THREADS${R}`,
|
|
206
|
+
`${Y}${bar}${R}`,
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
for (const t of stuck.slice(0, 5)) {
|
|
210
|
+
const user = t.user || t.author || "unknown";
|
|
211
|
+
const branch = t.branch ? ` ${D}·${R} ${C}${t.branch}${R}` : "";
|
|
212
|
+
const files = (t.files_touched || []).slice(0, 3).join(", ");
|
|
213
|
+
const narrative = t.narrative || t.last_note || "";
|
|
214
|
+
const snip =
|
|
215
|
+
narrative.length > 80 ? narrative.substring(0, 80) + "…" : narrative;
|
|
216
|
+
const threadId = (t.id || "").substring(0, 8);
|
|
217
|
+
const failed = t.failed_approaches || [];
|
|
218
|
+
|
|
219
|
+
lines.push(
|
|
220
|
+
`${Y} ┃${R} ${BR}[STUCK]${R} ${B}${user}${R}${branch}`
|
|
221
|
+
);
|
|
222
|
+
if (files) lines.push(`${Y} ┃${R} Files: ${files}`);
|
|
223
|
+
if (snip) lines.push(`${Y} ┃${R} ${D}"${snip}"${R}`);
|
|
224
|
+
if (failed.length > 0) {
|
|
225
|
+
lines.push(
|
|
226
|
+
`${Y} ┃${R} ${BR}⚠️ ${failed.length} failed approach(es)${R}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
lines.push(
|
|
230
|
+
`${Y} ┃${R} → Resume: ${C}npx sticky-note resume ${threadId}${R}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (stuck.length > 5) {
|
|
235
|
+
lines.push(`${Y} ┃${R} ... and ${stuck.length - 5} more`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
lines.push(`${Y}${bar}${R}`, "");
|
|
239
|
+
return lines.join("\n") + "\n";
|
|
240
|
+
}
|
|
241
|
+
|
|
124
242
|
function formatThreadsForInjection(threads, maxThreads, maxTokens) {
|
|
125
243
|
maxThreads = maxThreads || 10;
|
|
126
244
|
maxTokens = maxTokens || 500;
|
|
@@ -251,7 +369,7 @@ function formatPresence(presenceData, threads) {
|
|
|
251
369
|
try {
|
|
252
370
|
const ts = new Date(lastSeen).getTime();
|
|
253
371
|
if (isNaN(ts)) continue;
|
|
254
|
-
if (now - ts <
|
|
372
|
+
if (now - ts < 60 * 60 * 1000) {
|
|
255
373
|
const files = (info.active_files || []).slice(0, 3).join(", ");
|
|
256
374
|
const thread = userThreads[user];
|
|
257
375
|
let line = `- **${user}**`;
|
|
@@ -911,6 +1029,7 @@ function main() {
|
|
|
911
1029
|
const overlapContext = formatOverlapWarnings(
|
|
912
1030
|
(memory.threads || []).filter(Boolean), getUser(), memory
|
|
913
1031
|
);
|
|
1032
|
+
const versionWarning = formatVersionWarning(config);
|
|
914
1033
|
|
|
915
1034
|
// V2.5: Mark eagerly-injected stuck threads so PreToolUse won't re-inject.
|
|
916
1035
|
// Skip for Copilot CLI — its sessionStart output is not surfaced to the AI,
|
|
@@ -930,6 +1049,41 @@ function main() {
|
|
|
930
1049
|
|
|
931
1050
|
saveMemoryMerged(memoryPath, memory);
|
|
932
1051
|
|
|
1052
|
+
// ── Stuck threads startup banner ────────────────────────
|
|
1053
|
+
// Write a visible banner to stderr so the user sees stuck threads
|
|
1054
|
+
// immediately when the session starts, before any AI interaction.
|
|
1055
|
+
const stuckThreads = (memory.threads || [])
|
|
1056
|
+
.filter((t) => t && t.status === "stuck");
|
|
1057
|
+
if (stuckThreads.length > 0) {
|
|
1058
|
+
const fs = require("fs");
|
|
1059
|
+
const bannerShownPath = path.join(
|
|
1060
|
+
path.dirname(memoryPath),
|
|
1061
|
+
".sticky-banner-shown"
|
|
1062
|
+
);
|
|
1063
|
+
let showBanner = true;
|
|
1064
|
+
|
|
1065
|
+
// Copilot CLI fires sessionStart per-turn — only show once per session.
|
|
1066
|
+
if (isCopilotCli) {
|
|
1067
|
+
try {
|
|
1068
|
+
const shown = fs.readFileSync(bannerShownPath, "utf-8").trim();
|
|
1069
|
+
if (shown === sessionId) showBanner = false;
|
|
1070
|
+
} catch (_) {
|
|
1071
|
+
/* file doesn't exist yet */
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (showBanner) {
|
|
1076
|
+
process.stderr.write(formatStuckBanner(stuckThreads));
|
|
1077
|
+
if (isCopilotCli) {
|
|
1078
|
+
try {
|
|
1079
|
+
fs.writeFileSync(bannerShownPath, sessionId, "utf-8");
|
|
1080
|
+
} catch (_) {
|
|
1081
|
+
/* non-fatal */
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
933
1087
|
// ── Assemble output ───────────────────────────────────
|
|
934
1088
|
const parts = [];
|
|
935
1089
|
|
|
@@ -1013,6 +1167,7 @@ function main() {
|
|
|
1013
1167
|
}
|
|
1014
1168
|
|
|
1015
1169
|
// Overlap warning comes first — most urgent signal
|
|
1170
|
+
if (versionWarning) parts.push(versionWarning);
|
|
1016
1171
|
if (overlapContext) parts.push(overlapContext);
|
|
1017
1172
|
|
|
1018
1173
|
if (threadContext) parts.push(threadContext);
|
|
@@ -1028,6 +1183,8 @@ function main() {
|
|
|
1028
1183
|
try {
|
|
1029
1184
|
main();
|
|
1030
1185
|
} catch (err) {
|
|
1031
|
-
try { utils.
|
|
1186
|
+
try { utils.reportHookError("session-start", err); } catch (_) {
|
|
1187
|
+
try { utils.logHookError("session-start", err); } catch (_) {}
|
|
1188
|
+
}
|
|
1032
1189
|
_safeExit();
|
|
1033
1190
|
}
|
|
@@ -57,6 +57,94 @@ function logHookError(hookName, error) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Issue #14: Surface hook failures to stderr so the host tool's generic
|
|
61
|
+
// "<HookName> hook error" message has actionable detail next to it. Earlier
|
|
62
|
+
// versions silently swallowed `require()` failures and top-level exceptions,
|
|
63
|
+
// which made stale-path issues (#13) and other crashes impossible to diagnose
|
|
64
|
+
// — particularly under UserPromptSubmit, which Claude Code reports without
|
|
65
|
+
// a stack trace. This helper prefixes every line with `[STICKY-NOTE]` so the
|
|
66
|
+
// user can spot it in mixed hook output.
|
|
67
|
+
function reportHookError(hookName, error, hint) {
|
|
68
|
+
const msg = error instanceof Error ? (error.stack || error.message) : String(error);
|
|
69
|
+
const lines = [`[STICKY-NOTE] ${hookName} hook error: ${msg}`];
|
|
70
|
+
if (hint) lines.push(`[STICKY-NOTE] hint: ${hint}`);
|
|
71
|
+
try { process.stderr.write(lines.join("\n") + "\n"); } catch (_) { /* ignore */ }
|
|
72
|
+
try { logHookError(hookName, error); } catch (_) { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Issue #13: When a teammate copies `.claude/settings.json` from another
|
|
76
|
+
// machine (or runs an older sticky-note-cli that wrote absolute paths), the
|
|
77
|
+
// hook commands point at non-existent absolute paths and every hook fails
|
|
78
|
+
// with MODULE_NOT_FOUND. Re-running `npx sticky-note init` fixes it, but
|
|
79
|
+
// users typically don't know that — they just see "<Hook> hook error" each
|
|
80
|
+
// session. This helper detects and rewrites those stale commands in-place
|
|
81
|
+
// the first time any hook (e.g. session-start, inject-context) successfully
|
|
82
|
+
// loads, so the next session starts clean.
|
|
83
|
+
//
|
|
84
|
+
// The rewritten command always uses the relative form `node ".claude/hooks/<basename>"`,
|
|
85
|
+
// matching what `templates/settings.json` ships today. We extract `<basename>`
|
|
86
|
+
// from the existing command so we never invent hook scripts that don't exist.
|
|
87
|
+
//
|
|
88
|
+
// Returns the list of (hookEvent, oldCommand, newCommand) tuples that were
|
|
89
|
+
// rewritten, or [] if nothing was stale.
|
|
90
|
+
function selfHealHookPaths(settingsPath) {
|
|
91
|
+
const result = [];
|
|
92
|
+
try {
|
|
93
|
+
if (!settingsPath) {
|
|
94
|
+
settingsPath = path.join(process.cwd(), ".claude", "settings.json");
|
|
95
|
+
}
|
|
96
|
+
if (!fs.existsSync(settingsPath)) return result;
|
|
97
|
+
|
|
98
|
+
const raw = fs.readFileSync(settingsPath, "utf-8");
|
|
99
|
+
let settings;
|
|
100
|
+
try { settings = JSON.parse(raw); } catch (_) { return result; }
|
|
101
|
+
if (!settings || !settings.hooks) return result;
|
|
102
|
+
|
|
103
|
+
// Match the same shapes findStaleHookPaths() in bin/cli.js looks for:
|
|
104
|
+
// any absolute path inside the command string, or a $(git rev-parse ...)
|
|
105
|
+
// shell expansion that won't work on Windows / under Claude Code.
|
|
106
|
+
const STALE = /["'][A-Za-z]:[\\/]|["']\/(?:Users|home|root|tmp)\/|\$\(git rev-parse/;
|
|
107
|
+
// Match the script basename in either quoted or bare form.
|
|
108
|
+
const BASENAME = /([A-Za-z0-9_.-]+\.(?:js|sh|cjs|mjs))(?:["'`\s]|$)/;
|
|
109
|
+
|
|
110
|
+
let changed = false;
|
|
111
|
+
for (const [event, entries] of Object.entries(settings.hooks)) {
|
|
112
|
+
if (!Array.isArray(entries)) continue;
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
for (const h of (entry && entry.hooks) || []) {
|
|
115
|
+
const cmd = h && h.command;
|
|
116
|
+
if (typeof cmd !== "string" || !STALE.test(cmd)) continue;
|
|
117
|
+
const m = cmd.match(BASENAME);
|
|
118
|
+
if (!m) continue;
|
|
119
|
+
const basename = m[1];
|
|
120
|
+
const newCmd = `node ".claude/hooks/${basename}"`;
|
|
121
|
+
result.push({ event, oldCommand: cmd, newCommand: newCmd });
|
|
122
|
+
h.command = newCmd;
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!changed) return result;
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
131
|
+
try {
|
|
132
|
+
const summary =
|
|
133
|
+
`[STICKY-NOTE] ⚠️ Detected ${result.length} stale absolute hook path(s) in ` +
|
|
134
|
+
`${path.relative(process.cwd(), settingsPath) || settingsPath} — rewrote to ` +
|
|
135
|
+
`relative paths so hooks work on this machine. Commit the change to share ` +
|
|
136
|
+
`the fix with your team.`;
|
|
137
|
+
process.stderr.write(summary + "\n");
|
|
138
|
+
for (const { event, oldCommand, newCommand } of result.slice(0, 5)) {
|
|
139
|
+
process.stderr.write(`[STICKY-NOTE] ${event}: ${oldCommand} → ${newCommand}\n`);
|
|
140
|
+
}
|
|
141
|
+
} catch (_) { /* non-fatal */ }
|
|
142
|
+
} catch (err) {
|
|
143
|
+
try { logHookError("selfHealHookPaths", err); } catch (_) { /* ignore */ }
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
60
148
|
const _paths = {
|
|
61
149
|
memory: "sticky-note.json",
|
|
62
150
|
config: "sticky-note-config.json",
|
|
@@ -947,6 +1035,8 @@ module.exports = {
|
|
|
947
1035
|
RETRY_PATTERNS,
|
|
948
1036
|
FILE_PATH_PATTERN,
|
|
949
1037
|
logHookError,
|
|
1038
|
+
reportHookError,
|
|
1039
|
+
selfHealHookPaths,
|
|
950
1040
|
getMemoryPath,
|
|
951
1041
|
getConfigPath,
|
|
952
1042
|
getAuditPath,
|
package/templates/hooks.json
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"hooks": {
|
|
4
|
-
"sessionStart": [
|
|
5
|
-
{
|
|
6
|
-
"type": "command",
|
|
7
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
8
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
9
|
-
}
|
|
10
|
-
],
|
|
11
|
-
"sessionEnd": [
|
|
12
|
-
{
|
|
13
|
-
"type": "command",
|
|
14
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
15
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
16
|
-
}
|
|
17
|
-
],
|
|
18
|
-
"userPromptSubmitted": [
|
|
19
|
-
{
|
|
20
|
-
"type": "command",
|
|
21
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
22
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
"preToolUse": [
|
|
26
|
-
{
|
|
27
|
-
"type": "command",
|
|
28
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
29
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
30
|
-
}
|
|
31
|
-
],
|
|
32
|
-
"postToolUse": [
|
|
33
|
-
{
|
|
34
|
-
"type": "command",
|
|
35
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
36
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"errorOccurred": [
|
|
40
|
-
{
|
|
41
|
-
"type": "command",
|
|
42
|
-
"bash": "COPILOT_CLI=1 node \"
|
|
43
|
-
"powershell": "$env:COPILOT_CLI='1'; node \"
|
|
44
|
-
}
|
|
45
|
-
]
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"sessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"type": "command",
|
|
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
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"sessionEnd": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
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
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"userPromptSubmitted": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
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
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"preToolUse": [
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
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
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"postToolUse": [
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
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
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"errorOccurred": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
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
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
package/templates/settings.json
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"SessionStart": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "node \"
|
|
10
|
-
}
|
|
11
|
-
]
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"SessionEnd": [
|
|
15
|
-
{
|
|
16
|
-
"matcher": "",
|
|
17
|
-
"hooks": [
|
|
18
|
-
{
|
|
19
|
-
"type": "command",
|
|
20
|
-
"command": "node \"
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
"UserPromptSubmit": [
|
|
26
|
-
{
|
|
27
|
-
"matcher": "",
|
|
28
|
-
"hooks": [
|
|
29
|
-
{
|
|
30
|
-
"type": "command",
|
|
31
|
-
"command": "node \"
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
],
|
|
36
|
-
"PreToolUse": [
|
|
37
|
-
{
|
|
38
|
-
"matcher": "",
|
|
39
|
-
"hooks": [
|
|
40
|
-
{
|
|
41
|
-
"type": "command",
|
|
42
|
-
"command": "node \"
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
],
|
|
47
|
-
"PostToolUse": [
|
|
48
|
-
{
|
|
49
|
-
"matcher": "",
|
|
50
|
-
"hooks": [
|
|
51
|
-
{
|
|
52
|
-
"type": "command",
|
|
53
|
-
"command": "node \"
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
}
|
|
57
|
-
],
|
|
58
|
-
"Stop": [
|
|
59
|
-
{
|
|
60
|
-
"matcher": "",
|
|
61
|
-
"hooks": [
|
|
62
|
-
{
|
|
63
|
-
"type": "command",
|
|
64
|
-
"command": "node \"
|
|
65
|
-
}
|
|
66
|
-
]
|
|
67
|
-
}
|
|
68
|
-
],
|
|
69
|
-
"PostToolUseFailure": [
|
|
70
|
-
{
|
|
71
|
-
"matcher": "",
|
|
72
|
-
"hooks": [
|
|
73
|
-
{
|
|
74
|
-
"type": "command",
|
|
75
|
-
"command": "node \"
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
}
|
|
79
|
-
]
|
|
80
|
-
}
|
|
81
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \".claude/hooks/session-start.js\""
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"SessionEnd": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \".claude/hooks/session-end.js\""
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"UserPromptSubmit": [
|
|
26
|
+
{
|
|
27
|
+
"matcher": "",
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node \".claude/hooks/inject-context.js\""
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"PreToolUse": [
|
|
37
|
+
{
|
|
38
|
+
"matcher": "",
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "node \".claude/hooks/pre-tool-use.js\""
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"PostToolUse": [
|
|
48
|
+
{
|
|
49
|
+
"matcher": "",
|
|
50
|
+
"hooks": [
|
|
51
|
+
{
|
|
52
|
+
"type": "command",
|
|
53
|
+
"command": "node \".claude/hooks/track-work.js\""
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"Stop": [
|
|
59
|
+
{
|
|
60
|
+
"matcher": "",
|
|
61
|
+
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "node \".claude/hooks/on-stop.js\""
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"PostToolUseFailure": [
|
|
70
|
+
{
|
|
71
|
+
"matcher": "",
|
|
72
|
+
"hooks": [
|
|
73
|
+
{
|
|
74
|
+
"type": "command",
|
|
75
|
+
"command": "node \".claude/hooks/on-error.js\""
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|