sticky-note-cli 2.9.2 → 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.
- package/README.md +94 -10
- package/bin/cli.js +1153 -174
- package/bin/mcp-server.js +218 -36
- package/package.json +1 -1
- package/templates/CLAUDE.md +23 -0
- package/templates/gitignore-additions.txt +3 -0
- package/templates/hooks/data-branch.js +256 -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 +190 -21
- package/templates/hooks/session-start.js +81 -9
- package/templates/hooks/sticky-codex.ps1 +20 -4
- package/templates/hooks/sticky-codex.sh +14 -5
- package/templates/hooks/sticky-utils.js +335 -29
- package/templates/hooks/track-work.js +27 -13
- package/templates/hooks.json +47 -47
- package/templates/settings.json +81 -81
- package/templates/sticky-note-config.json +2 -0
- package/templates/sticky-note-install.yml +80 -0
- package/templates/sticky-note.json +5 -5
|
@@ -29,15 +29,24 @@ try {
|
|
|
29
29
|
_safeExit();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
let dataBranch = null;
|
|
33
|
+
try { dataBranch = require("./data-branch.js"); } catch (_) {}
|
|
34
|
+
|
|
32
35
|
const {
|
|
33
36
|
getMemoryPath,
|
|
34
37
|
getConfigPath,
|
|
38
|
+
getAuditDir,
|
|
39
|
+
getPresenceDir,
|
|
35
40
|
getUserPresencePath,
|
|
36
41
|
getAllAuditPaths,
|
|
42
|
+
getTranscriptsDir,
|
|
43
|
+
getTranscriptPath,
|
|
44
|
+
redactSecrets,
|
|
37
45
|
loadJson,
|
|
38
46
|
saveJson,
|
|
39
47
|
saveMemoryMerged,
|
|
40
48
|
appendAuditLine,
|
|
49
|
+
appendAuditLineBoth,
|
|
41
50
|
getUser,
|
|
42
51
|
getBranch,
|
|
43
52
|
detectTool,
|
|
@@ -58,6 +67,10 @@ const {
|
|
|
58
67
|
clearActiveResumeThreadId,
|
|
59
68
|
clearInjectedSet,
|
|
60
69
|
normalizeSep,
|
|
70
|
+
useCloud,
|
|
71
|
+
cloudReadThreads,
|
|
72
|
+
cloudWriteThread,
|
|
73
|
+
cloudDeletePresence,
|
|
61
74
|
syncStickyNote,
|
|
62
75
|
} = utils;
|
|
63
76
|
|
|
@@ -363,6 +376,73 @@ function extractFailedApproaches(hookInput) {
|
|
|
363
376
|
return extractFailedFromEntries(entries);
|
|
364
377
|
}
|
|
365
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
|
+
|
|
366
446
|
// ── Last note extraction ──────────────────────────────────
|
|
367
447
|
|
|
368
448
|
function extractLastNote(hookInput, narrative, sessionAnalysis, auditData) {
|
|
@@ -715,18 +795,60 @@ function tombstoneSweep(threads, staleDays) {
|
|
|
715
795
|
|
|
716
796
|
function clearPresence(user) {
|
|
717
797
|
const presencePath = getUserPresencePath(user);
|
|
798
|
+
try { fs.unlinkSync(presencePath); } catch (_) {}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ── Data-branch commit/push ───────────────────────────────
|
|
802
|
+
|
|
803
|
+
/** Collect files from a directory matching an extension into fileMap. */
|
|
804
|
+
function _collectDirFiles(dir, ext, prefix, fileMap) {
|
|
805
|
+
if (!dir || !fs.existsSync(dir)) return;
|
|
806
|
+
for (const file of fs.readdirSync(dir)) {
|
|
807
|
+
if (file.endsWith(ext)) {
|
|
808
|
+
fileMap[prefix + file] = fs.readFileSync(path.join(dir, file), "utf-8");
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function commitAndPushDataBranch() {
|
|
814
|
+
if (!dataBranch) return;
|
|
718
815
|
try {
|
|
719
|
-
|
|
720
|
-
|
|
816
|
+
const fileMap = {};
|
|
817
|
+
const memPath = getMemoryPath();
|
|
818
|
+
if (fs.existsSync(memPath)) {
|
|
819
|
+
fileMap["sticky-note.json"] = fs.readFileSync(memPath, "utf-8");
|
|
721
820
|
}
|
|
722
|
-
|
|
723
|
-
|
|
821
|
+
|
|
822
|
+
_collectDirFiles(getAuditDir(), ".jsonl", "audit/", fileMap);
|
|
823
|
+
_collectDirFiles(getPresenceDir(), ".json", "presence/", fileMap);
|
|
824
|
+
_collectDirFiles(getTranscriptsDir(), ".jsonl", "transcripts/", fileMap);
|
|
825
|
+
|
|
826
|
+
if (Object.keys(fileMap).length === 0) return;
|
|
827
|
+
|
|
828
|
+
dataBranch.commitFilesToBranch(dataBranch.DATA_BRANCH, fileMap);
|
|
829
|
+
|
|
830
|
+
const remote = dataBranch.getDefaultRemote();
|
|
831
|
+
if (remote) {
|
|
832
|
+
const pushResult = dataBranch.pushDataBranch(
|
|
833
|
+
remote, dataBranch.DATA_BRANCH, 3, getMemoryPath(), loadJson, saveJson
|
|
834
|
+
);
|
|
835
|
+
if (!pushResult.ok) {
|
|
836
|
+
process.stderr.write(
|
|
837
|
+
"[STICKY-NOTE] warning: failed to push data branch — " + pushResult.error + "\n" +
|
|
838
|
+
"[STICKY-NOTE] your data is safe locally in .git/sticky-note/\n"
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
} catch (err) {
|
|
843
|
+
process.stderr.write(
|
|
844
|
+
"[STICKY-NOTE] warning: data branch sync failed — " + err.message + "\n"
|
|
845
|
+
);
|
|
724
846
|
}
|
|
725
847
|
}
|
|
726
848
|
|
|
727
849
|
// ── Main ──────────────────────────────────────────────────
|
|
728
850
|
|
|
729
|
-
function main() {
|
|
851
|
+
async function main() {
|
|
730
852
|
let hookInput = {};
|
|
731
853
|
try {
|
|
732
854
|
const raw = fs.readFileSync(0, "utf-8");
|
|
@@ -742,17 +864,43 @@ function main() {
|
|
|
742
864
|
const now = new Date().toISOString();
|
|
743
865
|
const branch = getBranch();
|
|
744
866
|
|
|
867
|
+
const cloud = useCloud();
|
|
745
868
|
const memoryPath = getMemoryPath();
|
|
746
869
|
const memory = loadJson(memoryPath, { version: "2", project: "", threads: [] });
|
|
870
|
+
if (cloud) {
|
|
871
|
+
const cloudThreads = await cloudReadThreads();
|
|
872
|
+
if (cloudThreads) memory.threads = cloudThreads;
|
|
873
|
+
}
|
|
747
874
|
const config = loadJson(getConfigPath(), { stale_days: 14 });
|
|
748
875
|
const staleDays = config.stale_days || 14;
|
|
749
876
|
|
|
750
877
|
let filesTouched = extractFilesTouched(hookInput);
|
|
751
878
|
const auditData = extractSessionFromAudit(sessionId);
|
|
752
879
|
const sessionAnalysis = analyzeSessionActivities(hookInput, auditData);
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
+
}
|
|
756
904
|
|
|
757
905
|
// Merge audit-discovered files
|
|
758
906
|
const auditFiles = auditData.files || [];
|
|
@@ -832,7 +980,10 @@ function main() {
|
|
|
832
980
|
// Prompts from audit
|
|
833
981
|
const prompts = auditData.prompts || [];
|
|
834
982
|
const storedPrompts = prompts.length
|
|
835
|
-
? 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
|
+
})
|
|
836
987
|
: [];
|
|
837
988
|
|
|
838
989
|
if (existing) {
|
|
@@ -868,7 +1019,7 @@ function main() {
|
|
|
868
1019
|
}
|
|
869
1020
|
} else {
|
|
870
1021
|
// Create new thread
|
|
871
|
-
|
|
1022
|
+
existing = {
|
|
872
1023
|
id: crypto.randomUUID(),
|
|
873
1024
|
user,
|
|
874
1025
|
project: memory.project || "",
|
|
@@ -893,9 +1044,17 @@ function main() {
|
|
|
893
1044
|
// V2.5 fields
|
|
894
1045
|
contributors: [user],
|
|
895
1046
|
resume_history: [],
|
|
896
|
-
}
|
|
1047
|
+
};
|
|
1048
|
+
threads.push(existing);
|
|
897
1049
|
}
|
|
898
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
|
+
|
|
899
1058
|
// V2.5: Capture commit SHAs for attribution engine
|
|
900
1059
|
const commitShas = getSessionCommitShas();
|
|
901
1060
|
|
|
@@ -914,38 +1073,50 @@ function main() {
|
|
|
914
1073
|
if (commitShas.length > 0) {
|
|
915
1074
|
auditEntry.commit_shas = commitShas;
|
|
916
1075
|
}
|
|
917
|
-
|
|
1076
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
918
1077
|
|
|
919
1078
|
// V2.5: Also write individual commit_sha audit entries for bridge lookup
|
|
920
1079
|
for (const sha of commitShas) {
|
|
921
|
-
|
|
1080
|
+
const commitEntry = {
|
|
922
1081
|
type: "commit",
|
|
923
1082
|
user,
|
|
924
1083
|
ts: now,
|
|
925
1084
|
session_id: sessionId,
|
|
926
1085
|
commit_sha: sha,
|
|
927
|
-
}
|
|
1086
|
+
};
|
|
1087
|
+
appendAuditLineBoth(commitEntry, cloud);
|
|
928
1088
|
}
|
|
929
1089
|
// Only clear transient files for true session end (Claude Code).
|
|
930
1090
|
// Copilot CLI fires per-turn; clearing would break state across turns.
|
|
931
1091
|
if (!isCopilotCli) {
|
|
932
1092
|
clearPresence(user);
|
|
1093
|
+
if (cloud) {
|
|
1094
|
+
cloudDeletePresence(user).catch(() => {});
|
|
1095
|
+
}
|
|
933
1096
|
clearSessionFile();
|
|
934
1097
|
clearHeadFile();
|
|
935
1098
|
clearInjectedSet();
|
|
936
1099
|
}
|
|
937
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
|
+
}
|
|
938
1107
|
|
|
939
1108
|
// Auto-sync: commit (and optionally push) .sticky-note/ changes
|
|
940
|
-
|
|
941
|
-
if (syncConfig.auto_sync !== false) {
|
|
1109
|
+
if (config.auto_sync !== false) {
|
|
942
1110
|
try {
|
|
943
|
-
syncStickyNote({ push:
|
|
1111
|
+
syncStickyNote({ push: config.auto_push === true });
|
|
944
1112
|
} catch (_) {
|
|
945
1113
|
// Non-fatal — sync failure shouldn't block session-end
|
|
946
1114
|
}
|
|
947
1115
|
}
|
|
948
1116
|
|
|
1117
|
+
// Commit current data to sticky-note/data branch and push
|
|
1118
|
+
commitAndPushDataBranch();
|
|
1119
|
+
|
|
949
1120
|
const fileCount = filesTouched.length;
|
|
950
1121
|
const commitCount = commitShas.length;
|
|
951
1122
|
const statusLabel = isCopilotCli ? "updated" : "closed";
|
|
@@ -958,9 +1129,7 @@ function main() {
|
|
|
958
1129
|
process.exit(0);
|
|
959
1130
|
}
|
|
960
1131
|
|
|
961
|
-
|
|
962
|
-
main();
|
|
963
|
-
} catch (err) {
|
|
1132
|
+
main().catch((err) => {
|
|
964
1133
|
try { utils.logHookError("session-end", err); } catch (_) {}
|
|
965
1134
|
_safeExit();
|
|
966
|
-
}
|
|
1135
|
+
});
|
|
@@ -105,6 +105,10 @@ try {
|
|
|
105
105
|
// session-start to run.
|
|
106
106
|
try { utils.selfHealHookPaths && utils.selfHealHookPaths(); } catch (_) { /* non-fatal */ }
|
|
107
107
|
|
|
108
|
+
// Non-fatal: data-branch sync is disabled if the module is missing.
|
|
109
|
+
let dataBranch = null;
|
|
110
|
+
try { dataBranch = require("./data-branch.js"); } catch (_) {}
|
|
111
|
+
|
|
108
112
|
const {
|
|
109
113
|
getMemoryPath,
|
|
110
114
|
getConfigPath,
|
|
@@ -114,6 +118,7 @@ const {
|
|
|
114
118
|
saveJson,
|
|
115
119
|
saveMemoryMerged,
|
|
116
120
|
appendAuditLine,
|
|
121
|
+
appendAuditLineBoth,
|
|
117
122
|
getUser,
|
|
118
123
|
getBranch,
|
|
119
124
|
getSessionId,
|
|
@@ -126,6 +131,10 @@ const {
|
|
|
126
131
|
markThreadInjected,
|
|
127
132
|
clearActiveResumeThreadId,
|
|
128
133
|
getActiveResumeThreadId,
|
|
134
|
+
useCloud,
|
|
135
|
+
cloudReadThreads,
|
|
136
|
+
cloudReadPresence,
|
|
137
|
+
cloudReadConfig,
|
|
129
138
|
normalizeSep,
|
|
130
139
|
detectLostThreads,
|
|
131
140
|
} = utils;
|
|
@@ -879,9 +888,50 @@ function ensureEnvironmentProvisioned() {
|
|
|
879
888
|
if (debug) process.stderr.write("[sticky-note] environment provisioning complete\n");
|
|
880
889
|
}
|
|
881
890
|
|
|
891
|
+
// ── Data branch fetch-and-merge ───────────────────────────
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Fetches the remote sticky-note/data branch and merges into local memory.
|
|
895
|
+
* Also reads from the LOCAL data branch (fresh clones, offline, tests).
|
|
896
|
+
* Non-blocking — the session continues with local data on any failure.
|
|
897
|
+
*/
|
|
898
|
+
function fetchAndMergeDataBranch() {
|
|
899
|
+
if (!dataBranch) return;
|
|
900
|
+
|
|
901
|
+
const memPath = getMemoryPath();
|
|
902
|
+
const mergeFrom = (ref) => {
|
|
903
|
+
const content = dataBranch.readFileFromBranch(ref, "sticky-note.json");
|
|
904
|
+
if (content) {
|
|
905
|
+
dataBranch.mergeAndSaveFromRemote(memPath, content, loadJson, saveJson);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
const remote = dataBranch.getDefaultRemote();
|
|
911
|
+
|
|
912
|
+
if (remote) {
|
|
913
|
+
const fetchResult = dataBranch.fetchDataBranch(remote);
|
|
914
|
+
if (!fetchResult.ok) {
|
|
915
|
+
process.stderr.write(
|
|
916
|
+
"[STICKY-NOTE] offline — using local thread cache (" +
|
|
917
|
+
(fetchResult.error || "fetch failed") + ")\n"
|
|
918
|
+
);
|
|
919
|
+
} else if (fetchResult.remoteRef) {
|
|
920
|
+
mergeFrom(fetchResult.remoteRef);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
mergeFrom(dataBranch.DATA_REF);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
process.stderr.write(
|
|
927
|
+
"[STICKY-NOTE] offline — using local thread cache (" + err.message + ")\n"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
882
932
|
// ── Main ──────────────────────────────────────────────────
|
|
883
933
|
|
|
884
|
-
function main() {
|
|
934
|
+
async function main() {
|
|
885
935
|
let hookInput = {};
|
|
886
936
|
try {
|
|
887
937
|
if (!process.stdin.isTTY) {
|
|
@@ -892,6 +942,9 @@ function main() {
|
|
|
892
942
|
hookInput = {};
|
|
893
943
|
}
|
|
894
944
|
|
|
945
|
+
// Fetch remote data branch and merge into local cache — before reading memory
|
|
946
|
+
fetchAndMergeDataBranch();
|
|
947
|
+
|
|
895
948
|
let sessionId = getSessionId(hookInput);
|
|
896
949
|
const aiTool = detectTool(hookInput);
|
|
897
950
|
const isCopilotCli = aiTool === "copilot-cli" || process.argv.includes("--copilot-cli") || !!process.env.COPILOT_CLI;
|
|
@@ -921,6 +974,7 @@ function main() {
|
|
|
921
974
|
// Auto-register sticky-note MCP server in .mcp.json
|
|
922
975
|
ensureMcpServerRegistered();
|
|
923
976
|
|
|
977
|
+
const cloud = useCloud();
|
|
924
978
|
const memoryPath = getMemoryPath();
|
|
925
979
|
const memory = loadJson(memoryPath, {
|
|
926
980
|
version: "2",
|
|
@@ -928,6 +982,12 @@ function main() {
|
|
|
928
982
|
threads: [],
|
|
929
983
|
});
|
|
930
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
|
+
|
|
931
991
|
// Rollback detection: warn if threads went missing (e.g., after git reset)
|
|
932
992
|
try {
|
|
933
993
|
const lost = detectLostThreads(memoryPath);
|
|
@@ -941,7 +1001,7 @@ function main() {
|
|
|
941
1001
|
}
|
|
942
1002
|
} catch (_) { /* rollback detection must not break session */ }
|
|
943
1003
|
|
|
944
|
-
const config = loadJson(getConfigPath(), { stale_days: 14 });
|
|
1004
|
+
const config = cloudConfig || loadJson(getConfigPath(), { stale_days: 14 });
|
|
945
1005
|
const staleDays = config.stale_days != null ? config.stale_days : 14;
|
|
946
1006
|
const autoCloseHours =
|
|
947
1007
|
config.copilot_cli_auto_close_hours != null
|
|
@@ -1024,7 +1084,20 @@ function main() {
|
|
|
1024
1084
|
const threadResult = formatThreadsForInjection((memory.threads || []).filter(Boolean));
|
|
1025
1085
|
const threadContext = threadResult.text;
|
|
1026
1086
|
const configContext = formatConfigForInjection(config);
|
|
1027
|
-
|
|
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
|
+
}
|
|
1028
1101
|
const presenceContext = formatPresence(presenceData, (memory.threads || []).filter(Boolean));
|
|
1029
1102
|
const overlapContext = formatOverlapWarnings(
|
|
1030
1103
|
(memory.threads || []).filter(Boolean), getUser(), memory
|
|
@@ -1040,12 +1113,13 @@ function main() {
|
|
|
1040
1113
|
}
|
|
1041
1114
|
}
|
|
1042
1115
|
|
|
1043
|
-
|
|
1116
|
+
const auditEntry = {
|
|
1044
1117
|
type: "session_start",
|
|
1045
1118
|
user: getUser(),
|
|
1046
1119
|
ts: new Date().toISOString(),
|
|
1047
1120
|
session_id: sessionId,
|
|
1048
|
-
}
|
|
1121
|
+
};
|
|
1122
|
+
appendAuditLineBoth(auditEntry, cloud);
|
|
1049
1123
|
|
|
1050
1124
|
saveMemoryMerged(memoryPath, memory);
|
|
1051
1125
|
|
|
@@ -1180,11 +1254,9 @@ function main() {
|
|
|
1180
1254
|
|
|
1181
1255
|
// ── Entry point ───────────────────────────────────────────
|
|
1182
1256
|
|
|
1183
|
-
|
|
1184
|
-
main();
|
|
1185
|
-
} catch (err) {
|
|
1257
|
+
main().catch((err) => {
|
|
1186
1258
|
try { utils.reportHookError("session-start", err); } catch (_) {
|
|
1187
1259
|
try { utils.logHookError("session-start", err); } catch (_) {}
|
|
1188
1260
|
}
|
|
1189
1261
|
_safeExit();
|
|
1190
|
-
}
|
|
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"}
|