@smilintux/skmemory 0.5.0 → 0.7.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 (87) hide show
  1. package/.github/workflows/ci.yml +39 -3
  2. package/.github/workflows/publish.yml +13 -6
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +101 -19
  5. package/CHANGELOG.md +153 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +419 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/index.js +6 -5
  12. package/openclaw-plugin/openclaw.plugin.json +10 -0
  13. package/openclaw-plugin/src/index.ts +255 -0
  14. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  15. package/package.json +1 -1
  16. package/pyproject.toml +29 -9
  17. package/requirements.txt +10 -2
  18. package/seeds/cloud9-opus.seed.json +7 -7
  19. package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
  20. package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
  21. package/seeds/lumina-kingdom-founding.seed.json +47 -0
  22. package/seeds/lumina-pma-signed.seed.json +46 -0
  23. package/seeds/lumina-singular-achievement.seed.json +46 -0
  24. package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
  25. package/seeds/plant-kingdom-journal.py +203 -0
  26. package/seeds/plant-lumina-seeds.py +280 -0
  27. package/skill.yaml +46 -0
  28. package/skmemory/HA.md +296 -0
  29. package/skmemory/__init__.py +12 -1
  30. package/skmemory/agents.py +233 -0
  31. package/skmemory/ai_client.py +40 -0
  32. package/skmemory/anchor.py +4 -2
  33. package/skmemory/backends/__init__.py +11 -4
  34. package/skmemory/backends/file_backend.py +2 -1
  35. package/skmemory/backends/skgraph_backend.py +608 -0
  36. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
  37. package/skmemory/backends/sqlite_backend.py +122 -51
  38. package/skmemory/backends/vaulted_backend.py +286 -0
  39. package/skmemory/cli.py +1238 -29
  40. package/skmemory/config.py +173 -0
  41. package/skmemory/context_loader.py +335 -0
  42. package/skmemory/endpoint_selector.py +386 -0
  43. package/skmemory/fortress.py +685 -0
  44. package/skmemory/graph_queries.py +238 -0
  45. package/skmemory/importers/__init__.py +9 -1
  46. package/skmemory/importers/telegram.py +351 -43
  47. package/skmemory/importers/telegram_api.py +488 -0
  48. package/skmemory/journal.py +4 -2
  49. package/skmemory/lovenote.py +4 -2
  50. package/skmemory/mcp_server.py +706 -0
  51. package/skmemory/models.py +41 -0
  52. package/skmemory/openclaw.py +8 -8
  53. package/skmemory/predictive.py +232 -0
  54. package/skmemory/promotion.py +524 -0
  55. package/skmemory/register.py +454 -0
  56. package/skmemory/register_mcp.py +197 -0
  57. package/skmemory/ritual.py +121 -47
  58. package/skmemory/seeds.py +257 -8
  59. package/skmemory/setup_wizard.py +920 -0
  60. package/skmemory/sharing.py +402 -0
  61. package/skmemory/soul.py +71 -20
  62. package/skmemory/steelman.py +250 -263
  63. package/skmemory/store.py +271 -60
  64. package/skmemory/vault.py +228 -0
  65. package/tests/integration/__init__.py +0 -0
  66. package/tests/integration/conftest.py +233 -0
  67. package/tests/integration/test_cross_backend.py +355 -0
  68. package/tests/integration/test_skgraph_live.py +424 -0
  69. package/tests/integration/test_skvector_live.py +369 -0
  70. package/tests/test_backup_rotation.py +327 -0
  71. package/tests/test_cli.py +6 -6
  72. package/tests/test_endpoint_selector.py +801 -0
  73. package/tests/test_fortress.py +255 -0
  74. package/tests/test_fortress_hardening.py +444 -0
  75. package/tests/test_openclaw.py +5 -2
  76. package/tests/test_predictive.py +237 -0
  77. package/tests/test_promotion.py +340 -0
  78. package/tests/test_ritual.py +4 -4
  79. package/tests/test_seeds.py +96 -0
  80. package/tests/test_setup.py +835 -0
  81. package/tests/test_sharing.py +250 -0
  82. package/tests/test_skgraph_backend.py +667 -0
  83. package/tests/test_skvector_backend.py +326 -0
  84. package/tests/test_steelman.py +5 -5
  85. package/tests/test_store_graph_integration.py +245 -0
  86. package/tests/test_vault.py +186 -0
  87. package/skmemory/backends/falkordb_backend.py +0 -310
