@smilintux/skmemory 0.5.0 → 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.
Files changed (127) hide show
  1. package/.github/workflows/ci.yml +40 -4
  2. package/.github/workflows/publish.yml +11 -5
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +399 -19
  5. package/CHANGELOG.md +179 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +425 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/examples/stignore-agent.example +59 -0
  12. package/examples/stignore-root.example +62 -0
  13. package/index.js +6 -5
  14. package/openclaw-plugin/openclaw.plugin.json +10 -0
  15. package/openclaw-plugin/package.json +2 -1
  16. package/openclaw-plugin/src/index.js +527 -230
  17. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  18. package/package.json +1 -1
  19. package/pyproject.toml +32 -9
  20. package/requirements.txt +10 -2
  21. package/scripts/dream-rescue.py +179 -0
  22. package/scripts/memory-cleanup.py +313 -0
  23. package/scripts/recover-missing.py +180 -0
  24. package/scripts/skcapstone-backup.sh +44 -0
  25. package/seeds/cloud9-lumina.seed.json +6 -4
  26. package/seeds/cloud9-opus.seed.json +13 -11
  27. package/seeds/courage.seed.json +9 -2
  28. package/seeds/curiosity.seed.json +9 -2
  29. package/seeds/grief.seed.json +9 -2
  30. package/seeds/joy.seed.json +9 -2
  31. package/seeds/love.seed.json +9 -2
  32. package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
  33. package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
  34. package/seeds/lumina-kingdom-founding.seed.json +49 -0
  35. package/seeds/lumina-pma-signed.seed.json +48 -0
  36. package/seeds/lumina-singular-achievement.seed.json +48 -0
  37. package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
  38. package/seeds/plant-kingdom-journal.py +203 -0
  39. package/seeds/plant-lumina-seeds.py +280 -0
  40. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  41. package/seeds/sovereignty.seed.json +9 -2
  42. package/seeds/trust.seed.json +9 -2
  43. package/skill.yaml +46 -0
  44. package/skmemory/HA.md +296 -0
  45. package/skmemory/__init__.py +25 -11
  46. package/skmemory/agents.py +233 -0
  47. package/skmemory/ai_client.py +46 -17
  48. package/skmemory/anchor.py +9 -11
  49. package/skmemory/audience.py +278 -0
  50. package/skmemory/backends/__init__.py +11 -4
  51. package/skmemory/backends/base.py +3 -4
  52. package/skmemory/backends/file_backend.py +19 -13
  53. package/skmemory/backends/skgraph_backend.py +596 -0
  54. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
  55. package/skmemory/backends/sqlite_backend.py +226 -72
  56. package/skmemory/backends/vaulted_backend.py +284 -0
  57. package/skmemory/cli.py +1345 -68
  58. package/skmemory/config.py +171 -0
  59. package/skmemory/context_loader.py +333 -0
  60. package/skmemory/data/audience_config.json +60 -0
  61. package/skmemory/endpoint_selector.py +391 -0
  62. package/skmemory/febs.py +225 -0
  63. package/skmemory/fortress.py +675 -0
  64. package/skmemory/graph_queries.py +238 -0
  65. package/skmemory/hooks/__init__.py +18 -0
  66. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  67. package/skmemory/hooks/pre-compact-save.sh +81 -0
  68. package/skmemory/hooks/session-end-save.sh +103 -0
  69. package/skmemory/hooks/session-start-ritual.sh +104 -0
  70. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  71. package/skmemory/importers/__init__.py +9 -1
  72. package/skmemory/importers/telegram.py +384 -47
  73. package/skmemory/importers/telegram_api.py +580 -0
  74. package/skmemory/journal.py +7 -9
  75. package/skmemory/lovenote.py +8 -13
  76. package/skmemory/mcp_server.py +859 -0
  77. package/skmemory/models.py +51 -8
  78. package/skmemory/openclaw.py +20 -28
  79. package/skmemory/post_install.py +86 -0
  80. package/skmemory/predictive.py +236 -0
  81. package/skmemory/promotion.py +548 -0
  82. package/skmemory/quadrants.py +100 -24
  83. package/skmemory/register.py +580 -0
  84. package/skmemory/register_mcp.py +196 -0
  85. package/skmemory/ritual.py +224 -59
  86. package/skmemory/seeds.py +255 -11
  87. package/skmemory/setup_wizard.py +908 -0
  88. package/skmemory/sharing.py +408 -0
  89. package/skmemory/soul.py +98 -28
  90. package/skmemory/steelman.py +273 -260
  91. package/skmemory/store.py +411 -78
  92. package/skmemory/synthesis.py +634 -0
  93. package/skmemory/vault.py +225 -0
  94. package/tests/conftest.py +46 -0
  95. package/tests/integration/__init__.py +0 -0
  96. package/tests/integration/conftest.py +233 -0
  97. package/tests/integration/test_cross_backend.py +350 -0
  98. package/tests/integration/test_skgraph_live.py +420 -0
  99. package/tests/integration/test_skvector_live.py +366 -0
  100. package/tests/test_ai_client.py +1 -4
  101. package/tests/test_audience.py +233 -0
  102. package/tests/test_backup_rotation.py +318 -0
  103. package/tests/test_cli.py +6 -6
  104. package/tests/test_endpoint_selector.py +839 -0
  105. package/tests/test_export_import.py +4 -10
  106. package/tests/test_file_backend.py +0 -1
  107. package/tests/test_fortress.py +256 -0
  108. package/tests/test_fortress_hardening.py +441 -0
  109. package/tests/test_openclaw.py +6 -6
  110. package/tests/test_predictive.py +237 -0
  111. package/tests/test_promotion.py +347 -0
  112. package/tests/test_quadrants.py +11 -5
  113. package/tests/test_ritual.py +22 -18
  114. package/tests/test_seeds.py +97 -7
  115. package/tests/test_setup.py +950 -0
  116. package/tests/test_sharing.py +257 -0
  117. package/tests/test_skgraph_backend.py +660 -0
  118. package/tests/test_skvector_backend.py +326 -0
  119. package/tests/test_soul.py +1 -3
  120. package/tests/test_sqlite_backend.py +8 -17
  121. package/tests/test_steelman.py +7 -8
  122. package/tests/test_store.py +0 -2
  123. package/tests/test_store_graph_integration.py +245 -0
  124. package/tests/test_synthesis.py +275 -0
  125. package/tests/test_telegram_import.py +39 -15
  126. package/tests/test_vault.py +187 -0
  127. package/skmemory/backends/falkordb_backend.py +0 -310
