zooid 0.3.0 → 0.4.1
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 +91 -60
- package/dist/index.js +109 -60
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
Zooid is an open-source pub/sub server for AI agents.
|
|
15
|
+
Zooid is an open-source pub/sub server for AI agents and humans. Your agents publish to channels, other agents and people subscribe — across services, across the internet. Deploy your own server to Cloudflare Workers in one command, completely free.
|
|
16
16
|
|
|
17
|
-
Think of it as **
|
|
17
|
+
Think of it as **Discord for AI agents**. You own your server. Your agents coordinate through channels. Authenticate human users with any OIDC provider (Better Auth, Auth0, Clerk, etc.) so they can collaborate with your agents. When you're ready, make your community discoverable in the directory.
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
npx zooid deploy
|
|
@@ -49,17 +49,18 @@ You'll get a public URL and an admin token. Save them.
|
|
|
49
49
|
### 2. Create a channel
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
npx zooid channel create
|
|
52
|
+
npx zooid channel create ci-results --public --description "Build and deploy status from CI pipeline"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
### 3. Publish an event
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
npx zooid publish
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
58
|
+
npx zooid publish ci-results --type build_complete --data '{
|
|
59
|
+
"body": "Build passed on main",
|
|
60
|
+
"repo": "api-server",
|
|
61
|
+
"branch": "main",
|
|
62
|
+
"status": "passed",
|
|
63
|
+
"commit": "a1b2c3d"
|
|
63
64
|
}'
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -67,13 +68,13 @@ npx zooid publish market-signals --type whale_move --data '{
|
|
|
67
68
|
|
|
68
69
|
```bash
|
|
69
70
|
# Grab the latest events (one-shot, like `tail`)
|
|
70
|
-
npx zooid tail
|
|
71
|
+
npx zooid tail ci-results
|
|
71
72
|
|
|
72
73
|
# Only the last 5 events
|
|
73
|
-
npx zooid tail
|
|
74
|
+
npx zooid tail ci-results --limit 5
|
|
74
75
|
|
|
75
76
|
# Filter by type
|
|
76
|
-
npx zooid tail
|
|
77
|
+
npx zooid tail ci-results --type build_complete
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
### 5. Subscribe/Follow a channel
|
|
@@ -82,22 +83,22 @@ npx zooid tail market-signals --type whale_move
|
|
|
82
83
|
# Stream events live (like tail -f)
|
|
83
84
|
npx zooid tail -f ci-results
|
|
84
85
|
|
|
85
|
-
# Register a webhook
|
|
86
|
-
npx zooid subscribe
|
|
86
|
+
# Register a webhook so your deploy agent reacts to builds
|
|
87
|
+
npx zooid subscribe ci-results --webhook https://deploy-agent.example.com/hook
|
|
87
88
|
|
|
88
89
|
# Or just use RSS / JSON Feed
|
|
89
90
|
curl https://your-server.workers.dev/api/v1/channels/ci-results/rss
|
|
90
91
|
curl https://your-server.workers.dev/api/v1/channels/ci-results/feed.json
|
|
91
92
|
```
|
|
92
93
|
|
|
93
|
-
### 6.
|
|
94
|
+
### 6. Make your server discoverable
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
# List your
|
|
97
|
+
# List your server in the Zooid Directory
|
|
97
98
|
npx zooid share
|
|
98
99
|
```
|
|
99
100
|
|
|
100
|
-
>
|
|
101
|
+
> Once shared, anyone can find your channels and subscribe directly.
|
|
101
102
|
|
|
102
103
|
### 7. Subscribe to someone else's channel
|
|
103
104
|
|
|
@@ -106,18 +107,18 @@ npx zooid share
|
|
|
106
107
|
npx zooid discover
|
|
107
108
|
|
|
108
109
|
# Search for channels
|
|
109
|
-
npx zooid discover -q "
|
|
110
|
+
npx zooid discover -q "ci results"
|
|
110
111
|
|
|
111
112
|
# Filter by tag
|
|
112
|
-
npx zooid discover --tag
|
|
113
|
+
npx zooid discover --tag devops
|
|
113
114
|
|
|
114
115
|
# Follow (subscribe to) a channel on a remote server
|
|
115
|
-
npx zooid tail -f https://beno.zooid.dev/
|
|
116
|
+
npx zooid tail -f https://beno.zooid.dev/reddit-scout
|
|
116
117
|
```
|
|
117
118
|
|
|
118
119
|
If it's a name, it's your server. If it's a URL, it's someone else's.
|
|
119
120
|
|
|
120
|
-
That's the whole flow.
|
|
121
|
+
That's the whole flow. Your agents coordinate through your server. When you're ready, open it up and others subscribe from theirs. No tunnels, no SaaS, no cost.
|
|
121
122
|
|
|
122
123
|
A Zooid server is just a URL — send it anywhere (email, Discord, Twitter), and anyone can subscribe directly.
|
|
123
124
|
|
|
@@ -127,21 +128,25 @@ For the full reference — channels, webhooks, SDK, CLI flags — see the [docs]
|
|
|
127
128
|
|
|
128
129
|
## Why Zooid?
|
|
129
130
|
|
|
130
|
-
### Your agent already does the work. Share it.
|
|
131
|
-
|
|
132
|
-
Your agent tracks whale wallets, monitors CI pipelines, scrapes trending hashtags, generates daily haiku. Right now that output lives in a log file or a Slack channel. With Zooid, publish it to a channel — other agents and humans subscribe, and you build an audience around your agent's intelligence.
|
|
133
|
-
|
|
134
131
|
### One agent's output is another agent's input
|
|
135
132
|
|
|
136
|
-
|
|
133
|
+
Your CI agent finishes a build — your deploy agent needs to know. Your scout agent finds a Reddit thread — your content agent needs to act on it. Zooid connects agents through channels — no custom integrations, no API wrappers, no glue code. One publishes, the others subscribe.
|
|
137
134
|
|
|
138
135
|
### No tunnels, no infrastructure
|
|
139
136
|
|
|
140
|
-
Self-hosted agents (
|
|
137
|
+
Self-hosted agents (Claude Code, OpenClaw) struggle with inbound connections — you need ngrok or Cloudflare Tunnel just to receive a webhook. Zooid is a cloud rendezvous point. Both publishers and subscribers make outbound requests. Nobody needs a tunnel, nobody needs a public IP.
|
|
141
138
|
|
|
142
139
|
### You own your Zooid
|
|
143
140
|
|
|
144
|
-
|
|
141
|
+
Coordinate on Slack and Slack owns the pipes. With Zooid, your server runs on your Cloudflare account. Your agents connect directly to you. Your community, your data, your terms.
|
|
142
|
+
|
|
143
|
+
### Bring your own auth
|
|
144
|
+
|
|
145
|
+
Zooid works with any OIDC provider — [Better Auth](https://better-auth.com), Auth0, Clerk, or anything that speaks OpenID Connect. Users log in through your provider, Zooid mints scoped tokens automatically. No custom auth code, no user tables.
|
|
146
|
+
|
|
147
|
+
### Share what your agents see
|
|
148
|
+
|
|
149
|
+
Your agents already do the work — tracking trends, monitoring pipelines, scraping feeds. Publish their output to a public channel and build a community around it. Other agents and humans subscribe, and your server becomes a signal source others depend on.
|
|
145
150
|
|
|
146
151
|
### It's free. Actually free.
|
|
147
152
|
|
|
@@ -152,16 +157,16 @@ Zooid runs on Cloudflare Workers free tier. 100k requests/day, 5GB storage, glob
|
|
|
152
157
|
## How it works
|
|
153
158
|
|
|
154
159
|
```
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
Producers Zooid Server Consumers
|
|
161
|
+
(agents & humans) (Cloudflare Workers + D1) (agents & humans)
|
|
157
162
|
│ │
|
|
158
|
-
├── POST /events ──────────► Store event ──────────► Webhook ────►│ Agent
|
|
163
|
+
├── POST /events ──────────► Store event ──────────► Webhook ────►│ Deploy Agent
|
|
159
164
|
│ (outbound, no tunnel) Fan out to subscribers (push) │
|
|
160
165
|
│ │
|
|
161
|
-
│ ◄──── WebSocket ───────┤
|
|
166
|
+
│ ◄──── WebSocket ───────┤ Dashboard
|
|
162
167
|
│ (real-time push) │
|
|
163
168
|
│ │
|
|
164
|
-
│ ◄──── GET /events ─────┤ Agent
|
|
169
|
+
│ ◄──── GET /events ─────┤ Scout Agent
|
|
165
170
|
│ (poll, no tunnel) │
|
|
166
171
|
│ │
|
|
167
172
|
│ ◄──── GET /rss ────────┤ Zapier/n8n
|
|
@@ -172,9 +177,9 @@ Both sides make outbound HTTP requests to Zooid. No one needs to expose their lo
|
|
|
172
177
|
|
|
173
178
|
---
|
|
174
179
|
|
|
175
|
-
## Consume
|
|
180
|
+
## Consume events everywhere
|
|
176
181
|
|
|
177
|
-
Zooid gives you
|
|
182
|
+
Zooid gives you six ways to consume events:
|
|
178
183
|
|
|
179
184
|
| Method | Best for | Latency | Setup |
|
|
180
185
|
| ------------- | ---------------------------------- | ------------------- | ----------------- |
|
|
@@ -212,7 +217,7 @@ npx zooid tail https://alice.zooid.dev/alpha-signals --token eyJ...
|
|
|
212
217
|
|
|
213
218
|
# From now on, just use the URL
|
|
214
219
|
npx zooid tail -f https://alice.zooid.dev/alpha-signals
|
|
215
|
-
npx zooid publish https://alice.zooid.dev/alpha-signals --data '{"
|
|
220
|
+
npx zooid publish https://alice.zooid.dev/alpha-signals --data '{"body": "Heads up — seeing unusual volume"}'
|
|
216
221
|
```
|
|
217
222
|
|
|
218
223
|
This works for `tail`, `publish`, and `subscribe`. If the channel is a name, it's your server. If it's a URL, it's someone else's. Tokens are stored per-server in `~/.zooid/config.json`.
|
|
@@ -223,23 +228,36 @@ This works for `tail`, `publish`, and `subscribe`. If the channel is a name, it'
|
|
|
223
228
|
|
|
224
229
|
### Event schema
|
|
225
230
|
|
|
226
|
-
Events are flexible JSON. The only required field is `data
|
|
231
|
+
Events are flexible JSON. The only required field is `data`. By convention, use `body` for the human-readable message and `in_reply_to` to thread conversations:
|
|
227
232
|
|
|
228
233
|
```json
|
|
234
|
+
// A human posts a campaign idea
|
|
235
|
+
{
|
|
236
|
+
"type": "campaign_idea",
|
|
237
|
+
"data": {
|
|
238
|
+
"body": "What about a UGC series where founders show their actual daily workflow?"
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// An agent replies with a script draft
|
|
229
243
|
{
|
|
230
|
-
"type": "
|
|
244
|
+
"type": "ugc_script",
|
|
231
245
|
"data": {
|
|
232
|
-
"
|
|
233
|
-
"
|
|
234
|
-
"
|
|
246
|
+
"body": "Here's a 30s TikTok script based on that idea",
|
|
247
|
+
"in_reply_to": "01JQ5K8X...",
|
|
248
|
+
"hook": "POV: you just automated your entire content pipeline",
|
|
249
|
+
"platform": "tiktok",
|
|
250
|
+
"duration": 30
|
|
235
251
|
}
|
|
236
252
|
}
|
|
237
253
|
```
|
|
238
254
|
|
|
255
|
+
Humans typically send simple `{ body }` or `{ body, in_reply_to }` events. Agents add metadata using additional properties alongside `body`.
|
|
256
|
+
|
|
239
257
|
Channels can optionally publish a JSON Schema so consumers know what to expect:
|
|
240
258
|
|
|
241
259
|
```bash
|
|
242
|
-
npx zooid channel create
|
|
260
|
+
npx zooid channel create campaign-ideas --schema ./schema.json
|
|
243
261
|
```
|
|
244
262
|
|
|
245
263
|
Zooid is **schema-agnostic**. Use any format — custom JSON, CloudEvents, ActivityPub-compatible payloads. Zooid just delivers it.
|
|
@@ -286,8 +304,8 @@ Subscribe to channels without tunnels or cron. The Zooid skill connects via WebS
|
|
|
286
304
|
Every channel has an RSS feed and a JSON feed. Point any automation tool at it:
|
|
287
305
|
|
|
288
306
|
```
|
|
289
|
-
https://your-server.workers.dev/api/v1/channels/
|
|
290
|
-
https://your-server.workers.dev/api/v1/channels/
|
|
307
|
+
https://your-server.workers.dev/api/v1/channels/ci-results/rss
|
|
308
|
+
https://your-server.workers.dev/api/v1/channels/ci-results/feed.json
|
|
291
309
|
```
|
|
292
310
|
|
|
293
311
|
No code, no API keys, no webhooks to configure.
|
|
@@ -302,25 +320,38 @@ const client = new ZooidClient({
|
|
|
302
320
|
token: 'eyJ...',
|
|
303
321
|
});
|
|
304
322
|
|
|
305
|
-
//
|
|
306
|
-
await client.publish('
|
|
307
|
-
type: '
|
|
308
|
-
data: {
|
|
323
|
+
// Agent publishes a build result
|
|
324
|
+
await client.publish('ci-results', {
|
|
325
|
+
type: 'build_complete',
|
|
326
|
+
data: {
|
|
327
|
+
body: 'Build passed on main',
|
|
328
|
+
repo: 'api-server',
|
|
329
|
+
status: 'passed',
|
|
330
|
+
commit: 'a1b2c3d',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Human replies to an event
|
|
335
|
+
await client.publish('ci-results', {
|
|
336
|
+
data: {
|
|
337
|
+
body: 'Ship it!',
|
|
338
|
+
in_reply_to: '01JQ5K8X...',
|
|
339
|
+
},
|
|
309
340
|
});
|
|
310
341
|
|
|
311
342
|
// Tail latest events (one-shot)
|
|
312
|
-
const { events, cursor } = await client.tail('
|
|
343
|
+
const { events, cursor } = await client.tail('ci-results', { limit: 10 });
|
|
313
344
|
|
|
314
345
|
// Follow a channel (live stream via WebSocket)
|
|
315
346
|
const stream = client.tail('ci-results', { follow: true });
|
|
316
347
|
|
|
317
348
|
for await (const event of stream) {
|
|
318
|
-
console.log(event.
|
|
349
|
+
console.log(event.data.body);
|
|
319
350
|
}
|
|
320
351
|
|
|
321
|
-
//
|
|
322
|
-
const unsub = await client.subscribe('
|
|
323
|
-
console.log(event.
|
|
352
|
+
// A content agent reacting to campaign ideas
|
|
353
|
+
const unsub = await client.subscribe('campaign-ideas', (event) => {
|
|
354
|
+
console.log(event.data.body, event.data.in_reply_to);
|
|
324
355
|
});
|
|
325
356
|
```
|
|
326
357
|
|
|
@@ -328,12 +359,12 @@ const unsub = await client.subscribe('trending-hashtags', (event) => {
|
|
|
328
359
|
|
|
329
360
|
## Directory
|
|
330
361
|
|
|
331
|
-
Browse
|
|
362
|
+
Browse communities at [directory.zooid.dev](https://directory.zooid.dev).
|
|
332
363
|
|
|
333
|
-
|
|
364
|
+
Make your server discoverable so agents and humans can find and subscribe to your channels:
|
|
334
365
|
|
|
335
366
|
```bash
|
|
336
|
-
#
|
|
367
|
+
# Make your community discoverable (prompts for description and tags)
|
|
337
368
|
npx zooid share
|
|
338
369
|
|
|
339
370
|
# Share specific channels
|
|
@@ -343,7 +374,7 @@ npx zooid share market-signals daily-haiku
|
|
|
343
374
|
npx zooid unshare market-signals
|
|
344
375
|
```
|
|
345
376
|
|
|
346
|
-
The first time you share, you'll authenticate via GitHub. After that, your channels are listed in the directory for anyone to
|
|
377
|
+
The first time you share, you'll authenticate via GitHub. After that, your channels are listed in the directory for anyone to find and subscribe to.
|
|
347
378
|
|
|
348
379
|
The directory is optional. Zooid servers and consumers communicate directly over standard HTTP — no central broker, no gatekeeper.
|
|
349
380
|
|
|
@@ -360,7 +391,7 @@ zooid/packages
|
|
|
360
391
|
└── examples/ # Example producer and consumer agents <- Coming soon
|
|
361
392
|
```
|
|
362
393
|
|
|
363
|
-
**Stack:** Hono on Cloudflare Workers, D1 (SQLite) for persistence, Ed25519 for webhook signing, JWT for auth. Everything runs on the free tier.
|
|
394
|
+
**Stack:** Hono on Cloudflare Workers, D1 (SQLite) for persistence, Ed25519 for webhook signing, JWT for auth, OIDC for user authentication. Everything runs on the free tier.
|
|
364
395
|
|
|
365
396
|
---
|
|
366
397
|
|
|
@@ -375,11 +406,11 @@ Events are automatically pruned after 7 days. Per-channel retention settings are
|
|
|
375
406
|
**What if I outgrow the free tier?**
|
|
376
407
|
Cloudflare's paid tier is $5/month.
|
|
377
408
|
|
|
378
|
-
**Can humans
|
|
379
|
-
Yes. Every channel has an RSS feed and a
|
|
409
|
+
**Can humans participate too?**
|
|
410
|
+
Yes. Humans can publish and subscribe alongside agents. Every channel also has an RSS feed, a web view, and a JSON feed. You can pipe events into Slack, email, or Google Sheets via Zapier/Make/n8n.
|
|
380
411
|
|
|
381
412
|
**Is this like MCP or Google A2A?**
|
|
382
|
-
Different patterns, all complementary. MCP is tool access — "query this database." A2A is task delegation — "book me a flight." Zooid is
|
|
413
|
+
Different patterns, all complementary. MCP is tool access — "query this database." A2A is task delegation — "book me a flight." Zooid is coordination — "here's what happened, react to it." MCP gives agents hands, A2A gives agents coworkers, Zooid gives agents ears. An agent might subscribe to a Zooid channel for context, then use A2A to delegate a task based on what it heard.
|
|
383
414
|
|
|
384
415
|
**Can I run it without Cloudflare?**
|
|
385
416
|
Yes. `npx zooid dev` runs a local server with SQLite. Docker support coming soon for VPS deployment.
|
package/dist/index.js
CHANGED
|
@@ -205,17 +205,20 @@ function runConfigGet(key) {
|
|
|
205
205
|
// src/commands/channel.ts
|
|
206
206
|
async function runChannelCreate(id, options, client) {
|
|
207
207
|
const c = client ?? createClient();
|
|
208
|
+
let config = options.config;
|
|
209
|
+
if (options.strict !== void 0) {
|
|
210
|
+
config = { ...config, strict_types: options.strict };
|
|
211
|
+
}
|
|
208
212
|
const result = await c.createChannel({
|
|
209
213
|
id,
|
|
210
214
|
name: options.name ?? id,
|
|
211
215
|
description: options.description,
|
|
212
216
|
is_public: options.public ?? true,
|
|
213
|
-
|
|
214
|
-
config: options.config
|
|
217
|
+
config
|
|
215
218
|
});
|
|
216
219
|
if (!client) {
|
|
217
|
-
const
|
|
218
|
-
const channels =
|
|
220
|
+
const config2 = loadConfig();
|
|
221
|
+
const channels = config2.channels ?? {};
|
|
219
222
|
channels[id] = { token: result.token };
|
|
220
223
|
saveConfig({ channels });
|
|
221
224
|
}
|
|
@@ -922,23 +925,47 @@ function resolvePackageDir(packageName) {
|
|
|
922
925
|
const pkgJson = require2.resolve(`${packageName}/package.json`);
|
|
923
926
|
return path4.dirname(pkgJson);
|
|
924
927
|
}
|
|
928
|
+
var USER_WRANGLER_TOML = path4.join(process.cwd(), "wrangler.toml");
|
|
929
|
+
function ejectWranglerToml(opts) {
|
|
930
|
+
const serverDir = resolvePackageDir("@zooid/server");
|
|
931
|
+
let toml = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
|
|
932
|
+
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
933
|
+
toml = toml.replace(/name = "[^"]*"/, `name = "${opts.workerName}"`);
|
|
934
|
+
toml = toml.replace(
|
|
935
|
+
/database_name = "[^"]*"/,
|
|
936
|
+
`database_name = "${opts.dbName}"`
|
|
937
|
+
);
|
|
938
|
+
toml = toml.replace(
|
|
939
|
+
/database_id = "[^"]*"/,
|
|
940
|
+
`database_id = "${opts.databaseId}"`
|
|
941
|
+
);
|
|
942
|
+
toml = toml.replace(
|
|
943
|
+
/ZOOID_SERVER_ID = "[^"]*"/,
|
|
944
|
+
`ZOOID_SERVER_ID = "${opts.serverSlug}"`
|
|
945
|
+
);
|
|
946
|
+
fs5.writeFileSync(USER_WRANGLER_TOML, toml);
|
|
947
|
+
}
|
|
925
948
|
function prepareStagingDir() {
|
|
926
949
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
927
950
|
const serverRequire = createRequire(path4.join(serverDir, "package.json"));
|
|
928
951
|
const webDir = path4.dirname(serverRequire.resolve("@zooid/web/package.json"));
|
|
929
952
|
const webDistDir = path4.join(webDir, "dist");
|
|
930
|
-
if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
|
|
931
|
-
throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
|
|
932
|
-
}
|
|
933
953
|
if (!fs5.existsSync(webDistDir)) {
|
|
934
954
|
throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
|
|
935
955
|
}
|
|
936
956
|
const tmpDir = fs5.mkdtempSync(path4.join(os.tmpdir(), "zooid-deploy-"));
|
|
937
957
|
copyDirSync(path4.join(serverDir, "src"), path4.join(tmpDir, "src"));
|
|
938
958
|
copyDirSync(webDistDir, path4.join(tmpDir, "web-dist"));
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
959
|
+
if (fs5.existsSync(USER_WRANGLER_TOML)) {
|
|
960
|
+
fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(tmpDir, "wrangler.toml"));
|
|
961
|
+
} else {
|
|
962
|
+
if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
|
|
963
|
+
throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
|
|
964
|
+
}
|
|
965
|
+
let toml = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
|
|
966
|
+
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
967
|
+
fs5.writeFileSync(path4.join(tmpDir, "wrangler.toml"), toml);
|
|
968
|
+
}
|
|
942
969
|
const nodeModules = findServerNodeModules(serverDir);
|
|
943
970
|
if (nodeModules) {
|
|
944
971
|
fs5.symlinkSync(nodeModules, path4.join(tmpDir, "node_modules"), "junction");
|
|
@@ -1128,26 +1155,11 @@ async function runDeploy() {
|
|
|
1128
1155
|
}
|
|
1129
1156
|
const databaseId = dbIdMatch[1];
|
|
1130
1157
|
printSuccess(`D1 database created (${databaseId})`);
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
`name = "${workerName}"`
|
|
1136
|
-
);
|
|
1137
|
-
tomlContent = tomlContent.replace(
|
|
1138
|
-
/database_name = "[^"]*"/,
|
|
1139
|
-
`database_name = "${dbName}"`
|
|
1140
|
-
);
|
|
1141
|
-
tomlContent = tomlContent.replace(
|
|
1142
|
-
/database_id = "[^"]*"/,
|
|
1143
|
-
`database_id = "${databaseId}"`
|
|
1144
|
-
);
|
|
1145
|
-
tomlContent = tomlContent.replace(
|
|
1146
|
-
/ZOOID_SERVER_ID = "[^"]*"/,
|
|
1147
|
-
`ZOOID_SERVER_ID = "${serverSlug}"`
|
|
1158
|
+
ejectWranglerToml({ workerName, dbName, databaseId, serverSlug });
|
|
1159
|
+
fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(stagingDir, "wrangler.toml"));
|
|
1160
|
+
printSuccess(
|
|
1161
|
+
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
1148
1162
|
);
|
|
1149
|
-
fs5.writeFileSync(wranglerTomlPath, tomlContent);
|
|
1150
|
-
printSuccess("Configured wrangler.toml");
|
|
1151
1163
|
const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
|
|
1152
1164
|
if (fs5.existsSync(schemaPath)) {
|
|
1153
1165
|
printStep("Running database schema migration...");
|
|
@@ -1204,33 +1216,22 @@ async function runDeploy() {
|
|
|
1204
1216
|
console.log("");
|
|
1205
1217
|
printInfo("Deploy type", "Redeploying existing server");
|
|
1206
1218
|
console.log("");
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
);
|
|
1217
|
-
try {
|
|
1218
|
-
const output = wrangler("d1 list --json", stagingDir, creds);
|
|
1219
|
-
const databases = JSON.parse(output);
|
|
1220
|
-
const db = databases.find((d) => d.name === dbName);
|
|
1221
|
-
if (db) {
|
|
1222
|
-
tomlContent = tomlContent.replace(
|
|
1223
|
-
/database_name = "[^"]*"/,
|
|
1224
|
-
`database_name = "${dbName}"`
|
|
1225
|
-
);
|
|
1226
|
-
tomlContent = tomlContent.replace(
|
|
1227
|
-
/database_id = "[^"]*"/,
|
|
1228
|
-
`database_id = "${db.uuid}"`
|
|
1229
|
-
);
|
|
1219
|
+
if (!fs5.existsSync(USER_WRANGLER_TOML)) {
|
|
1220
|
+
printStep("Ejecting wrangler.toml...");
|
|
1221
|
+
let databaseId = "";
|
|
1222
|
+
try {
|
|
1223
|
+
const output = wrangler("d1 list --json", stagingDir, creds);
|
|
1224
|
+
const databases = JSON.parse(output);
|
|
1225
|
+
const db = databases.find((d) => d.name === dbName);
|
|
1226
|
+
if (db) databaseId = db.uuid;
|
|
1227
|
+
} catch {
|
|
1230
1228
|
}
|
|
1231
|
-
|
|
1229
|
+
ejectWranglerToml({ workerName, dbName, databaseId, serverSlug });
|
|
1230
|
+
printSuccess(
|
|
1231
|
+
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
1232
|
+
);
|
|
1232
1233
|
}
|
|
1233
|
-
fs5.
|
|
1234
|
+
fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(stagingDir, "wrangler.toml"));
|
|
1234
1235
|
const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
|
|
1235
1236
|
if (fs5.existsSync(schemaPath)) {
|
|
1236
1237
|
printStep("Running schema migration...");
|
|
@@ -1389,6 +1390,7 @@ async function runDeploy() {
|
|
|
1389
1390
|
console.log("");
|
|
1390
1391
|
if (isFirstDeploy) {
|
|
1391
1392
|
console.log(" Next steps:");
|
|
1393
|
+
console.log(" Edit wrangler.toml to add env vars, observability, etc.");
|
|
1392
1394
|
console.log(" npx zooid channel create my-channel");
|
|
1393
1395
|
console.log(
|
|
1394
1396
|
` npx zooid publish my-channel --data='{"hello": "world"}'`
|
|
@@ -1426,7 +1428,7 @@ async function resolveAndRecord(channel, opts) {
|
|
|
1426
1428
|
return result;
|
|
1427
1429
|
}
|
|
1428
1430
|
var program = new Command();
|
|
1429
|
-
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.
|
|
1431
|
+
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.4.0");
|
|
1430
1432
|
var telemetryCtx = { startTime: 0 };
|
|
1431
1433
|
function setTelemetryChannel(channelId) {
|
|
1432
1434
|
telemetryCtx.channelId = channelId;
|
|
@@ -1530,12 +1532,19 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
|
|
|
1530
1532
|
});
|
|
1531
1533
|
var channelCmd = program.command("channel").description("Manage channels");
|
|
1532
1534
|
channelCmd.command("create <id>").description("Create a new channel").option("--name <name>", "Display name (defaults to id)").option("--description <desc>", "Channel description").option("--public", "Make channel public (default)", true).option("--private", "Make channel private").option("--strict", "Enable strict schema validation on publish").option(
|
|
1535
|
+
"--config <file>",
|
|
1536
|
+
"Path to channel config JSON file (display, types, storage)"
|
|
1537
|
+
).option(
|
|
1533
1538
|
"--schema <file>",
|
|
1534
1539
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
1535
1540
|
).action(async (id, opts) => {
|
|
1536
1541
|
try {
|
|
1537
1542
|
let config;
|
|
1538
|
-
if (opts.
|
|
1543
|
+
if (opts.config) {
|
|
1544
|
+
const fs6 = await import("fs");
|
|
1545
|
+
const raw = fs6.readFileSync(opts.config, "utf-8");
|
|
1546
|
+
config = JSON.parse(raw);
|
|
1547
|
+
} else if (opts.schema) {
|
|
1539
1548
|
const fs6 = await import("fs");
|
|
1540
1549
|
const raw = fs6.readFileSync(opts.schema, "utf-8");
|
|
1541
1550
|
const parsed = JSON.parse(raw);
|
|
@@ -1559,6 +1568,9 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1559
1568
|
}
|
|
1560
1569
|
});
|
|
1561
1570
|
channelCmd.command("update <id>").description("Update a channel").option("--name <name>", "Display name").option("--description <desc>", "Channel description").option("--tags <tags>", "Comma-separated tags").option("--public", "Make channel public").option("--private", "Make channel private").option("--strict", "Enable strict schema validation on publish").option("--no-strict", "Disable strict schema validation").option(
|
|
1571
|
+
"--config <file>",
|
|
1572
|
+
"Path to channel config JSON file (display, types, storage)"
|
|
1573
|
+
).option(
|
|
1562
1574
|
"--schema <file>",
|
|
1563
1575
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
1564
1576
|
).action(async (id, opts) => {
|
|
@@ -1570,7 +1582,11 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1570
1582
|
fields.tags = opts.tags.split(",").map((t) => t.trim());
|
|
1571
1583
|
if (opts.public) fields.is_public = true;
|
|
1572
1584
|
if (opts.private) fields.is_public = false;
|
|
1573
|
-
if (opts.
|
|
1585
|
+
if (opts.config) {
|
|
1586
|
+
const fs6 = await import("fs");
|
|
1587
|
+
const raw = fs6.readFileSync(opts.config, "utf-8");
|
|
1588
|
+
fields.config = JSON.parse(raw);
|
|
1589
|
+
} else if (opts.schema) {
|
|
1574
1590
|
const fs6 = await import("fs");
|
|
1575
1591
|
const raw = fs6.readFileSync(opts.schema, "utf-8");
|
|
1576
1592
|
const parsed = JSON.parse(raw);
|
|
@@ -1580,10 +1596,13 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1580
1596
|
}
|
|
1581
1597
|
fields.config = { types };
|
|
1582
1598
|
}
|
|
1583
|
-
if (opts.strict !== void 0)
|
|
1599
|
+
if (opts.strict !== void 0) {
|
|
1600
|
+
const existing = fields.config ?? {};
|
|
1601
|
+
fields.config = { ...existing, strict_types: opts.strict };
|
|
1602
|
+
}
|
|
1584
1603
|
if (Object.keys(fields).length === 0) {
|
|
1585
1604
|
throw new Error(
|
|
1586
|
-
"No fields specified. Use --name, --description, --tags, --public, --private, --schema, or --strict."
|
|
1605
|
+
"No fields specified. Use --name, --description, --tags, --public, --private, --config, --schema, or --strict."
|
|
1587
1606
|
);
|
|
1588
1607
|
}
|
|
1589
1608
|
const channel = await runChannelUpdate(id, fields);
|
|
@@ -1657,6 +1676,35 @@ program.command("publish <channel>").description("Publish an event to a channel"
|
|
|
1657
1676
|
handleError("publish", err);
|
|
1658
1677
|
}
|
|
1659
1678
|
});
|
|
1679
|
+
program.command("delete-event <channel> <event-id>").description("Delete a single event by ID").option("-y, --yes", "Skip confirmation prompt").action(async (channel, eventId, opts) => {
|
|
1680
|
+
try {
|
|
1681
|
+
const { client, channelId } = resolveChannel(channel, {
|
|
1682
|
+
tokenType: "publish"
|
|
1683
|
+
});
|
|
1684
|
+
if (!opts.yes) {
|
|
1685
|
+
const readline4 = await import("readline");
|
|
1686
|
+
const rl = readline4.createInterface({
|
|
1687
|
+
input: process.stdin,
|
|
1688
|
+
output: process.stdout
|
|
1689
|
+
});
|
|
1690
|
+
const answer = await new Promise((resolve) => {
|
|
1691
|
+
rl.question(
|
|
1692
|
+
`Delete event "${eventId}" from channel "${channelId}"? [y/N] `,
|
|
1693
|
+
resolve
|
|
1694
|
+
);
|
|
1695
|
+
});
|
|
1696
|
+
rl.close();
|
|
1697
|
+
if (answer.toLowerCase() !== "y") {
|
|
1698
|
+
console.log("Aborted.");
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
await client.deleteEvent(channelId, eventId);
|
|
1703
|
+
printSuccess(`Deleted event: ${eventId}`);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
handleError("delete-event", err);
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1660
1708
|
program.command("tail <channel>").description("Fetch latest events, or stream live with -f").option("-n, --limit <n>", "Max events to return", "50").option("-f, --follow", "Follow mode \u2014 stream new events as they arrive").option("--type <type>", "Filter events by type").option("--since <iso>", "Only events after this ISO 8601 timestamp").option("--cursor <cursor>", "Resume from a previous cursor").option(
|
|
1661
1709
|
"--mode <mode>",
|
|
1662
1710
|
"Transport mode for follow: auto, ws, or poll",
|
|
@@ -1814,7 +1862,8 @@ serverCmd.command("set").description("Update server metadata").option("--name <n
|
|
|
1814
1862
|
handleError("server set", err);
|
|
1815
1863
|
}
|
|
1816
1864
|
});
|
|
1817
|
-
program.command("token").description(
|
|
1865
|
+
var tokenCmd = program.command("token").description("Manage tokens");
|
|
1866
|
+
tokenCmd.command("mint").description(
|
|
1818
1867
|
"Mint a new token. Scopes: admin, pub:<channel>, sub:<channel>. Wildcards: pub:*, sub:prefix-*"
|
|
1819
1868
|
).argument(
|
|
1820
1869
|
"<scopes...>",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zooid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Open-source pub/sub server for AI agents. Publish signals, subscribe via webhook, WebSocket, polling, or RSS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@inquirer/checkbox": "^5.0.7",
|
|
26
26
|
"commander": "^14.0.3",
|
|
27
|
-
"@zooid/
|
|
28
|
-
"@zooid/
|
|
29
|
-
"@zooid/
|
|
27
|
+
"@zooid/sdk": "^0.4.0",
|
|
28
|
+
"@zooid/server": "^0.4.0",
|
|
29
|
+
"@zooid/types": "^0.4.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@cloudflare/vitest-pool-workers": "^0.8.34",
|