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
storage/db.py ADDED
@@ -0,0 +1,528 @@
1
+ import sqlite3
2
+ import os
3
+ import logging
4
+ DB_PATH = "memory.db"
5
+ SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "schema.sql")
6
+
7
+ _initialized_dbs = set()
8
+
9
+ def get_connection():
10
+ global _initialized_dbs
11
+ db_path = os.getenv("MEMORY_OS_DB_PATH", DB_PATH)
12
+ abs_path = os.path.abspath(db_path)
13
+
14
+ # Ensure directory exists
15
+ parent = os.path.dirname(abs_path)
16
+ if parent:
17
+ os.makedirs(parent, exist_ok=True)
18
+
19
+ conn = sqlite3.connect(abs_path)
20
+ conn.row_factory = sqlite3.Row
21
+
22
+ if abs_path not in _initialized_dbs:
23
+ # Check if the schema is initialized (e.g. check if document_chunks table exists)
24
+ cursor = conn.cursor()
25
+ try:
26
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='document_chunks'")
27
+ table_exists = cursor.fetchone()
28
+ except sqlite3.OperationalError:
29
+ table_exists = False
30
+
31
+ if not table_exists:
32
+ _initialized_dbs.add(abs_path)
33
+ conn.close()
34
+ init_db()
35
+ conn = sqlite3.connect(abs_path)
36
+ conn.row_factory = sqlite3.Row
37
+ else:
38
+ _initialized_dbs.add(abs_path)
39
+
40
+ return conn
41
+
42
+
43
+
44
+ def init_db():
45
+ conn = get_connection()
46
+ cursor = conn.cursor()
47
+ # Check if repositories table exists and needs recreation (e.g. legacy schema)
48
+ cursor.execute("PRAGMA table_info(repositories)")
49
+ columns = [row[1] for row in cursor.fetchall()]
50
+ if columns and "visibility" not in columns:
51
+ cursor.execute("DROP TABLE repositories")
52
+ cursor.execute("DROP TABLE IF EXISTS repository_documents")
53
+
54
+ # Check if emails table exists and needs recreation (e.g. legacy schema without message_id)
55
+ cursor.execute("PRAGMA table_info(emails)")
56
+ email_columns = [row[1] for row in cursor.fetchall()]
57
+ if email_columns and "message_id" not in email_columns:
58
+ cursor.execute("DROP TABLE emails")
59
+
60
+ if os.path.exists(SCHEMA_PATH):
61
+ with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
62
+ schema_sql = f.read()
63
+ conn.executescript(schema_sql)
64
+ conn.commit()
65
+ conn.close()
66
+
67
+ def insert_repository(repo):
68
+ conn = get_connection()
69
+ cursor = conn.cursor()
70
+ cursor.execute("SELECT id FROM repositories WHERE repo_name = ?", (repo.repo_name,))
71
+ row = cursor.fetchone()
72
+ if row:
73
+ cursor.execute(
74
+ """
75
+ UPDATE repositories
76
+ SET description = ?, language = ?, visibility = ?, stars = ?, forks = ?, open_issues = ?, default_branch = ?, updated_at = ?, url = ?
77
+ WHERE id = ?
78
+ """,
79
+ (repo.description, repo.language, repo.visibility, repo.stars, repo.forks, repo.open_issues, repo.default_branch, repo.updated_at, repo.url, row[0])
80
+ )
81
+ else:
82
+ cursor.execute(
83
+ """
84
+ INSERT INTO repositories (repo_name, description, language, visibility, stars, forks, open_issues, default_branch, updated_at, url)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
86
+ """,
87
+ (repo.repo_name, repo.description, repo.language, repo.visibility, repo.stars, repo.forks, repo.open_issues, repo.default_branch, repo.updated_at, repo.url)
88
+ )
89
+ conn.commit()
90
+ conn.close()
91
+
92
+ def insert_repository_document(doc):
93
+ conn = get_connection()
94
+ cursor = conn.cursor()
95
+ cursor.execute(
96
+ "SELECT id FROM repository_documents WHERE repo_name = ? AND file_name = ?",
97
+ (doc.repo_name, doc.file_name)
98
+ )
99
+ row = cursor.fetchone()
100
+ if row:
101
+ cursor.execute(
102
+ "UPDATE repository_documents SET content = ?, source = ?, synced_at = ? WHERE id = ?",
103
+ (doc.content, doc.source, doc.synced_at, row[0])
104
+ )
105
+ else:
106
+ cursor.execute(
107
+ "INSERT INTO repository_documents (repo_name, file_name, content, source, synced_at) VALUES (?, ?, ?, ?, ?)",
108
+ (doc.repo_name, doc.file_name, doc.content, doc.source, doc.synced_at)
109
+ )
110
+ conn.commit()
111
+ conn.close()
112
+
113
+ def insert_email(email):
114
+ conn = get_connection()
115
+ cursor = conn.cursor()
116
+ if email.message_id:
117
+ cursor.execute("SELECT id FROM emails WHERE message_id = ?", (email.message_id,))
118
+ else:
119
+ cursor.execute(
120
+ "SELECT id FROM emails WHERE subject = ? AND sender = ? AND received_at = ?",
121
+ (email.subject, email.sender, email.received_at)
122
+ )
123
+ row = cursor.fetchone()
124
+ if row:
125
+ cursor.execute(
126
+ "UPDATE emails SET snippet = ?, subject = ?, sender = ?, received_at = ? WHERE id = ?",
127
+ (email.snippet, email.subject, email.sender, email.received_at, row[0])
128
+ )
129
+ else:
130
+ cursor.execute(
131
+ "INSERT INTO emails (message_id, subject, sender, snippet, received_at) VALUES (?, ?, ?, ?, ?)",
132
+ (email.message_id, email.subject, email.sender, email.snippet, email.received_at)
133
+ )
134
+ conn.commit()
135
+ conn.close()
136
+
137
+ def get_repo_count() -> int:
138
+ conn = get_connection()
139
+ cursor = conn.cursor()
140
+ cursor.execute("SELECT COUNT(*) FROM repositories")
141
+ count = cursor.fetchone()[0]
142
+ conn.close()
143
+ return count
144
+
145
+ def get_repository_document_count() -> int:
146
+ conn = get_connection()
147
+ cursor = conn.cursor()
148
+ cursor.execute("SELECT COUNT(*) FROM repository_documents")
149
+ count = cursor.fetchone()[0]
150
+ conn.close()
151
+ return count
152
+
153
+ def get_email_count() -> int:
154
+ conn = get_connection()
155
+ cursor = conn.cursor()
156
+ cursor.execute("SELECT COUNT(*) FROM emails")
157
+ count = cursor.fetchone()[0]
158
+ conn.close()
159
+ return count
160
+
161
+ def get_repository_details(repo_name: str):
162
+ conn = get_connection()
163
+ cursor = conn.cursor()
164
+ cursor.execute(
165
+ "SELECT repo_name, description, language, visibility, stars, forks, open_issues, default_branch, updated_at, url FROM repositories WHERE LOWER(repo_name) = LOWER(?)",
166
+ (repo_name,)
167
+ )
168
+ repo_row = cursor.fetchone()
169
+ if not repo_row:
170
+ conn.close()
171
+ return None
172
+
173
+ matched_name = repo_row[0]
174
+
175
+ # Get files stored for this repository
176
+ cursor.execute(
177
+ "SELECT file_name FROM repository_documents WHERE LOWER(repo_name) = LOWER(?)",
178
+ (repo_name,)
179
+ )
180
+ files = [row[0] for row in cursor.fetchall()]
181
+
182
+ # Get README content
183
+ cursor.execute(
184
+ "SELECT content FROM repository_documents WHERE LOWER(repo_name) = LOWER(?) AND LOWER(file_name) = 'readme.md'",
185
+ (repo_name,)
186
+ )
187
+ readme_row = cursor.fetchone()
188
+ readme_content = readme_row[0] if readme_row else None
189
+
190
+ conn.close()
191
+ return {
192
+ "repo_name": matched_name,
193
+ "description": repo_row[1],
194
+ "language": repo_row[2],
195
+ "visibility": repo_row[3],
196
+ "stars": repo_row[4],
197
+ "forks": repo_row[5],
198
+ "open_issues": repo_row[6],
199
+ "default_branch": repo_row[7],
200
+ "updated_at": repo_row[8],
201
+ "url": repo_row[9],
202
+ "files": files,
203
+ "readme": readme_content
204
+ }
205
+
206
+ def clear_all():
207
+ conn = get_connection()
208
+ cursor = conn.cursor()
209
+ cursor.execute("DELETE FROM repositories")
210
+ cursor.execute("DELETE FROM repository_documents")
211
+ cursor.execute("DELETE FROM emails")
212
+ cursor.execute("DELETE FROM document_chunks")
213
+ cursor.execute("DELETE FROM graph_relationships")
214
+ cursor.execute("DELETE FROM graph_nodes")
215
+ conn.commit()
216
+ conn.close()
217
+
218
+ def search_local_knowledge_ranked(query: str, repo_filter: str = None) -> list:
219
+ """Ranked search across repositories, documents, and emails.
220
+
221
+ Applies configurable boost for repository scores and weight for email scores.
222
+ Adds ``source_type`` to each result and logs detailed diagnostics.
223
+ """
224
+ conn = get_connection()
225
+ cursor = conn.cursor()
226
+
227
+ if repo_filter:
228
+ if isinstance(repo_filter, list):
229
+ placeholders = ",".join(["?"] * len(repo_filter))
230
+ cursor.execute(
231
+ f"SELECT repo_name, language, description FROM repositories WHERE LOWER(repo_name) IN ({placeholders})",
232
+ [r.lower() for r in repo_filter]
233
+ )
234
+ repos = cursor.fetchall()
235
+
236
+ cursor.execute(
237
+ f"SELECT repo_name, file_name, content FROM repository_documents WHERE LOWER(repo_name) IN ({placeholders})",
238
+ [r.lower() for r in repo_filter]
239
+ )
240
+ docs = cursor.fetchall()
241
+
242
+ emails = []
243
+ else:
244
+ cursor.execute("SELECT repo_name, language, description FROM repositories WHERE LOWER(repo_name) = LOWER(?)", (repo_filter,))
245
+ repos = cursor.fetchall()
246
+
247
+ cursor.execute("SELECT repo_name, file_name, content FROM repository_documents WHERE LOWER(repo_name) = LOWER(?)", (repo_filter,))
248
+ docs = cursor.fetchall()
249
+
250
+ emails = []
251
+ else:
252
+ # Fetch all data for scoring
253
+ cursor.execute("SELECT repo_name, language, description FROM repositories")
254
+ repos = cursor.fetchall()
255
+
256
+ cursor.execute("SELECT repo_name, file_name, content FROM repository_documents")
257
+ docs = cursor.fetchall()
258
+
259
+ cursor.execute("SELECT subject, sender, snippet FROM emails")
260
+ emails = cursor.fetchall()
261
+ conn.close()
262
+
263
+ # Configurable boosts (default = 1.0 i.e., no change)
264
+ repo_boost = float(os.getenv('REPO_SCORE_BOOST', '1.0'))
265
+ email_weight = float(os.getenv('EMAIL_SCORE_WEIGHT', '1.0'))
266
+
267
+ ranked_results = []
268
+ query_lower = query.lower()
269
+ logger = logging.getLogger(__name__)
270
+
271
+ # --- Score repositories ---
272
+ for repo_name, language, description in repos:
273
+ score = 0
274
+ if repo_name and query_lower in repo_name.lower():
275
+ score += 10
276
+ if description and query_lower in description.lower():
277
+ score += 8
278
+ if language and query_lower in language.lower():
279
+ score += 5
280
+ if score > 0:
281
+ score = int(score * repo_boost)
282
+ result = {
283
+ "type": "repository",
284
+ "score": score,
285
+ "repo_name": repo_name,
286
+ "language": language,
287
+ "description": description,
288
+ "source_type": "repository",
289
+ }
290
+ ranked_results.append(result)
291
+ logger.debug(f"Retrieval result - {result}")
292
+
293
+ # --- Score documents ---
294
+ for repo_name, file_name, content in docs:
295
+ score = 0
296
+ content_lower = content.lower() if content else ""
297
+ file_name_lower = file_name.lower() if file_name else ""
298
+ if query_lower in file_name_lower or query_lower in content_lower:
299
+ if file_name_lower == "readme.md":
300
+ score += 6
301
+ elif file_name_lower == "package.json":
302
+ score += 4
303
+ else:
304
+ score += 3
305
+ if score > 0:
306
+ result = {
307
+ "type": "document",
308
+ "score": score,
309
+ "repo_name": repo_name,
310
+ "file_name": file_name,
311
+ "content": content,
312
+ "source_type": "document",
313
+ }
314
+ ranked_results.append(result)
315
+ logger.debug(f"Retrieval result - {result}")
316
+
317
+ # --- Score emails ---
318
+ for subject, sender, snippet in emails:
319
+ score = 0
320
+ subj_l = subject.lower() if subject else ""
321
+ send_l = sender.lower() if sender else ""
322
+ snip_l = snippet.lower() if snippet else ""
323
+ if query_lower in subj_l or query_lower in send_l or query_lower in snip_l:
324
+ score += 2
325
+ if score > 0:
326
+ score = int(score * email_weight)
327
+ result = {
328
+ "type": "email",
329
+ "score": score,
330
+ "subject": subject,
331
+ "sender": sender,
332
+ "snippet": snippet,
333
+ "source_type": "email",
334
+ }
335
+ ranked_results.append(result)
336
+ logger.debug(f"Retrieval result - {result}")
337
+
338
+ # Sort descending by score, then stable alphabetical tie‑breaker
339
+ ranked_results.sort(key=lambda x: (-x["score"], x.get("repo_name") or x.get("subject") or ""))
340
+ return ranked_results
341
+
342
+ def get_repository_files(repo_name: str) -> list:
343
+ conn = get_connection()
344
+ cursor = conn.cursor()
345
+ cursor.execute("SELECT file_name FROM repository_documents WHERE LOWER(repo_name) = LOWER(?)", (repo_name,))
346
+ files = [row[0] for row in cursor.fetchall()]
347
+ conn.close()
348
+ return files
349
+
350
+ def get_repository_readme(repo_name: str) -> str:
351
+ conn = get_connection()
352
+ cursor = conn.cursor()
353
+ cursor.execute("SELECT content FROM repository_documents WHERE LOWER(repo_name) = LOWER(?) AND LOWER(file_name) = 'readme.md'", (repo_name,))
354
+ row = cursor.fetchone()
355
+ readme = row[0] if row else None
356
+ conn.close()
357
+ return readme
358
+
359
+ def get_all_repositories() -> list:
360
+ conn = get_connection()
361
+ cursor = conn.cursor()
362
+ cursor.execute("SELECT repo_name, language, description, stars, forks, updated_at FROM repositories")
363
+ repos = [
364
+ {
365
+ "repo_name": row[0],
366
+ "language": row[1],
367
+ "description": row[2],
368
+ "stars": row[3],
369
+ "forks": row[4],
370
+ "updated_at": row[5]
371
+ }
372
+ for row in cursor.fetchall()
373
+ ]
374
+ conn.close()
375
+ return repos
376
+
377
+ def get_all_documents() -> list:
378
+ conn = get_connection()
379
+ cursor = conn.cursor()
380
+ cursor.execute("SELECT repo_name, file_name, content FROM repository_documents")
381
+ docs = [
382
+ {"repo_name": row[0], "file_name": row[1], "content": row[2]}
383
+ for row in cursor.fetchall()
384
+ ]
385
+ conn.close()
386
+ return docs
387
+
388
+ def insert_document_chunk(repository_name: str, document_name: str, source_type: str, chunk_text: str, chunk_index: int, created_at: str):
389
+ conn = get_connection()
390
+ cursor = conn.cursor()
391
+ cursor.execute(
392
+ """
393
+ INSERT INTO document_chunks (repository_name, document_name, source_type, chunk_text, chunk_index, created_at)
394
+ VALUES (?, ?, ?, ?, ?, ?)
395
+ """,
396
+ (repository_name, document_name, source_type, chunk_text, chunk_index, created_at)
397
+ )
398
+ conn.commit()
399
+ conn.close()
400
+
401
+ def clear_document_chunks():
402
+ conn = get_connection()
403
+ cursor = conn.cursor()
404
+ cursor.execute("DELETE FROM document_chunks")
405
+ cursor.execute("DELETE FROM sqlite_sequence WHERE name = 'document_chunks'")
406
+ conn.commit()
407
+ conn.close()
408
+
409
+ def get_document_chunk_count() -> int:
410
+ conn = get_connection()
411
+ cursor = conn.cursor()
412
+ cursor.execute("SELECT COUNT(*) FROM document_chunks")
413
+ count = cursor.fetchone()[0]
414
+ conn.close()
415
+ return count
416
+
417
+ def get_all_document_chunks() -> list:
418
+ conn = get_connection()
419
+ cursor = conn.cursor()
420
+ cursor.execute("SELECT id, repository_name, document_name, source_type, chunk_text, chunk_index FROM document_chunks")
421
+ chunks = [
422
+ {
423
+ "id": row[0],
424
+ "repository_name": row[1],
425
+ "document_name": row[2],
426
+ "source_type": row[3],
427
+ "chunk_text": row[4],
428
+ "chunk_index": row[5]
429
+ }
430
+ for row in cursor.fetchall()
431
+ ]
432
+ conn.close()
433
+ return chunks
434
+
435
+ def get_all_emails() -> list:
436
+ conn = get_connection()
437
+ cursor = conn.cursor()
438
+ cursor.execute("SELECT message_id, subject, sender, snippet, received_at FROM emails")
439
+ emails = [
440
+ {
441
+ "message_id": row[0],
442
+ "subject": row[1],
443
+ "sender": row[2],
444
+ "snippet": row[3],
445
+ "received_at": row[4]
446
+ }
447
+ for row in cursor.fetchall()
448
+ ]
449
+ conn.close()
450
+ return emails
451
+
452
+ def insert_fallback_node(node_id: str, label: str, name: str):
453
+ conn = get_connection()
454
+ cursor = conn.cursor()
455
+ cursor.execute(
456
+ "INSERT OR REPLACE INTO graph_nodes (id, label, name) VALUES (?, ?, ?)",
457
+ (node_id, label, name)
458
+ )
459
+ conn.commit()
460
+ conn.close()
461
+
462
+ def insert_fallback_relationship(rel_id: str, source_id: str, target_id: str, type_str: str):
463
+ conn = get_connection()
464
+ cursor = conn.cursor()
465
+ cursor.execute(
466
+ "INSERT OR REPLACE INTO graph_relationships (id, source_id, target_id, type) VALUES (?, ?, ?, ?)",
467
+ (rel_id, source_id, target_id, type_str)
468
+ )
469
+ conn.commit()
470
+ conn.close()
471
+
472
+ def get_fallback_relationships(node_id: str) -> list:
473
+ conn = get_connection()
474
+ cursor = conn.cursor()
475
+ # Find relationships where node_id is either source or target
476
+ cursor.execute(
477
+ """
478
+ SELECT r.type, s.label, s.name, t.label, t.name
479
+ FROM graph_relationships r
480
+ JOIN graph_nodes s ON r.source_id = s.id
481
+ JOIN graph_nodes t ON r.target_id = t.id
482
+ WHERE r.source_id = ? OR r.target_id = ?
483
+ """,
484
+ (node_id, node_id)
485
+ )
486
+ results = [
487
+ {
488
+ "type": row[0],
489
+ "source_label": row[1],
490
+ "source_name": row[2],
491
+ "target_label": row[3],
492
+ "target_name": row[4]
493
+ }
494
+ for row in cursor.fetchall()
495
+ ]
496
+ conn.close()
497
+ return results
498
+
499
+ def clear_fallback_graph():
500
+ conn = get_connection()
501
+ cursor = conn.cursor()
502
+ cursor.execute("DELETE FROM graph_relationships")
503
+ cursor.execute("DELETE FROM graph_nodes")
504
+ conn.commit()
505
+ conn.close()
506
+
507
+ def delete_data_before_date(date_str: str) -> dict:
508
+ conn = get_connection()
509
+ cursor = conn.cursor()
510
+
511
+ # We compare string-wise since ISO dates sort correctly
512
+ cursor.execute("DELETE FROM emails WHERE received_at < ?", (date_str,))
513
+ emails_deleted = cursor.rowcount
514
+
515
+ cursor.execute("DELETE FROM repository_documents WHERE synced_at < ?", (date_str,))
516
+ docs_deleted = cursor.rowcount
517
+
518
+ cursor.execute("DELETE FROM document_chunks WHERE created_at < ?", (date_str,))
519
+ chunks_deleted = cursor.rowcount
520
+
521
+ conn.commit()
522
+ conn.close()
523
+
524
+ return {
525
+ "emails": emails_deleted,
526
+ "documents": docs_deleted,
527
+ "chunks": chunks_deleted
528
+ }