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
|
@@ -59,10 +59,11 @@ const {
|
|
|
59
59
|
getSessionId,
|
|
60
60
|
getUser,
|
|
61
61
|
appendAuditLine,
|
|
62
|
+
appendAuditLineBoth,
|
|
62
63
|
isThreadInjected,
|
|
63
64
|
markThreadInjected,
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
useCloud,
|
|
66
|
+
cloudReadPresence,
|
|
66
67
|
getMemoryPath,
|
|
67
68
|
loadJson,
|
|
68
69
|
normalizeSep,
|
|
@@ -137,47 +138,41 @@ function formatThreadForInjection(threadData, file) {
|
|
|
137
138
|
return lines.join("\n");
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
function _getRecentlyModifiedFiles() {
|
|
148
|
-
const files = new Set();
|
|
149
|
-
for (const target of ["HEAD~5", "HEAD~1"]) {
|
|
150
|
-
try {
|
|
151
|
-
const result = execSync(`git diff --name-only ${target}`, {
|
|
152
|
-
encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
153
|
-
});
|
|
154
|
-
for (const f of result.trim().split(/\r?\n/)) {
|
|
155
|
-
if (f.trim()) files.add(f.trim());
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
} catch (_) { /* target doesn't exist */ }
|
|
159
|
-
}
|
|
141
|
+
let _presenceCache = null;
|
|
142
|
+
let _presenceCacheTs = 0;
|
|
143
|
+
const PRESENCE_CACHE_TTL_MS = 60000;
|
|
144
|
+
|
|
145
|
+
async function _checkConflict(filePath) {
|
|
146
|
+
if (!filePath || !useCloud()) return "";
|
|
160
147
|
try {
|
|
161
|
-
const
|
|
162
|
-
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
if (!_presenceCache || (now - _presenceCacheTs) > PRESENCE_CACHE_TTL_MS) {
|
|
150
|
+
_presenceCache = await cloudReadPresence();
|
|
151
|
+
_presenceCacheTs = now;
|
|
152
|
+
}
|
|
153
|
+
const presence = _presenceCache;
|
|
154
|
+
if (!presence) return "";
|
|
155
|
+
const currentUser = getUser();
|
|
156
|
+
const conflicting = presence.filter(p => {
|
|
157
|
+
if (!p.user || p.user === currentUser) return false;
|
|
158
|
+
return (p.active_files || []).some(f =>
|
|
159
|
+
f === filePath || filePath.endsWith(f) || f.endsWith(filePath)
|
|
160
|
+
);
|
|
163
161
|
});
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
if (conflicting.length > 0) {
|
|
163
|
+
const names = conflicting.map(p => p.user).join(", ");
|
|
164
|
+
return `⚠️ [STICKY-NOTE] Conflict warning: ${names} also editing ${filePath}`;
|
|
166
165
|
}
|
|
167
|
-
} catch (_) {
|
|
168
|
-
|
|
166
|
+
} catch (_) {
|
|
167
|
+
// ignore
|
|
168
|
+
}
|
|
169
|
+
return "";
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function _checkOverlapsAndDeny() {
|
|
172
|
-
// Overlap alerting is now handled by the MCP server's check_overlaps tool.
|
|
173
|
-
// This deny-based approach was unreliable (deny reason consumed silently).
|
|
174
|
-
// Kept as no-op to avoid breaking the call site.
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
172
|
|
|
178
173
|
// ── Main ──────────────────────────────────────────────────
|
|
179
174
|
|
|
180
|
-
function main() {
|
|
175
|
+
async function main() {
|
|
181
176
|
let hookInput = {};
|
|
182
177
|
try {
|
|
183
178
|
const raw = require("fs").readFileSync(0, "utf-8");
|
|
@@ -189,6 +184,7 @@ function main() {
|
|
|
189
184
|
}
|
|
190
185
|
|
|
191
186
|
const sessionId = getSessionId(hookInput);
|
|
187
|
+
const cloud = useCloud();
|
|
192
188
|
|
|
193
189
|
// ── Auto-sync before git pull/merge/rebase ──
|
|
194
190
|
// Prevents "local changes would be overwritten" errors for .sticky-note/ files
|
|
@@ -201,14 +197,6 @@ function main() {
|
|
|
201
197
|
}
|
|
202
198
|
}
|
|
203
199
|
|
|
204
|
-
// ── Overlap deny gate (Copilot CLI only) ──
|
|
205
|
-
// Detect overlaps and deny on first tool call, keyed by COPILOT_LOADER_PID.
|
|
206
|
-
const denyResult = _checkOverlapsAndDeny();
|
|
207
|
-
if (denyResult) {
|
|
208
|
-
process.stdout.write(JSON.stringify(denyResult) + "\n");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
200
|
// Extract file path from tool input
|
|
213
201
|
const filePath = extractFilePath(hookInput);
|
|
214
202
|
if (!filePath) {
|
|
@@ -216,7 +204,6 @@ function main() {
|
|
|
216
204
|
return;
|
|
217
205
|
}
|
|
218
206
|
|
|
219
|
-
// Run attribution engine: get threads for this file with line ranges
|
|
220
207
|
let fileAttr;
|
|
221
208
|
try {
|
|
222
209
|
fileAttr = attribution.getFileAttribution(filePath);
|
|
@@ -226,6 +213,10 @@ function main() {
|
|
|
226
213
|
}
|
|
227
214
|
|
|
228
215
|
if (!fileAttr || !fileAttr.threads || fileAttr.threads.length === 0) {
|
|
216
|
+
if (cloud) {
|
|
217
|
+
const warning = await _checkConflict(filePath);
|
|
218
|
+
if (warning) { _emit(warning); return; }
|
|
219
|
+
}
|
|
229
220
|
_emit("");
|
|
230
221
|
return;
|
|
231
222
|
}
|
|
@@ -236,6 +227,10 @@ function main() {
|
|
|
236
227
|
);
|
|
237
228
|
|
|
238
229
|
if (newThreads.length === 0) {
|
|
230
|
+
if (cloud) {
|
|
231
|
+
const warning = await _checkConflict(filePath);
|
|
232
|
+
if (warning) { _emit(warning); return; }
|
|
233
|
+
}
|
|
239
234
|
_emit("");
|
|
240
235
|
return;
|
|
241
236
|
}
|
|
@@ -248,11 +243,11 @@ function main() {
|
|
|
248
243
|
markThreadInjected(threadId, sessionId);
|
|
249
244
|
}
|
|
250
245
|
|
|
251
|
-
|
|
246
|
+
let output = outputParts.join("\n\n");
|
|
252
247
|
|
|
253
248
|
// Audit the injection
|
|
254
249
|
try {
|
|
255
|
-
|
|
250
|
+
const auditEntry = {
|
|
256
251
|
type: "lazy_inject",
|
|
257
252
|
user: getUser(),
|
|
258
253
|
ts: new Date().toISOString(),
|
|
@@ -263,20 +258,22 @@ function main() {
|
|
|
263
258
|
const id = t.thread ? t.thread.id : t.id;
|
|
264
259
|
return (id || "").substring(0, 8);
|
|
265
260
|
}),
|
|
266
|
-
}
|
|
261
|
+
};
|
|
262
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
267
263
|
} catch (_) {
|
|
268
264
|
// ignore
|
|
269
265
|
}
|
|
270
266
|
|
|
267
|
+
if (cloud) {
|
|
268
|
+
const warning = await _checkConflict(filePath);
|
|
269
|
+
if (warning) output += "\n\n" + warning;
|
|
270
|
+
}
|
|
271
|
+
|
|
271
272
|
_emit(output);
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
// ── Entry point ───────────────────────────────────────────
|
|
275
276
|
|
|
276
277
|
if (require.main === module) {
|
|
277
|
-
|
|
278
|
-
main();
|
|
279
|
-
} catch (_) {
|
|
280
|
-
_safeExit();
|
|
281
|
-
}
|
|
278
|
+
main().catch(() => _safeExit());
|
|
282
279
|
}
|
|
@@ -39,10 +39,14 @@ const {
|
|
|
39
39
|
getPresenceDir,
|
|
40
40
|
getUserPresencePath,
|
|
41
41
|
getAllAuditPaths,
|
|
42
|
+
getTranscriptsDir,
|
|
43
|
+
getTranscriptPath,
|
|
44
|
+
redactSecrets,
|
|
42
45
|
loadJson,
|
|
43
46
|
saveJson,
|
|
44
47
|
saveMemoryMerged,
|
|
45
48
|
appendAuditLine,
|
|
49
|
+
appendAuditLineBoth,
|
|
46
50
|
getUser,
|
|
47
51
|
getBranch,
|
|
48
52
|
detectTool,
|
|
@@ -63,6 +67,10 @@ const {
|
|
|
63
67
|
clearActiveResumeThreadId,
|
|
64
68
|
clearInjectedSet,
|
|
65
69
|
normalizeSep,
|
|
70
|
+
useCloud,
|
|
71
|
+
cloudReadThreads,
|
|
72
|
+
cloudWriteThread,
|
|
73
|
+
cloudDeletePresence,
|
|
66
74
|
syncStickyNote,
|
|
67
75
|
} = utils;
|
|
68
76
|
|
|
@@ -368,6 +376,73 @@ function extractFailedApproaches(hookInput) {
|
|
|
368
376
|
return extractFailedFromEntries(entries);
|
|
369
377
|
}
|
|
370
378
|
|
|
379
|
+
// ── Transcript capture ────────────────────────────────────
|
|
380
|
+
// Persists the full verbatim session transcript (Claude Code's native
|
|
381
|
+
// transcript_path JSONL file) into sticky-note/data storage, keyed by
|
|
382
|
+
// thread ID rather than commit SHA — a thread's transcript belongs to the
|
|
383
|
+
// whole session, which may span zero, one, or many commits. Appended as
|
|
384
|
+
// one entry per contributing session so resumed threads accumulate their
|
|
385
|
+
// full history rather than overwriting it.
|
|
386
|
+
//
|
|
387
|
+
// Opt-out: set "capture_transcripts": false in sticky-note-config.json.
|
|
388
|
+
// Secrets are redacted (best-effort — see redactSecrets in sticky-utils.js)
|
|
389
|
+
// before anything is written to disk or committed.
|
|
390
|
+
|
|
391
|
+
function captureTranscript(hookInput, threadId, sessionId, aiTool, user, now, config) {
|
|
392
|
+
if (!threadId) return false;
|
|
393
|
+
if (config.capture_transcripts === false) return false;
|
|
394
|
+
|
|
395
|
+
const transcriptPath = hookInput.transcript_path || "";
|
|
396
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath)) return false;
|
|
397
|
+
|
|
398
|
+
let raw;
|
|
399
|
+
try {
|
|
400
|
+
raw = fs.readFileSync(transcriptPath, "utf-8");
|
|
401
|
+
} catch (_) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
if (!raw || !raw.trim()) return false;
|
|
405
|
+
|
|
406
|
+
const redact = config.redact_transcripts !== false;
|
|
407
|
+
const transcript = redact ? redactSecrets(raw) : raw;
|
|
408
|
+
|
|
409
|
+
const entry = {
|
|
410
|
+
session_id: sessionId,
|
|
411
|
+
tool: aiTool,
|
|
412
|
+
user,
|
|
413
|
+
captured_at: now,
|
|
414
|
+
redacted: redact,
|
|
415
|
+
transcript,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const dir = getTranscriptsDir();
|
|
420
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
421
|
+
const filePath = getTranscriptPath(threadId);
|
|
422
|
+
|
|
423
|
+
// Avoid duplicating the same session's transcript on repeated
|
|
424
|
+
// session-end fires (e.g. Copilot CLI's per-turn SessionEnd).
|
|
425
|
+
let existingLines = [];
|
|
426
|
+
if (fs.existsSync(filePath)) {
|
|
427
|
+
existingLines = fs.readFileSync(filePath, "utf-8")
|
|
428
|
+
.split(/\r?\n/)
|
|
429
|
+
.filter((l) => l.trim());
|
|
430
|
+
}
|
|
431
|
+
const filtered = existingLines.filter((line) => {
|
|
432
|
+
try {
|
|
433
|
+
return JSON.parse(line).session_id !== sessionId;
|
|
434
|
+
} catch (_) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
filtered.push(JSON.stringify(entry));
|
|
439
|
+
fs.writeFileSync(filePath, filtered.join("\n") + "\n", "utf-8");
|
|
440
|
+
return true;
|
|
441
|
+
} catch (_) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
371
446
|
// ── Last note extraction ──────────────────────────────────
|
|
372
447
|
|
|
373
448
|
function extractLastNote(hookInput, narrative, sessionAnalysis, auditData) {
|
|
@@ -720,13 +795,7 @@ function tombstoneSweep(threads, staleDays) {
|
|
|
720
795
|
|
|
721
796
|
function clearPresence(user) {
|
|
722
797
|
const presencePath = getUserPresencePath(user);
|
|
723
|
-
try {
|
|
724
|
-
if (fs.existsSync(presencePath)) {
|
|
725
|
-
fs.unlinkSync(presencePath);
|
|
726
|
-
}
|
|
727
|
-
} catch (_) {
|
|
728
|
-
// ignore
|
|
729
|
-
}
|
|
798
|
+
try { fs.unlinkSync(presencePath); } catch (_) {}
|
|
730
799
|
}
|
|
731
800
|
|
|
732
801
|
// ── Data-branch commit/push ───────────────────────────────
|
|
@@ -752,6 +821,7 @@ function commitAndPushDataBranch() {
|
|
|
752
821
|
|
|
753
822
|
_collectDirFiles(getAuditDir(), ".jsonl", "audit/", fileMap);
|
|
754
823
|
_collectDirFiles(getPresenceDir(), ".json", "presence/", fileMap);
|
|
824
|
+
_collectDirFiles(getTranscriptsDir(), ".jsonl", "transcripts/", fileMap);
|
|
755
825
|
|
|
756
826
|
if (Object.keys(fileMap).length === 0) return;
|
|
757
827
|
|
|
@@ -778,7 +848,7 @@ function commitAndPushDataBranch() {
|
|
|
778
848
|
|
|
779
849
|
// ── Main ──────────────────────────────────────────────────
|
|
780
850
|
|
|
781
|
-
function main() {
|
|
851
|
+
async function main() {
|
|
782
852
|
let hookInput = {};
|
|
783
853
|
try {
|
|
784
854
|
const raw = fs.readFileSync(0, "utf-8");
|
|
@@ -794,17 +864,43 @@ function main() {
|
|
|
794
864
|
const now = new Date().toISOString();
|
|
795
865
|
const branch = getBranch();
|
|
796
866
|
|
|
867
|
+
const cloud = useCloud();
|
|
797
868
|
const memoryPath = getMemoryPath();
|
|
798
869
|
const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
|
|
870
|
+
if (cloud) {
|
|
871
|
+
const cloudThreads = await cloudReadThreads();
|
|
872
|
+
if (cloudThreads) memory.threads = cloudThreads;
|
|
873
|
+
}
|
|
799
874
|
const config = loadJson(getConfigPath(), { stale_days: 14 });
|
|
800
875
|
const staleDays = config.stale_days || 14;
|
|
801
876
|
|
|
802
877
|
let filesTouched = extractFilesTouched(hookInput);
|
|
803
878
|
const auditData = extractSessionFromAudit(sessionId);
|
|
804
879
|
const sessionAnalysis = analyzeSessionActivities(hookInput, auditData);
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
880
|
+
let narrative = extractNarrative(hookInput);
|
|
881
|
+
let failed = extractFailedApproaches(hookInput);
|
|
882
|
+
let lastNote = extractLastNote(hookInput, narrative, sessionAnalysis, auditData);
|
|
883
|
+
|
|
884
|
+
// Redact secrets from these summary fields too — they're pulled from the
|
|
885
|
+
// same raw transcript as the full-transcript capture below, and land in
|
|
886
|
+
// sticky-note.json, which syncs to the whole team the same way the
|
|
887
|
+
// transcript store does. Without this, redact_transcripts would protect
|
|
888
|
+
// the verbatim copy while leaving the same secret sitting in plain sight
|
|
889
|
+
// in narrative/last_note/prompts.
|
|
890
|
+
if (config.redact_transcripts !== false) {
|
|
891
|
+
narrative = redactSecrets(narrative);
|
|
892
|
+
lastNote = redactSecrets(lastNote);
|
|
893
|
+
failed = failed.map((f) => {
|
|
894
|
+
if (typeof f === "string") return redactSecrets(f);
|
|
895
|
+
if (f && typeof f === "object") {
|
|
896
|
+
const copy = { ...f };
|
|
897
|
+
if (typeof copy.description === "string") copy.description = redactSecrets(copy.description);
|
|
898
|
+
if (typeof copy.error === "string") copy.error = redactSecrets(copy.error);
|
|
899
|
+
return copy;
|
|
900
|
+
}
|
|
901
|
+
return f;
|
|
902
|
+
});
|
|
903
|
+
}
|
|
808
904
|
|
|
809
905
|
// Merge audit-discovered files
|
|
810
906
|
const auditFiles = auditData.files || [];
|
|
@@ -884,7 +980,10 @@ function main() {
|
|
|
884
980
|
// Prompts from audit
|
|
885
981
|
const prompts = auditData.prompts || [];
|
|
886
982
|
const storedPrompts = prompts.length
|
|
887
|
-
? prompts.slice(0, 20).map((p) =>
|
|
983
|
+
? prompts.slice(0, 20).map((p) => {
|
|
984
|
+
const trimmed = p.substring(0, 300);
|
|
985
|
+
return config.redact_transcripts !== false ? redactSecrets(trimmed) : trimmed;
|
|
986
|
+
})
|
|
888
987
|
: [];
|
|
889
988
|
|
|
890
989
|
if (existing) {
|
|
@@ -920,7 +1019,7 @@ function main() {
|
|
|
920
1019
|
}
|
|
921
1020
|
} else {
|
|
922
1021
|
// Create new thread
|
|
923
|
-
|
|
1022
|
+
existing = {
|
|
924
1023
|
id: crypto.randomUUID(),
|
|
925
1024
|
user,
|
|
926
1025
|
project: memory.project || "",
|
|
@@ -945,9 +1044,17 @@ function main() {
|
|
|
945
1044
|
// V2.5 fields
|
|
946
1045
|
contributors: [user],
|
|
947
1046
|
resume_history: [],
|
|
948
|
-
}
|
|
1047
|
+
};
|
|
1048
|
+
threads.push(existing);
|
|
949
1049
|
}
|
|
950
1050
|
|
|
1051
|
+
// Persist the full verbatim transcript for this session, keyed by
|
|
1052
|
+
// thread ID. Opt-out via capture_transcripts: false in team config.
|
|
1053
|
+
const transcriptCaptured = captureTranscript(
|
|
1054
|
+
hookInput, existing.id, sessionId, aiTool, user, now, config
|
|
1055
|
+
);
|
|
1056
|
+
if (transcriptCaptured) existing.transcript_captured = true;
|
|
1057
|
+
|
|
951
1058
|
// V2.5: Capture commit SHAs for attribution engine
|
|
952
1059
|
const commitShas = getSessionCommitShas();
|
|
953
1060
|
|
|
@@ -966,33 +1073,42 @@ function main() {
|
|
|
966
1073
|
if (commitShas.length > 0) {
|
|
967
1074
|
auditEntry.commit_shas = commitShas;
|
|
968
1075
|
}
|
|
969
|
-
|
|
1076
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
970
1077
|
|
|
971
1078
|
// V2.5: Also write individual commit_sha audit entries for bridge lookup
|
|
972
1079
|
for (const sha of commitShas) {
|
|
973
|
-
|
|
1080
|
+
const commitEntry = {
|
|
974
1081
|
type: "commit",
|
|
975
1082
|
user,
|
|
976
1083
|
ts: now,
|
|
977
1084
|
session_id: sessionId,
|
|
978
1085
|
commit_sha: sha,
|
|
979
|
-
}
|
|
1086
|
+
};
|
|
1087
|
+
appendAuditLineBoth(commitEntry, cloud);
|
|
980
1088
|
}
|
|
981
1089
|
// Only clear transient files for true session end (Claude Code).
|
|
982
1090
|
// Copilot CLI fires per-turn; clearing would break state across turns.
|
|
983
1091
|
if (!isCopilotCli) {
|
|
984
1092
|
clearPresence(user);
|
|
1093
|
+
if (cloud) {
|
|
1094
|
+
cloudDeletePresence(user).catch(() => {});
|
|
1095
|
+
}
|
|
985
1096
|
clearSessionFile();
|
|
986
1097
|
clearHeadFile();
|
|
987
1098
|
clearInjectedSet();
|
|
988
1099
|
}
|
|
989
1100
|
saveMemoryMerged(memoryPath, memory);
|
|
1101
|
+
if (cloud) {
|
|
1102
|
+
const threadToSync = threads.find(t => t.session_id === sessionId);
|
|
1103
|
+
if (threadToSync) {
|
|
1104
|
+
cloudWriteThread(threadToSync).catch(() => {});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
990
1107
|
|
|
991
1108
|
// Auto-sync: commit (and optionally push) .sticky-note/ changes
|
|
992
|
-
|
|
993
|
-
if (syncConfig.auto_sync !== false) {
|
|
1109
|
+
if (config.auto_sync !== false) {
|
|
994
1110
|
try {
|
|
995
|
-
syncStickyNote({ push:
|
|
1111
|
+
syncStickyNote({ push: config.auto_push === true });
|
|
996
1112
|
} catch (_) {
|
|
997
1113
|
// Non-fatal — sync failure shouldn't block session-end
|
|
998
1114
|
}
|
|
@@ -1013,9 +1129,7 @@ function main() {
|
|
|
1013
1129
|
process.exit(0);
|
|
1014
1130
|
}
|
|
1015
1131
|
|
|
1016
|
-
|
|
1017
|
-
main();
|
|
1018
|
-
} catch (err) {
|
|
1132
|
+
main().catch((err) => {
|
|
1019
1133
|
try { utils.logHookError("session-end", err); } catch (_) {}
|
|
1020
1134
|
_safeExit();
|
|
1021
|
-
}
|
|
1135
|
+
});
|
|
@@ -118,6 +118,7 @@ const {
|
|
|
118
118
|
saveJson,
|
|
119
119
|
saveMemoryMerged,
|
|
120
120
|
appendAuditLine,
|
|
121
|
+
appendAuditLineBoth,
|
|
121
122
|
getUser,
|
|
122
123
|
getBranch,
|
|
123
124
|
getSessionId,
|
|
@@ -130,6 +131,10 @@ const {
|
|
|
130
131
|
markThreadInjected,
|
|
131
132
|
clearActiveResumeThreadId,
|
|
132
133
|
getActiveResumeThreadId,
|
|
134
|
+
useCloud,
|
|
135
|
+
cloudReadThreads,
|
|
136
|
+
cloudReadPresence,
|
|
137
|
+
cloudReadConfig,
|
|
133
138
|
normalizeSep,
|
|
134
139
|
detectLostThreads,
|
|
135
140
|
} = utils;
|
|
@@ -926,7 +931,7 @@ function fetchAndMergeDataBranch() {
|
|
|
926
931
|
|
|
927
932
|
// ── Main ──────────────────────────────────────────────────
|
|
928
933
|
|
|
929
|
-
function main() {
|
|
934
|
+
async function main() {
|
|
930
935
|
let hookInput = {};
|
|
931
936
|
try {
|
|
932
937
|
if (!process.stdin.isTTY) {
|
|
@@ -969,6 +974,7 @@ function main() {
|
|
|
969
974
|
// Auto-register sticky-note MCP server in .mcp.json
|
|
970
975
|
ensureMcpServerRegistered();
|
|
971
976
|
|
|
977
|
+
const cloud = useCloud();
|
|
972
978
|
const memoryPath = getMemoryPath();
|
|
973
979
|
const memory = loadJson(memoryPath, {
|
|
974
980
|
version: "2",
|
|
@@ -976,6 +982,12 @@ function main() {
|
|
|
976
982
|
threads: [],
|
|
977
983
|
});
|
|
978
984
|
|
|
985
|
+
let cloudThreads = null, cloudConfig = null;
|
|
986
|
+
if (cloud) {
|
|
987
|
+
[cloudThreads, cloudConfig] = await Promise.all([cloudReadThreads(), cloudReadConfig()]);
|
|
988
|
+
}
|
|
989
|
+
if (cloudThreads) memory.threads = cloudThreads;
|
|
990
|
+
|
|
979
991
|
// Rollback detection: warn if threads went missing (e.g., after git reset)
|
|
980
992
|
try {
|
|
981
993
|
const lost = detectLostThreads(memoryPath);
|
|
@@ -989,7 +1001,7 @@ function main() {
|
|
|
989
1001
|
}
|
|
990
1002
|
} catch (_) { /* rollback detection must not break session */ }
|
|
991
1003
|
|
|
992
|
-
const config = loadJson(getConfigPath(), { stale_days: 14 });
|
|
1004
|
+
const config = cloudConfig || loadJson(getConfigPath(), { stale_days: 14 });
|
|
993
1005
|
const staleDays = config.stale_days != null ? config.stale_days : 14;
|
|
994
1006
|
const autoCloseHours =
|
|
995
1007
|
config.copilot_cli_auto_close_hours != null
|
|
@@ -1072,7 +1084,20 @@ function main() {
|
|
|
1072
1084
|
const threadResult = formatThreadsForInjection((memory.threads || []).filter(Boolean));
|
|
1073
1085
|
const threadContext = threadResult.text;
|
|
1074
1086
|
const configContext = formatConfigForInjection(config);
|
|
1075
|
-
|
|
1087
|
+
let presenceData;
|
|
1088
|
+
if (cloud) {
|
|
1089
|
+
const cloudPresence = await cloudReadPresence();
|
|
1090
|
+
if (cloudPresence) {
|
|
1091
|
+
presenceData = {};
|
|
1092
|
+
for (const entry of cloudPresence) {
|
|
1093
|
+
const u = entry.user || entry.name;
|
|
1094
|
+
if (u) presenceData[u] = entry;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (!presenceData) {
|
|
1099
|
+
presenceData = loadAllPresence();
|
|
1100
|
+
}
|
|
1076
1101
|
const presenceContext = formatPresence(presenceData, (memory.threads || []).filter(Boolean));
|
|
1077
1102
|
const overlapContext = formatOverlapWarnings(
|
|
1078
1103
|
(memory.threads || []).filter(Boolean), getUser(), memory
|
|
@@ -1088,12 +1113,13 @@ function main() {
|
|
|
1088
1113
|
}
|
|
1089
1114
|
}
|
|
1090
1115
|
|
|
1091
|
-
|
|
1116
|
+
const auditEntry = {
|
|
1092
1117
|
type: "session_start",
|
|
1093
1118
|
user: getUser(),
|
|
1094
1119
|
ts: new Date().toISOString(),
|
|
1095
1120
|
session_id: sessionId,
|
|
1096
|
-
}
|
|
1121
|
+
};
|
|
1122
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
1097
1123
|
|
|
1098
1124
|
saveMemoryMerged(memoryPath, memory);
|
|
1099
1125
|
|
|
@@ -1228,11 +1254,9 @@ function main() {
|
|
|
1228
1254
|
|
|
1229
1255
|
// ── Entry point ───────────────────────────────────────────
|
|
1230
1256
|
|
|
1231
|
-
|
|
1232
|
-
main();
|
|
1233
|
-
} catch (err) {
|
|
1257
|
+
main().catch((err) => {
|
|
1234
1258
|
try { utils.reportHookError("session-start", err); } catch (_) {
|
|
1235
1259
|
try { utils.logHookError("session-start", err); } catch (_) {}
|
|
1236
1260
|
}
|
|
1237
1261
|
_safeExit();
|
|
1238
|
-
}
|
|
1262
|
+
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# sticky-codex.ps1 - Codex wrapper for Sticky Note
|
|
1
|
+
# sticky-codex.ps1 - Codex wrapper for Sticky Note V3 (Windows)
|
|
2
2
|
#
|
|
3
3
|
# Captures Codex stdout/stderr to a temp session log.
|
|
4
4
|
# On exit: calls session-end.js with the transcript for
|
|
5
5
|
# narrative + failed_approaches extraction.
|
|
6
6
|
#
|
|
7
|
+
# V3: If STICKY_URL is set, reads cloud context before session start
|
|
8
|
+
# and writes session data to cloud on exit (via hook scripts).
|
|
9
|
+
#
|
|
7
10
|
# Usage:
|
|
8
11
|
# .\sticky-codex.ps1 [codex args...]
|
|
9
12
|
#
|
|
10
13
|
# Setup:
|
|
11
14
|
# npx sticky-note init --codex
|
|
12
|
-
#
|
|
13
|
-
# Limitation: Context is printed to terminal after Codex exits,
|
|
14
|
-
# not injected into the Codex window. Per-prompt surfacing not available.
|
|
15
15
|
|
|
16
16
|
param(
|
|
17
17
|
[Parameter(ValueFromRemainingArguments = $true)]
|
|
@@ -34,6 +34,22 @@ Write-Host ""
|
|
|
34
34
|
Write-Host "[STICKY-NOTE] Injecting context for Codex session $SessionId"
|
|
35
35
|
Write-Host ""
|
|
36
36
|
|
|
37
|
+
# Load .env.sticky if it exists (for STICKY_URL and STICKY_API_KEY)
|
|
38
|
+
$EnvStickyFile = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptDir)) ".env.sticky"
|
|
39
|
+
if (Test-Path $EnvStickyFile) {
|
|
40
|
+
Get-Content $EnvStickyFile | ForEach-Object {
|
|
41
|
+
$line = $_.Trim()
|
|
42
|
+
if ($line -and -not $line.StartsWith("#")) {
|
|
43
|
+
$eq = $line.IndexOf("=")
|
|
44
|
+
if ($eq -gt 0) {
|
|
45
|
+
$key = $line.Substring(0, $eq).Trim()
|
|
46
|
+
$val = $line.Substring($eq + 1).Trim()
|
|
47
|
+
[Environment]::SetEnvironmentVariable($key, $val, "Process")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
$MemoryFile = Join-Path $StickyDir "sticky-note.json"
|
|
38
54
|
if (Test-Path $MemoryFile) {
|
|
39
55
|
try {
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# sticky-codex.sh — Codex wrapper for Sticky Note
|
|
2
|
+
# sticky-codex.sh — Codex wrapper for Sticky Note V3
|
|
3
3
|
#
|
|
4
4
|
# Captures Codex stdout/stderr to a temp session log.
|
|
5
5
|
# On exit: calls session-end.js with the transcript for
|
|
6
6
|
# narrative + failed_approaches extraction.
|
|
7
7
|
#
|
|
8
|
+
# V3: If STICKY_URL is set, reads cloud context before session start
|
|
9
|
+
# and writes session data to cloud on exit (via hook scripts).
|
|
10
|
+
#
|
|
8
11
|
# Usage:
|
|
9
12
|
# sticky-codex [codex args...]
|
|
10
13
|
#
|
|
11
14
|
# Setup:
|
|
12
15
|
# npx sticky-note init --codex
|
|
13
16
|
# alias sticky-codex="/path/to/.claude/hooks/sticky-codex.sh"
|
|
14
|
-
#
|
|
15
|
-
# Limitation: Context is printed to terminal after Codex exits,
|
|
16
|
-
# not injected into the Codex window. Per-prompt surfacing not available.
|
|
17
17
|
|
|
18
18
|
set -euo pipefail
|
|
19
19
|
|
|
@@ -28,9 +28,18 @@ mkdir -p "$STICKY_DIR"
|
|
|
28
28
|
|
|
29
29
|
# Print context from sticky-note before starting Codex
|
|
30
30
|
echo ""
|
|
31
|
-
echo "
|
|
31
|
+
echo "[STICKY-NOTE] Injecting context for Codex session $SESSION_ID"
|
|
32
32
|
echo ""
|
|
33
33
|
|
|
34
|
+
# Load .env.sticky if it exists (for STICKY_URL and STICKY_API_KEY)
|
|
35
|
+
ENV_STICKY="$(cd "$SCRIPT_DIR/../.." && pwd)/.env.sticky"
|
|
36
|
+
if [ -f "$ENV_STICKY" ]; then
|
|
37
|
+
set -a
|
|
38
|
+
# shellcheck disable=SC1090
|
|
39
|
+
source "$ENV_STICKY"
|
|
40
|
+
set +a
|
|
41
|
+
fi
|
|
42
|
+
|
|
34
43
|
if [ -f "$STICKY_DIR/sticky-note.json" ]; then
|
|
35
44
|
node "$SCRIPT_DIR/session-start.js" <<EOF 2>/dev/null || true
|
|
36
45
|
{"session_id": "$SESSION_ID"}
|