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. +

+

CI npm version @@ -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.