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.
Files changed (50) hide show
  1. cli/__init__.py +1 -0
  2. cli/commands/__init__.py +1 -0
  3. cli/commands/benchmark.py +151 -0
  4. cli/commands/config_cmd.py +57 -0
  5. cli/commands/doctor.py +61 -0
  6. cli/commands/export.py +106 -0
  7. cli/commands/import_cmd.py +143 -0
  8. cli/commands/init.py +205 -0
  9. cli/commands/logs.py +38 -0
  10. cli/commands/monitor.py +63 -0
  11. cli/commands/plugins.py +37 -0
  12. cli/commands/start.py +24 -0
  13. cli/commands/status.py +92 -0
  14. cli/commands/stop.py +19 -0
  15. cli/commands/version.py +25 -0
  16. cli/commands/workspace.py +154 -0
  17. cli/main.py +496 -0
  18. cli/parser.py +188 -0
  19. cli_memory_os-0.1.0.dist-info/METADATA +231 -0
  20. cli_memory_os-0.1.0.dist-info/RECORD +50 -0
  21. cli_memory_os-0.1.0.dist-info/WHEEL +5 -0
  22. cli_memory_os-0.1.0.dist-info/entry_points.txt +2 -0
  23. cli_memory_os-0.1.0.dist-info/licenses/LICENSE +21 -0
  24. cli_memory_os-0.1.0.dist-info/top_level.txt +6 -0
  25. connectors/__init__.py +1 -0
  26. connectors/base.py +39 -0
  27. connectors/github.py +273 -0
  28. connectors/gmail.py +116 -0
  29. connectors/notion.py +156 -0
  30. connectors/registry.py +60 -0
  31. core/__init__.py +1 -0
  32. core/chunker.py +136 -0
  33. core/context_builder.py +407 -0
  34. core/embedder.py +42 -0
  35. core/llm.py +301 -0
  36. core/vector_store.py +629 -0
  37. infrastructure/__init__.py +1 -0
  38. infrastructure/compose.py +136 -0
  39. infrastructure/config.py +280 -0
  40. infrastructure/docker.py +61 -0
  41. infrastructure/health.py +338 -0
  42. infrastructure/observability.py +160 -0
  43. infrastructure/workspace.py +152 -0
  44. models/__init__.py +1 -0
  45. models/memory.py +28 -0
  46. storage/__init__.py +1 -0
  47. storage/db.py +528 -0
  48. storage/graph.py +324 -0
  49. storage/schema.sql +57 -0
  50. 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"─────────────────────────────")
@@ -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("──────────────────────────────────────────────────")
@@ -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.")
@@ -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")