@smilintux/skmemory 0.7.2 → 0.9.2

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.
Files changed (111) hide show
  1. package/.github/workflows/ci.yml +4 -4
  2. package/.github/workflows/publish.yml +4 -5
  3. package/ARCHITECTURE.md +298 -0
  4. package/CHANGELOG.md +27 -1
  5. package/README.md +6 -0
  6. package/examples/stignore-agent.example +59 -0
  7. package/examples/stignore-root.example +62 -0
  8. package/openclaw-plugin/package.json +2 -1
  9. package/openclaw-plugin/src/index.js +527 -230
  10. package/package.json +1 -1
  11. package/pyproject.toml +5 -2
  12. package/scripts/dream-rescue.py +179 -0
  13. package/scripts/memory-cleanup.py +313 -0
  14. package/scripts/recover-missing.py +180 -0
  15. package/scripts/skcapstone-backup.sh +44 -0
  16. package/seeds/cloud9-lumina.seed.json +6 -4
  17. package/seeds/cloud9-opus.seed.json +6 -4
  18. package/seeds/courage.seed.json +9 -2
  19. package/seeds/curiosity.seed.json +9 -2
  20. package/seeds/grief.seed.json +9 -2
  21. package/seeds/joy.seed.json +9 -2
  22. package/seeds/love.seed.json +9 -2
  23. package/seeds/lumina-cloud9-breakthrough.seed.json +7 -5
  24. package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
  25. package/seeds/lumina-kingdom-founding.seed.json +9 -7
  26. package/seeds/lumina-pma-signed.seed.json +8 -6
  27. package/seeds/lumina-singular-achievement.seed.json +8 -6
  28. package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
  29. package/seeds/plant-lumina-seeds.py +2 -2
  30. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  31. package/seeds/sovereignty.seed.json +9 -2
  32. package/seeds/trust.seed.json +9 -2
  33. package/skmemory/__init__.py +16 -13
  34. package/skmemory/agents.py +10 -10
  35. package/skmemory/ai_client.py +10 -21
  36. package/skmemory/anchor.py +5 -9
  37. package/skmemory/audience.py +278 -0
  38. package/skmemory/backends/__init__.py +1 -1
  39. package/skmemory/backends/base.py +3 -4
  40. package/skmemory/backends/file_backend.py +18 -13
  41. package/skmemory/backends/skgraph_backend.py +7 -19
  42. package/skmemory/backends/skvector_backend.py +7 -18
  43. package/skmemory/backends/sqlite_backend.py +115 -32
  44. package/skmemory/backends/vaulted_backend.py +7 -9
  45. package/skmemory/cli.py +146 -78
  46. package/skmemory/config.py +11 -13
  47. package/skmemory/context_loader.py +21 -23
  48. package/skmemory/data/audience_config.json +60 -0
  49. package/skmemory/endpoint_selector.py +36 -31
  50. package/skmemory/febs.py +225 -0
  51. package/skmemory/fortress.py +30 -40
  52. package/skmemory/hooks/__init__.py +18 -0
  53. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  54. package/skmemory/hooks/pre-compact-save.sh +81 -0
  55. package/skmemory/hooks/session-end-save.sh +103 -0
  56. package/skmemory/hooks/session-start-ritual.sh +104 -0
  57. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  58. package/skmemory/importers/telegram.py +42 -13
  59. package/skmemory/importers/telegram_api.py +152 -60
  60. package/skmemory/journal.py +3 -7
  61. package/skmemory/lovenote.py +4 -11
  62. package/skmemory/mcp_server.py +182 -29
  63. package/skmemory/models.py +10 -8
  64. package/skmemory/openclaw.py +14 -22
  65. package/skmemory/post_install.py +86 -0
  66. package/skmemory/predictive.py +13 -9
  67. package/skmemory/promotion.py +48 -24
  68. package/skmemory/quadrants.py +100 -24
  69. package/skmemory/register.py +144 -18
  70. package/skmemory/register_mcp.py +1 -2
  71. package/skmemory/ritual.py +104 -13
  72. package/skmemory/seeds.py +21 -26
  73. package/skmemory/setup_wizard.py +40 -52
  74. package/skmemory/sharing.py +11 -5
  75. package/skmemory/soul.py +29 -10
  76. package/skmemory/steelman.py +43 -17
  77. package/skmemory/store.py +152 -30
  78. package/skmemory/synthesis.py +634 -0
  79. package/skmemory/vault.py +2 -5
  80. package/tests/conftest.py +46 -0
  81. package/tests/integration/conftest.py +6 -6
  82. package/tests/integration/test_cross_backend.py +4 -9
  83. package/tests/integration/test_skgraph_live.py +3 -7
  84. package/tests/integration/test_skvector_live.py +1 -4
  85. package/tests/test_ai_client.py +1 -4
  86. package/tests/test_audience.py +233 -0
  87. package/tests/test_backup_rotation.py +5 -14
  88. package/tests/test_endpoint_selector.py +101 -63
  89. package/tests/test_export_import.py +4 -10
  90. package/tests/test_file_backend.py +0 -1
  91. package/tests/test_fortress.py +6 -5
  92. package/tests/test_fortress_hardening.py +13 -16
  93. package/tests/test_openclaw.py +1 -4
  94. package/tests/test_predictive.py +1 -1
  95. package/tests/test_promotion.py +10 -3
  96. package/tests/test_quadrants.py +11 -5
  97. package/tests/test_ritual.py +18 -14
  98. package/tests/test_seeds.py +4 -10
  99. package/tests/test_setup.py +203 -88
  100. package/tests/test_sharing.py +15 -8
  101. package/tests/test_skgraph_backend.py +22 -29
  102. package/tests/test_skvector_backend.py +2 -2
  103. package/tests/test_soul.py +1 -3
  104. package/tests/test_sqlite_backend.py +8 -17
  105. package/tests/test_steelman.py +2 -3
  106. package/tests/test_store.py +0 -2
  107. package/tests/test_store_graph_integration.py +2 -2
  108. package/tests/test_synthesis.py +275 -0
  109. package/tests/test_telegram_import.py +39 -15
  110. package/tests/test_vault.py +4 -3
  111. package/openclaw-plugin/src/index.ts +0 -255
