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.
@@ -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
- if (fs.existsSync(presencePath)) {
720
- fs.unlinkSync(presencePath);
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
- } catch (_) {
723
- // ignore
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
- const narrative = extractNarrative(hookInput);
754
- const failed = extractFailedApproaches(hookInput);
755
- 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
+ }
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) => 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
+ })
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
- threads.push({
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
- appendAuditLine(auditEntry);
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
- appendAuditLine({
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
- const syncConfig = loadJson(getConfigPath(), {});
941
- if (syncConfig.auto_sync !== false) {
1109
+ if (config.auto_sync !== false) {
942
1110
  try {
943
- syncStickyNote({ push: syncConfig.auto_push === true });
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
- try {
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
- 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
+ }
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
- appendAuditLine({
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
- try {
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 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"}