@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,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
@@ -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
 
@@ -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 ~/.skmemory/anchor.json
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
- DEFAULT_ANCHOR_PATH = os.path.expanduser("~/.skmemory/anchor.json")
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 1 (file) - JSON files on disk, zero infrastructure.
5
- Level 2 (qdrant) - Vector search via Qdrant for semantic recall.
6
- Level 3 (graph) - FalkorDB graph relationships between memories.
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 = os.path.expanduser("~/.skmemory/memories")
30
+ DEFAULT_BASE_PATH = str(SKMEMORY_HOME)
30
31
 
31
32
 
32
33
  class FileBackend(BaseBackend):