sticky-note-cli 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- return path.resolve(gitDir, "sticky-note");
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
- if (!fs.existsSync(settingsPath)) return result;
107
-
108
- const raw = fs.readFileSync(settingsPath, "utf-8");
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
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
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
- return null;
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
- fs.writeFileSync(syncGuard, process.pid.toString(), "utf-8");
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
- if (fs.existsSync(memoryPath)) {
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
- return result;
695
+ _cachedBranch = result;
696
+ return _cachedBranch;
619
697
  } catch (_) {
620
- return "";
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
- appendAuditLine(entry);
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
- const serverName = autoDetectMcp(toolName);
295
- if (serverName) {
296
- const configPath = getConfigPath();
297
- const config = loadJson(configPath, { stale_days: 14, mcp_servers: [] });
298
- const mcpServers = config.mcp_servers || [];
299
- const knownNames = new Set(
300
- mcpServers.map((s) => (typeof s === "object" ? s.name : s))
301
- );
302
- if (!knownNames.has(serverName)) {
303
- mcpServers.push({ name: serverName, source: "auto-detected" });
304
- config.mcp_servers = mcpServers;
305
- saveJson(configPath, config);
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
 
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "npx sticky-note run-hook session-start"
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": "npx sticky-note run-hook session-end"
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": "npx sticky-note run-hook inject-context"
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": "npx sticky-note run-hook pre-tool-use"
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": "npx sticky-note run-hook track-work"
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": "npx sticky-note run-hook on-stop"
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": "npx sticky-note run-hook on-error"
75
+ "command": "node \".claude/hooks/on-error.js\""
76
76
  }
77
77
  ]
78
78
  }
@@ -4,6 +4,8 @@
4
4
  "inject_token_budget": 1000,
5
5
  "auto_sync": true,
6
6
  "auto_push": false,
7
+ "capture_transcripts": true,
8
+ "redact_transcripts": true,
7
9
  "mcp_servers": [],
8
10
  "skills": [],
9
11
  "conventions": [],
@@ -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