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
connectors/gmail.py ADDED
@@ -0,0 +1,116 @@
1
+ from composio import Composio
2
+ from models.memory import Email
3
+ from storage.db import insert_email, get_connection
4
+
5
+ import os
6
+ import sys
7
+
8
+ def sync_gmail():
9
+ try:
10
+ try:
11
+ sys.stdout.reconfigure(encoding='utf-8')
12
+ except Exception:
13
+ pass
14
+
15
+ c = Composio()
16
+ user_id = os.getenv("COMPOSIO_USER_ID", "user_123")
17
+ s = c.create(user_id=user_id)
18
+
19
+ # Verify connection
20
+ toolkits_info = s.toolkits()
21
+ gmail_tk = next((t for t in toolkits_info.items if t.slug == "gmail"), None)
22
+ if not gmail_tk or not (gmail_tk.connection and gmail_tk.connection.is_active):
23
+ print("Gmail connection not active.")
24
+ return
25
+
26
+ # Fetch emails
27
+ resp = s.execute(tool_slug="gmail_fetch_emails", arguments={"max_results": 50, "include_payload": True})
28
+ if not resp or resp.error or not resp.data:
29
+ print("No emails found.")
30
+ return
31
+
32
+ emails = resp.data.get("messages", [])
33
+ if not emails:
34
+ # Check response_data structure just in case
35
+ emails = resp.data.get("response_data", {}).get("messages", [])
36
+ if not emails:
37
+ print("No emails found.")
38
+ return
39
+
40
+ print(f"Found {len(emails)} emails")
41
+
42
+ conn = get_connection()
43
+ cursor = conn.cursor()
44
+
45
+ synced_count = 0
46
+ skipped_count = 0
47
+
48
+ for email in emails:
49
+ message_id = email.get("id") or email.get("messageId") or email.get("threadId") or ""
50
+
51
+ # Check if email is already in SQLite
52
+ if message_id:
53
+ cursor.execute("SELECT id FROM emails WHERE message_id = ?", (message_id,))
54
+ if cursor.fetchone():
55
+ skipped_count += 1
56
+ continue
57
+
58
+ subject = email.get("subject") or "No Subject"
59
+ sender = email.get("sender") or email.get("from") or "Unknown Sender"
60
+ snippet = email.get("messageText") or email.get("snippet") or ""
61
+ received_at = email.get("messageTimestamp") or ""
62
+ date_str = received_at[:10] if len(received_at) >= 10 else received_at
63
+
64
+ # Save to SQLite
65
+ db_email = Email(
66
+ message_id=message_id,
67
+ subject=subject,
68
+ sender=sender,
69
+ snippet=snippet,
70
+ received_at=received_at
71
+ )
72
+ insert_email(db_email)
73
+ synced_count += 1
74
+
75
+ # Print to terminal
76
+ print("--------------------------------------------------")
77
+ print(f"Subject: {subject}")
78
+ print(f"From: {sender}")
79
+ print(f"Date: {date_str}")
80
+ print("--------------------------------------------------")
81
+
82
+ conn.close()
83
+ print(f"Gmail Sync Complete: {synced_count} emails synced, {skipped_count} emails skipped.")
84
+
85
+ except Exception as e:
86
+ print(f"Error during Gmail sync: {e}")
87
+
88
+
89
+ from connectors.base import BaseConnector
90
+ from connectors.registry import register
91
+
92
+ @register
93
+ class GmailConnector(BaseConnector):
94
+ name = "Gmail"
95
+ slug = "gmail"
96
+
97
+ def authenticate(self) -> bool:
98
+ try:
99
+ c = Composio()
100
+ user_id = os.getenv("COMPOSIO_USER_ID", "user_123")
101
+ s = c.create(user_id=user_id)
102
+ toolkits_info = s.toolkits()
103
+ tk = next((t for t in toolkits_info.items if t.slug == "gmail"), None)
104
+ return bool(tk and tk.connection and tk.connection.is_active)
105
+ except Exception:
106
+ return False
107
+
108
+ def sync(self) -> dict:
109
+ sync_gmail()
110
+ return {"status": "success"}
111
+
112
+ def health(self) -> tuple[bool, str]:
113
+ if self.authenticate():
114
+ return True, "Connected"
115
+ return False, "Not connected"
116
+
connectors/notion.py ADDED
@@ -0,0 +1,156 @@
1
+ import sys
2
+ from datetime import datetime
3
+ from composio import Composio
4
+ from models.memory import RepositoryDocument
5
+ from storage.db import insert_repository_document, get_connection
6
+
7
+ def get_page_title(page: dict) -> str:
8
+ properties = page.get("properties", {}) or {}
9
+ # Title is usually in 'title' field or 'Name' field
10
+ title_text = ""
11
+ for field_name in ["title", "Name", "name"]:
12
+ prop = properties.get(field_name)
13
+ if isinstance(prop, dict):
14
+ title_list = prop.get("title") or prop.get("rich_text") or []
15
+ if isinstance(title_list, list) and title_list:
16
+ title_text = "".join([t.get("plain_text", "") for t in title_list if isinstance(t, dict)])
17
+ if title_text:
18
+ break
19
+ if not title_text:
20
+ title_text = f"Notion Page {page.get('id')}"
21
+ return title_text
22
+
23
+ import os
24
+
25
+ def sync_notion():
26
+ try:
27
+ try:
28
+ sys.stdout.reconfigure(encoding='utf-8')
29
+ except Exception:
30
+ pass
31
+
32
+ print("Syncing Notion...\n")
33
+ c = Composio()
34
+ user_id = os.getenv("COMPOSIO_USER_ID", "user_123")
35
+ s = c.create(user_id=user_id)
36
+
37
+ # Verify Notion toolkit is active
38
+ toolkits_info = s.toolkits()
39
+ notion_tk = next((t for t in toolkits_info.items if t.slug == "notion"), None)
40
+ if not notion_tk or not (notion_tk.connection and notion_tk.connection.is_active):
41
+ print("Notion connection not active.")
42
+ return
43
+
44
+ # Fetch page list via Composio
45
+ resp = s.execute(tool_slug="notion_search_notion_page", arguments={"query": ""})
46
+ if resp.error or not resp.data:
47
+ print("No Notion pages found.")
48
+ return
49
+
50
+ resp_data = resp.data
51
+ if "response_data" in resp_data:
52
+ resp_data = resp_data["response_data"]
53
+
54
+ pages = resp_data.get("results") or []
55
+ if not pages:
56
+ print("No Notion pages found in results.")
57
+ return
58
+
59
+ print(f"Found {len(pages)} Notion pages")
60
+
61
+ conn = get_connection()
62
+ cursor = conn.cursor()
63
+
64
+ synced_count = 0
65
+ skipped_count = 0
66
+
67
+ for page in pages:
68
+ if not isinstance(page, dict):
69
+ continue
70
+
71
+ page_id = page.get("id")
72
+ if not page_id:
73
+ continue
74
+
75
+ title = get_page_title(page)
76
+ last_edited_time = page.get("last_edited_time") or ""
77
+
78
+ # Incremental check: check if already in DB with same last_edited_time
79
+ cursor.execute(
80
+ "SELECT content, synced_at FROM repository_documents WHERE repo_name = 'Notion' AND file_name = ?",
81
+ (title,)
82
+ )
83
+ row = cursor.fetchone()
84
+ if row:
85
+ stored_synced_at = row[1]
86
+ # If we have stored it and it wasn't edited since our last sync, skip it
87
+ if stored_synced_at and last_edited_time and stored_synced_at >= last_edited_time:
88
+ skipped_count += 1
89
+ continue
90
+
91
+ # Fetch page content
92
+ try:
93
+ print(f"Syncing page: {title}")
94
+ resp_content = s.execute(
95
+ tool_slug="notion_get_page_markdown",
96
+ arguments={"page_id": page_id}
97
+ )
98
+ if resp_content and not resp_content.error and resp_content.data:
99
+ content_data = resp_content.data
100
+ if "response_data" in content_data:
101
+ content_data = content_data["response_data"]
102
+
103
+ markdown = content_data.get("markdown") or ""
104
+ if markdown.strip():
105
+ # Save to database
106
+ doc = RepositoryDocument(
107
+ repo_name="Notion",
108
+ file_name=title,
109
+ content=markdown,
110
+ source="notion_get_page_markdown",
111
+ synced_at=datetime.now().isoformat()
112
+ )
113
+ insert_repository_document(doc)
114
+ synced_count += 1
115
+ else:
116
+ print(f"Page '{title}' content was empty.")
117
+ else:
118
+ print(f"Failed to fetch content for page: {title}")
119
+ except Exception as e:
120
+ print(f"Error fetching page content for '{title}': {e}")
121
+
122
+ conn.close()
123
+ print(f"Notion Sync Complete: {synced_count} pages synced, {skipped_count} pages skipped.")
124
+
125
+ except Exception as e:
126
+ print(f"Error during Notion sync: {e}")
127
+
128
+
129
+ from connectors.base import BaseConnector
130
+ from connectors.registry import register
131
+
132
+ @register
133
+ class NotionConnector(BaseConnector):
134
+ name = "Notion"
135
+ slug = "notion"
136
+
137
+ def authenticate(self) -> bool:
138
+ try:
139
+ c = Composio()
140
+ user_id = os.getenv("COMPOSIO_USER_ID", "user_123")
141
+ s = c.create(user_id=user_id)
142
+ toolkits_info = s.toolkits()
143
+ tk = next((t for t in toolkits_info.items if t.slug == "notion"), None)
144
+ return bool(tk and tk.connection and tk.connection.is_active)
145
+ except Exception:
146
+ return False
147
+
148
+ def sync(self) -> dict:
149
+ sync_notion()
150
+ return {"status": "success"}
151
+
152
+ def health(self) -> tuple[bool, str]:
153
+ if self.authenticate():
154
+ return True, "Connected"
155
+ return False, "Not connected"
156
+
connectors/registry.py ADDED
@@ -0,0 +1,60 @@
1
+ """
2
+ Connector registry.
3
+
4
+ Discovers and manages available connectors. In Phase 1 this wraps
5
+ the existing sync functions. Future connectors simply subclass
6
+ BaseConnector and are auto-discovered.
7
+ """
8
+
9
+ import logging
10
+ from connectors.base import BaseConnector
11
+
12
+ logger = logging.getLogger("connectors.registry")
13
+
14
+ # Registry of known connector classes
15
+ _connector_classes: list[type[BaseConnector]] = []
16
+
17
+
18
+ def register(cls: type[BaseConnector]):
19
+ """Register a connector class."""
20
+ if cls not in _connector_classes:
21
+ _connector_classes.append(cls)
22
+ return cls
23
+
24
+
25
+ def discover_connectors() -> list[BaseConnector]:
26
+ """Discover and instantiate all registered connectors.
27
+
28
+ Imports connector modules to trigger registration, then
29
+ returns instances of all registered connector classes.
30
+ """
31
+ # Import connector modules to trigger @register decorators
32
+ try:
33
+ import connectors.github # noqa: F401
34
+ except ImportError:
35
+ logger.warning("Could not import connectors.github")
36
+
37
+ try:
38
+ import connectors.gmail # noqa: F401
39
+ except ImportError:
40
+ logger.warning("Could not import connectors.gmail")
41
+
42
+ try:
43
+ import connectors.notion # noqa: F401
44
+ except ImportError:
45
+ logger.warning("Could not import connectors.notion")
46
+
47
+ return [cls() for cls in _connector_classes]
48
+
49
+
50
+ def get_connector(name: str) -> BaseConnector | None:
51
+ """Return a connector instance by name."""
52
+ for connector in discover_connectors():
53
+ if connector.name.lower() == name.lower() or connector.slug == name.lower():
54
+ return connector
55
+ return None
56
+
57
+
58
+ def list_connectors() -> list[str]:
59
+ """Return names of all registered connectors."""
60
+ return [cls.name for cls in _connector_classes]
core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # core package
core/chunker.py ADDED
@@ -0,0 +1,136 @@
1
+ import datetime
2
+ from storage.db import (
3
+ get_all_repositories,
4
+ get_all_documents,
5
+ get_all_emails,
6
+ insert_document_chunk,
7
+ clear_document_chunks
8
+ )
9
+
10
+ def chunk_text(text: str, chunk_size: int = 800, overlap: int = 120) -> list:
11
+ if not text:
12
+ return []
13
+ chunks = []
14
+ start = 0
15
+ while start < len(text):
16
+ end = start + chunk_size
17
+ chunk = text[start:end]
18
+ chunks.append(chunk)
19
+ if end >= len(text):
20
+ break
21
+ start += chunk_size - overlap
22
+ return chunks
23
+
24
+ def generate_and_save_chunks(repo_names=None):
25
+ from storage.db import get_connection
26
+ created_at = datetime.datetime.now().isoformat()
27
+
28
+ conn = get_connection()
29
+ cursor = conn.cursor()
30
+
31
+ if repo_names is None:
32
+ clear_document_chunks()
33
+
34
+ # 1. Repositories (Metadata Chunks for identity search boosting)
35
+ repos = get_all_repositories()
36
+ for repo in repos:
37
+ repo_text = f"Repository: {repo['repo_name']}\nDescription: {repo['description'] or ''}\nLanguage: {repo['language'] or ''}"
38
+ # Insert repository metadata as a distinct source type to avoid mixing with document content
39
+ insert_document_chunk(
40
+ repository_name=repo["repo_name"],
41
+ document_name="metadata",
42
+ source_type="repository_metadata",
43
+ chunk_text=repo_text,
44
+ chunk_index=0,
45
+ created_at=created_at
46
+ )
47
+
48
+ # 2. Repository Documents
49
+ docs = get_all_documents()
50
+ for doc in docs:
51
+ content = doc["content"] or ""
52
+ chunks = chunk_text(content)
53
+ for idx, chunk in enumerate(chunks):
54
+ insert_document_chunk(
55
+ repository_name=doc["repo_name"],
56
+ document_name=doc["file_name"],
57
+ source_type="document",
58
+ chunk_text=chunk,
59
+ chunk_index=idx,
60
+ created_at=created_at
61
+ )
62
+
63
+ # 3. Emails
64
+ emails = get_all_emails()
65
+ for email in emails:
66
+ email_text = f"Subject: {email['subject']}\nFrom: {email['sender']}\nDate: {email['received_at']}\nContent: {email['snippet']}"
67
+ chunks = chunk_text(email_text)
68
+ for idx, chunk in enumerate(chunks):
69
+ insert_document_chunk(
70
+ repository_name=None,
71
+ document_name=email["subject"] or email["message_id"],
72
+ source_type="email",
73
+ chunk_text=chunk,
74
+ chunk_index=idx,
75
+ created_at=created_at
76
+ )
77
+ else:
78
+ # Incremental sync for specific repositories or emails
79
+ for r_name in repo_names:
80
+ if r_name == "__emails__":
81
+ # Delete existing email chunks
82
+ cursor.execute("DELETE FROM document_chunks WHERE source_type = 'email'")
83
+ conn.commit()
84
+ # Insert email chunks
85
+ emails = get_all_emails()
86
+ for email in emails:
87
+ email_text = f"Subject: {email['subject']}\nFrom: {email['sender']}\nDate: {email['received_at']}\nContent: {email['snippet']}"
88
+ chunks = chunk_text(email_text)
89
+ for idx, chunk in enumerate(chunks):
90
+ insert_document_chunk(
91
+ repository_name=None,
92
+ document_name=email["subject"] or email["message_id"],
93
+ source_type="email",
94
+ chunk_text=chunk,
95
+ chunk_index=idx,
96
+ created_at=created_at
97
+ )
98
+ else:
99
+ # Delete existing metadata and doc chunks for this repo
100
+ cursor.execute(
101
+ "DELETE FROM document_chunks WHERE repository_name = ? AND source_type IN ('document', 'repository_metadata')",
102
+ (r_name,)
103
+ )
104
+ conn.commit()
105
+
106
+ # Insert repo metadata chunk
107
+ cursor.execute("SELECT repo_name, description, language FROM repositories WHERE repo_name = ?", (r_name,))
108
+ repo_row = cursor.fetchone()
109
+ if repo_row:
110
+ repo_text = f"Repository: {repo_row[0]}\nDescription: {repo_row[1] or ''}\nLanguage: {repo_row[2] or ''}"
111
+ insert_document_chunk(
112
+ repository_name=repo_row[0],
113
+ document_name="metadata",
114
+ source_type="repository_metadata",
115
+ chunk_text=repo_text,
116
+ chunk_index=0,
117
+ created_at=created_at
118
+ )
119
+
120
+ # Insert repo document chunks
121
+ cursor.execute("SELECT repo_name, file_name, content FROM repository_documents WHERE repo_name = ?", (r_name,))
122
+ doc_rows = cursor.fetchall()
123
+ for d_row in doc_rows:
124
+ content = d_row[2] or ""
125
+ chunks = chunk_text(content)
126
+ for idx, chunk in enumerate(chunks):
127
+ insert_document_chunk(
128
+ repository_name=d_row[0],
129
+ document_name=d_row[1],
130
+ source_type="document",
131
+ chunk_text=chunk,
132
+ chunk_index=idx,
133
+ created_at=created_at
134
+ )
135
+ conn.close()
136
+