@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,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())