@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/mcp_server.py
CHANGED
|
@@ -32,14 +32,14 @@ from __future__ import annotations
|
|
|
32
32
|
import asyncio
|
|
33
33
|
import json
|
|
34
34
|
import logging
|
|
35
|
-
from typing import Any
|
|
35
|
+
from typing import Any
|
|
36
36
|
|
|
37
37
|
from mcp.server import Server
|
|
38
38
|
from mcp.server.stdio import stdio_server
|
|
39
39
|
from mcp.types import TextContent, Tool
|
|
40
40
|
|
|
41
|
-
from .store import MemoryStore
|
|
42
41
|
from .models import MemoryLayer
|
|
42
|
+
from .store import MemoryStore
|
|
43
43
|
|
|
44
44
|
logger = logging.getLogger("skmemory.mcp")
|
|
45
45
|
|
|
@@ -49,7 +49,7 @@ server = Server("skmemory")
|
|
|
49
49
|
# Shared store instance
|
|
50
50
|
# ---------------------------------------------------------------------------
|
|
51
51
|
|
|
52
|
-
_store:
|
|
52
|
+
_store: MemoryStore | None = None
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def _get_store() -> MemoryStore:
|
|
@@ -215,9 +215,7 @@ async def list_tools() -> list[Tool]:
|
|
|
215
215
|
),
|
|
216
216
|
Tool(
|
|
217
217
|
name="memory_consolidate",
|
|
218
|
-
description=(
|
|
219
|
-
"Compress a session's short-term memories into one mid-term memory."
|
|
220
|
-
),
|
|
218
|
+
description=("Compress a session's short-term memories into one mid-term memory."),
|
|
221
219
|
inputSchema={
|
|
222
220
|
"type": "object",
|
|
223
221
|
"properties": {
|
|
@@ -275,9 +273,7 @@ async def list_tools() -> list[Tool]:
|
|
|
275
273
|
),
|
|
276
274
|
Tool(
|
|
277
275
|
name="memory_health",
|
|
278
|
-
description=(
|
|
279
|
-
"Full health check across all backends (primary, vector, graph)."
|
|
280
|
-
),
|
|
276
|
+
description=("Full health check across all backends (primary, vector, graph)."),
|
|
281
277
|
inputSchema={"type": "object", "properties": {}, "required": []},
|
|
282
278
|
),
|
|
283
279
|
Tool(
|
|
@@ -306,6 +302,67 @@ async def list_tools() -> list[Tool]:
|
|
|
306
302
|
"required": ["action"],
|
|
307
303
|
},
|
|
308
304
|
),
|
|
305
|
+
# ── Synthesis & Auto-Context ──────────────────────────────
|
|
306
|
+
Tool(
|
|
307
|
+
name="memory_synthesize_daily",
|
|
308
|
+
description=(
|
|
309
|
+
"Synthesize today's (or a given date's) memories into a single "
|
|
310
|
+
"narrative entry stored in mid-term. No LLM — uses tag frequency, "
|
|
311
|
+
"emotional arc, and template-based narrative."
|
|
312
|
+
),
|
|
313
|
+
inputSchema={
|
|
314
|
+
"type": "object",
|
|
315
|
+
"properties": {
|
|
316
|
+
"date": {
|
|
317
|
+
"type": "string",
|
|
318
|
+
"description": "Date to synthesize (YYYY-MM-DD). Defaults to today.",
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
"required": [],
|
|
322
|
+
},
|
|
323
|
+
),
|
|
324
|
+
Tool(
|
|
325
|
+
name="memory_synthesize_dreams",
|
|
326
|
+
description=(
|
|
327
|
+
"Process dream-engine memories into curated narrative memories "
|
|
328
|
+
"grouped by theme. Creates one mid-term memory per theme cluster."
|
|
329
|
+
),
|
|
330
|
+
inputSchema={
|
|
331
|
+
"type": "object",
|
|
332
|
+
"properties": {
|
|
333
|
+
"since": {
|
|
334
|
+
"type": "string",
|
|
335
|
+
"description": (
|
|
336
|
+
"Only process dreams after this date (YYYY-MM-DD). "
|
|
337
|
+
"Defaults to 7 days ago."
|
|
338
|
+
),
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
"required": [],
|
|
342
|
+
},
|
|
343
|
+
),
|
|
344
|
+
Tool(
|
|
345
|
+
name="memory_auto_context",
|
|
346
|
+
description=(
|
|
347
|
+
"Search all memory layers for context related to keywords. "
|
|
348
|
+
"Deduplicates results and ranks by relevance + emotional intensity + importance. "
|
|
349
|
+
"Returns results within a token budget. Use this for contextual auto-search."
|
|
350
|
+
),
|
|
351
|
+
inputSchema={
|
|
352
|
+
"type": "object",
|
|
353
|
+
"properties": {
|
|
354
|
+
"keywords": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"description": "Space-separated keywords to search for.",
|
|
357
|
+
},
|
|
358
|
+
"token_budget": {
|
|
359
|
+
"type": "integer",
|
|
360
|
+
"description": "Max tokens for results (default: 2000).",
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
"required": ["keywords"],
|
|
364
|
+
},
|
|
365
|
+
),
|
|
309
366
|
# ── Telegram ───────────────────────────────────────────────
|
|
310
367
|
Tool(
|
|
311
368
|
name="telegram_import",
|
|
@@ -524,21 +581,25 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
|
524
581
|
promoted = store.promote(memory_id, target, summary=summary)
|
|
525
582
|
if promoted is None:
|
|
526
583
|
return _error_response(f"Memory not found: {memory_id}")
|
|
527
|
-
return _json_response(
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
584
|
+
return _json_response(
|
|
585
|
+
{
|
|
586
|
+
"promoted_id": promoted.id,
|
|
587
|
+
"source_id": memory_id,
|
|
588
|
+
"target_layer": target_str,
|
|
589
|
+
}
|
|
590
|
+
)
|
|
532
591
|
|
|
533
592
|
elif name == "memory_consolidate":
|
|
534
593
|
session_id = arguments["session_id"]
|
|
535
594
|
summary = arguments["summary"]
|
|
536
595
|
consolidated = store.consolidate_session(session_id, summary)
|
|
537
|
-
return _json_response(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
596
|
+
return _json_response(
|
|
597
|
+
{
|
|
598
|
+
"memory_id": consolidated.id,
|
|
599
|
+
"session_id": session_id,
|
|
600
|
+
"consolidated": True,
|
|
601
|
+
}
|
|
602
|
+
)
|
|
542
603
|
|
|
543
604
|
elif name == "memory_context":
|
|
544
605
|
token_budget = int(arguments.get("token_budget", 4000))
|
|
@@ -589,8 +650,8 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
|
589
650
|
return _json_response(health)
|
|
590
651
|
|
|
591
652
|
elif name == "memory_verify":
|
|
592
|
-
from .fortress import FortifiedMemoryStore
|
|
593
653
|
from .config import SKMEMORY_HOME
|
|
654
|
+
from .fortress import FortifiedMemoryStore
|
|
594
655
|
|
|
595
656
|
fortress = FortifiedMemoryStore(
|
|
596
657
|
primary=store.primary,
|
|
@@ -601,14 +662,102 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
|
601
662
|
return _json_response(result)
|
|
602
663
|
|
|
603
664
|
elif name == "memory_audit":
|
|
604
|
-
from .fortress import AuditLog
|
|
605
665
|
from .config import SKMEMORY_HOME
|
|
666
|
+
from .fortress import AuditLog
|
|
606
667
|
|
|
607
668
|
n = int(arguments.get("last", 20))
|
|
608
669
|
audit = AuditLog(path=SKMEMORY_HOME / "audit.jsonl")
|
|
609
670
|
records = audit.tail(n)
|
|
610
671
|
return _json_response(records)
|
|
611
672
|
|
|
673
|
+
# ── Synthesis & Auto-Context tools ────────────────────
|
|
674
|
+
elif name == "memory_synthesize_daily":
|
|
675
|
+
from .journal import Journal
|
|
676
|
+
from .synthesis import JournalSynthesizer
|
|
677
|
+
|
|
678
|
+
date = arguments.get("date")
|
|
679
|
+
synthesizer = JournalSynthesizer(store, Journal())
|
|
680
|
+
memory = synthesizer.synthesize_daily(date)
|
|
681
|
+
return _json_response(
|
|
682
|
+
{
|
|
683
|
+
"memory_id": memory.id,
|
|
684
|
+
"title": memory.title,
|
|
685
|
+
"themes": memory.metadata.get("themes", []),
|
|
686
|
+
"memory_count": memory.metadata.get("memory_count", 0),
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
elif name == "memory_synthesize_dreams":
|
|
691
|
+
from .journal import Journal
|
|
692
|
+
from .synthesis import JournalSynthesizer
|
|
693
|
+
|
|
694
|
+
since = arguments.get("since")
|
|
695
|
+
synthesizer = JournalSynthesizer(store, Journal())
|
|
696
|
+
memories = synthesizer.synthesize_dreams(since)
|
|
697
|
+
return _json_response(
|
|
698
|
+
{
|
|
699
|
+
"synthesized": len(memories),
|
|
700
|
+
"clusters": [
|
|
701
|
+
{
|
|
702
|
+
"memory_id": m.id,
|
|
703
|
+
"title": m.title,
|
|
704
|
+
"theme": m.metadata.get("theme", ""),
|
|
705
|
+
"dream_count": m.metadata.get("dream_count", 0),
|
|
706
|
+
}
|
|
707
|
+
for m in memories
|
|
708
|
+
],
|
|
709
|
+
}
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
elif name == "memory_auto_context":
|
|
713
|
+
keywords_str = arguments["keywords"]
|
|
714
|
+
token_budget = int(arguments.get("token_budget", 2000))
|
|
715
|
+
keywords = keywords_str.split()
|
|
716
|
+
|
|
717
|
+
# Search for each keyword and collect results
|
|
718
|
+
seen_ids: set[str] = set()
|
|
719
|
+
all_results: list[dict] = []
|
|
720
|
+
|
|
721
|
+
for kw in keywords[:10]: # cap at 10 keywords
|
|
722
|
+
results = store.search(kw, limit=10)
|
|
723
|
+
for m in results:
|
|
724
|
+
if m.id not in seen_ids:
|
|
725
|
+
seen_ids.add(m.id)
|
|
726
|
+
all_results.append(
|
|
727
|
+
{
|
|
728
|
+
"id": m.id,
|
|
729
|
+
"title": m.title,
|
|
730
|
+
"summary": m.summary or m.content[:200],
|
|
731
|
+
"layer": m.layer.value,
|
|
732
|
+
"intensity": m.emotional.intensity,
|
|
733
|
+
"tags": m.tags[:5],
|
|
734
|
+
"source": m.source,
|
|
735
|
+
}
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Rank by intensity (higher = more relevant emotional context)
|
|
739
|
+
all_results.sort(key=lambda r: r["intensity"], reverse=True)
|
|
740
|
+
|
|
741
|
+
# Trim to token budget (estimate: title + summary per entry)
|
|
742
|
+
trimmed: list[dict] = []
|
|
743
|
+
used_tokens = 0
|
|
744
|
+
for entry in all_results:
|
|
745
|
+
text = entry["title"] + " " + entry["summary"]
|
|
746
|
+
est = int(len(text.split()) * 1.3)
|
|
747
|
+
if used_tokens + est > token_budget:
|
|
748
|
+
break
|
|
749
|
+
used_tokens += est
|
|
750
|
+
trimmed.append(entry)
|
|
751
|
+
|
|
752
|
+
return _json_response(
|
|
753
|
+
{
|
|
754
|
+
"results": trimmed,
|
|
755
|
+
"total_found": len(all_results),
|
|
756
|
+
"returned": len(trimmed),
|
|
757
|
+
"token_estimate": used_tokens,
|
|
758
|
+
}
|
|
759
|
+
)
|
|
760
|
+
|
|
612
761
|
# ── Telegram tools ────────────────────────────────────
|
|
613
762
|
elif name == "telegram_import":
|
|
614
763
|
from .importers.telegram import import_telegram
|
|
@@ -663,17 +812,21 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
|
663
812
|
elif name == "telegram_catchup":
|
|
664
813
|
from .importers.telegram_api import import_telegram_api
|
|
665
814
|
|
|
666
|
-
chat =
|
|
667
|
-
limit =
|
|
668
|
-
since =
|
|
669
|
-
min_length =
|
|
670
|
-
tags_str =
|
|
815
|
+
chat = arguments["chat"]
|
|
816
|
+
limit = arguments.get("limit", 2000)
|
|
817
|
+
since = arguments.get("since")
|
|
818
|
+
min_length = arguments.get("min_length", 20)
|
|
819
|
+
tags_str = arguments.get("tags", "")
|
|
671
820
|
tags = [t.strip() for t in tags_str.split(",") if t.strip()] if tags_str else None
|
|
672
821
|
|
|
673
|
-
store = MemoryStore()
|
|
674
822
|
stats = import_telegram_api(
|
|
675
|
-
store,
|
|
676
|
-
|
|
823
|
+
store,
|
|
824
|
+
chat,
|
|
825
|
+
mode="catchup",
|
|
826
|
+
limit=limit,
|
|
827
|
+
since=since,
|
|
828
|
+
min_message_length=min_length,
|
|
829
|
+
tags=tags,
|
|
677
830
|
)
|
|
678
831
|
return _json_response(stats)
|
|
679
832
|
|
package/skmemory/models.py
CHANGED
|
@@ -11,7 +11,7 @@ import hashlib
|
|
|
11
11
|
import uuid
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
13
|
from enum import Enum
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
16
|
from pydantic import BaseModel, Field, field_validator
|
|
17
17
|
|
|
@@ -90,12 +90,8 @@ class Memory(BaseModel):
|
|
|
90
90
|
"""
|
|
91
91
|
|
|
92
92
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
93
|
-
created_at: str = Field(
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
updated_at: str = Field(
|
|
97
|
-
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
98
|
-
)
|
|
93
|
+
created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
94
|
+
updated_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
99
95
|
|
|
100
96
|
layer: MemoryLayer = Field(default=MemoryLayer.SHORT)
|
|
101
97
|
role: MemoryRole = Field(default=MemoryRole.GENERAL)
|
|
@@ -123,11 +119,17 @@ class Memory(BaseModel):
|
|
|
123
119
|
default_factory=list,
|
|
124
120
|
description="IDs of related memories (graph edges)",
|
|
125
121
|
)
|
|
126
|
-
parent_id:
|
|
122
|
+
parent_id: str | None = Field(
|
|
127
123
|
default=None,
|
|
128
124
|
description="ID of parent memory (for hierarchical chains)",
|
|
129
125
|
)
|
|
130
126
|
|
|
127
|
+
context_tag: str = Field(
|
|
128
|
+
default="@chef-only",
|
|
129
|
+
description="Audience context tag: @public, @community, @work-circle, @inner-circle, "
|
|
130
|
+
"@chef-only, or scoped like @work:chiro. Conservative default: @chef-only.",
|
|
131
|
+
)
|
|
132
|
+
|
|
131
133
|
intent: str = Field(
|
|
132
134
|
default="",
|
|
133
135
|
description="WHY this memory was stored — the purpose, not just the content. "
|
package/skmemory/openclaw.py
CHANGED
|
@@ -19,14 +19,11 @@ Or from the OpenClaw JS plugin (calls CLI under the hood).
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import json
|
|
22
|
-
import os
|
|
23
22
|
from pathlib import Path
|
|
24
|
-
from typing import Any, Optional
|
|
25
23
|
|
|
26
|
-
from .models import EmotionalSnapshot, MemoryLayer, MemoryRole
|
|
27
|
-
from .store import MemoryStore
|
|
28
24
|
from .backends.sqlite_backend import SQLiteBackend
|
|
29
|
-
|
|
25
|
+
from .models import EmotionalSnapshot, MemoryLayer
|
|
26
|
+
from .store import MemoryStore
|
|
30
27
|
|
|
31
28
|
OPENCLAW_BASE = Path.home() / ".openclaw"
|
|
32
29
|
SKMEMORY_OPENCLAW_DIR = OPENCLAW_BASE / "plugins" / "skmemory"
|
|
@@ -48,14 +45,15 @@ class SKMemoryPlugin:
|
|
|
48
45
|
|
|
49
46
|
def __init__(
|
|
50
47
|
self,
|
|
51
|
-
base_path:
|
|
52
|
-
skvector_url:
|
|
53
|
-
skvector_key:
|
|
48
|
+
base_path: str | None = None,
|
|
49
|
+
skvector_url: str | None = None,
|
|
50
|
+
skvector_key: str | None = None,
|
|
54
51
|
) -> None:
|
|
55
52
|
vector = None
|
|
56
53
|
if skvector_url:
|
|
57
54
|
try:
|
|
58
55
|
from .backends.skvector_backend import SKVectorBackend
|
|
56
|
+
|
|
59
57
|
vector = SKVectorBackend(url=skvector_url, api_key=skvector_key)
|
|
60
58
|
except Exception:
|
|
61
59
|
pass
|
|
@@ -97,10 +95,10 @@ class SKMemoryPlugin:
|
|
|
97
95
|
content: str = "",
|
|
98
96
|
*,
|
|
99
97
|
layer: str = "short-term",
|
|
100
|
-
tags:
|
|
98
|
+
tags: list[str] | None = None,
|
|
101
99
|
intensity: float = 0.0,
|
|
102
100
|
valence: float = 0.0,
|
|
103
|
-
emotions:
|
|
101
|
+
emotions: list[str] | None = None,
|
|
104
102
|
source: str = "openclaw",
|
|
105
103
|
) -> str:
|
|
106
104
|
"""Capture a memory snapshot.
|
|
@@ -152,16 +150,11 @@ class SKMemoryPlugin:
|
|
|
152
150
|
"ORDER BY created_at DESC LIMIT ?",
|
|
153
151
|
(q, q, q, limit),
|
|
154
152
|
).fetchall()
|
|
155
|
-
return [
|
|
156
|
-
self.store.primary._row_to_memory_summary(r) for r in rows
|
|
157
|
-
]
|
|
153
|
+
return [self.store.primary._row_to_memory_summary(r) for r in rows]
|
|
158
154
|
results = self.store.search(query, limit=limit)
|
|
159
|
-
return [
|
|
160
|
-
{"id": m.id, "title": m.title, "layer": m.layer.value}
|
|
161
|
-
for m in results
|
|
162
|
-
]
|
|
155
|
+
return [{"id": m.id, "title": m.title, "layer": m.layer.value} for m in results]
|
|
163
156
|
|
|
164
|
-
def recall(self, memory_id: str) ->
|
|
157
|
+
def recall(self, memory_id: str) -> dict | None:
|
|
165
158
|
"""Retrieve a full memory by ID.
|
|
166
159
|
|
|
167
160
|
Args:
|
|
@@ -191,7 +184,7 @@ class SKMemoryPlugin:
|
|
|
191
184
|
"context_prompt": result.context_prompt,
|
|
192
185
|
}
|
|
193
186
|
|
|
194
|
-
def export(self, output_path:
|
|
187
|
+
def export(self, output_path: str | None = None) -> str:
|
|
195
188
|
"""Export all memories to a dated JSON backup.
|
|
196
189
|
|
|
197
190
|
Args:
|
|
@@ -229,9 +222,8 @@ class SKMemoryPlugin:
|
|
|
229
222
|
"""
|
|
230
223
|
try:
|
|
231
224
|
from . import __version__
|
|
225
|
+
|
|
232
226
|
state["skmemory_version"] = __version__
|
|
233
|
-
SKMEMORY_STATE_FILE.write_text(
|
|
234
|
-
json.dumps(state, indent=2), encoding="utf-8"
|
|
235
|
-
)
|
|
227
|
+
SKMEMORY_STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
236
228
|
except Exception:
|
|
237
229
|
pass
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Post-install auto-registration for skmemory.
|
|
2
|
+
|
|
3
|
+
Runs `skmemory register` automatically after pip install to ensure:
|
|
4
|
+
- MCP server is registered in Claude Code, Cursor, etc.
|
|
5
|
+
- Auto-save hooks are installed in Claude Code settings
|
|
6
|
+
- Skill symlink is created
|
|
7
|
+
|
|
8
|
+
Called via:
|
|
9
|
+
- `skmemory-post-install` console script (entry point)
|
|
10
|
+
- `pip install skmemory && skmemory-post-install`
|
|
11
|
+
- Automatically on first `skmemory` CLI invocation (if not yet registered)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_registered() -> bool:
|
|
21
|
+
"""Check if hooks are already installed (quick check)."""
|
|
22
|
+
settings = Path.home() / ".claude" / "settings.json"
|
|
23
|
+
if not settings.exists():
|
|
24
|
+
return False
|
|
25
|
+
try:
|
|
26
|
+
import json
|
|
27
|
+
|
|
28
|
+
data = json.loads(settings.read_text())
|
|
29
|
+
hooks = data.get("hooks", {})
|
|
30
|
+
return "PreCompact" in hooks and "SessionEnd" in hooks
|
|
31
|
+
except Exception:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_post_install() -> None:
|
|
36
|
+
"""Register skmemory MCP server, hooks, and skill symlinks."""
|
|
37
|
+
from .register import detect_environments, register_package
|
|
38
|
+
|
|
39
|
+
print("skmemory: running post-install registration...")
|
|
40
|
+
|
|
41
|
+
detected = detect_environments()
|
|
42
|
+
if not detected:
|
|
43
|
+
print(" No supported environments detected. Skipping.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
print(f" Detected: {', '.join(detected)}")
|
|
47
|
+
|
|
48
|
+
skill_md = Path(__file__).parent.parent / "SKILL.md"
|
|
49
|
+
if not skill_md.exists():
|
|
50
|
+
skill_md = Path(__file__).parent / "SKILL.md"
|
|
51
|
+
|
|
52
|
+
result = register_package(
|
|
53
|
+
name="skmemory",
|
|
54
|
+
skill_md_path=skill_md,
|
|
55
|
+
mcp_command="skmemory-mcp",
|
|
56
|
+
mcp_args=[],
|
|
57
|
+
install_hooks=True,
|
|
58
|
+
environments=detected,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
skill_action = result.get("skill", {}).get("action", "—")
|
|
62
|
+
print(f" Skill: {skill_action}")
|
|
63
|
+
|
|
64
|
+
mcp = result.get("mcp", {})
|
|
65
|
+
for env_name, action in mcp.items():
|
|
66
|
+
print(f" MCP ({env_name}): {action}")
|
|
67
|
+
|
|
68
|
+
hooks = result.get("hooks", {})
|
|
69
|
+
if hooks:
|
|
70
|
+
print(f" Hooks: {hooks.get('action', '—')}")
|
|
71
|
+
|
|
72
|
+
print("skmemory: post-install complete.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main() -> None:
|
|
76
|
+
"""Entry point for skmemory-post-install console script."""
|
|
77
|
+
try:
|
|
78
|
+
run_post_install()
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
# Never fail the install — registration is best-effort
|
|
81
|
+
print(f"skmemory: post-install warning: {exc}", file=sys.stderr)
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
main()
|
package/skmemory/predictive.py
CHANGED
|
@@ -25,7 +25,6 @@ import math
|
|
|
25
25
|
import time
|
|
26
26
|
from collections import Counter, defaultdict
|
|
27
27
|
from pathlib import Path
|
|
28
|
-
from typing import Optional
|
|
29
28
|
|
|
30
29
|
from pydantic import BaseModel, Field
|
|
31
30
|
|
|
@@ -62,7 +61,7 @@ class PredictiveRecall:
|
|
|
62
61
|
|
|
63
62
|
def __init__(
|
|
64
63
|
self,
|
|
65
|
-
log_path:
|
|
64
|
+
log_path: Path | None = None,
|
|
66
65
|
max_events: int = 5000,
|
|
67
66
|
) -> None:
|
|
68
67
|
self._log_path = log_path or DEFAULT_ACCESS_LOG
|
|
@@ -100,7 +99,10 @@ class PredictiveRecall:
|
|
|
100
99
|
current_session: list[AccessEvent] = []
|
|
101
100
|
|
|
102
101
|
for event in sorted(self._events, key=lambda e: e.timestamp):
|
|
103
|
-
if
|
|
102
|
+
if (
|
|
103
|
+
current_session
|
|
104
|
+
and (event.timestamp - current_session[-1].timestamp) > session_window
|
|
105
|
+
):
|
|
104
106
|
sessions.append(current_session)
|
|
105
107
|
current_session = []
|
|
106
108
|
current_session.append(event)
|
|
@@ -111,7 +113,7 @@ class PredictiveRecall:
|
|
|
111
113
|
ids_in_session = [e.memory_id for e in session]
|
|
112
114
|
for i, mid in enumerate(ids_in_session):
|
|
113
115
|
self._frequency[mid] += 1
|
|
114
|
-
for other in ids_in_session[i + 1:]:
|
|
116
|
+
for other in ids_in_session[i + 1 :]:
|
|
115
117
|
if other != mid:
|
|
116
118
|
self._cooccurrence[mid][other] += 1
|
|
117
119
|
self._cooccurrence[other][mid] += 1
|
|
@@ -120,7 +122,9 @@ class PredictiveRecall:
|
|
|
120
122
|
for tag in event.tags:
|
|
121
123
|
self._tag_affinity[tag][event.memory_id] += 1
|
|
122
124
|
|
|
123
|
-
def log_access(
|
|
125
|
+
def log_access(
|
|
126
|
+
self, memory_id: str, tags: list[str] | None = None, layer: str = "", context: str = ""
|
|
127
|
+
) -> None:
|
|
124
128
|
"""Record a memory access event for pattern learning.
|
|
125
129
|
|
|
126
130
|
Args:
|
|
@@ -144,15 +148,15 @@ class PredictiveRecall:
|
|
|
144
148
|
self._tag_affinity[tag][memory_id] += 1
|
|
145
149
|
|
|
146
150
|
if len(self._events) > self._max_events:
|
|
147
|
-
self._events = self._events[-self._max_events:]
|
|
151
|
+
self._events = self._events[-self._max_events :]
|
|
148
152
|
self._rebuild_indices()
|
|
149
153
|
|
|
150
154
|
self._save()
|
|
151
155
|
|
|
152
156
|
def predict(
|
|
153
157
|
self,
|
|
154
|
-
recent_ids:
|
|
155
|
-
active_tags:
|
|
158
|
+
recent_ids: list[str] | None = None,
|
|
159
|
+
active_tags: list[str] | None = None,
|
|
156
160
|
limit: int = 10,
|
|
157
161
|
) -> list[dict]:
|
|
158
162
|
"""Predict which memories will be needed next.
|
|
@@ -228,5 +232,5 @@ class PredictiveRecall:
|
|
|
228
232
|
def _save(self) -> None:
|
|
229
233
|
"""Persist the access log to disk."""
|
|
230
234
|
self._log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
231
|
-
data = [e.model_dump() for e in self._events[-self._max_events:]]
|
|
235
|
+
data = [e.model_dump() for e in self._events[-self._max_events :]]
|
|
232
236
|
self._log_path.write_text(json.dumps(data, indent=2))
|