@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.
- package/.github/workflows/ci.yml +4 -4
- package/.github/workflows/publish.yml +4 -5
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +27 -1
- package/README.md +6 -0
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/package.json +1 -1
- package/pyproject.toml +5 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +6 -4
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +7 -5
- package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
- package/seeds/lumina-kingdom-founding.seed.json +9 -7
- package/seeds/lumina-pma-signed.seed.json +8 -6
- package/seeds/lumina-singular-achievement.seed.json +8 -6
- package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
- package/seeds/plant-lumina-seeds.py +2 -2
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skmemory/__init__.py +16 -13
- package/skmemory/agents.py +10 -10
- package/skmemory/ai_client.py +10 -21
- package/skmemory/anchor.py +5 -9
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +1 -1
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +18 -13
- package/skmemory/backends/skgraph_backend.py +7 -19
- package/skmemory/backends/skvector_backend.py +7 -18
- package/skmemory/backends/sqlite_backend.py +115 -32
- package/skmemory/backends/vaulted_backend.py +7 -9
- package/skmemory/cli.py +146 -78
- package/skmemory/config.py +11 -13
- package/skmemory/context_loader.py +21 -23
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +36 -31
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +30 -40
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/telegram.py +42 -13
- package/skmemory/importers/telegram_api.py +152 -60
- package/skmemory/journal.py +3 -7
- package/skmemory/lovenote.py +4 -11
- package/skmemory/mcp_server.py +182 -29
- package/skmemory/models.py +10 -8
- package/skmemory/openclaw.py +14 -22
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +13 -9
- package/skmemory/promotion.py +48 -24
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +144 -18
- package/skmemory/register_mcp.py +1 -2
- package/skmemory/ritual.py +104 -13
- package/skmemory/seeds.py +21 -26
- package/skmemory/setup_wizard.py +40 -52
- package/skmemory/sharing.py +11 -5
- package/skmemory/soul.py +29 -10
- package/skmemory/steelman.py +43 -17
- package/skmemory/store.py +152 -30
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +2 -5
- package/tests/conftest.py +46 -0
- package/tests/integration/conftest.py +6 -6
- package/tests/integration/test_cross_backend.py +4 -9
- package/tests/integration/test_skgraph_live.py +3 -7
- package/tests/integration/test_skvector_live.py +1 -4
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +5 -14
- package/tests/test_endpoint_selector.py +101 -63
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +6 -5
- package/tests/test_fortress_hardening.py +13 -16
- package/tests/test_openclaw.py +1 -4
- package/tests/test_predictive.py +1 -1
- package/tests/test_promotion.py +10 -3
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +18 -14
- package/tests/test_seeds.py +4 -10
- package/tests/test_setup.py +203 -88
- package/tests/test_sharing.py +15 -8
- package/tests/test_skgraph_backend.py +22 -29
- package/tests/test_skvector_backend.py +2 -2
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +2 -3
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +2 -2
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +4 -3
- package/openclaw-plugin/src/index.ts +0 -255
package/skmemory/fortress.py
CHANGED
|
@@ -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
|
|
24
|
+
from collections.abc import Callable
|
|
24
25
|
from datetime import datetime, timezone
|
|
25
26
|
from pathlib import Path
|
|
26
|
-
from typing import Any
|
|
27
|
+
from typing import Any
|
|
27
28
|
|
|
28
29
|
from .config import SKMEMORY_HOME
|
|
29
|
-
from .models import
|
|
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 = [
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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="
|
|
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:
|
|
358
|
-
vault_passphrase:
|
|
359
|
-
encryption_key_fingerprint:
|
|
360
|
-
gnupg_home:
|
|
361
|
-
alert_callbacks:
|
|
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:
|
|
381
|
-
self._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) ->
|
|
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:
|
|
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
|