memuron 0.1.1__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 (74) hide show
  1. memuron/__init__.py +3 -0
  2. memuron/actions/__init__.py +12 -0
  3. memuron/actions/context.py +63 -0
  4. memuron/actions/helpers.py +88 -0
  5. memuron/actions/memory.py +340 -0
  6. memuron/actions/memory_write.py +290 -0
  7. memuron/actions/nodes.py +340 -0
  8. memuron/actions/registry.py +5 -0
  9. memuron/actions/runtime.py +37 -0
  10. memuron/actions/spaces_documents.py +720 -0
  11. memuron/actions/sync.py +155 -0
  12. memuron/application/__init__.py +1 -0
  13. memuron/application/api.py +206 -0
  14. memuron/application/app.py +103 -0
  15. memuron/application/capabilities.py +82 -0
  16. memuron/application/cli.py +35 -0
  17. memuron/application/config.py +176 -0
  18. memuron/application/mcp.py +44 -0
  19. memuron/application/mcp_oauth.py +290 -0
  20. memuron/application/registry.py +52 -0
  21. memuron/context.py +532 -0
  22. memuron/documents/__init__.py +1 -0
  23. memuron/documents/link_guardian.py +192 -0
  24. memuron/documents/linking.py +292 -0
  25. memuron/documents/parser.py +1152 -0
  26. memuron/documents/storage.py +151 -0
  27. memuron/documents/url_ingest.py +375 -0
  28. memuron/domain/__init__.py +1 -0
  29. memuron/domain/decoders.py +1 -0
  30. memuron/domain/encoders.py +185 -0
  31. memuron/domain/lifecycles.py +8 -0
  32. memuron/domain/limits.py +6 -0
  33. memuron/domain/representations.py +56 -0
  34. memuron/domain/schemas.py +581 -0
  35. memuron/domain/scope_filter.py +104 -0
  36. memuron/graphfs/__init__.py +1 -0
  37. memuron/graphfs/manual.py +635 -0
  38. memuron/graphfs/projection.py +578 -0
  39. memuron/graphfs/query.py +1782 -0
  40. memuron/graphfs/read_model.py +574 -0
  41. memuron/ingest/__init__.py +1 -0
  42. memuron/ingest/guardian.py +213 -0
  43. memuron/ingest/jobs.py +424 -0
  44. memuron/ingest/prompts.py +147 -0
  45. memuron/memory/__init__.py +1 -0
  46. memuron/memory/engine.py +35 -0
  47. memuron/memory/projections.py +452 -0
  48. memuron/memory/recipes.py +3247 -0
  49. memuron/persistence/__init__.py +1 -0
  50. memuron/persistence/db_pool.py +57 -0
  51. memuron/persistence/identity_store.py +918 -0
  52. memuron/persistence/store_helpers.py +16 -0
  53. memuron/search/__init__.py +1 -0
  54. memuron/search/fulltext.py +110 -0
  55. memuron/search/hybrid.py +284 -0
  56. memuron/search/pgvector.py +252 -0
  57. memuron/security/__init__.py +1 -0
  58. memuron/security/auth.py +143 -0
  59. memuron/security/auth_provider.py +119 -0
  60. memuron/security/authorization.py +53 -0
  61. memuron/security/clerk_scopes.py +94 -0
  62. memuron/security/clerk_webhooks.py +61 -0
  63. memuron/security/jwt_tokens.py +53 -0
  64. memuron/security/passwords.py +38 -0
  65. memuron/security/tenant.py +58 -0
  66. memuron/spaces/__init__.py +1 -0
  67. memuron/spaces/model.py +35 -0
  68. memuron/spaces/service.py +155 -0
  69. memuron/sync/__init__.py +25 -0
  70. memuron/sync/folder.py +828 -0
  71. memuron-0.1.1.dist-info/METADATA +242 -0
  72. memuron-0.1.1.dist-info/RECORD +74 -0
  73. memuron-0.1.1.dist-info/WHEEL +4 -0
  74. memuron-0.1.1.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,155 @@
