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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # cli package
@@ -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)