cli-memory-os 0.1.0__py3-none-any.whl
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.
- cli/__init__.py +1 -0
- cli/commands/__init__.py +1 -0
- cli/commands/benchmark.py +151 -0
- cli/commands/config_cmd.py +57 -0
- cli/commands/doctor.py +61 -0
- cli/commands/export.py +106 -0
- cli/commands/import_cmd.py +143 -0
- cli/commands/init.py +205 -0
- cli/commands/logs.py +38 -0
- cli/commands/monitor.py +63 -0
- cli/commands/plugins.py +37 -0
- cli/commands/start.py +24 -0
- cli/commands/status.py +92 -0
- cli/commands/stop.py +19 -0
- cli/commands/version.py +25 -0
- cli/commands/workspace.py +154 -0
- cli/main.py +496 -0
- cli/parser.py +188 -0
- cli_memory_os-0.1.0.dist-info/METADATA +231 -0
- cli_memory_os-0.1.0.dist-info/RECORD +50 -0
- cli_memory_os-0.1.0.dist-info/WHEEL +5 -0
- cli_memory_os-0.1.0.dist-info/entry_points.txt +2 -0
- cli_memory_os-0.1.0.dist-info/licenses/LICENSE +21 -0
- cli_memory_os-0.1.0.dist-info/top_level.txt +6 -0
- connectors/__init__.py +1 -0
- connectors/base.py +39 -0
- connectors/github.py +273 -0
- connectors/gmail.py +116 -0
- connectors/notion.py +156 -0
- connectors/registry.py +60 -0
- core/__init__.py +1 -0
- core/chunker.py +136 -0
- core/context_builder.py +407 -0
- core/embedder.py +42 -0
- core/llm.py +301 -0
- core/vector_store.py +629 -0
- infrastructure/__init__.py +1 -0
- infrastructure/compose.py +136 -0
- infrastructure/config.py +280 -0
- infrastructure/docker.py +61 -0
- infrastructure/health.py +338 -0
- infrastructure/observability.py +160 -0
- infrastructure/workspace.py +152 -0
- models/__init__.py +1 -0
- models/memory.py +28 -0
- storage/__init__.py +1 -0
- storage/db.py +528 -0
- storage/graph.py +324 -0
- storage/schema.sql +57 -0
- storage/tech_detector.py +117 -0
cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# cli package
|
cli/commands/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# cli.commands package
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os benchmark
|
|
3
|
+
|
|
4
|
+
Runs performance latency diagnostics across search indexes, embedding model,
|
|
5
|
+
graph lookups, LLM invocation, and RAG pipelines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import sys
|
|
10
|
+
from infrastructure.health import check_neo4j, check_qdrant, check_groq_api, check_memory_usage
|
|
11
|
+
from core.embedder import Embedder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(args):
|
|
15
|
+
"""Run the benchmark command."""
|
|
16
|
+
from storage.db import init_db
|
|
17
|
+
try:
|
|
18
|
+
init_db()
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
print("──────────────────────────────────────────────────")
|
|
23
|
+
print(" Memory-OS Performance Benchmark")
|
|
24
|
+
print("──────────────────────────────────────────────────")
|
|
25
|
+
print(" Running latency measurements (please wait)...")
|
|
26
|
+
|
|
27
|
+
# Metrics dictionary
|
|
28
|
+
latencies = {
|
|
29
|
+
"Keyword Search": "N/A",
|
|
30
|
+
"Semantic Search": "N/A",
|
|
31
|
+
"Hybrid Search": "N/A",
|
|
32
|
+
"Graph Lookup": "N/A",
|
|
33
|
+
"Embedding Time": "N/A",
|
|
34
|
+
"LLM Time": "N/A",
|
|
35
|
+
"Average RAG Pipeline": "N/A",
|
|
36
|
+
"Memory Usage": "N/A",
|
|
37
|
+
"Vector Count": "0",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# 1. Memory Usage
|
|
41
|
+
_, mem_detail = check_memory_usage()
|
|
42
|
+
latencies["Memory Usage"] = mem_detail
|
|
43
|
+
|
|
44
|
+
# 2. Embedding Time
|
|
45
|
+
try:
|
|
46
|
+
start = time.perf_counter()
|
|
47
|
+
embedder = Embedder()
|
|
48
|
+
embedder.embed_documents(["This is a test snippet to benchmark the local embedding model latency."])
|
|
49
|
+
duration = time.perf_counter() - start
|
|
50
|
+
latencies["Embedding Time"] = f"{duration*1000:.1f} ms"
|
|
51
|
+
except Exception as e:
|
|
52
|
+
latencies["Embedding Time"] = f"Error: {e}"
|
|
53
|
+
|
|
54
|
+
# 3. SQLite Keyword Search
|
|
55
|
+
try:
|
|
56
|
+
from storage.db import get_connection
|
|
57
|
+
conn = get_connection()
|
|
58
|
+
cursor = conn.cursor()
|
|
59
|
+
|
|
60
|
+
# Test full text search query on SQLite
|
|
61
|
+
start = time.perf_counter()
|
|
62
|
+
cursor.execute("SELECT id FROM document_chunks WHERE chunk_text LIKE '%python%' LIMIT 5")
|
|
63
|
+
cursor.fetchall()
|
|
64
|
+
duration = time.perf_counter() - start
|
|
65
|
+
latencies["Keyword Search"] = f"{duration*1000:.2f} ms"
|
|
66
|
+
conn.close()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
latencies["Keyword Search"] = f"Error: {e}"
|
|
69
|
+
|
|
70
|
+
# 4. Qdrant Semantic Search
|
|
71
|
+
qdrant_ok, _ = check_qdrant()
|
|
72
|
+
if qdrant_ok:
|
|
73
|
+
try:
|
|
74
|
+
from core.vector_store import run_semantic_search, get_vector_index_stats
|
|
75
|
+
# Vector count
|
|
76
|
+
stats = get_vector_index_stats()
|
|
77
|
+
latencies["Vector Count"] = f"{stats.get('vectors', 0):,}"
|
|
78
|
+
|
|
79
|
+
# Semantic Query
|
|
80
|
+
start = time.perf_counter()
|
|
81
|
+
run_semantic_search("python", limit=5)
|
|
82
|
+
duration = time.perf_counter() - start
|
|
83
|
+
latencies["Semantic Search"] = f"{duration*1000:.2f} ms"
|
|
84
|
+
except Exception as e:
|
|
85
|
+
latencies["Semantic Search"] = f"Error: {e}"
|
|
86
|
+
else:
|
|
87
|
+
latencies["Semantic Search"] = "Offline (Qdrant down)"
|
|
88
|
+
|
|
89
|
+
# 5. Hybrid Search
|
|
90
|
+
if qdrant_ok:
|
|
91
|
+
try:
|
|
92
|
+
from core.vector_store import hybrid_search
|
|
93
|
+
start = time.perf_counter()
|
|
94
|
+
hybrid_search("python")
|
|
95
|
+
duration = time.perf_counter() - start
|
|
96
|
+
latencies["Hybrid Search"] = f"{duration*1000:.2f} ms"
|
|
97
|
+
except Exception as e:
|
|
98
|
+
latencies["Hybrid Search"] = f"Error: {e}"
|
|
99
|
+
else:
|
|
100
|
+
latencies["Hybrid Search"] = "Offline (Qdrant down)"
|
|
101
|
+
|
|
102
|
+
# 6. Graph Lookup
|
|
103
|
+
try:
|
|
104
|
+
from storage.graph import GraphStore
|
|
105
|
+
graph = GraphStore()
|
|
106
|
+
|
|
107
|
+
# Get arbitrary repo to query
|
|
108
|
+
from storage.db import get_connection
|
|
109
|
+
conn = get_connection()
|
|
110
|
+
cursor = conn.cursor()
|
|
111
|
+
cursor.execute("SELECT repo_name FROM repositories LIMIT 1")
|
|
112
|
+
row = cursor.fetchone()
|
|
113
|
+
conn.close()
|
|
114
|
+
|
|
115
|
+
repo_to_query = row[0] if row else "unknown-repo"
|
|
116
|
+
|
|
117
|
+
start = time.perf_counter()
|
|
118
|
+
graph.get_node_relationships("Repository", repo_to_query)
|
|
119
|
+
duration = time.perf_counter() - start
|
|
120
|
+
latencies["Graph Lookup"] = f"{duration*1000:.2f} ms"
|
|
121
|
+
except Exception as e:
|
|
122
|
+
latencies["Graph Lookup"] = f"Error: {e}"
|
|
123
|
+
|
|
124
|
+
# 7. LLM time & RAG Pipeline
|
|
125
|
+
groq_ok, _ = check_groq_api()
|
|
126
|
+
if groq_ok:
|
|
127
|
+
try:
|
|
128
|
+
from core.llm import run_hybrid_rag
|
|
129
|
+
start = time.perf_counter()
|
|
130
|
+
res = run_hybrid_rag("List repositories using Python")
|
|
131
|
+
duration = time.perf_counter() - start
|
|
132
|
+
latencies["Average RAG Pipeline"] = f"{duration:.2f} s"
|
|
133
|
+
|
|
134
|
+
# Estimate LLM time
|
|
135
|
+
# Assuming LLM call took most of RAG duration (minus search overhead)
|
|
136
|
+
latencies["LLM Time"] = f"{duration * 0.85:.2f} s"
|
|
137
|
+
except Exception as e:
|
|
138
|
+
latencies["Average RAG Pipeline"] = f"Error: {e}"
|
|
139
|
+
latencies["LLM Time"] = "Error"
|
|
140
|
+
else:
|
|
141
|
+
latencies["Average RAG Pipeline"] = "Offline (Groq key missing)"
|
|
142
|
+
latencies["LLM Time"] = "Offline"
|
|
143
|
+
|
|
144
|
+
# Display results
|
|
145
|
+
print("──────────────────────────────────────────────────")
|
|
146
|
+
print(" Benchmark Metrics")
|
|
147
|
+
print("──────────────────────────────────────────────────")
|
|
148
|
+
for name, score in latencies.items():
|
|
149
|
+
padding = " " * (25 - len(name))
|
|
150
|
+
print(f" {name}{padding}: {score}")
|
|
151
|
+
print("──────────────────────────────────────────────────")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os config show|get|set|reset
|
|
3
|
+
|
|
4
|
+
Enables querying and modification of key-value configuration pairs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from infrastructure.config import (
|
|
9
|
+
get_config,
|
|
10
|
+
get_dotted,
|
|
11
|
+
set_dotted,
|
|
12
|
+
save_config,
|
|
13
|
+
DEFAULTS
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def execute(args):
|
|
18
|
+
"""Run the config command."""
|
|
19
|
+
action = args.config_action
|
|
20
|
+
|
|
21
|
+
if action == "show":
|
|
22
|
+
config = get_config()
|
|
23
|
+
print("─────────────────────────────")
|
|
24
|
+
print(" Memory-OS Configuration")
|
|
25
|
+
print("─────────────────────────────")
|
|
26
|
+
for key, val in config.items():
|
|
27
|
+
if isinstance(val, dict):
|
|
28
|
+
print(f"[{key}]")
|
|
29
|
+
for k, v in val.items():
|
|
30
|
+
print(f" {k} = {v}")
|
|
31
|
+
else:
|
|
32
|
+
print(f"{key} = {val}")
|
|
33
|
+
print("─────────────────────────────")
|
|
34
|
+
|
|
35
|
+
elif action == "get":
|
|
36
|
+
val = get_dotted(args.key)
|
|
37
|
+
if val is None:
|
|
38
|
+
print(f"❌ Configuration key '{args.key}' not found.")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
print(val)
|
|
41
|
+
|
|
42
|
+
elif action == "set":
|
|
43
|
+
success, err = set_dotted(args.key, args.value)
|
|
44
|
+
if not success:
|
|
45
|
+
print(f"❌ Failed to set configuration: {err}")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
print(f"✓ Configuration updated: {args.key} = {args.value}")
|
|
48
|
+
|
|
49
|
+
elif action == "reset":
|
|
50
|
+
confirm = input("⚠️ Are you sure you want to reset ALL configurations to default values? (y/N): ").strip().lower()
|
|
51
|
+
if confirm == "y":
|
|
52
|
+
save_config(DEFAULTS)
|
|
53
|
+
print("✓ Configuration reset to defaults successfully.")
|
|
54
|
+
else:
|
|
55
|
+
print("Reset cancelled.")
|
|
56
|
+
else:
|
|
57
|
+
print("Usage: memory-os config show|get|set|reset")
|
cli/commands/doctor.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os doctor
|
|
3
|
+
|
|
4
|
+
Diagnoses system health and connectivity across all services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute(args):
|
|
9
|
+
"""Run the doctor command."""
|
|
10
|
+
from storage.db import init_db
|
|
11
|
+
try:
|
|
12
|
+
init_db()
|
|
13
|
+
except Exception:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
from infrastructure.health import run_all_checks
|
|
17
|
+
|
|
18
|
+
print("─────────────────────────────")
|
|
19
|
+
print(" memory-os doctor")
|
|
20
|
+
print("─────────────────────────────")
|
|
21
|
+
|
|
22
|
+
checks = run_all_checks()
|
|
23
|
+
|
|
24
|
+
max_name_len = max(len(name) for name, _, _ in checks)
|
|
25
|
+
|
|
26
|
+
for name, passed, detail in checks:
|
|
27
|
+
icon = "✓" if passed else "✗"
|
|
28
|
+
padding = " " * (max_name_len - len(name) + 2)
|
|
29
|
+
print(f" {name}{padding}{icon} {detail}")
|
|
30
|
+
|
|
31
|
+
print("─────────────────────────────")
|
|
32
|
+
|
|
33
|
+
failed = [(name, detail) for name, passed, detail in checks if not passed]
|
|
34
|
+
if failed:
|
|
35
|
+
print(f"\n {len(failed)} issue(s) detected.")
|
|
36
|
+
print("\n─────────────────────────────")
|
|
37
|
+
print(" Actionable Recommendations")
|
|
38
|
+
print("─────────────────────────────")
|
|
39
|
+
recommendations = {
|
|
40
|
+
"Docker": "Install Docker Desktop from https://www.docker.com/",
|
|
41
|
+
"Docker Compose": "Ensure Docker Compose is installed ('docker compose version').",
|
|
42
|
+
"Neo4j": "Run:\n memory-os start",
|
|
43
|
+
"Qdrant": "Run:\n memory-os start",
|
|
44
|
+
"Groq": "memory-os config set groq.api_key <key>",
|
|
45
|
+
"Composio": "memory-os config set composio.api_key <key>",
|
|
46
|
+
"SQLite": "Workspace not initialized. Run memory-os init.",
|
|
47
|
+
"Github": "Authenticate GitHub connector: run 'memory-os init'",
|
|
48
|
+
"Gmail": "Authenticate Gmail connector: run 'memory-os init'",
|
|
49
|
+
"Notion": "Authenticate Notion connector: run 'memory-os init'",
|
|
50
|
+
"Workspace": "Workspace not initialized.\nRun: memory-os init",
|
|
51
|
+
"Embedding Model": "Download embedding model: run 'memory-os init'",
|
|
52
|
+
"Config Validation": "Reset config.toml file: run 'memory-os config reset'",
|
|
53
|
+
"Active Plugins": "No plugins are authenticated. Run 'memory-os init' to set up connector logins.",
|
|
54
|
+
}
|
|
55
|
+
for name, detail in failed:
|
|
56
|
+
rec = recommendations.get(name, "Check log file logs/memory_os.log for errors.")
|
|
57
|
+
print(f" * {name}: {rec} (Detail: {detail})")
|
|
58
|
+
print("─────────────────────────────")
|
|
59
|
+
else:
|
|
60
|
+
print("\n All checks passed.")
|
|
61
|
+
|
cli/commands/export.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os export <file>
|
|
3
|
+
|
|
4
|
+
Exports the current active workspace profile databases, configurations,
|
|
5
|
+
and metadata into a single compressed zip archive.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import zipfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
from infrastructure.workspace import (
|
|
16
|
+
get_active_profile,
|
|
17
|
+
get_profile_path,
|
|
18
|
+
get_db_path,
|
|
19
|
+
get_qdrant_path,
|
|
20
|
+
get_neo4j_path,
|
|
21
|
+
get_workspace_size
|
|
22
|
+
)
|
|
23
|
+
from infrastructure.config import _get_config_path, get
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def execute(args):
|
|
27
|
+
"""Run the export command."""
|
|
28
|
+
export_path = Path(args.file)
|
|
29
|
+
print(f"Exporting active workspace profile to '{export_path}'...")
|
|
30
|
+
|
|
31
|
+
active_profile = get_active_profile()
|
|
32
|
+
profile_path = get_profile_path(active_profile)
|
|
33
|
+
db_path = get_db_path(active_profile)
|
|
34
|
+
qdrant_path = get_qdrant_path(active_profile)
|
|
35
|
+
neo4j_path = get_neo4j_path(active_profile)
|
|
36
|
+
config_path = _get_config_path()
|
|
37
|
+
|
|
38
|
+
if not profile_path.exists():
|
|
39
|
+
print(f"❌ Active workspace profile '{active_profile}' does not exist.")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
# 1. Gather Metadata
|
|
43
|
+
embedding_model = get("embeddings", "model", "all-MiniLM-L6-v2")
|
|
44
|
+
metadata = {
|
|
45
|
+
"version": "1.0",
|
|
46
|
+
"exported_at": datetime.now().isoformat(),
|
|
47
|
+
"active_profile": active_profile,
|
|
48
|
+
"embedding_model": embedding_model,
|
|
49
|
+
"storage_size_bytes": get_workspace_size(active_profile),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Resolve export file parent directory exists
|
|
54
|
+
if export_path.parent:
|
|
55
|
+
export_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
with zipfile.ZipFile(export_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
58
|
+
# A. Metadata File
|
|
59
|
+
zipf.writestr("metadata.json", json.dumps(metadata, indent=2))
|
|
60
|
+
print(" ✓ Packed metadata.json")
|
|
61
|
+
|
|
62
|
+
# B. Configuration File
|
|
63
|
+
if config_path.exists():
|
|
64
|
+
zipf.write(config_path, arcname="config.toml")
|
|
65
|
+
print(" ✓ Packed config.toml")
|
|
66
|
+
|
|
67
|
+
# C. SQLite Database
|
|
68
|
+
if db_path.exists():
|
|
69
|
+
zipf.write(db_path, arcname="workspace.db")
|
|
70
|
+
print(" ✓ Packed SQLite database")
|
|
71
|
+
|
|
72
|
+
# D. Qdrant storage folder recursively
|
|
73
|
+
if qdrant_path.exists():
|
|
74
|
+
qdrant_files_count = 0
|
|
75
|
+
for root, _, files in os.walk(qdrant_path):
|
|
76
|
+
for file in files:
|
|
77
|
+
file_path = Path(root) / file
|
|
78
|
+
arcname = Path("qdrant") / file_path.relative_to(qdrant_path)
|
|
79
|
+
zipf.write(file_path, arcname=arcname)
|
|
80
|
+
qdrant_files_count += 1
|
|
81
|
+
print(f" ✓ Packed {qdrant_files_count} Qdrant index files")
|
|
82
|
+
|
|
83
|
+
# E. Neo4j storage folder recursively
|
|
84
|
+
if neo4j_path.exists():
|
|
85
|
+
neo4j_files_count = 0
|
|
86
|
+
for root, _, files in os.walk(neo4j_path):
|
|
87
|
+
for file in files:
|
|
88
|
+
file_path = Path(root) / file
|
|
89
|
+
arcname = Path("neo4j") / file_path.relative_to(neo4j_path)
|
|
90
|
+
zipf.write(file_path, arcname=arcname)
|
|
91
|
+
neo4j_files_count += 1
|
|
92
|
+
print(f" ✓ Packed {neo4j_files_count} Neo4j graph database files")
|
|
93
|
+
|
|
94
|
+
print("──────────────────────────────────────────────────")
|
|
95
|
+
print(f"🎉 Workspace profile '{active_profile}' exported successfully!")
|
|
96
|
+
print(f"File: {export_path.resolve()}")
|
|
97
|
+
print("──────────────────────────────────────────────────")
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"❌ Failed to export workspace: {e}")
|
|
101
|
+
if export_path.exists():
|
|
102
|
+
try:
|
|
103
|
+
export_path.unlink()
|
|
104
|
+
except OSError:
|
|
105
|
+
pass
|
|
106
|
+
sys.exit(1)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os import <file>
|
|
3
|
+
|
|
4
|
+
Safely unpacks and restores a previously exported workspace archive
|
|
5
|
+
into the active workspace profile.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import zipfile
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from infrastructure.workspace import (
|
|
16
|
+
get_active_profile,
|
|
17
|
+
get_profile_path,
|
|
18
|
+
get_db_path,
|
|
19
|
+
get_qdrant_path,
|
|
20
|
+
get_neo4j_path
|
|
21
|
+
)
|
|
22
|
+
from infrastructure.config import _get_config_path, get, load_config
|
|
23
|
+
from infrastructure.compose import compose_stop, compose_up, wait_for_services
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def execute(args):
|
|
27
|
+
"""Run the import command."""
|
|
28
|
+
import_path = Path(args.file)
|
|
29
|
+
if not import_path.exists():
|
|
30
|
+
print(f"❌ Import file '{import_path}' does not exist.")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
print(f"Verifying compatibility of '{import_path}'...")
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with zipfile.ZipFile(import_path, "r") as zipf:
|
|
37
|
+
# 1. Read and Validate Metadata
|
|
38
|
+
if "metadata.json" not in zipf.namelist():
|
|
39
|
+
print("❌ Invalid archive: missing 'metadata.json'.")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
metadata_content = zipf.read("metadata.json").decode("utf-8")
|
|
43
|
+
metadata = json.loads(metadata_content)
|
|
44
|
+
|
|
45
|
+
archive_version = metadata.get("version", "0.0")
|
|
46
|
+
if float(archive_version) > 1.0:
|
|
47
|
+
print(f"❌ Incompatible archive version: {archive_version} (supported <= 1.0).")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
current_model = get("embeddings", "model", "all-MiniLM-L6-v2")
|
|
51
|
+
archive_model = metadata.get("embedding_model")
|
|
52
|
+
if archive_model and archive_model != current_model:
|
|
53
|
+
print(f"⚠️ Warning: Embedding model mismatch.")
|
|
54
|
+
print(f" Archive uses: {archive_model}")
|
|
55
|
+
print(f" Current config: {current_model}")
|
|
56
|
+
print(" Importing may cause vector similarity search discrepancy.")
|
|
57
|
+
|
|
58
|
+
# 2. Confirmation Prompt
|
|
59
|
+
active_profile = get_active_profile()
|
|
60
|
+
print(f"\n⚠️ WARNING: Importing will completely OVERWRITE data in the active workspace profile '{active_profile}'.")
|
|
61
|
+
confirm = input(f"Are you sure you want to proceed? (y/N): ").strip().lower()
|
|
62
|
+
if confirm != "y":
|
|
63
|
+
print("Import cancelled.")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
print("\nStopping local services to release database locks...")
|
|
67
|
+
compose_stop()
|
|
68
|
+
|
|
69
|
+
# 3. Clean up active profile paths
|
|
70
|
+
profile_path = get_profile_path(active_profile)
|
|
71
|
+
db_path = get_db_path(active_profile)
|
|
72
|
+
qdrant_path = get_qdrant_path(active_profile)
|
|
73
|
+
neo4j_path = get_neo4j_path(active_profile)
|
|
74
|
+
config_path = _get_config_path()
|
|
75
|
+
|
|
76
|
+
print("Cleaning existing database folders...")
|
|
77
|
+
if db_path.exists():
|
|
78
|
+
db_path.unlink()
|
|
79
|
+
if qdrant_path.exists():
|
|
80
|
+
shutil.rmtree(qdrant_path)
|
|
81
|
+
qdrant_path.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
if neo4j_path.exists():
|
|
83
|
+
shutil.rmtree(neo4j_path)
|
|
84
|
+
neo4j_path.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
# 4. Unpack files
|
|
87
|
+
print("Unpacking files from archive...")
|
|
88
|
+
|
|
89
|
+
# config.toml
|
|
90
|
+
if "config.toml" in zipf.namelist() and config_path:
|
|
91
|
+
with open(config_path, "wb") as f:
|
|
92
|
+
f.write(zipf.read("config.toml"))
|
|
93
|
+
print(" ✓ Restored config.toml")
|
|
94
|
+
|
|
95
|
+
# workspace.db
|
|
96
|
+
if "workspace.db" in zipf.namelist() and db_path:
|
|
97
|
+
with open(db_path, "wb") as f:
|
|
98
|
+
f.write(zipf.read("workspace.db"))
|
|
99
|
+
print(" ✓ Restored SQLite database")
|
|
100
|
+
|
|
101
|
+
# Qdrant files
|
|
102
|
+
qdrant_restored = 0
|
|
103
|
+
for name in zipf.namelist():
|
|
104
|
+
if name.startswith("qdrant/"):
|
|
105
|
+
target_file = qdrant_path / name[7:]
|
|
106
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
with open(target_file, "wb") as f:
|
|
108
|
+
f.write(zipf.read(name))
|
|
109
|
+
qdrant_restored += 1
|
|
110
|
+
if qdrant_restored > 0:
|
|
111
|
+
print(f" ✓ Restored {qdrant_restored} Qdrant index files")
|
|
112
|
+
|
|
113
|
+
# Neo4j files
|
|
114
|
+
neo4j_restored = 0
|
|
115
|
+
for name in zipf.namelist():
|
|
116
|
+
if name.startswith("neo4j/"):
|
|
117
|
+
target_file = neo4j_path / name[6:]
|
|
118
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
with open(target_file, "wb") as f:
|
|
120
|
+
f.write(zipf.read(name))
|
|
121
|
+
neo4j_restored += 1
|
|
122
|
+
if neo4j_restored > 0:
|
|
123
|
+
print(f" ✓ Restored {neo4j_restored} Neo4j database files")
|
|
124
|
+
|
|
125
|
+
# 5. Restart Services
|
|
126
|
+
print("\nStarting local services...")
|
|
127
|
+
load_config() # reload config from file
|
|
128
|
+
if compose_up():
|
|
129
|
+
print("Waiting for database health checks...")
|
|
130
|
+
if wait_for_services(timeout=60):
|
|
131
|
+
print("✓ Database services started and healthy.")
|
|
132
|
+
else:
|
|
133
|
+
print("⚠️ Services are starting, but health checks timed out. Run 'memory-os doctor' to check.")
|
|
134
|
+
else:
|
|
135
|
+
print("❌ Failed to start docker compose services.")
|
|
136
|
+
|
|
137
|
+
print("──────────────────────────────────────────────────")
|
|
138
|
+
print("🎉 Workspace imported and restored successfully!")
|
|
139
|
+
print("──────────────────────────────────────────────────")
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"❌ Import failed: {e}")
|
|
143
|
+
sys.exit(1)
|