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/commands/init.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os init
|
|
3
|
+
|
|
4
|
+
Performs first-time setup and onboarding for Memory-OS.
|
|
5
|
+
Provisions the workspace directory structure, creates config.toml,
|
|
6
|
+
spins up Neo4j and Qdrant via docker compose, initializes SQLite,
|
|
7
|
+
pre-warms the embedding model, and launches Composio toolkit OAuth flows.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
import time
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from infrastructure.workspace import ensure_workspace, get_db_path
|
|
16
|
+
from infrastructure.config import generate_default_config, save_config, load_config
|
|
17
|
+
from infrastructure.docker import check_docker_installed, check_docker_compose_installed, check_docker_running
|
|
18
|
+
from infrastructure.compose import compose_up, wait_for_services
|
|
19
|
+
from infrastructure.health import run_all_checks
|
|
20
|
+
from storage.db import init_db
|
|
21
|
+
from core.embedder import Embedder
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_input(prompt: str, secret: bool = False, default: str = "") -> str:
|
|
25
|
+
"""Prompt the user for input with an optional default value."""
|
|
26
|
+
import getpass
|
|
27
|
+
suffix = f" [{default}]" if default else ""
|
|
28
|
+
full_prompt = f"{prompt}{suffix}: "
|
|
29
|
+
try:
|
|
30
|
+
if secret:
|
|
31
|
+
val = getpass.getpass(full_prompt)
|
|
32
|
+
else:
|
|
33
|
+
val = input(full_prompt)
|
|
34
|
+
return val.strip() or default
|
|
35
|
+
except (KeyboardInterrupt, EOFError):
|
|
36
|
+
print("\nInitialization cancelled by user.")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def execute(args):
|
|
41
|
+
"""Run the init command."""
|
|
42
|
+
print("==================================================")
|
|
43
|
+
print("🧠 MEMORY-OS INITIALIZATION WIZARD")
|
|
44
|
+
print("==================================================")
|
|
45
|
+
|
|
46
|
+
# 1. System Dependency Checks
|
|
47
|
+
print("\n[1/7] Checking system dependencies...")
|
|
48
|
+
|
|
49
|
+
python_ok = sys.version_info >= (3, 12)
|
|
50
|
+
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
51
|
+
if not python_ok:
|
|
52
|
+
print(f"❌ Python version {py_ver} is not supported. Requires >= 3.12.")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
print(f" ✓ Python version: {py_ver}")
|
|
55
|
+
|
|
56
|
+
docker_ok, docker_ver = check_docker_installed()
|
|
57
|
+
if not docker_ok:
|
|
58
|
+
print("----------------------------------")
|
|
59
|
+
print("Docker is not installed or not in PATH.")
|
|
60
|
+
print("")
|
|
61
|
+
print("Please install Docker Desktop and rerun:")
|
|
62
|
+
print("memory-os init")
|
|
63
|
+
print("----------------------------------")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
print(f" ✓ Docker: {docker_ver}")
|
|
66
|
+
|
|
67
|
+
compose_ok, compose_ver = check_docker_compose_installed()
|
|
68
|
+
if not compose_ok:
|
|
69
|
+
print("----------------------------------")
|
|
70
|
+
print("Docker Compose is not installed.")
|
|
71
|
+
print("")
|
|
72
|
+
print("Please install Docker Compose and rerun:")
|
|
73
|
+
print("memory-os init")
|
|
74
|
+
print("----------------------------------")
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
print(f" ✓ Docker Compose: {compose_ver}")
|
|
77
|
+
|
|
78
|
+
if not check_docker_running():
|
|
79
|
+
print("----------------------------------")
|
|
80
|
+
print("Docker Desktop is not running.")
|
|
81
|
+
print("")
|
|
82
|
+
print("Start Docker Desktop and rerun:")
|
|
83
|
+
print("memory-os init")
|
|
84
|
+
print("----------------------------------")
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
print(" ✓ Docker Daemon: Running")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# 2. Workspace Tree Setup
|
|
90
|
+
print("\n[2/7] Creating workspace directories...")
|
|
91
|
+
ensure_workspace("default")
|
|
92
|
+
print(" ✓ Workspace path initialized at ~/.memory-os")
|
|
93
|
+
|
|
94
|
+
# 3. Configure API Credentials & Settings
|
|
95
|
+
print("\n[3/7] Configuring credentials...")
|
|
96
|
+
neo4j_password = get_input("Set Neo4j database password", secret=True, default="memory_neo")
|
|
97
|
+
groq_api_key = get_input("Enter Groq API Key", secret=True)
|
|
98
|
+
composio_api_key = get_input("Enter Composio API Key", secret=True)
|
|
99
|
+
|
|
100
|
+
# Generate auto Composio User UUID
|
|
101
|
+
composio_user_id = str(uuid.uuid4())
|
|
102
|
+
|
|
103
|
+
answers = {
|
|
104
|
+
"neo4j_password": neo4j_password,
|
|
105
|
+
"groq_api_key": groq_api_key,
|
|
106
|
+
"composio_api_key": composio_api_key,
|
|
107
|
+
"composio_user_id": composio_user_id
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
config_dict = generate_default_config(answers)
|
|
111
|
+
save_config(config_dict)
|
|
112
|
+
|
|
113
|
+
# Force reload config in current process memory
|
|
114
|
+
load_config()
|
|
115
|
+
|
|
116
|
+
# Overwrite MEMORY_OS_DB_PATH in environment dynamically to point to the workspace DB
|
|
117
|
+
os.environ["MEMORY_OS_DB_PATH"] = str(get_db_path("default"))
|
|
118
|
+
|
|
119
|
+
print(" ✓ Configuration saved to ~/.memory-os/config.toml")
|
|
120
|
+
|
|
121
|
+
# 4. Service Provisioning via Docker Compose
|
|
122
|
+
print("\n[4/7] Provisioning local services (Neo4j, Qdrant)...")
|
|
123
|
+
if not compose_up():
|
|
124
|
+
print("❌ Failed to start docker compose services.")
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
print(" ✓ Containers launched. Waiting for services to become healthy...")
|
|
127
|
+
|
|
128
|
+
if not wait_for_services(timeout=60):
|
|
129
|
+
print("❌ Services failed to start or respond to health checks in time.")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
print(" ✓ Services are healthy.")
|
|
132
|
+
|
|
133
|
+
# 5. Schema & Database Initialization
|
|
134
|
+
print("\n[5/7] Initializing SQLite database...")
|
|
135
|
+
init_db()
|
|
136
|
+
print(" ✓ SQLite database schema initialized.")
|
|
137
|
+
|
|
138
|
+
# 6. Optional Embedding Model Warming
|
|
139
|
+
print("\n[6/7] Preparing embedding model...")
|
|
140
|
+
warm_model = get_input("Download and cache embedding model (all-MiniLM-L6-v2) now? (Y/n)", default="y").lower()
|
|
141
|
+
if warm_model == "y":
|
|
142
|
+
print(" Downloading model (this may take a minute)...")
|
|
143
|
+
try:
|
|
144
|
+
embedder = Embedder()
|
|
145
|
+
_ = embedder.model
|
|
146
|
+
print(" ✓ Embedding model cached successfully.")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f" ⚠️ Warning: Failed to cache model during setup: {e}")
|
|
149
|
+
print(" Model will be downloaded automatically on the first query.")
|
|
150
|
+
else:
|
|
151
|
+
print(" Skipped model download.")
|
|
152
|
+
|
|
153
|
+
# 7. Optional Composio Integration OAuth
|
|
154
|
+
print("\n[7/7] Configuring integration connectors...")
|
|
155
|
+
setup_connectors = get_input("Configure connectors (GitHub, Gmail, Notion) now? (Y/n)", default="y").lower()
|
|
156
|
+
if setup_connectors == "y" and composio_api_key:
|
|
157
|
+
try:
|
|
158
|
+
from composio import Composio
|
|
159
|
+
c = Composio(api_key=composio_api_key)
|
|
160
|
+
session = c.create(user_id=composio_user_id)
|
|
161
|
+
|
|
162
|
+
toolkits_to_connect = [
|
|
163
|
+
("github", "GitHub"),
|
|
164
|
+
("gmail", "Gmail"),
|
|
165
|
+
("notion", "Notion")
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
for slug, name in toolkits_to_connect:
|
|
169
|
+
connect_this = get_input(f"Connect to {name}? (Y/n)", default="y").lower()
|
|
170
|
+
if connect_this == "y":
|
|
171
|
+
print(f" Initiating OAuth for {name}...")
|
|
172
|
+
try:
|
|
173
|
+
req = session.authorize(slug)
|
|
174
|
+
print(f" 👉 Please open this URL in your browser to complete authentication:\n {req.redirect_url}")
|
|
175
|
+
print(" Waiting up to 30 seconds for completion...")
|
|
176
|
+
try:
|
|
177
|
+
# Wait for active connection
|
|
178
|
+
req.wait_for_connection(timeout=30000)
|
|
179
|
+
print(f" ✓ {name} connected successfully!")
|
|
180
|
+
except Exception:
|
|
181
|
+
print(f" ⚠️ Connection verification timed out. You can connect it later or run 'memory-os doctor'.")
|
|
182
|
+
except Exception as err:
|
|
183
|
+
print(f" ⚠️ Failed to initiate connection for {name}: {err}")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f" ⚠️ Error setting up Composio session: {e}")
|
|
186
|
+
else:
|
|
187
|
+
print(" Skipped connectors configuration.")
|
|
188
|
+
|
|
189
|
+
# Verification Report
|
|
190
|
+
print("\n==================================================")
|
|
191
|
+
print("🏥 RUNNING INITIAL HEALTH CHECKS")
|
|
192
|
+
print("==================================================")
|
|
193
|
+
checks = run_all_checks()
|
|
194
|
+
for name, passed, detail in checks:
|
|
195
|
+
icon = "✓" if passed else "✗"
|
|
196
|
+
print(f" {icon} {name}: {detail}")
|
|
197
|
+
|
|
198
|
+
print("\n==================================================")
|
|
199
|
+
print("🎉 MEMORY-OS INITIALIZED SUCCESSFULLY!")
|
|
200
|
+
print("==================================================")
|
|
201
|
+
print("You can now run: ")
|
|
202
|
+
print(" memory-os sync - to import your documents and emails")
|
|
203
|
+
print(" memory-os ask - to query your personal knowledge base")
|
|
204
|
+
print(" memory-os - to launch the interactive menu")
|
|
205
|
+
print("==================================================")
|
cli/commands/logs.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os logs [--tail N]
|
|
3
|
+
|
|
4
|
+
Tails the active log file to inspect system traces, errors, and indexing events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from infrastructure.workspace import get_logs_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def execute(args):
|
|
12
|
+
"""Run the logs command."""
|
|
13
|
+
tail_count = args.tail
|
|
14
|
+
|
|
15
|
+
# Resolve log file path
|
|
16
|
+
try:
|
|
17
|
+
log_file = get_logs_path() / "memory_os.log"
|
|
18
|
+
except Exception:
|
|
19
|
+
log_file = Path("logs/memory_os.log")
|
|
20
|
+
|
|
21
|
+
if not log_file.exists():
|
|
22
|
+
print(f"No log file found at '{log_file.resolve()}'. Run commands to generate logs.")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
print(f"─────────────────────────────")
|
|
26
|
+
print(f" Memory-OS Log Tail (Last {tail_count} lines)")
|
|
27
|
+
print(f" File: {log_file.resolve()}")
|
|
28
|
+
print(f"─────────────────────────────")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
with open(log_file, "r", encoding="utf-8", errors="ignore") as f:
|
|
32
|
+
lines = f.readlines()
|
|
33
|
+
tail_lines = lines[-tail_count:]
|
|
34
|
+
for line in tail_lines:
|
|
35
|
+
print(line.rstrip())
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"❌ Failed to read log file: {e}")
|
|
38
|
+
print(f"─────────────────────────────")
|
cli/commands/monitor.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os monitor
|
|
3
|
+
|
|
4
|
+
Displays system observability and performance latency diagnostics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from infrastructure.observability import get_performance_summary
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(args):
|
|
11
|
+
"""Run the status command."""
|
|
12
|
+
from storage.db import init_db
|
|
13
|
+
try:
|
|
14
|
+
init_db()
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
print("──────────────────────────────────────────────────")
|
|
19
|
+
print(" Memory-OS Observability Dashboard")
|
|
20
|
+
print("──────────────────────────────────────────────────")
|
|
21
|
+
|
|
22
|
+
summary = get_performance_summary()
|
|
23
|
+
|
|
24
|
+
# Format and present results
|
|
25
|
+
print(" Sync Performance:")
|
|
26
|
+
print(f" Total Sync Runs: {summary['sync_count']}")
|
|
27
|
+
print(f" Last Sync Duration: {summary['last_sync_duration']:.2f}s")
|
|
28
|
+
print(f" Avg Sync Duration: {summary['avg_sync_duration']:.2f}s")
|
|
29
|
+
|
|
30
|
+
print("\n Data Insertion & indexing Latencies:")
|
|
31
|
+
if summary['avg_chunk_latency'] > 0:
|
|
32
|
+
print(f" Avg Embedding Time: {summary['avg_chunk_latency']*1000:.1f}ms / chunk")
|
|
33
|
+
else:
|
|
34
|
+
print(" Avg Embedding Time: N/A (no events recorded)")
|
|
35
|
+
|
|
36
|
+
if summary['avg_vector_latency'] > 0:
|
|
37
|
+
print(f" Qdrant Upload Time: {summary['avg_vector_latency']*1000:.1f}ms / vector")
|
|
38
|
+
else:
|
|
39
|
+
print(" Qdrant Upload Time: N/A")
|
|
40
|
+
|
|
41
|
+
if summary['avg_neo4j_sync'] > 0:
|
|
42
|
+
print(f" Neo4j Graph Sync: {summary['avg_neo4j_sync']:.2f}s (Total Nodes: {summary['total_nodes_synced']}, Rels: {summary['total_rels_synced']})")
|
|
43
|
+
else:
|
|
44
|
+
print(" Neo4j Graph Sync: N/A")
|
|
45
|
+
|
|
46
|
+
print("\n LLM & RAG Query Latencies:")
|
|
47
|
+
print(f" Total LLM Queries: {summary['llm_call_count']}")
|
|
48
|
+
if summary['avg_llm_duration'] > 0:
|
|
49
|
+
print(f" Avg LLM Call Time: {summary['avg_llm_duration']:.2f}s")
|
|
50
|
+
else:
|
|
51
|
+
print(" Avg LLM Call Time: N/A")
|
|
52
|
+
|
|
53
|
+
if summary['avg_retrieval_duration'] > 0:
|
|
54
|
+
print(f" Avg Retrieval Time: {summary['avg_retrieval_duration']*1000:.1f}ms (hybrid)")
|
|
55
|
+
else:
|
|
56
|
+
print(" Avg Retrieval Time: N/A")
|
|
57
|
+
|
|
58
|
+
if summary['avg_rag_duration'] > 0:
|
|
59
|
+
print(f" Average RAG Time: {summary['avg_rag_duration']:.2f}s")
|
|
60
|
+
else:
|
|
61
|
+
print(" Average RAG Time: N/A")
|
|
62
|
+
|
|
63
|
+
print("──────────────────────────────────────────────────")
|
cli/commands/plugins.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os plugins
|
|
3
|
+
|
|
4
|
+
Displays installed and available connector plugins.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from connectors.registry import discover_connectors
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(args):
|
|
11
|
+
"""Run the plugins command."""
|
|
12
|
+
print("─────────────────────────────")
|
|
13
|
+
print(" Memory-OS Connector Plugins")
|
|
14
|
+
print("─────────────────────────────")
|
|
15
|
+
|
|
16
|
+
# Discover active/installed connectors
|
|
17
|
+
installed = discover_connectors()
|
|
18
|
+
|
|
19
|
+
print(" Installed (Active):")
|
|
20
|
+
for conn in installed:
|
|
21
|
+
print(f" ✓ {conn.name} ({conn.slug})")
|
|
22
|
+
|
|
23
|
+
# Available but disabled / not-yet-integrated plugins
|
|
24
|
+
disabled = [
|
|
25
|
+
("Slack", "slack"),
|
|
26
|
+
("Jira", "jira"),
|
|
27
|
+
("Google Drive", "google-drive")
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
print("\n Disabled (Available):")
|
|
31
|
+
for name, slug in disabled:
|
|
32
|
+
print(f" - {name} ({slug})")
|
|
33
|
+
|
|
34
|
+
print("─────────────────────────────")
|
|
35
|
+
print(" To enable, run: memory-os config set composio.api_key <key>")
|
|
36
|
+
print(" Then authenticate during: memory-os init")
|
|
37
|
+
print("─────────────────────────────")
|
cli/commands/start.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os start
|
|
3
|
+
|
|
4
|
+
Starts the Docker services (Neo4j and Qdrant) via docker compose
|
|
5
|
+
and waits for them to become healthy.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from infrastructure.compose import compose_up, wait_for_services
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute(args):
|
|
13
|
+
"""Run the start command."""
|
|
14
|
+
print("Starting Memory-OS local services (Neo4j, Qdrant)...")
|
|
15
|
+
if not compose_up():
|
|
16
|
+
print("❌ Failed to run docker compose up.")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
print("Waiting for services to become healthy...")
|
|
20
|
+
if not wait_for_services(timeout=60):
|
|
21
|
+
print("❌ Services failed to start or respond to health checks in time.")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
print("✓ Services started successfully and are healthy.")
|
cli/commands/status.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os status
|
|
3
|
+
|
|
4
|
+
Shows workspace and service status information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute(args):
|
|
9
|
+
"""Run the status command."""
|
|
10
|
+
import logging
|
|
11
|
+
from infrastructure.workspace import get_active_profile, get_db_path
|
|
12
|
+
|
|
13
|
+
active = get_active_profile()
|
|
14
|
+
db_file = get_db_path(active)
|
|
15
|
+
|
|
16
|
+
# If the workspace database doesn't exist, show not initialized
|
|
17
|
+
if not db_file.exists():
|
|
18
|
+
print("----------------------------------")
|
|
19
|
+
print("Workspace not initialized.")
|
|
20
|
+
print("")
|
|
21
|
+
print("Run:")
|
|
22
|
+
print("memory-os init")
|
|
23
|
+
print("----------------------------------")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
# Call init_db to ensure schema exists
|
|
27
|
+
try:
|
|
28
|
+
from storage.db import init_db
|
|
29
|
+
init_db()
|
|
30
|
+
except Exception as e:
|
|
31
|
+
logging.getLogger("cli.status").exception("Failed to initialize database schema")
|
|
32
|
+
print("----------------------------------")
|
|
33
|
+
print("Workspace not initialized.")
|
|
34
|
+
print("")
|
|
35
|
+
print("Run:")
|
|
36
|
+
print("memory-os init")
|
|
37
|
+
print("----------------------------------")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
from storage.db import (
|
|
41
|
+
get_repo_count,
|
|
42
|
+
get_email_count,
|
|
43
|
+
get_repository_document_count,
|
|
44
|
+
get_document_chunk_count,
|
|
45
|
+
)
|
|
46
|
+
from core.vector_store import get_vector_index_stats
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
repos = get_repo_count()
|
|
50
|
+
docs = get_repository_document_count()
|
|
51
|
+
emails = get_email_count()
|
|
52
|
+
chunks = get_document_chunk_count()
|
|
53
|
+
stats = get_vector_index_stats()
|
|
54
|
+
vectors = stats.get('vectors', 0)
|
|
55
|
+
embedding_model = stats.get('embedding_model', 'N/A')
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logging.getLogger("cli.status").exception("SQLite OperationalError in status")
|
|
58
|
+
print("----------------------------------")
|
|
59
|
+
print("Workspace has not been initialized.")
|
|
60
|
+
print("")
|
|
61
|
+
print("Run:")
|
|
62
|
+
print("memory-os init")
|
|
63
|
+
print("----------------------------------")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Check if empty (repos=0, docs=0, emails=0, vectors=0)
|
|
67
|
+
if repos == 0 and docs == 0 and emails == 0 and vectors == 0:
|
|
68
|
+
print("----------------------------------")
|
|
69
|
+
print(f"Workspace Profile : {active}")
|
|
70
|
+
print("")
|
|
71
|
+
print(f"Repositories : 0")
|
|
72
|
+
print(f"Documents : 0")
|
|
73
|
+
print(f"Emails : 0")
|
|
74
|
+
print(f"Vectors : 0")
|
|
75
|
+
print("")
|
|
76
|
+
print("Workspace is initialized but contains no indexed data.")
|
|
77
|
+
print("")
|
|
78
|
+
print("Run:")
|
|
79
|
+
print("memory-os sync")
|
|
80
|
+
print("----------------------------------")
|
|
81
|
+
else:
|
|
82
|
+
print("─────────────────────────────")
|
|
83
|
+
print(" memory-os status")
|
|
84
|
+
print("─────────────────────────────")
|
|
85
|
+
print(f" Repositories {repos}")
|
|
86
|
+
print(f" Documents {docs}")
|
|
87
|
+
print(f" Emails {emails}")
|
|
88
|
+
print(f" Chunks {chunks}")
|
|
89
|
+
print(f" Vectors {vectors}")
|
|
90
|
+
print(f" Embedding Model {embedding_model}")
|
|
91
|
+
print("─────────────────────────────")
|
|
92
|
+
|
cli/commands/stop.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os stop
|
|
3
|
+
|
|
4
|
+
Stops the Docker services (Neo4j and Qdrant) via docker compose stop.
|
|
5
|
+
Keeps data volumes intact.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from infrastructure.compose import compose_stop
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute(args):
|
|
13
|
+
"""Run the stop command."""
|
|
14
|
+
print("Stopping Memory-OS local services (Neo4j, Qdrant)...")
|
|
15
|
+
if not compose_stop():
|
|
16
|
+
print("❌ Failed to run docker compose stop.")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
print("✓ Services stopped successfully. Data remains intact.")
|
cli/commands/version.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os version
|
|
3
|
+
|
|
4
|
+
Shows Memory-OS version, Python version, and Docker version.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(args):
|
|
11
|
+
"""Run the version command."""
|
|
12
|
+
from infrastructure.docker import check_docker_installed
|
|
13
|
+
from infrastructure.health import _get_version
|
|
14
|
+
|
|
15
|
+
version = _get_version()
|
|
16
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
17
|
+
|
|
18
|
+
docker_available, docker_version = check_docker_installed()
|
|
19
|
+
|
|
20
|
+
print(f"Memory-OS {version}")
|
|
21
|
+
print(f"Python {python_version}")
|
|
22
|
+
if docker_available:
|
|
23
|
+
print(f"Docker {docker_version}")
|
|
24
|
+
else:
|
|
25
|
+
print("Docker not installed")
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command: memory-os workspace create|switch|list|delete|info
|
|
3
|
+
|
|
4
|
+
Provides management of active workspaces and profile structures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import shutil
|
|
9
|
+
import os
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from infrastructure.workspace import (
|
|
13
|
+
get_active_profile,
|
|
14
|
+
list_profiles,
|
|
15
|
+
create_profile,
|
|
16
|
+
delete_profile,
|
|
17
|
+
set_active_profile,
|
|
18
|
+
get_workspace_size,
|
|
19
|
+
get_profile_path,
|
|
20
|
+
get_db_path
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def execute(args):
|
|
25
|
+
"""Run the workspace command."""
|
|
26
|
+
action = args.workspace_action
|
|
27
|
+
|
|
28
|
+
if action == "list":
|
|
29
|
+
profiles = list_profiles()
|
|
30
|
+
active = get_active_profile()
|
|
31
|
+
print("─────────────────────────────")
|
|
32
|
+
print(" Memory-OS Workspace Profiles")
|
|
33
|
+
print("─────────────────────────────")
|
|
34
|
+
for p in profiles:
|
|
35
|
+
prefix = "* " if p == active else " "
|
|
36
|
+
print(f"{prefix}{p}")
|
|
37
|
+
print("─────────────────────────────")
|
|
38
|
+
|
|
39
|
+
elif action == "create":
|
|
40
|
+
name = args.name
|
|
41
|
+
try:
|
|
42
|
+
create_profile(name)
|
|
43
|
+
print(f"✓ Workspace profile '{name}' created successfully.")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
print(f"❌ Failed to create workspace profile: {e}")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
elif action == "switch":
|
|
49
|
+
name = args.name
|
|
50
|
+
try:
|
|
51
|
+
set_active_profile(name)
|
|
52
|
+
print(f"✓ Switched active workspace profile to '{name}'.")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"❌ Failed to switch workspace profile: {e}")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
elif action == "delete":
|
|
58
|
+
name = args.name
|
|
59
|
+
active = get_active_profile()
|
|
60
|
+
if name == active:
|
|
61
|
+
print(f"❌ Cannot delete the currently active workspace profile '{name}'. Switch first.")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
confirm = input(f"⚠️ Are you sure you want to delete workspace profile '{name}' and ALL its databases? (y/N): ").strip().lower()
|
|
65
|
+
if confirm == "y":
|
|
66
|
+
try:
|
|
67
|
+
delete_profile(name)
|
|
68
|
+
print(f"✓ Workspace profile '{name}' deleted successfully.")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"❌ Failed to delete workspace profile: {e}")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
else:
|
|
73
|
+
print("Deletion cancelled.")
|
|
74
|
+
|
|
75
|
+
elif action == "info":
|
|
76
|
+
active = get_active_profile()
|
|
77
|
+
path = get_profile_path(active)
|
|
78
|
+
db_file = get_db_path(active)
|
|
79
|
+
|
|
80
|
+
# Retrieve counts
|
|
81
|
+
repos = 0
|
|
82
|
+
vectors = 0
|
|
83
|
+
nodes = 0
|
|
84
|
+
last_sync = "Never"
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
from storage.db import get_repo_count
|
|
88
|
+
repos = get_repo_count()
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
from core.vector_store import get_vector_index_stats
|
|
94
|
+
vectors = get_vector_index_stats().get("vectors", 0)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
from storage.graph import GraphStore
|
|
100
|
+
graph = GraphStore()
|
|
101
|
+
if graph.is_fallback:
|
|
102
|
+
from storage.db import get_connection
|
|
103
|
+
conn = get_connection()
|
|
104
|
+
cursor = conn.cursor()
|
|
105
|
+
cursor.execute("SELECT COUNT(*) FROM graph_nodes")
|
|
106
|
+
nodes = cursor.fetchone()[0]
|
|
107
|
+
conn.close()
|
|
108
|
+
else:
|
|
109
|
+
with graph.driver.session() as session:
|
|
110
|
+
nodes = session.run("MATCH (n) RETURN count(n) AS c").single()["c"]
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# Calculate last sync timestamp from DB file modification or max chunk created_at
|
|
115
|
+
if db_file.exists():
|
|
116
|
+
try:
|
|
117
|
+
from storage.db import get_connection
|
|
118
|
+
conn = get_connection()
|
|
119
|
+
cursor = conn.cursor()
|
|
120
|
+
cursor.execute("SELECT MAX(created_at) FROM document_chunks")
|
|
121
|
+
row = cursor.fetchone()
|
|
122
|
+
if row and row[0]:
|
|
123
|
+
last_sync = row[0][:19].replace("T", " ")
|
|
124
|
+
else:
|
|
125
|
+
mtime = os.path.getmtime(db_file)
|
|
126
|
+
last_sync = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')
|
|
127
|
+
conn.close()
|
|
128
|
+
except Exception:
|
|
129
|
+
import os
|
|
130
|
+
from datetime import datetime
|
|
131
|
+
mtime = os.path.getmtime(db_file)
|
|
132
|
+
last_sync = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')
|
|
133
|
+
|
|
134
|
+
size_bytes = get_workspace_size(active)
|
|
135
|
+
if size_bytes < 1024 * 1024:
|
|
136
|
+
size_str = f"{size_bytes / 1024:.0f} KB"
|
|
137
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
138
|
+
size_str = f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
139
|
+
else:
|
|
140
|
+
size_str = f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
|
141
|
+
|
|
142
|
+
print("──────────────────────────────────────────────────")
|
|
143
|
+
print(f" Memory-OS Workspace Info: {active}")
|
|
144
|
+
print("──────────────────────────────────────────────────")
|
|
145
|
+
print(f" Profile Location: {path.resolve()}")
|
|
146
|
+
print(f" Repositories: {repos}")
|
|
147
|
+
print(f" Vectors: {vectors}")
|
|
148
|
+
print(f" Graph Nodes: {nodes}")
|
|
149
|
+
print(f" Disk Storage Size: {size_str}")
|
|
150
|
+
print(f" Last Sync Time: {last_sync}")
|
|
151
|
+
print("──────────────────────────────────────────────────")
|
|
152
|
+
|
|
153
|
+
else:
|
|
154
|
+
print("Usage: memory-os workspace create|switch|list|delete|info")
|