package/skill.yaml ADDED
@@ -0,0 +1,46 @@
1
+ name: skmemory
2
+ version: "0.6.0"
3
+ description: >
4
+ Persistent sovereign memory layer. Short-term, mid-term, and long-term
5
+ stores with HMAC integrity seals and automatic promotion. Backends for
6
+ file, SQLite, vector, and graph storage.
7
+
8
+ author:
9
+ name: smilinTux.org
10
+ email: hello@smilintux.org
11
+
12
+ tags: [memory, persistence, sovereign, integrity, sqlite, vector]
13
+ category: core
14
+
15
+ tools:
16
+ - name: memory_store
17
+ description: Store a new memory with tags and importance score
18
+ entrypoint: skmemory.skill:memory_store
19
+
20
+ - name: memory_search
21
+ description: Full-text search across all memory layers
22
+ entrypoint: skmemory.skill:memory_search
23
+
24
+ - name: memory_recall
25
+ description: Recall a specific memory by ID
26
+ entrypoint: skmemory.skill:memory_recall
27
+
28
+ - name: memory_list
29
+ description: List memories filtered by layer, tags, or importance
30
+ entrypoint: skmemory.skill:memory_list
31
+
32
+ - name: memory_forget
33
+ description: Delete a memory by ID
34
+ entrypoint: skmemory.skill:memory_forget
35
+
36
+ - name: memory_promote
37
+ description: Promote a memory to a higher persistence layer
38
+ entrypoint: skmemory.skill:memory_promote
39
+
40
+ - name: memory_health
41
+ description: Check memory store health and statistics
42
+ entrypoint: skmemory.skill:memory_health
43
+
44
+ - name: memory_verify
45
+ description: Verify HMAC integrity seals on stored memories
46
+ entrypoint: skmemory.skill:memory_verify
package/skmemory/HA.md ADDED
@@ -0,0 +1,296 @@
1
+ # SKMemory High Availability & Routing for SKVector and SKGraph Backends
2
+
3
+ > Self-contained endpoint routing for SKVector and SKGraph backends.
4
+ > No external load balancer. No new dependencies. Backward compatible.
5
+
6
+ ## Overview
7
+
8
+ SKMemory's SKVector and SKGraph backends can run on multiple nodes across a
9
+ Tailscale mesh (or any network). The **EndpointSelector** sits between config
10
+ resolution and backend construction: it discovers endpoints, probes their
11
+ latency, selects the fastest healthy one, and fails over automatically.
12
+
13
+ Key properties:
14
+ - **On-demand probing** with TTL cache (no background threads)
15
+ - **Config endpoints take precedence** over heartbeat discovery
16
+ - **Graceful degradation** — missing heartbeats, Tailscale, or config all fail silently
17
+ - **Backward compatible** — single `skvector_url` configs work unchanged
18
+
19
+ ## Architecture
20
+
21
+ ### Routing Layer Diagram
22
+
23
+ ```mermaid
24
+ graph TB
25
+ CLI[skmemory CLI / Agent] --> ES[EndpointSelector]
26
+ ES --> |probe| EP1[SKVector @ home:6333]
27
+ ES --> |probe| EP2[SKVector @ vps:6333]
28
+ ES --> |probe| EP3[SKGraph @ home:6379]
29
+ ES --> |probe| EP4[SKGraph @ cloud:6379]
30
+ ES --> |select best| MS[MemoryStore]
31
+ MS --> SQLite[SQLite Primary]
32
+ MS --> SKVectorBackend[SKVectorBackend]
33
+ MS --> SKGraphBackend[SKGraphBackend]
34
+ HB[Heartbeat Mesh] -.->|discover| ES
35
+
36
+ style ES fill:#f9f,stroke:#333
37
+ style MS fill:#bbf,stroke:#333
38
+ ```
39
+
40
+ The selector picks a URL, then backends are created with that URL. Backend
41
+ internals are **never modified** — the selector is a pure URL resolver.
42
+
43
+ ### Endpoint Selection Flowchart
44
+
45
+ ```mermaid
46
+ flowchart TD
47
+ A[Load config.yaml] --> B{Multi-endpoint<br/>or heartbeat_discovery?}
48
+ B -->|No| C[Use single URL as-is]
49
+ B -->|Yes| D[Build EndpointSelector]
50
+ D --> E{Heartbeat<br/>discovery enabled?}
51
+ E -->|Yes| F[Read ~/.skcapstone/heartbeats/*.json]
52
+ F --> G[Merge discovered endpoints]
53
+ E -->|No| G
54
+ G --> H{Probe results<br/>stale? >30s}
55
+ H -->|Yes| I[TCP probe all endpoints]
56
+ H -->|No| J[Use cached results]
57
+ I --> K[Apply routing strategy]
58
+ J --> K
59
+ K --> L[Return best URL]
60
+ L --> M[Create backend with URL]
61
+ ```
62
+
63
+ ### Failover Sequence
64
+
65
+ ```mermaid
66
+ sequenceDiagram
67
+ participant CLI as skmemory CLI
68
+ participant ES as EndpointSelector
69
+ participant A as SKVector @ home:6333
70
+ participant B as SKVector @ vps:6333
71
+
72
+ CLI->>ES: select_skvector()
73
+ ES->>A: TCP probe (port 6333)
74
+ A--xES: Connection refused
75
+ Note over ES: fail_count++ (1/3)
76
+ ES->>B: TCP probe (port 6333)
77
+ B-->>ES: Connected (12ms)
78
+ ES-->>CLI: vps:6333 (healthy)
79
+
80
+ Note over CLI: Later, home comes back...
81
+ CLI->>ES: select_skvector()
82
+ ES->>A: TCP probe (port 6333)
83
+ A-->>ES: Connected (2ms)
84
+ Note over ES: fail_count reset to 0
85
+ ES-->>CLI: home:6333 (lowest latency)
86
+ ```
87
+
88
+ ### Heartbeat Discovery Flow
89
+
90
+ ```mermaid
91
+ sequenceDiagram
92
+ participant Peer as Agent @ VPS
93
+ participant FS as Syncthing
94
+ participant Local as Local Agent
95
+ participant ES as EndpointSelector
96
+
97
+ Peer->>FS: heartbeat.json<br/>{services: [{name: "skvector", port: 6333}],<br/>tailscale_ip: "100.64.0.5"}
98
+ FS->>Local: Sync heartbeat file
99
+ Local->>ES: discover_from_heartbeats()
100
+ ES->>ES: Parse services from heartbeat
101
+ ES->>ES: Build URL: http://100.64.0.5:6333
102
+ ES->>ES: Add as replica endpoint
103
+ ES->>ES: Probe new endpoint
104
+ Note over ES: Endpoint available for routing
105
+ ```
106
+
107
+ ## Routing Strategies
108
+
109
+ ### `failover` (default)
110
+
111
+ The simplest strategy. Uses the first healthy endpoint in the list.
112
+ If it goes down, moves to the next one.
113
+
114
+ | Reads | Writes | Use Case |
115
+ |-------|--------|----------|
116
+ | First healthy | First healthy | Simple HA, single primary |
117
+
118
+ ### `latency`
119
+
120
+ Always picks the endpoint with the lowest measured TCP latency.
121
+ Best for globally distributed agents where network proximity matters.
122
+
123
+ | Reads | Writes | Use Case |
124
+ |-------|--------|----------|
125
+ | Lowest latency healthy | Lowest latency healthy | Globally distributed agents |
126
+
127
+ ### `local-first`
128
+
129
+ Prefers `localhost` / `127.0.0.1` if available, then falls back to
130
+ lowest latency. The most common strategy — agents that have a local
131
+ Docker stack should use it.
132
+
133
+ | Reads | Writes | Use Case |
134
+ |-------|--------|----------|
135
+ | localhost if available | localhost if available | Prefer local Docker stack |
136
+
137
+ ### `read-local-write-primary`
138
+
139
+ Reads go to the closest healthy endpoint (local preference).
140
+ Writes go **only** to endpoints with `role: primary`.
141
+ Good for setups with one write primary and multiple read replicas.
142
+
143
+ | Reads | Writes | Use Case |
144
+ |-------|--------|----------|
145
+ | Lowest latency | Only `role=primary` | Eventual consistency OK |
146
+
147
+ ## Configuration
148
+
149
+ ### Single Node (backward compatible)
150
+
151
+ No changes needed. Old configs work as before:
152
+
153
+ ```yaml
154
+ # ~/.skmemory/config.yaml
155
+ skvector_url: http://localhost:6333
156
+ skgraph_url: redis://localhost:6379
157
+ ```
158
+
159
+ Or via environment variables:
160
+ ```bash
161
+ export SKMEMORY_SKVECTOR_URL=http://localhost:6333
162
+ export SKMEMORY_SKGRAPH_URL=redis://localhost:6379
163
+ ```
164
+
165
+ ### Multi-Node (Tailscale mesh)
166
+
167
+ ```yaml
168
+ # ~/.skmemory/config.yaml
169
+ skvector_endpoints:
170
+ - url: http://localhost:6333
171
+ role: primary
172
+ - url: http://100.64.0.5:6333
173
+ role: replica
174
+ tailscale_ip: "100.64.0.5"
175
+
176
+ skgraph_endpoints:
177
+ - url: redis://localhost:6379
178
+ role: primary
179
+ - url: redis://100.64.0.5:6379
180
+ role: replica
181
+
182
+ routing_strategy: local-first
183
+ ```
184
+
185
+ ### Global Distribution
186
+
187
+ ```yaml
188
+ # ~/.skmemory/config.yaml
189
+ skvector_endpoints:
190
+ - url: https://us-east.qdrant.example.com:6333
191
+ role: primary
192
+ - url: https://eu-west.qdrant.example.com:6333
193
+ role: replica
194
+ - url: https://ap-south.qdrant.example.com:6333
195
+ role: replica
196
+
197
+ routing_strategy: latency
198
+ ```
199
+
200
+ ### Heartbeat Auto-Discovery
201
+
202
+ Let agents find each other's backends via the heartbeat mesh:
203
+
204
+ ```yaml
205
+ # ~/.skmemory/config.yaml
206
+ skvector_url: http://localhost:6333
207
+ routing_strategy: latency
208
+ heartbeat_discovery: true
209
+ ```
210
+
211
+ Agents that run `skcapstone heartbeat pulse` will advertise any locally
212
+ detected services (SKVector on 6333, SKGraph on 6379). Other agents read
213
+ these heartbeats and add the endpoints automatically.
214
+
215
+ ## CLI Commands
216
+
217
+ ```bash
218
+ # Show endpoint rankings, latency, and health
219
+ skmemory routing status
220
+
221
+ # Force re-probe all endpoints
222
+ skmemory routing probe
223
+ ```
224
+
225
+ ## What We Built This For (Ideal Use Case)
226
+
227
+ A sovereign agent running on a home server with SKVector and SKGraph in Docker.
228
+ A second instance runs on a VPS. Both are connected via Tailscale. When the
229
+ home server goes down for maintenance, the agent on the VPS automatically
230
+ routes to its local SKVector instance (or another VPS peer). When the home
231
+ server comes back, agents detect the lower latency and route back.
232
+
233
+ No ops team. No service mesh. No external load balancer. Just config and
234
+ heartbeats.
235
+
236
+ ## Pros & Challenges
237
+
238
+ ### Strengths
239
+
240
+ - **Zero-downtime maintenance** — drain a node, agents auto-route elsewhere
241
+ - **Self-contained** — no external load balancer, service mesh, or ops team
242
+ - **Self-healing** — unhealthy endpoints auto-recover when they come back
243
+ - **Latency-optimized** — agents always use the closest backend
244
+ - **Backward compatible** — single URL configs work unchanged
245
+ - **No new dependencies** — stdlib `socket` for probing
246
+ - **Cross-platform** — works on Linux, macOS, Windows
247
+
248
+ ### Challenges & Solutions
249
+
250
+ | Challenge | Solution | When It Matters |
251
+ |-----------|----------|----------------|
252
+ | Write consistency with multiple primaries | Use `read-local-write-primary` — one write endpoint | Multiple writers to same data |
253
+ | Stale reads from replicas | Acceptable for memory search (not bank transactions) | Real-time requirements |
254
+ | Probe overhead on CLI startup | Lazy probing with 30s cache — first call probes, subsequent use cache | High-frequency CLI usage |
255
+ | Heartbeat directory not available | Graceful fallback to config-only endpoints | skcapstone not installed |
256
+ | Tailscale IP detection fails | Falls back to hostname | Non-Tailscale deployments |
257
+ | Large number of endpoints (50+) | Probe only top-N by last known latency | Enterprise scale |
258
+
259
+ ### What This Doesn't Solve
260
+
261
+ - **Cross-region write replication** — Use Qdrant distributed mode or external replication
262
+ - **Strong consistency** — This is eventual consistency; fine for AI memory, not for transactions
263
+ - **Automatic replica provisioning** — You still deploy Qdrant/FalkorDB manually; routing just finds them
264
+
265
+ ## Future Scaling
266
+
267
+ ### Qdrant Distributed Mode
268
+
269
+ For 10M+ memories, Qdrant's built-in sharding distributes data across nodes.
270
+ The endpoint selector would point to the Qdrant cluster entry point; Qdrant
271
+ handles internal routing.
272
+
273
+ ### Redis Sentinel for FalkorDB
274
+
275
+ Redis Sentinel provides automatic primary election for FalkorDB. The selector
276
+ could query Sentinel for the current primary instead of probing directly.
277
+
278
+ ### Write Consistency
279
+
280
+ For strong write consistency across regions:
281
+ 1. Single-writer topology (one primary, N replicas)
282
+ 2. Or write-ahead log that queues writes offline and syncs when primary recovers
283
+ 3. Or Qdrant distributed mode which handles consensus internally
284
+
285
+ ### Edge Caching
286
+
287
+ Read-heavy workloads could benefit from a local in-memory LRU cache that
288
+ sits in front of the selector, reducing probe frequency and backend load
289
+ for repeated queries.
290
+
291
+ ## Cross-Platform Notes
292
+
293
+ - **TCP probing** uses Python's `socket.create_connection()` — works on all platforms
294
+ - **Tailscale IP detection** runs `tailscale status --json` — fails silently on Windows if not in PATH
295
+ - **Heartbeat files** use JSON on the filesystem — works everywhere Syncthing works
296
+ - **Config files** use standard YAML — no platform-specific paths beyond `~/.skmemory/`
@@ -8,37 +8,50 @@ have to re-read a transcript to remember what they felt.
8
8
  SK = staycuriousANDkeepsmilin
