Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions docs/guides/BRAIN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# StackMemory Brain — shared, compounding context

> Move your brain onto a server. Codex, Claude, OpenCode, and Hermes all connect
> to it. Every experiment uploads a summary and conclusion, so your agents'
> mutual thinking keeps compounding.

The **brain** is a shared knowledge layer scoped two ways:

- **per repo** (`projectId`) — what this codebase has tried and learned.
- **per org** (`workspaceId`, from `stackmemory login`) — knowledge shared across
every repo in your workspace.

Each entry is an **experiment / decision / insight / note** with a `title`, a
`summary` (what was done) and the payload that compounds — the `conclusion`.
Entries sync online so the same brain is available on every machine and to
every agent.

```
Codex ─┐
Claude ─┼─► stackmemory brain record ──► brain_entries (local SQLite)
OpenCode ─┤ │ brain sync
Hermes ─┘ ▼
Provenant API (per repo + per org)
any machine/agent ◄── stackmemory brain recall ◄── brain sync (pull)
```

## How agents connect

Every tool connects the same way — by shelling out to the CLI (this is how the
Codex / OpenCode / Hermes wrappers already integrate with StackMemory):

```bash
# After an experiment, record the conclusion so others build on it:
stackmemory brain record \
--agent codex --kind experiment \
--title "Retry with jitter cut 5xx" \
--summary "Added exponential backoff + jitter to the sync client" \
--conclusion "p99 errors dropped 60%; adopt as the default" \
--tags sync,reliability --refs STA-412,abc1234

# Before planning, recall what's already been tried:
stackmemory brain recall "retry" # this repo
stackmemory brain recall "auth" --org # the whole org
```

Drop the recall into an agent's planning preamble (a hook, a wrapper, or a
prompt step) and every plan starts enriched by prior conclusions.

## CLI

```bash
stackmemory brain record --title ... [--summary] [--conclusion] [--kind] \
[--agent] [--tags a,b] [--refs x,y] [--confidence 0.8]
stackmemory brain recall [query] [--org] [--agent] [--kind] [--limit] [--all]
stackmemory brain list [--limit]
stackmemory brain show <id>
stackmemory brain sync [--push | --pull] # online push + pull
stackmemory brain status
```

`--json` is available on every subcommand for programmatic use.

### Kinds

| kind | use it for |
|------|-----------|
| `experiment` | something you tried + what happened (the compounding unit) |
| `decision` | a choice made and the reasoning |
| `insight` | a durable learning worth resurfacing |
| `note` | free-form context |

## Scoping: repo vs org

- `recall` defaults to the **current repo**.
- `recall --org` widens to the **whole workspace** — cross-pollinate learnings
between repos (e.g. "we standardized on Zod for request validation").
- An entry always carries both `projectId` and `workspaceId`, so the same row
is reachable from either scope.

`projectId` and `workspaceId` come from `~/.stackmemory/config.json` (written by
`stackmemory login`) or from `PROVENANT_PROJECT_ID` / `PROVENANT_WORKSPACE_ID` /
`PROVENANT_API_KEY` env vars.

## Online sync

```bash
stackmemory login you@example.com # provisions apiKey + workspaceId + projectId
stackmemory brain sync # push local entries, pull the rest
```

- **Transport:** `POST {endpoint}/v1/brain/push` and `/v1/brain/pull`, authed
with the same Bearer API key as cloud sync. The endpoint defaults to the
hosted Provenant API and is overridable with `PROVENANT_API_URL`.
- **Conflict resolution:** newest-wins by `updatedAt`. Pulling never clobbers a
locally-newer entry.
- **Offline-safe:** if the server is unreachable, the brain stays fully usable
locally and `sync` reports the error without throwing.
- **Isolation:** brain sync is deliberately separate from the frame
`CloudSyncEngine`, so it can never regress that path.

> The hosted `/v1/brain/*` endpoints live in the Provenant API
> (`packages/provenant`). The client here speaks the documented contract above;
> until the endpoints are deployed, the brain runs local-first and `brain sync`
> reports the endpoint as unreachable.

## Storage

