sticky-note-cli 3.1.10 → 3.1.12
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 +5 -12
- package/package.json +3 -2
- package/sticky-server/LICENSE +31 -0
- package/sticky-server/adapters/cf-kv.js +203 -0
- package/sticky-server/worker.js +229 -0
- package/sticky-server/wrangler.toml +10 -0
- package/templates/hooks/sticky-utils.js +18 -13
package/bin/cli.js
CHANGED
|
@@ -95,9 +95,8 @@ function resolveHookPaths(_obj, _rootDir) {
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// Detects hook commands in an existing settings.json that
|
|
99
|
-
//
|
|
100
|
-
// machine). Returns the list of stale command strings, or [] if all clean.
|
|
98
|
+
// Detects hook commands in an existing settings.json that are broken or stale.
|
|
99
|
+
// Returns the list of stale command strings, or [] if all clean.
|
|
101
100
|
function findStaleHookPaths(settings) {
|
|
102
101
|
const stale = [];
|
|
103
102
|
const hooks = (settings && settings.hooks) || {};
|
|
@@ -107,9 +106,6 @@ function findStaleHookPaths(settings) {
|
|
|
107
106
|
const cmd = h && h.command;
|
|
108
107
|
if (typeof cmd !== "string") continue;
|
|
109
108
|
// Absolute paths (Unix /Users, /home, /root or Windows C:/, D:/ etc.)
|
|
110
|
-
// inside the hook command are the smoking gun. Match anywhere in the
|
|
111
|
-
// string — the path may be preceded by other tokens (env vars, shell
|
|
112
|
-
// prefixes) and may contain spaces.
|
|
113
109
|
if (
|
|
114
110
|
/["'][A-Za-z]:[\\/]/.test(cmd) ||
|
|
115
111
|
/["']\/(?:Users|home|root|tmp)\//.test(cmd) ||
|
|
@@ -117,12 +113,9 @@ function findStaleHookPaths(settings) {
|
|
|
117
113
|
) {
|
|
118
114
|
stale.push(cmd);
|
|
119
115
|
}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
// not relative to where .claude/settings.json lives. The fix is to
|
|
124
|
-
// use `npx sticky-note run-hook foo` which walks up to find the hooks.
|
|
125
|
-
if (/node\s+["']\.claude[\\/]hooks[\\/][^"']+\.js["']/.test(cmd)) {
|
|
116
|
+
// Broken `npx sticky-note run-hook X` — npm has no package named "sticky-note"
|
|
117
|
+
// (the package is "sticky-note-cli"). Written by earlier 3.x releases.
|
|
118
|
+
if (/^npx\s+sticky-note\s+run-hook\s+\S+/.test(cmd)) {
|
|
126
119
|
stale.push(cmd);
|
|
127
120
|
}
|
|
128
121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sticky-note-cli",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.12",
|
|
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": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"bin/",
|
|
28
|
-
"templates/"
|
|
28
|
+
"templates/",
|
|
29
|
+
"sticky-server/"
|
|
29
30
|
],
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=16.0.0"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 Dheeraj Bandaru
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published
|
|
8
|
+
by the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
Additional permission under GNU AGPL version 3 section 7:
|
|
20
|
+
|
|
21
|
+
If you modify this Program, or any covered work, by linking or combining
|
|
22
|
+
it with other code, such combination is not considered a "covered work"
|
|
23
|
+
under the AGPL, provided that the other code is licensed under a
|
|
24
|
+
permissive license (MIT, BSD, Apache 2.0, or similar).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
The client layer of Sticky Note (bin/, templates/, hooks) is licensed
|
|
29
|
+
under the MIT License. See the root LICENSE file.
|
|
30
|
+
|
|
31
|
+
Only the sticky-server/ directory is covered by this AGPL license.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare KV adapter for Sticky Note V3.
|
|
3
|
+
*
|
|
4
|
+
* KV key schema (all prefixed by project):
|
|
5
|
+
* {project}:thread:{uuid} → thread JSON
|
|
6
|
+
* {project}:thread-index → JSON array of thread UUIDs
|
|
7
|
+
* {project}:audit:{timestamp}:{uuid} → audit record JSON
|
|
8
|
+
* {project}:audit-index → JSON array of audit keys
|
|
9
|
+
* {project}:presence:{user} → presence record JSON
|
|
10
|
+
* {project}:presence-index → JSON array of usernames
|
|
11
|
+
* {project}:config → config JSON
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Threads ──────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
async function getThreads(kv, project, filters = {}) {
|
|
17
|
+
const indexKey = `${project}:thread-index`;
|
|
18
|
+
const index = JSON.parse((await kv.get(indexKey)) || "[]");
|
|
19
|
+
|
|
20
|
+
if (index.length === 0) return [];
|
|
21
|
+
|
|
22
|
+
const threads = await Promise.all(
|
|
23
|
+
index.map(async (id) => {
|
|
24
|
+
const raw = await kv.get(`${project}:thread:${id}`);
|
|
25
|
+
return raw ? JSON.parse(raw) : null;
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
let result = threads.filter(Boolean);
|
|
30
|
+
|
|
31
|
+
if (filters.status) {
|
|
32
|
+
result = result.filter((t) => t.status === filters.status);
|
|
33
|
+
}
|
|
34
|
+
if (filters.q) {
|
|
35
|
+
const q = filters.q.toLowerCase();
|
|
36
|
+
result = result.filter(
|
|
37
|
+
(t) =>
|
|
38
|
+
(t.narrative || "").toLowerCase().includes(q) ||
|
|
39
|
+
(t.handoff_summary || "").toLowerCase().includes(q) ||
|
|
40
|
+
(t.last_note || "").toLowerCase().includes(q) ||
|
|
41
|
+
(t.files_touched || []).some((f) => f.toLowerCase().includes(q))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getThread(kv, project, id) {
|
|
49
|
+
const raw = await kv.get(`${project}:thread:${id}`);
|
|
50
|
+
return raw ? JSON.parse(raw) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function putThread(kv, project, thread) {
|
|
54
|
+
const id = thread.id;
|
|
55
|
+
await kv.put(`${project}:thread:${id}`, JSON.stringify(thread));
|
|
56
|
+
|
|
57
|
+
// Update index
|
|
58
|
+
const indexKey = `${project}:thread-index`;
|
|
59
|
+
const index = JSON.parse((await kv.get(indexKey)) || "[]");
|
|
60
|
+
if (!index.includes(id)) {
|
|
61
|
+
index.push(id);
|
|
62
|
+
await kv.put(indexKey, JSON.stringify(index));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function deleteThread(kv, project, id) {
|
|
67
|
+
// Tombstone: keep minimal record
|
|
68
|
+
const existing = await getThread(kv, project, id);
|
|
69
|
+
if (existing) {
|
|
70
|
+
const tombstone = {
|
|
71
|
+
id: existing.id,
|
|
72
|
+
user: existing.user,
|
|
73
|
+
status: "expired",
|
|
74
|
+
closed_at: existing.closed_at || new Date().toISOString(),
|
|
75
|
+
created_at: existing.created_at,
|
|
76
|
+
};
|
|
77
|
+
await kv.put(`${project}:thread:${id}`, JSON.stringify(tombstone));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Audit ────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
async function appendAudit(kv, project, record) {
|
|
84
|
+
const ts = record.timestamp || new Date().toISOString();
|
|
85
|
+
const uid = crypto.randomUUID();
|
|
86
|
+
const key = `${project}:audit:${ts}:${uid}`;
|
|
87
|
+
await kv.put(key, JSON.stringify(record));
|
|
88
|
+
|
|
89
|
+
const indexKey = `${project}:audit-index`;
|
|
90
|
+
const index = JSON.parse((await kv.get(indexKey)) || "[]");
|
|
91
|
+
index.push(key);
|
|
92
|
+
|
|
93
|
+
// Cap at 10,000 entries; trim oldest on overflow
|
|
94
|
+
if (index.length > 10000) {
|
|
95
|
+
const removed = index.splice(0, index.length - 10000);
|
|
96
|
+
await Promise.all(removed.map((k) => kv.delete(k)));
|
|
97
|
+
}
|
|
98
|
+
await kv.put(indexKey, JSON.stringify(index));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function queryAudit(kv, project, filters = {}) {
|
|
102
|
+
const indexKey = `${project}:audit-index`;
|
|
103
|
+
const index = JSON.parse((await kv.get(indexKey)) || "[]");
|
|
104
|
+
|
|
105
|
+
if (index.length === 0) return [];
|
|
106
|
+
|
|
107
|
+
const records = await Promise.all(
|
|
108
|
+
index.map(async (key) => {
|
|
109
|
+
const raw = await kv.get(key);
|
|
110
|
+
return raw ? JSON.parse(raw) : null;
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
let result = records.filter(Boolean);
|
|
115
|
+
|
|
116
|
+
if (filters.user) {
|
|
117
|
+
result = result.filter((r) => r.user === filters.user);
|
|
118
|
+
}
|
|
119
|
+
if (filters.file) {
|
|
120
|
+
result = result.filter((r) => (r.file || "").includes(filters.file));
|
|
121
|
+
}
|
|
122
|
+
if (filters.tool) {
|
|
123
|
+
result = result.filter((r) => r.tool === filters.tool);
|
|
124
|
+
}
|
|
125
|
+
if (filters.since) {
|
|
126
|
+
const since = new Date(filters.since).getTime();
|
|
127
|
+
result = result.filter(
|
|
128
|
+
(r) => new Date(r.timestamp || 0).getTime() >= since
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Presence ─────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
const PRESENCE_TTL_SECONDS = 15 * 60; // 15 minutes
|
|
138
|
+
|
|
139
|
+
async function getPresence(kv, project) {
|
|
140
|
+
const indexKey = `${project}:presence-index`;
|
|
141
|
+
const index = JSON.parse((await kv.get(indexKey)) || "[]");
|
|
142
|
+
|
|
143
|
+
if (index.length === 0) return [];
|
|
144
|
+
|
|
145
|
+
const records = await Promise.all(
|
|
146
|
+
index.map(async (user) => {
|
|
147
|
+
const raw = await kv.get(`${project}:presence:${user}`);
|
|
148
|
+
return raw ? JSON.parse(raw) : null;
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return records.filter(Boolean);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function upsertPresence(kv, project, record) {
|
|
156
|
+
const user = record.user;
|
|
157
|
+
await kv.put(`${project}:presence:${user}`, JSON.stringify(record), {
|
|
158
|
+
expirationTtl: PRESENCE_TTL_SECONDS,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const indexKey = `${project}:presence-index`;
|
|
162
|
+
const index = JSON.parse((await kv.get(indexKey)) || "[]");
|
|
163
|
+
if (!index.includes(user)) {
|
|
164
|
+
index.push(user);
|
|
165
|
+
await kv.put(indexKey, JSON.stringify(index));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function deletePresence(kv, project, user) {
|
|
170
|
+
await kv.delete(`${project}:presence:${user}`);
|
|
171
|
+
|
|
172
|
+
const indexKey = `${project}:presence-index`;
|
|
173
|
+
const index = JSON.parse((await kv.get(indexKey)) || "[]");
|
|
174
|
+
const filtered = index.filter((u) => u !== user);
|
|
175
|
+
await kv.put(indexKey, JSON.stringify(filtered));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Config ───────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
async function getConfig(kv, project) {
|
|
181
|
+
const raw = await kv.get(`${project}:config`);
|
|
182
|
+
return raw ? JSON.parse(raw) : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function putConfig(kv, project, config) {
|
|
186
|
+
await kv.put(`${project}:config`, JSON.stringify(config));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Export ────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
getThreads,
|
|
193
|
+
getThread,
|
|
194
|
+
putThread,
|
|
195
|
+
deleteThread,
|
|
196
|
+
appendAudit,
|
|
197
|
+
queryAudit,
|
|
198
|
+
getPresence,
|
|
199
|
+
upsertPresence,
|
|
200
|
+
deletePresence,
|
|
201
|
+
getConfig,
|
|
202
|
+
putConfig,
|
|
203
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticky Note V3 — Cloudflare Worker
|
|
3
|
+
*
|
|
4
|
+
* REST API backed by Cloudflare KV.
|
|
5
|
+
* Auth: X-Sticky-API-Key header matched against STICKY_API_KEY env var.
|
|
6
|
+
* Project routing: X-Sticky-Project header (auto-set by client hooks).
|
|
7
|
+
*
|
|
8
|
+
* Endpoints:
|
|
9
|
+
* POST /threads — create thread
|
|
10
|
+
* GET /threads — list threads (?status=, ?q=)
|
|
11
|
+
* GET /threads/:id — get thread
|
|
12
|
+
* PUT /threads/:id — update thread
|
|
13
|
+
* DELETE /threads/:id — tombstone thread
|
|
14
|
+
* GET /audit — query audit (?file=, ?user=, ?since=, ?tool=)
|
|
15
|
+
* POST /audit — append audit record
|
|
16
|
+
* GET /presence — list active developers
|
|
17
|
+
* POST /presence — heartbeat upsert
|
|
18
|
+
* DELETE /presence/:user — clear presence
|
|
19
|
+
* GET /config — get team config
|
|
20
|
+
* PUT /config — update team config
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as adapter from "./adapters/cf-kv.js";
|
|
24
|
+
|
|
25
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function json(data, status = 200) {
|
|
28
|
+
return new Response(JSON.stringify(data), {
|
|
29
|
+
status,
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function error(message, status = 400) {
|
|
35
|
+
return json({ error: message }, status);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function authenticate(request, env) {
|
|
39
|
+
const key = request.headers.get("X-Sticky-API-Key");
|
|
40
|
+
if (!env.STICKY_API_KEY) return true; // no key configured = open
|
|
41
|
+
return key === env.STICKY_API_KEY;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getProject(request) {
|
|
45
|
+
return request.headers.get("X-Sticky-Project") || "default";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Router ───────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
function matchRoute(method, pathname) {
|
|
51
|
+
// Thread routes
|
|
52
|
+
if (method === "POST" && pathname === "/threads") return { handler: "createThread" };
|
|
53
|
+
if (method === "GET" && pathname === "/threads") return { handler: "listThreads" };
|
|
54
|
+
if (method === "GET" && pathname.startsWith("/threads/")) {
|
|
55
|
+
return { handler: "getThread", id: pathname.slice("/threads/".length) };
|
|
56
|
+
}
|
|
57
|
+
if (method === "PUT" && pathname.startsWith("/threads/")) {
|
|
58
|
+
return { handler: "updateThread", id: pathname.slice("/threads/".length) };
|
|
59
|
+
}
|
|
60
|
+
if (method === "DELETE" && pathname.startsWith("/threads/")) {
|
|
61
|
+
return { handler: "deleteThread", id: pathname.slice("/threads/".length) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Audit routes
|
|
65
|
+
if (method === "GET" && pathname === "/audit") return { handler: "queryAudit" };
|
|
66
|
+
if (method === "POST" && pathname === "/audit") return { handler: "appendAudit" };
|
|
67
|
+
|
|
68
|
+
// Presence routes
|
|
69
|
+
if (method === "GET" && pathname === "/presence") return { handler: "getPresence" };
|
|
70
|
+
if (method === "POST" && pathname === "/presence") return { handler: "upsertPresence" };
|
|
71
|
+
if (method === "DELETE" && pathname.startsWith("/presence/")) {
|
|
72
|
+
return { handler: "deletePresence", user: decodeURIComponent(pathname.slice("/presence/".length)) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Config routes
|
|
76
|
+
if (method === "GET" && pathname === "/config") return { handler: "getConfig" };
|
|
77
|
+
if (method === "PUT" && pathname === "/config") return { handler: "putConfig" };
|
|
78
|
+
|
|
79
|
+
// Health
|
|
80
|
+
if (method === "GET" && pathname === "/health") return { handler: "health" };
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Handlers ─────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const handlers = {
|
|
88
|
+
async health() {
|
|
89
|
+
return json({ status: "ok", version: "3.0.0" });
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// ── Threads ──
|
|
93
|
+
|
|
94
|
+
async createThread(request, kv, project) {
|
|
95
|
+
const thread = await request.json();
|
|
96
|
+
if (!thread.id) return error("thread.id is required");
|
|
97
|
+
await adapter.putThread(kv, project, thread);
|
|
98
|
+
return json(thread, 201);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async listThreads(request, kv, project) {
|
|
102
|
+
const url = new URL(request.url);
|
|
103
|
+
const filters = {
|
|
104
|
+
status: url.searchParams.get("status") || undefined,
|
|
105
|
+
q: url.searchParams.get("q") || undefined,
|
|
106
|
+
};
|
|
107
|
+
const threads = await adapter.getThreads(kv, project, filters);
|
|
108
|
+
return json({ threads });
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async getThread(_request, kv, project, { id }) {
|
|
112
|
+
const thread = await adapter.getThread(kv, project, id);
|
|
113
|
+
if (!thread) return error("Thread not found", 404);
|
|
114
|
+
return json(thread);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async updateThread(request, kv, project, { id }) {
|
|
118
|
+
const thread = await request.json();
|
|
119
|
+
thread.id = id;
|
|
120
|
+
await adapter.putThread(kv, project, thread);
|
|
121
|
+
return json(thread);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async deleteThread(_request, kv, project, { id }) {
|
|
125
|
+
await adapter.deleteThread(kv, project, id);
|
|
126
|
+
return json({ deleted: id });
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// ── Audit ──
|
|
130
|
+
|
|
131
|
+
async queryAudit(request, kv, project) {
|
|
132
|
+
const url = new URL(request.url);
|
|
133
|
+
const filters = {
|
|
134
|
+
user: url.searchParams.get("user") || undefined,
|
|
135
|
+
file: url.searchParams.get("file") || undefined,
|
|
136
|
+
tool: url.searchParams.get("tool") || undefined,
|
|
137
|
+
since: url.searchParams.get("since") || undefined,
|
|
138
|
+
};
|
|
139
|
+
const records = await adapter.queryAudit(kv, project, filters);
|
|
140
|
+
return json({ audit: records });
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async appendAudit(request, kv, project) {
|
|
144
|
+
const record = await request.json();
|
|
145
|
+
await adapter.appendAudit(kv, project, record);
|
|
146
|
+
return json({ ok: true }, 201);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// ── Presence ──
|
|
150
|
+
|
|
151
|
+
async getPresence(_request, kv, project) {
|
|
152
|
+
const records = await adapter.getPresence(kv, project);
|
|
153
|
+
return json({ presence: records });
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
async upsertPresence(request, kv, project) {
|
|
157
|
+
const record = await request.json();
|
|
158
|
+
if (!record.user) return error("record.user is required");
|
|
159
|
+
await adapter.upsertPresence(kv, project, record);
|
|
160
|
+
return json({ ok: true });
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async deletePresence(_request, kv, project, { user }) {
|
|
164
|
+
await adapter.deletePresence(kv, project, user);
|
|
165
|
+
return json({ deleted: user });
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// ── Config ──
|
|
169
|
+
|
|
170
|
+
async getConfig(_request, kv, project) {
|
|
171
|
+
const config = await adapter.getConfig(kv, project);
|
|
172
|
+
if (!config) return json({});
|
|
173
|
+
return json(config);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async putConfig(request, kv, project) {
|
|
177
|
+
const config = await request.json();
|
|
178
|
+
await adapter.putConfig(kv, project, config);
|
|
179
|
+
return json(config);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// ── Worker entry point ───────────────────────────────────
|
|
184
|
+
|
|
185
|
+
export default {
|
|
186
|
+
async fetch(request, env) {
|
|
187
|
+
// CORS preflight
|
|
188
|
+
if (request.method === "OPTIONS") {
|
|
189
|
+
return new Response(null, {
|
|
190
|
+
status: 204,
|
|
191
|
+
headers: {
|
|
192
|
+
"Access-Control-Allow-Origin": "*",
|
|
193
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
194
|
+
"Access-Control-Allow-Headers": "Content-Type, X-Sticky-API-Key, X-Sticky-Project",
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Auth
|
|
200
|
+
if (!authenticate(request, env)) {
|
|
201
|
+
return error("Unauthorized", 401);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const url = new URL(request.url);
|
|
205
|
+
const route = matchRoute(request.method, url.pathname);
|
|
206
|
+
|
|
207
|
+
if (!route) {
|
|
208
|
+
return error("Not found", 404);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const project = getProject(request);
|
|
212
|
+
const kv = env.STICKY_KV;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const handler = handlers[route.handler];
|
|
216
|
+
const response = await handler(request, kv, project, route);
|
|
217
|
+
|
|
218
|
+
// Add CORS headers to all responses
|
|
219
|
+
const headers = new Headers(response.headers);
|
|
220
|
+
headers.set("Access-Control-Allow-Origin", "*");
|
|
221
|
+
return new Response(response.body, {
|
|
222
|
+
status: response.status,
|
|
223
|
+
headers,
|
|
224
|
+
});
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return error(`Internal error: ${err.message}`, 500);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
@@ -157,13 +157,13 @@ function selfHealHookPaths(settingsPath) {
|
|
|
157
157
|
try { settings = JSON.parse(raw); } catch (_) { return result; }
|
|
158
158
|
if (!settings || !settings.hooks) return result;
|
|
159
159
|
|
|
160
|
-
//
|
|
160
|
+
// Stale patterns that need rewriting to the canonical template format:
|
|
161
161
|
// 1. Absolute paths (written by older sticky-note-cli or copied from another machine)
|
|
162
|
-
// 2.
|
|
163
|
-
//
|
|
164
|
-
//
|
|
162
|
+
// 2. `npx sticky-note run-hook X` — broken: npm has no package named "sticky-note"
|
|
163
|
+
// (the package is "sticky-note-cli"). Written by earlier 3.x releases via the
|
|
164
|
+
// now-removed selfHeal rewrite. Canonical form is `node ".claude/hooks/X.js"`.
|
|
165
165
|
const STALE_ABSOLUTE = /["'][A-Za-z]:[\\/]|["']\/(?:Users|home|root|tmp)\/|\$\(git rev-parse/;
|
|
166
|
-
const
|
|
166
|
+
const STALE_NPX = /^npx\s+sticky-note\s+run-hook\s+(\S+)/;
|
|
167
167
|
// Match the script basename in either quoted or bare form.
|
|
168
168
|
const BASENAME = /([A-Za-z0-9_.-]+)\.(?:js|sh|cjs|mjs)(?:["'`\s]|$)/;
|
|
169
169
|
|
|
@@ -174,12 +174,17 @@ function selfHealHookPaths(settingsPath) {
|
|
|
174
174
|
for (const h of (entry && entry.hooks) || []) {
|
|
175
175
|
const cmd = h && h.command;
|
|
176
176
|
if (typeof cmd !== "string") continue;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
177
|
+
let basename = null;
|
|
178
|
+
// Check for broken `npx sticky-note run-hook X` pattern first
|
|
179
|
+
const npxMatch = cmd.match(STALE_NPX);
|
|
180
|
+
if (npxMatch) {
|
|
181
|
+
basename = npxMatch[1];
|
|
182
|
+
} else if (STALE_ABSOLUTE.test(cmd)) {
|
|
183
|
+
const m = cmd.match(BASENAME);
|
|
184
|
+
if (m) basename = m[1];
|
|
185
|
+
}
|
|
186
|
+
if (!basename) continue;
|
|
187
|
+
const newCmd = `node ".claude/hooks/${basename}.js"`;
|
|
183
188
|
result.push({ event, oldCommand: cmd, newCommand: newCmd });
|
|
184
189
|
h.command = newCmd;
|
|
185
190
|
changed = true;
|
|
@@ -192,9 +197,9 @@ function selfHealHookPaths(settingsPath) {
|
|
|
192
197
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
193
198
|
try {
|
|
194
199
|
const summary =
|
|
195
|
-
`[STICKY-NOTE] ⚠️ Detected ${result.length} stale hook
|
|
200
|
+
`[STICKY-NOTE] ⚠️ Detected ${result.length} stale hook command(s) in ` +
|
|
196
201
|
`${path.relative(process.cwd(), settingsPath) || settingsPath} — rewrote to ` +
|
|
197
|
-
`"
|
|
202
|
+
`"node \\".claude/hooks/<name>.js\\"" (canonical form). ` +
|
|
198
203
|
`Commit the change to share the fix with your team.`;
|
|
199
204
|
process.stderr.write(summary + "\n");
|
|
200
205
|
for (const { event, oldCommand, newCommand } of result.slice(0, 5)) {
|