@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,920 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SKMemory Setup Wizard — Docker orchestration for SKVector & SKGraph.
|
|
3
|
+
|
|
4
|
+
Detects Docker, deploys containers via ``docker compose``, verifies
|
|
5
|
+
health, installs Python dependencies, and persists configuration so
|
|
6
|
+
all future ``skmemory`` commands auto-connect.
|
|
7
|
+
|
|
8
|
+
Cross-platform: macOS, Linux, Windows.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import shutil
|
|
16
|
+
import socket
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Callable, Optional
|
|
26
|
+
|
|
27
|
+
from .config import CONFIG_DIR, SKMemoryConfig, load_config, save_config
|
|
28
|
+
|
|
29
|
+
# ─────────────────────────────────────────────────────────
|
|
30
|
+
# Docker Desktop direct download URLs
|
|
31
|
+
# ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
DOCKER_DESKTOP_DOWNLOAD = {
|
|
34
|
+
"Windows": (
|
|
35
|
+
"https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe"
|
|
36
|
+
),
|
|
37
|
+
"Darwin": "https://docs.docker.com/desktop/install/mac-install/",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# ─────────────────────────────────────────────────────────
|
|
41
|
+
# Platform detection
|
|
42
|
+
# ─────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
DOCKER_COMPOSE_YAML = """\
|
|
45
|
+
# Auto-generated by skmemory setup wizard
|
|
46
|
+
# Source: skmemory/docker-compose.yml
|
|
47
|
+
|
|
48
|
+
services:
|
|
49
|
+
skvector:
|
|
50
|
+
image: qdrant/qdrant:latest
|
|
51
|
+
container_name: skmemory-skvector
|
|
52
|
+
ports:
|
|
53
|
+
- "6333:6333"
|
|
54
|
+
- "6334:6334"
|
|
55
|
+
volumes:
|
|
56
|
+
- skvector_data:/qdrant/storage
|
|
57
|
+
environment:
|
|
58
|
+
- QDRANT__SERVICE__GRPC_PORT=6334
|
|
59
|
+
restart: unless-stopped
|
|
60
|
+
deploy:
|
|
61
|
+
resources:
|
|
62
|
+
limits:
|
|
63
|
+
memory: 512M
|
|
64
|
+
|
|
65
|
+
skgraph:
|
|
66
|
+
image: falkordb/falkordb:latest
|
|
67
|
+
container_name: skmemory-skgraph
|
|
68
|
+
ports:
|
|
69
|
+
- "6379:6379"
|
|
70
|
+
volumes:
|
|
71
|
+
- skgraph_data:/data
|
|
72
|
+
restart: unless-stopped
|
|
73
|
+
deploy:
|
|
74
|
+
resources:
|
|
75
|
+
limits:
|
|
76
|
+
memory: 256M
|
|
77
|
+
|
|
78
|
+
volumes:
|
|
79
|
+
skvector_data:
|
|
80
|
+
driver: local
|
|
81
|
+
skgraph_data:
|
|
82
|
+
driver: local
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class PlatformInfo:
|
|
88
|
+
"""Platform and Docker availability snapshot."""
|
|
89
|
+
|
|
90
|
+
os_name: str = ""
|
|
91
|
+
docker_available: bool = False
|
|
92
|
+
compose_available: bool = False
|
|
93
|
+
compose_legacy: bool = False
|
|
94
|
+
docker_version: str = ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def detect_platform() -> PlatformInfo:
|
|
98
|
+
"""Detect OS and Docker/Compose availability.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
PlatformInfo with detected values.
|
|
102
|
+
"""
|
|
103
|
+
info = PlatformInfo(os_name=platform.system())
|
|
104
|
+
|
|
105
|
+
# Docker binary
|
|
106
|
+
if not shutil.which("docker"):
|
|
107
|
+
return info
|
|
108
|
+
|
|
109
|
+
# Docker daemon running?
|
|
110
|
+
try:
|
|
111
|
+
result = subprocess.run(
|
|
112
|
+
["docker", "info"],
|
|
113
|
+
capture_output=True,
|
|
114
|
+
timeout=10,
|
|
115
|
+
)
|
|
116
|
+
if result.returncode != 0:
|
|
117
|
+
return info
|
|
118
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
119
|
+
return info
|
|
120
|
+
|
|
121
|
+
info.docker_available = True
|
|
122
|
+
|
|
123
|
+
# Docker version
|
|
124
|
+
try:
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
["docker", "version", "--format", "{{.Server.Version}}"],
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
timeout=10,
|
|
130
|
+
)
|
|
131
|
+
if result.returncode == 0:
|
|
132
|
+
info.docker_version = result.stdout.strip()
|
|
133
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Compose v2 (docker compose)
|
|
137
|
+
try:
|
|
138
|
+
result = subprocess.run(
|
|
139
|
+
["docker", "compose", "version"],
|
|
140
|
+
capture_output=True,
|
|
141
|
+
text=True,
|
|
142
|
+
timeout=10,
|
|
143
|
+
)
|
|
144
|
+
if result.returncode == 0:
|
|
145
|
+
info.compose_available = True
|
|
146
|
+
info.compose_legacy = False
|
|
147
|
+
return info
|
|
148
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
# Compose v1 (docker-compose)
|
|
152
|
+
try:
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
["docker-compose", "--version"],
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True,
|
|
157
|
+
timeout=10,
|
|
158
|
+
)
|
|
159
|
+
if result.returncode == 0:
|
|
160
|
+
info.compose_available = True
|
|
161
|
+
info.compose_legacy = True
|
|
162
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
return info
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_docker_install_instructions(os_name: str) -> str:
|
|
169
|
+
"""Return platform-specific Docker install instructions.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
os_name: Result of ``platform.system()`` (Linux, Darwin, Windows).
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Human-readable install guide.
|
|
176
|
+
"""
|
|
177
|
+
if os_name == "Linux":
|
|
178
|
+
return (
|
|
179
|
+
"Install Docker Engine:\n"
|
|
180
|
+
" curl -fsSL https://get.docker.com | sh\n"
|
|
181
|
+
" sudo usermod -aG docker $USER\n"
|
|
182
|
+
" (log out and back in, then retry)"
|
|
183
|
+
)
|
|
184
|
+
elif os_name == "Darwin":
|
|
185
|
+
return (
|
|
186
|
+
"Install Docker Desktop for macOS:\n"
|
|
187
|
+
" https://docs.docker.com/desktop/install/mac-install/\n"
|
|
188
|
+
" Or: brew install --cask docker"
|
|
189
|
+
)
|
|
190
|
+
elif os_name == "Windows":
|
|
191
|
+
return (
|
|
192
|
+
"Install Docker Desktop for Windows:\n"
|
|
193
|
+
" https://docs.docker.com/desktop/install/windows-install/\n"
|
|
194
|
+
" WSL2 backend recommended."
|
|
195
|
+
)
|
|
196
|
+
return "Install Docker: https://docs.docker.com/get-docker/"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ─────────────────────────────────────────────────────────
|
|
200
|
+
# Docker installation helpers
|
|
201
|
+
# ─────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _open_url_in_browser(url: str) -> None:
|
|
205
|
+
"""Open *url* in the default browser — best-effort, never raises."""
|
|
206
|
+
try:
|
|
207
|
+
os_name = platform.system()
|
|
208
|
+
if os_name == "Windows":
|
|
209
|
+
os.system(f'start "" "{url}"')
|
|
210
|
+
elif os_name == "Darwin":
|
|
211
|
+
subprocess.run(["open", url], timeout=5, check=False)
|
|
212
|
+
else:
|
|
213
|
+
subprocess.run(["xdg-open", url], timeout=5,
|
|
214
|
+
capture_output=True, check=False)
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _try_install_docker_linux(echo: Callable, input_fn: Callable) -> bool:
|
|
220
|
+
"""Offer to run the Docker convenience install script on Linux.
|
|
221
|
+
|
|
222
|
+
Returns True only if Docker is detected running after the script.
|
|
223
|
+
"""
|
|
224
|
+
echo("")
|
|
225
|
+
echo("Docker is required to run SKVector and SKGraph locally.")
|
|
226
|
+
echo("")
|
|
227
|
+
echo("Fastest install on Linux:")
|
|
228
|
+
echo(" curl -fsSL https://get.docker.com | sh")
|
|
229
|
+
echo(" sudo usermod -aG docker $USER (then log out and back in)")
|
|
230
|
+
echo("")
|
|
231
|
+
ans = input_fn("Run the Docker install script now? [y/N] ").strip().lower()
|
|
232
|
+
if ans not in ("y", "yes"):
|
|
233
|
+
echo("")
|
|
234
|
+
echo("Run it yourself when ready, then come back:")
|
|
235
|
+
echo(" skmemory setup wizard")
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
echo("Running Docker install script (this may take a minute)...")
|
|
239
|
+
try:
|
|
240
|
+
result = subprocess.run(
|
|
241
|
+
"curl -fsSL https://get.docker.com | sh",
|
|
242
|
+
shell=True,
|
|
243
|
+
timeout=300,
|
|
244
|
+
)
|
|
245
|
+
if result.returncode != 0:
|
|
246
|
+
echo("Docker install script failed. Please install manually.")
|
|
247
|
+
return False
|
|
248
|
+
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
249
|
+
echo(f"Installation error: {exc}")
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
echo("")
|
|
253
|
+
echo("Docker installed!")
|
|
254
|
+
echo("Add yourself to the docker group, then log out/in and re-run the wizard:")
|
|
255
|
+
echo(" sudo usermod -aG docker $USER")
|
|
256
|
+
echo(" skmemory setup wizard")
|
|
257
|
+
# The current shell session needs a new group — require re-run.
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _try_install_docker_macos(echo: Callable, input_fn: Callable) -> bool:
|
|
262
|
+
"""Offer Homebrew install or open Docker Desktop download on macOS.
|
|
263
|
+
|
|
264
|
+
Returns True only if Docker is detected running after install.
|
|
265
|
+
"""
|
|
266
|
+
echo("")
|
|
267
|
+
echo("Docker Desktop is required to run SKVector and SKGraph locally.")
|
|
268
|
+
echo("")
|
|
269
|
+
|
|
270
|
+
has_brew = shutil.which("brew") is not None
|
|
271
|
+
if has_brew:
|
|
272
|
+
echo("Option 1 — Install via Homebrew (recommended):")
|
|
273
|
+
echo(" brew install --cask docker")
|
|
274
|
+
echo("")
|
|
275
|
+
echo("Option 2 — Download Docker Desktop:")
|
|
276
|
+
echo(f" {DOCKER_DESKTOP_DOWNLOAD['Darwin']}")
|
|
277
|
+
echo("")
|
|
278
|
+
ans = input_fn("Install via Homebrew now? [Y/n] ").strip().lower()
|
|
279
|
+
if ans in ("", "y", "yes"):
|
|
280
|
+
echo("Running: brew install --cask docker (may take a few minutes)")
|
|
281
|
+
try:
|
|
282
|
+
subprocess.run(
|
|
283
|
+
["brew", "install", "--cask", "docker"],
|
|
284
|
+
timeout=600,
|
|
285
|
+
check=False,
|
|
286
|
+
)
|
|
287
|
+
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
288
|
+
echo(f"Homebrew install error: {exc}")
|
|
289
|
+
echo("")
|
|
290
|
+
echo("Docker Desktop installed! Launch it from Applications,")
|
|
291
|
+
echo("wait for the whale icon in the menu bar, then run:")
|
|
292
|
+
echo(" skmemory setup wizard")
|
|
293
|
+
return False
|
|
294
|
+
# User said no to Homebrew — fall through to browser option.
|
|
295
|
+
|
|
296
|
+
url = DOCKER_DESKTOP_DOWNLOAD["Darwin"]
|
|
297
|
+
echo(f"Docker Desktop for macOS: {url}")
|
|
298
|
+
echo("")
|
|
299
|
+
ans = input_fn("Open download page in your browser? [Y/n] ").strip().lower()
|
|
300
|
+
if ans in ("", "y", "yes"):
|
|
301
|
+
_open_url_in_browser(url)
|
|
302
|
+
echo("Download page opened.")
|
|
303
|
+
echo("After installing Docker Desktop, run: skmemory setup wizard")
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _try_install_docker_windows(echo: Callable, input_fn: Callable) -> bool:
|
|
308
|
+
"""Offer winget install or open Docker Desktop download on Windows.
|
|
309
|
+
|
|
310
|
+
Returns True only if Docker is detected running after install.
|
|
311
|
+
"""
|
|
312
|
+
echo("")
|
|
313
|
+
echo("Docker Desktop for Windows is required to run SKVector and SKGraph locally.")
|
|
314
|
+
echo("WSL2 backend is recommended (selected automatically during install).")
|
|
315
|
+
echo("")
|
|
316
|
+
|
|
317
|
+
installer_url = DOCKER_DESKTOP_DOWNLOAD["Windows"]
|
|
318
|
+
has_winget = shutil.which("winget") is not None
|
|
319
|
+
|
|
320
|
+
if has_winget:
|
|
321
|
+
echo("Option 1 — Install via Windows Package Manager (winget):")
|
|
322
|
+
echo(" winget install --id Docker.DockerDesktop")
|
|
323
|
+
echo("")
|
|
324
|
+
echo("Option 2 — Download the installer directly:")
|
|
325
|
+
echo(f" {installer_url}")
|
|
326
|
+
echo("")
|
|
327
|
+
ans = input_fn("Install via winget now? [Y/n] ").strip().lower()
|
|
328
|
+
if ans in ("", "y", "yes"):
|
|
329
|
+
echo("Running winget install Docker.DockerDesktop ...")
|
|
330
|
+
echo("(This may take several minutes. Admin elevation may be required.)")
|
|
331
|
+
try:
|
|
332
|
+
result = subprocess.run(
|
|
333
|
+
[
|
|
334
|
+
"winget", "install",
|
|
335
|
+
"--id", "Docker.DockerDesktop",
|
|
336
|
+
"--source", "winget",
|
|
337
|
+
"--accept-package-agreements",
|
|
338
|
+
"--accept-source-agreements",
|
|
339
|
+
],
|
|
340
|
+
timeout=600,
|
|
341
|
+
check=False,
|
|
342
|
+
)
|
|
343
|
+
if result.returncode == 0:
|
|
344
|
+
echo("")
|
|
345
|
+
echo("Docker Desktop installed!")
|
|
346
|
+
echo("Please restart your computer, then start Docker Desktop")
|
|
347
|
+
echo("(look for the whale icon in the system tray), then run:")
|
|
348
|
+
echo(" skmemory setup wizard")
|
|
349
|
+
return False
|
|
350
|
+
else:
|
|
351
|
+
echo("winget install failed. Opening the download page...")
|
|
352
|
+
_open_url_in_browser(installer_url)
|
|
353
|
+
echo(f"Download the installer, run it, restart, then:")
|
|
354
|
+
echo(" skmemory setup wizard")
|
|
355
|
+
return False
|
|
356
|
+
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
357
|
+
echo(f"winget error: {exc}")
|
|
358
|
+
echo(f"Download manually: {installer_url}")
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
# No winget — offer browser download.
|
|
362
|
+
echo(f"Download Docker Desktop for Windows:")
|
|
363
|
+
echo(f" {installer_url}")
|
|
364
|
+
echo("")
|
|
365
|
+
echo("After installing:")
|
|
366
|
+
echo(" 1. Restart your computer")
|
|
367
|
+
echo(" 2. Start Docker Desktop (whale icon in system tray)")
|
|
368
|
+
echo(" 3. Run: skmemory setup wizard")
|
|
369
|
+
echo("")
|
|
370
|
+
ans = input_fn("Open the download page in your browser? [Y/n] ").strip().lower()
|
|
371
|
+
if ans in ("", "y", "yes"):
|
|
372
|
+
_open_url_in_browser(installer_url)
|
|
373
|
+
echo("Download page opened in browser.")
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def try_install_docker(
|
|
378
|
+
os_name: str,
|
|
379
|
+
echo: Callable,
|
|
380
|
+
input_fn: Optional[Callable] = None,
|
|
381
|
+
) -> bool:
|
|
382
|
+
"""Offer to help the user install Docker for their OS.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
os_name: Result of ``platform.system()`` (Linux, Darwin, Windows).
|
|
386
|
+
echo: Output callable.
|
|
387
|
+
input_fn: Input callable (defaults to built-in ``input``).
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
True if Docker is now available (rare — most paths require a re-run
|
|
391
|
+
after install/reboot), False otherwise.
|
|
392
|
+
"""
|
|
393
|
+
if input_fn is None:
|
|
394
|
+
input_fn = input
|
|
395
|
+
|
|
396
|
+
if os_name == "Linux":
|
|
397
|
+
return _try_install_docker_linux(echo, input_fn)
|
|
398
|
+
if os_name == "Darwin":
|
|
399
|
+
return _try_install_docker_macos(echo, input_fn)
|
|
400
|
+
if os_name == "Windows":
|
|
401
|
+
return _try_install_docker_windows(echo, input_fn)
|
|
402
|
+
|
|
403
|
+
echo("")
|
|
404
|
+
echo("Docker not found. Install it from: https://docs.docker.com/get-docker/")
|
|
405
|
+
echo("Then run: skmemory setup wizard")
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def check_port_available(port: int) -> bool:
|
|
410
|
+
"""Test if a TCP port is available for binding.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
port: Port number to check.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
True if the port is free, False if occupied.
|
|
417
|
+
"""
|
|
418
|
+
try:
|
|
419
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
420
|
+
s.bind(("127.0.0.1", port))
|
|
421
|
+
return True
|
|
422
|
+
except OSError:
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ─────────────────────────────────────────────────────────
|
|
427
|
+
# Container lifecycle
|
|
428
|
+
# ─────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _compose_cmd(use_legacy: bool) -> list[str]:
|
|
432
|
+
"""Return the base compose command based on version."""
|
|
433
|
+
if use_legacy:
|
|
434
|
+
return ["docker-compose"]
|
|
435
|
+
return ["docker", "compose"]
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def find_compose_file() -> Path:
|
|
439
|
+
"""Locate the docker-compose.yml.
|
|
440
|
+
|
|
441
|
+
Checks:
|
|
442
|
+
1. Package-bundled compose file (next to this source file's parent).
|
|
443
|
+
2. Falls back to generating one in ``~/.skcapstone/docker-compose.yml``.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Path to the compose file.
|
|
447
|
+
"""
|
|
448
|
+
# Check package-bundled location
|
|
449
|
+
package_dir = Path(__file__).resolve().parent.parent
|
|
450
|
+
bundled = package_dir / "docker-compose.yml"
|
|
451
|
+
if bundled.exists():
|
|
452
|
+
return bundled
|
|
453
|
+
|
|
454
|
+
# Fall back to ~/.skcapstone/
|
|
455
|
+
fallback = CONFIG_DIR / "docker-compose.yml"
|
|
456
|
+
if not fallback.exists():
|
|
457
|
+
fallback.parent.mkdir(parents=True, exist_ok=True)
|
|
458
|
+
fallback.write_text(DOCKER_COMPOSE_YAML)
|
|
459
|
+
return fallback
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def compose_up(
|
|
463
|
+
services: Optional[list[str]] = None,
|
|
464
|
+
compose_file: Optional[Path] = None,
|
|
465
|
+
use_legacy: bool = False,
|
|
466
|
+
) -> subprocess.CompletedProcess:
|
|
467
|
+
"""Start containers via docker compose.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
services: Specific services to start (None = all).
|
|
471
|
+
compose_file: Path to compose file.
|
|
472
|
+
use_legacy: Use ``docker-compose`` instead of ``docker compose``.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
CompletedProcess from the subprocess call.
|
|
476
|
+
"""
|
|
477
|
+
if compose_file is None:
|
|
478
|
+
compose_file = find_compose_file()
|
|
479
|
+
|
|
480
|
+
cmd = _compose_cmd(use_legacy) + ["-f", str(compose_file), "up", "-d"]
|
|
481
|
+
if services:
|
|
482
|
+
cmd.extend(services)
|
|
483
|
+
|
|
484
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def compose_down(
|
|
488
|
+
compose_file: Optional[Path] = None,
|
|
489
|
+
remove_volumes: bool = False,
|
|
490
|
+
use_legacy: bool = False,
|
|
491
|
+
) -> subprocess.CompletedProcess:
|
|
492
|
+
"""Stop and remove containers.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
compose_file: Path to compose file.
|
|
496
|
+
remove_volumes: Also remove data volumes.
|
|
497
|
+
use_legacy: Use ``docker-compose`` instead of ``docker compose``.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
CompletedProcess from the subprocess call.
|
|
501
|
+
"""
|
|
502
|
+
if compose_file is None:
|
|
503
|
+
compose_file = find_compose_file()
|
|
504
|
+
|
|
505
|
+
cmd = _compose_cmd(use_legacy) + ["-f", str(compose_file), "down"]
|
|
506
|
+
if remove_volumes:
|
|
507
|
+
cmd.append("-v")
|
|
508
|
+
|
|
509
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def compose_ps(
|
|
513
|
+
compose_file: Optional[Path] = None,
|
|
514
|
+
use_legacy: bool = False,
|
|
515
|
+
) -> subprocess.CompletedProcess:
|
|
516
|
+
"""Show container status.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
compose_file: Path to compose file.
|
|
520
|
+
use_legacy: Use ``docker-compose`` instead of ``docker compose``.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
CompletedProcess from the subprocess call.
|
|
524
|
+
"""
|
|
525
|
+
if compose_file is None:
|
|
526
|
+
compose_file = find_compose_file()
|
|
527
|
+
|
|
528
|
+
cmd = _compose_cmd(use_legacy) + ["-f", str(compose_file), "ps"]
|
|
529
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
# ─────────────────────────────────────────────────────────
|
|
533
|
+
# Health checks
|
|
534
|
+
# ─────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def check_skvector_health(url: str = "http://localhost:6333", timeout: int = 30) -> bool:
|
|
538
|
+
"""Poll SKVector's /healthz endpoint until healthy or timeout.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
url: SKVector (Qdrant) REST API base URL.
|
|
542
|
+
timeout: Max seconds to wait.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
True if SKVector responded healthy within the timeout.
|
|
546
|
+
"""
|
|
547
|
+
deadline = time.monotonic() + timeout
|
|
548
|
+
healthz = f"{url.rstrip('/')}/healthz"
|
|
549
|
+
|
|
550
|
+
while time.monotonic() < deadline:
|
|
551
|
+
try:
|
|
552
|
+
req = urllib.request.Request(healthz, method="GET")
|
|
553
|
+
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
554
|
+
if resp.status == 200:
|
|
555
|
+
return True
|
|
556
|
+
except (urllib.error.URLError, OSError, TimeoutError):
|
|
557
|
+
pass
|
|
558
|
+
time.sleep(2)
|
|
559
|
+
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def check_skgraph_health(
|
|
564
|
+
host: str = "localhost", port: int = 6379, timeout: int = 30
|
|
565
|
+
) -> bool:
|
|
566
|
+
"""Send Redis PING to SKGraph and wait for PONG.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
host: SKGraph (FalkorDB) hostname.
|
|
570
|
+
port: SKGraph port (Redis protocol).
|
|
571
|
+
timeout: Max seconds to wait.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
True if PONG received within the timeout.
|
|
575
|
+
"""
|
|
576
|
+
deadline = time.monotonic() + timeout
|
|
577
|
+
|
|
578
|
+
while time.monotonic() < deadline:
|
|
579
|
+
try:
|
|
580
|
+
with socket.create_connection((host, port), timeout=3) as sock:
|
|
581
|
+
sock.sendall(b"PING\r\n")
|
|
582
|
+
data = sock.recv(64)
|
|
583
|
+
if b"+PONG" in data:
|
|
584
|
+
return True
|
|
585
|
+
except (OSError, TimeoutError):
|
|
586
|
+
pass
|
|
587
|
+
time.sleep(2)
|
|
588
|
+
|
|
589
|
+
return False
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# ─────────────────────────────────────────────────────────
|
|
593
|
+
# Dependency installer
|
|
594
|
+
# ─────────────────────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def install_python_deps(backends: list[str]) -> subprocess.CompletedProcess:
|
|
598
|
+
"""Install Python packages for the selected backends.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
backends: List of backend names (``"skvector"``, ``"skgraph"``).
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
CompletedProcess from pip.
|
|
605
|
+
"""
|
|
606
|
+
packages: list[str] = []
|
|
607
|
+
if "skvector" in backends:
|
|
608
|
+
packages.extend(["qdrant-client", "sentence-transformers"])
|
|
609
|
+
if "skgraph" in backends:
|
|
610
|
+
packages.append("falkordb")
|
|
611
|
+
|
|
612
|
+
if not packages:
|
|
613
|
+
return subprocess.CompletedProcess(args=[], returncode=0)
|
|
614
|
+
|
|
615
|
+
cmd = [sys.executable, "-m", "pip", "install"] + packages
|
|
616
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# ─────────────────────────────────────────────────────────
|
|
620
|
+
# Main wizard
|
|
621
|
+
# ─────────────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def run_setup_wizard(
|
|
625
|
+
enable_skvector: bool = True,
|
|
626
|
+
enable_skgraph: bool = True,
|
|
627
|
+
skip_deps: bool = False,
|
|
628
|
+
non_interactive: bool = False,
|
|
629
|
+
deployment_mode: Optional[str] = None,
|
|
630
|
+
echo: Optional[Callable] = None,
|
|
631
|
+
input_fn: Optional[Callable] = None,
|
|
632
|
+
) -> dict:
|
|
633
|
+
"""Run the full interactive setup wizard.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
enable_skvector: Include SKVector (vector search) backend.
|
|
637
|
+
enable_skgraph: Include SKGraph (graph db) backend.
|
|
638
|
+
skip_deps: Skip Python dependency installation.
|
|
639
|
+
non_interactive: Don't prompt — use defaults (implies local mode).
|
|
640
|
+
deployment_mode: ``"local"`` (Docker), ``"remote"`` (SaaS / custom URL),
|
|
641
|
+
or ``None`` to ask interactively.
|
|
642
|
+
echo: Callable for output (default: print).
|
|
643
|
+
input_fn: Callable for text input (default: built-in ``input``).
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
dict with wizard results including ``success``, ``mode``,
|
|
647
|
+
``services``, ``health``, and ``config_path``.
|
|
648
|
+
"""
|
|
649
|
+
if echo is None:
|
|
650
|
+
echo = print
|
|
651
|
+
# Wrap echo so it accepts (and ignores) kwargs like end=
|
|
652
|
+
_raw_echo = echo
|
|
653
|
+
|
|
654
|
+
def echo(msg="", **kwargs): # noqa: F811
|
|
655
|
+
_raw_echo(msg)
|
|
656
|
+
|
|
657
|
+
if input_fn is None:
|
|
658
|
+
input_fn = input
|
|
659
|
+
|
|
660
|
+
result: dict = {
|
|
661
|
+
"success": False,
|
|
662
|
+
"mode": deployment_mode or "local",
|
|
663
|
+
"services": [],
|
|
664
|
+
"health": {},
|
|
665
|
+
"config_path": None,
|
|
666
|
+
"errors": [],
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
echo("")
|
|
670
|
+
echo("SKMemory Setup Wizard")
|
|
671
|
+
echo("=" * 40)
|
|
672
|
+
|
|
673
|
+
# ── Step 0: local vs remote ──────────────────────────────────────────────
|
|
674
|
+
if not non_interactive and deployment_mode is None:
|
|
675
|
+
echo("")
|
|
676
|
+
echo("SKVector and SKGraph can run:")
|
|
677
|
+
echo(" [1] Locally with Docker (private, no cloud needed)")
|
|
678
|
+
echo(" [2] Remote / SaaS URL (Qdrant Cloud, self-hosted server, etc.)")
|
|
679
|
+
echo("")
|
|
680
|
+
choice = input_fn("Choice [1]: ").strip()
|
|
681
|
+
deployment_mode = "remote" if choice == "2" else "local"
|
|
682
|
+
result["mode"] = deployment_mode
|
|
683
|
+
|
|
684
|
+
if deployment_mode is None:
|
|
685
|
+
deployment_mode = "local"
|
|
686
|
+
|
|
687
|
+
# ── Remote / SaaS path ──────────────────────────────────────────────────
|
|
688
|
+
if deployment_mode == "remote":
|
|
689
|
+
echo("")
|
|
690
|
+
echo("Enter your remote backend URLs.")
|
|
691
|
+
echo("Press Enter to skip a backend you don't want to enable.")
|
|
692
|
+
echo("")
|
|
693
|
+
|
|
694
|
+
skvector_url: Optional[str] = None
|
|
695
|
+
skvector_key: Optional[str] = None
|
|
696
|
+
skgraph_url: Optional[str] = None
|
|
697
|
+
|
|
698
|
+
if enable_skvector:
|
|
699
|
+
raw = input_fn(
|
|
700
|
+
"SKVector URL (e.g. https://xyz.cloud.qdrant.io:6333): "
|
|
701
|
+
).strip()
|
|
702
|
+
if raw:
|
|
703
|
+
skvector_url = raw
|
|
704
|
+
key_raw = input_fn(
|
|
705
|
+
"SKVector API key (press Enter if none): "
|
|
706
|
+
).strip()
|
|
707
|
+
skvector_key = key_raw or None
|
|
708
|
+
|
|
709
|
+
if enable_skgraph:
|
|
710
|
+
raw = input_fn(
|
|
711
|
+
"SKGraph URL (e.g. redis://myserver:6379): "
|
|
712
|
+
).strip()
|
|
713
|
+
if raw:
|
|
714
|
+
skgraph_url = raw
|
|
715
|
+
|
|
716
|
+
if not skvector_url and not skgraph_url:
|
|
717
|
+
echo("No URLs provided. Nothing configured.")
|
|
718
|
+
result["success"] = True
|
|
719
|
+
return result
|
|
720
|
+
|
|
721
|
+
backends_remote: list[str] = []
|
|
722
|
+
if skvector_url:
|
|
723
|
+
backends_remote.append("skvector")
|
|
724
|
+
if skgraph_url:
|
|
725
|
+
backends_remote.append("skgraph")
|
|
726
|
+
|
|
727
|
+
if not skip_deps:
|
|
728
|
+
echo("Installing Python deps...", end=" ")
|
|
729
|
+
pip_result = install_python_deps(backends_remote)
|
|
730
|
+
if pip_result.returncode != 0:
|
|
731
|
+
echo("failed!")
|
|
732
|
+
echo(f" {pip_result.stderr.strip()[:200]}")
|
|
733
|
+
result["errors"].append("pip install failed")
|
|
734
|
+
else:
|
|
735
|
+
echo("done")
|
|
736
|
+
|
|
737
|
+
cfg = SKMemoryConfig(
|
|
738
|
+
skvector_url=skvector_url,
|
|
739
|
+
skvector_key=skvector_key,
|
|
740
|
+
skgraph_url=skgraph_url,
|
|
741
|
+
backends_enabled=backends_remote,
|
|
742
|
+
setup_completed_at=datetime.now(timezone.utc).isoformat(),
|
|
743
|
+
)
|
|
744
|
+
config_path = save_config(cfg)
|
|
745
|
+
result["config_path"] = str(config_path)
|
|
746
|
+
echo(f"\nConfig saved: {config_path}")
|
|
747
|
+
|
|
748
|
+
if not any("failed" in e for e in result["errors"]):
|
|
749
|
+
result["success"] = True
|
|
750
|
+
echo("Setup complete!")
|
|
751
|
+
return result
|
|
752
|
+
|
|
753
|
+
# ── Local / Docker path ──────────────────────────────────────────────────
|
|
754
|
+
echo("Checking Docker...", end=" ")
|
|
755
|
+
plat = detect_platform()
|
|
756
|
+
|
|
757
|
+
if not plat.docker_available:
|
|
758
|
+
echo("not found!")
|
|
759
|
+
|
|
760
|
+
if non_interactive:
|
|
761
|
+
echo("")
|
|
762
|
+
echo(get_docker_install_instructions(plat.os_name))
|
|
763
|
+
result["errors"].append("Docker not available")
|
|
764
|
+
return result
|
|
765
|
+
|
|
766
|
+
# Offer to help install Docker for the user's OS.
|
|
767
|
+
installed = try_install_docker(plat.os_name, echo, input_fn)
|
|
768
|
+
if not installed:
|
|
769
|
+
result["errors"].append("Docker not available")
|
|
770
|
+
return result
|
|
771
|
+
|
|
772
|
+
# Re-detect after a successful in-process install (rare but possible).
|
|
773
|
+
plat = detect_platform()
|
|
774
|
+
if not plat.docker_available:
|
|
775
|
+
result["errors"].append("Docker still not available after install attempt")
|
|
776
|
+
return result
|
|
777
|
+
|
|
778
|
+
version_str = f" (Docker {plat.docker_version})" if plat.docker_version else ""
|
|
779
|
+
echo(f"found{version_str}")
|
|
780
|
+
|
|
781
|
+
if not plat.compose_available:
|
|
782
|
+
echo("Docker Compose... not found!")
|
|
783
|
+
echo("Install Docker Compose: https://docs.docker.com/compose/install/")
|
|
784
|
+
result["errors"].append("Docker Compose not available")
|
|
785
|
+
return result
|
|
786
|
+
|
|
787
|
+
compose_type = "v1 (legacy)" if plat.compose_legacy else "v2"
|
|
788
|
+
echo(f"Docker Compose {compose_type}... found")
|
|
789
|
+
|
|
790
|
+
if not non_interactive:
|
|
791
|
+
echo("")
|
|
792
|
+
echo("Your memory system runs with SQLite (zero infrastructure).")
|
|
793
|
+
echo("About to deploy:")
|
|
794
|
+
if enable_skvector:
|
|
795
|
+
echo(" SKVector — vector / semantic search (~200 MB RAM, port 6333)")
|
|
796
|
+
if enable_skgraph:
|
|
797
|
+
echo(" SKGraph — graph / relationship db (~100 MB RAM, port 6379)")
|
|
798
|
+
echo("")
|
|
799
|
+
|
|
800
|
+
services_to_start: list[str] = []
|
|
801
|
+
if enable_skvector:
|
|
802
|
+
services_to_start.append("skvector")
|
|
803
|
+
if enable_skgraph:
|
|
804
|
+
services_to_start.append("skgraph")
|
|
805
|
+
|
|
806
|
+
if not services_to_start:
|
|
807
|
+
echo("No backends selected. Nothing to deploy.")
|
|
808
|
+
result["success"] = True
|
|
809
|
+
return result
|
|
810
|
+
|
|
811
|
+
# 3. Port checks
|
|
812
|
+
echo("Checking ports...", end=" ")
|
|
813
|
+
port_issues = []
|
|
814
|
+
if enable_skvector:
|
|
815
|
+
if not check_port_available(6333):
|
|
816
|
+
port_issues.append("6333 (SKVector REST) in use")
|
|
817
|
+
if not check_port_available(6334):
|
|
818
|
+
port_issues.append("6334 (SKVector gRPC) in use")
|
|
819
|
+
if enable_skgraph:
|
|
820
|
+
if not check_port_available(6379):
|
|
821
|
+
port_issues.append("6379 (SKGraph) in use")
|
|
822
|
+
|
|
823
|
+
if port_issues:
|
|
824
|
+
echo("conflict!")
|
|
825
|
+
for issue in port_issues:
|
|
826
|
+
echo(f" Port {issue}")
|
|
827
|
+
echo("Stop conflicting services or adjust docker-compose.yml ports.")
|
|
828
|
+
result["errors"].append(f"Port conflicts: {', '.join(port_issues)}")
|
|
829
|
+
return result
|
|
830
|
+
|
|
831
|
+
ports_ok = []
|
|
832
|
+
if enable_skvector:
|
|
833
|
+
ports_ok.extend(["6333", "6334"])
|
|
834
|
+
if enable_skgraph:
|
|
835
|
+
ports_ok.append("6379")
|
|
836
|
+
echo(f"{', '.join(ports_ok)} available")
|
|
837
|
+
|
|
838
|
+
# 4. Deploy containers
|
|
839
|
+
compose_file = find_compose_file()
|
|
840
|
+
echo("Starting containers...", end=" ")
|
|
841
|
+
up_result = compose_up(
|
|
842
|
+
services=services_to_start,
|
|
843
|
+
compose_file=compose_file,
|
|
844
|
+
use_legacy=plat.compose_legacy,
|
|
845
|
+
)
|
|
846
|
+
if up_result.returncode != 0:
|
|
847
|
+
echo("failed!")
|
|
848
|
+
echo(f" {up_result.stderr.strip()}")
|
|
849
|
+
result["errors"].append(f"docker compose up failed: {up_result.stderr.strip()}")
|
|
850
|
+
return result
|
|
851
|
+
echo("done")
|
|
852
|
+
result["services"] = services_to_start
|
|
853
|
+
|
|
854
|
+
# 5. Health checks
|
|
855
|
+
echo("Health checks...", end=" ")
|
|
856
|
+
health_parts = []
|
|
857
|
+
if enable_skvector:
|
|
858
|
+
start = time.monotonic()
|
|
859
|
+
skvector_ok = check_skvector_health(timeout=30)
|
|
860
|
+
elapsed = time.monotonic() - start
|
|
861
|
+
result["health"]["skvector"] = skvector_ok
|
|
862
|
+
if skvector_ok:
|
|
863
|
+
health_parts.append(f"SKVector healthy ({elapsed:.1f}s)")
|
|
864
|
+
else:
|
|
865
|
+
health_parts.append("SKVector TIMEOUT")
|
|
866
|
+
result["errors"].append("SKVector health check timed out")
|
|
867
|
+
|
|
868
|
+
if enable_skgraph:
|
|
869
|
+
start = time.monotonic()
|
|
870
|
+
skgraph_ok = check_skgraph_health(timeout=30)
|
|
871
|
+
elapsed = time.monotonic() - start
|
|
872
|
+
result["health"]["skgraph"] = skgraph_ok
|
|
873
|
+
if skgraph_ok:
|
|
874
|
+
health_parts.append(f"SKGraph healthy ({elapsed:.1f}s)")
|
|
875
|
+
else:
|
|
876
|
+
health_parts.append("SKGraph TIMEOUT")
|
|
877
|
+
result["errors"].append("SKGraph health check timed out")
|
|
878
|
+
|
|
879
|
+
echo(", ".join(health_parts))
|
|
880
|
+
|
|
881
|
+
# 6. Python deps
|
|
882
|
+
if not skip_deps:
|
|
883
|
+
echo("Installing Python deps...", end=" ")
|
|
884
|
+
pip_result = install_python_deps(services_to_start)
|
|
885
|
+
if pip_result.returncode != 0:
|
|
886
|
+
echo("failed!")
|
|
887
|
+
echo(f" {pip_result.stderr.strip()[:200]}")
|
|
888
|
+
result["errors"].append("pip install failed")
|
|
889
|
+
else:
|
|
890
|
+
echo("done")
|
|
891
|
+
|
|
892
|
+
# 7. Save config
|
|
893
|
+
backends_enabled = []
|
|
894
|
+
if enable_skvector:
|
|
895
|
+
backends_enabled.append("skvector")
|
|
896
|
+
if enable_skgraph:
|
|
897
|
+
backends_enabled.append("skgraph")
|
|
898
|
+
|
|
899
|
+
cfg = SKMemoryConfig(
|
|
900
|
+
skvector_url="http://localhost:6333" if enable_skvector else None,
|
|
901
|
+
skgraph_url="redis://localhost:6379" if enable_skgraph else None,
|
|
902
|
+
backends_enabled=backends_enabled,
|
|
903
|
+
docker_compose_file=str(compose_file),
|
|
904
|
+
setup_completed_at=datetime.now(timezone.utc).isoformat(),
|
|
905
|
+
)
|
|
906
|
+
config_path = save_config(cfg)
|
|
907
|
+
result["config_path"] = str(config_path)
|
|
908
|
+
|
|
909
|
+
echo(f"\nConfig saved: {config_path}")
|
|
910
|
+
|
|
911
|
+
has_errors = any(
|
|
912
|
+
"timed out" in e or "failed" in e for e in result["errors"]
|
|
913
|
+
)
|
|
914
|
+
if not has_errors:
|
|
915
|
+
result["success"] = True
|
|
916
|
+
echo("Setup complete!")
|
|
917
|
+
else:
|
|
918
|
+
echo("Setup completed with warnings. Check errors above.")
|
|
919
|
+
|
|
920
|
+
return result
|