| | |
|--|--|
| Table | `brain_entries` (created lazily in the project's `.stackmemory/context.db`) |
| Sync cursors | `brain_sync_meta(direction, cursor)` |
| Columns | `entry_id, workspace_id, project_id, agent, kind, title, summary, conclusion, tags, refs, confidence, status, superseded_by, created_at, updated_at` |

## Files

| Path | Purpose |
|------|---------|
| `src/core/brain/brain-store.ts` | Local SQLite store (record / recall / supersede) |
| `src/core/brain/brain-sync.ts` | Online push/pull client (newest-wins, offline-safe) |
| `src/core/brain/index.ts` | Scope + config resolution, `openBrain()` |
| `src/cli/commands/brain.ts` | `stackmemory brain` command |
155 changes: 155 additions & 0 deletions docs/guides/PORTAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# StackMemory Portal — Run Claude Code 24/7

> A VPS, Claude Code in tmux, a Tailscale VPN, and a vibecoded web terminal.
> Your agents run 24/7. You experience life.

The **portal** is a self-hosted, browser-based terminal into a persistent
`tmux` session running Claude Code. Put it on a small VPS behind Tailscale and
you get a private, always-on coding agent you can check on from your phone,
laptop, or tablet — no exposed ports, no SaaS in the middle.

```
┌── Hetzner CX22 (~€4.5/mo) ───────────────────────────┐
│ │
│ tmux session "claude" ──► claude (max plan) │
│ ▲ │
│ │ node-pty │
│ stackmemory portal ──► :7799 (xterm.js + WS) │
│ ▲ │
└────────┼──────────────────────────────────────────────┘
│ Tailscale (WireGuard, 100.x address)
Your browser → http://100.x.y.z:7799/?token=…
```

**Why this shape?**

- **tmux** keeps the agent alive when you close the browser or the portal
restarts. Reattach over SSH any time.
- **Tailscale** gives you an encrypted private address with zero open ports —
no nginx, no TLS certs, no firewall holes.
- **node-pty + xterm.js** stream the real terminal, so Claude Code's TUI,
permissions prompts, and colors all work exactly as they do locally.

---

## Quick start (Hetzner cloud-init)

The fastest path — the server provisions itself on first boot.

1. Create a Tailscale **auth key** at
<https://login.tailscale.com/admin/settings/keys> (reusable, ephemeral off).
2. In Hetzner Cloud, **Add Server** → Ubuntu 24.04 → type **CX22**.
3. Expand **Cloud config** and paste
[`scripts/portal/cloud-init.yaml`](../../scripts/portal/cloud-init.yaml).
Set `TS_AUTHKEY=` to your key inside the pasted config.
4. Create the server. After ~2 minutes it's on your tailnet.

Then finish the two interactive steps over SSH:

```bash
ssh root@<hetzner-ip>

# Authenticate Claude Code (max plan) once — it caches credentials in ~/.claude
tmux attach -t claude # log in, approve, then detach with: Ctrl-b d

# Grab your access URL + token
journalctl -u stackmemory-portal --no-pager | grep -i token
```

Open `http://100.x.y.z:7799/?token=…` (the `100.x` Tailscale address) from any
device signed into your tailnet. You're now looking at your agent.

---

## Manual setup

Prefer to do it by hand, or installing on an existing box?

```bash
# On the VPS (Debian/Ubuntu):
curl -fsSL https://raw.githubusercontent.com/stackmemoryai/stackmemory/main/scripts/portal/setup.sh | bash

sudo tailscale up # join your tailnet (prints an auth URL)
tmux new -s claude 'claude' # authenticate Claude, then Ctrl-b d to detach
stackmemory portal start --cwd ~/work # start the portal (prints the URL + token)
```

For 24/7 operation, install the service:

```bash
sudo cp scripts/portal/stackmemory-portal.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now stackmemory-portal
journalctl -u stackmemory-portal -f # tail logs (the access URL is printed here)
```

---

## The CLI

```bash
stackmemory portal start # start the server (foreground; systemd runs this)
stackmemory portal status # show status + the access URL for this machine
stackmemory portal stop # stop a running portal
stackmemory portal token # print the access token
```

`start` options:

| Flag | Default | Description |
|------|---------|-------------|
| `--port <n>` | `7799` | Port to listen on |
| `--host <h>` | `0.0.0.0` | Interface to bind (reachable over the tailnet) |
| `--session <name>` | `claude` | tmux session name |
| `--command <cmd>` | `claude` | Command tmux runs (`"claude --resume"`, a wrapper, etc.) |
| `--cwd <dir>` | cwd | Working directory for the session |
| `--no-auth` | off | Disable the token (rely on Tailscale alone) |

The portal runs `tmux new-session -A -s <session> <command>`: it **attaches** to
the session if it already exists, otherwise creates it. Multiple browser tabs
share the same live session. Closing a tab detaches but never kills the agent.

---

## Security model

- **Network:** binding to `0.0.0.0` is safe *because* the box only has a public
IP plus its Tailscale address — keep the cloud firewall closed to `:7799` and
reach it exclusively over the tailnet. (Hetzner's firewall: allow `22` from
your IP, deny the rest.)
- **Token:** a 48-char token is generated on first start and stored at
`~/.stackmemory/portal/token` (`chmod 600`). It's required on both the page
load (`?token=`) and the WebSocket handshake. Rotate it by deleting the file
and restarting. `--no-auth` turns this off if you trust your tailnet ACLs.
- **No inbound ports on the internet.** Tailscale is WireGuard point-to-point;
there is nothing to port-scan.

> Treat the token like an SSH key — anyone with the URL gets a live shell as the
> user running the portal.

---

## Troubleshooting

| Symptom | Fix |
|---------|-----|
| `tmux is not installed` | `sudo apt install tmux` |
| Page loads but terminal is blank / "Cannot start session" | `node-pty` missing on the server: `npm install -g node-pty` (needs `build-essential` + `python3`) |
| `401 Unauthorized` | Append `?token=<token>` to the URL (`stackmemory portal token`) |
| Can't reach `100.x` address | `tailscale status` on both ends; make sure your client is logged into the same tailnet |
| Claude asks to log in every time | Authenticate once inside the tmux session so credentials land in `~/.claude`; ensure systemd `HOME=` points at that user's home |
| Agent died but portal is up | `tmux attach -t claude` to inspect; the portal recreates the session on next connect |

---

## Files

| Path | Purpose |
|------|---------|
| `src/features/portal/server.ts` | Express + Socket.io + node-pty bridge |
| `src/features/portal/ui.ts` | Embedded xterm.js terminal UI |
| `src/cli/commands/portal.ts` | `stackmemory portal` command |
| `scripts/portal/setup.sh` | One-shot VPS installer |
| `scripts/portal/cloud-init.yaml` | Hetzner first-boot provisioning |
| `scripts/portal/stackmemory-portal.service` | systemd unit for 24/7 operation |
Loading
Loading