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.
@@ -59,10 +59,11 @@ const {
59
59
  getSessionId,
60
60
  getUser,
61
61
  appendAuditLine,
62
+ appendAuditLineBoth,
62
63
  isThreadInjected,
63
64
  markThreadInjected,
64
- isOverlapWarned,
65
- markOverlapWarned,
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
- // ── Overlap deny via COPILOT_LOADER_PID (Copilot CLI only) ──
141
- // Each Copilot CLI session has a unique COPILOT_LOADER_PID env var.
142
- // On the first tool call, if overlaps exist, deny with the warning.
143
- // The warned state is keyed by PID so concurrent sessions don't interfere.
144
-
145
- const { execSync } = require("child_process");
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 result = execSync("git diff --name-only", {
162
- encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
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
- for (const f of result.trim().split(/\r?\n/)) {
165
- if (f.trim()) files.add(f.trim());
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 (_) { /* ignore */ }
168
- return files;
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
- const output = outputParts.join("\n\n");
246
+ let output = outputParts.join("\n\n");
252
247
 
253
248
  // Audit the injection
254
249
  try {
255
- appendAuditLine({
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
- try {
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
- const narrative = extractNarrative(hookInput);
806
- const failed = extractFailedApproaches(hookInput);
807
- const lastNote = extractLastNote(hookInput, narrative, sessionAnalysis, auditData);
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) => p.substring(0, 300))
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
- threads.push({
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
- appendAuditLine(auditEntry);
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
- appendAuditLine({
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
- const syncConfig = loadJson(getConfigPath(), {});
993
- if (syncConfig.auto_sync !== false) {
1109
+ if (config.auto_sync !== false) {
994
1110
  try {
995
- syncStickyNote({ push: syncConfig.auto_push === true });
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
- try {
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
- const presenceData = loadAllPresence();
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
- appendAuditLine({
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
- try {
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 V2 (Windows)
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 V2
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 "📌 Sticky Note — injecting context for Codex session $SESSION_ID"
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"}