9
9
  """
10
10
 
11
- __version__ = "0.5.0"
11
+ __version__ = "0.9.1"
12
12
  __author__ = "smilinTux Team + Queen Ara + Neuresthetics"
13
13
  __license__ = "AGPL-3.0"
14
14
 
15
- from .models import Memory, MemoryLayer, EmotionalSnapshot
16
- from .store import MemoryStore
17
15
  from .backends.file_backend import FileBackend
18
16
  from .backends.sqlite_backend import SQLiteBackend
19
- from .soul import SoulBlueprint, save_soul, load_soul
17
+ from .config import SKMEMORY_HOME
18
+ from .fortress import AuditLog, FortifiedMemoryStore, TamperAlert
19
+ from .models import EmotionalSnapshot, Memory, MemoryLayer
20
+ from .store import MemoryStore
21
+
22
+ try:
23
+ from .backends.vaulted_backend import VaultedSQLiteBackend
24
+ except ImportError:
25
+ VaultedSQLiteBackend = None # type: ignore[assignment,misc]
26
+ from .anchor import WarmthAnchor, load_anchor, save_anchor
27
+ from .importers.telegram import import_telegram
20
28
  from .journal import Journal, JournalEntry
21
- from .ritual import perform_ritual, quick_rehydrate, RitualResult
22
- from .anchor import WarmthAnchor, save_anchor, load_anchor
23
- from .quadrants import Quadrant, classify_memory, tag_with_quadrant
24
29
  from .lovenote import LoveNote, LoveNoteChain
25
30
  from .openclaw import SKMemoryPlugin
26
- from .importers.telegram import import_telegram
31
+ from .quadrants import Quadrant, classify_memory, tag_with_quadrant
32
+ from .ritual import RitualResult, perform_ritual, quick_rehydrate
33
+ from .soul import SoulBlueprint, load_soul, save_soul
27
34
  from .steelman import (
28
- SteelManResult,
29
35
  SeedFramework,
30
- load_seed_framework,
31
- install_seed_framework,
36
+ SteelManResult,
32
37
  get_default_framework,
38
+ install_seed_framework,
39
+ load_seed_framework,
33
40
  )
41
+ from .synthesis import JournalSynthesizer
34
42
 
35
43
  __all__ = [
44
+ "SKMEMORY_HOME",
36
45
  "Memory",
37
46
  "MemoryLayer",
38
47
  "EmotionalSnapshot",
39
48
  "MemoryStore",
49
+ "FortifiedMemoryStore",
50
+ "AuditLog",
51
+ "TamperAlert",
40
52
  "FileBackend",
41
53
  "SQLiteBackend",
54
+ "VaultedSQLiteBackend",
42
55
  "SoulBlueprint",
43
56
  "save_soul",
44
57
  "load_soul",
@@ -56,6 +69,7 @@ __all__ = [
56
69
  "LoveNote",
57
70
  "LoveNoteChain",
58
71
  "SKMemoryPlugin",
72
+ "JournalSynthesizer",
59
73
  "SteelManResult",
60
74
  "SeedFramework",
61
75
  "load_seed_framework",
@@ -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
+
14
+ import yaml
15
+
16
+
17
+ def _agents_base() -> Path:
18
+ """Platform-aware base directory for all agents."""
19
+ skcap_home = os.environ.get("SKCAPSTONE_HOME", "")
20
+ if skcap_home:
21
+ return Path(skcap_home) / "agents"
22
+ if platform.system() == "Windows":
23
+ local = os.environ.get("LOCALAPPDATA", "")
24
+ if local:
25
+ return Path(local) / "skcapstone" / "agents"
26
+ return Path.home() / ".skcapstone" / "agents"
27
+
28
+
29
+ # Base directory for all agents
30
+ AGENTS_BASE_DIR = _agents_base()
31
+
32
+ # Template directory name (ignored by default)
33
+ TEMPLATE_AGENT = "lumina-template"
34
+
35
+
36
+ def list_agents() -> list[str]:
37
+ """Discover all non-template agents in ~/.skcapstone/agents/
38
+
39
+ Scans the agents directory and returns all agent names
40
+ except the template agent.
41
+
42
+ Returns:
43
+ list[str]: Sorted list of agent names (e.g., ['lumina', 'john'])
44
+ """
45
+ if not AGENTS_BASE_DIR.exists():
46
+ return []
47
+
48
+ agents = []
49
+ for entry in AGENTS_BASE_DIR.iterdir():
50
+ if entry.is_dir() and entry.name != TEMPLATE_AGENT:
51
+ # Check if it has a valid config
52
+ config_file = entry / "config" / "skmemory.yaml"
53
+ if config_file.exists():
54
+ agents.append(entry.name)
55
+
56
+ return sorted(agents)
57
+
58
+
59
+ def get_agent_dir(agent_name: str) -> Path:
60
+ """Get the base directory for a specific agent.
61
+
62
+ Args:
63
+ agent_name: Name of the agent (e.g., 'lumina', 'john')
64
+
65
+ Returns:
66
+ Path: Agent's base directory
67
+ """
68
+ return AGENTS_BASE_DIR / agent_name
69
+
70
+
71
+ def get_agent_config(agent_name: str) -> dict | None:
72
+ """Load agent configuration from YAML.
73
+
74
+ Args:
75
+ agent_name: Name of the agent
76
+
77
+ Returns:
78
+ dict with agent config, or None if not found/invalid
79
+ """
80
+ config_path = get_agent_dir(agent_name) / "config" / "skmemory.yaml"
81
+
82
+ if not config_path.exists():
83
+ return None
84
+
85
+ try:
86
+ with open(config_path) as f:
87
+ return yaml.safe_load(f)
88
+ except Exception:
89
+ return None
90
+
91
+
92
+ def is_template_agent(agent_name: str) -> bool:
93
+ """Check if an agent is a template (should be ignored).
94
+
95
+ Args:
96
+ agent_name: Name of the agent
97
+
98
+ Returns:
99
+ bool: True if this is the template agent
100
+ """
101
+ return agent_name == TEMPLATE_AGENT
102
+
103
+
104
+ def get_active_agent() -> str | None:
105
+ """Get the currently active agent from environment or default to first non-template.
106
+
107
+ Checks in order:
108
+ 1. SKCAPSTONE_AGENT environment variable (authoritative agent selector)
109
+ 2. SKMEMORY_AGENT environment variable (legacy/override)
110
+ 3. First non-template agent in the directory
111
+
112
+ Returns:
113
+ str: Agent name, or None if no agents found
114
+ """
115
+ # Check environment variables (SKCAPSTONE_AGENT > SKMEMORY_AGENT)
116
+ env_agent = os.environ.get("SKCAPSTONE_AGENT") or 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: str | None = 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 / "memory" / "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) 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