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
|
@@ -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
|
+
[](https://pypi.org/project/memory-os/)
|
|
30
|
+
[](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,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.
|
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
|
+
|