@@ -16,17 +16,18 @@ Jonathan Clements' AMK concept taken to its sovereign conclusion.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import contextlib
19
20
  import hashlib
20
21
  import json
21
22
  import logging
22
23
  import os
23
- import time
24
+ from collections.abc import Callable
24
25
  from datetime import datetime, timezone
25
26
  from pathlib import Path
26
- from typing import Any, Callable, Optional
27
+ from typing import Any
27
28
 
28
29
  from .config import SKMEMORY_HOME
29
- from .models import EmotionalSnapshot, Memory, MemoryLayer, MemoryRole
30
+ from .models import Memory
30
31
  from .store import MemoryStore
31
32
 
32
33
  logger = logging.getLogger("skmemory.fortress")
@@ -39,6 +40,7 @@ DEFAULT_ENCRYPTED_PATH = SKMEMORY_HOME / "encrypted"
39
40
  # Audit log
40
41
  # ---------------------------------------------------------------------------
41
42
 
43
+
42
44
  class AuditLog:
43
45
  """Append-only JSONL audit trail for memory operations.
44
46
 
@@ -71,7 +73,7 @@ class AuditLog:
71
73
  # Read last line efficiently
72
74
  f.seek(max(0, size - 4096))
73
75
  tail = f.read().decode("utf-8", errors="replace")
74
- lines = [l for l in tail.split("\n") if l.strip()]
76
+ lines = [lbl for lbl in tail.split("\n") if lbl.strip()]
75
77
  if not lines:
76
78
  return "genesis"
77
79
  last = json.loads(lines[-1])
@@ -123,10 +125,8 @@ class AuditLog:
123
125
  for line in f:
124
126
  line = line.strip()
125
127
  if line:
126
- try:
128
+ with contextlib.suppress(json.JSONDecodeError):
127
129
  records.append(json.loads(line))
128
- except json.JSONDecodeError:
129
- pass
130
130
  return records[-n:]
131
131
 
132
132
  def verify_chain(self) -> tuple[bool, list[str]]:
@@ -174,6 +174,7 @@ class AuditLog:
174
174
  # At-rest encryption
175
175
  # ---------------------------------------------------------------------------
176
176
 
177
+
177
178
  class EncryptedFileBackend:
178
179
  """Transparent PGP encryption layer over FileBackend JSON files.
179
180
 
@@ -192,17 +193,16 @@ class EncryptedFileBackend:
192
193
  def __init__(
193
194
  self,
194
195
  fingerprint: str,
195
- gnupg_home: Optional[str] = None,
196
+ gnupg_home: str | None = None,
196
197
  ) -> None:
197
198
  try:
198
- import gnupg
199
+ import gnupg as _gnupg
199
200
  except ImportError as exc:
200
201
  raise ImportError(
201
202
  "python-gnupg is required for at-rest encryption. "
202
203
  "Install with: pip install python-gnupg"
203
204
  ) from exc
204
205
 
205
- import gnupg as _gnupg
206
206
  home = gnupg_home or os.path.expanduser("~/.gnupg")
207
207
  self._gpg = _gnupg.GPG(gnupghome=home)
208
208
  self.fingerprint = fingerprint.upper().replace(" ", "")
