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/main.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory-OS CLI entry point.
|
|
3
|
+
|
|
4
|
+
Handles argument parsing, config loading, command routing,
|
|
5
|
+
and the interactive REPL.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from cli.parser import build_parser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def setup_logging():
|
|
16
|
+
"""Configure standardized rotating logging."""
|
|
17
|
+
from logging.handlers import RotatingFileHandler
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from infrastructure.workspace import get_logs_path
|
|
22
|
+
logs_dir = get_logs_path()
|
|
23
|
+
except Exception:
|
|
24
|
+
logs_dir = Path("logs")
|
|
25
|
+
|
|
26
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
log_file = logs_dir / "memory_os.log"
|
|
28
|
+
|
|
29
|
+
debug_mode = os.getenv("DEBUG", "false").lower() == "true"
|
|
30
|
+
level = logging.DEBUG if debug_mode else logging.INFO
|
|
31
|
+
|
|
32
|
+
file_handler = RotatingFileHandler(
|
|
33
|
+
log_file,
|
|
34
|
+
maxBytes=5 * 1024 * 1024,
|
|
35
|
+
backupCount=3,
|
|
36
|
+
encoding="utf-8"
|
|
37
|
+
)
|
|
38
|
+
file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))
|
|
39
|
+
file_handler.setLevel(level)
|
|
40
|
+
|
|
41
|
+
stream_handler = logging.StreamHandler(sys.stderr) if debug_mode else logging.NullHandler()
|
|
42
|
+
|
|
43
|
+
root_logger = logging.getLogger()
|
|
44
|
+
root_logger.setLevel(level)
|
|
45
|
+
|
|
46
|
+
for h in list(root_logger.handlers):
|
|
47
|
+
root_logger.removeHandler(h)
|
|
48
|
+
|
|
49
|
+
root_logger.addHandler(file_handler)
|
|
50
|
+
root_logger.addHandler(stream_handler)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_preview_snippet(content: str, query: str, length: int = 120) -> str:
|
|
54
|
+
"""Extract a relevant preview snippet from content around the query match."""
|
|
55
|
+
if not content:
|
|
56
|
+
return ""
|
|
57
|
+
idx = content.lower().find(query.lower())
|
|
58
|
+
if idx != -1:
|
|
59
|
+
start = max(0, idx - 40)
|
|
60
|
+
if start > 0:
|
|
61
|
+
space_idx = content.find(" ", start, idx)
|
|
62
|
+
if space_idx != -1:
|
|
63
|
+
start = space_idx + 1
|
|
64
|
+
end = min(len(content), idx + len(query) + 80)
|
|
65
|
+
if end < len(content):
|
|
66
|
+
space_idx = content.rfind(" ", idx + len(query), end)
|
|
67
|
+
if space_idx != -1 and space_idx > idx + len(query):
|
|
68
|
+
end = space_idx
|
|
69
|
+
|
|
70
|
+
prefix = "..." if start > 0 else ""
|
|
71
|
+
suffix = "..." if end < len(content) else ""
|
|
72
|
+
return prefix + content[start:end].replace("\n", " ").replace("\r", " ").strip() + suffix
|
|
73
|
+
else:
|
|
74
|
+
snippet = content[:length].replace("\n", " ").replace("\r", " ").strip()
|
|
75
|
+
if len(content) > length:
|
|
76
|
+
snippet += "..."
|
|
77
|
+
return snippet
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def print_menu():
|
|
81
|
+
"""Print the interactive REPL menu."""
|
|
82
|
+
print("==================================================")
|
|
83
|
+
print("🧠 MEMORY-OS CLI")
|
|
84
|
+
print("==================================================")
|
|
85
|
+
print("Commands:")
|
|
86
|
+
print(" sync - Incremental sync from all sources")
|
|
87
|
+
print(" sync --rebuild - Full reset and sync from all sources")
|
|
88
|
+
print(" stats - Show database record counts")
|
|
89
|
+
print(" search <query> - Hybrid search across knowledge base")
|
|
90
|
+
print(" semantic-search <query> - Semantic search in vector index")
|
|
91
|
+
print(" ask <question> - Query RAG pipeline for grounded answer")
|
|
92
|
+
print(" projects - List all repositories")
|
|
93
|
+
print(" repo-info <repo> - Show details for repository")
|
|
94
|
+
print(" graph <repo> - Show graph relationships for repository")
|
|
95
|
+
print(" reset - Reset all local storage")
|
|
96
|
+
print(" exit - Exit Memory-OS")
|
|
97
|
+
print("==================================================")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_interactive():
|
|
101
|
+
"""Run the interactive REPL loop (preserves all existing behavior)."""
|
|
102
|
+
from storage.db import (
|
|
103
|
+
init_db,
|
|
104
|
+
get_repo_count,
|
|
105
|
+
get_email_count,
|
|
106
|
+
get_repository_details,
|
|
107
|
+
get_repository_document_count,
|
|
108
|
+
get_all_repositories,
|
|
109
|
+
clear_all,
|
|
110
|
+
)
|
|
111
|
+
from core.vector_store import (
|
|
112
|
+
hybrid_search,
|
|
113
|
+
run_reindexing,
|
|
114
|
+
run_semantic_search,
|
|
115
|
+
close_qdrant_client,
|
|
116
|
+
)
|
|
117
|
+
from core.embedder import Embedder
|
|
118
|
+
from core.llm import run_hybrid_rag
|
|
119
|
+
from storage.graph import GraphStore
|
|
120
|
+
from connectors.github import sync_github
|
|
121
|
+
from connectors.gmail import sync_gmail
|
|
122
|
+
from connectors.notion import sync_notion
|
|
123
|
+
|
|
124
|
+
import sqlite3
|
|
125
|
+
try:
|
|
126
|
+
init_db()
|
|
127
|
+
except sqlite3.OperationalError as e:
|
|
128
|
+
import logging
|
|
129
|
+
logging.getLogger("repl").exception("Database operational error in REPL startup")
|
|
130
|
+
print("----------------------------------")
|
|
131
|
+
print("Workspace not initialized.")
|
|
132
|
+
print("")
|
|
133
|
+
print("Run:")
|
|
134
|
+
print("memory-os init")
|
|
135
|
+
print("----------------------------------")
|
|
136
|
+
return
|
|
137
|
+
except Exception as e:
|
|
138
|
+
import logging
|
|
139
|
+
logging.getLogger("repl").exception("Unexpected error in REPL startup database init")
|
|
140
|
+
print(f"❌ Failed to initialize database: {e}")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
# Load SentenceTransformer model exactly once at startup
|
|
144
|
+
try:
|
|
145
|
+
print("Initializing embedding model...")
|
|
146
|
+
embedder = Embedder()
|
|
147
|
+
_ = embedder.model
|
|
148
|
+
print("System ready.")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
import logging
|
|
151
|
+
logging.getLogger("repl").exception("Failed to initialize embedding model in REPL")
|
|
152
|
+
print(f"⚠️ Warning: Failed to initialize embedding model: {e}")
|
|
153
|
+
print("Model will download automatically on first query.")
|
|
154
|
+
|
|
155
|
+
print_menu()
|
|
156
|
+
|
|
157
|
+
while True:
|
|
158
|
+
try:
|
|
159
|
+
user_input = input("\nYou: ").strip()
|
|
160
|
+
if not user_input:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
parts = user_input.split(maxsplit=1)
|
|
164
|
+
cmd = parts[0].lower()
|
|
165
|
+
arg = parts[1].strip() if len(parts) > 1 else ""
|
|
166
|
+
|
|
167
|
+
# Natural Language Routing
|
|
168
|
+
VALID_COMMANDS = {
|
|
169
|
+
"exit", "sync", "stats", "search", "semantic-search", "ask",
|
|
170
|
+
"projects", "repo-info", "graph", "reset"
|
|
171
|
+
}
|
|
172
|
+
if cmd not in VALID_COMMANDS:
|
|
173
|
+
is_natural_language = (
|
|
174
|
+
user_input.rstrip().endswith("?") or
|
|
175
|
+
cmd in {"what", "how", "which", "who", "where", "why", "when",
|
|
176
|
+
"is", "are", "can", "do", "does", "tell", "show",
|
|
177
|
+
"explain", "describe", "find", "list", "get", "help", "please"}
|
|
178
|
+
)
|
|
179
|
+
if is_natural_language:
|
|
180
|
+
arg = user_input
|
|
181
|
+
cmd = "ask"
|
|
182
|
+
|
|
183
|
+
if cmd == "exit":
|
|
184
|
+
print("Closing resources...")
|
|
185
|
+
close_qdrant_client()
|
|
186
|
+
GraphStore().close()
|
|
187
|
+
print("Goodbye!")
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
elif cmd == "sync":
|
|
191
|
+
import time
|
|
192
|
+
start_time = time.perf_counter()
|
|
193
|
+
if arg == "--rebuild":
|
|
194
|
+
print("Performing full rebuild reset...")
|
|
195
|
+
clear_all()
|
|
196
|
+
close_qdrant_client()
|
|
197
|
+
run_reindexing()
|
|
198
|
+
GraphStore().clear_graph()
|
|
199
|
+
|
|
200
|
+
sync_github()
|
|
201
|
+
sync_gmail()
|
|
202
|
+
sync_notion()
|
|
203
|
+
|
|
204
|
+
run_reindexing()
|
|
205
|
+
GraphStore().extract_and_sync_graph()
|
|
206
|
+
duration = time.perf_counter() - start_time
|
|
207
|
+
print(f"\nRebuild complete. Total Duration: {duration:.2f}s")
|
|
208
|
+
logging.getLogger("main").info(f"Full rebuild completed in {duration:.2f}s")
|
|
209
|
+
else:
|
|
210
|
+
sync_github()
|
|
211
|
+
sync_gmail()
|
|
212
|
+
sync_notion()
|
|
213
|
+
run_reindexing()
|
|
214
|
+
GraphStore().extract_and_sync_graph()
|
|
215
|
+
duration = time.perf_counter() - start_time
|
|
216
|
+
print(f"\nSync complete. Total Duration: {duration:.2f}s")
|
|
217
|
+
logging.getLogger("main").info(f"Sync completed in {duration:.2f}s")
|
|
218
|
+
|
|
219
|
+
elif cmd == "stats":
|
|
220
|
+
repos = get_repo_count()
|
|
221
|
+
emails = get_email_count()
|
|
222
|
+
docs = get_repository_document_count()
|
|
223
|
+
print("========================================")
|
|
224
|
+
print("MEMORY-OS STATS")
|
|
225
|
+
print("========================================")
|
|
226
|
+
print(f"Repositories: {repos}")
|
|
227
|
+
print(f"Documents: {docs}")
|
|
228
|
+
print(f"Emails: {emails}")
|
|
229
|
+
print("========================================")
|
|
230
|
+
|
|
231
|
+
elif cmd == "search":
|
|
232
|
+
if not arg:
|
|
233
|
+
print("Usage: search <query>")
|
|
234
|
+
else:
|
|
235
|
+
results = hybrid_search(arg)
|
|
236
|
+
print("========================================")
|
|
237
|
+
print("HYBRID SEARCH RESULTS")
|
|
238
|
+
print("========================================")
|
|
239
|
+
if not results:
|
|
240
|
+
print("No matching results found.")
|
|
241
|
+
else:
|
|
242
|
+
for idx, item in enumerate(results, start=1):
|
|
243
|
+
t = item["type"]
|
|
244
|
+
if t == "repository":
|
|
245
|
+
print(f"{idx}. [Repository] {item['repo_name']} (Score: {item['score']})")
|
|
246
|
+
print(f" Language: {item['language']}")
|
|
247
|
+
desc = item['description'] or "No description."
|
|
248
|
+
desc_cleaned = desc.replace("\n", " ").replace("\r", " ").strip()
|
|
249
|
+
print(f" Description: {desc_cleaned}")
|
|
250
|
+
elif t == "document":
|
|
251
|
+
print(f"{idx}. [Document] {item['repo_name']} - {item['file_name']} (Score: {item['score']})")
|
|
252
|
+
preview = get_preview_snippet(item.get("content") or "", arg)
|
|
253
|
+
print(f" Preview: {preview}")
|
|
254
|
+
elif t == "email":
|
|
255
|
+
print(f"{idx}. [Email] {item['subject']} (Score: {item['score']})")
|
|
256
|
+
print(f" Sender: {item['sender']}")
|
|
257
|
+
preview = get_preview_snippet(item.get("snippet") or "", arg)
|
|
258
|
+
print(f" Preview: {preview}")
|
|
259
|
+
print("-" * 40)
|
|
260
|
+
print("========================================")
|
|
261
|
+
|
|
262
|
+
elif cmd == "semantic-search":
|
|
263
|
+
if not arg:
|
|
264
|
+
print("Usage: semantic-search <query>")
|
|
265
|
+
else:
|
|
266
|
+
results = run_semantic_search(arg, limit=5, source_filter=None)
|
|
267
|
+
print("========================================")
|
|
268
|
+
print("SEMANTIC SEARCH RESULTS")
|
|
269
|
+
print("========================================")
|
|
270
|
+
print(f"Query: {arg}")
|
|
271
|
+
if not results:
|
|
272
|
+
print("No results found.")
|
|
273
|
+
else:
|
|
274
|
+
for idx, res in enumerate(results, start=1):
|
|
275
|
+
print(f"{idx}. [Source: {res['source_type']}] (Score: {res['score']:.4f})")
|
|
276
|
+
print(f" Repository: {res['repository_name']}")
|
|
277
|
+
print(f" Document: {res['document_name']}")
|
|
278
|
+
print(f" Chunk Text: {res['chunk_text']}")
|
|
279
|
+
print("-" * 40)
|
|
280
|
+
print("========================================")
|
|
281
|
+
|
|
282
|
+
elif cmd == "ask":
|
|
283
|
+
if not arg:
|
|
284
|
+
print("Usage: ask <question>")
|
|
285
|
+
else:
|
|
286
|
+
rag_res = run_hybrid_rag(arg)
|
|
287
|
+
print("========================================")
|
|
288
|
+
print("ANSWER")
|
|
289
|
+
print("========================================")
|
|
290
|
+
print(rag_res["answer"])
|
|
291
|
+
print("\n========================================")
|
|
292
|
+
print("SOURCES")
|
|
293
|
+
print("========================================")
|
|
294
|
+
if rag_res["sources"]:
|
|
295
|
+
for s in rag_res["sources"]:
|
|
296
|
+
print(f"- {s}")
|
|
297
|
+
else:
|
|
298
|
+
print("None")
|
|
299
|
+
print("\n========================================")
|
|
300
|
+
print("REPOSITORIES USED")
|
|
301
|
+
print("========================================")
|
|
302
|
+
if rag_res["repositories"]:
|
|
303
|
+
for r in rag_res["repositories"]:
|
|
304
|
+
print(f"- {r}")
|
|
305
|
+
else:
|
|
306
|
+
print("None")
|
|
307
|
+
print("\n========================================")
|
|
308
|
+
print(f"Confidence: {rag_res['confidence']:.2f}")
|
|
309
|
+
print("========================================")
|
|
310
|
+
|
|
311
|
+
elif cmd == "repo-info":
|
|
312
|
+
if not arg:
|
|
313
|
+
print("Usage: repo-info <repository_name>")
|
|
314
|
+
else:
|
|
315
|
+
details = get_repository_details(arg)
|
|
316
|
+
if not details:
|
|
317
|
+
print(f"Repository '{arg}' not found.")
|
|
318
|
+
else:
|
|
319
|
+
print(f"Repository: {details['repo_name']}")
|
|
320
|
+
print(f"Description: {details['description']}")
|
|
321
|
+
print(f"Language: {details['language']}")
|
|
322
|
+
print(f"Stars: {details['stars']}")
|
|
323
|
+
print(f"Forks: {details['forks']}")
|
|
324
|
+
print(f"Last Updated: {details['updated_at'][:10] if details['updated_at'] else 'N/A'}")
|
|
325
|
+
print("Files synced:")
|
|
326
|
+
for f in sorted(details['files']):
|
|
327
|
+
print(f"- {f}")
|
|
328
|
+
if details['readme']:
|
|
329
|
+
print("\nREADME Preview:")
|
|
330
|
+
print(details['readme'][:400] + "...")
|
|
331
|
+
|
|
332
|
+
elif cmd == "projects":
|
|
333
|
+
repos = get_all_repositories()
|
|
334
|
+
print(f"Projects Found ({len(repos)}):")
|
|
335
|
+
for idx, r in enumerate(sorted(repos, key=lambda x: x["repo_name"].lower()), start=1):
|
|
336
|
+
print(f"{idx}. {r['repo_name']}")
|
|
337
|
+
|
|
338
|
+
elif cmd == "graph":
|
|
339
|
+
if not arg:
|
|
340
|
+
print("Usage: graph <repository_name>")
|
|
341
|
+
else:
|
|
342
|
+
g = GraphStore()
|
|
343
|
+
rels = g.get_node_relationships("Repository", arg)
|
|
344
|
+
print("========================================")
|
|
345
|
+
print(f"GRAPH RELATIONSHIPS FOR REPOSITORY: {arg}")
|
|
346
|
+
print("========================================")
|
|
347
|
+
if not rels:
|
|
348
|
+
print("No relationships found.")
|
|
349
|
+
else:
|
|
350
|
+
for r in sorted(rels):
|
|
351
|
+
print(f"- {r}")
|
|
352
|
+
print("========================================")
|
|
353
|
+
|
|
354
|
+
elif cmd == "reset":
|
|
355
|
+
confirm = input("Are you sure you want to reset all data? (y/N): ").strip().lower()
|
|
356
|
+
if confirm == "y":
|
|
357
|
+
clear_all()
|
|
358
|
+
close_qdrant_client()
|
|
359
|
+
run_reindexing()
|
|
360
|
+
GraphStore().clear_graph()
|
|
361
|
+
print("All data reset successfully.")
|
|
362
|
+
else:
|
|
363
|
+
print("Reset cancelled.")
|
|
364
|
+
|
|
365
|
+
else:
|
|
366
|
+
print(f"Unknown command: '{cmd}'. Type exit to quit or sync to pull latest data.")
|
|
367
|
+
|
|
368
|
+
except KeyboardInterrupt:
|
|
369
|
+
print("\nClosing resources...")
|
|
370
|
+
close_qdrant_client()
|
|
371
|
+
GraphStore().close()
|
|
372
|
+
print("Goodbye!")
|
|
373
|
+
break
|
|
374
|
+
except sqlite3.OperationalError as e:
|
|
375
|
+
import logging
|
|
376
|
+
logging.getLogger("repl").exception("Database operational error in REPL loop")
|
|
377
|
+
print("----------------------------------")
|
|
378
|
+
print("Workspace has not been initialized.")
|
|
379
|
+
print("")
|
|
380
|
+
print("Run:")
|
|
381
|
+
print("memory-os init")
|
|
382
|
+
print("----------------------------------")
|
|
383
|
+
except Exception as e:
|
|
384
|
+
import logging
|
|
385
|
+
logging.getLogger("repl").exception("Unexpected error during command execution in REPL")
|
|
386
|
+
print(f"Error during command execution: {e}")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def route_command(args):
|
|
390
|
+
"""Route a parsed subcommand to its handler module."""
|
|
391
|
+
command = args.command
|
|
392
|
+
|
|
393
|
+
if command is None:
|
|
394
|
+
# No subcommand given — launch interactive REPL
|
|
395
|
+
run_interactive()
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
# Import and execute the appropriate command module
|
|
399
|
+
if command == "doctor":
|
|
400
|
+
from cli.commands.doctor import execute
|
|
401
|
+
execute(args)
|
|
402
|
+
elif command == "version":
|
|
403
|
+
from cli.commands.version import execute
|
|
404
|
+
execute(args)
|
|
405
|
+
elif command == "status":
|
|
406
|
+
from cli.commands.status import execute
|
|
407
|
+
execute(args)
|
|
408
|
+
elif command == "init":
|
|
409
|
+
from cli.commands.init import execute
|
|
410
|
+
execute(args)
|
|
411
|
+
elif command == "start":
|
|
412
|
+
from cli.commands.start import execute
|
|
413
|
+
execute(args)
|
|
414
|
+
elif command == "stop":
|
|
415
|
+
from cli.commands.stop import execute
|
|
416
|
+
execute(args)
|
|
417
|
+
elif command == "plugins":
|
|
418
|
+
from cli.commands.plugins import execute
|
|
419
|
+
execute(args)
|
|
420
|
+
elif command == "monitor":
|
|
421
|
+
from cli.commands.monitor import execute
|
|
422
|
+
execute(args)
|
|
423
|
+
elif command == "export":
|
|
424
|
+
from cli.commands.export import execute
|
|
425
|
+
execute(args)
|
|
426
|
+
elif command == "import":
|
|
427
|
+
from cli.commands.import_cmd import execute
|
|
428
|
+
execute(args)
|
|
429
|
+
elif command == "config":
|
|
430
|
+
from cli.commands.config_cmd import execute
|
|
431
|
+
execute(args)
|
|
432
|
+
elif command == "workspace":
|
|
433
|
+
from cli.commands.workspace import execute
|
|
434
|
+
execute(args)
|
|
435
|
+
elif command == "benchmark":
|
|
436
|
+
from cli.commands.benchmark import execute
|
|
437
|
+
execute(args)
|
|
438
|
+
elif command == "logs":
|
|
439
|
+
from cli.commands.logs import execute
|
|
440
|
+
execute(args)
|
|
441
|
+
else:
|
|
442
|
+
# For commands not yet implemented, show a helpful message
|
|
443
|
+
print(f"Command '{command}' is not yet implemented.")
|
|
444
|
+
print("Run 'memory-os --help' for available commands.")
|
|
445
|
+
print("Run 'memory-os' (no arguments) for the interactive REPL.")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def cli_entrypoint():
|
|
450
|
+
"""Main CLI entry point. Called by `memory-os` console script and `python main.py`."""
|
|
451
|
+
# Force UTF-8 stdout on Windows
|
|
452
|
+
try:
|
|
453
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
setup_logging()
|
|
458
|
+
|
|
459
|
+
# Load configuration (TOML → .env → defaults)
|
|
460
|
+
try:
|
|
461
|
+
from infrastructure.config import load_config
|
|
462
|
+
load_config()
|
|
463
|
+
except Exception:
|
|
464
|
+
# Config loading is best-effort at this stage; .env fallback
|
|
465
|
+
# still works via load_dotenv in config.py
|
|
466
|
+
from dotenv import load_dotenv
|
|
467
|
+
load_dotenv()
|
|
468
|
+
|
|
469
|
+
parser = build_parser()
|
|
470
|
+
args = parser.parse_args()
|
|
471
|
+
|
|
472
|
+
import logging
|
|
473
|
+
import sqlite3
|
|
474
|
+
logger = logging.getLogger("cli.main")
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
route_command(args)
|
|
478
|
+
except sqlite3.OperationalError as e:
|
|
479
|
+
logger.exception("Database operational error occurred")
|
|
480
|
+
print("----------------------------------")
|
|
481
|
+
print("Workspace has not been initialized.")
|
|
482
|
+
print("")
|
|
483
|
+
print("Run:")
|
|
484
|
+
print("memory-os init")
|
|
485
|
+
print("----------------------------------")
|
|
486
|
+
sys.exit(1)
|
|
487
|
+
except Exception as e:
|
|
488
|
+
logger.exception("Unexpected error in command execution")
|
|
489
|
+
print(f"❌ An unexpected error occurred: {e}")
|
|
490
|
+
print("Please check logs/memory_os.log for full details.")
|
|
491
|
+
sys.exit(1)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
if __name__ == "__main__":
|
|
495
|
+
cli_entrypoint()
|
|
496
|
+
|
cli/parser.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI argument parser.
|
|
3
|
+
|
|
4
|
+
Builds the argparse parser with subcommands for all Memory-OS commands.
|
|
5
|
+
Each subcommand routes to its corresponding module in cli/commands/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
"""Build and return the Memory-OS CLI argument parser."""
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="memory-os",
|
|
15
|
+
description="Memory-OS — CLI-based Personal Knowledge Operating System",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
19
|
+
|
|
20
|
+
# ── init ──────────────────────────────────────────────────
|
|
21
|
+
subparsers.add_parser(
|
|
22
|
+
"init",
|
|
23
|
+
help="Initialize Memory-OS (workspace, config, Docker services)",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# ── sync ──────────────────────────────────────────────────
|
|
27
|
+
sync_parser = subparsers.add_parser(
|
|
28
|
+
"sync",
|
|
29
|
+
help="Sync data from connected sources",
|
|
30
|
+
)
|
|
31
|
+
sync_parser.add_argument(
|
|
32
|
+
"--rebuild", action="store_true",
|
|
33
|
+
help="Full reset and rebuild from all sources",
|
|
34
|
+
)
|
|
35
|
+
sync_parser.add_argument(
|
|
36
|
+
"--source", type=str, choices=["github", "gmail", "notion"],
|
|
37
|
+
help="Sync only a specific source",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ── ask ────────────────────────────────────────────────────
|
|
41
|
+
ask_parser = subparsers.add_parser(
|
|
42
|
+
"ask",
|
|
43
|
+
help="Query the RAG pipeline with a question",
|
|
44
|
+
)
|
|
45
|
+
ask_parser.add_argument(
|
|
46
|
+
"question", nargs="+", type=str,
|
|
47
|
+
help="The question to ask",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# ── doctor ────────────────────────────────────────────────
|
|
51
|
+
subparsers.add_parser(
|
|
52
|
+
"doctor",
|
|
53
|
+
help="Diagnose system health and connectivity",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# ── status ────────────────────────────────────────────────
|
|
57
|
+
subparsers.add_parser(
|
|
58
|
+
"status",
|
|
59
|
+
help="Show workspace and service status",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# ── backup ────────────────────────────────────────────────
|
|
63
|
+
backup_parser = subparsers.add_parser(
|
|
64
|
+
"backup",
|
|
65
|
+
help="Create a timestamped backup of workspace data",
|
|
66
|
+
)
|
|
67
|
+
backup_parser.add_argument(
|
|
68
|
+
"--include-logs", action="store_true",
|
|
69
|
+
help="Include log files in the backup",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# ── export ────────────────────────────────────────────────
|
|
73
|
+
export_parser = subparsers.add_parser(
|
|
74
|
+
"export",
|
|
75
|
+
help="Export active workspace profile and configuration to a compressed file",
|
|
76
|
+
)
|
|
77
|
+
export_parser.add_argument(
|
|
78
|
+
"file", type=str,
|
|
79
|
+
help="Target archive file path (e.g. backup.zip)",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# ── import ────────────────────────────────────────────────
|
|
83
|
+
import_parser = subparsers.add_parser(
|
|
84
|
+
"import",
|
|
85
|
+
help="Import and restore a previously exported workspace archive",
|
|
86
|
+
)
|
|
87
|
+
import_parser.add_argument(
|
|
88
|
+
"file", type=str,
|
|
89
|
+
help="Workspace archive file path to import",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── graph ─────────────────────────────────────────────────
|
|
94
|
+
graph_parser = subparsers.add_parser(
|
|
95
|
+
"graph",
|
|
96
|
+
help="Show knowledge graph relationships for a repository",
|
|
97
|
+
)
|
|
98
|
+
graph_parser.add_argument(
|
|
99
|
+
"repo", type=str,
|
|
100
|
+
help="Repository name to inspect",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# ── update ────────────────────────────────────────────────
|
|
104
|
+
subparsers.add_parser(
|
|
105
|
+
"update",
|
|
106
|
+
help="Check and update Docker service images",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ── start ─────────────────────────────────────────────────
|
|
110
|
+
subparsers.add_parser(
|
|
111
|
+
"start",
|
|
112
|
+
help="Start Docker services (Neo4j, Qdrant)",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# ── stop ──────────────────────────────────────────────────
|
|
116
|
+
subparsers.add_parser(
|
|
117
|
+
"stop",
|
|
118
|
+
help="Stop Docker services (preserves data)",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ── version ───────────────────────────────────────────────
|
|
122
|
+
subparsers.add_parser(
|
|
123
|
+
"version",
|
|
124
|
+
help="Show Memory-OS version information",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ── logs ──────────────────────────────────────────────────
|
|
128
|
+
logs_parser = subparsers.add_parser(
|
|
129
|
+
"logs",
|
|
130
|
+
help="View Memory-OS log output",
|
|
131
|
+
)
|
|
132
|
+
logs_parser.add_argument(
|
|
133
|
+
"--tail", type=int, default=50,
|
|
134
|
+
help="Number of log lines to show (default: 50)",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# ── migrate ───────────────────────────────────────────────
|
|
138
|
+
subparsers.add_parser(
|
|
139
|
+
"migrate",
|
|
140
|
+
help="Migrate existing data into the workspace",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# ── workspace ─────────────────────────────────────────────
|
|
144
|
+
workspace_parser = subparsers.add_parser(
|
|
145
|
+
"workspace",
|
|
146
|
+
help="Manage workspace profiles",
|
|
147
|
+
)
|
|
148
|
+
workspace_sub = workspace_parser.add_subparsers(dest="workspace_action")
|
|
149
|
+
ws_create = workspace_sub.add_parser("create", help="Create a new workspace profile")
|
|
150
|
+
ws_create.add_argument("name", type=str, help="Profile name")
|
|
151
|
+
ws_switch = workspace_sub.add_parser("switch", help="Switch active workspace profile")
|
|
152
|
+
ws_switch.add_argument("name", type=str, help="Profile name to switch to")
|
|
153
|
+
workspace_sub.add_parser("list", help="List all workspace profiles")
|
|
154
|
+
ws_delete = workspace_sub.add_parser("delete", help="Delete a workspace profile")
|
|
155
|
+
ws_delete.add_argument("name", type=str, help="Profile name to delete")
|
|
156
|
+
|
|
157
|
+
# ── config ────────────────────────────────────────────────
|
|
158
|
+
config_parser = subparsers.add_parser(
|
|
159
|
+
"config",
|
|
160
|
+
help="Inspect or modify configuration",
|
|
161
|
+
)
|
|
162
|
+
config_sub = config_parser.add_subparsers(dest="config_action")
|
|
163
|
+
config_sub.add_parser("show", help="Show full configuration")
|
|
164
|
+
config_get = config_sub.add_parser("get", help="Get a config value")
|
|
165
|
+
config_get.add_argument("key", type=str, help="Config key (e.g. groq.model)")
|
|
166
|
+
config_set = config_sub.add_parser("set", help="Set a config value")
|
|
167
|
+
config_set.add_argument("key", type=str, help="Config key (e.g. groq.model)")
|
|
168
|
+
config_set.add_argument("value", type=str, help="New value")
|
|
169
|
+
|
|
170
|
+
# ── plugins ───────────────────────────────────────────────
|
|
171
|
+
subparsers.add_parser(
|
|
172
|
+
"plugins",
|
|
173
|
+
help="List installed and available connector plugins",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# ── monitor ───────────────────────────────────────────────
|
|
177
|
+
subparsers.add_parser(
|
|
178
|
+
"monitor",
|
|
179
|
+
help="Show system observability and latency performance metrics",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# ── benchmark ─────────────────────────────────────────────
|
|
183
|
+
subparsers.add_parser(
|
|
184
|
+
"benchmark",
|
|
185
|
+
help="Perform speed run latency benchmarks on query pipelines",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return parser
|