diff --git a/README.md b/README.md
index da62432..b2ae048 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,10 @@
Animated diagrams your AI agent can write.
+
+ Local-first. Your code never leaves your machine. No telemetry.
+
+
@@ -30,7 +34,9 @@
Install ·
Use cases ·
How it works ·
- Examples
+ Examples ·
+ Docs ·
+ Discord
---
@@ -88,6 +94,14 @@ npx openhop init
npx openskills install naorsabag/openhop
```
+**Path C — plugin install**
+
+```text
+/plugin install naorsabag/openhop
+```
+
+…or from your agent GUI.
+
**Want to start the server yourself?**
```bash
@@ -107,6 +121,13 @@ git clone https://github.com/naorsabag/openhop.git
cd openhop && npm install && npm run dev
```
+> **OpenHop v0.1 is local-first.**
+> The CLI runs entirely on your machine. There's no hosted backend, no flow
+> storage, no servers we keep running. Sharing today = sharing the YAML file
+> (or the URL-fragment share link from the [hosted playground](https://naorsabag.github.io/openhop/),
+> which compresses the flow into the URL hash — still no server-side
+> storage).
+
## Use cases
Once the skill is installed, point your agent at a codebase and ask it things like:
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..4dfb0ab
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,28 @@
+# OpenHop docs
+
+Reference material for the people who already know what OpenHop is. If you're
+new, start with the [project README](../README.md) and `npx openhop demo`.
+
+## Contents
+
+| Doc | What's in it |
+| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [install.md](install.md) | Every install path — `npx openhop init`, OpenSkills, the Claude Code `/plugin` command, and the manual file-drop layout for each supported client. |
+| [yaml.md](yaml.md) | YAML schema reference. Every step kind, every data shape, every keyword the skill is allowed to emit. |
+| [cli.md](cli.md) | Full CLI reference — every command, every flag, every exit code. |
+| [architecture.md](architecture.md) | What runs where, why it's local-first, and the wire shape between the agent, the CLI, the API, and the web renderer. |
+
+The animated playground lives at . It
+loads the same renderer used locally — same sprites, same animation, same
+inspector — but stores nothing server-side (the flow is encoded into the URL
+hash). Good for sharing a snapshot without anyone installing anything.
+
+## Conventions
+
+- Commands prefixed with `$` are run by you. Output below is what you'd see.
+- `npx openhop …` and `openhop …` are interchangeable once the package is
+ installed globally or as a dev dependency. The docs use `npx` because it
+ works without any prior install.
+- Anything labeled **agent-facing** is what your AI coding agent emits or
+ consumes — you generally don't write it by hand. The source of truth for
+ the agent's contract is [`skills/openhop/SKILL.md`](../skills/openhop/SKILL.md).
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..c0d99de
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,90 @@
+# Architecture
+
+What runs where, and why "local-first" is the load-bearing word in the README.
+
+## The three packages
+
+| Package | Where it runs | What it does |
+| ----------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| `openhop` (CLI) | your laptop | Validates YAML, talks HTTP to the API. Entry point for `init` / `demo` / `serve` / `push` / `patch` / `list` / `remove`. |
+| `@openhop/server` | your laptop | Fastify API on `:8787`. Owns flow storage (in-memory plus a JSON-on-disk fallback at `~/.openhop/`). Exposes REST routes + Swagger at `/docs`. |
+| `@openhop/web` | your browser | PIXI-rendered web UI on `:8788`. Subscribes to flow updates, runs the pixel-art animation. |
+
+## Wire shape
+
+```
+┌─────────┐ prompt ┌───────┐ YAML ┌────────┐
+│ user │ ────────────▶ │ agent │ ────────────▶ │ CLI │
+└─────────┘ └───────┘ └────┬───┘
+ ▲ │ HTTP POST /flows
+ │ animated URL ▼
+ │ return-trip ┌────────┐
+ └───────────────────── │ API │
+ └────┬───┘
+ │ broadcast
+ ▼
+ ┌────────┐
+ │ web UI │
+ └────────┘
+ ▲
+ │ open in browser
+ ┌────────┐
+ │browser │
+ └────────┘
+```
+
+The skill at `skills/openhop/SKILL.md` is the agent's contract. It tells the
+agent how to recognize the trigger phrases, how to draft the YAML, and what
+the CLI's surface looks like.
+
+## Local-first, on purpose
+
+- **No hosted backend.** There is no openhop.dev API server you can point at.
+ Even the [Pages playground](https://naorsabag.github.io/openhop/) runs the
+ same web bundle locally in your browser — the flow lives in the URL hash,
+ not on a server we keep running.
+- **No telemetry, no analytics, no phone-home.** The CLI never makes a
+ network request beyond your configured server URL (`--server`, defaults
+ `http://localhost:8787`).
+- **No account.** No login, no signup, no API key.
+- **No flow storage outside your machine.** Flows go to `~/.openhop/` and
+ the in-memory store of your local API. `openhop remove` actually deletes.
+
+## Sharing without a server
+
+Two ways to share a flow without anyone hosting one:
+
+1. **YAML file.** Open a PR, paste into a chat, attach to an issue. Anyone
+ with `openhop push ` (or the demo) can render it.
+2. **URL-fragment share link** from the Pages playground. The YAML is
+ lz-compressed into the URL's hash (after `#…`). The hash never hits a
+ server — Pages just serves the static bundle, and the bundle decodes the
+ hash in the browser. The URL is the entire payload.
+
+If we ever ship a hosted, shareable backend, it'll be **opt-in** and never
+the default.
+
+## Component map
+
+```
+packages/
+├── shared/ # zod schema, parseFlowYaml(), exit codes
+├── server/ # Fastify app, in-memory + disk store
+├── web/ # PIXI renderer, React Flow canvas, two app shells
+│ ├── App.tsx # API-backed mode (npx openhop demo / serve)
+│ └── AppFragment.tsx # hash-only mode (GitHub Pages deploy)
+└── cli/ # commander.js entry, talks to the API
+skills/
+└── openhop/
+ └── SKILL.md # agent-facing contract
+```
+
+## What "local-first" doesn't mean
+
+- It's not E2E-encrypted — anything you give the API is sent in plaintext to
+ `http://localhost:…`. If you `--server` to a remote host, that host sees
+ everything.
+- It's not offline-first — the agent still calls its LLM provider over the
+ internet. We just don't add a round-trip on top.
+- It's not sandboxed — the CLI runs with your user permissions. Don't pipe
+ untrusted YAML through `push` if you don't trust the source.
diff --git a/docs/cli.md b/docs/cli.md
new file mode 100644
index 0000000..c79bd3e
--- /dev/null
+++ b/docs/cli.md
@@ -0,0 +1,153 @@
+# CLI reference
+
+Every command, every flag. The source of truth is `packages/cli/src/index.ts`.
+
+All commands share the same data plane: read from stdin or a path, post to a
+local Fastify server, get a flow id back. Output discipline: **data on stdout,
+logs on stderr**, `--json` for machine-readable summaries.
+
+## `openhop serve`
+
+Start the API server (`:8787`) and the web UI (`:8788`).
+
+```bash
+openhop serve # both API + web
+openhop serve --no-web # API only (headless agents, CI)
+openhop serve -p 9000 # override API port
+openhop serve --web-port 9001 # override web port
+```
+
+| Flag | Default | Description |
+| ------------------- | ------- | ------------------------------------------------------------------------ |
+| `-p, --port ` | `8787` | API port. |
+| `--web-port ` | `8788` | Web UI port. |
+| `--no-web` | — | Start API only; skip the web UI. |
+| `--no-wait-ready` | — | Don't print the machine-parseable `openhop: ready api=…` line on stdout. |
+
+When both come up, the ready line is:
+
+```
+openhop: ready api=http://0.0.0.0:8787 web=http://0.0.0.0:8788 elapsed=0s
+```
+
+Agents block on that line to know when to start pushing.
+
+## `openhop demo`
+
+Zero-config bootstrap. Boots API + web in-process, posts a starter
+authentication flow, opens your browser at the rendered URL, stays running
+until Ctrl-C.
+
+```bash
+openhop demo # API on :8787, web on :8788, browser opens
+openhop demo --no-open # print the URL only
+openhop demo -p 9000 --web-port 9001 # custom ports
+```
+
+Substitute for the hosted playground when you'd rather see it run locally.
+
+## `openhop init`
+
+Install the OpenHop skill into every detected AI client on this machine. See
+[install.md](install.md) for the per-client paths.
+
+```bash
+openhop init # all detected clients
+openhop init --dry-run # plan only, write nothing
+openhop init --force # overwrite existing skills
+openhop init --client claude-code # one client only
+openhop init --json # JSON summary on stdout
+```
+
+| Flag | Description |
+| --------------- | ---------------------------------------------------------------- |
+| `--dry-run` | Print what would be written; don't touch disk. |
+| `--force` | Overwrite an existing skill instead of skipping. |
+| `--client ` | One of `claude-code`, `cursor`, `windsurf`, `cline`, `continue`. |
+| `--json` | Machine-readable summary. |
+
+Exit codes:
+
+| Code | Meaning |
+| ---- | -------------------------------------------------------------------------------------- |
+| 0 | At least one client was processed (installed, skipped already-installed, or advisory). |
+| 4 | No clients detected — nothing to do. |
+| 5 | At least one install failed. |
+
+## `openhop push `
+
+Push a YAML flow to the server. Returns the flow id + render URL.
+
+```bash
+openhop push examples/order-flow.yaml
+openhop push - < ./flow.yaml # stdin
+openhop push --json examples/order-flow.yaml
+```
+
+| Flag | Default | Description |
+| -------------------- | ----------------------- | -------------------- |
+| `-s, --server ` | `http://localhost:8787` | Server URL. |
+| `--json` | — | Emit JSON on stdout. |
+
+Stdout format (text mode): `https://localhost:8788/flow/`. Stderr carries
+validation errors / fuzzy-typo hints when the YAML is invalid (exit 2).
+
+## `openhop patch `
+
+Apply patch operations to an existing flow. Same input shape as the
+agent-facing patch contract (`add-nodes`, `remove-nodes`, `add-steps`, etc.) —
+see [yaml.md](yaml.md) and the SKILL for the full list.
+
+```bash
+openhop patch f_a8b3 ./patch.yaml
+openhop patch f_a8b3 --json ./patch.yaml
+```
+
+| Flag | Default | Description |
+| -------------------- | ----------------------- | -------------------- |
+| `-s, --server ` | `http://localhost:8787` | Server URL. |
+| `--json` | — | Emit JSON on stdout. |
+
+## `openhop list`
+
+List flows on the server.
+
+```bash
+openhop list # flat table
+openhop list --tree # path-based hierarchy
+openhop list --search auth # substring + fuzzy search
+openhop list --search auth --limit 10 # cap results
+openhop list --json
+```
+
+| Flag | Default | Description |
+| -------------------- | ----------------------- | ------------------------------------------------------------ |
+| `-s, --server ` | `http://localhost:8787` | Server URL. |
+| `--tree` | — | Render as a directory tree instead of a flat table. |
+| `--search ` | — | Substring + fuzzy match across title, path, description, id. |
+| `--limit ` | `50` | Max search results. |
+| `--json` | — | Machine-readable summary. |
+
+## `openhop remove `
+
+Delete a flow.
+
+```bash
+openhop remove f_a8b3
+openhop remove f_a8b3 --json
+```
+
+| Flag | Default | Description |
+| -------------------- | ----------------------- | -------------------- |
+| `-s, --server ` | `http://localhost:8787` | Server URL. |
+| `--json` | — | Emit JSON on stdout. |
+
+## `openhop --version` / `openhop --api-version`
+
+```bash
+openhop --version # CLI semver
+openhop --api-version # OpenHop YAML schema version (independent of CLI semver)
+```
+
+Agents read `--api-version` to decide whether their generated YAML targets a
+schema this CLI supports.
diff --git a/docs/install.md b/docs/install.md
new file mode 100644
index 0000000..7e9dabf
--- /dev/null
+++ b/docs/install.md
@@ -0,0 +1,72 @@
+# Install
+
+Three install paths, picked by which AI client you use.
+
+## Path A — `npx openhop init` (auto-detect)
+
+The shortest happy path. Auto-detects every Tier-1 AI client on your machine
+and drops the `SKILL.md` into the right place.
+
+```bash
+npx openhop init
+```
+
+Supported by `init`:
+
+| Client | Skills directory | Notes |
+| ------------------ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| Claude Code | `~/.claude/skills/openhop/` | Native. |
+| Cursor (v2.4+) | `~/.cursor/skills/openhop/` | Native. Also auto-discovers `~/.agents/skills/`. |
+| Windsurf (Cascade) | `~/.codeium/windsurf/skills/openhop/` | Native. |
+| Cline (3.48+) | `~/.cline/skills/openhop/` | Requires one-time toggle: **VS Code → Settings → Cline → Features → Enable Skills (experimental)**. |
+| Continue.dev | (advisory) | No native skills surface; the rules system at `~/.continue/rules/` is too small for a full `SKILL.md`. Tracked for a condensed-rule translator. |
+
+Flags: see [cli.md#openhop-init](cli.md#openhop-init).
+
+## Path B — OpenSkills (cross-vendor universal installer)
+
+For any client `npx openhop init` doesn't know about.
+
+```bash
+npx openskills install naorsabag/openhop
+```
+
+[OpenSkills](https://github.com/numman-ali/openskills) knows every client's
+skills directory and drops the file in the right place. Covers Codex CLI,
+Gemini CLI, Junie, GitHub Copilot, OpenCode, Goose, Antigravity, and more.
+
+After OpenSkills places the skill, also run `npx openhop init` so the CLI
+machinery the skill calls into is installed locally. `init` skips the skill
+copy if OpenSkills already wrote it.
+
+## Path C — plugin install
+
+```text
+/plugin install naorsabag/openhop
+```
+
+…or from your agent GUI.
+
+## Manual / advisory clients
+
+For clients with no published skills directory (or where the convention is
+still in flux), the install is "drop `SKILL.md` somewhere the agent reads
+from and re-launch". The directories above are the conventions we ship to.
+
+If you've installed OpenHop somewhere not listed here and it works, open a
+PR adding the client to [`packages/cli/src/init.ts:buildClients`](../packages/cli/src/init.ts).
+
+## What gets installed
+
+Just `skills/openhop/SKILL.md` (plus its small assets). The CLI + server +
+web renderer ship as the same `openhop` npm package and the agent boots them
+on demand the first time you ask for a flow — they don't need to be
+pre-installed.
+
+To pre-install everything:
+
+```bash
+npm install -g openhop
+```
+
+…but `npx` works without it.
diff --git a/docs/yaml.md b/docs/yaml.md
new file mode 100644
index 0000000..dae8067
--- /dev/null
+++ b/docs/yaml.md
@@ -0,0 +1,140 @@
+# YAML schema reference
+
+The full schema your agent emits (and you can write by hand). Lifted from the
+zod schema in `packages/shared` and the agent-facing reference in
+[`skills/openhop/SKILL.md`](../skills/openhop/SKILL.md). When the two
+disagree, **the zod schema wins** — that's what validates on `push` / `patch`.
+
+## Root
+
+```yaml
+meta:
+ title:
+ description:
+ path:
+flow:
+ nodes:
+ steps:
+```
+
+## Node
+
+| Field | Required | Notes |
+| ------- | -------- | --------------------------------------------------------------------------------------------------- |
+| `id` | yes | alphanumeric + `-` + `_` only. Referenced from `steps` by id. |
+| `label` | yes | display name, **≤ 4 words** so it fits the fixed-width label slot. Longer labels truncate with `…`. |
+| `type` | no | **closed enum** — see table below. Defaults to `service` if omitted. |
+| `icon` | no | Iconify icon ID (e.g. `logos:postgresql`). Browse at . |
+| `color` | no | hex color override (e.g. `#336791`). |
+| `flow` | no | nested sub-flow `{ nodes, steps }`. Makes the node expandable — click to drill in. |
+
+### `type` is a category, `label` is the name
+
+The 12 type values pick the sprite + accent the renderer draws (database = barrel, cache = lightning, queue = stack, etc.). **Never** put a variant name (`redis`, `stripe`, `postgres`) in `type` — that's a label.
+
+| `type` | Common labels |
+| ----------- | ------------------------------------------------------------ |
+| `actor` | user, admin, customer, operator, agent, bot, service-account |
+| `endpoint` | rest-api, graphql, grpc, webhook, websocket, sse |
+| `auth` | oauth, jwt, session, api-key, saml, ldap, mfa |
+| `database` | postgres, mysql, mongodb, sqlite, cassandra, dynamodb |
+| `external` | stripe, twilio, sendgrid, github, slack, openai, anthropic |
+| `cache` | redis, memcached, cdn, ram, http-cache |
+| `queue` | kafka, rabbitmq, sqs, pubsub, nats, kinesis, celery |
+| `service` | microservice, worker, gateway, proxy, loadbalancer |
+| `docker` | container, sidecar, init-container, compose-service |
+| `k8s` | pod, deployment, statefulset, daemonset, job, cronjob |
+| `scheduler` | cron, airflow, temporal, celery-beat, sidekiq |
+| `custom` | anything — also set `icon` + `color` |
+
+Anything outside that list fails validation. When nothing fits, use `custom`.
+
+## Step
+
+Each item in `flow.steps` is exactly one of four kinds.
+
+### Move
+
+```yaml
+- from:
+ to:
+ data:
+ drilldown:
+```
+
+### Parallel
+
+Two or more move steps that fire concurrently — pixels travel at the same
+time. Use when transfers logically happen together (orchestrator fans out;
+multiple upstream nodes deliver to one target).
+
+```yaml
+- parallel:
+ - from: api
+ to: order-service
+ data: order payload
+ - from: authz
+ to: order-service
+ data: auth context
+```
+
+### Create
+
+Spawn a new node mid-flow (e.g. a temporary worker, an audit logger). The
+created node enters the canvas with a fade-in animation.
+
+```yaml
+- create: audit
+ from: order-service
+ node: { id: audit, label: Audit Log, type: service }
+ data: log event
+```
+
+### Destroy
+
+Remove a previously-created node from the canvas (it fades out and edges to
+it are hidden).
+
+```yaml
+- destroy: audit
+```
+
+## Data
+
+A step's `data` is either a plain string (sketch mode) or an object (detail mode). Never a list at the top level — a list inside an object is fine.
+
+```yaml
+# String — quickest form. Renders as the carrot's tooltip label.
+data: "HTTP Request"
+
+# Object — adds inspectable detail in the right-hand panel.
+data:
+ label: Order payload # required when using object form
+ color: "#4aff7a" # optional pixel color override
+ fields: # optional — shown in tooltip + inspector
+ - name: items
+ type: "list[OrderItem]"
+ - name: total
+ type: float
+ added: true # green highlight (new field)
+ - name: old_field
+ removed: true # red strikethrough
+ - name: amount
+ changed: true # yellow highlight (modified)
+
+# Array of objects — multiple data items sent on the same step.
+data:
+ - label: request body
+ fields: [{ name: items, type: "list[Item]" }]
+ - label: auth context
+ fields: [{ name: user_id, type: int }]
+```
+
+## Common mistakes
+
+- **`type: redis`** — `redis` is a label, not a category. Use `type: cache`, `label: Redis`.
+- **`data: [...]` with strings inside** — array form is for objects only. For one string, just `data: "thing"`.
+- **`type: transform` / `type: validation`** — not in the enum. Use `service` or `custom`.
+- **5-word labels** — `"User Authentication Service Layer"` truncates. Tighten to `"Auth Layer"`.
+
+See [`examples/`](../examples) for full working flows of every common shape.