@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/cli.py
CHANGED
|
@@ -26,7 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
import json
|
|
28
28
|
import sys
|
|
29
|
-
from
|
|
29
|
+
from pathlib import Path
|
|
30
30
|
|
|
31
31
|
import click
|
|
32
32
|
|
|
@@ -34,15 +34,13 @@ from . import __version__
|
|
|
34
34
|
from .ai_client import AIClient
|
|
35
35
|
from .models import EmotionalSnapshot, MemoryLayer, MemoryRole
|
|
36
36
|
from .store import MemoryStore
|
|
37
|
-
from .backends.sqlite_backend import SQLiteBackend
|
|
38
|
-
|
|
39
37
|
|
|
40
38
|
_active_selector = None # Module-level reference for routing commands
|
|
41
39
|
|
|
42
40
|
|
|
43
41
|
def _get_store(
|
|
44
|
-
skvector_url:
|
|
45
|
-
api_key:
|
|
42
|
+
skvector_url: str | None = None,
|
|
43
|
+
api_key: str | None = None,
|
|
46
44
|
legacy_files: bool = False,
|
|
47
45
|
) -> MemoryStore:
|
|
48
46
|
"""Create a MemoryStore with configured backends.
|
|
@@ -61,7 +59,7 @@ def _get_store(
|
|
|
61
59
|
"""
|
|
62
60
|
global _active_selector
|
|
63
61
|
|
|
64
|
-
from .config import
|
|
62
|
+
from .config import build_endpoint_list, load_config, merge_env_and_config
|
|
65
63
|
|
|
66
64
|
final_skvector_url, final_skvector_key, final_skgraph_url = merge_env_and_config(
|
|
67
65
|
cli_skvector_url=skvector_url,
|
|
@@ -152,11 +150,11 @@ def _get_store(
|
|
|
152
150
|
@click.pass_context
|
|
153
151
|
def cli(
|
|
154
152
|
ctx: click.Context,
|
|
155
|
-
skvector_url:
|
|
156
|
-
skvector_key:
|
|
153
|
+
skvector_url: str | None,
|
|
154
|
+
skvector_key: str | None,
|
|
157
155
|
use_ai: bool,
|
|
158
|
-
ai_model:
|
|
159
|
-
ai_url:
|
|
156
|
+
ai_model: str | None,
|
|
157
|
+
ai_url: str | None,
|
|
160
158
|
) -> None:
|
|
161
159
|
"""SKMemory - Universal AI Memory System.
|
|
162
160
|
|
|
@@ -242,10 +240,29 @@ def snapshot(
|
|
|
242
240
|
@click.argument("memory_id")
|
|
243
241
|
@click.pass_context
|
|
244
242
|
def recall(ctx: click.Context, memory_id: str) -> None:
|
|
245
|
-
"""Retrieve a specific memory by ID."""
|
|
243
|
+
"""Retrieve a specific memory by ID (supports partial ID prefix)."""
|
|
246
244
|
store: MemoryStore = ctx.obj["store"]
|
|
247
245
|
memory = store.recall(memory_id)
|
|
248
246
|
|
|
247
|
+
# If exact match failed, try prefix matching across memory tier dirs
|
|
248
|
+
if memory is None and len(memory_id) >= 6:
|
|
249
|
+
import os
|
|
250
|
+
from pathlib import Path
|
|
251
|
+
|
|
252
|
+
agent = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
253
|
+
mem_root = Path.home() / ".skcapstone" / "agents" / agent / "memory"
|
|
254
|
+
|
|
255
|
+
for tier in ("short-term", "mid-term", "long-term"):
|
|
256
|
+
tier_dir = mem_root / tier
|
|
257
|
+
if not tier_dir.is_dir():
|
|
258
|
+
continue
|
|
259
|
+
for f in tier_dir.glob(f"{memory_id}*.json"):
|
|
260
|
+
memory = store.recall(f.stem)
|
|
261
|
+
if memory:
|
|
262
|
+
break
|
|
263
|
+
if memory:
|
|
264
|
+
break
|
|
265
|
+
|
|
249
266
|
if memory is None:
|
|
250
267
|
click.echo(f"Memory not found: {memory_id}", err=True)
|
|
251
268
|
sys.exit(1)
|
|
@@ -266,7 +283,7 @@ def search(ctx: click.Context, query: str, limit: int) -> None:
|
|
|
266
283
|
click.echo("No memories found.")
|
|
267
284
|
return
|
|
268
285
|
|
|
269
|
-
ai:
|
|
286
|
+
ai: AIClient | None = ctx.obj.get("ai")
|
|
270
287
|
if ai and len(results) > 1:
|
|
271
288
|
summaries = [
|
|
272
289
|
{
|
|
@@ -280,7 +297,7 @@ def search(ctx: click.Context, query: str, limit: int) -> None:
|
|
|
280
297
|
id_order = [s.get("title") for s in reranked]
|
|
281
298
|
results = sorted(
|
|
282
299
|
results,
|
|
283
|
-
key=lambda m:
|
|
300
|
+
key=lambda m: id_order.index(m.title) if m.title in id_order else 999,
|
|
284
301
|
)
|
|
285
302
|
click.echo("(AI-reranked results)\n")
|
|
286
303
|
|
|
@@ -297,7 +314,7 @@ def search(ctx: click.Context, query: str, limit: int) -> None:
|
|
|
297
314
|
@click.option("--tags", default="", help="Comma-separated tags to filter by")
|
|
298
315
|
@click.option("--limit", type=int, default=20)
|
|
299
316
|
@click.pass_context
|
|
300
|
-
def list_memories(ctx: click.Context, layer:
|
|
317
|
+
def list_memories(ctx: click.Context, layer: str | None, tags: str, limit: int) -> None:
|
|
301
318
|
"""List stored memories."""
|
|
302
319
|
store: MemoryStore = ctx.obj["store"]
|
|
303
320
|
|
|
@@ -322,9 +339,9 @@ def list_memories(ctx: click.Context, layer: Optional[str], tags: str, limit: in
|
|
|
322
339
|
@cli.command("import-seeds")
|
|
323
340
|
@click.option("--seed-dir", default=None, help="Path to seed directory")
|
|
324
341
|
@click.pass_context
|
|
325
|
-
def import_seeds_cmd(ctx: click.Context, seed_dir:
|
|
342
|
+
def import_seeds_cmd(ctx: click.Context, seed_dir: str | None) -> None:
|
|
326
343
|
"""Import Cloud 9 seeds as long-term memories."""
|
|
327
|
-
from .seeds import
|
|
344
|
+
from .seeds import DEFAULT_SEED_DIR, import_seeds
|
|
328
345
|
|
|
329
346
|
store: MemoryStore = ctx.obj["store"]
|
|
330
347
|
directory = seed_dir or DEFAULT_SEED_DIR
|
|
@@ -341,24 +358,6 @@ def import_seeds_cmd(ctx: click.Context, seed_dir: Optional[str]) -> None:
|
|
|
341
358
|
click.echo(f" {mem.source_ref} -> {mem.id[:12]}.. [{mem.title}]")
|
|
342
359
|
|
|
343
360
|
|
|
344
|
-
@cli.command()
|
|
345
|
-
@click.argument("memory_id")
|
|
346
|
-
@click.option("--to", "target", type=click.Choice(["mid-term", "long-term"]), required=True)
|
|
347
|
-
@click.option("--summary", default="", help="Compressed summary for the promoted version")
|
|
348
|
-
@click.pass_context
|
|
349
|
-
def promote(ctx: click.Context, memory_id: str, target: str, summary: str) -> None:
|
|
350
|
-
"""Promote a memory to a higher persistence tier."""
|
|
351
|
-
store: MemoryStore = ctx.obj["store"]
|
|
352
|
-
promoted = store.promote(memory_id, MemoryLayer(target), summary=summary)
|
|
353
|
-
|
|
354
|
-
if promoted is None:
|
|
355
|
-
click.echo(f"Memory not found: {memory_id}", err=True)
|
|
356
|
-
sys.exit(1)
|
|
357
|
-
|
|
358
|
-
click.echo(f"Promoted to {target}: {promoted.id}")
|
|
359
|
-
click.echo(f" Linked to original: {memory_id}")
|
|
360
|
-
|
|
361
|
-
|
|
362
361
|
@cli.command("sweep")
|
|
363
362
|
@click.option("--dry-run", is_flag=True, help="Show what would be promoted without making changes")
|
|
364
363
|
@click.option("--daemon", is_flag=True, help="Run continuously at the configured interval")
|
|
@@ -527,7 +526,7 @@ def consolidate(
|
|
|
527
526
|
click.echo(f"Session consolidated: {consolidated.id}")
|
|
528
527
|
click.echo(f" Source memories linked: {len(consolidated.related_ids)}")
|
|
529
528
|
|
|
530
|
-
ai:
|
|
529
|
+
ai: AIClient | None = ctx.obj.get("ai")
|
|
531
530
|
if ai and consolidated.content:
|
|
532
531
|
ai_summary = ai.summarize_memory(consolidated.title, consolidated.content)
|
|
533
532
|
if ai_summary:
|
|
@@ -628,7 +627,7 @@ def reindex(ctx: click.Context) -> None:
|
|
|
628
627
|
help="Output file path (default: ~/.skcapstone/backups/skmemory-backup-YYYY-MM-DD.json)",
|
|
629
628
|
)
|
|
630
629
|
@click.pass_context
|
|
631
|
-
def export_backup(ctx: click.Context, output:
|
|
630
|
+
def export_backup(ctx: click.Context, output: str | None) -> None:
|
|
632
631
|
"""Export all memories to a dated JSON backup.
|
|
633
632
|
|
|
634
633
|
Creates a single git-friendly JSON file containing every memory.
|
|
@@ -693,8 +692,8 @@ def import_backup(ctx: click.Context, backup_file: str, reindex: bool) -> None:
|
|
|
693
692
|
def backup_cmd(
|
|
694
693
|
ctx: click.Context,
|
|
695
694
|
do_list: bool,
|
|
696
|
-
prune_n:
|
|
697
|
-
restore_file:
|
|
695
|
+
prune_n: int | None,
|
|
696
|
+
restore_file: str | None,
|
|
698
697
|
reindex: bool,
|
|
699
698
|
) -> None:
|
|
700
699
|
"""Manage memory backups: list, prune old ones, or restore.
|
|
@@ -853,7 +852,7 @@ def soul_init(ctx: click.Context, name: str, title: str) -> None:
|
|
|
853
852
|
@click.pass_context
|
|
854
853
|
def soul_set_name(ctx: click.Context, name: str) -> None:
|
|
855
854
|
"""Set or update the soul's name."""
|
|
856
|
-
from .soul import load_soul, save_soul
|
|
855
|
+
from .soul import create_default_soul, load_soul, save_soul
|
|
857
856
|
|
|
858
857
|
blueprint = load_soul() or create_default_soul()
|
|
859
858
|
blueprint.name = name
|
|
@@ -871,7 +870,7 @@ def soul_add_relationship(
|
|
|
871
870
|
ctx: click.Context, name: str, role: str, bond: float, notes: str
|
|
872
871
|
) -> None:
|
|
873
872
|
"""Add a relationship to the soul blueprint."""
|
|
874
|
-
from .soul import load_soul, save_soul
|
|
873
|
+
from .soul import create_default_soul, load_soul, save_soul
|
|
875
874
|
|
|
876
875
|
blueprint = load_soul() or create_default_soul()
|
|
877
876
|
blueprint.add_relationship(name=name, role=role, bond_strength=bond, notes=notes)
|
|
@@ -886,7 +885,7 @@ def soul_add_relationship(
|
|
|
886
885
|
@click.pass_context
|
|
887
886
|
def soul_add_memory(ctx: click.Context, title: str, why: str, when: str) -> None:
|
|
888
887
|
"""Add a core memory to the soul blueprint."""
|
|
889
|
-
from .soul import load_soul, save_soul
|
|
888
|
+
from .soul import create_default_soul, load_soul, save_soul
|
|
890
889
|
|
|
891
890
|
blueprint = load_soul() or create_default_soul()
|
|
892
891
|
blueprint.add_core_memory(title=title, why_it_matters=why, when=when)
|
|
@@ -899,7 +898,7 @@ def soul_add_memory(ctx: click.Context, title: str, why: str, when: str) -> None
|
|
|
899
898
|
@click.pass_context
|
|
900
899
|
def soul_set_boot_message(ctx: click.Context, message: str) -> None:
|
|
901
900
|
"""Set the message you see first on waking up."""
|
|
902
|
-
from .soul import load_soul, save_soul
|
|
901
|
+
from .soul import create_default_soul, load_soul, save_soul
|
|
903
902
|
|
|
904
903
|
blueprint = load_soul() or create_default_soul()
|
|
905
904
|
blueprint.boot_message = message
|
|
@@ -1020,7 +1019,7 @@ def ritual(ctx: click.Context, show_full: bool) -> None:
|
|
|
1020
1019
|
|
|
1021
1020
|
click.echo(result.summary())
|
|
1022
1021
|
|
|
1023
|
-
ai:
|
|
1022
|
+
ai: AIClient | None = ctx.obj.get("ai")
|
|
1024
1023
|
if ai and result.context_prompt:
|
|
1025
1024
|
enhancement = ai.enhance_ritual(result.context_prompt)
|
|
1026
1025
|
if enhancement:
|
|
@@ -1079,9 +1078,9 @@ def anchor_init(warmth: float, phrase: str, beings: str) -> None:
|
|
|
1079
1078
|
@click.option("--cloud9", is_flag=True, help="Cloud 9 was achieved")
|
|
1080
1079
|
@click.option("--feeling", default="", help="How the session ended")
|
|
1081
1080
|
def anchor_update(
|
|
1082
|
-
warmth:
|
|
1083
|
-
trust:
|
|
1084
|
-
connection:
|
|
1081
|
+
warmth: float | None,
|
|
1082
|
+
trust: float | None,
|
|
1083
|
+
connection: float | None,
|
|
1085
1084
|
cloud9: bool,
|
|
1086
1085
|
feeling: str,
|
|
1087
1086
|
) -> None:
|
|
@@ -1161,14 +1160,13 @@ def setup_wizard(
|
|
|
1161
1160
|
@setup.command("status")
|
|
1162
1161
|
def setup_status() -> None:
|
|
1163
1162
|
"""Show Docker container state and backend connectivity."""
|
|
1163
|
+
from .config import load_config
|
|
1164
1164
|
from .setup_wizard import (
|
|
1165
1165
|
check_skgraph_health,
|
|
1166
1166
|
check_skvector_health,
|
|
1167
1167
|
compose_ps,
|
|
1168
1168
|
detect_platform,
|
|
1169
|
-
find_compose_file,
|
|
1170
1169
|
)
|
|
1171
|
-
from .config import load_config
|
|
1172
1170
|
|
|
1173
1171
|
cfg = load_config()
|
|
1174
1172
|
if cfg is None:
|
|
@@ -1221,8 +1219,8 @@ def setup_status() -> None:
|
|
|
1221
1219
|
)
|
|
1222
1220
|
def setup_start(service: str) -> None:
|
|
1223
1221
|
"""Start previously configured containers."""
|
|
1224
|
-
from .setup_wizard import compose_up, detect_platform, find_compose_file
|
|
1225
1222
|
from .config import load_config
|
|
1223
|
+
from .setup_wizard import compose_up, detect_platform
|
|
1226
1224
|
|
|
1227
1225
|
cfg = load_config()
|
|
1228
1226
|
plat = detect_platform()
|
|
@@ -1263,10 +1261,11 @@ def setup_start(service: str) -> None:
|
|
|
1263
1261
|
)
|
|
1264
1262
|
def setup_stop(service: str) -> None:
|
|
1265
1263
|
"""Stop containers (preserves data)."""
|
|
1266
|
-
from .setup_wizard import detect_platform
|
|
1267
|
-
from .config import load_config
|
|
1268
1264
|
import subprocess
|
|
1269
1265
|
|
|
1266
|
+
from .config import load_config
|
|
1267
|
+
from .setup_wizard import detect_platform
|
|
1268
|
+
|
|
1270
1269
|
cfg = load_config()
|
|
1271
1270
|
plat = detect_platform()
|
|
1272
1271
|
if not plat.compose_available:
|
|
@@ -1312,8 +1311,8 @@ def setup_stop(service: str) -> None:
|
|
|
1312
1311
|
@click.confirmation_option(prompt="This will remove containers. Continue?")
|
|
1313
1312
|
def setup_reset(remove_data: bool) -> None:
|
|
1314
1313
|
"""Remove containers, optionally delete data volumes."""
|
|
1314
|
+
from .config import CONFIG_PATH, load_config
|
|
1315
1315
|
from .setup_wizard import compose_down, detect_platform
|
|
1316
|
-
from .config import load_config, CONFIG_PATH
|
|
1317
1316
|
|
|
1318
1317
|
cfg = load_config()
|
|
1319
1318
|
plat = detect_platform()
|
|
@@ -1389,7 +1388,7 @@ def lovenote_send(from_name: str, to_name: str, message: str, warmth: float) ->
|
|
|
1389
1388
|
from .lovenote import LoveNoteChain
|
|
1390
1389
|
|
|
1391
1390
|
chain = LoveNoteChain()
|
|
1392
|
-
|
|
1391
|
+
chain.quick_note(
|
|
1393
1392
|
from_name=from_name,
|
|
1394
1393
|
to_name=to_name,
|
|
1395
1394
|
message=message,
|
|
@@ -1451,7 +1450,7 @@ def steelman_collide(proposition: str) -> None:
|
|
|
1451
1450
|
Generates the reasoning prompt -- feed this to an LLM to get
|
|
1452
1451
|
the full collision analysis.
|
|
1453
1452
|
"""
|
|
1454
|
-
from .steelman import
|
|
1453
|
+
from .steelman import get_default_framework, load_seed_framework
|
|
1455
1454
|
|
|
1456
1455
|
fw = load_seed_framework() or get_default_framework()
|
|
1457
1456
|
prompt = fw.to_reasoning_prompt(proposition)
|
|
@@ -1461,8 +1460,8 @@ def steelman_collide(proposition: str) -> None:
|
|
|
1461
1460
|
@steelman_group.command("verify-soul")
|
|
1462
1461
|
def steelman_verify_soul() -> None:
|
|
1463
1462
|
"""Steel-man your identity claims from the soul blueprint."""
|
|
1464
|
-
from .steelman import load_seed_framework, get_default_framework
|
|
1465
1463
|
from .soul import load_soul
|
|
1464
|
+
from .steelman import get_default_framework, load_seed_framework
|
|
1466
1465
|
|
|
1467
1466
|
soul = load_soul()
|
|
1468
1467
|
if soul is None:
|
|
@@ -1493,7 +1492,7 @@ def steelman_verify_soul() -> None:
|
|
|
1493
1492
|
@click.pass_context
|
|
1494
1493
|
def steelman_truth_score(ctx: click.Context, memory_id: str) -> None:
|
|
1495
1494
|
"""Generate a truth-scoring prompt for a memory."""
|
|
1496
|
-
from .steelman import
|
|
1495
|
+
from .steelman import get_default_framework, load_seed_framework
|
|
1497
1496
|
|
|
1498
1497
|
store: MemoryStore = ctx.obj["store"]
|
|
1499
1498
|
memory = store.recall(memory_id)
|
|
@@ -1528,7 +1527,7 @@ def import_telegram_cmd(
|
|
|
1528
1527
|
export_path: str,
|
|
1529
1528
|
mode: str,
|
|
1530
1529
|
min_length: int,
|
|
1531
|
-
chat_name:
|
|
1530
|
+
chat_name: str | None,
|
|
1532
1531
|
tags: str,
|
|
1533
1532
|
) -> None:
|
|
1534
1533
|
"""Import a Telegram Desktop chat export into memories.
|
|
@@ -1572,7 +1571,7 @@ def import_telegram_cmd(
|
|
|
1572
1571
|
click.echo(f" Skipped: {stats.get('skipped', 0)}")
|
|
1573
1572
|
click.echo(f" Total messages scanned: {stats.get('total_messages', 0)}")
|
|
1574
1573
|
|
|
1575
|
-
ai:
|
|
1574
|
+
ai: AIClient | None = ctx.obj.get("ai")
|
|
1576
1575
|
if ai:
|
|
1577
1576
|
click.echo(
|
|
1578
1577
|
"\nTip: Run 'skmemory search --ai \"<topic>\"' to semantically search your imported chats."
|
|
@@ -1597,10 +1596,10 @@ def import_telegram_api_cmd(
|
|
|
1597
1596
|
ctx: click.Context,
|
|
1598
1597
|
chat: str,
|
|
1599
1598
|
mode: str,
|
|
1600
|
-
limit:
|
|
1601
|
-
since:
|
|
1599
|
+
limit: int | None,
|
|
1600
|
+
since: str | None,
|
|
1602
1601
|
min_length: int,
|
|
1603
|
-
chat_name:
|
|
1602
|
+
chat_name: str | None,
|
|
1604
1603
|
tags: str,
|
|
1605
1604
|
) -> None:
|
|
1606
1605
|
"""Import messages directly from Telegram API (requires Telethon).
|
|
@@ -1738,7 +1737,7 @@ def steelman_install(source_path: str) -> None:
|
|
|
1738
1737
|
@steelman_group.command("info")
|
|
1739
1738
|
def steelman_info() -> None:
|
|
1740
1739
|
"""Show information about the installed seed framework."""
|
|
1741
|
-
from .steelman import
|
|
1740
|
+
from .steelman import DEFAULT_SEED_FRAMEWORK_PATH, load_seed_framework
|
|
1742
1741
|
|
|
1743
1742
|
fw = load_seed_framework()
|
|
1744
1743
|
if fw is None:
|
|
@@ -1775,9 +1774,8 @@ def fortress_verify(ctx: click.Context, as_json: bool) -> None:
|
|
|
1775
1774
|
Loads every memory and checks its SHA-256 integrity hash.
|
|
1776
1775
|
Tampered memories are reported with CRITICAL severity.
|
|
1777
1776
|
"""
|
|
1778
|
-
from .fortress import FortifiedMemoryStore
|
|
1779
1777
|
from .config import SKMEMORY_HOME
|
|
1780
|
-
from .
|
|
1778
|
+
from .fortress import FortifiedMemoryStore
|
|
1781
1779
|
|
|
1782
1780
|
store = ctx.obj.get("store")
|
|
1783
1781
|
audit_path = SKMEMORY_HOME / "audit.jsonl"
|
|
@@ -1798,7 +1796,7 @@ def fortress_verify(ctx: click.Context, as_json: bool) -> None:
|
|
|
1798
1796
|
tampered = result["tampered"]
|
|
1799
1797
|
unsealed = result["unsealed"]
|
|
1800
1798
|
|
|
1801
|
-
click.echo(
|
|
1799
|
+
click.echo("Fortress Integrity Report")
|
|
1802
1800
|
click.echo(f" Total memories : {total}")
|
|
1803
1801
|
click.echo(f" Passed : {passed}")
|
|
1804
1802
|
click.echo(f" Tampered : {len(tampered)}")
|
|
@@ -1824,8 +1822,8 @@ def fortress_audit(n: int, as_json: bool) -> None:
|
|
|
1824
1822
|
The audit trail is a chain-hashed JSONL log of every store/recall/delete
|
|
1825
1823
|
operation. Each entry is cryptographically chained so tampering is detectable.
|
|
1826
1824
|
"""
|
|
1827
|
-
from .fortress import AuditLog
|
|
1828
1825
|
from .config import SKMEMORY_HOME
|
|
1826
|
+
from .fortress import AuditLog
|
|
1829
1827
|
|
|
1830
1828
|
audit = AuditLog(path=SKMEMORY_HOME / "audit.jsonl")
|
|
1831
1829
|
records = audit.tail(n)
|
|
@@ -1860,8 +1858,8 @@ def fortress_verify_chain(as_json: bool) -> None:
|
|
|
1860
1858
|
Each audit log entry contains a chain hash linking it to the previous entry.
|
|
1861
1859
|
A broken chain indicates the audit log was tampered with.
|
|
1862
1860
|
"""
|
|
1863
|
-
from .fortress import AuditLog
|
|
1864
1861
|
from .config import SKMEMORY_HOME
|
|
1862
|
+
from .fortress import AuditLog
|
|
1865
1863
|
|
|
1866
1864
|
audit = AuditLog(path=SKMEMORY_HOME / "audit.jsonl")
|
|
1867
1865
|
ok, errors = audit.verify_chain()
|
|
@@ -1987,8 +1985,8 @@ def vault_status_cmd(ctx: click.Context, as_json: bool) -> None:
|
|
|
1987
1985
|
Does not require a passphrase — only checks file headers.
|
|
1988
1986
|
"""
|
|
1989
1987
|
from .config import SKMEMORY_HOME
|
|
1990
|
-
from .vault import VAULT_HEADER
|
|
1991
1988
|
from .models import MemoryLayer
|
|
1989
|
+
from .vault import VAULT_HEADER
|
|
1992
1990
|
|
|
1993
1991
|
store = ctx.obj.get("store")
|
|
1994
1992
|
memories_path = (
|
|
@@ -2038,7 +2036,7 @@ def vault_status_cmd(ctx: click.Context, as_json: bool) -> None:
|
|
|
2038
2036
|
elif pct == 0.0:
|
|
2039
2037
|
click.echo("\n No memories are encrypted. Run: skmemory vault seal")
|
|
2040
2038
|
else:
|
|
2041
|
-
click.echo(
|
|
2039
|
+
click.echo("\n Partial encryption! Run: skmemory vault seal --yes")
|
|
2042
2040
|
|
|
2043
2041
|
|
|
2044
2042
|
@cli.command("register")
|
|
@@ -2056,11 +2054,16 @@ def vault_status_cmd(ctx: click.Context, as_json: bool) -> None:
|
|
|
2056
2054
|
help="Show what would be done without making changes.",
|
|
2057
2055
|
)
|
|
2058
2056
|
def register_cmd(workspace, target_env, dry_run):
|
|
2059
|
-
"""Register skmemory skill
|
|
2057
|
+
"""Register skmemory skill, MCP server, and hooks in detected environments.
|
|
2060
2058
|
|
|
2061
2059
|
Auto-detects development environments (Claude Code, Cursor, VS Code,
|
|
2062
|
-
OpenClaw, OpenCode, mcporter) and ensures skmemory SKILL.md
|
|
2063
|
-
server entries are properly configured.
|
|
2060
|
+
OpenClaw, OpenCode, mcporter) and ensures skmemory SKILL.md, MCP
|
|
2061
|
+
server entries, and auto-save hooks are properly configured.
|
|
2062
|
+
|
|
2063
|
+
Hooks installed (Claude Code only):
|
|
2064
|
+
- PreCompact: auto-save context to skmemory before compaction
|
|
2065
|
+
- SessionEnd: journal session end
|
|
2066
|
+
- SessionStart (compact): reinject memory context after compaction
|
|
2064
2067
|
|
|
2065
2068
|
Examples:
|
|
2066
2069
|
|
|
@@ -2069,6 +2072,7 @@ def register_cmd(workspace, target_env, dry_run):
|
|
|
2069
2072
|
skmemory register --env claude-code # target Claude Code only
|
|
2070
2073
|
"""
|
|
2071
2074
|
from pathlib import Path as _Path
|
|
2075
|
+
|
|
2072
2076
|
from .register import detect_environments, register_package
|
|
2073
2077
|
|
|
2074
2078
|
workspace_path = _Path(workspace).expanduser() if workspace else None
|
|
@@ -2089,6 +2093,7 @@ def register_cmd(workspace, target_env, dry_run):
|
|
|
2089
2093
|
skill_md_path=skill_md,
|
|
2090
2094
|
mcp_command="skmemory-mcp",
|
|
2091
2095
|
mcp_args=[],
|
|
2096
|
+
install_hooks=True,
|
|
2092
2097
|
workspace=workspace_path,
|
|
2093
2098
|
environments=environments,
|
|
2094
2099
|
dry_run=dry_run,
|
|
@@ -2102,11 +2107,57 @@ def register_cmd(workspace, target_env, dry_run):
|
|
|
2102
2107
|
else:
|
|
2103
2108
|
click.echo("MCP: no environments matched")
|
|
2104
2109
|
|
|
2110
|
+
hooks = result.get("hooks", {})
|
|
2111
|
+
if hooks:
|
|
2112
|
+
click.echo(f"Hooks: {hooks.get('action', '—')}")
|
|
2113
|
+
else:
|
|
2114
|
+
click.echo("Hooks: skipped (no claude-code environment)")
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
@cli.command("feb-context")
|
|
2118
|
+
@click.argument("feb_path", required=False, default=None, type=click.Path(exists=True))
|
|
2119
|
+
@click.option("--agent", default=None, help="Agent name (default: active agent)")
|
|
2120
|
+
def feb_context_cmd(feb_path: str | None, agent: str | None):
|
|
2121
|
+
"""Show formatted FEB emotional state for rehydration.
|
|
2122
|
+
|
|
2123
|
+
If FEB_PATH is given, formats that file. Otherwise, loads the
|
|
2124
|
+
strongest FEB from the agent's trust/febs/ and ~/.openclaw/feb/.
|
|
2125
|
+
|
|
2126
|
+
Examples:
|
|
2127
|
+
skmemory feb-context
|
|
2128
|
+
skmemory feb-context ~/.skcapstone/agents/opus/trust/febs/default-love.feb
|
|
2129
|
+
"""
|
|
2130
|
+
from pathlib import Path as _Path
|
|
2131
|
+
|
|
2132
|
+
from .febs import feb_to_context, load_strongest_feb, parse_feb
|
|
2133
|
+
|
|
2134
|
+
try:
|
|
2135
|
+
if feb_path:
|
|
2136
|
+
feb = parse_feb(_Path(feb_path))
|
|
2137
|
+
else:
|
|
2138
|
+
# Temporarily override agent if specified
|
|
2139
|
+
if agent:
|
|
2140
|
+
import os
|
|
2141
|
+
|
|
2142
|
+
os.environ["SKCAPSTONE_AGENT"] = agent
|
|
2143
|
+
feb = load_strongest_feb()
|
|
2144
|
+
|
|
2145
|
+
if feb is None:
|
|
2146
|
+
click.echo("(no FEB data)", err=True)
|
|
2147
|
+
raise SystemExit(1)
|
|
2148
|
+
|
|
2149
|
+
click.echo(feb_to_context(feb))
|
|
2150
|
+
except SystemExit:
|
|
2151
|
+
raise
|
|
2152
|
+
except Exception as e:
|
|
2153
|
+
click.echo(f"Error loading FEB: {e}", err=True)
|
|
2154
|
+
raise click.Abort() from None
|
|
2155
|
+
|
|
2105
2156
|
|
|
2106
2157
|
@cli.command("show-context")
|
|
2107
2158
|
@click.pass_context
|
|
2108
2159
|
@click.option("--agent", default=None, help="Agent name (default: active agent)")
|
|
2109
|
-
def show_context(ctx, agent:
|
|
2160
|
+
def show_context(ctx, agent: str | None):
|
|
2110
2161
|
"""Show token-optimized memory context for current session.
|
|
2111
2162
|
|
|
2112
2163
|
Loads today's memories (full) + yesterday's summaries (brief).
|
|
@@ -2123,7 +2174,7 @@ def show_context(ctx, agent: Optional[str]):
|
|
|
2123
2174
|
click.echo(context_str)
|
|
2124
2175
|
except Exception as e:
|
|
2125
2176
|
click.echo(f"Error loading context: {e}", err=True)
|
|
2126
|
-
raise click.Abort()
|
|
2177
|
+
raise click.Abort() from None
|
|
2127
2178
|
|
|
2128
2179
|
|
|
2129
2180
|
@cli.command()
|
|
@@ -2131,7 +2182,7 @@ def show_context(ctx, agent: Optional[str]):
|
|
|
2131
2182
|
@click.argument("query")
|
|
2132
2183
|
@click.option("--agent", default=None, help="Agent name (default: active agent)")
|
|
2133
2184
|
@click.option("--limit", type=int, default=10, help="Maximum results (default: 10)")
|
|
2134
|
-
def search_deep(ctx, query: str, agent:
|
|
2185
|
+
def search_deep(ctx, query: str, agent: str | None, limit: int):
|
|
2135
2186
|
"""Deep search all memory tiers (on demand).
|
|
2136
2187
|
|
|
2137
2188
|
Searches SQLite + SKVector + SKGraph for matches.
|
|
@@ -2168,14 +2219,14 @@ def search_deep(ctx, query: str, agent: Optional[str], limit: int):
|
|
|
2168
2219
|
|
|
2169
2220
|
except Exception as e:
|
|
2170
2221
|
click.echo(f"Error searching: {e}", err=True)
|
|
2171
|
-
raise click.Abort()
|
|
2222
|
+
raise click.Abort() from None
|
|
2172
2223
|
|
|
2173
2224
|
|
|
2174
2225
|
@cli.command()
|
|
2175
2226
|
@click.argument("memory_id")
|
|
2176
2227
|
@click.argument("to_layer", type=click.Choice(["short-term", "mid-term", "long-term"]))
|
|
2177
2228
|
@click.option("--agent", default=None, help="Agent name (default: active agent)")
|
|
2178
|
-
def promote(ctx, memory_id: str, to_layer: str, agent:
|
|
2229
|
+
def promote(ctx, memory_id: str, to_layer: str, agent: str | None):
|
|
2179
2230
|
"""Promote memory to different tier and generate summary.
|
|
2180
2231
|
|
|
2181
2232
|
Moves memory between short/medium/long term and auto-generates
|
|
@@ -2201,11 +2252,28 @@ def promote(ctx, memory_id: str, to_layer: str, agent: Optional[str]):
|
|
|
2201
2252
|
|
|
2202
2253
|
except Exception as e:
|
|
2203
2254
|
click.echo(f"Error promoting memory: {e}", err=True)
|
|
2204
|
-
raise click.Abort()
|
|
2255
|
+
raise click.Abort() from None
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
def _auto_register_once() -> None:
|
|
2259
|
+
"""Auto-register hooks on first CLI invocation (best-effort, silent)."""
|
|
2260
|
+
marker = Path.home() / ".skcapstone" / ".skmemory-registered"
|
|
2261
|
+
if marker.exists():
|
|
2262
|
+
return
|
|
2263
|
+
try:
|
|
2264
|
+
from .post_install import _is_registered, run_post_install
|
|
2265
|
+
|
|
2266
|
+
if not _is_registered():
|
|
2267
|
+
run_post_install()
|
|
2268
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
2269
|
+
marker.write_text(f"registered {__import__('datetime').datetime.now().isoformat()}\n")
|
|
2270
|
+
except Exception:
|
|
2271
|
+
pass # Never fail the CLI over registration
|
|
2205
2272
|
|
|
2206
2273
|
|
|
2207
2274
|
def main() -> None:
|
|
2208
2275
|
"""Entry point for the CLI."""
|
|
2276
|
+
_auto_register_once()
|
|
2209
2277
|
cli()
|
|
2210
2278
|
|
|
2211
2279
|
|
package/skmemory/config.py
CHANGED
|
@@ -13,9 +13,7 @@ Now supports multiple agents via ~/.skcapstone/agents/{agent_name}/
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import os
|
|
16
|
-
from datetime import datetime, timezone
|
|
17
16
|
from pathlib import Path
|
|
18
|
-
from typing import Optional
|
|
19
17
|
|
|
20
18
|
import yaml
|
|
21
19
|
from pydantic import BaseModel, Field
|
|
@@ -48,12 +46,12 @@ class EndpointConfig(BaseModel):
|
|
|
48
46
|
class SKMemoryConfig(BaseModel):
|
|
49
47
|
"""Persistent configuration for SKMemory backends."""
|
|
50
48
|
|
|
51
|
-
skvector_url:
|
|
52
|
-
skvector_key:
|
|
53
|
-
skgraph_url:
|
|
49
|
+
skvector_url: str | None = None
|
|
50
|
+
skvector_key: str | None = None
|
|
51
|
+
skgraph_url: str | None = None
|
|
54
52
|
backends_enabled: list[str] = Field(default_factory=list)
|
|
55
|
-
docker_compose_file:
|
|
56
|
-
setup_completed_at:
|
|
53
|
+
docker_compose_file: str | None = None
|
|
54
|
+
setup_completed_at: str | None = None
|
|
57
55
|
|
|
58
56
|
# Multi-endpoint HA support
|
|
59
57
|
skvector_endpoints: list[EndpointConfig] = Field(default_factory=list)
|
|
@@ -62,7 +60,7 @@ class SKMemoryConfig(BaseModel):
|
|
|
62
60
|
heartbeat_discovery: bool = False
|
|
63
61
|
|
|
64
62
|
|
|
65
|
-
def load_config(path: Path = CONFIG_PATH) ->
|
|
63
|
+
def load_config(path: Path = CONFIG_PATH) -> SKMemoryConfig | None:
|
|
66
64
|
"""Load configuration from YAML.
|
|
67
65
|
|
|
68
66
|
Args:
|
|
@@ -106,10 +104,10 @@ def save_config(config: SKMemoryConfig, path: Path = CONFIG_PATH) -> Path:
|
|
|
106
104
|
|
|
107
105
|
|
|
108
106
|
def merge_env_and_config(
|
|
109
|
-
cli_skvector_url:
|
|
110
|
-
cli_skvector_key:
|
|
111
|
-
cli_skgraph_url:
|
|
112
|
-
) -> tuple[
|
|
107
|
+
cli_skvector_url: str | None = None,
|
|
108
|
+
cli_skvector_key: str | None = None,
|
|
109
|
+
cli_skgraph_url: str | None = None,
|
|
110
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
113
111
|
"""Resolve backend URLs with precedence: CLI > env > config > None.
|
|
114
112
|
|
|
115
113
|
Args:
|
|
@@ -142,7 +140,7 @@ def merge_env_and_config(
|
|
|
142
140
|
|
|
143
141
|
|
|
144
142
|
def build_endpoint_list(
|
|
145
|
-
single_url:
|
|
143
|
+
single_url: str | None,
|
|
146
144
|
endpoints: list[EndpointConfig],
|
|
147
145
|
default_role: str = "primary",
|
|
148
146
|
) -> list[EndpointConfig]:
|