poke-conduit
README.md
# poke-conduit
**The durable brain Poke consults.** Poke already lives in iMessage (sanctioned, no Apple API, no
ban risk) and is an **MCP client** that calls external MCP servers. poke-conduit inverts the usual
integration: instead of building our own iMessage transport, **Poke is the front-end terminal** and
poke-conduit is the **MCP server it calls** — a production-shaped, durable, multi-agent backend.
```
iMessage ──► Poke (MCP client) ──► poke-conduit /mcp ──► Postgres (Neon)
▲ │ orchestrator · worker-agents
└───── proactive push ───┘ council · backlog · scheduler
(Poke inbound API)
```
You text Poke; Poke calls a tool here; poke-conduit does durable work and can **text you back later**
through Poke's inbound API (async council verdicts, fired reminders).
## What it does
| Capability | Tools | Notes |
|---|---|---|
| **Queued-notes backlog** (flagship) | `add_note` · `list_backlog` · `complete_note` · `pin_note` | Durable to-read / to-do queue, 1-based refs, pinning. |
| **Council** (multi-agent) | `ask_council` · `council_status` | Fans a hard question out to six persona agents (Builder · Skeptic · Operator · User-Advocate · Strategist · Pragmatist), then a synthesizer makes the call. `deliver=return` replies inline; `deliver=async` returns now and **pushes** the verdict when ready. |
| **Proactive reminders** | `set_reminder` · `list_reminders` | Minute-resolution scheduler fires due reminders and pushes them via Poke. One-shot or `daily`. |
| **Availability / DND** | `get_status` · `set_status` | `active` / `dnd` / `deep_work`, optional note + `until`. |
| **Recipes** | `list_recipes` · `install_recipe` · `run_recipe` | Saved routines. A recipe is a free-text prompt **and/or** an executable `steps` macro — a JSON array of `{ tool, args }` that `run_recipe` dispatches through the conduit's own tools in order (offline-testable, can't recurse into itself). |
## Architecture
A few small, single-responsibility seams make the whole thing testable offline and deployable with
just Neon:
- **Hand-rolled MCP** (`src/mcp/`, `src/http/core.ts`) — a Streamable-HTTP JSON-RPC handler over Web
`Request`/`Response`. No SDK: the surface Poke speaks is tiny, and hand-rolling keeps it
edge-runtime-compatible and fully unit-testable. (Same choice the `poke-amb-bridge` made.) `POST
/mcp` is rate-limited per identity (Poke user id, else IP) via a `RateLimiter` port
(`src/http/ratelimit.ts`) — in-memory locally, Postgres-backed in prod; a static landing page is
served at `/`, and a browser `GET /mcp` returns friendly server info instead of a 500.
- **`Store` port** (`src/store/`) — one `SqlStore` runs on **pg-mem** (tests / `serve` / demo) and
**Neon** (prod) over an injected `Sql` driver. All timestamps are ISO text so they sort lexically
and round-trip byte-identically between the two.
- **`Model` port** (`src/model/`) — `ClaudeModel` when `ANTHROPIC_API_KEY` is set, else a
deterministic `MockModel` (so tests and the demo need zero credentials).
- **`Step` seam** (`src/durable/step.ts`) — council logic is written once against `Step`. It runs
under `LocalStep` (in-process, memoized) inline within Vercel's 300 s budget by default, or on
**Inngest** for true serverless durability when configured. The async council routes through an
optional `dispatchAsyncCouncil` provider (`src/durable/inngest.ts`): set `INNGEST_EVENT_KEY` (and
`npm install inngest`) and runs survive function freezes with per-step retries; leave it unset and
it transparently falls back to the inline path. Inngest stays out of the 3-dep core — it's loaded
lazily through a non-literal import, so the package is needed only when the feature is switched on.
- **Two credentials, two directions** — *inbound* (Poke → conduit: `Bearer`/`x-poke-key` +
injected `x-poke-user-id`, gated by `MCP_AUTH_ENFORCE`) is distinct from *outbound* (conduit →
Poke: `POKE_API_KEY` to Poke's inbound API, which makes Poke **act on** the message).
## Run it locally
No credentials required — it falls back to pg-mem + MockModel + a mock Poke client.
```bash
npm install
npm test # 129 tests (unit + tool handlers + full MCP wire e2e)
npm run demo # narrated, self-asserting end-to-end walkthrough (22 checks)
npm run serve # local HTTP server: GET / · POST /mcp · GET /cron · GET /healthz
```
The demo runs the **real wire path** (`handleMcp(Request) → Response`) and never sends a real
message. Set `ANTHROPIC_API_KEY` first to watch the council deliberate with real Claude:
```bash
export ANTHROPIC_API_KEY=sk-ant-...
npm run demo
```
Drive the local server by hand:
```bash
npm run serve &
curl -s localhost:7411/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools | length' # 13
```
## Deploy
See **[DEPLOY.md](./DEPLOY.md)** — Vercel (edge functions) + Neon, env vars, the cron schedule
caveat, and how to point Poke at your `/mcp` endpoint.
## Project layout
```
api/ mcp.ts · cron.ts · inngest.ts · healthz.ts · _store.ts (Vercel edge entry points)
public/ index.html (static landing page served at /)
src/
config.ts · ids.ts
store/ types · schema · sql (SqlStore) · pgmem · pg (Neon) · index
model/ types · mock · claude · index
durable/ step.ts (Step · LocalStep · fromInngestStep) · inngest.ts (optional durable provider)
poke/ index.ts (PokeClient · HttpPokeClient · MockPokeClient)
agents/ personas · worker · council · orchestrator
tools/ backlog · reminders · status · recipes · recipe-runner · council · index
mcp/ auth.ts · server.ts (JSON-RPC)
http/ core.ts (Request→Response core) · ratelimit.ts (RateLimiter port)
scheduler.ts · render.ts · serve.ts · demo.ts
test/ store · model · step · council · council-async · scheduler · render · auth · tools · recipe-runner · mcp · ratelimit
docs/superpowers/ specs/ · plans/
```
Built as a finished MVP from a design spec → plan → task-by-task implementation, tests written
immediately after each unit. See `docs/superpowers/` for the spec (with the as-built addendum) and
the implementation plan.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues