sticky-note-cli 3.0.0 → 3.1.1
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 +94 -10
- package/bin/cli.js +850 -167
- package/bin/mcp-server.js +218 -36
- package/package.json +5 -5
- package/templates/CLAUDE.md +23 -0
- package/templates/hooks/inject-context.js +26 -12
- package/templates/hooks/on-error.js +22 -9
- package/templates/hooks/on-stop.js +12 -7
- package/templates/hooks/pre-tool-use.js +49 -52
- package/templates/hooks/session-end.js +138 -24
- package/templates/hooks/session-start.js +33 -9
- package/templates/hooks/sticky-codex.ps1 +20 -4
- package/templates/hooks/sticky-codex.sh +14 -5
- package/templates/hooks/sticky-utils.js +306 -20
- package/templates/hooks/track-work.js +27 -13
- package/templates/settings.json +7 -7
- package/templates/sticky-note-config.json +2 -0
- package/templates/sticky-note-install.yml +80 -0
|
@@ -11,7 +11,6 @@ const { execFileSync } = require("child_process");
|
|
|
11
11
|
|
|
12
12
|
// ── Cross-platform path normalization ──────────────────────
|
|
13
13
|
|
|
14
|
-
// Normalize path separators to forward slashes for consistent storage/comparison.
|
|
15
14
|
function normalizeSep(p) {
|
|
16
15
|
return typeof p === "string" ? p.replace(/\\/g, "/") : p;
|
|
17
16
|
}
|
|
@@ -35,14 +34,59 @@ const RETRY_PATTERNS = new RegExp(
|
|
|
35
34
|
|
|
36
35
|
const FILE_PATH_PATTERN = /[\w./\\-]+\.\w{1,10}/g;
|
|
37
36
|
|
|
37
|
+
// ── Secret redaction (best-effort, applied before transcripts are persisted) ──
|
|
38
|
+
// This is a pattern-matching floor, not a guarantee — it catches common,
|
|
39
|
+
// recognizably-shaped secrets (cloud provider keys, tokens, private key
|
|
40
|
+
// blocks, labeled key=value pairs) before a transcript is written to
|
|
41
|
+
// sticky-note/data and pushed to the team's remote. It will not catch novel
|
|
42
|
+
// or oddly-formatted secrets. Teams handling especially sensitive sessions
|
|
43
|
+
// should still set `capture_transcripts: false` for those repos.
|
|
44
|
+
const REDACTION_PATTERNS = [
|
|
45
|
+
// AWS access key IDs
|
|
46
|
+
{ name: "aws_access_key", re: /\bAKIA[0-9A-Z]{16}\b/g, replacement: "[REDACTED:aws_access_key]" },
|
|
47
|
+
// GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
|
|
48
|
+
{ name: "github_token", re: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g, replacement: "[REDACTED:github_token]" },
|
|
49
|
+
// Slack tokens
|
|
50
|
+
{ name: "slack_token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,72}\b/g, replacement: "[REDACTED:slack_token]" },
|
|
51
|
+
// PEM-style private key blocks
|
|
52
|
+
{ name: "private_key_block", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, replacement: "[REDACTED:private_key_block]" },
|
|
53
|
+
// Bearer tokens in headers/output
|
|
54
|
+
{ name: "bearer_token", re: /\bBearer\s+[A-Za-z0-9\-_.~+/]+=*/gi, replacement: "Bearer [REDACTED:token]" },
|
|
55
|
+
// JWTs (three base64url segments)
|
|
56
|
+
{ name: "jwt", re: /\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, replacement: "[REDACTED:jwt]" },
|
|
57
|
+
// Labeled secrets: api_key=..., token: "...", password=... etc. Keeps the
|
|
58
|
+
// label so the transcript still reads sensibly, redacts only the value.
|
|
59
|
+
//
|
|
60
|
+
// Deliberately no trailing ["']? here: transcripts are stored as raw JSONL
|
|
61
|
+
// text, so a value sitting right before a JSON string's closing quote is
|
|
62
|
+
// common ("...token: abc123"}) — an earlier version of this pattern also
|
|
63
|
+
// matched that closing quote and dropped it in the replacement, corrupting
|
|
64
|
+
// the line's JSON so it failed to parse back. The value class below
|
|
65
|
+
// ([^\s"'\n]{6,}) already excludes quotes, so it naturally stops just
|
|
66
|
+
// before one and the quote is left untouched in the output.
|
|
67
|
+
{ name: "labeled_secret", re: /((?:api|secret|access)[_-]?key|token|password|passwd|pwd)(\s*[:=]\s*)["']?[^\s"'\n]{6,}/gi, replacement: "$1$2[REDACTED]" },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function redactSecrets(text) {
|
|
71
|
+
if (!text || typeof text !== "string") return text;
|
|
72
|
+
let out = text;
|
|
73
|
+
for (const pattern of REDACTION_PATTERNS) {
|
|
74
|
+
out = out.replace(pattern.re, pattern.replacement);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
38
79
|
// ── Path helpers ──────────────────────────────────────────
|
|
39
80
|
|
|
81
|
+
let _cachedStickyDir = null;
|
|
40
82
|
function _stickyDir() {
|
|
83
|
+
if (_cachedStickyDir) return _cachedStickyDir;
|
|
41
84
|
try {
|
|
42
85
|
const gitDir = execFileSync("git", ["rev-parse", "--absolute-git-dir"], {
|
|
43
86
|
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
44
87
|
}).trim();
|
|
45
|
-
|
|
88
|
+
_cachedStickyDir = path.resolve(gitDir, "sticky-note");
|
|
89
|
+
return _cachedStickyDir;
|
|
46
90
|
} catch (_) {
|
|
47
91
|
// Fallback for non-git environments (e.g., unit tests without git init)
|
|
48
92
|
const scriptDir = path.dirname(path.resolve(__filename));
|
|
@@ -103,9 +147,12 @@ function selfHealHookPaths(settingsPath) {
|
|
|
103
147
|
if (!settingsPath) {
|
|
104
148
|
settingsPath = path.join(process.cwd(), ".claude", "settings.json");
|
|
105
149
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
150
|
+
let raw;
|
|
151
|
+
try {
|
|
152
|
+
raw = fs.readFileSync(settingsPath, "utf-8");
|
|
153
|
+
} catch (_) {
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
109
156
|
let settings;
|
|
110
157
|
try { settings = JSON.parse(raw); } catch (_) { return result; }
|
|
111
158
|
if (!settings || !settings.hooks) return result;
|
|
@@ -165,6 +212,7 @@ const _paths = {
|
|
|
165
212
|
config: "sticky-note-config.json",
|
|
166
213
|
auditDir: "audit",
|
|
167
214
|
presenceDir: "presence",
|
|
215
|
+
transcriptsDir: "transcripts",
|
|
168
216
|
// Legacy single-file paths (for migration only)
|
|
169
217
|
legacyAudit: "sticky-note-audit.jsonl",
|
|
170
218
|
legacyPresence: ".sticky-presence.json",
|
|
@@ -181,6 +229,18 @@ function getPresenceDir() { return _p("presenceDir"); }
|
|
|
181
229
|
function getLegacyAuditPath() { return _p("legacyAudit"); }
|
|
182
230
|
function getLegacyPresencePath() { return _p("legacyPresence"); }
|
|
183
231
|
|
|
232
|
+
// ── Transcript storage (session-level, keyed by thread ID) ────
|
|
233
|
+
// Transcripts live alongside audit/presence in the sticky-note/data
|
|
234
|
+
// storage layer — one JSONL file per thread, one line per contributing
|
|
235
|
+
// session. Not commit-anchored (git notes) because a thread's transcript
|
|
236
|
+
// belongs to the whole session, not to any single commit it produced.
|
|
237
|
+
|
|
238
|
+
function getTranscriptsDir() { return _p("transcriptsDir"); }
|
|
239
|
+
|
|
240
|
+
function getTranscriptPath(threadId) {
|
|
241
|
+
return path.join(getTranscriptsDir(), threadId + ".jsonl");
|
|
242
|
+
}
|
|
243
|
+
|
|
184
244
|
function getUserAuditPath(user) {
|
|
185
245
|
if (!user) user = getUser();
|
|
186
246
|
return path.join(getAuditDir(), user + ".jsonl");
|
|
@@ -245,14 +305,17 @@ function clearSessionFile() {
|
|
|
245
305
|
|
|
246
306
|
// ── Git sync (auto-commit/push .sticky-note/ files) ──────
|
|
247
307
|
|
|
308
|
+
let _cachedGitRepoRoot = null; // null = not yet computed; "" = computed but git failed
|
|
248
309
|
function _gitRepoRoot() {
|
|
310
|
+
if (_cachedGitRepoRoot !== null) return _cachedGitRepoRoot || null;
|
|
249
311
|
try {
|
|
250
|
-
|
|
312
|
+
_cachedGitRepoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
251
313
|
encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
252
314
|
}).trim();
|
|
253
315
|
} catch (_) {
|
|
254
|
-
|
|
316
|
+
_cachedGitRepoRoot = "";
|
|
255
317
|
}
|
|
318
|
+
return _cachedGitRepoRoot || null;
|
|
256
319
|
}
|
|
257
320
|
|
|
258
321
|
/**
|
|
@@ -267,7 +330,6 @@ function syncStickyNote(opts) {
|
|
|
267
330
|
if (!root) { result.skipped = true; return result; }
|
|
268
331
|
|
|
269
332
|
const syncGuard = path.join(_stickyDir(), ".sticky-syncing");
|
|
270
|
-
if (fs.existsSync(syncGuard)) { result.skipped = true; return result; }
|
|
271
333
|
|
|
272
334
|
// Check for dirty .sticky-note/ files (staged or unstaged)
|
|
273
335
|
const status = execFileSync("git", [
|
|
@@ -277,8 +339,12 @@ function syncStickyNote(opts) {
|
|
|
277
339
|
|
|
278
340
|
if (!status) { result.skipped = true; return result; }
|
|
279
341
|
|
|
280
|
-
// Write guard to prevent recursive post-commit triggers
|
|
281
|
-
|
|
342
|
+
// Write guard atomically to prevent recursive post-commit triggers
|
|
343
|
+
try {
|
|
344
|
+
fs.writeFileSync(syncGuard, process.pid.toString(), { flag: "wx", encoding: "utf-8" });
|
|
345
|
+
} catch (_) {
|
|
346
|
+
result.skipped = true; return result;
|
|
347
|
+
}
|
|
282
348
|
|
|
283
349
|
try {
|
|
284
350
|
// Stage the relevant files
|
|
@@ -511,9 +577,7 @@ function saveMemoryMerged(memoryPath, memory) {
|
|
|
511
577
|
const lockPath = acquireLock(memoryPath);
|
|
512
578
|
try {
|
|
513
579
|
// Create backup before writing
|
|
514
|
-
|
|
515
|
-
rotateBackup(memoryPath);
|
|
516
|
-
}
|
|
580
|
+
rotateBackup(memoryPath);
|
|
517
581
|
|
|
518
582
|
// Load merge helpers (field-level thread merge)
|
|
519
583
|
let mergeOneThread2Way = null;
|
|
@@ -558,6 +622,17 @@ function appendAuditLine(entry) {
|
|
|
558
622
|
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf-8");
|
|
559
623
|
}
|
|
560
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Write an audit entry both to the local JSONL file and (fire-and-forget)
|
|
627
|
+
* to the cloud backend when cloud mode is active.
|
|
628
|
+
*/
|
|
629
|
+
function appendAuditLineBoth(entry, cloud) {
|
|
630
|
+
try { appendAuditLine(entry); } catch (_) {}
|
|
631
|
+
if (cloud) {
|
|
632
|
+
cloudAppendAudit(entry).catch(() => {});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
561
636
|
// ── Environment helpers ───────────────────────────────────
|
|
562
637
|
|
|
563
638
|
function getUser() {
|
|
@@ -608,16 +683,20 @@ function getSessionId(hookInput) {
|
|
|
608
683
|
return "unknown";
|
|
609
684
|
}
|
|
610
685
|
|
|
686
|
+
let _cachedBranch = null;
|
|
611
687
|
function getBranch() {
|
|
688
|
+
if (_cachedBranch !== null) return _cachedBranch;
|
|
612
689
|
try {
|
|
613
690
|
const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
614
691
|
encoding: "utf-8",
|
|
615
692
|
timeout: 5000,
|
|
616
693
|
stdio: ["pipe", "pipe", "pipe"],
|
|
617
694
|
}).trim();
|
|
618
|
-
|
|
695
|
+
_cachedBranch = result;
|
|
696
|
+
return _cachedBranch;
|
|
619
697
|
} catch (_) {
|
|
620
|
-
|
|
698
|
+
_cachedBranch = "";
|
|
699
|
+
return _cachedBranch;
|
|
621
700
|
}
|
|
622
701
|
}
|
|
623
702
|
|
|
@@ -1049,6 +1128,11 @@ module.exports = {
|
|
|
1049
1128
|
ERROR_PATTERNS,
|
|
1050
1129
|
RETRY_PATTERNS,
|
|
1051
1130
|
FILE_PATH_PATTERN,
|
|
1131
|
+
// Transcript capture
|
|
1132
|
+
REDACTION_PATTERNS,
|
|
1133
|
+
redactSecrets,
|
|
1134
|
+
getTranscriptsDir,
|
|
1135
|
+
getTranscriptPath,
|
|
1052
1136
|
logHookError,
|
|
1053
1137
|
reportHookError,
|
|
1054
1138
|
selfHealHookPaths,
|
|
@@ -1080,6 +1164,7 @@ module.exports = {
|
|
|
1080
1164
|
saveJson,
|
|
1081
1165
|
saveMemoryMerged,
|
|
1082
1166
|
appendAuditLine,
|
|
1167
|
+
appendAuditLineBoth,
|
|
1083
1168
|
getUser,
|
|
1084
1169
|
detectTool,
|
|
1085
1170
|
getSessionId,
|
|
@@ -1112,6 +1197,22 @@ module.exports = {
|
|
|
1112
1197
|
markOverlapDismissed,
|
|
1113
1198
|
// V2.5: cross-platform path helper
|
|
1114
1199
|
normalizeSep,
|
|
1200
|
+
// V3: cloud transport (opt-in when STICKY_URL is set)
|
|
1201
|
+
useCloud,
|
|
1202
|
+
getCloudConfig,
|
|
1203
|
+
getProjectName,
|
|
1204
|
+
cloudFetch,
|
|
1205
|
+
cloudReadThreads,
|
|
1206
|
+
cloudReadThread,
|
|
1207
|
+
cloudWriteThread,
|
|
1208
|
+
cloudDeleteThread,
|
|
1209
|
+
cloudAppendAudit,
|
|
1210
|
+
cloudQueryAudit,
|
|
1211
|
+
cloudReadPresence,
|
|
1212
|
+
cloudWritePresence,
|
|
1213
|
+
cloudDeletePresence,
|
|
1214
|
+
cloudReadConfig,
|
|
1215
|
+
cloudWriteConfig,
|
|
1115
1216
|
// V2.7: auto-sync
|
|
1116
1217
|
syncStickyNote,
|
|
1117
1218
|
// V2.9: Copilot CLI MCP config
|
|
@@ -1123,10 +1224,195 @@ module.exports = {
|
|
|
1123
1224
|
rotateBackup,
|
|
1124
1225
|
acquireLock,
|
|
1125
1226
|
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 () => {},
|
|
1131
1227
|
};
|
|
1228
|
+
|
|
1229
|
+
// ── V3: Cloud transport layer ─────────────────────────────
|
|
1230
|
+
|
|
1231
|
+
function _loadEnvStickyFile() {
|
|
1232
|
+
try {
|
|
1233
|
+
const repoRoot = _gitRepoRoot();
|
|
1234
|
+
if (!repoRoot) return {};
|
|
1235
|
+
const envPath = path.join(repoRoot, ".env.sticky");
|
|
1236
|
+
let raw;
|
|
1237
|
+
try {
|
|
1238
|
+
raw = fs.readFileSync(envPath, "utf-8");
|
|
1239
|
+
} catch (_) {
|
|
1240
|
+
return {};
|
|
1241
|
+
}
|
|
1242
|
+
const vars = {};
|
|
1243
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1244
|
+
const trimmed = line.trim();
|
|
1245
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1246
|
+
const eq = trimmed.indexOf("=");
|
|
1247
|
+
if (eq > 0) {
|
|
1248
|
+
vars[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return vars;
|
|
1252
|
+
} catch (_) {
|
|
1253
|
+
return {};
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
let _envStickyCache = null;
|
|
1258
|
+
function _getEnvSticky() {
|
|
1259
|
+
if (_envStickyCache === null) {
|
|
1260
|
+
_envStickyCache = _loadEnvStickyFile();
|
|
1261
|
+
}
|
|
1262
|
+
return _envStickyCache;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function getCloudConfig() {
|
|
1266
|
+
const envFile = _getEnvSticky();
|
|
1267
|
+
return {
|
|
1268
|
+
url: process.env.STICKY_URL || envFile.STICKY_URL || "",
|
|
1269
|
+
apiKey: process.env.STICKY_API_KEY || envFile.STICKY_API_KEY || "",
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
let _cachedUseCloud = null;
|
|
1274
|
+
function useCloud() {
|
|
1275
|
+
if (_cachedUseCloud !== null) return _cachedUseCloud;
|
|
1276
|
+
_cachedUseCloud = !!getCloudConfig().url;
|
|
1277
|
+
return _cachedUseCloud;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
let _cachedProjectName = null;
|
|
1281
|
+
function getProjectName() {
|
|
1282
|
+
if (_cachedProjectName !== null) return _cachedProjectName;
|
|
1283
|
+
|
|
1284
|
+
// Check config override first
|
|
1285
|
+
try {
|
|
1286
|
+
const config = loadJson(getConfigPath(), {});
|
|
1287
|
+
if (config.project) {
|
|
1288
|
+
_cachedProjectName = config.project;
|
|
1289
|
+
return _cachedProjectName;
|
|
1290
|
+
}
|
|
1291
|
+
} catch (_) {}
|
|
1292
|
+
|
|
1293
|
+
// Auto-detect from git remote
|
|
1294
|
+
try {
|
|
1295
|
+
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
1296
|
+
encoding: "utf-8",
|
|
1297
|
+
timeout: 5000,
|
|
1298
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1299
|
+
}).trim();
|
|
1300
|
+
// git@github.com:Owner/Repo.git → Owner/Repo
|
|
1301
|
+
// https://github.com/Owner/Repo.git → Owner/Repo
|
|
1302
|
+
const match = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
1303
|
+
if (match) {
|
|
1304
|
+
_cachedProjectName = match[1];
|
|
1305
|
+
return _cachedProjectName;
|
|
1306
|
+
}
|
|
1307
|
+
} catch (_) {}
|
|
1308
|
+
|
|
1309
|
+
_cachedProjectName = "default";
|
|
1310
|
+
return _cachedProjectName;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
let _cloudWarned = false;
|
|
1314
|
+
async function cloudFetch(method, endpoint, body) {
|
|
1315
|
+
if (typeof fetch === "undefined") {
|
|
1316
|
+
// Node < 18 — cloud transport not supported
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
const { url, apiKey } = getCloudConfig();
|
|
1320
|
+
if (!url) return null;
|
|
1321
|
+
|
|
1322
|
+
const project = getProjectName();
|
|
1323
|
+
const headers = {
|
|
1324
|
+
"Content-Type": "application/json",
|
|
1325
|
+
"X-Sticky-Project": project,
|
|
1326
|
+
};
|
|
1327
|
+
if (apiKey) {
|
|
1328
|
+
headers["X-Sticky-API-Key"] = apiKey;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const opts = { method, headers };
|
|
1332
|
+
if (body && (method === "POST" || method === "PUT")) {
|
|
1333
|
+
opts.body = JSON.stringify(body);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
const resp = await fetch(`${url}${endpoint}`, opts);
|
|
1338
|
+
if (!resp.ok) {
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
const text = await resp.text();
|
|
1342
|
+
return text ? JSON.parse(text) : {};
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
if (!_cloudWarned) {
|
|
1345
|
+
_cloudWarned = true;
|
|
1346
|
+
process.stderr.write(
|
|
1347
|
+
"[STICKY-NOTE] Cloud unreachable, using local fallback\n"
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// ── Cloud: Threads ────────────────────────────────────────
|
|
1355
|
+
|
|
1356
|
+
async function cloudReadThreads(filters) {
|
|
1357
|
+
const params = new URLSearchParams();
|
|
1358
|
+
if (filters && filters.status) params.set("status", filters.status);
|
|
1359
|
+
if (filters && filters.q) params.set("q", filters.q);
|
|
1360
|
+
const qs = params.toString();
|
|
1361
|
+
const result = await cloudFetch("GET", `/threads${qs ? "?" + qs : ""}`);
|
|
1362
|
+
return result ? result.threads || [] : null;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
async function cloudReadThread(id) {
|
|
1366
|
+
return await cloudFetch("GET", `/threads/${id}`);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function cloudWriteThread(thread) {
|
|
1370
|
+
return await cloudFetch("PUT", `/threads/${thread.id}`, thread);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async function cloudDeleteThread(id) {
|
|
1374
|
+
return await cloudFetch("DELETE", `/threads/${id}`);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// ── Cloud: Audit ──────────────────────────────────────────
|
|
1378
|
+
|
|
1379
|
+
async function cloudAppendAudit(record) {
|
|
1380
|
+
return await cloudFetch("POST", "/audit", record);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
async function cloudQueryAudit(filters) {
|
|
1384
|
+
const params = new URLSearchParams();
|
|
1385
|
+
if (filters && filters.user) params.set("user", filters.user);
|
|
1386
|
+
if (filters && filters.file) params.set("file", filters.file);
|
|
1387
|
+
if (filters && filters.tool) params.set("tool", filters.tool);
|
|
1388
|
+
if (filters && filters.since) params.set("since", filters.since);
|
|
1389
|
+
const qs = params.toString();
|
|
1390
|
+
const result = await cloudFetch("GET", `/audit${qs ? "?" + qs : ""}`);
|
|
1391
|
+
return result ? result.audit || [] : null;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// ── Cloud: Presence ───────────────────────────────────────
|
|
1395
|
+
|
|
1396
|
+
async function cloudReadPresence() {
|
|
1397
|
+
const result = await cloudFetch("GET", "/presence");
|
|
1398
|
+
return result ? result.presence || [] : null;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function cloudWritePresence(record) {
|
|
1402
|
+
return await cloudFetch("POST", "/presence", record);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
async function cloudDeletePresence(user) {
|
|
1406
|
+
return await cloudFetch("DELETE", `/presence/${encodeURIComponent(user)}`);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// ── Cloud: Config ─────────────────────────────────────────
|
|
1410
|
+
|
|
1411
|
+
async function cloudReadConfig() {
|
|
1412
|
+
return await cloudFetch("GET", "/config");
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
async function cloudWriteConfig(config) {
|
|
1416
|
+
return await cloudFetch("PUT", "/config", config);
|
|
1417
|
+
}
|
|
1132
1418
|
// tier1 test
|
|
@@ -43,9 +43,12 @@ const {
|
|
|
43
43
|
saveJson,
|
|
44
44
|
saveMemoryMerged,
|
|
45
45
|
appendAuditLine,
|
|
46
|
+
appendAuditLineBoth,
|
|
46
47
|
getUser,
|
|
47
48
|
getBranch,
|
|
48
49
|
getSessionId,
|
|
50
|
+
useCloud,
|
|
51
|
+
cloudWritePresence,
|
|
49
52
|
normalizeSep,
|
|
50
53
|
} = utils;
|
|
51
54
|
|
|
@@ -227,6 +230,7 @@ function main() {
|
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
const sessionId = getSessionId(hookInput);
|
|
233
|
+
const cloud = useCloud();
|
|
230
234
|
|
|
231
235
|
let toolName = hookInput.tool_name || "unknown";
|
|
232
236
|
if (toolName === "unknown") {
|
|
@@ -282,27 +286,37 @@ function main() {
|
|
|
282
286
|
if (checkpoint) {
|
|
283
287
|
entry.checkpoint_topic = checkpoint.topic;
|
|
284
288
|
}
|
|
285
|
-
|
|
289
|
+
appendAuditLineBoth(entry, cloud);
|
|
286
290
|
|
|
287
291
|
updatePresence(user, filePath);
|
|
292
|
+
if (cloud) {
|
|
293
|
+
cloudWritePresence({
|
|
294
|
+
user,
|
|
295
|
+
last_seen: now,
|
|
296
|
+
active_files: filePath ? [filePath] : [],
|
|
297
|
+
session_id: sessionId,
|
|
298
|
+
}).catch(() => {});
|
|
299
|
+
}
|
|
288
300
|
|
|
289
301
|
// V2.5: Write Git Note for write tools
|
|
290
302
|
if (isWriteTool && filePath) {
|
|
291
303
|
writeEditNote(sessionId, user, filePath, lineRanges, checkpoint);
|
|
292
304
|
}
|
|
293
305
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
+
if (toolName && toolName.startsWith("mcp__")) {
|
|
307
|
+
const serverName = autoDetectMcp(toolName);
|
|
308
|
+
if (serverName) {
|
|
309
|
+
const configPath = getConfigPath();
|
|
310
|
+
const config = loadJson(configPath, { stale_days: 14, mcp_servers: [] });
|
|
311
|
+
const mcpServers = config.mcp_servers || [];
|
|
312
|
+
const knownNames = new Set(
|
|
313
|
+
mcpServers.map((s) => (typeof s === "object" ? s.name : s))
|
|
314
|
+
);
|
|
315
|
+
if (!knownNames.has(serverName)) {
|
|
316
|
+
mcpServers.push({ name: serverName, source: "auto-detected" });
|
|
317
|
+
config.mcp_servers = mcpServers;
|
|
318
|
+
saveJson(configPath, config);
|
|
319
|
+
}
|
|
306
320
|
}
|
|
307
321
|
}
|
|
308
322
|
|
package/templates/settings.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "
|
|
9
|
+
"command": "node \".claude/hooks/session-start.js\""
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "
|
|
20
|
+
"command": "node \".claude/hooks/session-end.js\""
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"hooks": [
|
|
29
29
|
{
|
|
30
30
|
"type": "command",
|
|
31
|
-
"command": "
|
|
31
|
+
"command": "node \".claude/hooks/inject-context.js\""
|
|
32
32
|
}
|
|
33
33
|
]
|
|
34
34
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"hooks": [
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "
|
|
42
|
+
"command": "node \".claude/hooks/pre-tool-use.js\""
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"hooks": [
|
|
51
51
|
{
|
|
52
52
|
"type": "command",
|
|
53
|
-
"command": "
|
|
53
|
+
"command": "node \".claude/hooks/track-work.js\""
|
|
54
54
|
}
|
|
55
55
|
]
|
|
56
56
|
}
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"hooks": [
|
|
62
62
|
{
|
|
63
63
|
"type": "command",
|
|
64
|
-
"command": "
|
|
64
|
+
"command": "node \".claude/hooks/on-stop.js\""
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
67
|
}
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"hooks": [
|
|
73
73
|
{
|
|
74
74
|
"type": "command",
|
|
75
|
-
"command": "
|
|
75
|
+
"command": "node \".claude/hooks/on-error.js\""
|
|
76
76
|
}
|
|
77
77
|
]
|
|
78
78
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Sticky Note — Auto-Install GitHub Action
|
|
2
|
+
#
|
|
3
|
+
# Installs or updates Sticky Note hooks on every repo in your org.
|
|
4
|
+
# Add this workflow to your org's .github repository, or to individual repos.
|
|
5
|
+
#
|
|
6
|
+
# Required org secrets:
|
|
7
|
+
# STICKY_URL — Cloud backend URL (e.g., https://sticky.acme.workers.dev)
|
|
8
|
+
# STICKY_API_KEY — API key for cloud backend
|
|
9
|
+
#
|
|
10
|
+
# Optional org variables:
|
|
11
|
+
# STICKY_STALE_DAYS — Thread expiry days (default: 14)
|
|
12
|
+
# STICKY_CONVENTIONS — Comma-separated team conventions
|
|
13
|
+
# STICKY_MCP_SERVERS — JSON array of MCP server configs
|
|
14
|
+
|
|
15
|
+
name: Sticky Note Install
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
branches: [main]
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: write
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
install:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- uses: actions/setup-node@v4
|
|
32
|
+
with:
|
|
33
|
+
node-version: '20'
|
|
34
|
+
|
|
35
|
+
- name: Check if Sticky Note is installed
|
|
36
|
+
id: check
|
|
37
|
+
run: |
|
|
38
|
+
if [ -f ".claude/hooks/session-start.js" ]; then
|
|
39
|
+
echo "installed=true" >> $GITHUB_OUTPUT
|
|
40
|
+
else
|
|
41
|
+
echo "installed=false" >> $GITHUB_OUTPUT
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
- name: Install Sticky Note
|
|
45
|
+
if: steps.check.outputs.installed == 'false'
|
|
46
|
+
run: npx sticky-note-cli@latest init --ci --no-prompts
|
|
47
|
+
env:
|
|
48
|
+
STICKY_URL: ${{ secrets.STICKY_URL }}
|
|
49
|
+
STICKY_API_KEY: ${{ secrets.STICKY_API_KEY }}
|
|
50
|
+
STICKY_STALE_DAYS: ${{ vars.STICKY_STALE_DAYS }}
|
|
51
|
+
STICKY_CONVENTIONS: ${{ vars.STICKY_CONVENTIONS }}
|
|
52
|
+
STICKY_MCP_SERVERS: ${{ vars.STICKY_MCP_SERVERS }}
|
|
53
|
+
|
|
54
|
+
- name: Update Sticky Note
|
|
55
|
+
if: steps.check.outputs.installed == 'true'
|
|
56
|
+
run: npx sticky-note-cli@latest update --ci
|
|
57
|
+
env:
|
|
58
|
+
STICKY_URL: ${{ secrets.STICKY_URL }}
|
|
59
|
+
STICKY_API_KEY: ${{ secrets.STICKY_API_KEY }}
|
|
60
|
+
|
|
61
|
+
- name: Write .env.sticky
|
|
62
|
+
if: env.STICKY_URL != ''
|
|
63
|
+
run: |
|
|
64
|
+
echo "STICKY_URL=$STICKY_URL" > .env.sticky
|
|
65
|
+
echo "STICKY_API_KEY=$STICKY_API_KEY" >> .env.sticky
|
|
66
|
+
env:
|
|
67
|
+
STICKY_URL: ${{ secrets.STICKY_URL }}
|
|
68
|
+
STICKY_API_KEY: ${{ secrets.STICKY_API_KEY }}
|
|
69
|
+
|
|
70
|
+
- name: Commit changes
|
|
71
|
+
run: |
|
|
72
|
+
git config user.name "sticky-note[bot]"
|
|
73
|
+
git config user.email "sticky-note[bot]@users.noreply.github.com"
|
|
74
|
+
git add -A
|
|
75
|
+
if git diff --cached --quiet; then
|
|
76
|
+
echo "No changes to commit"
|
|
77
|
+
else
|
|
78
|
+
git commit -m "chore: install sticky note [sticky-note] [skip ci]"
|
|
79
|
+
git push
|
|
80
|
+
fi
|