diff --git a/AGENTS.md b/AGENTS.md
index ffb66f7..f8458e7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -57,7 +57,7 @@ npx tsc --noEmit -p packages/cli
npm run build -w @openhop/cli
```
-There is **no** CI lint or formatter enforced — don't try to run one.
+CI runs `npm run lint` and `npm run format:check` (see `.github/workflows/ci.yml`). Run them locally before pushing if you're touching a lot of files.
## Tests
@@ -66,7 +66,7 @@ npm test -w @openhop/shared # vitest, fast
npm test -w @openhop/server # vitest, touches the in-memory store
```
-Known pre-existing failures in `packages/shared/__tests__/patch.test.ts` — it references old singular op names (`add-node`) that were renamed to plural (`add-nodes`). Don't waste time on those unless fixing them is your ticket.
+The shared test suite is fully passing. The previous `patch.test.ts` "old singular op names" caveat no longer applies — it has been updated to the plural ops.
## Authoring flows
diff --git a/README.md b/README.md
index 8ef786b..b9491d8 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,15 @@
+
+ Works with
+
+
+
+
+
+
+
---
## Why
@@ -37,7 +46,9 @@ between components on a pixel-art canvas. Click any node to drill into its sub-f
- 🔍 **Multi-level drill-down.** Click a node to zoom into its sub-flow. Infinite depth.
- ⚡ **Live re-render.** `openhop patch` applies incremental changes without a full reload.
- 🧠 **Strict schema + fuzzy typo hints.** Invalid YAML fails loudly with helpful suggestions.
-- 🐚 **CLI + HTTP API + web UI.** Script it, hit it from tools, or browse at .
+- 🐚 **CLI + HTTP API + web UI.** Script it, hit it from tools, or browse at `http://localhost:8788`.
+- 🔒 **Local-first, no telemetry.** Runs entirely on your machine — no analytics, no phone-home, no account required.
+- ✂️ **Token-light.** A typical flow YAML is ~5–20× smaller than the equivalent prose explanation an agent would otherwise emit.
## Try it in 60 seconds
@@ -119,6 +130,7 @@ Pre-made flows under [`examples/`](examples/):
- `order-flow.yaml` — e-commerce order pipeline
- `simple-crud.yaml` — minimal CRUD example
- `type-variants.yaml` — every node type in one flow
+- `self-loops.yaml` — same-node steps (internal work, retries) plus broadcasts and multi-data steps
Push any of them:
diff --git a/docker-compose.yml b/docker-compose.yml
index 0057574..8c10ef7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -14,6 +14,10 @@ services:
- openhop-flows:/root/.openhop
environment:
- CHOKIDAR_USEPOLLING=true
+ # Inside the container we need to bind every interface so the host
+ # port-mapping above can reach the server. Outside docker the server
+ # defaults to 127.0.0.1.
+ - HOST=0.0.0.0
restart: unless-stopped
volumes:
diff --git a/packages/cli/src/help-json.ts b/packages/cli/src/help-json.ts
index 1dbc4b4..5074180 100644
--- a/packages/cli/src/help-json.ts
+++ b/packages/cli/src/help-json.ts
@@ -120,7 +120,13 @@ const COMMAND_META: Record
],
},
init: {
- exitCodes: [ExitCode.SUCCESS, ExitCode.GENERIC, ExitCode.USAGE],
+ exitCodes: [
+ ExitCode.SUCCESS,
+ ExitCode.GENERIC,
+ ExitCode.USAGE,
+ ExitCode.NOT_FOUND,
+ ExitCode.CONFLICT,
+ ],
examples: [
'openhop init',
'openhop init --dry-run --json',
diff --git a/packages/cli/src/init.ts b/packages/cli/src/init.ts
index e565b94..33dcfc7 100644
--- a/packages/cli/src/init.ts
+++ b/packages/cli/src/init.ts
@@ -25,6 +25,8 @@ import { fileURLToPath } from 'node:url'
const EXIT_OK = 0
const EXIT_GENERIC = 1
const EXIT_USAGE = 2
+const EXIT_NOT_FOUND = 4
+const EXIT_CONFLICT = 5
/** A supported AI client and where it expects skills on this machine. */
export interface ClientSpec {
@@ -324,13 +326,18 @@ export function registerInit(program: Command): void {
}
}
- // Exit code policy: success if anything was installed (or, in dry-run,
- // would have been installed). Otherwise generic failure when nothing
- // was actionable.
- const anySuccess = installed.length > 0 || wouldInstall.length > 0
- if (!anySuccess && installed.length === 0 && wouldInstall.length === 0) {
- process.exit(EXIT_GENERIC)
- }
+ // Exit code policy:
+ // CONFLICT (5) — at least one install actually errored.
+ // NOT_FOUND (4) — no clients were detected at all (every result is
+ // `skipped: not detected`); nothing to do.
+ // SUCCESS (0) — anything else, including a converged machine where
+ // every detected client is `already installed`, and
+ // advisory-only detections (e.g. Continue.dev).
+ if (failed.length > 0) process.exit(EXIT_CONFLICT)
+ const detectedCount = results.filter(
+ (r) => !(r.status === 'skipped' && r.reason === 'not detected')
+ ).length
+ if (detectedCount === 0) process.exit(EXIT_NOT_FOUND)
process.exit(EXIT_OK)
})
}
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 80115c0..a6e08d0 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -51,6 +51,9 @@ try {
}
const port = parseInt(process.env.PORT ?? '8787')
-await app.listen({ port, host: '0.0.0.0' })
-console.log(`OpenHop server running on http://localhost:${port}`)
+// Default to loopback so a casual `npm run dev` is not exposed to the LAN.
+// Containers / docker-compose set HOST=0.0.0.0 to bind every interface.
+const host = process.env.HOST ?? '127.0.0.1'
+await app.listen({ port, host })
+console.log(`OpenHop server running on http://localhost:${port} (bound to ${host})`)
console.log(`Swagger docs at http://localhost:${port}/docs`)