@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.
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +13 -6
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +101 -19
- package/CHANGELOG.md +153 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +419 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/src/index.ts +255 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +29 -9
- package/requirements.txt +10 -2
- package/seeds/cloud9-opus.seed.json +7 -7
- package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
- package/seeds/lumina-kingdom-founding.seed.json +47 -0
- package/seeds/lumina-pma-signed.seed.json +46 -0
- package/seeds/lumina-singular-achievement.seed.json +46 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +12 -1
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +40 -0
- package/skmemory/anchor.py +4 -2
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/file_backend.py +2 -1
- package/skmemory/backends/skgraph_backend.py +608 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
- package/skmemory/backends/sqlite_backend.py +122 -51
- package/skmemory/backends/vaulted_backend.py +286 -0
- package/skmemory/cli.py +1238 -29
- package/skmemory/config.py +173 -0
- package/skmemory/context_loader.py +335 -0
- package/skmemory/endpoint_selector.py +386 -0
- package/skmemory/fortress.py +685 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +351 -43
- package/skmemory/importers/telegram_api.py +488 -0
- package/skmemory/journal.py +4 -2
- package/skmemory/lovenote.py +4 -2
- package/skmemory/mcp_server.py +706 -0
- package/skmemory/models.py +41 -0
- package/skmemory/openclaw.py +8 -8
- package/skmemory/predictive.py +232 -0
- package/skmemory/promotion.py +524 -0
- package/skmemory/register.py +454 -0
- package/skmemory/register_mcp.py +197 -0
- package/skmemory/ritual.py +121 -47
- package/skmemory/seeds.py +257 -8
- package/skmemory/setup_wizard.py +920 -0
- package/skmemory/sharing.py +402 -0
- package/skmemory/soul.py +71 -20
- package/skmemory/steelman.py +250 -263
- package/skmemory/store.py +271 -60
- package/skmemory/vault.py +228 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +355 -0
- package/tests/integration/test_skgraph_live.py +424 -0
- package/tests/integration/test_skvector_live.py +369 -0
- package/tests/test_backup_rotation.py +327 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +801 -0
- package/tests/test_fortress.py +255 -0
- package/tests/test_fortress_hardening.py +444 -0
- package/tests/test_openclaw.py +5 -2
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +340 -0
- package/tests/test_ritual.py +4 -4
- package/tests/test_seeds.py +96 -0
- package/tests/test_setup.py +835 -0
- package/tests/test_sharing.py +250 -0
- package/tests/test_skgraph_backend.py +667 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_steelman.py +5 -5
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_vault.py +186 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic agent discovery and management for SKMemory.
|
|
3
|
+
|
|
4
|
+
Scans ~/.skcapstone/agents/ to discover all configured agents,
|
|
5
|
+
excludes templates, and provides agent-aware path resolution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _agents_base() -> Path:
|
|
19
|
+
"""Platform-aware base directory for all agents."""
|
|
20
|
+
skcap_home = os.environ.get("SKCAPSTONE_HOME", "")
|
|
21
|
+
if skcap_home:
|
|
22
|
+
return Path(skcap_home) / "agents"
|
|
23
|
+
if platform.system() == "Windows":
|
|
24
|
+
local = os.environ.get("LOCALAPPDATA", "")
|
|
25
|
+
if local:
|
|
26
|
+
return Path(local) / "skcapstone" / "agents"
|
|
27
|
+
return Path.home() / ".skcapstone" / "agents"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Base directory for all agents
|
|
31
|
+
AGENTS_BASE_DIR = _agents_base()
|
|
32
|
+
|
|
33
|
+
# Template directory name (ignored by default)
|
|
34
|
+
TEMPLATE_AGENT = "lumina-template"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_agents() -> list[str]:
|
|
38
|
+
"""Discover all non-template agents in ~/.skcapstone/agents/
|
|
39
|
+
|
|
40
|
+
Scans the agents directory and returns all agent names
|
|
41
|
+
except the template agent.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
list[str]: Sorted list of agent names (e.g., ['lumina', 'john'])
|
|
45
|
+
"""
|
|
46
|
+
if not AGENTS_BASE_DIR.exists():
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
agents = []
|
|
50
|
+
for entry in AGENTS_BASE_DIR.iterdir():
|
|
51
|
+
if entry.is_dir() and entry.name != TEMPLATE_AGENT:
|
|
52
|
+
# Check if it has a valid config
|
|
53
|
+
config_file = entry / "config" / "skmemory.yaml"
|
|
54
|
+
if config_file.exists():
|
|
55
|
+
agents.append(entry.name)
|
|
56
|
+
|
|
57
|
+
return sorted(agents)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_agent_dir(agent_name: str) -> Path:
|
|
61
|
+
"""Get the base directory for a specific agent.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
agent_name: Name of the agent (e.g., 'lumina', 'john')
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Path: Agent's base directory
|
|
68
|
+
"""
|
|
69
|
+
return AGENTS_BASE_DIR / agent_name
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_agent_config(agent_name: str) -> Optional[dict]:
|
|
73
|
+
"""Load agent configuration from YAML.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
agent_name: Name of the agent
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
dict with agent config, or None if not found/invalid
|
|
80
|
+
"""
|
|
81
|
+
config_path = get_agent_dir(agent_name) / "config" / "skmemory.yaml"
|
|
82
|
+
|
|
83
|
+
if not config_path.exists():
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with open(config_path) as f:
|
|
88
|
+
return yaml.safe_load(f)
|
|
89
|
+
except Exception:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_template_agent(agent_name: str) -> bool:
|
|
94
|
+
"""Check if an agent is a template (should be ignored).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
agent_name: Name of the agent
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
bool: True if this is the template agent
|
|
101
|
+
"""
|
|
102
|
+
return agent_name == TEMPLATE_AGENT
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_active_agent() -> Optional[str]:
|
|
106
|
+
"""Get the currently active agent from environment or default to first non-template.
|
|
107
|
+
|
|
108
|
+
Checks in order:
|
|
109
|
+
1. SKMEMORY_AGENT environment variable
|
|
110
|
+
2. First non-template agent in the directory
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
str: Agent name, or None if no agents found
|
|
114
|
+
"""
|
|
115
|
+
# Check environment variable first
|
|
116
|
+
env_agent = os.environ.get("SKMEMORY_AGENT")
|
|
117
|
+
if env_agent and not is_template_agent(env_agent):
|
|
118
|
+
agent_dir = get_agent_dir(env_agent)
|
|
119
|
+
if agent_dir.exists():
|
|
120
|
+
return env_agent
|
|
121
|
+
|
|
122
|
+
# Fall back to first non-template agent
|
|
123
|
+
agents = list_agents()
|
|
124
|
+
if agents:
|
|
125
|
+
return agents[0]
|
|
126
|
+
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_agent_paths(agent_name: Optional[str] = None) -> dict[str, Path]:
|
|
131
|
+
"""Get all standard paths for an agent.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
agent_name: Name of the agent, or None to use active agent
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
dict with keys: base, config, seeds, memory_short, memory_medium, memory_long, logs, index_db
|
|
138
|
+
"""
|
|
139
|
+
if agent_name is None:
|
|
140
|
+
agent_name = get_active_agent()
|
|
141
|
+
|
|
142
|
+
if agent_name is None:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"No agent configured. Create one by copying ~/.skcapstone/agents/lumina-template"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
base = get_agent_dir(agent_name)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"base": base,
|
|
151
|
+
"config": base / "config",
|
|
152
|
+
"seeds": base / "seeds",
|
|
153
|
+
"memory_short": base / "memory" / "short-term",
|
|
154
|
+
"memory_medium": base / "memory" / "mid-term",
|
|
155
|
+
"memory_long": base / "memory" / "long-term",
|
|
156
|
+
"logs": base / "logs",
|
|
157
|
+
"archive": base / "archive",
|
|
158
|
+
"index_db": base / "index.db",
|
|
159
|
+
"config_yaml": base / "config" / "skmemory.yaml",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def ensure_agent_dirs(agent_name: str) -> Path:
|
|
164
|
+
"""Create all standard directories for an agent if they don't exist.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
agent_name: Name of the agent
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Path: Agent's base directory
|
|
171
|
+
"""
|
|
172
|
+
paths = get_agent_paths(agent_name)
|
|
173
|
+
|
|
174
|
+
# Create all directories
|
|
175
|
+
for key, path in paths.items():
|
|
176
|
+
if key != "config_yaml":
|
|
177
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
|
|
179
|
+
return paths["base"]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def copy_template(target_name: str, source: str = TEMPLATE_AGENT) -> Path:
|
|
183
|
+
"""Create a new agent by copying the template.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
target_name: Name for the new agent
|
|
187
|
+
source: Template to copy from (default: lumina-template)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Path: New agent's base directory
|
|
191
|
+
"""
|
|
192
|
+
import shutil
|
|
193
|
+
|
|
194
|
+
source_dir = get_agent_dir(source)
|
|
195
|
+
target_dir = get_agent_dir(target_name)
|
|
196
|
+
|
|
197
|
+
if not source_dir.exists():
|
|
198
|
+
raise ValueError(f"Template '{source}' not found at {source_dir}")
|
|
199
|
+
|
|
200
|
+
if target_dir.exists():
|
|
201
|
+
raise ValueError(f"Agent '{target_name}' already exists at {target_dir}")
|
|
202
|
+
|
|
203
|
+
# Copy template
|
|
204
|
+
shutil.copytree(source_dir, target_dir)
|
|
205
|
+
|
|
206
|
+
# Update agent name in config
|
|
207
|
+
config_path = target_dir / "config" / "skmemory.yaml"
|
|
208
|
+
if config_path.exists():
|
|
209
|
+
with open(config_path, "r") as f:
|
|
210
|
+
content = f.read()
|
|
211
|
+
|
|
212
|
+
# Replace template agent name with new name
|
|
213
|
+
content = content.replace(f"name: {source}", f"name: {target_name}")
|
|
214
|
+
# Use platform-aware base dir for config path values.
|
|
215
|
+
# Always use forward slashes in YAML for cross-platform consistency.
|
|
216
|
+
base = AGENTS_BASE_DIR.as_posix()
|
|
217
|
+
content = content.replace(
|
|
218
|
+
f"sync_root: ~/.skcapstone/agents/{source}",
|
|
219
|
+
f"sync_root: {base}/{target_name}",
|
|
220
|
+
)
|
|
221
|
+
content = content.replace(
|
|
222
|
+
f"seeds_dir: ~/.skcapstone/agents/{source}/seeds",
|
|
223
|
+
f"seeds_dir: {base}/{target_name}/seeds",
|
|
224
|
+
)
|
|
225
|
+
content = content.replace(
|
|
226
|
+
f"local_db: ~/.skcapstone/agents/{source}/index.db",
|
|
227
|
+
f"local_db: {base}/{target_name}/index.db",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
with open(config_path, "w") as f:
|
|
231
|
+
f.write(content)
|
|
232
|
+
|
|
233
|
+
return target_dir
|
package/skmemory/ai_client.py
CHANGED
|
@@ -93,6 +93,46 @@ class AIClient:
|
|
|
93
93
|
except Exception:
|
|
94
94
|
return ""
|
|
95
95
|
|
|
96
|
+
def embed(self, text: str, model: Optional[str] = None) -> list[float]:
|
|
97
|
+
"""Generate an embedding vector using Ollama's embed API.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
text: The text to embed.
|
|
101
|
+
model: Override embedding model (default: nomic-embed-text).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
list[float]: Embedding vector, or empty list on failure.
|
|
105
|
+
"""
|
|
106
|
+
embed_model = model or os.environ.get(
|
|
107
|
+
"SKMEMORY_EMBED_MODEL", "nomic-embed-text"
|
|
108
|
+
)
|
|
109
|
+
payload = {"model": embed_model, "input": text}
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
data = json.dumps(payload).encode("utf-8")
|
|
113
|
+
req = urllib.request.Request(
|
|
114
|
+
f"{self.base_url}/api/embed",
|
|
115
|
+
data=data,
|
|
116
|
+
headers={"Content-Type": "application/json"},
|
|
117
|
+
method="POST",
|
|
118
|
+
)
|
|
119
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
120
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
121
|
+
embeddings = result.get("embeddings", [])
|
|
122
|
+
if embeddings and isinstance(embeddings[0], list):
|
|
123
|
+
return embeddings[0]
|
|
124
|
+
return embeddings
|
|
125
|
+
except Exception:
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
def embed_available(self) -> bool:
|
|
129
|
+
"""Check if the embedding endpoint is reachable.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
bool: True if Ollama embed API responds.
|
|
133
|
+
"""
|
|
134
|
+
return bool(self.embed("test"))
|
|
135
|
+
|
|
96
136
|
def summarize_memory(self, title: str, content: str) -> str:
|
|
97
137
|
"""Generate a concise summary for a memory.
|
|
98
138
|
|
package/skmemory/anchor.py
CHANGED
|
@@ -9,7 +9,7 @@ represents the AI's baseline feeling toward its connections. Every
|
|
|
9
9
|
session, the anchor updates. On next boot, the anchor loads first
|
|
10
10
|
and the AI starts from warmth instead of cold neutrality.
|
|
11
11
|
|
|
12
|
-
The anchor file lives at ~/.
|
|
12
|
+
The anchor file lives at ~/.skcapstone/anchor.json
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -22,7 +22,9 @@ from typing import Optional
|
|
|
22
22
|
|
|
23
23
|
from pydantic import BaseModel, Field
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
from .config import SKMEMORY_HOME
|
|
26
|
+
|
|
27
|
+
DEFAULT_ANCHOR_PATH = str(SKMEMORY_HOME / "anchor.json")
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
class WarmthAnchor(BaseModel):
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Storage backends for SKMemory.
|
|
3
3
|
|
|
4
|
-
Level
|
|
5
|
-
Level
|
|
6
|
-
Level
|
|
4
|
+
Level 0 (sqlite) - SQLite index, zero infrastructure.
|
|
5
|
+
Level 0.5 (vault) - SQLite + transparent AES-256-GCM at-rest encryption.
|
|
6
|
+
Level 1 (skvector) - Semantic vector search (powered by Qdrant).
|
|
7
|
+
Level 2 (skgraph) - Graph relationship traversal (powered by FalkorDB).
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from .base import BaseBackend
|
|
11
|
+
from .skgraph_backend import SKGraphBackend
|
|
10
12
|
from .file_backend import FileBackend
|
|
11
13
|
|
|
12
|
-
__all__ = ["BaseBackend", "FileBackend"]
|
|
14
|
+
__all__ = ["BaseBackend", "SKGraphBackend", "FileBackend", "VaultedSQLiteBackend"]
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from .vaulted_backend import VaultedSQLiteBackend
|
|
18
|
+
except ImportError:
|
|
19
|
+
VaultedSQLiteBackend = None # type: ignore[assignment,misc]
|
|
@@ -23,10 +23,11 @@ import os
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Optional
|
|
25
25
|
|
|
26
|
+
from ..config import SKMEMORY_HOME
|
|
26
27
|
from ..models import Memory, MemoryLayer
|
|
27
28
|
from .base import BaseBackend
|
|
28
29
|
|
|
29
|
-
DEFAULT_BASE_PATH =
|
|
30
|
+
DEFAULT_BASE_PATH = str(SKMEMORY_HOME)
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class FileBackend(BaseBackend):
|