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
@@ -0,0 +1,338 @@
1
+ """
2
+ Infrastructure: Service health checks.
3
+
4
+ Individual check functions for Neo4j, Qdrant, SQLite, Groq, Composio,
5
+ embedding model, and connectors. Used by the ``doctor`` command.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import logging
11
+
12
+ logger = logging.getLogger("infrastructure.health")
13
+
14
+
15
+ def check_python() -> tuple[bool, str]:
16
+ """Check Python version."""
17
+ version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
18
+ ok = sys.version_info >= (3, 12)
19
+ return ok, version
20
+
21
+
22
+ def check_neo4j() -> tuple[bool, str]:
23
+ """Attempt a Neo4j bolt connection."""
24
+ try:
25
+ from neo4j import GraphDatabase
26
+ uri = os.getenv("NEO4J_URI", "bolt://localhost:7687")
27
+ user = os.getenv("NEO4J_USER", "neo4j")
28
+ password = os.getenv("NEO4J_PASSWORD", "password")
29
+ driver = GraphDatabase.driver(uri, auth=(user, password))
30
+ driver.verify_connectivity()
31
+ driver.close()
32
+ return True, "Healthy"
33
+ except Exception as e:
34
+ return False, str(e)
35
+
36
+
37
+ def check_qdrant() -> tuple[bool, str]:
38
+ """Ping the Qdrant HTTP health endpoint."""
39
+ try:
40
+ import urllib.request
41
+ port = os.getenv("QDRANT_PORT", "6333")
42
+ url = f"http://localhost:{port}/healthz"
43
+ req = urllib.request.urlopen(url, timeout=5)
44
+ if req.status == 200:
45
+ return True, "Healthy"
46
+ return False, f"HTTP {req.status}"
47
+ except Exception as e:
48
+ return False, str(e)
49
+
50
+
51
+ def check_sqlite(db_path: str | None = None) -> tuple[bool, str]:
52
+ """Verify a SQLite database file is valid."""
53
+ import sqlite3
54
+ if db_path is None:
55
+ db_path = os.getenv("MEMORY_OS_DB_PATH", "memory.db")
56
+ if not os.path.exists(db_path):
57
+ return False, f"File not found: {db_path}"
58
+ try:
59
+ conn = sqlite3.connect(db_path)
60
+ cursor = conn.cursor()
61
+ cursor.execute("PRAGMA integrity_check")
62
+ result = cursor.fetchone()
63
+ conn.close()
64
+ if result and result[0] == "ok":
65
+ return True, "Healthy"
66
+ return False, f"Integrity check: {result}"
67
+ except Exception as e:
68
+ return False, str(e)
69
+
70
+
71
+ def check_groq_api() -> tuple[bool, str]:
72
+ """Make a minimal Groq API test call."""
73
+ try:
74
+ from groq import Groq
75
+ key = os.getenv("GROQ_API_KEY")
76
+ if not key:
77
+ return False, "No API key"
78
+ client = Groq(api_key=key)
79
+ # List models as a lightweight connectivity test
80
+ client.models.list()
81
+ return True, "Connected"
82
+ except Exception as e:
83
+ return False, str(e)
84
+
85
+
86
+ def check_composio() -> tuple[bool, str]:
87
+ """Verify Composio toolkit status."""
88
+ try:
89
+ from composio import Composio
90
+ c = Composio()
91
+ s = c.create(user_id=os.getenv("COMPOSIO_USER_ID", "user_123"))
92
+ toolkits = s.toolkits()
93
+ count = len(toolkits.items) if toolkits and toolkits.items else 0
94
+ return True, f"Connected ({count} toolkits)"
95
+ except Exception as e:
96
+ return False, str(e)
97
+
98
+
99
+ def check_embedding_model() -> tuple[bool, str]:
100
+ """Check if the sentence-transformers model is cached locally."""
101
+ try:
102
+ from sentence_transformers import SentenceTransformer
103
+ model_name = os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")
104
+ # Try loading from cache only
105
+ SentenceTransformer(model_name, local_files_only=True)
106
+ return True, model_name
107
+ except Exception:
108
+ model_name = os.getenv("EMBEDDING_MODEL", "all-MiniLM-L6-v2")
109
+ return False, f"{model_name} (not cached)"
110
+
111
+
112
+ def check_connector(name: str) -> tuple[bool, str]:
113
+ """Check if a Composio connector (github, gmail, notion) is active."""
114
+ try:
115
+ from composio import Composio
116
+ c = Composio()
117
+ s = c.create(user_id=os.getenv("COMPOSIO_USER_ID", "user_123"))
118
+ toolkits = s.toolkits()
119
+ tk = next((t for t in toolkits.items if t.slug == name), None)
120
+ if tk and tk.connection and tk.connection.is_active:
121
+ return True, "Connected"
122
+ return False, "Not connected"
123
+ except Exception as e:
124
+ return False, str(e)
125
+
126
+
127
+ def check_workspace() -> tuple[bool, str]:
128
+ """Check if the workspace directory exists and is writable."""
129
+ from infrastructure.workspace import get_workspace_root
130
+ root = get_workspace_root()
131
+ if not root.exists():
132
+ return False, "Not initialized"
133
+ # Check writable
134
+ test_file = root / ".write_test"
135
+ try:
136
+ test_file.write_text("test", encoding="utf-8")
137
+ test_file.unlink()
138
+ return True, str(root)
139
+ except Exception as e:
140
+ return False, f"Not writable: {e}"
141
+
142
+
143
+ def check_disk_usage() -> tuple[bool, str]:
144
+ """Calculate workspace disk usage."""
145
+ from infrastructure.workspace import get_workspace_root, get_workspace_size
146
+ root = get_workspace_root()
147
+ if not root.exists():
148
+ return False, "Workspace not found"
149
+ size_bytes = get_workspace_size()
150
+ if size_bytes < 1024 * 1024:
151
+ size_str = f"{size_bytes / 1024:.0f} KB"
152
+ elif size_bytes < 1024 * 1024 * 1024:
153
+ size_str = f"{size_bytes / (1024 * 1024):.0f} MB"
154
+ else:
155
+ size_str = f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
156
+ return True, size_str
157
+
158
+
159
+ def check_memory_usage() -> tuple[bool, str]:
160
+ """Report current process memory usage."""
161
+ try:
162
+ import psutil
163
+ process = psutil.Process(os.getpid())
164
+ mem = process.memory_info().rss
165
+ if mem < 1024 * 1024:
166
+ return True, f"{mem / 1024:.0f} KB"
167
+ return True, f"{mem / (1024 * 1024):.0f} MB"
168
+ except ImportError:
169
+ # psutil not available, use a rough estimate
170
+ return True, "N/A (psutil not installed)"
171
+ except Exception as e:
172
+ return False, str(e)
173
+
174
+
175
+ def check_config_validation() -> tuple[bool, str]:
176
+ """Verify configuration validation."""
177
+ from infrastructure.config import get_config, DEFAULTS
178
+ try:
179
+ config = get_config()
180
+ for key in DEFAULTS:
181
+ if key not in config:
182
+ return False, f"Missing section '{key}'"
183
+ if isinstance(DEFAULTS[key], dict):
184
+ if not isinstance(config[key], dict):
185
+ return False, f"Section '{key}' must be a dictionary"
186
+ for subkey in DEFAULTS[key]:
187
+ if subkey not in config[key]:
188
+ return False, f"Missing subkey '{key}.{subkey}'"
189
+ return True, "Valid"
190
+ except Exception as e:
191
+ return False, str(e)
192
+
193
+
194
+ def check_active_plugins() -> tuple[bool, str]:
195
+ """Verify connector plugins registration and connection status."""
196
+ try:
197
+ from connectors.registry import discover_connectors
198
+ active = []
199
+ for conn in discover_connectors():
200
+ if conn.authenticate():
201
+ active.append(conn.name)
202
+ if active:
203
+ return True, f"{len(active)} active ({', '.join(active)})"
204
+ return False, "0 active"
205
+ except Exception as e:
206
+ return False, str(e)
207
+
208
+
209
+ def run_all_checks() -> list[tuple[str, bool, str]]:
210
+ """Run all health checks and return a list of (name, passed, detail)."""
211
+ from infrastructure.docker import check_docker_installed, check_docker_compose_installed
212
+
213
+ checks = []
214
+
215
+ # Python
216
+ ok, detail = check_python()
217
+ checks.append(("Python", ok, detail))
218
+
219
+ # Docker
220
+ ok, detail = check_docker_installed()
221
+ checks.append(("Docker", ok, detail))
222
+
223
+ # Docker Compose
224
+ ok, detail = check_docker_compose_installed()
225
+ checks.append(("Docker Compose", ok, detail))
226
+
227
+ # SQLite
228
+ ok, detail = check_sqlite()
229
+ checks.append(("SQLite", ok, detail))
230
+
231
+ # Neo4j
232
+ ok, detail = check_neo4j()
233
+ checks.append(("Neo4j", ok, detail))
234
+
235
+ # Qdrant
236
+ ok, detail = check_qdrant()
237
+ checks.append(("Qdrant", ok, detail))
238
+
239
+ # Embedding Model
240
+ ok, detail = check_embedding_model()
241
+ checks.append(("Embedding Model", ok, detail))
242
+
243
+ # Groq
244
+ ok, detail = check_groq_api()
245
+ checks.append(("Groq", ok, detail))
246
+
247
+ # Composio
248
+ ok, detail = check_composio()
249
+ checks.append(("Composio", ok, detail))
250
+
251
+ # Config Validation
252
+ ok, detail = check_config_validation()
253
+ checks.append(("Config Validation", ok, detail))
254
+
255
+ # Active Plugins
256
+ ok, detail = check_active_plugins()
257
+ checks.append(("Active Plugins", ok, detail))
258
+
259
+ # Connectors
260
+ for name in ["github", "gmail", "notion"]:
261
+ ok, detail = check_connector(name)
262
+ checks.append((name.capitalize(), ok, detail))
263
+
264
+ # Workspace
265
+ ok, detail = check_workspace()
266
+ checks.append(("Workspace", ok, detail))
267
+
268
+ # Disk Usage
269
+ ok, detail = check_disk_usage()
270
+ checks.append(("Disk Usage", ok, detail))
271
+
272
+ # Memory Usage
273
+ ok, detail = check_memory_usage()
274
+ checks.append(("Memory Usage", ok, detail))
275
+
276
+ # Version
277
+ checks.append(("Version", True, _get_version()))
278
+
279
+ # Workspace Profile & Database Counts
280
+ try:
281
+ from infrastructure.workspace import get_active_profile
282
+ checks.append(("Workspace Profile", True, get_active_profile()))
283
+ except Exception:
284
+ pass
285
+
286
+ try:
287
+ from core.vector_store import get_vector_index_stats
288
+ vectors = get_vector_index_stats().get("vectors", 0)
289
+ checks.append(("Vector Count", True, f"{vectors:,}"))
290
+ except Exception:
291
+ checks.append(("Vector Count", True, "0"))
292
+
293
+ try:
294
+ from storage.db import get_repo_count, get_email_count
295
+ repos = get_repo_count()
296
+ emails = get_email_count()
297
+ checks.append(("Repositories", True, str(repos)))
298
+ checks.append(("Emails", True, str(emails)))
299
+ except Exception:
300
+ checks.append(("Repositories", True, "0"))
301
+ checks.append(("Emails", True, "0"))
302
+
303
+ try:
304
+ from storage.graph import GraphStore
305
+ nodes = 0
306
+ rels = 0
307
+ graph = GraphStore()
308
+ if graph.is_fallback:
309
+ from storage.db import get_connection
310
+ conn = get_connection()
311
+ cursor = conn.cursor()
312
+ cursor.execute("SELECT COUNT(*) FROM graph_nodes")
313
+ nodes = cursor.fetchone()[0]
314
+ cursor.execute("SELECT COUNT(*) FROM graph_relationships")
315
+ rels = cursor.fetchone()[0]
316
+ conn.close()
317
+ else:
318
+ with graph.driver.session() as session:
319
+ r_nodes = session.run("MATCH (n) RETURN count(n) AS c")
320
+ nodes = r_nodes.single()["c"]
321
+ r_rels = session.run("MATCH ()-[r]->() RETURN count(r) AS c")
322
+ rels = r_rels.single()["c"]
323
+ checks.append(("Graph Nodes", True, str(nodes)))
324
+ checks.append(("Graph Relationships", True, str(rels)))
325
+ except Exception:
326
+ checks.append(("Graph Nodes", True, "0"))
327
+ checks.append(("Graph Relationships", True, "0"))
328
+
329
+ return checks
330
+
331
+
332
+ def _get_version() -> str:
333
+ """Get the Memory-OS package version."""
334
+ try:
335
+ from importlib.metadata import version
336
+ return version("memory-os")
337
+ except Exception:
338
+ return "0.1.0"
@@ -0,0 +1,160 @@
1
+ """
2
+ Infrastructure: Observability and Performance Monitoring.
3
+
4
+ Parses logs from the active log file (or tracks metrics) to measure:
5
+ - Sync execution time
6
+ - Embedding generation latency
7
+ - Qdrant upload time
8
+ - Neo4j insertion time
9
+ - LLM call count and durations
10
+ - RAG query execution time
11
+ """
12
+
13
+ import os
14
+ import re
15
+ from pathlib import Path
16
+ from infrastructure.workspace import get_workspace_root
17
+
18
+
19
+ def get_log_file_path() -> Path:
20
+ """Return path to active memory_os.log."""
21
+ return get_workspace_root() / "logs" / "memory_os.log"
22
+
23
+
24
+ def parse_observability_metrics() -> dict:
25
+ """Scan the memory_os.log file and parse latency/observability metrics."""
26
+ log_path = get_log_file_path()
27
+
28
+ # Fallback to local logs directory if not in home workspace yet
29
+ if not log_path.exists():
30
+ log_path = Path("logs/memory_os.log")
31
+
32
+ metrics = {
33
+ "sync_times": [],
34
+ "embedding_times": [], # (duration, num_chunks)
35
+ "qdrant_uploads": [], # (duration, num_vectors)
36
+ "neo4j_inserts": [], # (duration, nodes, relationships)
37
+ "llm_calls": [],
38
+ "retrievals": [],
39
+ "rag_times": [],
40
+ }
41
+
42
+ if not log_path.exists():
43
+ return metrics
44
+
45
+ # Regex definitions matching existing logs in core/storage/main
46
+ r_sync = re.compile(r"Sync completed in (\d+\.\d+)s")
47
+ r_rebuild = re.compile(r"Full rebuild completed in (\d+\.\d+)s")
48
+ r_embed = re.compile(r"Embedding generation complete for (\d+) chunks in (\d+\.\d+)s")
49
+ r_upload = re.compile(r"Vector upload complete for (\d+) vectors in (\d+\.\d+)s")
50
+ r_graph = re.compile(r"Graph Sync Complete.*Nodes: (\d+), Relationships: (\d+)\. Duration: (\d+\.\d+)s")
51
+ r_llm = re.compile(r"Groq LLM generation finished in (\d+\.\d+)s")
52
+ r_retrieval = re.compile(r"Hybrid retrieval finished for query:.*Retrieved (\d+) items in (\d+\.\d+)s")
53
+ r_rag = re.compile(r"RAG pipeline complete for question:.*in (\d+\.\d+)s")
54
+
55
+ try:
56
+ with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
57
+ for line in f:
58
+ # Sync
59
+ m = r_sync.search(line) or r_rebuild.search(line)
60
+ if m:
61
+ metrics["sync_times"].append(float(m.group(1)))
62
+ continue
63
+
64
+ # Embeddings
65
+ m = r_embed.search(line)
66
+ if m:
67
+ metrics["embedding_times"].append((float(m.group(2)), int(m.group(1))))
68
+ continue
69
+
70
+ # Qdrant Uploads
71
+ m = r_upload.search(line)
72
+ if m:
73
+ metrics["qdrant_uploads"].append((float(m.group(2)), int(m.group(1))))
74
+ continue
75
+
76
+ # Neo4j Graph Sync
77
+ m = r_graph.search(line)
78
+ if m:
79
+ metrics["neo4j_inserts"].append((float(m.group(3)), int(m.group(1)), int(m.group(2))))
80
+ continue
81
+
82
+ # LLM Calls
83
+ m = r_llm.search(line)
84
+ if m:
85
+ metrics["llm_calls"].append(float(m.group(1)))
86
+ continue
87
+
88
+ # Hybrid Retrieval
89
+ m = r_retrieval.search(line)
90
+ if m:
91
+ metrics["retrievals"].append(float(m.group(2)))
92
+ continue
93
+
94
+ # RAG Times
95
+ m = r_rag.search(line)
96
+ if m:
97
+ metrics["rag_times"].append(float(m.group(1)))
98
+ continue
99
+ except Exception:
100
+ pass
101
+
102
+ return metrics
103
+
104
+
105
+ def get_performance_summary() -> dict:
106
+ """Format metrics into a user-friendly statistics summary."""
107
+ data = parse_observability_metrics()
108
+
109
+ def avg(lst):
110
+ return sum(lst) / len(lst) if lst else 0.0
111
+
112
+ # Sync
113
+ syncs = data["sync_times"]
114
+ last_sync = syncs[-1] if syncs else 0.0
115
+ avg_sync = avg(syncs)
116
+
117
+ # Embeddings
118
+ embeds = data["embedding_times"]
119
+ total_embed_time = sum(t for t, _ in embeds)
120
+ total_chunks = sum(c for _, c in embeds)
121
+ avg_chunk_latency = (total_embed_time / total_chunks) if total_chunks > 0 else 0.0
122
+
123
+ # Qdrant uploads
124
+ uploads = data["qdrant_uploads"]
125
+ total_upload_time = sum(t for t, _ in uploads)
126
+ total_vectors = sum(v for _, v in uploads)
127
+ avg_vector_latency = (total_upload_time / total_vectors) if total_vectors > 0 else 0.0
128
+
129
+ # Neo4j
130
+ inserts = data["neo4j_inserts"]
131
+ avg_neo4j = avg([t for t, _, _ in inserts])
132
+ total_nodes = sum(n for _, n, _ in inserts)
133
+ total_rels = sum(r for _, _, r in inserts)
134
+
135
+ # LLM
136
+ llms = data["llm_calls"]
137
+ total_llm_calls = len(llms)
138
+ avg_llm = avg(llms)
139
+
140
+ # Retrieval & RAG
141
+ retrievals = data["retrievals"]
142
+ avg_retrieval = avg(retrievals)
143
+
144
+ rags = data["rag_times"]
145
+ avg_rag = avg(rags)
146
+
147
+ return {
148
+ "sync_count": len(syncs),
149
+ "last_sync_duration": last_sync,
150
+ "avg_sync_duration": avg_sync,
151
+ "avg_chunk_latency": avg_chunk_latency,
152
+ "avg_vector_latency": avg_vector_latency,
153
+ "avg_neo4j_sync": avg_neo4j,
154
+ "total_nodes_synced": total_nodes,
155
+ "total_rels_synced": total_rels,
156
+ "llm_call_count": total_llm_calls,
157
+ "avg_llm_duration": avg_llm,
158
+ "avg_retrieval_duration": avg_retrieval,
159
+ "avg_rag_duration": avg_rag,
160
+ }
@@ -0,0 +1,152 @@
1
+ """
2
+ Infrastructure: Workspace directory and profile management.
3
+
4
+ Manages the ~/.memory-os/ directory tree and workspace profiles
5
+ (default, work, personal, etc.).
6
+ """
7
+
8
+ import os
9
+ import shutil
10
+ from pathlib import Path
11
+
12
+
13
+ def get_workspace_root() -> Path:
14
+ """Return the root workspace directory (~/.memory-os/)."""
15
+ root = os.getenv("MEMORY_OS_WORKSPACE", "~/.memory-os")
16
+ return Path(os.path.expanduser(root))
17
+
18
+
19
+ def ensure_workspace(profile: str = "default"):
20
+ """Create the full workspace directory tree for a profile.
21
+
22
+ Creates:
23
+ ~/.memory-os/
24
+ ├── config.toml (not created here, handled by config.py)
25
+ ├── active_workspace
26
+ ├── workspaces/
27
+ │ └── <profile>/
28
+ │ ├── workspace.db (created by SQLite on first use)
29
+ │ ├── qdrant/
30
+ │ ├── neo4j/
31
+ │ └── embeddings/
32
+ ├── logs/
33
+ ├── cache/
34
+ └── backups/
35
+ """
36
+ root = get_workspace_root()
37
+
38
+ # Top-level shared directories
39
+ (root / "logs").mkdir(parents=True, exist_ok=True)
40
+ (root / "cache").mkdir(parents=True, exist_ok=True)
41
+ (root / "backups").mkdir(parents=True, exist_ok=True)
42
+ (root / "workspaces").mkdir(parents=True, exist_ok=True)
43
+
44
+ # Profile-specific directories
45
+ profile_dir = root / "workspaces" / profile
46
+ (profile_dir / "qdrant").mkdir(parents=True, exist_ok=True)
47
+ (profile_dir / "neo4j").mkdir(parents=True, exist_ok=True)
48
+ (profile_dir / "embeddings").mkdir(parents=True, exist_ok=True)
49
+
50
+ # Set active workspace if no active_workspace file exists
51
+ active_file = root / "active_workspace"
52
+ if not active_file.exists():
53
+ active_file.write_text(profile, encoding="utf-8")
54
+
55
+
56
+ def get_active_profile() -> str:
57
+ """Read the currently active workspace profile name."""
58
+ active_file = get_workspace_root() / "active_workspace"
59
+ if active_file.exists():
60
+ return active_file.read_text(encoding="utf-8").strip()
61
+ return "default"
62
+
63
+
64
+ def set_active_profile(name: str):
65
+ """Set the active workspace profile."""
66
+ root = get_workspace_root()
67
+ profile_dir = root / "workspaces" / name
68
+ if not profile_dir.exists():
69
+ raise FileNotFoundError(f"Workspace profile '{name}' does not exist.")
70
+ active_file = root / "active_workspace"
71
+ active_file.write_text(name, encoding="utf-8")
72
+
73
+
74
+ def list_profiles() -> list[str]:
75
+ """List all workspace profile names."""
76
+ workspaces_dir = get_workspace_root() / "workspaces"
77
+ if not workspaces_dir.exists():
78
+ return []
79
+ return sorted([
80
+ d.name for d in workspaces_dir.iterdir()
81
+ if d.is_dir()
82
+ ])
83
+
84
+
85
+ def create_profile(name: str):
86
+ """Create a new workspace profile."""
87
+ root = get_workspace_root()
88
+ profile_dir = root / "workspaces" / name
89
+ if profile_dir.exists():
90
+ raise FileExistsError(f"Workspace profile '{name}' already exists.")
91
+ ensure_workspace(profile=name)
92
+
93
+
94
+ def delete_profile(name: str):
95
+ """Delete a workspace profile directory."""
96
+ if name == get_active_profile():
97
+ raise ValueError(f"Cannot delete the active workspace profile '{name}'. Switch first.")
98
+ profile_dir = get_workspace_root() / "workspaces" / name
99
+ if not profile_dir.exists():
100
+ raise FileNotFoundError(f"Workspace profile '{name}' does not exist.")
101
+ shutil.rmtree(profile_dir)
102
+
103
+
104
+ def get_profile_path(name: str | None = None) -> Path:
105
+ """Return the path for a specific profile (or the active one)."""
106
+ if name is None:
107
+ name = get_active_profile()
108
+ return get_workspace_root() / "workspaces" / name
109
+
110
+
111
+ def get_db_path(profile: str | None = None) -> Path:
112
+ """Return the SQLite database path for a workspace profile."""
113
+ return get_profile_path(profile) / "workspace.db"
114
+
115
+
116
+ def get_qdrant_path(profile: str | None = None) -> Path:
117
+ """Return the Qdrant storage path for a workspace profile."""
118
+ return get_profile_path(profile) / "qdrant"
119
+
120
+
121
+ def get_neo4j_path(profile: str | None = None) -> Path:
122
+ """Return the Neo4j data path for a workspace profile."""
123
+ return get_profile_path(profile) / "neo4j"
124
+
125
+
126
+ def get_logs_path() -> Path:
127
+ """Return the shared logs directory path."""
128
+ return get_workspace_root() / "logs"
129
+
130
+
131
+ def get_backups_path() -> Path:
132
+ """Return the shared backups directory path."""
133
+ return get_workspace_root() / "backups"
134
+
135
+
136
+ def get_cache_path() -> Path:
137
+ """Return the shared cache directory path."""
138
+ return get_workspace_root() / "cache"
139
+
140
+
141
+ def get_workspace_size(profile: str | None = None) -> int:
142
+ """Calculate total disk usage in bytes for a workspace profile."""
143
+ path = get_profile_path(profile) if profile else get_workspace_root()
144
+ total = 0
145
+ for dirpath, dirnames, filenames in os.walk(path):
146
+ for f in filenames:
147
+ fp = os.path.join(dirpath, f)
148
+ try:
149
+ total += os.path.getsize(fp)
150
+ except OSError:
151
+ pass
152
+ return total
models/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # models package
models/memory.py ADDED
@@ -0,0 +1,28 @@
1
+ class Repository:
2
+ def __init__(self, repo_name: str, description: str, language: str, visibility: str, stars: int, forks: int, open_issues: int, default_branch: str, updated_at: str, url: str):
3
+ self.repo_name = repo_name
4
+ self.description = description
5
+ self.language = language
6
+ self.visibility = visibility
7
+ self.stars = stars
8
+ self.forks = forks
9
+ self.open_issues = open_issues
10
+ self.default_branch = default_branch
11
+ self.updated_at = updated_at
12
+ self.url = url
13
+
14
+ class RepositoryDocument:
15
+ def __init__(self, repo_name: str, file_name: str, content: str, source: str, synced_at: str):
16
+ self.repo_name = repo_name
17
+ self.file_name = file_name
18
+ self.content = content
19
+ self.source = source
20
+ self.synced_at = synced_at
21
+
22
+ class Email:
23
+ def __init__(self, message_id: str, subject: str, sender: str, snippet: str, received_at: str):
24
+ self.message_id = message_id
25
+ self.subject = subject
26
+ self.sender = sender
27
+ self.snippet = snippet
28
+ self.received_at = received_at
storage/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # storage package