1
+ """Folder sync actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from artha_engine import ActionContext, ArthaEngine
9
+ from pydantic import BaseModel, Field
10
+
11
+ from memuron.actions.helpers import event_metadata, require_user_org
12
+ from memuron.actions.registry import actions
13
+ from memuron.documents.parser import MAX_DOCUMENT_UPLOAD_BYTES
14
+ from memuron.persistence.identity_store import IdentityStore
15
+ from memuron.security.tenant import merge_org_scope
16
+ from memuron.spaces.service import resolve_space_reference
17
+ from memuron.sync.folder import (
18
+ init_manifest,
19
+ load_manifest,
20
+ resolve_manifest_path,
21
+ run_engine_folder_sync,
22
+ save_manifest,
23
+ )
24
+
25
+
26
+ class SyncInitInput(BaseModel):
27
+ path: str = Field(..., description="Local folder root to sync from.")
28
+ space_ref: str = Field(..., description="Target space UUID, slug, token, or /spaces path.")
29
+ include: list[str] = Field(default_factory=list, description="Include pattern; repeatable.")
30
+ exclude: list[str] = Field(default_factory=list, description="Exclude pattern; repeatable.")
31
+ manifest_path: str | None = Field(None, description="Manifest path override.")
32
+ max_file_bytes: int = Field(
33
+ MAX_DOCUMENT_UPLOAD_BYTES,
34
+ ge=1,
35
+ description="Skip files larger than this many bytes.",
36
+ )
37
+ overwrite: bool = False
38
+
39
+
40
+ class SyncRunInput(BaseModel):
41
+ path: str = Field(..., description="Folder root or manifest JSON path.")
42
+ manifest_path: str | None = Field(None, description="Manifest path override.")
43
+ dry_run: bool = False
44
+
45
+
46
+ def _space_payload(identity: IdentityStore, *, org_id: str, space_ref: str) -> dict[str, Any]:
47
+ space = resolve_space_reference(identity, org_id=org_id, space_ref=space_ref)
48
+ if space is None:
49
+ raise KeyError("Space not found")
50
+ return {
51
+ "id": str(space["id"]),
52
+ "slug": str(space["slug"]),
53
+ "name": str(space["name"]),
54
+ "token": str(space["token"]),
55
+ }
56
+
57
+
58
+ @actions.action(
59
+ name="sync.init",
60
+ description="Initialize a one-way folder sync manifest for a local directory.",
61
+ kind="write",
62
+ scopes=["memory:write"],
63
+ cli="sync init",
64
+ http=False,
65
+ mcp=False,
66
+ inject={"identity": "identity"},
67
+ tags=["sync"],
68
+ )
69
+ def sync_init(
70
+ input: SyncInitInput,
71
+ context: ActionContext,
72
+ identity: IdentityStore,
73
+ ) -> dict[str, Any]:
74
+ _user_id, org_id = require_user_org(context.auth)
75
+ space = _space_payload(identity, org_id=org_id, space_ref=input.space_ref)
76
+ manifest, path = init_manifest(
77
+ input.path,
78
+ space_ref=input.space_ref,
79
+ include=input.include,
80
+ exclude=input.exclude,
81
+ max_file_bytes=input.max_file_bytes,
82
+ manifest_path=input.manifest_path,
83
+ overwrite=input.overwrite,
84
+ )
85
+ manifest.space = space
86
+ save_manifest(manifest, path)
87
+ return {
88
+ "status": "success",
89
+ "manifest_path": str(path),
90
+ "manifest": manifest.to_dict(),
91
+ }
92
+
93
+
94
+ @actions.action(
95
+ name="sync.add",
96
+ description="Add or replace a one-way folder sync manifest for a local directory.",
97
+ kind="write",
98
+ scopes=["memory:write"],
99
+ cli="sync add",
100
+ http=False,
101
+ mcp=False,
102
+ inject={"identity": "identity"},
103
+ tags=["sync"],
104
+ )
105
+ def sync_add(
106
+ input: SyncInitInput,
107
+ context: ActionContext,
108
+ identity: IdentityStore,
109
+ ) -> dict[str, Any]:
110
+ return sync_init(
111
+ SyncInitInput(**{**input.model_dump(), "overwrite": True}),
112
+ context,
113
+ identity,
114
+ )
115
+
116
+
117
+ @actions.action(
118
+ name="sync.run",
119
+ description="Run a one-way folder sync from a manifest, reporting local deletes without deleting remote graph nodes.",
120
+ kind="write",
121
+ scopes=["memory:write"],
122
+ cli="sync run",
123
+ http=False,
124
+ mcp=False,
125
+ inject={"identity": "identity"},
126
+ tags=["sync"],
127
+ )
128
+ def sync_run(
129
+ input: SyncRunInput,
130
+ engine: ArthaEngine,
131
+ context: ActionContext,
132
+ identity: IdentityStore,
133
+ ) -> dict[str, Any]:
134
+ _user_id, org_id = require_user_org(context.auth)
135
+ manifest_path = resolve_manifest_path(input.path, input.manifest_path)
136
+ if not manifest_path.exists():
137
+ raise KeyError(f"Folder sync manifest not found: {manifest_path}")
138
+ manifest = load_manifest(manifest_path)
139
+ space = _space_payload(
140
+ identity,
141
+ org_id=org_id,
142
+ space_ref=str(manifest.space.get("token") or manifest.space_ref),
143
+ )
144
+ manifest.space = space
145
+ scope = merge_org_scope([space["token"]], org_id)
146
+ result = run_engine_folder_sync(
147
+ engine,
148
+ manifest,
149
+ scope=scope,
150
+ event_metadata=event_metadata(context),
151
+ manifest_path=manifest_path,
152
+ dry_run=input.dry_run,
153
+ )
154
+ result["manifest_path"] = str(Path(manifest_path))
155
+ return result
@@ -0,0 +1 @@
1
+ """Application entrypoints, configuration, and runtime assembly."""
@@ -0,0 +1,206 @@
1
+ """Memuron HTTP application factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import logging
8
+ import os
9
+ from pathlib import Path
10
+
11
+ from artha_engine.store import is_postgres_dsn
12
+ from fastapi import HTTPException, Request
13
+
14
+ from memuron.application.app import configure_clerk_env, create_application
15
+ from memuron.security.clerk_webhooks import router as clerk_webhook_router
16
+ from memuron.application.config import resolve_database_url, settings
17
+ from memuron.persistence.db_pool import close_postgres_pool
18
+ from memuron.ingest.jobs import start_ingest_workers, stop_ingest_workers
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ChatGPTMcpHeaderCompat:
24
+ """Normalize ChatGPT connector JSON-RPC preflight posts for MCP."""
25
+
26
+ def __init__(self, app, auth_challenge: str):
27
+ self.app = app
28
+ self.auth_challenge = auth_challenge
29
+
30
+ async def __call__(self, scope, receive, send):
31
+ if scope.get("type") != "http":
32
+ await self.app(scope, receive, send)
33
+ return
34
+ if scope.get("method") == "POST" and scope.get("path") == "/mcp":
35
+ if self._has_empty_body(scope):
36
+ await self._send_auth_challenge(send)
37
+ return
38
+ headers = []
39
+ saw_accept = False
40
+ for name, value in scope.get("headers", []):
41
+ normalized_name = name.lower()
42
+ if (
43
+ normalized_name == b"content-type"
44
+ and value.split(b";", 1)[0].strip().lower()
45
+ == b"application/octet-stream"
46
+ ):
47
+ headers.append((name, b"application/json"))
48
+ elif normalized_name == b"accept":
49
+ saw_accept = True
50
+ media_types = [
51
+ item.split(b";", 1)[0].strip().lower()
52
+ for item in value.split(b",")
53
+ ]
54
+ if not {
55
+ b"application/json",
56
+ b"text/event-stream",
57
+ }.issubset(set(media_types)):
58
+ headers.append(
59
+ (name, b"application/json, text/event-stream")
60
+ )
61
+ else:
62
+ headers.append((name, value))
63
+ else:
64
+ headers.append((name, value))
65
+ if not saw_accept:
66
+ headers.append((b"accept", b"application/json, text/event-stream"))
67
+ scope = {**scope, "headers": headers}
68
+ await self.app(scope, receive, send)
69
+
70
+ def _has_empty_body(self, scope) -> bool:
71
+ for name, value in scope.get("headers", []):
72
+ if name.lower() == b"content-length":
73
+ return value.strip() == b"0"
74
+ return False
75
+
76
+ async def _send_auth_challenge(self, send) -> None:
77
+ content = b'{"error":"authorization_required"}'
78
+ await send(
79
+ {
80
+ "type": "http.response.start",
81
+ "status": 401,
82
+ "headers": [
83
+ (b"content-type", b"application/json"),
84
+ (b"content-length", str(len(content)).encode("ascii")),
85
+ (b"www-authenticate", self.auth_challenge.encode("utf-8")),
86
+ ],
87
+ }
88
+ )
89
+ await send({"type": "http.response.body", "body": content})
90
+
91
+
92
+ def create_app(db: str | Path | None = None):
93
+ configure_clerk_env()
94
+ application = create_application(db)
95
+ target = resolve_database_url() if db is None else str(db)
96
+ engine = application.engine
97
+ guardian = application.services["guardian"]
98
+
99
+ http_app = application.create_http_app(expose_engine=False)
100
+
101
+ from artha_engine.runtime.api.app import create_app as create_engine_app
102
+
103
+ from memuron.application.capabilities import memuron_engine_profile_capability
104
+
105
+ async def engine_auth(request: Request):
106
+ from artha_engine.app.auth import AuthenticationError
107
+
108
+ try:
109
+ auth = await application.auth_provider.authenticate_http(request)
110
+ except AuthenticationError as exc:
111
+ raise HTTPException(status_code=401, detail=str(exc)) from exc
112
+ required = {"artha:admin"}
113
+ if "*" not in auth.scopes and not required.issubset(set(auth.scopes)):
114
+ raise HTTPException(status_code=403, detail="Engine admin scope required")
115
+ return auth
116
+
117
+ profile = memuron_engine_profile_capability(application.manifest())
118
+ http_app.mount(
119
+ "/engine",
120
+ create_engine_app(
121
+ engine,
122
+ auth_dependency=engine_auth,
123
+ profile_capabilities=[profile],
124
+ ),
125
+ )
126
+
127
+ remote_mcp = None
128
+ public_domain = os.environ.get("RAILWAY_PUBLIC_DOMAIN", "").strip()
129
+ public_mcp_url = os.environ.get("MEMURON_MCP_PUBLIC_URL", "").strip()
130
+ if not public_mcp_url and public_domain:
131
+ public_mcp_url = f"https://{public_domain}/mcp"
132
+ if public_mcp_url:
133
+ from memuron.application.mcp_oauth import (
134
+ create_remote_mcp_server,
135
+ mcp_auth_challenge,
136
+ protected_resource_metadata,
137
+ protected_resource_metadata_path,
138
+ )
139
+
140
+ remote_mcp = create_remote_mcp_server(application, public_mcp_url)
141
+
142
+ @http_app.get(
143
+ protected_resource_metadata_path(public_mcp_url),
144
+ include_in_schema=False,
145
+ )
146
+ def mcp_protected_resource_metadata():
147
+ return protected_resource_metadata(public_mcp_url)
148
+
149
+ http_app.mount(
150
+ "/",
151
+ ChatGPTMcpHeaderCompat(
152
+ remote_mcp.streamable_http_app(),
153
+ mcp_auth_challenge(public_mcp_url),
154
+ ),
155
+ )
156
+
157
+ def _warmup_embedder() -> None:
158
+ try:
159
+ engine.embedder.embed_queries(["memuron warmup"])
160
+ logger.info("Embedder warmed up (%s)", settings.embed_model)
161
+ except Exception as exc:
162
+ logger.warning("Embedder warmup failed: %s", exc)
163
+
164
+ def _init_projections() -> None:
165
+ from memuron.memory.recipes import ensure_memory_projections
166
+
167
+ ensure_memory_projections(engine)
168
+ engine.refresh_projection("memuron_fs")
169
+
170
+ @contextlib.asynccontextmanager
171
+ async def lifespan(_app):
172
+ async with contextlib.AsyncExitStack() as stack:
173
+ if remote_mcp is not None:
174
+ await stack.enter_async_context(remote_mcp.session_manager.run())
175
+ await asyncio.to_thread(_warmup_embedder)
176
+ await asyncio.to_thread(_init_projections)
177
+ http_app.state.identity_store = application.services["identity"]
178
+ await start_ingest_workers(http_app, engine, guardian)
179
+ try:
180
+ yield
181
+ finally:
182
+ await stop_ingest_workers(http_app)
183
+ identity_store = getattr(http_app.state, "identity_store", None)
184
+ if identity_store is not None:
185
+ identity_store.close()
186
+ store_target = getattr(engine.store, "dsn", None) or getattr(
187
+ engine.store, "database_target", target
188
+ )
189
+ if is_postgres_dsn(str(store_target)):
190
+ close_postgres_pool(engine.store)
191
+
192
+ http_app.router.lifespan_context = lifespan
193
+
194
+ http_app.state.artha_application = application
195
+ http_app.state.engine = engine
196
+ http_app.state.guardian = guardian
197
+ http_app.state.identity_store = application.services["identity"]
198
+ http_app.state.ingest_job_store = application.services.get("jobs")
199
+ http_app.include_router(clerk_webhook_router)
200
+ return http_app
201
+
202
+
203
+ def create_engine(db: str | Path | None = None):
204
+ from memuron.application.app import create_engine as _create_engine
205
+
206
+ return _create_engine(db)
@@ -0,0 +1,103 @@
1
+ """Memuron ArthaApplication factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from artha_engine import ArthaApplication, create_store, resolve_database_target
10
+ from artha_engine.app.managed_api_keys import register_api_key_actions
11
+ from artha_engine.store.api_key_store import ApiKeyStore
12
+ from artha_engine.store import is_postgres_dsn
13
+
14
+ from memuron.actions import actions
15
+ from memuron.security.auth_provider import create_memuron_auth_provider
16
+ from memuron.security.authorization import MemuronAuthorizationPolicy
17
+ from memuron.application.config import resolve_database_url, settings
18
+ from memuron.persistence.db_pool import install_postgres_pool
19
+ from memuron.persistence.db_pool import close_postgres_pool
20
+ from memuron.memory.engine import MemuronArthaEngine
21
+ from memuron.ingest.guardian import create_guardian
22
+ from memuron.persistence.identity_store import IdentityStore
23
+ from memuron.ingest.jobs import IngestJobStore
24
+ from memuron.application.registry import build_registry
25
+
26
+
27
+ def _build_embedder():
28
+ from artha_engine import DeterministicTextEmbedder, FastEmbedTextEmbedder
29
+
30
+ if settings.embedder == "deterministic":
31
+ return DeterministicTextEmbedder(dimensions=8)
32
+ try:
33
+ return FastEmbedTextEmbedder(model_name=settings.embed_model)
34
+ except Exception:
35
+ return DeterministicTextEmbedder(dimensions=8)
36
+
37
+
38
+ def create_engine(db: str | Path | None = None) -> MemuronArthaEngine:
39
+ registry = build_registry()
40
+ target = resolve_database_target(db or resolve_database_url())
41
+ store = create_store(target, type_registry=registry.arthaanu_types)
42
+ if is_postgres_dsn(target):
43
+ install_postgres_pool(store)
44
+ return MemuronArthaEngine(
45
+ store=store,
46
+ registry=registry,
47
+ embedder=_build_embedder(),
48
+ )
49
+
50
+
51
+ def create_application(db: str | Path | None = None) -> ArthaApplication:
52
+ target = resolve_database_target(db or resolve_database_url())
53
+ engine = create_engine(db)
54
+ services: dict[str, Any] = {
55
+ "guardian": create_guardian(),
56
+ "identity": IdentityStore.from_target(target),
57
+ "jobs": IngestJobStore(target) if is_postgres_dsn(target) else None,
58
+ }
59
+ application = ArthaApplication(
60
+ name="memuron",
61
+ version="0.1.1",
62
+ description="Product memory layer on Artha Engine",
63
+ engine=engine,
64
+ actions=actions,
65
+ auth_provider=create_memuron_auth_provider(),
66
+ authorization_policy=MemuronAuthorizationPolicy(),
67
+ services=services,
68
+ )
69
+ api_key_store = ApiKeyStore.from_target(str(target))
70
+ registered = {item.name for item in actions.list()}
71
+ if "api_key.create" not in registered:
72
+ register_api_key_actions(
73
+ actions,
74
+ api_key_store,
75
+ path_prefix="/memuron/api-keys",
76
+ )
77
+ application.services["api_keys"] = api_key_store
78
+ application.auth_provider = create_memuron_auth_provider(api_key_store=api_key_store)
79
+ return application
80
+
81
+
82
+ def close_application(application: ArthaApplication) -> None:
83
+ """Release resources owned by a short-lived CLI or MCP application."""
84
+ identity = application.services.get("identity")
85
+ close = getattr(identity, "close", None)
86
+ if callable(close):
87
+ close()
88
+ close_postgres_pool(application.engine.store)
89
+
90
+
91
+ def configure_clerk_env() -> None:
92
+ """Apply sensible Clerk defaults from documented env vars."""
93
+ if not os.environ.get("CLERK_ISSUER") and os.environ.get("NEXT_PUBLIC_CLERK_FRONTEND_API"):
94
+ os.environ["CLERK_ISSUER"] = os.environ["NEXT_PUBLIC_CLERK_FRONTEND_API"].rstrip("/")
95
+ if not os.environ.get("CLERK_ISSUER"):
96
+ os.environ.setdefault(
97
+ "CLERK_ISSUER",
98
+ "https://credible-perch-55.clerk.accounts.dev",
99
+ )
100
+ issuer = os.environ["CLERK_ISSUER"].rstrip("/")
101
+ os.environ.setdefault("CLERK_JWKS_URL", f"{issuer}/.well-known/jwks.json")
102
+ os.environ.setdefault("CLERK_AUTHORIZED_PARTIES", "http://localhost:3000")
103
+ os.environ.setdefault("ARTHA_AUTH_PROVIDER", "clerk_api_key")
@@ -0,0 +1,82 @@
1
+ """Memuron profile manifest built from registered actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ MEMURON_PROFILE_NAME = "memuron"
8
+
9
+ _BASE_MANIFEST: dict[str, Any] = {
10
+ "profile": MEMURON_PROFILE_NAME,
11
+ "description": "Product memory layer: Guardian ingest, rich nodes, collections, links, semantic search.",
12
+ "api_prefix": "/memuron",
13
+ "arthaanu_types": ["memory", "memory_link", "memory_placement"],
14
+ "encoders": ["memory", "memory_link", "memory_placement"],
15
+ "projections": ["memuron_memories", "memuron_links", "memuron_placements", "memuron_fs"],
16
+ "semantic_events": [
17
+ "memory.created",
18
+ "memory.updated",
19
+ "collection.created",
20
+ "link.created",
21
+ "placement.created",
22
+ "delete",
23
+ ],
24
+ "event_metadata": [
25
+ "domain_event_type",
26
+ "actor_id",
27
+ "tenant_id",
28
+ "auth_scopes",
29
+ "request_id",
30
+ ],
31
+ "agent_tools": [
32
+ {
33
+ "name": "list_spaces",
34
+ "description": "List registered spaces with name, token, description, and default flag.",
35
+ "route": "GET /memuron/spaces",
36
+ },
37
+ {
38
+ "name": "read_graph_filesystem_manual",
39
+ "description": "Read this before issuing filesystem commands.",
40
+ "route": "GET /memuron/spaces/query/manual?topic=overview",
41
+ },
42
+ {
43
+ "name": "query_graph_filesystem",
44
+ "description": "Execute one command or a | pipeline.",
45
+ "route": "POST /memuron/spaces/query",
46
+ },
47
+ ],
48
+ }
49
+
50
+
51
+ def memuron_profile_manifest(application_manifest: dict[str, Any] | None = None) -> dict[str, Any]:
52
+ manifest = dict(_BASE_MANIFEST)
53
+ if application_manifest is not None:
54
+ manifest["actions"] = application_manifest.get("actions", [])
55
+ return manifest
56
+
57
+
58
+ def memuron_engine_profile_capability(
59
+ application_manifest: dict[str, Any] | None = None,
60
+ ) -> dict[str, Any]:
61
+ manifest = memuron_profile_manifest(application_manifest)
62
+ return {
63
+ "name": manifest["profile"],
64
+ "description": manifest.get("description"),
65
+ "projections": manifest.get("projections", []),
66
+ "arthaanu_types": manifest.get("arthaanu_types", []),
67
+ "actions": manifest.get("actions", []),
68
+ "routes": manifest.get("agent_tools", []),
69
+ "metadata": {
70
+ key: value
71
+ for key, value in manifest.items()
72
+ if key
73
+ not in {
74
+ "profile",
75
+ "description",
76
+ "projections",
77
+ "arthaanu_types",
78
+ "actions",
79
+ "agent_tools",
80
+ }
81
+ },
82
+ }
@@ -0,0 +1,35 @@
1
+ """Memuron CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from memuron.application.app import close_application, configure_clerk_env, create_application
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ configure_clerk_env()
14
+ os.environ.setdefault("ARTHA_AUTH_PROVIDER", "api_key")
15
+ os.environ.setdefault(
16
+ "ARTHA_CONFIG_PATH",
17
+ str(Path.home() / ".config" / "memuron" / "config.json"),
18
+ )
19
+ if os.environ.get("MEMURON_API_KEY") and not os.environ.get("ARTHA_API_KEY"):
20
+ os.environ["ARTHA_API_KEY"] = os.environ["MEMURON_API_KEY"]
21
+ if os.environ.get("MEMURON_BASE_URL") and not os.environ.get("ARTHA_BASE_URL"):
22
+ os.environ["ARTHA_BASE_URL"] = os.environ["MEMURON_BASE_URL"]
23
+ effective_argv = list(sys.argv[1:] if argv is None else argv)
24
+ help_only = not effective_argv or any(
25
+ item in {"-h", "--help"} for item in effective_argv
26
+ )
27
+ application = create_application(":memory:" if help_only else None)
28
+ try:
29
+ return application.run_cli(argv, prog="memuron")
30
+ finally:
31
+ close_application(application)
32
+
33
+
34
+ if __name__ == "__main__":
35
+ raise SystemExit(main())