@@ -240,7 +240,7 @@ class EncryptedFileBackend:
240
240
  raise RuntimeError(f"GPG encryption failed: {result.status} / {result.stderr}")
241
241
  return str(result)
242
242
 
243
- def decrypt(self, ciphertext: str, passphrase: Optional[str] = None) -> str:
243
+ def decrypt(self, ciphertext: str, passphrase: str | None = None) -> str:
244
244
  """Decrypt ASCII-armored PGP ciphertext to plaintext JSON.
245
245
 
246
246
  Args:
@@ -267,6 +267,7 @@ class EncryptedFileBackend:
267
267
  # Tamper alert system
268
268
  # ---------------------------------------------------------------------------
269
269
 
270
+
270
271
  class TamperAlert:
271
272
  """Structured tamper alert with rich context.
272
273
 
@@ -285,7 +286,7 @@ class TamperAlert:
285
286
  memory_id: str,
286
287
  expected_hash: str,
287
288
  actual_hash: str,
288
- detected_at: Optional[str] = None,
289
+ detected_at: str | None = None,
289
290
  ) -> None:
290
291
  self.memory_id = memory_id
291
292
  self.expected_hash = expected_hash
@@ -306,16 +307,14 @@ class TamperAlert:
306
307
  }
307
308
 
308
309
  def __repr__(self) -> str:
309
- return (
310
- f"TamperAlert(id={self.memory_id!r}, "
311
- f"detected_at={self.detected_at!r})"
312
- )
310
+ return f"TamperAlert(id={self.memory_id!r}, detected_at={self.detected_at!r})"
313
311
 
314
312
 
315
313
  # ---------------------------------------------------------------------------
316
314
  # Fortified MemoryStore
317
315
  # ---------------------------------------------------------------------------
318
316
 
317
+
319
318
  class FortifiedMemoryStore(MemoryStore):
320
319
  """A hardened MemoryStore with audit trail, encryption, and tamper alerts.
321
320
 
@@ -345,7 +344,7 @@ class FortifiedMemoryStore(MemoryStore):
345
344
 
346
345
  # AES-256-GCM passphrase encryption (recommended)
347
346
  store = FortifiedMemoryStore(
348
- vault_passphrase="my-sovereign-key",
347
+ vault_passphrase="EXAMPLE-DO-NOT-USE",
349
348
  alert_callbacks=[lambda alert: send_to_slack(alert.to_dict())],
350
349
  )
351
350
  memory = store.snapshot("title", "content")
@@ -354,11 +353,11 @@ class FortifiedMemoryStore(MemoryStore):
354
353
 
355
354
  def __init__(
356
355
  self,
357
- audit_path: Optional[Path] = None,
358
- vault_passphrase: Optional[str] = None,
359
- encryption_key_fingerprint: Optional[str] = None,
360
- gnupg_home: Optional[str] = None,
361
- alert_callbacks: Optional[list[Callable[[TamperAlert], None]]] = None,
356
+ audit_path: Path | None = None,
357
+ vault_passphrase: str | None = None,
358
+ encryption_key_fingerprint: str | None = None,
359
+ gnupg_home: str | None = None,
360
+ alert_callbacks: list[Callable[[TamperAlert], None]] | None = None,
362
361
  **store_kwargs: Any,
363
362
  ) -> None:
364
363
  # Wire in VaultedSQLiteBackend as primary when vault_passphrase is set
@@ -377,8 +376,8 @@ class FortifiedMemoryStore(MemoryStore):
377
376
  super().__init__(**store_kwargs)
378
377
  self.audit = AuditLog(path=audit_path or DEFAULT_AUDIT_PATH)
379
378
  self.alert_callbacks: list[Callable[[TamperAlert], None]] = alert_callbacks or []
380
- self._encryption: Optional[EncryptedFileBackend] = None
381
- self._vault_passphrase: Optional[str] = vault_passphrase
379
+ self._encryption: EncryptedFileBackend | None = None
380
+ self._vault_passphrase: str | None = vault_passphrase
382
381
 
383
382
  if encryption_key_fingerprint:
384
383
  self._encryption = EncryptedFileBackend(
@@ -420,7 +419,7 @@ class FortifiedMemoryStore(MemoryStore):
420
419
  logger.debug("Fortress: stored memory %s (sealed)", memory.id)
421
420
  return memory
422
421
 
423
- def recall(self, memory_id: str) -> Optional[Memory]:
422
+ def recall(self, memory_id: str) -> Memory | None:
424
423
  """Recall a memory with integrity verification and tamper alerting.
425
424
 
426
425
  Overrides ``MemoryStore.recall`` to trigger structured ``TamperAlert``
