Skip to content

Commit d84708c

Browse files
phernandezclaude
andauthored
feat: add per-project local/cloud routing with API key auth (#555)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1428d18 commit d84708c

30 files changed

Lines changed: 1323 additions & 216 deletions

.github/workflows/test.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ jobs:
3737
run: |
3838
pip install uv
3939
40-
- name: Install just (Linux/macOS)
40+
- name: Install just (Linux)
4141
if: runner.os != 'Windows'
4242
run: |
43-
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
43+
sudo apt-get update
44+
sudo apt-get install -y just
4445
4546
- name: Install just (Windows)
4647
if: runner.os == 'Windows'
@@ -97,7 +98,8 @@ jobs:
9798
9899
- name: Install just
99100
run: |
100-
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
101+
sudo apt-get update
102+
sudo apt-get install -y just
101103
102104
- name: Create virtual env
103105
run: |
@@ -133,7 +135,8 @@ jobs:
133135
134136
- name: Install just
135137
run: |
136-
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
138+
sudo apt-get update
139+
sudo apt-get install -y just
137140
138141
- name: Create virtual env
139142
run: |

AGENTS.md

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,27 +189,46 @@ Flow: MCP Tool → Typed Client → HTTP API → Router → Service → Reposito
189189

190190
### Async Client Pattern (Important!)
191191

192-
**All MCP tools and CLI commands use the context manager pattern for HTTP clients:**
192+
**MCP tools use `get_project_client()` for per-project routing:**
193+
194+
```python
195+
from basic_memory.mcp.project_context import get_project_client
196+
197+
@mcp.tool()
198+
async def my_tool(project: str | None = None, context: Context | None = None):
199+
async with get_project_client(project, context) as (client, active_project):
200+
# client is routed based on project's mode (local ASGI or cloud HTTP)
201+
response = await call_get(client, "/path")
202+
return response
203+
```
204+
205+
**CLI commands and non-project-scoped code use `get_client()` directly:**
193206

194207
```python
195208
from basic_memory.mcp.async_client import get_client
196209

197-
async def my_mcp_tool():
210+
async def my_cli_command():
198211
async with get_client() as client:
199-
# Use client for API calls
200212
response = await call_get(client, "/path")
201213
return response
214+
215+
# Per-project routing (when project name is known):
216+
async with get_client(project_name="research") as client:
217+
...
202218
```
203219

204220
**Do NOT use:**
205221
-`from basic_memory.mcp.async_client import client` (deprecated module-level client)
206222
- ❌ Manual auth header management
207223
-`inject_auth_header()` (deleted)
224+
- ❌ Separate `get_client()` + `get_active_project()` in MCP tools (use `get_project_client()` instead)
208225

209226
**Key principles:**
210227
- Auth happens at client creation, not per-request
211228
- Proper resource management via context managers
212-
- Supports three modes: Local (ASGI), CLI cloud (HTTP + auth), Cloud app (factory injection)
229+
- Per-project routing: each project can be LOCAL or CLOUD independently
230+
- Cloud projects use API key (`cloud_api_key` in config) as Bearer token
231+
- Routing priority: factory injection > force-local > per-project cloud > global cloud > local ASGI
213232
- Factory pattern enables dependency injection for cloud consolidation
214233

215234
**For cloud app integration:**
@@ -250,15 +269,19 @@ See SPEC-16 for full context manager refactor details.
250269
- List projects: `basic-memory project list`
251270
- Add project: `basic-memory project add "name" ~/path`
252271
- Project info: `basic-memory project info`
272+
- Set cloud mode: `basic-memory project set-cloud "name"`
273+
- Set local mode: `basic-memory project set-local "name"`
253274
- One-way sync (local -> cloud): `basic-memory project sync`
254275
- Bidirectional sync: `basic-memory project bisync`
255276
- Integrity check: `basic-memory project check`
256277

257278
**Cloud Commands (requires subscription):**
258-
- Authenticate: `basic-memory cloud login`
259-
- Logout: `basic-memory cloud logout`
279+
- Authenticate (global): `basic-memory cloud login`
280+
- Logout (global): `basic-memory cloud logout`
260281
- Check cloud status: `basic-memory cloud status`
261282
- Setup cloud sync: `basic-memory cloud setup`
283+
- Save API key: `basic-memory cloud set-key bmc_...`
284+
- Create API key: `basic-memory cloud create-key "name"`
262285
- Manage snapshots: `basic-memory cloud snapshot [create|list|delete|show|browse]`
263286
- Restore from snapshot: `basic-memory cloud restore <path> --snapshot <id>`
264287

@@ -329,9 +352,22 @@ Basic Memory now supports cloud synchronization and storage (requires active sub
329352
- Background relation resolution (non-blocking startup)
330353
- API performance optimizations (SPEC-11)
331354

332-
**CLI Routing Flags:**
355+
**Per-Project Cloud Routing:**
356+
357+
Individual projects can be routed through the cloud while others stay local, using an API key:
358+
359+
```bash
360+
# Save API key and set project to cloud mode
361+
basic-memory cloud set-key bmc_abc123...
362+
basic-memory project set-cloud research # route through cloud
363+
basic-memory project set-local research # revert to local
364+
```
365+
366+
MCP tools use `get_project_client()` which automatically routes based on the project's mode. Cloud projects use the `cloud_api_key` from config as Bearer token.
367+
368+
**CLI Routing Flags (Global Cloud Mode):**
333369

334-
When cloud mode is enabled, CLI commands route to the cloud API by default. Use `--local` and `--cloud` flags to override:
370+
When global cloud mode is enabled, CLI commands route to the cloud API by default. Use `--local` and `--cloud` flags to override:
335371

336372
```bash
337373
# Force local routing (ignore cloud mode)
@@ -348,6 +384,7 @@ Key behaviors:
348384
- This allows simultaneous use of local Claude Desktop and cloud-based clients
349385
- Some commands (like `project default`, `project sync-config`, `project move`) require `--local` in cloud mode since they modify local configuration
350386
- Environment variable `BASIC_MEMORY_FORCE_LOCAL=true` forces local routing globally
387+
- Per-project cloud routing via API key works independently of global cloud mode
351388

352389
## AI-Human Collaborative Development
353390

README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ basic-memory sync --watch
344344
3. Cloud features (optional, requires subscription):
345345

346346
```bash
347-
# Authenticate with cloud
347+
# Authenticate with cloud (global cloud mode via OAuth)
348348
basic-memory cloud login
349349

350350
# Bidirectional sync with cloud
@@ -357,9 +357,29 @@ basic-memory cloud check
357357
basic-memory cloud mount
358358
```
359359

360-
**Routing Flags** (for users with cloud subscriptions):
360+
**Per-Project Cloud Routing** (API key based):
361361

362-
When cloud mode is enabled, CLI commands communicate with the cloud API by default. Use routing flags to override this:
362+
Individual projects can be routed through the cloud while others stay local. This uses an API key instead of OAuth:
363+
364+
```bash
365+
# Save an API key (create one in the web app or via CLI)
366+
basic-memory cloud set-key bmc_abc123...
367+
# Or create one via CLI (requires OAuth login first)
368+
basic-memory cloud create-key "my-laptop"
369+
370+
# Set a project to route through cloud
371+
basic-memory project set-cloud research
372+
373+
# Revert a project to local mode
374+
basic-memory project set-local research
375+
376+
# List projects with mode column (local/cloud)
377+
basic-memory project list
378+
```
379+
380+
**Routing Flags** (for users with global cloud mode):
381+
382+
When global cloud mode is enabled, CLI commands communicate with the cloud API by default. Use routing flags to override this:
363383

364384
```bash
365385
# Force local routing (useful for local MCP server while cloud mode is enabled)

docs/ARCHITECTURE.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def resolve_runtime_mode(cloud_mode_enabled: bool, is_test_env: bool) -> Runtime
110110
return RuntimeMode.LOCAL
111111
```
112112

113+
**Note**: `RuntimeMode` determines global behavior (e.g., whether to start file sync). Per-project routing is orthogonal — individual projects can be set to `cloud` mode via `ProjectMode` in config, which affects client routing in `get_client(project_name=...)` without changing the global runtime mode.
114+
113115
## Dependencies Package
114116

115117
### Structure
@@ -221,9 +223,7 @@ async def search_notes(
221223
tags: list[str] | None = None,
222224
status: str | None = None,
223225
) -> SearchResponse:
224-
async with get_client() as client:
225-
active_project = await get_active_project(client, project)
226-
226+
async with get_project_client(project, context) as (client, active_project):
227227
# Import client inside function to avoid circular imports
228228
from basic_memory.mcp.clients import SearchClient
229229
from basic_memory.schemas.search import SearchQuery
@@ -238,6 +238,25 @@ async def search_notes(
238238
return await search_client.search(search_query.model_dump())
239239
```
240240

241+
### Per-Project Client Routing
242+
243+
`get_project_client()` from `mcp/project_context.py` is an async context manager that:
244+
1. Resolves the project name from config (no network call)
245+
2. Creates the correctly-routed client based on the project's mode (local ASGI or cloud HTTP with API key)
246+
3. Validates the project via the API
247+
4. Yields `(client, active_project)` tuple
248+
249+
This solves the bootstrap problem: you need the project name to choose the right client (local vs cloud), but you need the client to validate the project exists.
250+
251+
```python
252+
from basic_memory.mcp.project_context import get_project_client
253+
254+
async with get_project_client(project, context) as (client, active_project):
255+
# client is routed based on project's mode (local or cloud)
256+
# active_project is validated via the API
257+
...
258+
```
259+
241260
## Sync Coordination
242261

243262
### SyncCoordinator

0 commit comments

Comments
 (0)