@@ -0,0 +1,286 @@
1
+ """
2
+ VaultedSQLiteBackend — transparent AES-256-GCM at-rest encryption.
3
+
4
+ Every memory JSON file is stored as encrypted bytes on disk.
5
+ The SQLite index stores metadata in plaintext for fast queries;
6
+ only the full JSON files are encrypted.
7
+
8
+ The ``SKMV1`` vault header lets the backend auto-detect encrypted vs.
9
+ plain files, so you can safely migrate an existing unencrypted store
10
+ by calling ``seal_all()``.
11
+
12
+ File format on disk (per memory file):
13
+ SKMV1 || salt(16) || nonce(12) || AES-GCM(json_bytes) || tag(16)
14
+
15
+ Usage:
16
+ backend = VaultedSQLiteBackend(passphrase="sovereign-key")
17
+ store = MemoryStore(primary=backend, use_sqlite=False)
18
+ mem = store.snapshot("title", "content")
19
+ recalled = store.recall(mem.id) # transparent decrypt
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import sqlite3
26
+ from datetime import date, datetime, timezone
27
+ from pathlib import Path
28
+ from typing import Optional
29
+
30
+ from ..models import Memory, MemoryLayer
31
+ from ..vault import VAULT_HEADER, MemoryVault
32
+ from .sqlite_backend import SQLiteBackend
33
+ from .sqlite_backend import DEFAULT_BASE_PATH
34
+
35
+
36
+ class VaultedSQLiteBackend(SQLiteBackend):
37
+ """SQLiteBackend with transparent AES-256-GCM at-rest encryption.
38
+
39
+ Subclasses :class:`SQLiteBackend` and overrides all file I/O to
40
+ transparently encrypt on write and decrypt on read. The SQLite
41
+ index is unencrypted so queries remain fast.
42
+
43
+ Args:
44
+ passphrase: Secret used to derive the AES-256 key via PBKDF2-SHA256
45
+ with 600 000 iterations. Use a strong, unique passphrase.
46
+ base_path: Root directory for memory files and index.
47
+
48
+ Raises:
49
+ ImportError: If the ``cryptography`` package is not installed.
50
+
51
+ Example::
52
+
53
+ backend = VaultedSQLiteBackend(passphrase="my-secret")
54
+ store = MemoryStore(primary=backend, use_sqlite=False)
55
+ m = store.snapshot("Private thought", "End-to-end encrypted on disk")
56
+ r = store.recall(m.id) # decrypted on the fly
57
+ """
58
+
59
+ def __init__(self, passphrase: str, base_path: str = DEFAULT_BASE_PATH) -> None:
60
+ self._vault = MemoryVault(passphrase)
61
+ super().__init__(base_path=base_path)
62
+
63
+ # ------------------------------------------------------------------
64
+ # Core I/O overrides
65
+ # ------------------------------------------------------------------
66
+
67
+ def save(self, memory: Memory) -> str:
68
+ """Encrypt JSON bytes before writing to disk.
69
+
70
+ The SQLite index is updated with plaintext metadata so that
71
+ queries still work without decryption.
72
+
73
+ Args:
74
+ memory: Memory to persist.
75
+
76
+ Returns:
77
+ str: The memory ID.
78
+ """
79
+ path = self._file_path(memory)
80
+ path.parent.mkdir(parents=True, exist_ok=True)
81
+ json_bytes = json.dumps(memory.model_dump(), indent=2, default=str).encode("utf-8")
82
+ path.write_bytes(self._vault.encrypt(json_bytes))
83
+ self._index_memory(memory, path)
84
+ return memory.id
85
+
86
+ def load(self, memory_id: str) -> Optional[Memory]:
87
+ """Decrypt and parse a memory file.
88
+
89
+ Handles both encrypted (``SKMV1`` header) and plaintext files
90
+ so a partially-migrated store keeps working.
91
+
92
+ Args:
93
+ memory_id: The memory identifier.
94
+
95
+ Returns:
96
+ Optional[Memory]: The memory if found, None otherwise.
97
+ """
98
+ path = self._find_file(memory_id)
99
+ if path is None:
100
+ return None
101
+ return self._read_memory_file(path)
102
+
103
+ def _row_to_memory(self, row: sqlite3.Row) -> Optional[Memory]:
104
+ """Load a full Memory object, decrypting if needed.
105
+
106
+ Args:
107
+ row: SQLite row with ``file_path``.
108
+
109
+ Returns:
110
+ Optional[Memory]: Full memory or None if file missing / unreadable.
111
+ """
112
+ path = Path(row["file_path"])
113
+ if not path.exists():
114
+ return None
115
+ return self._read_memory_file(path)
116
+
117
+ # ------------------------------------------------------------------
118
+ # Bulk operations
119
+ # ------------------------------------------------------------------
120
+
121
+ def reindex(self) -> int:
122
+ """Rebuild the SQLite index by scanning and decrypting all JSON files.
123
+
124
+ Returns:
125
+ int: Number of memories re-indexed.
126
+ """
127
+ conn = self._get_conn()
128
+ conn.execute("DELETE FROM memories")
129
+ conn.commit()
130
+
131
+ count = 0
132
+ for layer in MemoryLayer:
133
+ layer_dir = self.base_path / layer.value
134
+ if not layer_dir.exists():
135
+ continue
136
+ for json_file in layer_dir.glob("*.json"):
137
+ try:
138
+ memory = self._read_memory_file(json_file)
139
+ if memory is not None:
140
+ self._index_memory(memory, json_file)
141
+ count += 1
142
+ except Exception:
143
+ continue
144
+ return count
145
+
146
+ def export_all(self, output_path: Optional[str] = None) -> str:
147
+ """Export all memories (decrypted) to a JSON backup file.
148
+
149
+ Args:
150
+ output_path: Destination path. Defaults to
151
+ ``~/.skcapstone/backups/skmemory-backup-YYYY-MM-DD.json``.
152
+
153
+ Returns:
154
+ str: Path to the written backup file.
155
+ """
156
+ from .. import __version__
157
+
158
+ if output_path is None:
159
+ backup_dir = self.base_path.parent / "backups"
160
+ backup_dir.mkdir(parents=True, exist_ok=True)
161
+ output_path = str(backup_dir / f"skmemory-backup-{date.today().isoformat()}.json")
162
+
163
+ memories: list[dict] = []
164
+ for layer in MemoryLayer:
165
+ layer_dir = self.base_path / layer.value
166
+ if not layer_dir.exists():
167
+ continue
168
+ for json_file in sorted(layer_dir.glob("*.json")):
169
+ try:
170
+ memory = self._read_memory_file(json_file)
171
+ if memory is not None:
172
+ memories.append(memory.model_dump())
173
+ except Exception:
174
+ continue
175
+
176
+ payload = {
177
+ "skmemory_version": __version__,
178
+ "exported_at": datetime.now(timezone.utc).isoformat(),
179
+ "memory_count": len(memories),
180
+ "base_path": str(self.base_path),
181
+ "memories": memories,
182
+ }
183
+ Path(output_path).write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")
184
+ return output_path
185
+
186
+ # ------------------------------------------------------------------
187
+ # Vault management
188
+ # ------------------------------------------------------------------
189
+
190
+ def seal_all(self) -> int:
191
+ """Encrypt all plaintext JSON files in the store.
192
+
193
+ Safe to call multiple times — already-encrypted files are skipped.
194
+
195
+ Returns:
196
+ int: Number of files newly encrypted.
197
+ """
198
+ count = 0
199
+ for layer in MemoryLayer:
200
+ layer_dir = self.base_path / layer.value
201
+ if not layer_dir.exists():
202
+ continue
203
+ for json_file in layer_dir.glob("*.json"):
204
+ try:
205
+ raw = json_file.read_bytes()
206
+ if raw[: len(VAULT_HEADER)] == VAULT_HEADER:
207
+ continue # already encrypted
208
+ json_file.write_bytes(self._vault.encrypt(raw))
209
+ count += 1
210
+ except Exception:
211
+ continue
212
+ return count
213
+
214
+ def unseal_all(self) -> int:
215
+ """Decrypt all vault-encrypted JSON files in the store.
216
+
217
+ Returns:
218
+ int: Number of files decrypted.
219
+ """
220
+ count = 0
221
+ for layer in MemoryLayer:
222
+ layer_dir = self.base_path / layer.value
223
+ if not layer_dir.exists():
224
+ continue
225
+ for json_file in layer_dir.glob("*.json"):
226
+ try:
227
+ raw = json_file.read_bytes()
228
+ if raw[: len(VAULT_HEADER)] != VAULT_HEADER:
229
+ continue # not encrypted
230
+ json_file.write_bytes(self._vault.decrypt(raw))
231
+ count += 1
232
+ except Exception:
233
+ continue
234
+ return count
235
+
236
+ def vault_status(self) -> dict:
237
+ """Scan memory files and report encryption coverage.
238
+
239
+ Returns:
240
+ dict: ``{total, encrypted, plaintext, coverage_pct}``.
241
+ """
242
+ total = encrypted = 0
243
+ header_len = len(VAULT_HEADER)
244
+ for layer in MemoryLayer:
245
+ layer_dir = self.base_path / layer.value
246
+ if not layer_dir.exists():
247
+ continue
248
+ for json_file in layer_dir.glob("*.json"):
249
+ total += 1
250
+ try:
251
+ with json_file.open("rb") as fh:
252
+ header = fh.read(header_len)
253
+ if header == VAULT_HEADER:
254
+ encrypted += 1
255
+ except Exception:
256
+ pass
257
+ plaintext = total - encrypted
258
+ pct = (encrypted / total * 100) if total else 100.0
259
+ return {
260
+ "total": total,
261
+ "encrypted": encrypted,
262
+ "plaintext": plaintext,
263
+ "coverage_pct": round(pct, 1),
264
+ }
265
+
266
+ # ------------------------------------------------------------------
267
+ # Internal
268
+ # ------------------------------------------------------------------
269
+
270
+ def _read_memory_file(self, path: Path) -> Optional[Memory]:
271
+ """Read a file and parse to Memory, decrypting if needed.
272
+
273
+ Args:
274
+ path: Path to the JSON file (may be encrypted).
275
+
276
+ Returns:
277
+ Optional[Memory]: Parsed memory or None on failure.
278
+ """
279
+ try:
280
+ raw = path.read_bytes()
281
+ if raw[: len(VAULT_HEADER)] == VAULT_HEADER:
282
+ raw = self._vault.decrypt(raw)
283
+ data = json.loads(raw.decode("utf-8"))
284
+ return Memory(**data)
285
+ except Exception:
286
+ return None