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,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-memory-os
3
+ Version: 0.1.0
4
+ Summary: CLI-based Personal Knowledge Operating System
5
+ Author-email: Anirudh T <anirudh200584@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: RAG,Knowledge-Graph,Vector-Search,Personal-Assistant,LLM,Composio
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: composio==0.13.1
16
+ Requires-Dist: composio-langchain==0.13.1
17
+ Requires-Dist: langchain==1.3.9
18
+ Requires-Dist: langchain-groq==1.1.3
19
+ Requires-Dist: python-dotenv==1.2.2
20
+ Requires-Dist: qdrant-client==1.18.0
21
+ Requires-Dist: neo4j==6.2.0
22
+ Requires-Dist: sentence-transformers==5.6.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # Memory‑OS 🧠
28
+
29
+ [![PyPI version](https://img.shields.io/pypi/v/memory-os.svg)](https://pypi.org/project/memory-os/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+
32
+ **Memory‑OS** is a local Personal Knowledge Operating System that syncs, indexes, and retrieves information across your GitHub repositories, emails, and Notion workspaces. It runs a unified interactive CLI, exposing hybrid keyword + semantic search and natural language QA powered by RAG, local embeddings, and a knowledge graph.
33
+
34
+ ---
35
+
36
+ ## 🏗️ Architecture
37
+
38
+ Memory-OS is built as a modular architecture consisting of ingestion, databases, scoring ranking engines, and a terminal user loop:
39
+
40
+ ```mermaid
41
+ graph TD
42
+ %% Ingestion
43
+ subgraph Ingestion [1. Ingestion Layer]
44
+ GH[GitHub Repos & Docs]
45
+ GM[Gmail Inbox Messages]
46
+ NT[Notion Page Contents]
47
+ CP[Composio Integration Platform]
48
+ GH --> CP
49
+ GM --> CP
50
+ NT --> CP
51
+ end
52
+
53
+ %% Storage & Indexing
54
+ subgraph Storage [2. Storage & Indexing Layer]
55
+ DB[(SQLite: workspace.db)]
56
+ QD[(Qdrant: Qdrant Server)]
57
+ N4J[(Neo4j Graph Database)]
58
+ SQL_G[(SQLite Graph Fallback)]
59
+
60
+ CP -->|Insert Metadata & Docs| DB
61
+ DB -->|Text Chunks| CH[Chunking Core]
62
+ CH -->|Local Embeddings| EM[SentenceTransformer]
63
+ EM -->|Vectors| QD
64
+
65
+ DB -->|Graph Construction| N4J
66
+ DB -->|Graph Construction| SQL_G
67
+ end
68
+
69
+ %% Retrieval & RAG
70
+ subgraph Retrieval [3. Retrieval & RAG Layer]
71
+ HS[Hybrid Search Router]
72
+ QD -->|Cosine Similarity| HS
73
+ DB -->|Keyword Matching| HS
74
+ N4J -->|Graph Lookups| HS
75
+ SQL_G -->|Graph Fallback Lookups| HS
76
+
77
+ HR[Hybrid Ranking Scoring]
78
+ HS --> HR
79
+
80
+ RAG[RAG Context Builder]
81
+ HR -->|Merged Context| RAG
82
+
83
+ LLM[Groq LLM Pipeline]
84
+ RAG -->|Prompt Assembly| LLM
85
+ end
86
+
87
+ %% User Interaction
88
+ subgraph User [4. Interface Layer]
89
+ CLI[main.py: CLI Command Loop]
90
+ CLI -->|Sync/Rebuild| Ingestion
91
+ CLI -->|Search/Ask Queries| Retrieval
92
+ LLM -->|Formatted Answer| CLI
93
+ end
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 🗄️ Database Technology Rationale
99
+
100
+ Memory-OS adopts a **multi-model storage engine strategy**, selecting each technology to excel at its designated retrieve-and-rank role:
101
+
102
+ | Database | Selection Rationale |
103
+ | :--- | :--- |
104
+ | **SQLite (`workspace.db`)** | Chosen for lightweight structured storage. It holds raw documents, chunk segments, email metadata, and repository statistics, providing ACID compliance and ultra-fast exact keyword searches. |
105
+ | **Qdrant** | Chosen as a high-performance vector database optimized for storing and executing cosine similarity search queries on $384$-dimensional dense vector embeddings generated by Sentence-Transformers. |
106
+ | **Neo4j / Fallback SQLite** | Neo4j is utilized as a native graph database to map complex developer relationships (e.g. `Repository-[USES]->Technology` or `Email-[SENT_BY]->User`). If Neo4j is unreachable, it seamlessly falls back to a relational SQLite graph schema, preserving search functionality offline. |
107
+
108
+ ---
109
+
110
+ ## 🗄️ Workspace Structure
111
+
112
+ Memory-OS manages directories and configuration under the user's home folder (`~/.memory-os/`):
113
+ ```
114
+ ~/.memory-os/
115
+ ├── config.toml # Global TOML settings configuration
116
+ ├── active_profile # Stores name of the currently active profile
117
+ └── workspaces/
118
+ ├── default/ # Default workspace profile directory
119
+ │ ├── workspace.db # SQLite relational database
120
+ │ ├── qdrant/ # Qdrant local bind-mounted storage folder
121
+ │ ├── neo4j/ # Neo4j local bind-mounted storage folder
122
+ │ ├── logs/ # Log folder containing memory_os.log
123
+ │ └── cache/ # Chunker cache folder
124
+ └── personal/ # Personal workspace profile directory
125
+ ```
126
+
127
+ ---
128
+
129
+ ## 📦 Setup & Installation
130
+
131
+ Ensure you have **Python >= 3.12** and **Docker + Docker Compose** installed.
132
+
133
+ ### 1. Install via pip
134
+ You can install Memory-OS directly as a package:
135
+ ```bash
136
+ pip install .
137
+ ```
138
+ This registers the CLI entry point executable `memory-os` on your path.
139
+
140
+ ### 2. Run the Initialization Wizard
141
+ Kick off the interactive wizard to verify system dependencies, configure API keys, spin up containers, and pre-warm model weights:
142
+ ```bash
143
+ memory-os init
144
+ ```
145
+
146
+ ---
147
+
148
+ ## 🚀 CLI Commands Reference
149
+
150
+ Memory-OS exposes a comprehensive CLI for administration:
151
+
152
+ ### Core Daemon Lifecycle
153
+ - **`memory-os start`**: Spins up Neo4j and Qdrant database services in the background using Docker Compose.
154
+ - **`memory-os stop`**: Stops the database services while retaining data directories intact.
155
+
156
+ ### Operations & Ingestion
157
+ - **`memory-os sync [--source SOURCE] [--rebuild]`**: Triggers incremental data imports from registered sources (GitHub, Gmail, Notion). Add `--rebuild` for full vector/graph resets.
158
+ - **`memory-os ask <question>`**: Runs natural language queries against the RAG retrieval pipeline.
159
+ - **`memory-os graph <repo>`**: Visualizes the relationships of an indexed repository in the terminal knowledge graph.
160
+
161
+ ### Diagnostics & Monitoring
162
+ - **`memory-os doctor`**: Analyzes connection validation health across all endpoints and prints actionable troubleshooting tips on failure.
163
+ - **`memory-os monitor`**: Displays aggregated data latencies (indexing speed, search rates, LLM times) by parsing system log traces.
164
+ - **`memory-os benchmark`**: Performs query speed runs on keyword, semantic, hybrid searches, and RAG pipelines.
165
+ - **`memory-os logs [--tail N]`**: Tails the running logs of Memory-OS (rotates at 5MB, up to 3 backups).
166
+
167
+ ### Configuration Management
168
+ - **`memory-os config show`**: Displays the active key-value configuration block.
169
+ - **`memory-os config get <key>`**: Fetches a nested key value (e.g. `groq.model`).
170
+ - **`memory-os config set <key> <value>`**: Sets a nested key value with type validation checks.
171
+ - **`memory-os config reset`**: Prompts and reverts all configurations to factory defaults.
172
+
173
+ ### Workspace Profile Profiles
174
+ - **`memory-os workspace list`**: Lists all profiles (* marks active).
175
+ - **`memory-os workspace create <name>`**: Allocates a new workspace folder tree.
176
+ - **`memory-os workspace switch <name>`**: Switches the active profile context.
177
+ - **`memory-os workspace delete <name>`**: Wipes profile folder directories.
178
+ - **`memory-os workspace info`**: Displays detailed record metrics (nodes, vectors, sizes) for the active profile.
179
+
180
+ ### Portability (Export / Import)
181
+ - **`memory-os export <backup-zip>`**: Compresses database schemas, configurations, and vector indices into a versioned zip package.
182
+ - **`memory-os import <backup-zip>`**: Overwrites the active workspace profile using files from an export package after validating versions and model compatibility.
183
+
184
+ ---
185
+
186
+ ## 🔌 Plugin System Architecture
187
+
188
+ Memory-OS features a structured connector registry. Every connector implements `BaseConnector` (`connectors/base.py`) and is registered using the `@register` decorator (`connectors/registry.py`):
189
+
190
+ ```python
191
+ from connectors.base import BaseConnector
192
+ from connectors.registry import register
193
+
194
+ @register
195
+ class SlackConnector(BaseConnector):
196
+ name = "Slack"
197
+ slug = "slack"
198
+
199
+ def authenticate(self) -> bool:
200
+ # Check OAuth or API status
201
+ return True
202
+
203
+ def sync(self) -> dict:
204
+ # Fetch channels and messages
205
+ return {"synced": 42}
206
+
207
+ def health(self) -> tuple[bool, str]:
208
+ return True, "Connected"
209
+ ```
210
+
211
+ To list registered plugins:
212
+ ```bash
213
+ memory-os plugins
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 🛠️ Troubleshooting
219
+
220
+ - **Database Offline / Port conflicts**: If Neo4j (ports 7474/7687) or Qdrant (port 6333) fails to start, modify port configurations:
221
+ ```bash
222
+ memory-os config set neo4j.port_http 7475
223
+ memory-os config set qdrant.port 6334
224
+ memory-os start
225
+ ```
226
+ - **Failing Diagnostics**: Run `memory-os doctor` to inspect status. It provides detailed actionable tips to address common environment issues.
227
+
228
+ ---
229
+
230
+ ## 📜 License
231
+ This project is licensed under the **MIT License**.
@@ -0,0 +1,50 @@
1
+ cli/__init__.py,sha256=QHJaTQVcqGsvBTXZQN2GFSjZLYQImJg5Xwqj2DzR5IA,14
2
+ cli/main.py,sha256=95s8VAuSmReoXvQvetPHBgGv4OGu7tCByC5mE1oHsRc,19853
3
+ cli/parser.py,sha256=aZXjqL9MA25RvgPQ70J_-O2axO1yex4Chu9FBFczH1g,8384
4
+ cli/commands/__init__.py,sha256=S8cCOAW7jWWQwD6sHjvbDtITsX2pWDZT8-R1mHwArFE,23
5
+ cli/commands/benchmark.py,sha256=Wk7PZzcAlRVlBiaiZ8mOOkfIbIT1-Rzy7Db99Ucq6yI,5785
6
+ cli/commands/config_cmd.py,sha256=nwft5p2hM-2uIVxEr-uhTYObW4wcqAhP9WS7uo9XjjI,1874
7
+ cli/commands/doctor.py,sha256=8_KgoP3WDsHu3JUNgXJHotuZnl13inoh1zgpYxPLgLc,2773
8
+ cli/commands/export.py,sha256=iI_10jGe5pqYSN17vtSpjxyq4hwnYgnkbLqCxJriFrs,3992
9
+ cli/commands/import_cmd.py,sha256=MDNvDtVPkWx3gcDttDlzqaQ-XOM1AnPEF2ltvteM738,5883
10
+ cli/commands/init.py,sha256=ApL8OuQ0cmNgRJlXuIQu7gNWjyv-gz2td-uY2KiIMFA,8376
11
+ cli/commands/logs.py,sha256=XZKt5yMtqA0zb9oejSsOezjJkSgvf7XKvUWyo0NuWNw,1310
12
+ cli/commands/monitor.py,sha256=JI9-3EkGpS1YSKdbt9l1OevuqvEciHuaF6wA_dsrPNE,2633
13
+ cli/commands/plugins.py,sha256=fAGpM3EqltunByjdUENEz3BFjUO9yzay_8cSYbeturk,1270
14
+ cli/commands/start.py,sha256=GBhsd7oQbCcDkXNnvkLJ9QSStFgrr-Dxnk9kp41MUT8,715
15
+ cli/commands/status.py,sha256=T-HlH5Y8P-KD8HadAeW9FM4BaDQJXlc-llsSKPUrtw0,3224
16
+ cli/commands/stop.py,sha256=sG8tspAux9vRTTCSMHTDP1oquvzxqR-OnSt-dejimyE,490
17
+ cli/commands/version.py,sha256=4kfZiATehobqeJB0yWn8aQ7AVrjiXYxOh-CnNEVuNQ8,672
18
+ cli/commands/workspace.py,sha256=zd_SGNY4iLbQUPCWaxVIGQaCJ6TcrldBQBiQjGm3-bg,5828
19
+ cli_memory_os-0.1.0.dist-info/licenses/LICENSE,sha256=GpOhGcWM40QPUZUgla7eWQSmtxX6aorZzm6sRIAYHH8,1074
20
+ connectors/__init__.py,sha256=AXFbXzb1TrpJykApKqN6WarHxwtDm5ZtW_CT1xiSsG4,21
21
+ connectors/base.py,sha256=wtqDPMkrLJrsUARhrAiZIghE-He0_yz-DuCoAg2s3mw,934
22
+ connectors/github.py,sha256=wFtiPjGguSMxCCg1T1dOGhDfjB2A9M2Ig4MbYz086WA,11009
23
+ connectors/gmail.py,sha256=hLXmHapgGSdDcwHsmNTp9bQyKytPEODQMiXyGG59LWQ,3903
24
+ connectors/notion.py,sha256=eS-T6XGPbN10eabU_OYJ7dvszSS0Rl1TtY2BC_gKpwE,5580
25
+ connectors/registry.py,sha256=v9IbztU8AvcEkiti4hKNTrdCv2p3XKqkJE3DGtWH_Js,1763
26
+ core/__init__.py,sha256=XSEHCVZFjVWh0MiJPA2hhHR_XNL6lhR6WMhtc4nUhgw,15
27
+ core/chunker.py,sha256=2xeCNxSskJI-qQq_v69ZxaxXei1bI3uFIQWcWQ5Og5g,5695
28
+ core/context_builder.py,sha256=EpCiHWsVE7rE5kVDETiAx_lEW8BHo8IDm82Mq2eQEfQ,18940
29
+ core/embedder.py,sha256=_ISGc9Zhlh2iGdejySkraet3HAZY5Xppd1U-00AY7lA,1330
30
+ core/llm.py,sha256=kNMsJALKNcYNvPX3llB3FJy-wz5saspuPJWcCQYRjuk,11897
31
+ core/vector_store.py,sha256=IOv98uqJzItoxGtjUhg-Yq7Hf6kkJyvTqpZs0ln5V_c,24380
32
+ infrastructure/__init__.py,sha256=JVrwf4YJ8d8Oz9tTNuUn3qR5-sDT2C0zPwELBVZ4akY,25
33
+ infrastructure/compose.py,sha256=lRDYuvycZPLs0_6jEa93EQUhM3CFAlBeT_Y8rTnuHHg,4152
34
+ infrastructure/config.py,sha256=XoCJCaeK07FPPgH0A6976Yi2N4M9fr9LCYIelZK3LKw,8617
35
+ infrastructure/docker.py,sha256=GAHs3neBn9RT-LOaxkO4RxNPg-r7-b179KpfPAcBBB4,1796
36
+ infrastructure/health.py,sha256=eBsMjgI2V6p4ii-imMWRrwrJxOzXa-Hphz2AGle44EM,11039
37
+ infrastructure/observability.py,sha256=d46wMjU9afY2ql6z0MGrSHTCcMZ3kRoE5U_BFy9dHi0,5451
38
+ infrastructure/workspace.py,sha256=5u5KWNCR6beFZTelJmjAF2PDeLuHjFRx_gou_SxNwc0,5025
39
+ models/__init__.py,sha256=H8_hNLjdE649el_5XPoWR54vBs0IYM2_GG_Fv6bUZ7A,17
40
+ models/memory.py,sha256=uNrCYU-DyFI44J-p4_K8ZshmmzG7OAQrWWgEm-tUdp4,1112
41
+ storage/__init__.py,sha256=c3L5t0o9t2EU138qN0DQV5FEObaMBT20QJv7XIAifMU,18
42
+ storage/db.py,sha256=lRau0SGcizSmT_KKVozbV81ZqZkob20X6IkMpJUEEIo,18030
43
+ storage/graph.py,sha256=4ekFcyvnIt8grVXaEOlKX-KHWtJ7bghRiV3gadae404,14421
44
+ storage/schema.sql,sha256=DyF6j04MUiAktOrYElA6vt918VdKyFGlznu6ZurkYY8,1244
45
+ storage/tech_detector.py,sha256=HRmEiXAEZVl2mUKQFAlWp3sHcVQKQDIVZFVoOawzFEU,4166
46
+ cli_memory_os-0.1.0.dist-info/METADATA,sha256=1br7XgYQVcqqM-UCa3FUjT5nWBF9a31E53vo2LTqgd0,9483
47
+ cli_memory_os-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
48
+ cli_memory_os-0.1.0.dist-info/entry_points.txt,sha256=QRmguBsVJwGhUUNQQTPRtScf_q3BbConmsh-f9EPZIo,54
49
+ cli_memory_os-0.1.0.dist-info/top_level.txt,sha256=dejPIyF3Ll6dTZuSWMjn3Fgs3TY2T4wRF7RIJmSEzC4,50
50
+ cli_memory_os-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ memory-os = cli.main:cli_entrypoint
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Memory-OS Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ cli
2
+ connectors
3
+ core
4
+ infrastructure
5
+ models
6
+ storage
connectors/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # connectors package
connectors/base.py ADDED
@@ -0,0 +1,39 @@
1
+ """
2
+ Connector plugin interface.
3
+
4
+ All connectors must subclass BaseConnector and implement
5
+ authenticate(), sync(), and health() methods.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+
10
+
11
+ class BaseConnector(ABC):
12
+ """Interface that all Memory-OS connectors must implement."""
13
+
14
+ name: str = ""
15
+ slug: str = ""
16
+
17
+ @abstractmethod
18
+ def authenticate(self) -> bool:
19
+ """Check if authentication is active for this connector.
20
+
21
+ Returns True if the connector is authenticated and ready to sync.
22
+ """
23
+ ...
24
+
25
+ @abstractmethod
26
+ def sync(self) -> dict:
27
+ """Run the sync process for this connector.
28
+
29
+ Returns a summary dict with keys like 'synced', 'skipped', 'errors'.
30
+ """
31
+ ...
32
+
33
+ @abstractmethod
34
+ def health(self) -> tuple[bool, str]:
35
+ """Check the health/connectivity status of this connector.
36
+
37
+ Returns (is_healthy, detail_string).
38
+ """
39
+ ...
connectors/github.py ADDED
@@ -0,0 +1,273 @@
1
+ import base64
2
+ from datetime import datetime
3
+ from composio import Composio
4
+ from models.memory import Repository, RepositoryDocument
5
+ from storage.db import (
6
+ insert_repository,
7
+ insert_repository_document,
8
+ get_repo_count,
9
+ get_repository_document_count,
10
+ get_repository_details
11
+ )
12
+
13
+ def decode_github_content(content_str: str, encoding: str = "base64") -> str:
14
+ if not content_str:
15
+ return ""
16
+ cleaned = content_str.replace("\n", "").replace("\r", "").strip()
17
+ try:
18
+ decoded_bytes = base64.b64decode(cleaned)
19
+ return decoded_bytes.decode("utf-8", errors="ignore")
20
+ except Exception:
21
+ return content_str
22
+
23
+ def extract_metadata(data: dict):
24
+ repo_name = data.get("name") or ""
25
+ description = data.get("description") or ""
26
+ language = data.get("language") or "N/A"
27
+ visibility = data.get("visibility") or "public"
28
+
29
+ stars = data.get("stargazers_count")
30
+ if stars is None:
31
+ stars = data.get("stars") or 0
32
+
33
+ forks = data.get("forks_count")
34
+ if forks is None:
35
+ forks = data.get("forks") or 0
36
+
37
+ open_issues = data.get("open_issues_count")
38
+ if open_issues is None:
39
+ open_issues = data.get("open_issues") or 0
40
+
41
+ default_branch = data.get("default_branch") or "main"
42
+ updated_at = data.get("updated_at") or ""
43
+ url = data.get("html_url") or data.get("url") or ""
44
+
45
+ return repo_name, description, language, visibility, stars, forks, open_issues, default_branch, updated_at, url
46
+
47
+ import os
48
+ import sys
49
+
50
+ def sync_github():
51
+ try:
52
+ try:
53
+ sys.stdout.reconfigure(encoding='utf-8')
54
+ except Exception:
55
+ pass
56
+
57
+ c = Composio()
58
+ user_id = os.getenv("COMPOSIO_USER_ID", "user_123")
59
+ s = c.create(user_id=user_id)
60
+
61
+ # Verify connection
62
+ toolkits_info = s.toolkits()
63
+ github_tk = next((t for t in toolkits_info.items if t.slug == "github"), None)
64
+ if not github_tk or not (github_tk.connection and github_tk.connection.is_active):
65
+ print("GitHub connection not active.")
66
+ return
67
+
68
+ print("Syncing GitHub...\n")
69
+
70
+ # Fetch repositories
71
+ resp = s.execute(tool_slug="github_list_repositories_for_the_authenticated_user", arguments={})
72
+ if resp.error or not resp.data:
73
+ print("No repositories found.")
74
+ return
75
+
76
+ resp_data = resp.data
77
+ if isinstance(resp_data, dict) and "response_data" in resp_data:
78
+ resp_data = resp_data["response_data"]
79
+
80
+ repos = []
81
+ if isinstance(resp_data, list):
82
+ repos = resp_data
83
+ elif isinstance(resp_data, dict):
84
+ repos = resp_data.get("repositories") or resp_data.get("items") or []
85
+
86
+ if not isinstance(repos, list) or not repos:
87
+ print("No repositories found.")
88
+ return
89
+
90
+ print(f"Found {len(repos)} repositories\n")
91
+
92
+ for repo in repos:
93
+ if not isinstance(repo, dict):
94
+ continue
95
+ repo_name = repo.get("name")
96
+ if not repo_name:
97
+ continue
98
+
99
+ # Incremental sync check
100
+ existing = get_repository_details(repo_name)
101
+ if existing:
102
+ existing_updated_at = existing.get("updated_at") or ""
103
+ repo_updated_at = repo.get("updated_at") or ""
104
+ if existing_updated_at and repo_updated_at and existing_updated_at[:19] == repo_updated_at[:19]:
105
+ if existing.get("files"):
106
+ print(f"Repository {repo_name} is up-to-date. Skipping sync.")
107
+ continue
108
+
109
+ full_name = repo.get("full_name", "")
110
+ owner = None
111
+ if repo.get("owner") and isinstance(repo.get("owner"), dict):
112
+ owner = repo.get("owner", {}).get("login")
113
+ if not owner and "/" in full_name:
114
+ owner = full_name.split("/")[0]
115
+ if not owner:
116
+ continue
117
+
118
+ # Fetch detailed metadata
119
+ try:
120
+ resp_meta = s.execute(
121
+ tool_slug="github_get_a_repository",
122
+ arguments={"owner": owner, "repo": repo_name}
123
+ )
124
+ if resp_meta and not resp_meta.error and resp_meta.data:
125
+ meta_data = resp_meta.data
126
+ if "response_data" in meta_data and isinstance(meta_data["response_data"], dict):
127
+ meta_data = meta_data["response_data"]
128
+ else:
129
+ meta_data = repo
130
+ except Exception:
131
+ meta_data = repo
132
+
133
+ # Extract & Save Repository Metadata
134
+ repo_name, description, language, visibility, stars, forks, open_issues, default_branch, updated_at, url = extract_metadata(meta_data)
135
+ db_repo = Repository(
136
+ repo_name=repo_name,
137
+ description=description,
138
+ language=language,
139
+ visibility=visibility,
140
+ stars=stars,
141
+ forks=forks,
142
+ open_issues=open_issues,
143
+ default_branch=default_branch,
144
+ updated_at=updated_at,
145
+ url=url
146
+ )
147
+ insert_repository(db_repo)
148
+
149
+ stored_files = {
150
+ "README.md": False,
151
+ "package.json": False,
152
+ "requirements.txt": False
153
+ }
154
+
155
+ # Fetch README
156
+ readme_stored = False
157
+ synced_at_str = datetime.now().isoformat()
158
+ try:
159
+ resp_readme = s.execute(
160
+ tool_slug="github_get_a_repository_readme",
161
+ arguments={"owner": owner, "repo": repo_name}
162
+ )
163
+ if resp_readme and not resp_readme.error and resp_readme.data:
164
+ readme_data = resp_readme.data
165
+ if "response_data" in readme_data and isinstance(readme_data["response_data"], dict):
166
+ readme_data = readme_data["response_data"]
167
+ if "content" in readme_data and isinstance(readme_data["content"], dict):
168
+ readme_data = readme_data["content"]
169
+
170
+ raw_content = readme_data.get("content") or ""
171
+ encoding = readme_data.get("encoding") or "base64"
172
+ decoded_readme = decode_github_content(raw_content, encoding)
173
+ if decoded_readme.strip():
174
+ doc = RepositoryDocument(
175
+ repo_name=repo_name,
176
+ file_name="README.md",
177
+ content=decoded_readme,
178
+ source="github_get_a_repository_readme",
179
+ synced_at=synced_at_str
180
+ )
181
+ insert_repository_document(doc)
182
+ stored_files["README.md"] = True
183
+ readme_stored = True
184
+ except Exception:
185
+ pass
186
+
187
+ # Fetch specific configuration files
188
+ files_to_check = ["README.md", "package.json", "requirements.txt", "pyproject.toml", "docker-compose.yml", "Dockerfile"]
189
+ for file_path in files_to_check:
190
+ if file_path == "README.md" and readme_stored:
191
+ continue
192
+
193
+ try:
194
+ resp_file = s.execute(
195
+ tool_slug="github_get_repository_content",
196
+ arguments={"owner": owner, "repo": repo_name, "path": file_path}
197
+ )
198
+ if resp_file and not resp_file.error and resp_file.data:
199
+ file_data = resp_file.data
200
+ if "response_data" in file_data and isinstance(file_data["response_data"], dict):
201
+ file_data = file_data["response_data"]
202
+ if "content" in file_data and isinstance(file_data["content"], dict):
203
+ file_data = file_data["content"]
204
+
205
+ raw_content = file_data.get("content")
206
+ if raw_content:
207
+ encoding = file_data.get("encoding") or "base64"
208
+ decoded_content = decode_github_content(raw_content, encoding)
209
+ if decoded_content.strip():
210
+ doc = RepositoryDocument(
211
+ repo_name=repo_name,
212
+ file_name=file_path,
213
+ content=decoded_content,
214
+ source="github_get_repository_content",
215
+ synced_at=synced_at_str
216
+ )
217
+ insert_repository_document(doc)
218
+ if file_path in stored_files:
219
+ stored_files[file_path] = True
220
+ except Exception:
221
+ pass
222
+
223
+ # Print repository sync details
224
+ print("--------------------------------------------------")
225
+ print(f"Repository: {repo_name}")
226
+ print(f"Language: {language}")
227
+ print(f"Stars: {stars}")
228
+ print(f"Forks: {forks}")
229
+ print(f"README: {'✓' if stored_files['README.md'] else '✗'}")
230
+ print(f"package.json: {'✓' if stored_files['package.json'] else '✗'}")
231
+ print(f"requirements.txt: {'✓' if stored_files['requirements.txt'] else '✗'}")
232
+ print("--------------------------------------------------")
233
+ print()
234
+
235
+ # Print final summary stats
236
+ total_repos = get_repo_count()
237
+ total_docs = get_repository_document_count()
238
+ print("GitHub Sync Complete")
239
+ print(f"Repositories Stored: {total_repos}")
240
+ print(f"Documents Stored: {total_docs}")
241
+
242
+ except Exception as e:
243
+ print(f"Error during GitHub sync: {e}")
244
+
245
+
246
+ from connectors.base import BaseConnector
247
+ from connectors.registry import register
248
+
249
+ @register
250
+ class GitHubConnector(BaseConnector):
251
+ name = "GitHub"
252
+ slug = "github"
253
+
254
+ def authenticate(self) -> bool:
255
+ try:
256
+ c = Composio()
257
+ user_id = os.getenv("COMPOSIO_USER_ID", "user_123")
258
+ s = c.create(user_id=user_id)
259
+ toolkits_info = s.toolkits()
260
+ tk = next((t for t in toolkits_info.items if t.slug == "github"), None)
261
+ return bool(tk and tk.connection and tk.connection.is_active)
262
+ except Exception:
263
+ return False
264
+
265
+ def sync(self) -> dict:
266
+ sync_github()
267
+ return {"status": "success"}
268
+
269
+ def health(self) -> tuple[bool, str]:
270
+ if self.authenticate():
271
+ return True, "Connected"
272
+ return False, "Not connected"
273
+