memuron 0.1.1__tar.gz
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.
- memuron-0.1.1/PKG-INFO +242 -0
- memuron-0.1.1/README.md +216 -0
- memuron-0.1.1/pyproject.toml +45 -0
- memuron-0.1.1/src/memuron/__init__.py +3 -0
- memuron-0.1.1/src/memuron/actions/__init__.py +12 -0
- memuron-0.1.1/src/memuron/actions/context.py +63 -0
- memuron-0.1.1/src/memuron/actions/helpers.py +88 -0
- memuron-0.1.1/src/memuron/actions/memory.py +340 -0
- memuron-0.1.1/src/memuron/actions/memory_write.py +290 -0
- memuron-0.1.1/src/memuron/actions/nodes.py +340 -0
- memuron-0.1.1/src/memuron/actions/registry.py +5 -0
- memuron-0.1.1/src/memuron/actions/runtime.py +37 -0
- memuron-0.1.1/src/memuron/actions/spaces_documents.py +720 -0
- memuron-0.1.1/src/memuron/actions/sync.py +155 -0
- memuron-0.1.1/src/memuron/application/__init__.py +1 -0
- memuron-0.1.1/src/memuron/application/api.py +206 -0
- memuron-0.1.1/src/memuron/application/app.py +103 -0
- memuron-0.1.1/src/memuron/application/capabilities.py +82 -0
- memuron-0.1.1/src/memuron/application/cli.py +35 -0
- memuron-0.1.1/src/memuron/application/config.py +176 -0
- memuron-0.1.1/src/memuron/application/mcp.py +44 -0
- memuron-0.1.1/src/memuron/application/mcp_oauth.py +290 -0
- memuron-0.1.1/src/memuron/application/registry.py +52 -0
- memuron-0.1.1/src/memuron/context.py +532 -0
- memuron-0.1.1/src/memuron/documents/__init__.py +1 -0
- memuron-0.1.1/src/memuron/documents/link_guardian.py +192 -0
- memuron-0.1.1/src/memuron/documents/linking.py +292 -0
- memuron-0.1.1/src/memuron/documents/parser.py +1152 -0
- memuron-0.1.1/src/memuron/documents/storage.py +151 -0
- memuron-0.1.1/src/memuron/documents/url_ingest.py +375 -0
- memuron-0.1.1/src/memuron/domain/__init__.py +1 -0
- memuron-0.1.1/src/memuron/domain/decoders.py +1 -0
- memuron-0.1.1/src/memuron/domain/encoders.py +185 -0
- memuron-0.1.1/src/memuron/domain/lifecycles.py +8 -0
- memuron-0.1.1/src/memuron/domain/limits.py +6 -0
- memuron-0.1.1/src/memuron/domain/representations.py +56 -0
- memuron-0.1.1/src/memuron/domain/schemas.py +581 -0
- memuron-0.1.1/src/memuron/domain/scope_filter.py +104 -0
- memuron-0.1.1/src/memuron/graphfs/__init__.py +1 -0
- memuron-0.1.1/src/memuron/graphfs/manual.py +635 -0
- memuron-0.1.1/src/memuron/graphfs/projection.py +578 -0
- memuron-0.1.1/src/memuron/graphfs/query.py +1782 -0
- memuron-0.1.1/src/memuron/graphfs/read_model.py +574 -0
- memuron-0.1.1/src/memuron/ingest/__init__.py +1 -0
- memuron-0.1.1/src/memuron/ingest/guardian.py +213 -0
- memuron-0.1.1/src/memuron/ingest/jobs.py +424 -0
- memuron-0.1.1/src/memuron/ingest/prompts.py +147 -0
- memuron-0.1.1/src/memuron/memory/__init__.py +1 -0
- memuron-0.1.1/src/memuron/memory/engine.py +35 -0
- memuron-0.1.1/src/memuron/memory/projections.py +452 -0
- memuron-0.1.1/src/memuron/memory/recipes.py +3247 -0
- memuron-0.1.1/src/memuron/persistence/__init__.py +1 -0
- memuron-0.1.1/src/memuron/persistence/db_pool.py +57 -0
- memuron-0.1.1/src/memuron/persistence/identity_store.py +918 -0
- memuron-0.1.1/src/memuron/persistence/store_helpers.py +16 -0
- memuron-0.1.1/src/memuron/search/__init__.py +1 -0
- memuron-0.1.1/src/memuron/search/fulltext.py +110 -0
- memuron-0.1.1/src/memuron/search/hybrid.py +284 -0
- memuron-0.1.1/src/memuron/search/pgvector.py +252 -0
- memuron-0.1.1/src/memuron/security/__init__.py +1 -0
- memuron-0.1.1/src/memuron/security/auth.py +143 -0
- memuron-0.1.1/src/memuron/security/auth_provider.py +119 -0
- memuron-0.1.1/src/memuron/security/authorization.py +53 -0
- memuron-0.1.1/src/memuron/security/clerk_scopes.py +94 -0
- memuron-0.1.1/src/memuron/security/clerk_webhooks.py +61 -0
- memuron-0.1.1/src/memuron/security/jwt_tokens.py +53 -0
- memuron-0.1.1/src/memuron/security/passwords.py +38 -0
- memuron-0.1.1/src/memuron/security/tenant.py +58 -0
- memuron-0.1.1/src/memuron/spaces/__init__.py +1 -0
- memuron-0.1.1/src/memuron/spaces/model.py +35 -0
- memuron-0.1.1/src/memuron/spaces/service.py +155 -0
- memuron-0.1.1/src/memuron/sync/__init__.py +25 -0
- memuron-0.1.1/src/memuron/sync/folder.py +828 -0
memuron-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: memuron
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Arthaanu application built on Artha Engine.
|
|
5
|
+
Requires-Dist: artha-engine[auth,mcp,app]>=0.1.3
|
|
6
|
+
Requires-Dist: agno>=2.3.11
|
|
7
|
+
Requires-Dist: fastapi>=0.115.0
|
|
8
|
+
Requires-Dist: uvicorn[standard]>=0.34.0
|
|
9
|
+
Requires-Dist: psycopg[binary]>=3.2.0
|
|
10
|
+
Requires-Dist: psycopg-pool>=3.3.1
|
|
11
|
+
Requires-Dist: pyjwt>=2.10.0
|
|
12
|
+
Requires-Dist: email-validator>=2.2.0
|
|
13
|
+
Requires-Dist: pypdf>=6.0.0
|
|
14
|
+
Requires-Dist: python-multipart>=0.0.20
|
|
15
|
+
Requires-Dist: requests>=2.34.0
|
|
16
|
+
Requires-Dist: pymupdf>=1.23
|
|
17
|
+
Requires-Dist: python-docx>=1.1.0
|
|
18
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
19
|
+
Requires-Dist: python-pptx>=1.0.2
|
|
20
|
+
Requires-Dist: xlrd>=2.0.2
|
|
21
|
+
Requires-Dist: olefile>=0.47
|
|
22
|
+
Requires-Dist: svix>=1.40.0
|
|
23
|
+
Requires-Dist: boto3>=1.35.0
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# memuron
|
|
28
|
+
|
|
29
|
+
An Arthaanu application built on Artha Engine.
|
|
30
|
+
|
|
31
|
+
**Documentation:** see [`docs/`](./docs/README.md) for architecture, rich nodes, document ingest, Guardian/spaces, frontend workbench, deployment, [Railway agent deploy guide](./docs/railway-deploy.md), and troubleshooting.
|
|
32
|
+
|
|
33
|
+
## Mental Model
|
|
34
|
+
|
|
35
|
+
Keep the engine small and generic. Put product memory behavior here:
|
|
36
|
+
|
|
37
|
+
- custom Arthaanu types in `memuron/representations.py`
|
|
38
|
+
- encoders that turn raw product input into meaning objects
|
|
39
|
+
- lifecycle steps that normalize, merge, dedupe, or retire objects
|
|
40
|
+
- projections that replay the event ledger into fast read models
|
|
41
|
+
- decoders/API routes that expose product-specific reads
|
|
42
|
+
|
|
43
|
+
The semantic event ledger is canonical. Projections are derived and rebuildable.
|
|
44
|
+
|
|
45
|
+
Memuron writes product semantics into that ledger, but keeps the engine model clean:
|
|
46
|
+
|
|
47
|
+
- memory create/update events stay product-specific: `memory.created`, `memory.updated`
|
|
48
|
+
- memory/link deletes use the canonical engine event type `delete`
|
|
49
|
+
- Memuron-specific delete meaning lives in metadata as `domain_event_type = memory.deleted` or `link.removed`
|
|
50
|
+
- request identity is audit metadata, not hidden global state
|
|
51
|
+
|
|
52
|
+
## Database
|
|
53
|
+
|
|
54
|
+
Production uses **PostgreSQL** (Railway project `talented-exploration`).
|
|
55
|
+
|
|
56
|
+
| Variable | Purpose |
|
|
57
|
+
|----------|---------|
|
|
58
|
+
| `ARTHA_DATABASE_URL` | PostgreSQL DSN for ledger + projections (preferred) |
|
|
59
|
+
| `ARTHA_DB_PATH` | Fallback SQLite path for local-only dev |
|
|
60
|
+
| `OPENROUTER_API_KEY` | OpenRouter key for Agno Guardian writes |
|
|
61
|
+
| `GUARDIAN_MODEL` | Guardian LLM model (default: `inception/mercury-2`) |
|
|
62
|
+
| `ARTHA_EMBEDDER` | `fastembed` (default) or `deterministic` |
|
|
63
|
+
| `ARTHA_EMBED_MODEL` | Nomic embed model (default: `nomic-ai/nomic-embed-text-v1.5-Q`) |
|
|
64
|
+
| `MEMURON_API_KEY` | Optional API key for `/memuron/*` and `/engine/*` routes |
|
|
65
|
+
|
|
66
|
+
Copy `.env.example` to `.env` and set `ARTHA_DATABASE_URL` to the Railway **public** Postgres URL for local development.
|
|
67
|
+
|
|
68
|
+
## Run
|
|
69
|
+
|
|
70
|
+
Use **two terminals**, both starting from the **memuron repo root** (`Documents/memuron`), not `frontend/`.
|
|
71
|
+
|
|
72
|
+
**Terminal 1 — backend**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd /Users/rakshithg/Documents/memuron
|
|
76
|
+
./scripts/dev-backend.sh
|
|
77
|
+
# or manually:
|
|
78
|
+
# source .env && uv run uvicorn memuron.application.api:create_app --factory --host 127.0.0.1 --port 8767
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Terminal 2 — frontend**
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd /Users/rakshithg/Documents/memuron
|
|
85
|
+
./scripts/dev-frontend.sh
|
|
86
|
+
# or: cd frontend && npm run dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Open http://localhost:3000 — the workbench proxies to `http://127.0.0.1:8767/engine`; Memories uses `http://127.0.0.1:8767/memuron`.
|
|
90
|
+
|
|
91
|
+
`frontend/.env.local` must use the **`/engine` suffix**:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ARTHA_ENGINE_URL=http://127.0.0.1:8767/engine
|
|
95
|
+
MEMURON_API_URL=http://127.0.0.1:8767/memuron
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If you see **502** on `/api/engine/*`, the backend is not running or the URL is wrong. Do not run `source .env` or `uv run` from inside `frontend/` — `.env` lives in the repo root and `uv` must use the memuron project.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv sync
|
|
102
|
+
cp .env.example .env # then edit ARTHA_DATABASE_URL
|
|
103
|
+
uv run pytest # fast local suite (SQLite, skips integration)
|
|
104
|
+
uv run pytest -m integration # Postgres job queue + live Guardian (needs .env)
|
|
105
|
+
uv run artha doctor
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## MCP and CLI
|
|
109
|
+
|
|
110
|
+
The stdio MCP server is intentionally restricted to one explicit Clerk user
|
|
111
|
+
and organization. Configure its identity before starting it:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
export MEMURON_MCP_ACTOR_ID=user_...
|
|
115
|
+
export MEMURON_MCP_TENANT_ID=org_...
|
|
116
|
+
export MEMURON_MCP_SCOPES=memory:read,memory:write,memory:delete,space:admin
|
|
117
|
+
uv run memuron-mcp
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The compact MCP surface centers on `memuron_help` and `memuron_query`, with
|
|
121
|
+
memory ingest/get/update/delete/job polling and the complete space lifecycle.
|
|
122
|
+
`memuron_document_source` resolves a document, chunk, image, or document
|
|
123
|
+
collection node to the original uploaded file metadata and a short-lived
|
|
124
|
+
download URL.
|
|
125
|
+
Lower-level graph, list, collection, and bulk operations remain available
|
|
126
|
+
through HTTP and the broader CLI without overwhelming an agent's tool picker.
|
|
127
|
+
`memuron_get` bounds content by default and supports field selection, while
|
|
128
|
+
`memuron_update` exposes flat `memory_id`, `content`, and `scope` arguments.
|
|
129
|
+
Space arguments accept UUIDs, slugs, `space.*` tokens, and `/spaces/space.*`
|
|
130
|
+
paths.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
uv run memuron --help
|
|
134
|
+
uv run memuron --tenant-id org_... query \
|
|
135
|
+
--cwd /spaces/space.personal \
|
|
136
|
+
--query 'semantic "deployment decisions" | head 5'
|
|
137
|
+
uv run memuron --tenant-id org_... space list
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
All direct-ID operations verify that the target belongs to the active
|
|
141
|
+
organization. API-key and local CLI callers must therefore provide a tenant.
|
|
142
|
+
|
|
143
|
+
For editor login/logout, use the Railway-hosted Streamable HTTP endpoint:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"mcpServers": {
|
|
148
|
+
"memuron": {
|
|
149
|
+
"url": "https://memuron-production.up.railway.app/mcp"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Cursor discovers Clerk through OAuth protected-resource metadata, opens the
|
|
156
|
+
Clerk consent flow, and stores its own access and refresh tokens. The OAuth
|
|
157
|
+
application only needs the standard `profile` scope. Memuron resolves the
|
|
158
|
+
signed-in user's Clerk organization membership server-side and maps it to the
|
|
159
|
+
configured Memuron tenant. Enable Dynamic client registration in the Clerk
|
|
160
|
+
Dashboard under OAuth applications so MCP clients can register with PKCE
|
|
161
|
+
automatically.
|
|
162
|
+
|
|
163
|
+
For users who belong to several mapped Clerk organizations, set
|
|
164
|
+
`MEMURON_MCP_DEFAULT_CLERK_ORG_ID` to select one explicitly when the OAuth token
|
|
165
|
+
does not carry an active organization.
|
|
166
|
+
|
|
167
|
+
### Document storage
|
|
168
|
+
|
|
169
|
+
Memuron keeps the searchable document graph in Postgres: normalized markdown,
|
|
170
|
+
chunks, embeddings, links, metadata, and append-only events. Original uploads
|
|
171
|
+
and extracted binary assets belong in S3-compatible object storage. This hybrid
|
|
172
|
+
boundary preserves transactional graph queries without turning Postgres into a
|
|
173
|
+
binary file store.
|
|
174
|
+
|
|
175
|
+
## API (prefix `/memuron`)
|
|
176
|
+
|
|
177
|
+
Core routes (see [`docs/README.md`](./docs/README.md) for full detail):
|
|
178
|
+
|
|
179
|
+
| Method | Path | Description |
|
|
180
|
+
|--------|------|-------------|
|
|
181
|
+
| POST | `/memories` | Create memory (async Guardian ingest, returns job id) |
|
|
182
|
+
| POST | `/nodes` | Rich node (text / image / document / collection) + optional auto-link |
|
|
183
|
+
| POST | `/documents/ingest` | Multipart file → collection + source + chunks + filtered images |
|
|
184
|
+
| POST | `/collections` | Collection node |
|
|
185
|
+
| POST | `/collections/{id}/placements` | Place member in collection |
|
|
186
|
+
| GET | `/collections/{id}/members` | List collection members |
|
|
187
|
+
| GET | `/graph/export` | Graph for UI |
|
|
188
|
+
| GET | `/memories` | List memories (`scope`, `limit`, `offset`) |
|
|
189
|
+
| POST | `/memories/search` | Semantic search (`query`, `k`, optional `scope`) |
|
|
190
|
+
| GET | `/memories/{id}` | Get one memory |
|
|
191
|
+
| POST | `/memories/batch` | Get many by id |
|
|
192
|
+
| PUT | `/memories/{id}` | Update content/scope (sync, no Guardian) |
|
|
193
|
+
| DELETE | `/memories/{id}` | Delete one memory |
|
|
194
|
+
| POST | `/memories/bulk-delete` | Delete by scope filter |
|
|
195
|
+
| POST | `/memories/unlink` | Remove link between two memories |
|
|
196
|
+
| GET | `/memories/count` | Count with optional filters |
|
|
197
|
+
| GET | `/jobs/{id}` | Poll async ingest job status |
|
|
198
|
+
| GET | `/spaces` | List org spaces |
|
|
199
|
+
|
|
200
|
+
## Auth and Audit Metadata
|
|
201
|
+
|
|
202
|
+
Memuron uses bring-your-own API-key auth at the product boundary. When `MEMURON_API_KEY` is set, protected routes accept either:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
Authorization: Bearer <key>
|
|
206
|
+
X-Memuron-Api-Key: <key>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Optional audit headers are copied into semantic event metadata:
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
X-Memuron-Actor-Id: agent_123
|
|
213
|
+
X-Memuron-Tenant-Id: workspace_456
|
|
214
|
+
X-Memuron-Scopes: memory:write,memory:delete
|
|
215
|
+
X-Request-Id: req_abc
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This applies to Memuron writes and to the mounted Artha Engine API under `/engine`. The engine remains auth-provider agnostic; Memuron decides how API keys, actors, tenants, and scopes map to product permissions.
|
|
219
|
+
|
|
220
|
+
The mounted engine exposes profile discovery at:
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
GET /engine/runtime/capabilities
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Example create → read flow:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Create (async)
|
|
230
|
+
curl -s -X POST http://127.0.0.1:8767/memuron/memories \
|
|
231
|
+
-H 'Content-Type: application/json' \
|
|
232
|
+
-d '{"content":"User enjoys rock climbing on weekends.","scope":["fitness.climbing"]}'
|
|
233
|
+
|
|
234
|
+
# Poll job until completed, then read
|
|
235
|
+
curl -s http://127.0.0.1:8767/memuron/memories/{memory_id}
|
|
236
|
+
|
|
237
|
+
# List and search
|
|
238
|
+
curl -s 'http://127.0.0.1:8767/memuron/memories?limit=20'
|
|
239
|
+
curl -s -X POST http://127.0.0.1:8767/memuron/memories/search \
|
|
240
|
+
-H 'Content-Type: application/json' \
|
|
241
|
+
-d '{"query":"rock climbing","k":5}'
|
|
242
|
+
```
|
memuron-0.1.1/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# memuron
|
|
2
|
+
|
|
3
|
+
An Arthaanu application built on Artha Engine.
|
|
4
|
+
|
|
5
|
+
**Documentation:** see [`docs/`](./docs/README.md) for architecture, rich nodes, document ingest, Guardian/spaces, frontend workbench, deployment, [Railway agent deploy guide](./docs/railway-deploy.md), and troubleshooting.
|
|
6
|
+
|
|
7
|
+
## Mental Model
|
|
8
|
+
|
|
9
|
+
Keep the engine small and generic. Put product memory behavior here:
|
|
10
|
+
|
|
11
|
+
- custom Arthaanu types in `memuron/representations.py`
|
|
12
|
+
- encoders that turn raw product input into meaning objects
|
|
13
|
+
- lifecycle steps that normalize, merge, dedupe, or retire objects
|
|
14
|
+
- projections that replay the event ledger into fast read models
|
|
15
|
+
- decoders/API routes that expose product-specific reads
|
|
16
|
+
|
|
17
|
+
The semantic event ledger is canonical. Projections are derived and rebuildable.
|
|
18
|
+
|
|
19
|
+
Memuron writes product semantics into that ledger, but keeps the engine model clean:
|
|
20
|
+
|
|
21
|
+
- memory create/update events stay product-specific: `memory.created`, `memory.updated`
|
|
22
|
+
- memory/link deletes use the canonical engine event type `delete`
|
|
23
|
+
- Memuron-specific delete meaning lives in metadata as `domain_event_type = memory.deleted` or `link.removed`
|
|
24
|
+
- request identity is audit metadata, not hidden global state
|
|
25
|
+
|
|
26
|
+
## Database
|
|
27
|
+
|
|
28
|
+
Production uses **PostgreSQL** (Railway project `talented-exploration`).
|
|
29
|
+
|
|
30
|
+
| Variable | Purpose |
|
|
31
|
+
|----------|---------|
|
|
32
|
+
| `ARTHA_DATABASE_URL` | PostgreSQL DSN for ledger + projections (preferred) |
|
|
33
|
+
| `ARTHA_DB_PATH` | Fallback SQLite path for local-only dev |
|
|
34
|
+
| `OPENROUTER_API_KEY` | OpenRouter key for Agno Guardian writes |
|
|
35
|
+
| `GUARDIAN_MODEL` | Guardian LLM model (default: `inception/mercury-2`) |
|
|
36
|
+
| `ARTHA_EMBEDDER` | `fastembed` (default) or `deterministic` |
|
|
37
|
+
| `ARTHA_EMBED_MODEL` | Nomic embed model (default: `nomic-ai/nomic-embed-text-v1.5-Q`) |
|
|
38
|
+
| `MEMURON_API_KEY` | Optional API key for `/memuron/*` and `/engine/*` routes |
|
|
39
|
+
|
|
40
|
+
Copy `.env.example` to `.env` and set `ARTHA_DATABASE_URL` to the Railway **public** Postgres URL for local development.
|
|
41
|
+
|
|
42
|
+
## Run
|
|
43
|
+
|
|
44
|
+
Use **two terminals**, both starting from the **memuron repo root** (`Documents/memuron`), not `frontend/`.
|
|
45
|
+
|
|
46
|
+
**Terminal 1 — backend**
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cd /Users/rakshithg/Documents/memuron
|
|
50
|
+
./scripts/dev-backend.sh
|
|
51
|
+
# or manually:
|
|
52
|
+
# source .env && uv run uvicorn memuron.application.api:create_app --factory --host 127.0.0.1 --port 8767
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Terminal 2 — frontend**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd /Users/rakshithg/Documents/memuron
|
|
59
|
+
./scripts/dev-frontend.sh
|
|
60
|
+
# or: cd frontend && npm run dev
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open http://localhost:3000 — the workbench proxies to `http://127.0.0.1:8767/engine`; Memories uses `http://127.0.0.1:8767/memuron`.
|
|
64
|
+
|
|
65
|
+
`frontend/.env.local` must use the **`/engine` suffix**:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
ARTHA_ENGINE_URL=http://127.0.0.1:8767/engine
|
|
69
|
+
MEMURON_API_URL=http://127.0.0.1:8767/memuron
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If you see **502** on `/api/engine/*`, the backend is not running or the URL is wrong. Do not run `source .env` or `uv run` from inside `frontend/` — `.env` lives in the repo root and `uv` must use the memuron project.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync
|
|
76
|
+
cp .env.example .env # then edit ARTHA_DATABASE_URL
|
|
77
|
+
uv run pytest # fast local suite (SQLite, skips integration)
|
|
78
|
+
uv run pytest -m integration # Postgres job queue + live Guardian (needs .env)
|
|
79
|
+
uv run artha doctor
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## MCP and CLI
|
|
83
|
+
|
|
84
|
+
The stdio MCP server is intentionally restricted to one explicit Clerk user
|
|
85
|
+
and organization. Configure its identity before starting it:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
export MEMURON_MCP_ACTOR_ID=user_...
|
|
89
|
+
export MEMURON_MCP_TENANT_ID=org_...
|
|
90
|
+
export MEMURON_MCP_SCOPES=memory:read,memory:write,memory:delete,space:admin
|
|
91
|
+
uv run memuron-mcp
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The compact MCP surface centers on `memuron_help` and `memuron_query`, with
|
|
95
|
+
memory ingest/get/update/delete/job polling and the complete space lifecycle.
|
|
96
|
+
`memuron_document_source` resolves a document, chunk, image, or document
|
|
97
|
+
collection node to the original uploaded file metadata and a short-lived
|
|
98
|
+
download URL.
|
|
99
|
+
Lower-level graph, list, collection, and bulk operations remain available
|
|
100
|
+
through HTTP and the broader CLI without overwhelming an agent's tool picker.
|
|
101
|
+
`memuron_get` bounds content by default and supports field selection, while
|
|
102
|
+
`memuron_update` exposes flat `memory_id`, `content`, and `scope` arguments.
|
|
103
|
+
Space arguments accept UUIDs, slugs, `space.*` tokens, and `/spaces/space.*`
|
|
104
|
+
paths.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv run memuron --help
|
|
108
|
+
uv run memuron --tenant-id org_... query \
|
|
109
|
+
--cwd /spaces/space.personal \
|
|
110
|
+
--query 'semantic "deployment decisions" | head 5'
|
|
111
|
+
uv run memuron --tenant-id org_... space list
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
All direct-ID operations verify that the target belongs to the active
|
|
115
|
+
organization. API-key and local CLI callers must therefore provide a tenant.
|
|
116
|
+
|
|
117
|
+
For editor login/logout, use the Railway-hosted Streamable HTTP endpoint:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"memuron": {
|
|
123
|
+
"url": "https://memuron-production.up.railway.app/mcp"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Cursor discovers Clerk through OAuth protected-resource metadata, opens the
|
|
130
|
+
Clerk consent flow, and stores its own access and refresh tokens. The OAuth
|
|
131
|
+
application only needs the standard `profile` scope. Memuron resolves the
|
|
132
|
+
signed-in user's Clerk organization membership server-side and maps it to the
|
|
133
|
+
configured Memuron tenant. Enable Dynamic client registration in the Clerk
|
|
134
|
+
Dashboard under OAuth applications so MCP clients can register with PKCE
|
|
135
|
+
automatically.
|
|
136
|
+
|
|
137
|
+
For users who belong to several mapped Clerk organizations, set
|
|
138
|
+
`MEMURON_MCP_DEFAULT_CLERK_ORG_ID` to select one explicitly when the OAuth token
|
|
139
|
+
does not carry an active organization.
|
|
140
|
+
|
|
141
|
+
### Document storage
|
|
142
|
+
|
|
143
|
+
Memuron keeps the searchable document graph in Postgres: normalized markdown,
|
|
144
|
+
chunks, embeddings, links, metadata, and append-only events. Original uploads
|
|
145
|
+
and extracted binary assets belong in S3-compatible object storage. This hybrid
|
|
146
|
+
boundary preserves transactional graph queries without turning Postgres into a
|
|
147
|
+
binary file store.
|
|
148
|
+
|
|
149
|
+
## API (prefix `/memuron`)
|
|
150
|
+
|
|
151
|
+
Core routes (see [`docs/README.md`](./docs/README.md) for full detail):
|
|
152
|
+
|
|
153
|
+
| Method | Path | Description |
|
|
154
|
+
|--------|------|-------------|
|
|
155
|
+
| POST | `/memories` | Create memory (async Guardian ingest, returns job id) |
|
|
156
|
+
| POST | `/nodes` | Rich node (text / image / document / collection) + optional auto-link |
|
|
157
|
+
| POST | `/documents/ingest` | Multipart file → collection + source + chunks + filtered images |
|
|
158
|
+
| POST | `/collections` | Collection node |
|
|
159
|
+
| POST | `/collections/{id}/placements` | Place member in collection |
|
|
160
|
+
| GET | `/collections/{id}/members` | List collection members |
|
|
161
|
+
| GET | `/graph/export` | Graph for UI |
|
|
162
|
+
| GET | `/memories` | List memories (`scope`, `limit`, `offset`) |
|
|
163
|
+
| POST | `/memories/search` | Semantic search (`query`, `k`, optional `scope`) |
|
|
164
|
+
| GET | `/memories/{id}` | Get one memory |
|
|
165
|
+
| POST | `/memories/batch` | Get many by id |
|
|
166
|
+
| PUT | `/memories/{id}` | Update content/scope (sync, no Guardian) |
|
|
167
|
+
| DELETE | `/memories/{id}` | Delete one memory |
|
|
168
|
+
| POST | `/memories/bulk-delete` | Delete by scope filter |
|
|
169
|
+
| POST | `/memories/unlink` | Remove link between two memories |
|
|
170
|
+
| GET | `/memories/count` | Count with optional filters |
|
|
171
|
+
| GET | `/jobs/{id}` | Poll async ingest job status |
|
|
172
|
+
| GET | `/spaces` | List org spaces |
|
|
173
|
+
|
|
174
|
+
## Auth and Audit Metadata
|
|
175
|
+
|
|
176
|
+
Memuron uses bring-your-own API-key auth at the product boundary. When `MEMURON_API_KEY` is set, protected routes accept either:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
Authorization: Bearer <key>
|
|
180
|
+
X-Memuron-Api-Key: <key>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Optional audit headers are copied into semantic event metadata:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
X-Memuron-Actor-Id: agent_123
|
|
187
|
+
X-Memuron-Tenant-Id: workspace_456
|
|
188
|
+
X-Memuron-Scopes: memory:write,memory:delete
|
|
189
|
+
X-Request-Id: req_abc
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This applies to Memuron writes and to the mounted Artha Engine API under `/engine`. The engine remains auth-provider agnostic; Memuron decides how API keys, actors, tenants, and scopes map to product permissions.
|
|
193
|
+
|
|
194
|
+
The mounted engine exposes profile discovery at:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
GET /engine/runtime/capabilities
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Example create → read flow:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Create (async)
|
|
204
|
+
curl -s -X POST http://127.0.0.1:8767/memuron/memories \
|
|
205
|
+
-H 'Content-Type: application/json' \
|
|
206
|
+
-d '{"content":"User enjoys rock climbing on weekends.","scope":["fitness.climbing"]}'
|
|
207
|
+
|
|
208
|
+
# Poll job until completed, then read
|
|
209
|
+
curl -s http://127.0.0.1:8767/memuron/memories/{memory_id}
|
|
210
|
+
|
|
211
|
+
# List and search
|
|
212
|
+
curl -s 'http://127.0.0.1:8767/memuron/memories?limit=20'
|
|
213
|
+
curl -s -X POST http://127.0.0.1:8767/memuron/memories/search \
|
|
214
|
+
-H 'Content-Type: application/json' \
|
|
215
|
+
-d '{"query":"rock climbing","k":5}'
|
|
216
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "memuron"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Arthaanu application built on Artha Engine."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"artha-engine[auth,mcp,app]>=0.1.3",
|
|
9
|
+
"agno>=2.3.11",
|
|
10
|
+
"fastapi>=0.115.0",
|
|
11
|
+
"uvicorn[standard]>=0.34.0",
|
|
12
|
+
"psycopg[binary]>=3.2.0",
|
|
13
|
+
"psycopg-pool>=3.3.1",
|
|
14
|
+
"pyjwt>=2.10.0",
|
|
15
|
+
"email-validator>=2.2.0",
|
|
16
|
+
"pypdf>=6.0.0",
|
|
17
|
+
"python-multipart>=0.0.20",
|
|
18
|
+
"requests>=2.34.0",
|
|
19
|
+
"pymupdf>=1.23",
|
|
20
|
+
"python-docx>=1.1.0",
|
|
21
|
+
"openpyxl>=3.1.5",
|
|
22
|
+
"python-pptx>=1.0.2",
|
|
23
|
+
"xlrd>=2.0.2",
|
|
24
|
+
"olefile>=0.47",
|
|
25
|
+
"svix>=1.40.0",
|
|
26
|
+
"boto3>=1.35.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
memuron = "memuron.application.cli:main"
|
|
31
|
+
memuron-mcp = "memuron.application.mcp:main"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.9.7,<0.10.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = ["pytest>=9.0.3", "pytest-asyncio>=0.25.0", "httpx>=0.28.0"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
asyncio_mode = "auto"
|
|
42
|
+
addopts = "-m 'not integration'"
|
|
43
|
+
markers = [
|
|
44
|
+
"integration: hits Postgres and/or live OpenRouter (run with: pytest -m integration)",
|
|
45
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Import all action modules to register handlers on the shared registry."""
|
|
2
|
+
|
|
3
|
+
from memuron.actions.registry import actions
|
|
4
|
+
from memuron.actions import context as _context # noqa: F401
|
|
5
|
+
from memuron.actions import memory as _memory # noqa: F401
|
|
6
|
+
from memuron.actions import memory_write as _memory_write # noqa: F401
|
|
7
|
+
from memuron.actions import nodes as _nodes # noqa: F401
|
|
8
|
+
from memuron.actions import runtime as _runtime # noqa: F401
|
|
9
|
+
from memuron.actions import spaces_documents as _spaces_documents # noqa: F401
|
|
10
|
+
from memuron.actions import sync as _sync # noqa: F401
|
|
11
|
+
|
|
12
|
+
__all__ = ["actions"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Context assembly actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from artha_engine import ActionContext, ArthaEngine, HttpExposure
|
|
6
|
+
|
|
7
|
+
from memuron.actions.helpers import merge_tenant_scope, require_user_org
|
|
8
|
+
from memuron.actions.registry import actions
|
|
9
|
+
from memuron.context import assemble_context
|
|
10
|
+
from memuron.domain.schemas import AssembleContextRequest, AssembleContextResponse
|
|
11
|
+
from memuron.persistence.identity_store import IdentityStore
|
|
12
|
+
from memuron.spaces.service import resolve_space_reference
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@actions.action(
|
|
16
|
+
name="context.assemble",
|
|
17
|
+
description=(
|
|
18
|
+
"Assemble graph-native prompt context from memory search results, "
|
|
19
|
+
"collection breadcrumbs, and semantic links with citation metadata."
|
|
20
|
+
),
|
|
21
|
+
kind="read",
|
|
22
|
+
scopes=["memory:read"],
|
|
23
|
+
http=HttpExposure("POST", "/memuron/context/assemble"),
|
|
24
|
+
mcp=False,
|
|
25
|
+
cli=False,
|
|
26
|
+
inject={"identity": "identity"},
|
|
27
|
+
tags=["context"],
|
|
28
|
+
)
|
|
29
|
+
def context_assemble(
|
|
30
|
+
input: AssembleContextRequest,
|
|
31
|
+
engine: ArthaEngine,
|
|
32
|
+
context: ActionContext,
|
|
33
|
+
identity: IdentityStore,
|
|
34
|
+
) -> AssembleContextResponse:
|
|
35
|
+
_user_id, org_id = require_user_org(context.auth)
|
|
36
|
+
scoped = merge_tenant_scope(input.scope, context)
|
|
37
|
+
preferred_space_token = None
|
|
38
|
+
if input.space_ref:
|
|
39
|
+
space = resolve_space_reference(
|
|
40
|
+
identity,
|
|
41
|
+
org_id=org_id,
|
|
42
|
+
space_ref=input.space_ref,
|
|
43
|
+
)
|
|
44
|
+
if space is None:
|
|
45
|
+
raise KeyError("Space not found")
|
|
46
|
+
preferred_space_token = str(space["token"])
|
|
47
|
+
scoped = [token for token in scoped if not token.startswith("space.")]
|
|
48
|
+
if preferred_space_token not in scoped:
|
|
49
|
+
scoped.append(preferred_space_token)
|
|
50
|
+
|
|
51
|
+
payload = assemble_context(
|
|
52
|
+
engine,
|
|
53
|
+
query=input.query,
|
|
54
|
+
k=input.k,
|
|
55
|
+
scope=scoped or None,
|
|
56
|
+
org_id=org_id,
|
|
57
|
+
preferred_space_token=preferred_space_token,
|
|
58
|
+
token_budget=input.token_budget,
|
|
59
|
+
char_budget=input.char_budget,
|
|
60
|
+
include_links=input.include_links,
|
|
61
|
+
include_breadcrumbs=input.include_breadcrumbs,
|
|
62
|
+
)
|
|
63
|
+
return AssembleContextResponse.model_validate(payload)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Shared helpers for Memuron Artha actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from artha_engine.app.context import ActionContext
|
|
9
|
+
from artha_engine.runtime.auth import AuthContext
|
|
10
|
+
|
|
11
|
+
from memuron.domain.limits import MAX_SCOPE_ITEMS, MAX_SCOPE_TOKEN_LEN
|
|
12
|
+
from memuron.security.tenant import merge_org_scope
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def require_user_org(auth: AuthContext) -> tuple[str, str]:
|
|
16
|
+
org_id = auth.tenant_id
|
|
17
|
+
if not org_id:
|
|
18
|
+
raise ValueError("Organization context required")
|
|
19
|
+
return str(auth.actor_id), str(org_id)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def require_memory_in_tenant(
|
|
23
|
+
engine: Any,
|
|
24
|
+
memory_id: str,
|
|
25
|
+
context: ActionContext,
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
"""Return a memory only when it belongs to the caller's active organization."""
|
|
28
|
+
from memuron.memory.recipes import get_memory
|
|
29
|
+
from memuron.security.tenant import org_scope_token
|
|
30
|
+
|
|
31
|
+
_user_id, org_id = require_user_org(context.auth)
|
|
32
|
+
memory = get_memory(engine, memory_id)
|
|
33
|
+
if org_scope_token(org_id) not in set(memory.get("scope") or []):
|
|
34
|
+
raise KeyError(f"Memory not found: {memory_id}")
|
|
35
|
+
return memory
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def event_metadata(
|
|
39
|
+
context: ActionContext,
|
|
40
|
+
*,
|
|
41
|
+
space_context: dict[str, str] | None = None,
|
|
42
|
+
) -> dict[str, object]:
|
|
43
|
+
metadata = context.auth.event_metadata()
|
|
44
|
+
if context.request_id:
|
|
45
|
+
metadata["request_id"] = context.request_id
|
|
46
|
+
if space_context:
|
|
47
|
+
metadata["space_context"] = space_context
|
|
48
|
+
return metadata
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def merge_tenant_scope(scope: list[str] | None, context: ActionContext) -> list[str]:
|
|
52
|
+
return merge_org_scope(scope, context.auth.tenant_id)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def tenant_scope_query(context: ActionContext, scope: str | None) -> str | None:
|
|
56
|
+
from memuron.security.tenant import tenant_scope_query as _tenant_scope_query
|
|
57
|
+
|
|
58
|
+
return _tenant_scope_query(context.auth.tenant_id, scope)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_scope_form(value: str | None) -> list[str] | None:
|
|
62
|
+
if value is None or not value.strip():
|
|
63
|
+
return None
|
|
64
|
+
raw = value.strip()
|
|
65
|
+
if raw.startswith("["):
|
|
66
|
+
decoded = json.loads(raw)
|
|
67
|
+
if not isinstance(decoded, list) or not all(isinstance(item, str) for item in decoded):
|
|
68
|
+
raise ValueError("scope must be a list of strings")
|
|
69
|
+
scope = decoded
|
|
70
|
+
else:
|
|
71
|
+
scope = [part.strip() for part in raw.split(",") if part.strip()]
|
|
72
|
+
if len(scope) > MAX_SCOPE_ITEMS:
|
|
73
|
+
raise ValueError(f"scope can contain at most {MAX_SCOPE_ITEMS} tokens")
|
|
74
|
+
for token in scope:
|
|
75
|
+
if len(token) > MAX_SCOPE_TOKEN_LEN:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Each scope token must be at most {MAX_SCOPE_TOKEN_LEN} characters"
|
|
78
|
+
)
|
|
79
|
+
return scope
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_metadata_form(value: str | None) -> dict[str, object]:
|
|
83
|
+
if value is None or not value.strip():
|
|
84
|
+
return {}
|
|
85
|
+
decoded = json.loads(value)
|
|
86
|
+
if not isinstance(decoded, dict):
|
|
87
|
+
raise ValueError("metadata must be a JSON object")
|
|
88
|
+
return decoded
|