mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-09 03:18:04 +00:00
docs(smfs): address review feedback on provider guides
- Mermaid: replace literal \\n with <br/> in node labels (didn't render) - e2b: clarify FUSE chmod / sleep comments; use Path().read_text(); move sudo Note above Pattern B - daytona: extract shared SMFS_INSTALL snippet; upload agent.py in Pattern A (was missing); reword PATH tip; reconcile warning + note - vercel: scope Pattern A to self-hosted Node (Vercel Functions can't hold a FUSE mount); switch Pattern B to @supermemory/bash for serverless deployments - cloudflare: switch wrangler.toml -> wrangler.jsonc with proper Container Durable Object binding + migrations; define Container subclass and pass secrets via envVars; use containerFetch + getContainer; add /exec security warning; switch Flask dev server to gunicorn
This commit is contained in:
parent
ae8108adbe
commit
3eb7933a5c
4 changed files with 299 additions and 141 deletions
|
|
@ -20,7 +20,7 @@ mount. The entrypoint sets up the mount and starts the agent.
|
|||
```mermaid
|
||||
graph LR
|
||||
subgraph Cloudflare Container
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["/memory\n(SMFS mount)"]
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["/memory<br/>(SMFS mount)"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
```
|
||||
|
|
@ -32,9 +32,9 @@ HTTP. The container exposes a simple exec endpoint.
|
|||
|
||||
```mermaid
|
||||
graph LR
|
||||
Agent["Worker\n(agent logic)"] -->|"fetch('/exec')"| Container
|
||||
Agent["Worker<br/>(agent logic)"] -->|"containerFetch('/exec')"| Container
|
||||
subgraph Container ["Cloudflare Container"]
|
||||
Mount["/memory\n(SMFS mount)"]
|
||||
Mount["/memory<br/>(SMFS mount)"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
```
|
||||
|
|
@ -43,8 +43,17 @@ graph LR
|
|||
|
||||
- A [Supermemory API key](https://supermemory.ai)
|
||||
- An [Anthropic API key](https://console.anthropic.com)
|
||||
- A [Cloudflare account](https://dash.cloudflare.com) with Containers enabled
|
||||
- A [Cloudflare account](https://dash.cloudflare.com) with Containers enabled (Workers Paid plan)
|
||||
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
|
||||
- The [`@cloudflare/containers`](https://www.npmjs.com/package/@cloudflare/containers) package: `npm install @cloudflare/containers`
|
||||
|
||||
<Note>
|
||||
Cloudflare Containers are implemented as container-enabled Durable Objects.
|
||||
You declare a `Container` subclass, bind it as a Durable Object, and
|
||||
reference its image in the `containers` array. Worker secrets are **not**
|
||||
automatically visible inside the container — you have to pass them through
|
||||
`envVars` when starting the container (see below).
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -108,16 +117,63 @@ async def main():
|
|||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Deploy
|
||||
### Worker
|
||||
|
||||
```toml wrangler.toml
|
||||
name = "memory-agent"
|
||||
main = "worker.ts"
|
||||
The Worker defines the `Container` subclass and forwards Worker secrets into
|
||||
the container via `envVars`:
|
||||
|
||||
[[containers]]
|
||||
class_name = "MY_CONTAINER"
|
||||
image = "./Dockerfile"
|
||||
max_instances = 5
|
||||
```typescript worker.ts
|
||||
import { Container, getContainer } from "@cloudflare/containers";
|
||||
|
||||
export class MyAgentContainer extends Container {
|
||||
defaultPort = 8080;
|
||||
// Forward Worker secrets into the container at start time.
|
||||
// `this.env` is the Worker env object, populated from wrangler secrets.
|
||||
envVars = {
|
||||
SUPERMEMORY_API_KEY: this.env.SUPERMEMORY_API_KEY,
|
||||
ANTHROPIC_API_KEY: this.env.ANTHROPIC_API_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
// The container runs the agent and exits; this Worker route just kicks
|
||||
// it off (e.g. on a queue message or scheduled trigger).
|
||||
const container = getContainer(env.MY_CONTAINER, "agent-singleton");
|
||||
return container.fetch(request);
|
||||
},
|
||||
};
|
||||
|
||||
interface Env {
|
||||
MY_CONTAINER: DurableObjectNamespace<MyAgentContainer>;
|
||||
SUPERMEMORY_API_KEY: string;
|
||||
ANTHROPIC_API_KEY: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
```jsonc wrangler.jsonc
|
||||
{
|
||||
"name": "memory-agent",
|
||||
"main": "worker.ts",
|
||||
"compatibility_date": "2025-04-03",
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "MyAgentContainer",
|
||||
"image": "./Dockerfile",
|
||||
"max_instances": 5
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{ "name": "MY_CONTAINER", "class_name": "MyAgentContainer" }
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_sqlite_classes": ["MyAgentContainer"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
|
|
@ -133,8 +189,21 @@ wrangler deploy
|
|||
The agent logic lives in a Worker. The container just runs SMFS and exposes an
|
||||
HTTP endpoint for executing commands against the mount.
|
||||
|
||||
<Warning>
|
||||
The `/exec` endpoint below runs arbitrary shell commands inside the
|
||||
container. **Only call it from your Worker** — never expose it publicly,
|
||||
and never pass user input straight into `command` without validation.
|
||||
Cloudflare Containers are addressable only through their Worker by default,
|
||||
so this is safe as long as you don't add a public route that proxies to
|
||||
`/exec`.
|
||||
</Warning>
|
||||
|
||||
### Container (exec server)
|
||||
|
||||
The Dockerfile and entrypoint are nearly identical to Pattern A — the only
|
||||
differences are the Python deps (`flask` instead of `claude-agent-sdk`) and
|
||||
the file we exec at the end.
|
||||
|
||||
```dockerfile Dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
|
|
@ -143,7 +212,7 @@ RUN echo 'user_allow_other' >> /etc/fuse.conf
|
|||
|
||||
RUN curl -fsSL https://smfs.ai/install | bash -s -- 0.0.1-rc2
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
RUN pip install flask
|
||||
RUN pip install flask gunicorn
|
||||
|
||||
COPY server.py /app/server.py
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
|
@ -152,6 +221,9 @@ RUN chmod +x /entrypoint.sh
|
|||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
```
|
||||
|
||||
The entrypoint differs from Pattern A only in the final `exec` line — we run
|
||||
gunicorn against the Flask app instead of `python3 agent.py`:
|
||||
|
||||
```bash entrypoint.sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
|
@ -160,7 +232,7 @@ smfs login --key "$SUPERMEMORY_API_KEY"
|
|||
smfs mount my_agent --ephemeral --path /memory --foreground &
|
||||
sleep 3
|
||||
|
||||
exec python3 /app/server.py
|
||||
exec gunicorn -b 0.0.0.0:8080 --chdir /app server:app
|
||||
```
|
||||
|
||||
```python server.py
|
||||
|
|
@ -176,28 +248,78 @@ def exec_command():
|
|||
cmd, shell=True, capture_output=True, text=True, cwd="/memory", timeout=10
|
||||
)
|
||||
return jsonify(stdout=result.stdout, stderr=result.stderr, code=result.returncode)
|
||||
|
||||
app.run(host="0.0.0.0", port=8080)
|
||||
```
|
||||
|
||||
<Note>
|
||||
We use gunicorn instead of `app.run(...)` because Flask's built-in dev
|
||||
server isn't meant for production traffic. If you'd rather just see it
|
||||
work, you can replace the `exec` line with
|
||||
`exec python3 /app/server.py` and add `app.run(host="0.0.0.0", port=8080)`
|
||||
to `server.py` — but switch back to gunicorn before you ship.
|
||||
</Note>
|
||||
|
||||
### Worker (agent logic)
|
||||
|
||||
```typescript worker.ts
|
||||
import { Container, getContainer } from "@cloudflare/containers";
|
||||
|
||||
export class ExecContainer extends Container {
|
||||
defaultPort = 8080;
|
||||
envVars = {
|
||||
SUPERMEMORY_API_KEY: this.env.SUPERMEMORY_API_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: any) {
|
||||
const container = await env.MY_CONTAINER.start();
|
||||
async fetch(_request: Request, env: Env) {
|
||||
const container = getContainer(env.MY_CONTAINER, "agent-singleton");
|
||||
|
||||
const profile = await container
|
||||
.fetch("/exec", {
|
||||
.fetch(new Request("http://container/exec", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ command: "cat /memory/profile.md" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r: Response) => r.json());
|
||||
}))
|
||||
.then((r) => r.json<{ stdout: string }>());
|
||||
|
||||
return Response.json({ profile: profile.stdout });
|
||||
},
|
||||
};
|
||||
|
||||
interface Env {
|
||||
MY_CONTAINER: DurableObjectNamespace<ExecContainer>;
|
||||
SUPERMEMORY_API_KEY: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
```jsonc wrangler.jsonc
|
||||
{
|
||||
"name": "memory-exec",
|
||||
"main": "worker.ts",
|
||||
"compatibility_date": "2025-04-03",
|
||||
"containers": [
|
||||
{
|
||||
"class_name": "ExecContainer",
|
||||
"image": "./Dockerfile",
|
||||
"max_instances": 5
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{ "name": "MY_CONTAINER", "class_name": "ExecContainer" }
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_sqlite_classes": ["ExecContainer"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
wrangler secret put SUPERMEMORY_API_KEY
|
||||
wrangler deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -207,3 +329,9 @@ export default {
|
|||
- Use `--ephemeral` for container mounts — keeps the cache in memory only, but
|
||||
writes still push to Supermemory
|
||||
- Use `smfs grep 'query'` for semantic search across all files
|
||||
- Worker secrets aren't automatically visible inside the container. Pass each
|
||||
one through the `envVars` field on your `Container` subclass (see the Worker
|
||||
snippets above)
|
||||
- Use `containerFetch` from within a Container class method (e.g., lifecycle
|
||||
hooks) to call the container's own HTTP server. From the Worker, use the
|
||||
stub's `.fetch()` method instead
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ Mount a Supermemory container inside a [Daytona](https://daytona.io) sandbox so
|
|||
your agent can read and write memory using standard filesystem commands.
|
||||
|
||||
<Warning>
|
||||
Daytona sandboxes currently cannot reach `api.supermemory.ai` due to network
|
||||
restrictions from their datacenter IPs. The SMFS binary installs and the FUSE
|
||||
mount starts, but it cannot sync data. We're working with Daytona to resolve
|
||||
this. In the meantime, use [E2B](/smfs/providers/e2b) or a
|
||||
[local mount](/smfs/providers/vercel) instead.
|
||||
Daytona sandboxes currently cannot reach `api.supermemory.ai` from their
|
||||
datacenter IPs. The SMFS binary still installs (we download it directly from
|
||||
GitHub Releases), the FUSE mount still starts, and `pip install
|
||||
claude-agent-sdk` still works — but the runtime sync to Supermemory fails. We're
|
||||
working with Daytona to resolve this. In the meantime, use
|
||||
[E2B](/smfs/providers/e2b) or a [self-hosted mount](/smfs/providers/vercel).
|
||||
</Warning>
|
||||
|
||||
## How it works
|
||||
|
|
@ -26,7 +27,7 @@ The agent process runs inside the sandbox and accesses the SMFS mount directly.
|
|||
```mermaid
|
||||
graph LR
|
||||
subgraph Daytona Sandbox
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["/home/daytona/memory\n(SMFS mount)"]
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["/home/daytona/memory<br/>(SMFS mount)"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
```
|
||||
|
|
@ -38,9 +39,9 @@ sandbox remotely.
|
|||
|
||||
```mermaid
|
||||
graph LR
|
||||
Agent["Claude Agent\n(your server)"] -->|"sandbox.process.exec()"| Sandbox
|
||||
Agent["Claude Agent<br/>(your server)"] -->|"sandbox.process.exec()"| Sandbox
|
||||
subgraph Sandbox ["Daytona Sandbox"]
|
||||
Mount["/home/daytona/memory\n(SMFS mount)"]
|
||||
Mount["/home/daytona/memory<br/>(SMFS mount)"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
```
|
||||
|
|
@ -53,6 +54,40 @@ graph LR
|
|||
|
||||
---
|
||||
|
||||
## Install SMFS in a Daytona sandbox
|
||||
|
||||
Both patterns below run the same setup snippet inside the sandbox before
|
||||
mounting. Daytona can't reach `smfs.ai`, so we download the binary directly
|
||||
from GitHub Releases and add `~/.local/bin` to PATH.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
```python
|
||||
SMFS_INSTALL = (
|
||||
"mkdir -p $HOME/.local/bin && "
|
||||
"curl -sL https://github.com/supermemoryai/smfs/releases/download/"
|
||||
"v0.0.1-rc2/smfs-linux-x64 -o $HOME/.local/bin/smfs && "
|
||||
"chmod +x $HOME/.local/bin/smfs && "
|
||||
"echo 'user_allow_other' | sudo tee -a /etc/fuse.conf > /dev/null && "
|
||||
"pip install claude-agent-sdk"
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
```typescript
|
||||
const SMFS_INSTALL =
|
||||
"mkdir -p $HOME/.local/bin && " +
|
||||
"curl -sL https://github.com/supermemoryai/smfs/releases/download/" +
|
||||
"v0.0.1-rc2/smfs-linux-x64 -o $HOME/.local/bin/smfs && " +
|
||||
"chmod +x $HOME/.local/bin/smfs && " +
|
||||
"echo 'user_allow_other' | sudo tee -a /etc/fuse.conf > /dev/null && " +
|
||||
"pip install claude-agent-sdk";
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Pattern A: Agent inside the sandbox
|
||||
|
||||
### Agent code
|
||||
|
|
@ -84,6 +119,7 @@ asyncio.run(main())
|
|||
<Tab title="Python">
|
||||
```python run.py
|
||||
import os
|
||||
from pathlib import Path
|
||||
from daytona_sdk import Daytona, DaytonaConfig
|
||||
|
||||
daytona = Daytona(DaytonaConfig(
|
||||
|
|
@ -96,19 +132,8 @@ asyncio.run(main())
|
|||
},
|
||||
)
|
||||
|
||||
# Install SMFS (from GitHub releases — smfs.ai is unreachable from Daytona)
|
||||
sandbox.process.exec(
|
||||
"mkdir -p $HOME/.local/bin && "
|
||||
"curl -sL https://github.com/supermemoryai/smfs/releases/download/"
|
||||
"v0.0.1-rc2/smfs-linux-x64 -o $HOME/.local/bin/smfs && "
|
||||
"chmod +x $HOME/.local/bin/smfs"
|
||||
)
|
||||
|
||||
# Fix FUSE config and install agent SDK
|
||||
sandbox.process.exec(
|
||||
"echo 'user_allow_other' | sudo tee -a /etc/fuse.conf > /dev/null"
|
||||
)
|
||||
sandbox.process.exec("pip install claude-agent-sdk")
|
||||
# See "Install SMFS in a Daytona sandbox" above
|
||||
sandbox.process.exec(SMFS_INSTALL)
|
||||
|
||||
# Mount memory
|
||||
sandbox.process.exec("$HOME/.local/bin/smfs login --key $SUPERMEMORY_API_KEY")
|
||||
|
|
@ -117,7 +142,8 @@ asyncio.run(main())
|
|||
" --path /home/daytona/memory --foreground &' && sleep 3"
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
# Upload and run the agent
|
||||
sandbox.fs.upload_file(Path("agent.py").read_bytes(), "agent.py")
|
||||
result = sandbox.process.exec("python3 agent.py")
|
||||
print(result.result)
|
||||
|
||||
|
|
@ -127,6 +153,7 @@ asyncio.run(main())
|
|||
<Tab title="TypeScript">
|
||||
```typescript run.ts
|
||||
import { Daytona } from "@daytonaio/sdk";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const daytona = new Daytona({
|
||||
apiKey: process.env.DAYTONA_API_KEY!,
|
||||
|
|
@ -138,19 +165,8 @@ asyncio.run(main())
|
|||
},
|
||||
});
|
||||
|
||||
// Install SMFS (from GitHub releases — smfs.ai is unreachable from Daytona)
|
||||
await sandbox.process.exec(
|
||||
"mkdir -p $HOME/.local/bin && " +
|
||||
"curl -sL https://github.com/supermemoryai/smfs/releases/download/" +
|
||||
"v0.0.1-rc2/smfs-linux-x64 -o $HOME/.local/bin/smfs && " +
|
||||
"chmod +x $HOME/.local/bin/smfs"
|
||||
);
|
||||
|
||||
// Fix FUSE config and install agent SDK
|
||||
await sandbox.process.exec(
|
||||
"echo 'user_allow_other' | sudo tee -a /etc/fuse.conf > /dev/null"
|
||||
);
|
||||
await sandbox.process.exec("pip install claude-agent-sdk");
|
||||
// See "Install SMFS in a Daytona sandbox" above
|
||||
await sandbox.process.exec(SMFS_INSTALL);
|
||||
|
||||
// Mount memory
|
||||
await sandbox.process.exec(
|
||||
|
|
@ -162,6 +178,7 @@ asyncio.run(main())
|
|||
);
|
||||
|
||||
// Upload and run the agent
|
||||
await sandbox.fs.uploadFile(readFileSync("agent.py"), "agent.py");
|
||||
const result = await sandbox.process.exec("python3 agent.py");
|
||||
console.log(result.result);
|
||||
|
||||
|
|
@ -192,16 +209,8 @@ remotely via `sandbox.process.exec()`.
|
|||
},
|
||||
)
|
||||
|
||||
# Install and mount SMFS
|
||||
sandbox.process.exec(
|
||||
"mkdir -p $HOME/.local/bin && "
|
||||
"curl -sL https://github.com/supermemoryai/smfs/releases/download/"
|
||||
"v0.0.1-rc2/smfs-linux-x64 -o $HOME/.local/bin/smfs && "
|
||||
"chmod +x $HOME/.local/bin/smfs"
|
||||
)
|
||||
sandbox.process.exec(
|
||||
"echo 'user_allow_other' | sudo tee -a /etc/fuse.conf > /dev/null"
|
||||
)
|
||||
# See "Install SMFS in a Daytona sandbox" above
|
||||
sandbox.process.exec(SMFS_INSTALL)
|
||||
sandbox.process.exec("$HOME/.local/bin/smfs login --key $SUPERMEMORY_API_KEY")
|
||||
sandbox.process.exec(
|
||||
"bash -c '$HOME/.local/bin/smfs mount my_agent --ephemeral"
|
||||
|
|
@ -235,16 +244,8 @@ remotely via `sandbox.process.exec()`.
|
|||
},
|
||||
});
|
||||
|
||||
// Install and mount SMFS
|
||||
await sandbox.process.exec(
|
||||
"mkdir -p $HOME/.local/bin && " +
|
||||
"curl -sL https://github.com/supermemoryai/smfs/releases/download/" +
|
||||
"v0.0.1-rc2/smfs-linux-x64 -o $HOME/.local/bin/smfs && " +
|
||||
"chmod +x $HOME/.local/bin/smfs"
|
||||
);
|
||||
await sandbox.process.exec(
|
||||
"echo 'user_allow_other' | sudo tee -a /etc/fuse.conf > /dev/null"
|
||||
);
|
||||
// See "Install SMFS in a Daytona sandbox" above
|
||||
await sandbox.process.exec(SMFS_INSTALL);
|
||||
await sandbox.process.exec(
|
||||
"$HOME/.local/bin/smfs login --key $SUPERMEMORY_API_KEY"
|
||||
);
|
||||
|
|
@ -275,12 +276,7 @@ remotely via `sandbox.process.exec()`.
|
|||
|
||||
- FUSE is available in Daytona sandboxes but `user_allow_other` needs to be
|
||||
added to `/etc/fuse.conf`
|
||||
- The binary installs to `~/.local/bin/` which isn't on PATH by default in
|
||||
Daytona's zsh — use the full path or `export PATH=$HOME/.local/bin:$PATH`
|
||||
- We invoke SMFS as `$HOME/.local/bin/smfs` in the examples because Daytona's
|
||||
default zsh PATH doesn't include `~/.local/bin`. Alternatively, prepend it
|
||||
once with `export PATH=$HOME/.local/bin:$PATH`
|
||||
- Use `pip install claude-agent-sdk` to install the agent SDK (PyPI is reachable)
|
||||
|
||||
<Note>
|
||||
Daytona sandboxes can't reach `smfs.ai`, so the install downloads the binary
|
||||
directly from GitHub releases. The SMFS binary and Claude Agent SDK both
|
||||
install successfully — only the Supermemory API connection is blocked.
|
||||
</Note>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Your orchestrating code just boots the sandbox and kicks off the agent.
|
|||
```mermaid
|
||||
graph LR
|
||||
subgraph E2B Sandbox
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["/home/user/memory\n(SMFS mount)"]
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["/home/user/memory<br/>(SMFS mount)"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
```
|
||||
|
|
@ -32,9 +32,9 @@ infra.
|
|||
|
||||
```mermaid
|
||||
graph LR
|
||||
Agent["Claude Agent\n(your server)"] -->|"sbx.commands.run()"| Sandbox
|
||||
Agent["Claude Agent<br/>(your server)"] -->|"sbx.commands.run()"| Sandbox
|
||||
subgraph Sandbox ["E2B Sandbox"]
|
||||
Mount["/home/user/memory\n(SMFS mount)"]
|
||||
Mount["/home/user/memory<br/>(SMFS mount)"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
```
|
||||
|
|
@ -99,6 +99,7 @@ asyncio.run(main())
|
|||
<Tab title="Python">
|
||||
```python run.py
|
||||
import os
|
||||
from pathlib import Path
|
||||
from e2b_code_interpreter import Sandbox
|
||||
|
||||
sbx = Sandbox.create(
|
||||
|
|
@ -110,10 +111,11 @@ asyncio.run(main())
|
|||
},
|
||||
)
|
||||
|
||||
# One-time FUSE fix (device exists but is root-only by default)
|
||||
# /dev/fuse exists in E2B but is root-only by default. chmod once per sandbox.
|
||||
sbx.commands.run("sudo chmod 666 /dev/fuse")
|
||||
|
||||
# Mount memory
|
||||
# Mount memory. We background the foreground daemon so this command returns,
|
||||
# then sleep briefly to let the FUSE mount come up before the agent reads it.
|
||||
sbx.commands.run("smfs login --key $SUPERMEMORY_API_KEY")
|
||||
sbx.commands.run(
|
||||
"bash -c 'smfs mount my_agent --ephemeral"
|
||||
|
|
@ -121,7 +123,7 @@ asyncio.run(main())
|
|||
)
|
||||
|
||||
# Upload and run the agent
|
||||
sbx.files.write("/home/user/agent.py", open("agent.py").read())
|
||||
sbx.files.write("/home/user/agent.py", Path("agent.py").read_text())
|
||||
result = sbx.commands.run("python3 /home/user/agent.py", timeout=120)
|
||||
print(result.stdout)
|
||||
|
||||
|
|
@ -142,10 +144,11 @@ asyncio.run(main())
|
|||
},
|
||||
});
|
||||
|
||||
// One-time FUSE fix (device exists but is root-only by default)
|
||||
// /dev/fuse exists in E2B but is root-only by default. chmod once per sandbox.
|
||||
await sbx.commands.run("sudo chmod 666 /dev/fuse");
|
||||
|
||||
// Mount memory
|
||||
// Mount memory. We background the foreground daemon so this command returns,
|
||||
// then sleep briefly to let the FUSE mount come up before the agent reads it.
|
||||
await sbx.commands.run("smfs login --key $SUPERMEMORY_API_KEY");
|
||||
await sbx.commands.run(
|
||||
"bash -c 'smfs mount my_agent --ephemeral --path /home/user/memory --foreground &' && sleep 3"
|
||||
|
|
@ -171,6 +174,12 @@ The agent runs in your server process and executes commands inside the sandbox
|
|||
remotely via `sbx.commands.run()`. The SMFS mount lives inside the sandbox —
|
||||
the agent never touches the filesystem directly.
|
||||
|
||||
<Note>
|
||||
The FUSE mount is owned by root inside the sandbox. When writing to it from
|
||||
outside the agent, wrap the command in `sudo bash -c '…'` so the redirect
|
||||
runs with the right permissions. You'll see this in the write examples below.
|
||||
</Note>
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
```python run.py
|
||||
|
|
@ -242,11 +251,6 @@ the agent never touches the filesystem directly.
|
|||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Note>
|
||||
The FUSE mount is owned by root. When writing files from outside the agent,
|
||||
use `sudo bash -c 'echo "..." > /path/file'`.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
|
|
|||
|
|
@ -3,48 +3,59 @@ title: "Vercel AI SDK"
|
|||
description: "Give your AI agent persistent memory using SMFS with the Vercel AI SDK"
|
||||
---
|
||||
|
||||
Mount a Supermemory container on your server and let a Claude agent read and
|
||||
write memory using standard filesystem commands.
|
||||
This guide is about the [Vercel AI SDK](https://ai-sdk.dev) — the TypeScript
|
||||
agent framework — not Vercel hosting. The choice of pattern depends on where
|
||||
your code actually runs:
|
||||
|
||||
- **Self-hosted Node** (your own VM, ECS, Fly.io, Railway, a Vercel Sandbox,
|
||||
etc.): you can mount SMFS as a real filesystem on the server.
|
||||
- **Vercel Functions / serverless / edge**: there's no long-lived process to
|
||||
hold a FUSE mount, so use the [Bash Tool](/smfs/bash-tool)
|
||||
(`@supermemory/bash`) instead. The container becomes the filesystem; no mount
|
||||
needed.
|
||||
|
||||
## How it works
|
||||
|
||||
There are two ways to wire SMFS into a Vercel-based agent — pick the one that
|
||||
fits your architecture.
|
||||
|
||||
### Claude Agent SDK (agent has full filesystem access)
|
||||
### Self-hosted Node (real mount)
|
||||
|
||||
The agent runs as a separate process with direct access to the SMFS mount.
|
||||
Best when you want the agent to have full bash, read, and write capabilities.
|
||||
Best when you want full bash, read, and write capabilities and your server is
|
||||
long-lived.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Your Server
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["./memory\n(SMFS mount)"]
|
||||
Agent["Claude Agent"] -->|"cat, ls, echo"| Mount["./memory<br/>(SMFS mount)"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
```
|
||||
|
||||
### Vercel AI SDK (agent uses a tool)
|
||||
### Vercel Functions / serverless (Bash Tool)
|
||||
|
||||
The agent runs inside `generateText` and accesses memory through a bash tool
|
||||
you define. Best when you're building an API route and want to keep everything
|
||||
in one TypeScript process.
|
||||
The agent runs inside `generateText` and accesses memory through `@supermemory/bash`,
|
||||
which proxies bash commands to your Supermemory container over HTTP. No mount,
|
||||
no FUSE, no long-lived process required.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Your Server
|
||||
AI["generateText()"] -->|"bash tool"| Mount["./memory\n(SMFS mount)"]
|
||||
subgraph Vercel Function
|
||||
AI["generateText()"] -->|"bash tool"| Bash["@supermemory/bash"]
|
||||
end
|
||||
Mount -->|sync| SM["Supermemory"]
|
||||
Bash -->|HTTPS| SM["Supermemory"]
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A [Supermemory API key](https://supermemory.ai)
|
||||
- An [Anthropic API key](https://console.anthropic.com)
|
||||
- SMFS installed: `curl -fsSL https://smfs.ai/install | bash`
|
||||
- For Pattern A only: SMFS installed on your server (`curl -fsSL https://smfs.ai/install | bash`)
|
||||
|
||||
## Mount memory
|
||||
---
|
||||
|
||||
## Pattern A: Claude Agent SDK on self-hosted Node
|
||||
|
||||
Use this when the Vercel AI SDK is just the orchestrator and your real workload
|
||||
is a Claude agent running on a long-lived server you control.
|
||||
|
||||
Start the mount once when your server boots — not per-request:
|
||||
|
||||
|
|
@ -53,12 +64,14 @@ smfs login --key $SUPERMEMORY_API_KEY
|
|||
smfs mount my_agent --path ./memory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern A: Claude Agent SDK
|
||||
<Note>
|
||||
This won't work on Vercel Functions or any serverless runtime: there's no
|
||||
process between requests to hold the mount, and FUSE isn't available. For
|
||||
those targets, jump to Pattern B.
|
||||
</Note>
|
||||
|
||||
Write a standalone agent script. Nothing server-specific — just Python that
|
||||
reads and writes files.
|
||||
reads and writes files:
|
||||
|
||||
```python agent.py
|
||||
import asyncio
|
||||
|
|
@ -87,39 +100,37 @@ python3 agent.py
|
|||
|
||||
---
|
||||
|
||||
## Pattern B: Vercel AI SDK
|
||||
## Pattern B: Vercel AI SDK + Bash Tool (serverless-friendly)
|
||||
|
||||
Expose the memory filesystem as a bash tool inside an API route. The agent
|
||||
calls the tool to run commands against the mount.
|
||||
`@supermemory/bash` exposes your Supermemory container as a single agent tool
|
||||
— `run_bash(command)` — without mounting anything. It runs anywhere TypeScript
|
||||
runs, including Vercel Functions, edge runtimes, and Lambda.
|
||||
|
||||
```bash
|
||||
npm install @supermemory/bash ai @ai-sdk/anthropic zod
|
||||
```
|
||||
|
||||
```typescript api/agent.ts
|
||||
import { generateText, tool } from "ai";
|
||||
import { anthropic } from "@ai-sdk/anthropic";
|
||||
import { createBash } from "@supermemory/bash";
|
||||
import { z } from "zod";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const MEMORY = "./memory";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { prompt } = await req.json();
|
||||
|
||||
const { bash, toolDescription } = await createBash({
|
||||
apiKey: process.env.SUPERMEMORY_API_KEY!,
|
||||
containerTag: "my_agent",
|
||||
});
|
||||
|
||||
const result = await generateText({
|
||||
model: anthropic("claude-sonnet-4-20250514"),
|
||||
model: anthropic("claude-sonnet-4-5"),
|
||||
tools: {
|
||||
bash: tool({
|
||||
description: `Run a bash command. Memory filesystem is at ${MEMORY}.`,
|
||||
parameters: z.object({ command: z.string() }),
|
||||
execute: async ({ command }) => {
|
||||
try {
|
||||
return execSync(command, {
|
||||
cwd: MEMORY,
|
||||
encoding: "utf-8",
|
||||
timeout: 10_000,
|
||||
});
|
||||
} catch (e: any) {
|
||||
return e.stderr || e.message;
|
||||
}
|
||||
},
|
||||
description: toolDescription,
|
||||
inputSchema: z.object({ cmd: z.string() }),
|
||||
execute: async ({ cmd }) => bash.exec(cmd),
|
||||
}),
|
||||
},
|
||||
maxSteps: 10,
|
||||
|
|
@ -130,10 +141,29 @@ export async function POST(req: Request) {
|
|||
}
|
||||
```
|
||||
|
||||
A few things worth calling out:
|
||||
|
||||
- **`maxSteps: 10`** lets the agent chain multiple bash calls per request
|
||||
(read `profile.md`, then `cat` a few notes, then write a summary). Bump it
|
||||
if your agent needs deeper chains; lower it to cap cost per request.
|
||||
- **`toolDescription`** is a pre-written description of the available bash
|
||||
surface (semantic `sgrep`, `cat`, `ls`, redirects, etc.). Hand it straight
|
||||
to the model — don't roll your own.
|
||||
- **No timeout/abort plumbing.** `bash.exec` already runs against the
|
||||
container over HTTPS, so it returns when the command returns. No event-loop
|
||||
blocking and no FUSE.
|
||||
|
||||
See the [Bash Tool reference](/smfs/bash-tool) for the full command surface,
|
||||
memory path configuration, and other framework integrations.
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- Mount SMFS once when your server starts, not per-request
|
||||
- Use `smfs grep 'query'` for semantic search across all files
|
||||
- Use `--ephemeral` if you don't need a local cache on the server
|
||||
- **Pattern A**: mount SMFS once when your server starts, not per-request.
|
||||
Use `--ephemeral` if you don't need a local cache on the server.
|
||||
- **Pattern B**: configure memory paths once at startup with
|
||||
`configureMemoryPaths(["/notes/", "/journal.md"])` to control which files
|
||||
get distilled into Supermemory memories.
|
||||
- Both: use `smfs grep 'query'` (Pattern A) or `sgrep 'query'` inside the
|
||||
bash tool (Pattern B) for semantic search across all files.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue