zenctl 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # zenctl — OpenCode Zen distributed egress proxy (v2)
2
+
3
+ A single compiled binary, `zenctl`, that spreads OpenCode Zen API traffic across
4
+ a pool of egress nodes. Run one **coordinator** on a public host and any number
5
+ of **nodes** on machines whose IPs you want to use for egress. Requests are
6
+ routed by tier priority (your own node first, then others, then a direct
7
+ fallback), so per-IP rate limits are spread across the pool.
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ cloudflared (TLS, one hostname)
13
+
14
+ OpenCode ──patch baseURL──▶ Coordinator (Bun, one port 9090)
15
+ │ • OpenAI API (/v1/chat/completions, /v1/models)
16
+ │ • Dashboard (GET /) + SSE logs (/api/logs/stream)
17
+ │ • Admin API (/api/admin/*)
18
+ │ • WebSocket for nodes (/ws)
19
+
20
+ ┌─────────────┼─────────────┐
21
+ Node tier 1 Node tier 2 Node tier 3
22
+ │ │ │
23
+ └─────────────┼─────────────┘
24
+
25
+ opencode.ai/zen/v1
26
+ (request egresses from a node's IP)
27
+ ```
28
+
29
+ Everything runs on **one port** (Bun serves HTTP + WebSocket together). Nodes
30
+ connect out over WebSocket, so they work behind NAT. There is no TUI and no
31
+ separate proxy daemon — `zenctl node` is the only process a node machine runs.
32
+
33
+ ## Requirements
34
+
35
+ - [Bun](https://bun.sh) ≥ 1.3
36
+ - MongoDB (optional — the coordinator degrades to in-memory storage if absent)
37
+ - `cloudflared` on the coordinator host for public TLS ingress (optional)
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ # Via npm (requires Bun)
43
+ npm install -g zenctl
44
+
45
+ # Or run directly without installing:
46
+ npx zenctl --help
47
+
48
+ # Or via bun:
49
+ bunx zenctl --help
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ```bash
55
+ # 1. On the coordinator host:
56
+ zenctl coordinator --port 9090
57
+ # → prints a bootstrap admin key (ak_...) when running without MongoDB
58
+
59
+ # 2. Mint a node key using the admin API (or `zenctl keys generate`):
60
+ curl -X POST http://localhost:9090/api/admin/keys \
61
+ -H "Authorization: Bearer ak_..." -H "Content-Type: application/json" \
62
+ -d '{"label":"my-laptop","type":"node"}'
63
+
64
+ # 3. On each node machine:
65
+ zenctl node --server ws://localhost:9090/ws --api-key nk_... --tier 1
66
+
67
+ # 4. On the client machine, point OpenCode at the coordinator:
68
+ zenctl patch --server https://ai.example.com
69
+ ```
70
+
71
+ ## Commands
72
+
73
+ | Command | Purpose | Key flags |
74
+ |---|---|---|
75
+ | `zenctl coordinator` | Start the coordinator server | `--port`, `--config`, `--mongo-uri`, `--no-cloudflared` |
76
+ | `zenctl node` | Start a node client | `--server` (required), `--api-key`, `--tier` (1-3), `--data-dir` |
77
+ | `zenctl patch` | Point OpenCode's `baseURL` at the coordinator | `--server <url>`, `--restore`, `--show` |
78
+ | `zenctl init` | Flag-driven setup (writes `~/.config/zenctl/config.json`) | `--role coordinator\|node`, `--server`, `--api-key`, `--tier`, `--port`, `--mongo-uri`, `--force` |
79
+ | `zenctl keys generate` | Generate a node (`nk_*`) or admin (`ak_*`) key | `--type node\|admin`, `--label` |
80
+ | `zenctl keys list` | List keys (prefix + label only, never the raw key) | |
81
+ | `zenctl keys revoke` | Revoke a key by prefix | `--prefix` |
82
+
83
+ Run any command with `--help` for full details. `zenctl --version` prints the
84
+ version.
85
+
86
+ ## Admin API
87
+
88
+ All `/api/admin/*` endpoints require an `Authorization: Bearer ak_*` admin key
89
+ and are rate-limited to 10 requests/minute per IP.
90
+
91
+ | Method & path | Purpose |
92
+ |---|---|
93
+ | `GET /api/admin/status` | Version, uptime, node count, rate-limited count, MongoDB status |
94
+ | `GET /api/admin/nodes` | Connected nodes (id, name, tier, status, activeRequests, …) |
95
+ | `GET /api/admin/keys` | List keys (prefix, label, type, createdAt, revoked) |
96
+ | `POST /api/admin/keys` | Create a key `{ label, type }` — returns the raw key once |
97
+ | `DELETE /api/admin/keys/:prefix` | Revoke a key (refuses the last active admin key) |
98
+
99
+ Raw keys and hashes are never returned by any list endpoint. A freshly created
100
+ key is the only place the raw value appears, exactly once.
101
+
102
+ ## Configuration
103
+
104
+ Resolution order (later wins): built-in defaults → config file
105
+ (`ZENCTL_CONFIG` path or `~/.config/zenctl/config.json`) → `ZENCTL_*` env vars.
106
+
107
+ Env vars follow `ZENCTL_<SECTION>_<KEY>`, e.g.:
108
+
109
+ | Env var | Overrides |
110
+ |---|---|
111
+ | `ZENCTL_COORDINATOR_PORT` | Coordinator listen port (default 9090) |
112
+ | `ZENCTL_MONGODB_URI` | MongoDB connection string |
113
+ | `ZENCTL_NODE_SERVER_URL` | Node's coordinator WebSocket URL |
114
+ | `ZENCTL_NODE_API_KEY` | Node API key |
115
+ | `ZENCTL_LOGGING_BUFFERSIZE` | In-memory log buffer size (default 1000) |
116
+
117
+ Malformed values fall back to defaults; the loader never throws.
118
+
119
+ ## Deployment
120
+
121
+ - **systemd:** copy [`scripts/opencode-zenctl.service`](./scripts/opencode-zenctl.service)
122
+ (coordinator) or [`scripts/opencode-zenctl-node.service`](./scripts/opencode-zenctl-node.service)
123
+ (node) to `/etc/systemd/system/`, then `systemctl enable --now`.
124
+ - **cloudflared:** see [`scripts/cloudflared-config.yml`](./scripts/cloudflared-config.yml)
125
+ — a single ingress rule routes your hostname to `localhost:9090` (HTTP and
126
+ WebSocket share the port).
127
+
128
+ ## Tests
129
+
130
+ ```bash
131
+ bun test # full v2 suite
132
+ bun run tsc --noEmit # type check
133
+ ```
134
+
package/bin/zenctl.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ // zenctl — Node shim that spawns the Bun runtime to execute the compiled bundle.
3
+ // This allows `npx zenctl` to work on systems with Bun installed.
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname, join } from "node:path";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const entry = join(__dirname, "..", "dist", "zenctl.js");
11
+
12
+ // Try `bun` first, then `bun.exe` on Windows
13
+ const candidates = process.platform === "win32" ? ["bun", "bun.exe"] : ["bun"];
14
+
15
+ async function findBun() {
16
+ for (const name of candidates) {
17
+ try {
18
+ const { execSync } = await import("node:child_process");
19
+ execSync(name + " --version", { stdio: "ignore" });
20
+ return name;
21
+ } catch {
22
+ // not found, try next
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+
28
+ const bun = await findBun();
29
+
30
+ if (!bun) {
31
+ console.error("");
32
+ console.error(" zenctl requires Bun to run.");
33
+ console.error(" Install it: https://bun.sh");
34
+ console.error(" Then try: bunx zenctl");
35
+ console.error("");
36
+ process.exit(1);
37
+ }
38
+
39
+ const child = spawn(bun, ["run", entry, ...process.argv.slice(2)], {
40
+ stdio: "inherit",
41
+ cwd: process.cwd(),
42
+ env: process.env,
43
+ });
44
+
45
+ child.on("exit", (code, signal) => {
46
+ if (signal) {
47
+ process.kill(process.pid, signal);
48
+ } else {
49
+ process.exit(code ?? 0);
50
+ }
51
+ });