@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,454 @@
|
|
|
1
|
+
"""SK* skill, MCP server, and OpenClaw plugin auto-registration.
|
|
2
|
+
|
|
3
|
+
Detects the user's development environments (OpenClaw, Claude Code, Cursor,
|
|
4
|
+
VS Code, OpenCode CLI, mcporter) and registers SKILL.md symlinks, MCP
|
|
5
|
+
server entries, and OpenClaw plugin manifests so everything works out-of-the-box.
|
|
6
|
+
|
|
7
|
+
This module is the shared engine — individual packages call it, and
|
|
8
|
+
skcapstone orchestrates registration for the whole suite.
|
|
9
|
+
|
|
10
|
+
Usage (from any SK* package):
|
|
11
|
+
from skmemory.register import register_package, detect_environments
|
|
12
|
+
|
|
13
|
+
register_package(
|
|
14
|
+
name="skmemory",
|
|
15
|
+
skill_md_path=Path(__file__).parent / "SKILL.md",
|
|
16
|
+
mcp_command="skmemory-mcp",
|
|
17
|
+
mcp_args=[],
|
|
18
|
+
openclaw_plugin_path=Path(__file__).parent.parent / "openclaw-plugin" / "src" / "index.ts",
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Environment detection ────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def detect_environments() -> list[str]:
|
|
36
|
+
"""Return list of detected development environments.
|
|
37
|
+
|
|
38
|
+
Checks for:
|
|
39
|
+
- openclaw: OpenClaw CLI installed
|
|
40
|
+
- claude-code: ~/.claude/ directory
|
|
41
|
+
- cursor: ~/.cursor/ directory
|
|
42
|
+
- vscode: ~/.vscode/ or ~/.config/Code/ directory
|
|
43
|
+
- opencode: ~/.opencode/ or opencode binary
|
|
44
|
+
- mcporter: mcporter.json in known locations
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
List of environment identifiers (e.g. ["claude-code", "mcporter"]).
|
|
48
|
+
"""
|
|
49
|
+
home = Path.home()
|
|
50
|
+
envs: list[str] = []
|
|
51
|
+
|
|
52
|
+
# OpenClaw
|
|
53
|
+
openclaw_lib = home / ".npm-global" / "lib" / "node_modules" / "openclaw"
|
|
54
|
+
if openclaw_lib.is_dir() or shutil.which("openclaw"):
|
|
55
|
+
envs.append("openclaw")
|
|
56
|
+
|
|
57
|
+
# Claude Code
|
|
58
|
+
if (home / ".claude").is_dir():
|
|
59
|
+
envs.append("claude-code")
|
|
60
|
+
|
|
61
|
+
# Cursor
|
|
62
|
+
if (home / ".cursor").is_dir():
|
|
63
|
+
envs.append("cursor")
|
|
64
|
+
|
|
65
|
+
# VS Code
|
|
66
|
+
if (home / ".vscode").is_dir() or (home / ".config" / "Code").is_dir():
|
|
67
|
+
envs.append("vscode")
|
|
68
|
+
|
|
69
|
+
# OpenCode CLI
|
|
70
|
+
if (home / ".opencode").is_dir() or shutil.which("opencode"):
|
|
71
|
+
envs.append("opencode")
|
|
72
|
+
|
|
73
|
+
# mcporter
|
|
74
|
+
mcporter_paths = [
|
|
75
|
+
home / "clawd" / "config" / "mcporter.json",
|
|
76
|
+
home / ".config" / "mcporter" / "mcporter.json",
|
|
77
|
+
]
|
|
78
|
+
for p in mcporter_paths:
|
|
79
|
+
if p.is_file():
|
|
80
|
+
envs.append("mcporter")
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
return envs
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Skill registration ───────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def register_skill(
|
|
90
|
+
name: str,
|
|
91
|
+
skill_md_path: Path,
|
|
92
|
+
workspace: Optional[Path] = None,
|
|
93
|
+
) -> dict:
|
|
94
|
+
"""Register a skill by symlinking its SKILL.md into the workspace skills dir.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
name: Package/skill name (e.g. "skmemory").
|
|
98
|
+
skill_md_path: Absolute path to the source SKILL.md file.
|
|
99
|
+
workspace: Workspace root (defaults to ~/clawd/).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict with 'skill' key describing what was done.
|
|
103
|
+
"""
|
|
104
|
+
if workspace is None:
|
|
105
|
+
workspace = Path.home() / "clawd"
|
|
106
|
+
|
|
107
|
+
skill_dir = workspace / "skills" / name
|
|
108
|
+
target = skill_dir / "SKILL.md"
|
|
109
|
+
|
|
110
|
+
result: dict = {"name": name, "action": "skip", "path": str(target)}
|
|
111
|
+
|
|
112
|
+
if not skill_md_path.exists():
|
|
113
|
+
result["action"] = "error"
|
|
114
|
+
result["error"] = f"Source SKILL.md not found: {skill_md_path}"
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
# If target already exists and is correct, skip
|
|
120
|
+
if target.is_symlink():
|
|
121
|
+
try:
|
|
122
|
+
if target.resolve() == skill_md_path.resolve():
|
|
123
|
+
result["action"] = "exists"
|
|
124
|
+
return result
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
# Remove broken or wrong symlink
|
|
128
|
+
target.unlink()
|
|
129
|
+
|
|
130
|
+
if target.exists():
|
|
131
|
+
result["action"] = "exists"
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
# Create symlink — prefer relative path
|
|
135
|
+
try:
|
|
136
|
+
rel = os.path.relpath(skill_md_path, skill_dir)
|
|
137
|
+
target.symlink_to(rel)
|
|
138
|
+
except (ValueError, OSError):
|
|
139
|
+
target.symlink_to(skill_md_path)
|
|
140
|
+
|
|
141
|
+
result["action"] = "created"
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── MCP registration ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _read_json(path: Path) -> dict:
|
|
149
|
+
"""Read a JSON file, returning empty dict on error."""
|
|
150
|
+
try:
|
|
151
|
+
return json.loads(path.read_text())
|
|
152
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
153
|
+
return {}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _write_json(path: Path, data: dict) -> None:
|
|
157
|
+
"""Write dict as pretty-printed JSON."""
|
|
158
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _upsert_mcp_entry(
|
|
163
|
+
path: Path,
|
|
164
|
+
name: str,
|
|
165
|
+
command: str,
|
|
166
|
+
args: list,
|
|
167
|
+
env: Optional[dict] = None,
|
|
168
|
+
) -> str:
|
|
169
|
+
"""Add or update an MCP server entry in a JSON config file.
|
|
170
|
+
|
|
171
|
+
Expects the file to have a top-level "mcpServers" key.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
path: Path to the JSON config file.
|
|
175
|
+
name: Server name.
|
|
176
|
+
command: Command to run.
|
|
177
|
+
args: Command arguments.
|
|
178
|
+
env: Optional environment variables.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
"created", "updated", or "exists".
|
|
182
|
+
"""
|
|
183
|
+
data = _read_json(path)
|
|
184
|
+
servers = data.setdefault("mcpServers", {})
|
|
185
|
+
|
|
186
|
+
entry: dict = {"command": command, "args": args}
|
|
187
|
+
if env:
|
|
188
|
+
entry["env"] = env
|
|
189
|
+
|
|
190
|
+
if name in servers:
|
|
191
|
+
if servers[name] == entry:
|
|
192
|
+
return "exists"
|
|
193
|
+
servers[name] = entry
|
|
194
|
+
_write_json(path, data)
|
|
195
|
+
return "updated"
|
|
196
|
+
|
|
197
|
+
servers[name] = entry
|
|
198
|
+
_write_json(path, data)
|
|
199
|
+
return "created"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def register_mcp(
|
|
203
|
+
name: str,
|
|
204
|
+
command: str,
|
|
205
|
+
args: list,
|
|
206
|
+
env: Optional[dict] = None,
|
|
207
|
+
environments: Optional[list[str]] = None,
|
|
208
|
+
) -> dict:
|
|
209
|
+
"""Register an MCP server in detected (or specified) environments.
|
|
210
|
+
|
|
211
|
+
Writes to:
|
|
212
|
+
- claude-code: ~/.claude/mcp.json
|
|
213
|
+
- cursor: ~/.cursor/mcp.json
|
|
214
|
+
- vscode: (skipped — requires workspace .vscode/)
|
|
215
|
+
- opencode: ~/.opencode/mcp.json
|
|
216
|
+
- mcporter: ~/clawd/config/mcporter.json or ~/.config/mcporter/mcporter.json
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
name: Server name (e.g. "skmemory").
|
|
220
|
+
command: Command to run (e.g. "skmemory-mcp").
|
|
221
|
+
args: Command arguments.
|
|
222
|
+
env: Optional environment variables.
|
|
223
|
+
environments: Target environments. If None, auto-detect.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Dict mapping environment -> action taken.
|
|
227
|
+
"""
|
|
228
|
+
if environments is None:
|
|
229
|
+
environments = detect_environments()
|
|
230
|
+
|
|
231
|
+
home = Path.home()
|
|
232
|
+
results: dict = {}
|
|
233
|
+
|
|
234
|
+
env_to_path: dict[str, Path] = {
|
|
235
|
+
"claude-code": home / ".claude" / "mcp.json",
|
|
236
|
+
"cursor": home / ".cursor" / "mcp.json",
|
|
237
|
+
"opencode": home / ".opencode" / "mcp.json",
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# OpenClaw: uses mcporter for MCP — no native mcpServers key in
|
|
241
|
+
# openclaw.json. MCP registration for OpenClaw happens via mcporter.
|
|
242
|
+
|
|
243
|
+
# mcporter: find first existing file
|
|
244
|
+
mcporter_candidates = [
|
|
245
|
+
home / "clawd" / "config" / "mcporter.json",
|
|
246
|
+
home / ".config" / "mcporter" / "mcporter.json",
|
|
247
|
+
]
|
|
248
|
+
for p in mcporter_candidates:
|
|
249
|
+
if p.is_file():
|
|
250
|
+
env_to_path["mcporter"] = p
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
for env_name in environments:
|
|
254
|
+
path = env_to_path.get(env_name)
|
|
255
|
+
if path is None:
|
|
256
|
+
continue
|
|
257
|
+
try:
|
|
258
|
+
action = _upsert_mcp_entry(path, name, command, args, env)
|
|
259
|
+
results[env_name] = action
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
results[env_name] = f"error: {exc}"
|
|
262
|
+
|
|
263
|
+
return results
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ── OpenClaw plugin registration ─────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get_openclaw_json_path() -> Path:
|
|
270
|
+
"""Return path to ~/.openclaw/openclaw.json."""
|
|
271
|
+
return Path.home() / ".openclaw" / "openclaw.json"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _is_openclaw_plugin_registered(plugin_path: Path) -> bool:
|
|
275
|
+
"""Check if plugin_path is already in openclaw.json plugins.load.paths."""
|
|
276
|
+
oc_json = _get_openclaw_json_path()
|
|
277
|
+
if not oc_json.is_file():
|
|
278
|
+
return False
|
|
279
|
+
data = _read_json(oc_json)
|
|
280
|
+
paths = data.get("plugins", {}).get("load", {}).get("paths", [])
|
|
281
|
+
resolved = str(plugin_path.resolve())
|
|
282
|
+
return any(str(Path(p).resolve()) == resolved for p in paths)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _upsert_openclaw_plugin_path(plugin_id: str, plugin_path: Path) -> None:
|
|
286
|
+
"""Add plugin to plugins.load.paths and plugins.installs in openclaw.json."""
|
|
287
|
+
oc_json = _get_openclaw_json_path()
|
|
288
|
+
data = _read_json(oc_json)
|
|
289
|
+
|
|
290
|
+
plugins = data.setdefault("plugins", {})
|
|
291
|
+
load = plugins.setdefault("load", {})
|
|
292
|
+
paths = load.setdefault("paths", [])
|
|
293
|
+
|
|
294
|
+
resolved = str(plugin_path.resolve())
|
|
295
|
+
if resolved not in paths:
|
|
296
|
+
paths.append(resolved)
|
|
297
|
+
|
|
298
|
+
installs = plugins.setdefault("installs", {})
|
|
299
|
+
# Point to the plugin directory (parent of src/index.ts)
|
|
300
|
+
plugin_dir = str(plugin_path.resolve().parent.parent)
|
|
301
|
+
installs[plugin_id] = {"path": plugin_dir, "linked": True}
|
|
302
|
+
|
|
303
|
+
_write_json(oc_json, data)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _ensure_openclaw_plugin_enabled(plugin_id: str) -> None:
|
|
307
|
+
"""Set plugins.entries.<plugin_id>.enabled = true (idempotent)."""
|
|
308
|
+
oc_json = _get_openclaw_json_path()
|
|
309
|
+
data = _read_json(oc_json)
|
|
310
|
+
|
|
311
|
+
entries = data.setdefault("plugins", {}).setdefault("entries", {})
|
|
312
|
+
entry = entries.setdefault(plugin_id, {})
|
|
313
|
+
|
|
314
|
+
if entry.get("enabled") is True:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
entry["enabled"] = True
|
|
318
|
+
_write_json(oc_json, data)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _detect_plugin_id(plugin_path: Path, fallback: str) -> str:
|
|
322
|
+
"""Read plugin id from openclaw.plugin.json manifest."""
|
|
323
|
+
manifest = plugin_path.parent / "openclaw.plugin.json"
|
|
324
|
+
if manifest.is_file():
|
|
325
|
+
try:
|
|
326
|
+
data = json.loads(manifest.read_text())
|
|
327
|
+
return data.get("id", fallback)
|
|
328
|
+
except (json.JSONDecodeError, KeyError):
|
|
329
|
+
pass
|
|
330
|
+
return fallback
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def register_openclaw_plugin(
|
|
334
|
+
plugin_id: str,
|
|
335
|
+
plugin_path: Path,
|
|
336
|
+
dry_run: bool = False,
|
|
337
|
+
) -> str:
|
|
338
|
+
"""Register an OpenClaw plugin. Returns 'exists', 'created', or 'error:...'.
|
|
339
|
+
|
|
340
|
+
1. Check _is_openclaw_plugin_registered() -> skip if already registered
|
|
341
|
+
2. Try `openclaw plugins install --link <path>` via subprocess
|
|
342
|
+
3. Fallback: write openclaw.json directly if CLI fails
|
|
343
|
+
4. _ensure_openclaw_plugin_enabled()
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
plugin_id: Plugin identifier (e.g. "skmemory").
|
|
347
|
+
plugin_path: Path to the plugin entry point (e.g. src/index.ts).
|
|
348
|
+
dry_run: If True, only report what would be done.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Action taken: "exists", "created", "dry-run", or "error:...".
|
|
352
|
+
"""
|
|
353
|
+
if dry_run:
|
|
354
|
+
return "dry-run"
|
|
355
|
+
|
|
356
|
+
if not plugin_path.exists():
|
|
357
|
+
return f"error: plugin not found: {plugin_path}"
|
|
358
|
+
|
|
359
|
+
# Detect actual plugin ID from manifest
|
|
360
|
+
actual_id = _detect_plugin_id(plugin_path, plugin_id)
|
|
361
|
+
|
|
362
|
+
if _is_openclaw_plugin_registered(plugin_path):
|
|
363
|
+
_ensure_openclaw_plugin_enabled(actual_id)
|
|
364
|
+
return "exists"
|
|
365
|
+
|
|
366
|
+
# Try CLI first
|
|
367
|
+
plugin_dir = str(plugin_path.resolve().parent.parent)
|
|
368
|
+
try:
|
|
369
|
+
subprocess.run(
|
|
370
|
+
["openclaw", "plugins", "install", "--link", plugin_dir],
|
|
371
|
+
capture_output=True,
|
|
372
|
+
text=True,
|
|
373
|
+
timeout=15,
|
|
374
|
+
)
|
|
375
|
+
if _is_openclaw_plugin_registered(plugin_path):
|
|
376
|
+
_ensure_openclaw_plugin_enabled(actual_id)
|
|
377
|
+
return "created"
|
|
378
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
# Fallback: write directly
|
|
382
|
+
try:
|
|
383
|
+
_upsert_openclaw_plugin_path(actual_id, plugin_path)
|
|
384
|
+
_ensure_openclaw_plugin_enabled(actual_id)
|
|
385
|
+
return "created"
|
|
386
|
+
except Exception as exc:
|
|
387
|
+
return f"error: {exc}"
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ── High-level package registration ──────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def register_package(
|
|
394
|
+
name: str,
|
|
395
|
+
skill_md_path: Path,
|
|
396
|
+
mcp_command: Optional[str] = None,
|
|
397
|
+
mcp_args: Optional[list] = None,
|
|
398
|
+
mcp_env: Optional[dict] = None,
|
|
399
|
+
openclaw_plugin_path: Optional[Path] = None,
|
|
400
|
+
workspace: Optional[Path] = None,
|
|
401
|
+
environments: Optional[list[str]] = None,
|
|
402
|
+
dry_run: bool = False,
|
|
403
|
+
) -> dict:
|
|
404
|
+
"""Register a skill, MCP server, and OpenClaw plugin in all detected environments.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
name: Package/skill name.
|
|
408
|
+
skill_md_path: Path to the SKILL.md file.
|
|
409
|
+
mcp_command: MCP server command (None to skip MCP registration).
|
|
410
|
+
mcp_args: MCP server arguments.
|
|
411
|
+
mcp_env: MCP server environment variables.
|
|
412
|
+
openclaw_plugin_path: Path to OpenClaw plugin entry (e.g. src/index.ts).
|
|
413
|
+
workspace: Workspace root for skill symlinks.
|
|
414
|
+
environments: Target environments (auto-detect if None).
|
|
415
|
+
dry_run: If True, only report what would be done.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Dict with 'skill', 'mcp', and 'openclaw_plugin' results.
|
|
419
|
+
"""
|
|
420
|
+
if environments is None:
|
|
421
|
+
environments = detect_environments()
|
|
422
|
+
|
|
423
|
+
result: dict = {"name": name, "environments": environments}
|
|
424
|
+
|
|
425
|
+
if dry_run:
|
|
426
|
+
result["skill"] = {"action": "dry-run", "path": str(
|
|
427
|
+
(workspace or Path.home() / "clawd") / "skills" / name / "SKILL.md"
|
|
428
|
+
)}
|
|
429
|
+
if mcp_command:
|
|
430
|
+
result["mcp"] = {env: "dry-run" for env in environments}
|
|
431
|
+
if openclaw_plugin_path and "openclaw" in environments:
|
|
432
|
+
result["openclaw_plugin"] = "dry-run"
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
# Register skill
|
|
436
|
+
result["skill"] = register_skill(name, skill_md_path, workspace)
|
|
437
|
+
|
|
438
|
+
# Register MCP server
|
|
439
|
+
if mcp_command is not None:
|
|
440
|
+
result["mcp"] = register_mcp(
|
|
441
|
+
name,
|
|
442
|
+
mcp_command,
|
|
443
|
+
mcp_args or [],
|
|
444
|
+
env=mcp_env,
|
|
445
|
+
environments=environments,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Register OpenClaw plugin
|
|
449
|
+
if openclaw_plugin_path is not None and "openclaw" in environments:
|
|
450
|
+
result["openclaw_plugin"] = register_openclaw_plugin(
|
|
451
|
+
name, openclaw_plugin_path, dry_run=dry_run,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return result
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Server Registration for SKMemory/SKCapstone
|
|
4
|
+
|
|
5
|
+
Auto-registers MCP servers with OpenCode, Claude Code, and OpenClaw.
|
|
6
|
+
Usage:
|
|
7
|
+
python -m skmemory.register_mcp
|
|
8
|
+
python -m skmemory.register_mcp --env opencode
|
|
9
|
+
python -m skmemory.register_mcp --env claude
|
|
10
|
+
python -m skmemory.register_mcp --env openclaw
|
|
11
|
+
python -m skmemory.register_mcp --agent lumina
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_agent_name() -> str:
|
|
25
|
+
"""Get agent name from environment or default."""
|
|
26
|
+
return os.environ.get("SKMEMORY_AGENT") or os.environ.get("SKCAPSTONE_AGENT") or "lumina"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_opencode(agent: str, dry_run: bool = False) -> bool:
|
|
30
|
+
"""Register SKMemory with OpenCode."""
|
|
31
|
+
config_dir = Path.home() / ".opencode"
|
|
32
|
+
config_file = config_dir / "mcp.json"
|
|
33
|
+
|
|
34
|
+
if dry_run:
|
|
35
|
+
print(f"[DRY-RUN] Would create: {config_file}")
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
# Build MCP config
|
|
41
|
+
config = {
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"skmemory": {
|
|
44
|
+
"command": "python",
|
|
45
|
+
"args": ["-m", "skmemory.mcp_server"],
|
|
46
|
+
"env": {
|
|
47
|
+
"SKMEMORY_AGENT": agent,
|
|
48
|
+
"SKMEMORY_HOME": str(Path.home() / ".skcapstone" / "agents" / agent),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"skcapstone": {
|
|
52
|
+
"command": "python",
|
|
53
|
+
"args": ["-m", "skcapstone.mcp_server"],
|
|
54
|
+
"env": {"SKCAPSTONE_AGENT": agent},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"skills": [
|
|
58
|
+
{
|
|
59
|
+
"name": "skmemory",
|
|
60
|
+
"path": str(Path.home() / "clawd" / "skcapstone-repos" / "skmemory" / "SKILL.md"),
|
|
61
|
+
},
|
|
62
|
+
{"name": "skcapstone", "path": str(Path.home() / "clawd" / "skcapstone" / "SKILL.md")},
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
with open(config_file, "w") as f:
|
|
67
|
+
json.dump(config, f, indent=2)
|
|
68
|
+
|
|
69
|
+
print(f"✓ Registered with OpenCode: {config_file}")
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def register_claude(agent: str, dry_run: bool = False) -> bool:
|
|
74
|
+
"""Register SKMemory with Claude Code."""
|
|
75
|
+
config_file = Path.home() / ".config" / "claude" / "claude_desktop_config.json"
|
|
76
|
+
|
|
77
|
+
if dry_run:
|
|
78
|
+
print(f"[DRY-RUN] Would create: {config_file}")
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
config = {
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"skmemory": {
|
|
86
|
+
"command": "python",
|
|
87
|
+
"args": ["-m", "skmemory.mcp_server"],
|
|
88
|
+
"env": {
|
|
89
|
+
"SKMEMORY_AGENT": agent,
|
|
90
|
+
"SKMEMORY_HOME": str(Path.home() / ".skcapstone" / "agents" / agent),
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
"skcapstone": {
|
|
94
|
+
"command": "python",
|
|
95
|
+
"args": ["-m", "skcapstone.mcp_server"],
|
|
96
|
+
"env": {"SKCAPSTONE_AGENT": agent},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
with open(config_file, "w") as f:
|
|
102
|
+
json.dump(config, f, indent=2)
|
|
103
|
+
|
|
104
|
+
print(f"✓ Registered with Claude Code: {config_file}")
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def register_openclaw(agent: str, dry_run: bool = False) -> bool:
|
|
109
|
+
"""Register SKMemory with OpenClaw (via plugin)."""
|
|
110
|
+
config_file = Path.home() / ".openclaw" / "openclaw.json"
|
|
111
|
+
|
|
112
|
+
if dry_run:
|
|
113
|
+
print(f"[DRY-RUN] Would update: {config_file}")
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
# Read existing config
|
|
117
|
+
if config_file.exists():
|
|
118
|
+
with open(config_file, "r") as f:
|
|
119
|
+
config = json.load(f)
|
|
120
|
+
else:
|
|
121
|
+
config = {}
|
|
122
|
+
|
|
123
|
+
# Add plugins
|
|
124
|
+
config.setdefault("plugins", {})
|
|
125
|
+
config["plugins"]["skmemory"] = {
|
|
126
|
+
"enabled": True,
|
|
127
|
+
"path": str(Path.home() / "clawd" / "skcapstone-repos" / "skmemory" / "openclaw-plugin"),
|
|
128
|
+
}
|
|
129
|
+
config["plugins"]["skcapstone"] = {
|
|
130
|
+
"enabled": True,
|
|
131
|
+
"path": str(Path.home() / "clawd" / "skcapstone" / "openclaw-plugin"),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
with open(config_file, "w") as f:
|
|
135
|
+
json.dump(config, f, indent=2)
|
|
136
|
+
|
|
137
|
+
print(f"✓ Registered with OpenClaw: {config_file}")
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main():
|
|
142
|
+
parser = argparse.ArgumentParser(description="Register SKMemory MCP servers with AI clients")
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--env",
|
|
145
|
+
choices=["opencode", "claude", "openclaw", "all"],
|
|
146
|
+
default="all",
|
|
147
|
+
help="Target environment (default: all)",
|
|
148
|
+
)
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"--agent", default=None, help="Agent name (default: SKMEMORY_AGENT env var or 'lumina')"
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--dry-run", action="store_true", help="Show what would be done without making changes"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
args = parser.parse_args()
|
|
157
|
+
|
|
158
|
+
agent = args.agent or get_agent_name()
|
|
159
|
+
|
|
160
|
+
print(f"Registering MCP servers for agent: {agent}")
|
|
161
|
+
if args.dry_run:
|
|
162
|
+
print("[DRY-RUN MODE - No changes will be made]")
|
|
163
|
+
print()
|
|
164
|
+
|
|
165
|
+
results = []
|
|
166
|
+
|
|
167
|
+
if args.env in ("opencode", "all"):
|
|
168
|
+
results.append(("OpenCode", register_opencode(agent, args.dry_run)))
|
|
169
|
+
|
|
170
|
+
if args.env in ("claude", "all"):
|
|
171
|
+
results.append(("Claude Code", register_claude(agent, args.dry_run)))
|
|
172
|
+
|
|
173
|
+
if args.env in ("openclaw", "all"):
|
|
174
|
+
results.append(("OpenClaw", register_openclaw(agent, args.dry_run)))
|
|
175
|
+
|
|
176
|
+
print()
|
|
177
|
+
print("=" * 50)
|
|
178
|
+
print("Registration Summary")
|
|
179
|
+
print("=" * 50)
|
|
180
|
+
|
|
181
|
+
for name, success in results:
|
|
182
|
+
status = "✓" if success else "✗"
|
|
183
|
+
print(f"{status} {name}")
|
|
184
|
+
|
|
185
|
+
if all(success for _, success in results):
|
|
186
|
+
print("\n✓ All MCP servers registered successfully!")
|
|
187
|
+
print("\nNext steps:")
|
|
188
|
+
print(" 1. Restart your AI client")
|
|
189
|
+
print(" 2. Verify with: skmemory show-context")
|
|
190
|
+
return 0
|
|
191
|
+
else:
|
|
192
|
+
print("\n✗ Some registrations failed")
|
|
193
|
+
return 1
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
sys.exit(main())
|