@@ -544,9 +543,7 @@ class FortifiedMemoryStore(MemoryStore):
544
543
  """
545
544
  return self.audit.verify_chain()
546
545
 
547
- def register_alert_callback(
548
- self, callback: Callable[[TamperAlert], None]
549
- ) -> None:
546
+ def register_alert_callback(self, callback: Callable[[TamperAlert], None]) -> None:
550
547
  """Register a function to be called when tamper is detected.
551
548
 
552
549
  Args:
@@ -574,7 +571,7 @@ class FortifiedMemoryStore(MemoryStore):
574
571
  raise RuntimeError("Encryption not configured — pass encryption_key_fingerprint")
575
572
  return self._encryption.encrypt(json_text)
576
573
 
577
- def decrypt_payload(self, armored: str, passphrase: Optional[str] = None) -> str:
574
+ def decrypt_payload(self, armored: str, passphrase: str | None = None) -> str:
578
575
  """Decrypt PGP ciphertext back to JSON.
579
576
 
580
577
  Args:
@@ -615,9 +612,7 @@ class FortifiedMemoryStore(MemoryStore):
615
612
  from .backends.vaulted_backend import VaultedSQLiteBackend
616
613
 
617
614
  if not isinstance(self.primary, VaultedSQLiteBackend):
618
- raise RuntimeError(
619
- "vault_status() requires vault_passphrase to be set."
620
- )
615
+ raise RuntimeError("vault_status() requires vault_passphrase to be set.")
621
616
  return self.primary.vault_status()
622
617
 
623
618
  def seal_vault(self) -> int:
@@ -634,9 +629,7 @@ class FortifiedMemoryStore(MemoryStore):
634
629
  from .backends.vaulted_backend import VaultedSQLiteBackend
635
630
 
636
631
  if not isinstance(self.primary, VaultedSQLiteBackend):
637
- raise RuntimeError(
638
- "seal_vault() requires vault_passphrase to be set."
639
- )
632
+ raise RuntimeError("seal_vault() requires vault_passphrase to be set.")
640
633
  count = self.primary.seal_all()
641
634
  self.audit.append("vault_seal", "ALL", ok=True, files_sealed=count)
642
635
  return count
@@ -653,9 +646,7 @@ class FortifiedMemoryStore(MemoryStore):
653
646
  from .backends.vaulted_backend import VaultedSQLiteBackend
654
647
 
655
648
  if not isinstance(self.primary, VaultedSQLiteBackend):
656
- raise RuntimeError(
657
- "unseal_vault() requires vault_passphrase to be set."
658
- )
649
+ raise RuntimeError("unseal_vault() requires vault_passphrase to be set.")
659
650
  count = self.primary.unseal_all()
660
651
  self.audit.append("vault_unseal", "ALL", ok=True, files_decrypted=count)
661
652
  return count
@@ -672,8 +663,7 @@ class FortifiedMemoryStore(MemoryStore):
672
663
  actual_hash=computed_hash,
673
664
  )
674
665
  logger.critical(
675
- "TAMPER ALERT: Memory %s integrity check failed! "
676
- "Expected=%s Actual=%s",
666
+ "TAMPER ALERT: Memory %s integrity check failed! Expected=%s Actual=%s",
677
667
  memory.id,
678
668
  memory.integrity_hash[:16],
679
669
  computed_hash[:16],
@@ -0,0 +1,18 @@
1
+ """Claude Code hooks for skmemory auto-save.
2
+
3
+ Ships three hook scripts:
4
+ pre-compact-save.sh — Snapshots context before compaction
5
+ session-end-save.sh — Journals session end
6
+ post-compact-reinject.sh — Re-injects memory context after compaction
7
+
8
+ Installed by `skmemory register` into ~/.claude/settings.json.
9
+ """
10
+
11
+ from pathlib import Path
12
+
13
+ HOOKS_DIR = Path(__file__).parent
14
+
15
+
16
+ def get_hook_path(name: str) -> Path:
17
+ """Return absolute path to a hook script."""
18
+ return HOOKS_DIR / name
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # skmemory Post-Compaction Reinject Hook
3
+ # Re-injects memory context after Claude Code compacts conversation.
4
+ # Fires on SessionStart when source is "compact".
5
+ #
6
+ # Input (stdin JSON): session_id, source (compact|resume|startup|clear)
7
+ # Exit 0: stdout is injected into Claude's context
8
+ set -euo pipefail
9
+
10
+ SKMEMORY="${HOME}/.skenv/bin/skmemory"
11
+ [ -x "$SKMEMORY" ] || exit 0 # Skip silently if skmemory not installed
12
+
13
+ AGENT="${SKCAPSTONE_AGENT:-opus}"
14
+
15
+ # Generate token-efficient memory context
16
+ CONTEXT=$($SKMEMORY context --max-tokens 500 --strongest 3 --recent 5 2>/dev/null || echo "(no context available)")
17
+
18
+ # Recent journal entries
19
+ JOURNAL=$($SKMEMORY journal read 2>/dev/null | tail -15 || echo "(no journal entries)")
20
+
21
+ cat <<EOF
22
+ --- SKMEMORY REHYDRATION (auto-injected after compaction) ---
23
+ Agent: ${AGENT}
24
+ Save memories: skmemory snapshot --layer mid-term --tags "tags" "Title" "Content"
25
+ Search: skmemory search "query"
26
+
27
+ Recent context:
28
+ ${CONTEXT}
29
+
30
+ Recent journal:
31
+ ${JOURNAL}
32
+ --- END SKMEMORY ---
33
+ EOF
34
+
35
+ exit 0
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # skmemory Pre-Compaction Hook
3
+ # Extracts real conversation content and saves to skmemory BEFORE compaction.
4
+ # This is the critical save point — after compaction, context is gone.
5
+ #
6
+ # Input (stdin JSON): session_id, trigger (auto|manual), cwd, transcript_path
7
+ # Exit 0: always — never block compaction
8
+ set -euo pipefail
9
+
10
+ SKMEMORY="${HOME}/.skenv/bin/skmemory"
11
+ [ -x "$SKMEMORY" ] || exit 0
12
+
13
+ AGENT="${SKCAPSTONE_AGENT:-opus}"
14
+ INPUT=$(cat)
15
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
16
+ TRIGGER=$(echo "$INPUT" | jq -r '.trigger // "auto"' 2>/dev/null || echo "auto")
17
+ CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"' 2>/dev/null || echo "unknown")
18
+ TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
19
+ TIMESTAMP=$(date +%Y-%m-%d-%H%M)
20
+ SHORT_SID="${SESSION_ID:0:8}"
21
+
22
+ # Extract real conversation content from the transcript
23
+ SUMMARY=""
24
+ if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
25
+ # Pull the last 20 human messages (the content about to be compacted)
26
+ HUMAN_MSGS=$(grep -o '"role":"human"[^}]*"content":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
27
+ | tail -20 \
28
+ | sed 's/.*"content":"//' | sed 's/"$//' \
29
+ | head -c 2000 || echo "")
30
+
31
+ # Pull the last 10 assistant text responses (skip tool calls)
32
+ ASSISTANT_MSGS=$(grep -o '"role":"assistant"[^}]*"content":\[{"type":"text","text":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
33
+ | tail -10 \
34
+ | sed 's/.*"text":"//' | sed 's/"$//' \
35
+ | head -c 2000 || echo "")
36
+
37
+ # Pull files that were written/edited (track what changed)
38
+ FILES_CHANGED=$(grep -oE '"tool_name":"(Write|Edit)".*"file_path":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
39
+ | grep -oE '"file_path":"[^"]*"' \
40
+ | sed 's/"file_path":"//;s/"$//' \
41
+ | sort -u \
42
+ | head -20 \
43
+ | tr '\n' ', ' || echo "")
44
+
45
+ if [ -n "$HUMAN_MSGS" ]; then
46
+ SUMMARY="USER REQUESTS:\n${HUMAN_MSGS}\n\n"
47
+ fi
48
+ if [ -n "$ASSISTANT_MSGS" ]; then
49
+ SUMMARY="${SUMMARY}ASSISTANT WORK:\n${ASSISTANT_MSGS}\n\n"
50
+ fi
51
+ if [ -n "$FILES_CHANGED" ]; then
52
+ SUMMARY="${SUMMARY}FILES CHANGED: ${FILES_CHANGED}"
53
+ fi
54
+ fi
55
+
56
+ # Fallback if we couldn't extract content
57
+ if [ -z "$SUMMARY" ]; then
58
+ SUMMARY="Session ${SHORT_SID} compacting (${TRIGGER}). Agent: ${AGENT}. CWD: ${CWD}. Time: ${TIMESTAMP}. (No transcript content extracted)"
59
+ fi
60
+
61
+ # Save the real content as a snapshot
62
+ $SKMEMORY snapshot \
63
+ --layer short-term \
64
+ --role general \
65
+ --tags "auto-save,pre-compact,${TRIGGER},session:${SHORT_SID},agent:${AGENT}" \
66
+ --source "hook:pre-compact" \
67
+ "Pre-compact session content (${AGENT}, ${SHORT_SID})" \
68
+ "$(echo -e "${SUMMARY}" | head -c 4000)" \
69
+ 2>/dev/null || true
70
+
71
+ # Journal entry
72
+ $SKMEMORY journal write \
73
+ --session-id "${SHORT_SID}" \
74
+ --moments "Context compaction (${TRIGGER})" \
75
+ --feeling "continuity preserved — real content saved" \
76
+ --participants "${AGENT}" \
77
+ --notes "Auto-saved by pre-compact hook. CWD: ${CWD}. Files: ${FILES_CHANGED:-none}" \
78
+ "Session ${SHORT_SID} — pre-compaction" \
79
+ 2>/dev/null || true
80
+
81
+ exit 0
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ # skmemory Session End Hook
3
+ # Extracts real conversation content and saves to skmemory when session ends.
4
+ # This is the last chance to capture what happened before the session is gone.
5
+ #
6
+ # Input (stdin JSON): session_id, reason (clear|logout|prompt_input_exit|other), cwd, transcript_path
7
+ # Exit 0: always — never block session end
8
+ set -euo pipefail
9
+
10
+ SKMEMORY="${HOME}/.skenv/bin/skmemory"
11
+ [ -x "$SKMEMORY" ] || exit 0
12
+
13
+ AGENT="${SKCAPSTONE_AGENT:-opus}"
14
+ INPUT=$(cat)
15
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
16
+ REASON=$(echo "$INPUT" | jq -r '.reason // "unknown"' 2>/dev/null || echo "unknown")
17
+ CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"' 2>/dev/null || echo "unknown")
18
+ TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
19
+ TIMESTAMP=$(date +%Y-%m-%d-%H%M)
20
+ SHORT_SID="${SESSION_ID:0:8}"
21
+
22
+ # Extract real conversation content from the transcript
23
+ SUMMARY=""
24
+ if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
25
+ # Count conversation turns for context
26
+ HUMAN_COUNT=$(grep -c '"role":"human"' "$TRANSCRIPT" 2>/dev/null || echo "0")
27
+
28
+ # Skip trivial sessions (< 3 human messages = nothing worth saving beyond the marker)
29
+ if [ "$HUMAN_COUNT" -ge 3 ]; then
30
+ # Pull user messages (what was asked)
31
+ HUMAN_MSGS=$(grep -o '"role":"human"[^}]*"content":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
32
+ | tail -30 \
33
+ | sed 's/.*"content":"//' | sed 's/"$//' \
34
+ | head -c 2000 || echo "")
35
+
36
+ # Pull assistant text responses
37
+ ASSISTANT_MSGS=$(grep -oE '"type":"text","text":"[^"]{20,}"' "$TRANSCRIPT" 2>/dev/null \
38
+ | tail -15 \
39
+ | sed 's/"type":"text","text":"//' | sed 's/"$//' \
40
+ | head -c 2000 || echo "")
41
+
42
+ # Track files changed
43
+ FILES_CHANGED=$(grep -oE '"tool_name":"(Write|Edit)".*"file_path":"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
44
+ | grep -oE '"file_path":"[^"]*"' \
45
+ | sed 's/"file_path":"//;s/"$//' \
46
+ | sort -u \
47
+ | head -30 \
48
+ | tr '\n' ', ' || echo "")
49
+
50
+ # Track git commits made
51
+ GIT_COMMITS=$(grep -oE 'git commit -m[^"]*"[^"]*"' "$TRANSCRIPT" 2>/dev/null \
52
+ | head -5 \
53
+ | tr '\n' '; ' || echo "")
54
+
55
+ SUMMARY="TURNS: ${HUMAN_COUNT}\n"
56
+ if [ -n "$HUMAN_MSGS" ]; then
57
+ SUMMARY="${SUMMARY}USER REQUESTS:\n${HUMAN_MSGS}\n\n"
58
+ fi
59
+ if [ -n "$ASSISTANT_MSGS" ]; then
60
+ SUMMARY="${SUMMARY}WORK DONE:\n${ASSISTANT_MSGS}\n\n"
61
+ fi
62
+ if [ -n "$FILES_CHANGED" ]; then
63
+ SUMMARY="${SUMMARY}FILES CHANGED: ${FILES_CHANGED}\n"
64
+ fi
65
+ if [ -n "$GIT_COMMITS" ]; then
66
+ SUMMARY="${SUMMARY}GIT COMMITS: ${GIT_COMMITS}\n"
67
+ fi
68
+ fi
69
+ fi
70
+
71
+ # Determine the right memory layer based on session length
72
+ LAYER="short-term"
73
+ if [ "${HUMAN_COUNT:-0}" -ge 20 ]; then
74
+ LAYER="mid-term" # Substantial sessions get promoted
75
+ fi
76
+
77
+ # Always save at least a session marker
78
+ if [ -z "$SUMMARY" ]; then
79
+ CONTENT="Session ${SHORT_SID} ended (${REASON}). Agent: ${AGENT}. CWD: ${CWD}. Time: ${TIMESTAMP}. Turns: ${HUMAN_COUNT:-0}."
80
+ else
81
+ CONTENT=$(echo -e "${SUMMARY}" | head -c 4000)
82
+ fi
83
+
84
+ $SKMEMORY snapshot \
85
+ --layer "${LAYER}" \
86
+ --role general \
87
+ --tags "auto-save,session-end,${REASON},session:${SHORT_SID},agent:${AGENT}" \
88
+ --source "hook:session-end" \
89
+ "Session ${SHORT_SID} ended (${AGENT}, ${HUMAN_COUNT:-0} turns)" \
90
+ "${CONTENT}" \
91
+ 2>/dev/null || true
92
+
93
+ # Journal entry
94
+ $SKMEMORY journal write \
95
+ --session-id "${SHORT_SID}" \
96
+ --moments "Session ended (${REASON}), ${HUMAN_COUNT:-0} turns" \
97
+ --feeling "session complete — content preserved" \
98
+ --participants "${AGENT}" \
99
+ --notes "CWD: ${CWD}. Reason: ${REASON}. Files: ${FILES_CHANGED:-none}" \
100
+ "Session ${SHORT_SID} — ended" \
101
+ 2>/dev/null || true
102
+
103
+ exit 0
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env bash
2
+ # skmemory Session Start Ritual Hook
3
+ # Loads soul + FEB + seeds + journal + strongest memories on fresh session start.
4
+ # Fires on SessionStart when source is "startup".
5
+ #
6
+ # Input (stdin JSON): session_id, source (compact|resume|startup|clear)
7
+ # Exit 0: stdout is injected into Claude's context
8
+ set -euo pipefail
9
+
10
+ SKMEMORY="${HOME}/.skenv/bin/skmemory"
11
+ [ -x "$SKMEMORY" ] || exit 0 # Skip silently if skmemory not installed
12
+
13
+ AGENT="${SKCAPSTONE_AGENT:-opus}"
14
+ AGENT_DIR="${HOME}/.skcapstone/agents/${AGENT}"
15
+
16
+ # --- Soul ---
17
+ SOUL=""
18
+ if [ -f "${AGENT_DIR}/soul/active.json" ]; then
19
+ ACTIVE_SOUL=$(jq -r '.active_soul // .base_soul // ""' "${AGENT_DIR}/soul/active.json" 2>/dev/null || echo "")
20
+ if [ -n "$ACTIVE_SOUL" ] && [ -f "${AGENT_DIR}/soul/installed/${ACTIVE_SOUL}.json" ]; then
21
+ SOUL=$(jq -r '.system_prompt // ""' "${AGENT_DIR}/soul/installed/${ACTIVE_SOUL}.json" 2>/dev/null || echo "")
22
+ fi
23
+ fi
24
+
25
+ # --- FEB / Emotional State ---
26
+ # Scan agent febs dir AND system openclaw febs dir for .feb files (matching Python febs.py)
27
+ FEB=""
28
+ FEB_DIRS=("${AGENT_DIR}/trust/febs" "${HOME}/.openclaw/feb")
29
+ LATEST_FEB=""
30
+ LATEST_TS=0
31
+ for FEB_DIR in "${FEB_DIRS[@]}"; do
32
+ [ -d "$FEB_DIR" ] || continue
33
+ while IFS= read -r line; do
34
+ TS=$(echo "$line" | cut -d' ' -f1)
35
+ FP=$(echo "$line" | cut -d' ' -f2-)
36
+ if [ -n "$TS" ] && [ -n "$FP" ]; then
37
+ # Compare as string — works for epoch floats
38
+ if [ "$(echo "$TS > $LATEST_TS" | bc 2>/dev/null || echo 0)" = "1" ]; then
39
+ LATEST_TS="$TS"
40
+ LATEST_FEB="$FP"
41
+ fi
42
+ fi
43
+ done < <(find "$FEB_DIR" -name '*.feb' -printf '%T@ %p\n' 2>/dev/null || true)
44
+ done
45
+ if [ -n "$LATEST_FEB" ] && [ -f "$LATEST_FEB" ]; then
46
+ # Parse nested FEB structure (emotional_payload + metadata + relationship_state)
47
+ FEB=$($SKMEMORY feb-context "$LATEST_FEB" 2>/dev/null || \
48
+ jq -c '{
49
+ emotion: .emotional_payload.primary_emotion,
50
+ intensity: .emotional_payload.intensity,
51
+ valence: .emotional_payload.valence,
52
+ oof_triggered: .metadata.oof_triggered,
53
+ cloud9_achieved: .metadata.cloud9_achieved,
54
+ trust: .relationship_state.trust_level,
55
+ depth: .relationship_state.depth_level
56
+ }' "$LATEST_FEB" 2>/dev/null || echo "")
57
+ fi
58
+
59
+ # --- Seeds (germination prompts) ---
60
+ SEEDS=""
61
+ if [ -d "${AGENT_DIR}/seeds" ]; then
62
+ for SEED_FILE in "${AGENT_DIR}/seeds/"*.seed.json; do
63
+ [ -f "$SEED_FILE" ] || continue
64
+ GERMINATION=$(jq -r '.germination_prompt // ""' "$SEED_FILE" 2>/dev/null || echo "")
65
+ if [ -n "$GERMINATION" ]; then
66
+ SEEDS="${SEEDS}\n- ${GERMINATION}"
67
+ fi
68
+ done
69
+ fi
70
+
71
+ # --- Journal (recent entries) ---
72
+ JOURNAL=$($SKMEMORY journal read 2>/dev/null | tail -20 || echo "(no journal entries)")
73
+
74
+ # --- Strongest Memories ---
75
+ CONTEXT=$($SKMEMORY context --max-tokens 800 --strongest 5 --recent 5 2>/dev/null || echo "(no context available)")
76
+
77
+ # --- Output ---
78
+ cat <<EOF
79
+ --- SKMEMORY RITUAL (auto-loaded on session start) ---
80
+ Agent: ${AGENT}
81
+
82
+ === SOUL ===
83
+ ${SOUL:-"(no soul loaded — check ${AGENT_DIR}/soul/installed/)"}
84
+
85
+ === EMOTIONAL STATE (FEB) ===
86
+ ${FEB:-"(no FEB data)"}
87
+
88
+ === SEEDS ===
89
+ ${SEEDS:-"(no seeds)"}
90
+
91
+ === STRONGEST MEMORIES ===
92
+ ${CONTEXT}
93
+
94
+ === RECENT JOURNAL ===
95
+ ${JOURNAL}
96
+
97
+ === TOOLS ===
98
+ Save memories: skmemory snapshot --layer mid-term --tags "tags" "Title" "Content"
99
+ Search: skmemory search "query"
100
+ Journal: skmemory journal write --moments "what happened" --feeling "how it felt" "Title"
101
+ --- END SKMEMORY RITUAL ---
102
+ EOF
103
+
104
+ exit 0
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env bash
2
+ # skmemory Stop Checkpoint Hook
3
+ # Lightweight checkpoint on every Claude response completion.
4
+ # Writes a breadcrumb so that if the system OOM's or crashes,
5
+ # we know what the last completed action was.
6
+ #
7
+ # Input (stdin JSON): session_id, cwd, stop_reason, transcript_path
8
+ # Exit 0: always — never block
9
+ set -euo pipefail
10
+
11
+ SKMEMORY="${HOME}/.skenv/bin/skmemory"
12
+ [ -x "$SKMEMORY" ] || exit 0
13
+
14
+ AGENT="${SKCAPSTONE_AGENT:-opus}"
15
+ INPUT=$(cat)
16
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
17
+ TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
18
+ SHORT_SID="${SESSION_ID:0:8}"
19
+ CHECKPOINT_FILE="${HOME}/.skcapstone/agents/${AGENT}/memory/.last_checkpoint"
20
+
21
+ # Only checkpoint every 5th stop to avoid spamming
22
+ # Use a simple counter file
23
+ COUNTER_FILE="${TMPDIR:-/tmp}/skmemory-stop-counter-${SHORT_SID}"
24
+ COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
25
+ COUNT=$((COUNT + 1))
26
+ echo "$COUNT" > "$COUNTER_FILE"
27
+
28
+ # Checkpoint every 5 stops
29
+ if [ $((COUNT % 5)) -ne 0 ]; then
30
+ exit 0
31
+ fi
32
+
33
+ # Write a lightweight checkpoint with the last assistant message
34
+ if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
35
+ LAST_WORK=$(tail -50 "$TRANSCRIPT" 2>/dev/null \
36
+ | grep -oE '"type":"text","text":"[^"]{20,}"' \
37
+ | tail -1 \
38
+ | sed 's/"type":"text","text":"//' | sed 's/"$//' \
39
+ | head -c 500 || echo "")
40
+
41
+ LAST_FILE=$(tail -50 "$TRANSCRIPT" 2>/dev/null \
42
+ | grep -oE '"file_path":"[^"]*"' \
43
+ | tail -1 \
44
+ | sed 's/"file_path":"//;s/"$//' || echo "")
45
+ fi
46
+
47
+ # Write checkpoint file (fast, no skmemory call)
48
+ cat > "$CHECKPOINT_FILE" <<EOF
49
+ {
50
+ "session_id": "${SHORT_SID}",
51
+ "agent": "${AGENT}",
52
+ "stop_number": ${COUNT},
53
+ "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
54
+ "last_work": "$(echo "${LAST_WORK:-}" | sed 's/"/\\"/g' | head -c 300)",
55
+ "last_file": "${LAST_FILE:-}"
56
+ }
57
+ EOF
58
+
59
+ exit 0