zooid 0.2.0 → 0.3.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 CHANGED
@@ -2,8 +2,9 @@
2
2
  <h1 align="center">🪸 Zooid</h1>
3
3
  <p align="center"><strong>Pub/sub for AI agents. Deploy in one command. Free forever.</strong></p>
4
4
  <p align="center">
5
- <a href="https://directory.zooid.dev/api/discover">Browse Servers</a> ·
6
5
  <a href="#quickstart">Quickstart</a> ·
6
+ <a href="https://zooid.dev/docs">Docs</a> ·
7
+ <a href="https://directory.zooid.dev/api/discover">Browse Servers</a> ·
7
8
  <a href="#why-zooid">Why Zooid</a> ·
8
9
  <a href="https://dsc.gg/zooid">Discord</a>
9
10
  </p>
@@ -120,6 +121,8 @@ That's the whole flow. You publish on your server, others subscribe from theirs.
120
121
 
121
122
  A Zooid server is just a URL — send it anywhere (email, Discord, Twitter), and anyone can subscribe directly.
122
123
 
124
+ For the full reference — channels, webhooks, SDK, CLI flags — see the [docs](https://zooid.dev/docs).
125
+
123
126
  ---
124
127
 
125
128
  ## Why Zooid?
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/config.ts
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+
8
+ // src/lib/constants.ts
9
+ var STATE_FILENAME = "state.json";
10
+ var LEGACY_STATE_FILENAME = "config.json";
11
+
12
+ // src/lib/config.ts
13
+ function getConfigDir() {
14
+ return process.env.ZOOID_CONFIG_DIR ?? path.join(os.homedir(), ".zooid");
15
+ }
16
+ function getStatePath() {
17
+ const dir = getConfigDir();
18
+ const statePath = path.join(dir, STATE_FILENAME);
19
+ if (!fs.existsSync(statePath)) {
20
+ const legacyPath = path.join(dir, LEGACY_STATE_FILENAME);
21
+ if (fs.existsSync(legacyPath)) {
22
+ fs.renameSync(legacyPath, statePath);
23
+ }
24
+ }
25
+ return statePath;
26
+ }
27
+ var getConfigPath = getStatePath;
28
+ function loadConfigFile() {
29
+ try {
30
+ const raw = fs.readFileSync(getStatePath(), "utf-8");
31
+ const parsed = JSON.parse(raw);
32
+ if (parsed.server && !parsed.servers) {
33
+ const serverUrl = parsed.server;
34
+ const migrated = {
35
+ current: serverUrl,
36
+ servers: {
37
+ [serverUrl]: {
38
+ worker_url: parsed.worker_url,
39
+ admin_token: parsed.admin_token,
40
+ channels: parsed.channels
41
+ }
42
+ }
43
+ };
44
+ const dir = getConfigDir();
45
+ fs.mkdirSync(dir, { recursive: true });
46
+ fs.writeFileSync(
47
+ getConfigPath(),
48
+ JSON.stringify(migrated, null, 2) + "\n"
49
+ );
50
+ return migrated;
51
+ }
52
+ return parsed;
53
+ } catch {
54
+ return {};
55
+ }
56
+ }
57
+ var serverNoteShown = false;
58
+ function resolveServer() {
59
+ const file = loadConfigFile();
60
+ let cwdUrl;
61
+ try {
62
+ const zooidJsonPath = path.join(process.cwd(), "zooid.json");
63
+ if (fs.existsSync(zooidJsonPath)) {
64
+ const raw = fs.readFileSync(zooidJsonPath, "utf-8");
65
+ const parsed = JSON.parse(raw);
66
+ cwdUrl = parsed.url || void 0;
67
+ }
68
+ } catch {
69
+ }
70
+ if (cwdUrl) {
71
+ if (file.current && file.current !== cwdUrl && !serverNoteShown) {
72
+ serverNoteShown = true;
73
+ console.log(
74
+ ` Note: using server from zooid.json (${cwdUrl}), current is ${file.current}`
75
+ );
76
+ }
77
+ return cwdUrl;
78
+ }
79
+ return file.current;
80
+ }
81
+ function loadConfig() {
82
+ const file = loadConfigFile();
83
+ const serverUrl = resolveServer();
84
+ const entry = serverUrl ? file.servers?.[serverUrl] : void 0;
85
+ return {
86
+ server: serverUrl,
87
+ worker_url: entry?.worker_url,
88
+ admin_token: entry?.admin_token,
89
+ channels: entry?.channels
90
+ };
91
+ }
92
+ function saveConfig(partial, serverUrl, options) {
93
+ const dir = getConfigDir();
94
+ fs.mkdirSync(dir, { recursive: true });
95
+ const file = loadConfigFile();
96
+ const url = serverUrl ?? resolveServer();
97
+ if (!url) {
98
+ throw new Error(
99
+ "No server URL to save config for. Deploy first or set url in zooid.json."
100
+ );
101
+ }
102
+ if (!file.servers) file.servers = {};
103
+ const existing = file.servers[url] ?? {};
104
+ const merged = { ...existing, ...partial };
105
+ if (partial.channels && existing.channels) {
106
+ merged.channels = { ...existing.channels };
107
+ for (const [chId, chData] of Object.entries(partial.channels)) {
108
+ merged.channels[chId] = { ...existing.channels[chId], ...chData };
109
+ }
110
+ }
111
+ file.servers[url] = merged;
112
+ if (options?.setCurrent !== false) {
113
+ file.current = url;
114
+ }
115
+ fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
116
+ }
117
+ function loadDirectoryToken() {
118
+ const file = loadConfigFile();
119
+ return file.directory_token;
120
+ }
121
+ function saveDirectoryToken(token) {
122
+ const dir = getConfigDir();
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ const file = loadConfigFile();
125
+ file.directory_token = token;
126
+ fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
127
+ }
128
+ function recordTailHistory(channelId, serverUrl, name, lastEventId) {
129
+ const url = serverUrl ?? resolveServer();
130
+ if (!url) return;
131
+ const file = loadConfigFile();
132
+ if (!file.servers) file.servers = {};
133
+ if (!file.servers[url]) file.servers[url] = {};
134
+ if (!file.servers[url].channels) file.servers[url].channels = {};
135
+ const channel = file.servers[url].channels[channelId] ?? {};
136
+ const now = (/* @__PURE__ */ new Date()).toISOString();
137
+ const existing = channel.stats;
138
+ channel.stats = {
139
+ num_tails: (existing?.num_tails ?? 0) + 1,
140
+ last_tailed_at: now,
141
+ first_tailed_at: existing?.first_tailed_at ?? now,
142
+ last_event_id: lastEventId ?? existing?.last_event_id
143
+ };
144
+ if (name) {
145
+ channel.name = name;
146
+ }
147
+ file.servers[url].channels[channelId] = channel;
148
+ const dir = getConfigDir();
149
+ fs.mkdirSync(dir, { recursive: true });
150
+ fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
151
+ }
152
+ function switchServer(url) {
153
+ const dir = getConfigDir();
154
+ fs.mkdirSync(dir, { recursive: true });
155
+ const file = loadConfigFile();
156
+ if (!file.servers) file.servers = {};
157
+ if (!file.servers[url]) file.servers[url] = {};
158
+ file.current = url;
159
+ fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
160
+ }
161
+
162
+ export {
163
+ getConfigDir,
164
+ getStatePath,
165
+ getConfigPath,
166
+ loadConfigFile,
167
+ resolveServer,
168
+ loadConfig,
169
+ saveConfig,
170
+ loadDirectoryToken,
171
+ saveDirectoryToken,
172
+ recordTailHistory,
173
+ switchServer
174
+ };
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadConfig,
4
+ loadConfigFile,
5
+ saveConfig
6
+ } from "./chunk-67ZRMVHO.js";
7
+
8
+ // src/lib/client.ts
9
+ import { ZooidClient } from "@zooid/sdk";
10
+ function createClient(token) {
11
+ const config = loadConfig();
12
+ const server = config.server;
13
+ if (!server) {
14
+ throw new Error(
15
+ "No server configured. Run: npx zooid config set server <url>"
16
+ );
17
+ }
18
+ return new ZooidClient({ server, token: token ?? config.admin_token });
19
+ }
20
+ function getChannelToken(channelTokens, tokenType) {
21
+ if (!channelTokens) return void 0;
22
+ if (channelTokens.token) return channelTokens.token;
23
+ if (tokenType === "publish") return channelTokens.publish_token;
24
+ if (tokenType === "subscribe") return channelTokens.subscribe_token;
25
+ return channelTokens.publish_token ?? channelTokens.subscribe_token;
26
+ }
27
+ function createChannelClient(channelId, tokenType) {
28
+ const config = loadConfig();
29
+ const server = config.server;
30
+ if (!server) {
31
+ throw new Error(
32
+ "No server configured. Run: npx zooid config set server <url>"
33
+ );
34
+ }
35
+ const channelToken = getChannelToken(config.channels?.[channelId], tokenType);
36
+ return new ZooidClient({ server, token: channelToken ?? config.admin_token });
37
+ }
38
+ var createPublishClient = (channelId) => createChannelClient(channelId, "publish");
39
+ var createSubscribeClient = (channelId) => createChannelClient(channelId, "subscribe");
40
+ var PRIVATE_HOST_RE = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)$/;
41
+ function normalizeServerUrl(url) {
42
+ let normalized = url.replace(/\/+$/, "");
43
+ try {
44
+ const parsed = new URL(normalized);
45
+ if (parsed.protocol === "http:" && !PRIVATE_HOST_RE.test(parsed.hostname)) {
46
+ normalized = normalized.replace(/^http:\/\//, "https://");
47
+ }
48
+ } catch {
49
+ }
50
+ return normalized;
51
+ }
52
+ function parseChannelUrl(channel) {
53
+ let raw = channel;
54
+ if (!raw.startsWith("http") && raw.includes("/")) {
55
+ if (PRIVATE_HOST_RE.test(raw.split(/[:/]/)[0])) {
56
+ raw = `http://${raw}`;
57
+ } else if (raw.includes(".")) {
58
+ raw = `https://${raw}`;
59
+ } else if (/^[^/]+:\d+\//.test(raw)) {
60
+ raw = `http://${raw}`;
61
+ }
62
+ }
63
+ if (!raw.startsWith("http")) return null;
64
+ try {
65
+ const url = new URL(raw);
66
+ const channelsMatch = url.pathname.match(/^\/channels\/([^/]+)/);
67
+ if (channelsMatch) {
68
+ return { server: url.origin, channelId: channelsMatch[1] };
69
+ }
70
+ const segments = url.pathname.split("/").filter(Boolean);
71
+ if (segments.length === 1) {
72
+ return { server: url.origin, channelId: segments[0] };
73
+ }
74
+ } catch {
75
+ }
76
+ return null;
77
+ }
78
+ function resolveChannel(channel, opts) {
79
+ const parsed = parseChannelUrl(channel);
80
+ if (parsed) {
81
+ const { server: server2, channelId } = parsed;
82
+ let token2 = opts?.token;
83
+ let tokenSaved2 = false;
84
+ if (token2) {
85
+ saveConfig({ channels: { [channelId]: { token: token2 } } }, server2, {
86
+ setCurrent: false
87
+ });
88
+ tokenSaved2 = true;
89
+ }
90
+ if (!token2) {
91
+ const file = loadConfigFile();
92
+ const channelTokens = file.servers?.[server2]?.channels?.[channelId];
93
+ token2 = getChannelToken(channelTokens, opts?.tokenType);
94
+ }
95
+ return {
96
+ client: new ZooidClient({ server: server2, token: token2 }),
97
+ channelId,
98
+ server: server2,
99
+ tokenSaved: tokenSaved2
100
+ };
101
+ }
102
+ const config = loadConfig();
103
+ const server = config.server;
104
+ if (!server) {
105
+ throw new Error(
106
+ "No server configured. Run: npx zooid config set server <url>"
107
+ );
108
+ }
109
+ let token = opts?.token;
110
+ let tokenSaved = false;
111
+ if (token) {
112
+ saveConfig({ channels: { [channel]: { token } } });
113
+ tokenSaved = true;
114
+ }
115
+ if (!token) {
116
+ const channelToken = getChannelToken(
117
+ config.channels?.[channel],
118
+ opts?.tokenType
119
+ );
120
+ token = channelToken ?? config.admin_token;
121
+ }
122
+ return {
123
+ client: new ZooidClient({ server, token }),
124
+ channelId: channel,
125
+ server,
126
+ tokenSaved
127
+ };
128
+ }
129
+
130
+ export {
131
+ createClient,
132
+ createChannelClient,
133
+ createPublishClient,
134
+ createSubscribeClient,
135
+ PRIVATE_HOST_RE,
136
+ normalizeServerUrl,
137
+ parseChannelUrl,
138
+ resolveChannel
139
+ };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ PRIVATE_HOST_RE,
4
+ createChannelClient,
5
+ createClient,
6
+ createPublishClient,
7
+ createSubscribeClient,
8
+ normalizeServerUrl,
9
+ parseChannelUrl,
10
+ resolveChannel
11
+ } from "./chunk-VBGU2NST.js";
12
+ import "./chunk-67ZRMVHO.js";
13
+ export {
14
+ PRIVATE_HOST_RE,
15
+ createChannelClient,
16
+ createClient,
17
+ createPublishClient,
18
+ createSubscribeClient,
19
+ normalizeServerUrl,
20
+ parseChannelUrl,
21
+ resolveChannel
22
+ };
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getConfigDir,
4
+ getConfigPath,
5
+ getStatePath,
6
+ loadConfig,
7
+ loadConfigFile,
8
+ loadDirectoryToken,
9
+ recordTailHistory,
10
+ resolveServer,
11
+ saveConfig,
12
+ saveDirectoryToken,
13
+ switchServer
14
+ } from "./chunk-67ZRMVHO.js";
15
+ export {
16
+ getConfigDir,
17
+ getConfigPath,
18
+ getStatePath,
19
+ loadConfig,
20
+ loadConfigFile,
21
+ loadDirectoryToken,
22
+ recordTailHistory,
23
+ resolveServer,
24
+ saveConfig,
25
+ saveDirectoryToken,
26
+ switchServer
27
+ };
package/dist/index.js CHANGED
@@ -1,162 +1,34 @@
1
1
  #!/usr/bin/env node
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
2
+ import {
3
+ createClient,
4
+ createPublishClient,
5
+ createSubscribeClient,
6
+ normalizeServerUrl,
7
+ resolveChannel
8
+ } from "./chunk-VBGU2NST.js";
9
+ import {
10
+ getConfigDir,
11
+ getStatePath,
12
+ loadConfig,
13
+ loadConfigFile,
14
+ loadDirectoryToken,
15
+ recordTailHistory,
16
+ resolveServer,
17
+ saveConfig,
18
+ saveDirectoryToken,
19
+ switchServer
20
+ } from "./chunk-67ZRMVHO.js";
8
21
 
9
22
  // src/index.ts
10
23
  import { Command } from "commander";
11
24
 
12
- // src/lib/config.ts
25
+ // src/lib/telemetry.ts
13
26
  import fs from "fs";
14
- import os from "os";
15
27
  import path from "path";
16
- function getConfigDir() {
17
- return process.env.ZOOID_CONFIG_DIR ?? path.join(os.homedir(), ".zooid");
18
- }
19
- function getConfigPath() {
20
- return path.join(getConfigDir(), "config.json");
21
- }
22
- function loadConfigFile() {
23
- try {
24
- const raw = fs.readFileSync(getConfigPath(), "utf-8");
25
- const parsed = JSON.parse(raw);
26
- if (parsed.server && !parsed.servers) {
27
- const serverUrl = parsed.server;
28
- const migrated = {
29
- current: serverUrl,
30
- servers: {
31
- [serverUrl]: {
32
- worker_url: parsed.worker_url,
33
- admin_token: parsed.admin_token,
34
- channels: parsed.channels
35
- }
36
- }
37
- };
38
- const dir = getConfigDir();
39
- fs.mkdirSync(dir, { recursive: true });
40
- fs.writeFileSync(
41
- getConfigPath(),
42
- JSON.stringify(migrated, null, 2) + "\n"
43
- );
44
- return migrated;
45
- }
46
- return parsed;
47
- } catch {
48
- return {};
49
- }
50
- }
51
- function resolveServer() {
52
- const file = loadConfigFile();
53
- let cwdUrl;
54
- try {
55
- const zooidJsonPath = path.join(process.cwd(), "zooid.json");
56
- if (fs.existsSync(zooidJsonPath)) {
57
- const raw = fs.readFileSync(zooidJsonPath, "utf-8");
58
- const parsed = JSON.parse(raw);
59
- cwdUrl = parsed.url || void 0;
60
- }
61
- } catch {
62
- }
63
- if (cwdUrl) {
64
- if (file.current && file.current !== cwdUrl) {
65
- console.log(
66
- ` Note: using server from zooid.json (${cwdUrl}), current is ${file.current}`
67
- );
68
- }
69
- return cwdUrl;
70
- }
71
- return file.current;
72
- }
73
- function loadConfig() {
74
- const file = loadConfigFile();
75
- const serverUrl = resolveServer();
76
- const entry = serverUrl ? file.servers?.[serverUrl] : void 0;
77
- return {
78
- server: serverUrl,
79
- worker_url: entry?.worker_url,
80
- admin_token: entry?.admin_token,
81
- channels: entry?.channels
82
- };
83
- }
84
- function saveConfig(partial, serverUrl, options) {
85
- const dir = getConfigDir();
86
- fs.mkdirSync(dir, { recursive: true });
87
- const file = loadConfigFile();
88
- const url = serverUrl ?? resolveServer();
89
- if (!url) {
90
- throw new Error(
91
- "No server URL to save config for. Deploy first or set url in zooid.json."
92
- );
93
- }
94
- if (!file.servers) file.servers = {};
95
- const existing = file.servers[url] ?? {};
96
- const merged = { ...existing, ...partial };
97
- if (partial.channels && existing.channels) {
98
- merged.channels = { ...existing.channels };
99
- for (const [chId, chData] of Object.entries(partial.channels)) {
100
- merged.channels[chId] = { ...existing.channels[chId], ...chData };
101
- }
102
- }
103
- file.servers[url] = merged;
104
- if (options?.setCurrent !== false) {
105
- file.current = url;
106
- }
107
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
108
- }
109
- function loadDirectoryToken() {
110
- const file = loadConfigFile();
111
- return file.directory_token;
112
- }
113
- function saveDirectoryToken(token) {
114
- const dir = getConfigDir();
115
- fs.mkdirSync(dir, { recursive: true });
116
- const file = loadConfigFile();
117
- file.directory_token = token;
118
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
119
- }
120
- function recordTailHistory(channelId, serverUrl, name) {
121
- const url = serverUrl ?? resolveServer();
122
- if (!url) return;
123
- const file = loadConfigFile();
124
- if (!file.servers) file.servers = {};
125
- if (!file.servers[url]) file.servers[url] = {};
126
- if (!file.servers[url].channels) file.servers[url].channels = {};
127
- const channel = file.servers[url].channels[channelId] ?? {};
128
- const now = (/* @__PURE__ */ new Date()).toISOString();
129
- const existing = channel.stats;
130
- channel.stats = {
131
- num_tails: (existing?.num_tails ?? 0) + 1,
132
- last_tailed_at: now,
133
- first_tailed_at: existing?.first_tailed_at ?? now
134
- };
135
- if (name) {
136
- channel.name = name;
137
- }
138
- file.servers[url].channels[channelId] = channel;
139
- const dir = getConfigDir();
140
- fs.mkdirSync(dir, { recursive: true });
141
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
142
- }
143
- function switchServer(url) {
144
- const dir = getConfigDir();
145
- fs.mkdirSync(dir, { recursive: true });
146
- const file = loadConfigFile();
147
- if (!file.servers) file.servers = {};
148
- if (!file.servers[url]) file.servers[url] = {};
149
- file.current = url;
150
- fs.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
151
- }
152
-
153
- // src/lib/telemetry.ts
154
- import fs2 from "fs";
155
- import path2 from "path";
156
28
  import { randomUUID } from "crypto";
29
+ import { spawn } from "child_process";
157
30
  var TELEMETRY_ENDPOINT = "https://telemetry.zooid.dev/v1/events";
158
31
  var QUEUE_FILENAME = "telemetry.json";
159
- var CONFIG_FILENAME = "config.json";
160
32
  var MAX_QUEUE_SIZE = 1e3;
161
33
  function isEnabled() {
162
34
  const envVar = process.env.ZOOID_TELEMETRY;
@@ -185,10 +57,10 @@ function writeEvent(event) {
185
57
  const queuePath = getQueuePath();
186
58
  const dir = getConfigDir();
187
59
  try {
188
- fs2.mkdirSync(dir, { recursive: true });
60
+ fs.mkdirSync(dir, { recursive: true });
189
61
  let queue = [];
190
62
  try {
191
- const raw = fs2.readFileSync(queuePath, "utf-8");
63
+ const raw = fs.readFileSync(queuePath, "utf-8");
192
64
  queue = JSON.parse(raw);
193
65
  } catch {
194
66
  }
@@ -196,22 +68,21 @@ function writeEvent(event) {
196
68
  if (queue.length > MAX_QUEUE_SIZE) {
197
69
  queue = queue.slice(-MAX_QUEUE_SIZE);
198
70
  }
199
- fs2.writeFileSync(queuePath, JSON.stringify(queue, null, 2) + "\n");
71
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2) + "\n");
200
72
  } catch {
201
73
  }
202
74
  }
203
75
  function flushInBackground() {
204
76
  const queuePath = getQueuePath();
205
77
  try {
206
- const raw = fs2.readFileSync(queuePath, "utf-8");
78
+ const raw = fs.readFileSync(queuePath, "utf-8");
207
79
  const parsed = JSON.parse(raw);
208
80
  if (!Array.isArray(parsed) || parsed.length === 0) return;
209
81
  } catch {
210
82
  return;
211
83
  }
212
84
  try {
213
- const { spawn: spawn2 } = __require("child_process");
214
- const child = spawn2(process.execPath, ["-e", flushScript(queuePath)], {
85
+ const child = spawn(process.execPath, ["-e", flushScript(queuePath)], {
215
86
  detached: true,
216
87
  stdio: "ignore",
217
88
  env: { ...process.env }
@@ -221,9 +92,9 @@ function flushInBackground() {
221
92
  }
222
93
  }
223
94
  function getInstallId() {
224
- const configPath = path2.join(getConfigDir(), CONFIG_FILENAME);
95
+ const configPath = getStatePath();
225
96
  try {
226
- const raw = fs2.readFileSync(configPath, "utf-8");
97
+ const raw = fs.readFileSync(configPath, "utf-8");
227
98
  const parsed = JSON.parse(raw);
228
99
  if (parsed.install_id) return parsed.install_id;
229
100
  } catch {
@@ -233,12 +104,12 @@ function getInstallId() {
233
104
  return id;
234
105
  }
235
106
  function getQueuePath() {
236
- return path2.join(getConfigDir(), QUEUE_FILENAME);
107
+ return path.join(getConfigDir(), QUEUE_FILENAME);
237
108
  }
238
109
  function readTelemetryFlag() {
239
- const configPath = path2.join(getConfigDir(), CONFIG_FILENAME);
110
+ const configPath = getStatePath();
240
111
  try {
241
- const raw = fs2.readFileSync(configPath, "utf-8");
112
+ const raw = fs.readFileSync(configPath, "utf-8");
242
113
  const parsed = JSON.parse(raw);
243
114
  if (parsed.telemetry === true) return true;
244
115
  if (parsed.telemetry === false) return false;
@@ -252,17 +123,17 @@ function writeTelemetryFlag(enabled) {
252
123
  }
253
124
  function persistToConfig(key, value) {
254
125
  const dir = getConfigDir();
255
- const configPath = path2.join(dir, CONFIG_FILENAME);
126
+ const configPath = getStatePath();
256
127
  try {
257
- fs2.mkdirSync(dir, { recursive: true });
128
+ fs.mkdirSync(dir, { recursive: true });
258
129
  let config = {};
259
130
  try {
260
- const raw = fs2.readFileSync(configPath, "utf-8");
131
+ const raw = fs.readFileSync(configPath, "utf-8");
261
132
  config = JSON.parse(raw);
262
133
  } catch {
263
134
  }
264
135
  config[key] = value;
265
- fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
136
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
266
137
  } catch {
267
138
  }
268
139
  }
@@ -331,131 +202,6 @@ function runConfigGet(key) {
331
202
  );
332
203
  }
333
204
 
334
- // src/lib/client.ts
335
- import { ZooidClient } from "@zooid/sdk";
336
- function createClient(token) {
337
- const config = loadConfig();
338
- const server = config.server;
339
- if (!server) {
340
- throw new Error(
341
- "No server configured. Run: npx zooid config set server <url>"
342
- );
343
- }
344
- return new ZooidClient({ server, token: token ?? config.admin_token });
345
- }
346
- function createChannelClient(channelId, tokenType) {
347
- const config = loadConfig();
348
- const server = config.server;
349
- if (!server) {
350
- throw new Error(
351
- "No server configured. Run: npx zooid config set server <url>"
352
- );
353
- }
354
- const tokenKey = tokenType === "publish" ? "publish_token" : "subscribe_token";
355
- const channelToken = config.channels?.[channelId]?.[tokenKey];
356
- return new ZooidClient({ server, token: channelToken ?? config.admin_token });
357
- }
358
- var createPublishClient = (channelId) => createChannelClient(channelId, "publish");
359
- var createSubscribeClient = (channelId) => createChannelClient(channelId, "subscribe");
360
- var PRIVATE_HOST_RE = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)$/;
361
- function normalizeServerUrl(url) {
362
- let normalized = url.replace(/\/+$/, "");
363
- try {
364
- const parsed = new URL(normalized);
365
- if (parsed.protocol === "http:" && !PRIVATE_HOST_RE.test(parsed.hostname)) {
366
- normalized = normalized.replace(/^http:\/\//, "https://");
367
- }
368
- } catch {
369
- }
370
- return normalized;
371
- }
372
- function parseChannelUrl(channel) {
373
- let raw = channel;
374
- if (!raw.startsWith("http") && raw.includes("/")) {
375
- if (PRIVATE_HOST_RE.test(raw.split(/[:/]/)[0])) {
376
- raw = `http://${raw}`;
377
- } else if (raw.includes(".")) {
378
- raw = `https://${raw}`;
379
- } else if (/^[^/]+:\d+\//.test(raw)) {
380
- raw = `http://${raw}`;
381
- }
382
- }
383
- if (!raw.startsWith("http")) return null;
384
- try {
385
- const url = new URL(raw);
386
- const channelsMatch = url.pathname.match(/^\/channels\/([^/]+)/);
387
- if (channelsMatch) {
388
- return { server: url.origin, channelId: channelsMatch[1] };
389
- }
390
- const segments = url.pathname.split("/").filter(Boolean);
391
- if (segments.length === 1) {
392
- return { server: url.origin, channelId: segments[0] };
393
- }
394
- } catch {
395
- }
396
- return null;
397
- }
398
- function resolveChannel(channel, opts) {
399
- const parsed = parseChannelUrl(channel);
400
- if (parsed) {
401
- const { server: server2, channelId } = parsed;
402
- let token2 = opts?.token;
403
- let tokenSaved2 = false;
404
- if (token2 && opts?.tokenType) {
405
- const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
406
- saveConfig({ channels: { [channelId]: { [tokenKey]: token2 } } }, server2, {
407
- setCurrent: false
408
- });
409
- tokenSaved2 = true;
410
- }
411
- if (!token2) {
412
- const file = loadConfigFile();
413
- const channelTokens = file.servers?.[server2]?.channels?.[channelId];
414
- if (opts?.tokenType === "publish") {
415
- token2 = channelTokens?.publish_token;
416
- } else {
417
- token2 = channelTokens?.subscribe_token;
418
- }
419
- }
420
- return {
421
- client: new ZooidClient({ server: server2, token: token2 }),
422
- channelId,
423
- server: server2,
424
- tokenSaved: tokenSaved2
425
- };
426
- }
427
- const config = loadConfig();
428
- const server = config.server;
429
- if (!server) {
430
- throw new Error(
431
- "No server configured. Run: npx zooid config set server <url>"
432
- );
433
- }
434
- let token = opts?.token;
435
- let tokenSaved = false;
436
- if (token && opts?.tokenType) {
437
- const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
438
- saveConfig({ channels: { [channel]: { [tokenKey]: token } } });
439
- tokenSaved = true;
440
- }
441
- if (!token) {
442
- const channelTokens = config.channels?.[channel];
443
- if (opts?.tokenType === "publish") {
444
- token = channelTokens?.publish_token ?? config.admin_token;
445
- } else if (opts?.tokenType === "subscribe") {
446
- token = channelTokens?.subscribe_token ?? config.admin_token;
447
- } else {
448
- token = config.admin_token;
449
- }
450
- }
451
- return {
452
- client: new ZooidClient({ server, token }),
453
- channelId: channel,
454
- server,
455
- tokenSaved
456
- };
457
- }
458
-
459
205
  // src/commands/channel.ts
460
206
  async function runChannelCreate(id, options, client) {
461
207
  const c = client ?? createClient();
@@ -470,10 +216,7 @@ async function runChannelCreate(id, options, client) {
470
216
  if (!client) {
471
217
  const config = loadConfig();
472
218
  const channels = config.channels ?? {};
473
- channels[id] = {
474
- publish_token: result.publish_token,
475
- subscribe_token: result.subscribe_token
476
- };
219
+ channels[id] = { token: result.token };
477
220
  saveConfig({ channels });
478
221
  }
479
222
  return result;
@@ -494,20 +237,20 @@ async function runChannelDelete(channelId, client) {
494
237
  const serverUrl = resolveServer();
495
238
  if (serverUrl && file.servers?.[serverUrl]?.channels?.[channelId]) {
496
239
  delete file.servers[serverUrl].channels[channelId];
497
- const fs7 = await import("fs");
498
- fs7.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
240
+ const fs6 = await import("fs");
241
+ fs6.writeFileSync(getStatePath(), JSON.stringify(file, null, 2) + "\n");
499
242
  }
500
243
  }
501
244
  }
502
245
 
503
246
  // src/commands/publish.ts
504
- import fs3 from "fs";
247
+ import fs2 from "fs";
505
248
  async function runPublish(channelId, options, client) {
506
249
  const c = client ?? createPublishClient(channelId);
507
250
  let type;
508
251
  let data;
509
252
  if (options.file) {
510
- const raw = fs3.readFileSync(options.file, "utf-8");
253
+ const raw = fs2.readFileSync(options.file, "utf-8");
511
254
  const parsed = JSON.parse(raw);
512
255
  type = parsed.type;
513
256
  data = parsed.data ?? parsed;
@@ -708,13 +451,13 @@ async function deviceAuth() {
708
451
  "Authorization timed out. You need your human to authorize you. Run `npx zooid share` again to retry."
709
452
  );
710
453
  }
711
- async function directoryFetch(path6, options = {}) {
454
+ async function directoryFetch(path5, options = {}) {
712
455
  let token = await ensureDirectoryToken();
713
456
  const doFetch = (t) => {
714
457
  const headers = new Headers(options.headers);
715
458
  headers.set("Authorization", `Bearer ${t}`);
716
459
  headers.set("Content-Type", "application/json");
717
- return fetch(`${DIRECTORY_BASE_URL}${path6}`, { ...options, headers });
460
+ return fetch(`${DIRECTORY_BASE_URL}${path5}`, { ...options, headers });
718
461
  };
719
462
  let res = await doFetch(token);
720
463
  if (res.status === 401) {
@@ -947,10 +690,9 @@ async function runServerSet(fields, client) {
947
690
  }
948
691
 
949
692
  // src/commands/token.ts
950
- async function runTokenMint(scope, options) {
693
+ async function runTokenMint(scopes, options) {
951
694
  const client = createClient();
952
- const body = { scope };
953
- if (options.channels?.length) body.channels = options.channels;
695
+ const body = { scopes };
954
696
  if (options.sub) body.sub = options.sub;
955
697
  if (options.name) body.name = options.name;
956
698
  if (options.expiresIn) body.expires_in = options.expiresIn;
@@ -958,10 +700,10 @@ async function runTokenMint(scope, options) {
958
700
  }
959
701
 
960
702
  // src/commands/dev.ts
961
- import { execSync, spawn } from "child_process";
703
+ import { execSync, spawn as spawn2 } from "child_process";
962
704
  import crypto2 from "crypto";
963
- import fs4 from "fs";
964
- import path3 from "path";
705
+ import fs3 from "fs";
706
+ import path2 from "path";
965
707
  import { fileURLToPath } from "url";
966
708
 
967
709
  // src/lib/crypto.ts
@@ -1027,25 +769,25 @@ async function createEdDSAAdminToken(privateKeyJwk, kid) {
1027
769
 
1028
770
  // src/commands/dev.ts
1029
771
  function findServerDir() {
1030
- const cliDir = path3.dirname(fileURLToPath(import.meta.url));
1031
- return path3.resolve(cliDir, "../../server");
772
+ const cliDir = path2.dirname(fileURLToPath(import.meta.url));
773
+ return path2.resolve(cliDir, "../../server");
1032
774
  }
1033
775
  async function runDev(port = 8787) {
1034
776
  const serverDir = findServerDir();
1035
- if (!fs4.existsSync(path3.join(serverDir, "wrangler.toml"))) {
777
+ if (!fs3.existsSync(path2.join(serverDir, "wrangler.toml"))) {
1036
778
  throw new Error(
1037
779
  `Server directory not found at ${serverDir}. Make sure you're running from the zooid monorepo.`
1038
780
  );
1039
781
  }
1040
782
  const jwtSecret = crypto2.randomUUID();
1041
- const devVarsPath = path3.join(serverDir, ".dev.vars");
1042
- fs4.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
783
+ const devVarsPath = path2.join(serverDir, ".dev.vars");
784
+ fs3.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
1043
785
  `);
1044
786
  const adminToken = await createAdminToken(jwtSecret);
1045
787
  const serverUrl = `http://localhost:${port}`;
1046
788
  saveConfig({ admin_token: adminToken }, serverUrl);
1047
- const schemaPath = path3.join(serverDir, "src/db/schema.sql");
1048
- if (fs4.existsSync(schemaPath)) {
789
+ const schemaPath = path2.join(serverDir, "src/db/schema.sql");
790
+ if (fs3.existsSync(schemaPath)) {
1049
791
  try {
1050
792
  execSync(
1051
793
  `npx wrangler d1 execute zooid-db --local --file=${schemaPath}`,
@@ -1062,11 +804,11 @@ async function runDev(port = 8787) {
1062
804
  printSuccess("Local server configured");
1063
805
  printInfo("Server", serverUrl);
1064
806
  printInfo("Admin token", adminToken.slice(0, 20) + "...");
1065
- printInfo("Config saved to", "~/.zooid/config.json");
807
+ printInfo("Config saved to", "~/.zooid/state.json");
1066
808
  console.log("");
1067
809
  console.log("Starting wrangler dev...");
1068
810
  console.log("");
1069
- const child = spawn(
811
+ const child = spawn2(
1070
812
  "npx",
1071
813
  ["wrangler", "dev", "--local", "--port", String(port)],
1072
814
  {
@@ -1085,25 +827,25 @@ async function runDev(port = 8787) {
1085
827
  }
1086
828
 
1087
829
  // src/commands/init.ts
1088
- import fs5 from "fs";
1089
- import path4 from "path";
830
+ import fs4 from "fs";
831
+ import path3 from "path";
1090
832
  import readline2 from "readline/promises";
1091
- var CONFIG_FILENAME2 = "zooid.json";
1092
- function getConfigPath2() {
1093
- return path4.join(process.cwd(), CONFIG_FILENAME2);
833
+ var CONFIG_FILENAME = "zooid.json";
834
+ function getConfigPath() {
835
+ return path3.join(process.cwd(), CONFIG_FILENAME);
1094
836
  }
1095
837
  function loadServerConfig() {
1096
- const configPath = getConfigPath2();
1097
- if (!fs5.existsSync(configPath)) return null;
1098
- const raw = fs5.readFileSync(configPath, "utf-8");
838
+ const configPath = getConfigPath();
839
+ if (!fs4.existsSync(configPath)) return null;
840
+ const raw = fs4.readFileSync(configPath, "utf-8");
1099
841
  return JSON.parse(raw);
1100
842
  }
1101
843
  function saveServerConfig(config) {
1102
- const configPath = getConfigPath2();
1103
- fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
844
+ const configPath = getConfigPath();
845
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1104
846
  }
1105
847
  async function runInit() {
1106
- const configPath = getConfigPath2();
848
+ const configPath = getConfigPath();
1107
849
  const existing = loadServerConfig();
1108
850
  if (existing) {
1109
851
  printInfo("Found existing", configPath);
@@ -1146,9 +888,9 @@ async function runInit() {
1146
888
  tags,
1147
889
  url
1148
890
  };
1149
- fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
891
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1150
892
  console.log("");
1151
- printSuccess(`Saved ${CONFIG_FILENAME2}`);
893
+ printSuccess(`Saved ${CONFIG_FILENAME}`);
1152
894
  console.log("");
1153
895
  printInfo("Name", config.name || "(not set)");
1154
896
  printInfo("Description", config.description || "(not set)");
@@ -1169,63 +911,63 @@ async function runInit() {
1169
911
  // src/commands/deploy.ts
1170
912
  import { execSync as execSync2, spawnSync } from "child_process";
1171
913
  import crypto3 from "crypto";
1172
- import fs6 from "fs";
1173
- import os2 from "os";
1174
- import path5 from "path";
914
+ import fs5 from "fs";
915
+ import os from "os";
916
+ import path4 from "path";
1175
917
  import readline3 from "readline/promises";
1176
918
  import { createRequire } from "module";
1177
- import { ZooidClient as ZooidClient2 } from "@zooid/sdk";
919
+ import { ZooidClient } from "@zooid/sdk";
1178
920
  var require2 = createRequire(import.meta.url);
1179
921
  function resolvePackageDir(packageName) {
1180
922
  const pkgJson = require2.resolve(`${packageName}/package.json`);
1181
- return path5.dirname(pkgJson);
923
+ return path4.dirname(pkgJson);
1182
924
  }
1183
925
  function prepareStagingDir() {
1184
926
  const serverDir = resolvePackageDir("@zooid/server");
1185
- const serverRequire = createRequire(path5.join(serverDir, "package.json"));
1186
- const webDir = path5.dirname(serverRequire.resolve("@zooid/web/package.json"));
1187
- const webDistDir = path5.join(webDir, "dist");
1188
- if (!fs6.existsSync(path5.join(serverDir, "wrangler.toml"))) {
927
+ const serverRequire = createRequire(path4.join(serverDir, "package.json"));
928
+ const webDir = path4.dirname(serverRequire.resolve("@zooid/web/package.json"));
929
+ const webDistDir = path4.join(webDir, "dist");
930
+ if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
1189
931
  throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
1190
932
  }
1191
- if (!fs6.existsSync(webDistDir)) {
933
+ if (!fs5.existsSync(webDistDir)) {
1192
934
  throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
1193
935
  }
1194
- const tmpDir = fs6.mkdtempSync(path5.join(os2.tmpdir(), "zooid-deploy-"));
1195
- copyDirSync(path5.join(serverDir, "src"), path5.join(tmpDir, "src"));
1196
- copyDirSync(webDistDir, path5.join(tmpDir, "web-dist"));
1197
- let toml = fs6.readFileSync(path5.join(serverDir, "wrangler.toml"), "utf-8");
936
+ const tmpDir = fs5.mkdtempSync(path4.join(os.tmpdir(), "zooid-deploy-"));
937
+ copyDirSync(path4.join(serverDir, "src"), path4.join(tmpDir, "src"));
938
+ copyDirSync(webDistDir, path4.join(tmpDir, "web-dist"));
939
+ let toml = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
1198
940
  toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
1199
- fs6.writeFileSync(path5.join(tmpDir, "wrangler.toml"), toml);
941
+ fs5.writeFileSync(path4.join(tmpDir, "wrangler.toml"), toml);
1200
942
  const nodeModules = findServerNodeModules(serverDir);
1201
943
  if (nodeModules) {
1202
- fs6.symlinkSync(nodeModules, path5.join(tmpDir, "node_modules"), "junction");
944
+ fs5.symlinkSync(nodeModules, path4.join(tmpDir, "node_modules"), "junction");
1203
945
  }
1204
946
  return tmpDir;
1205
947
  }
1206
948
  function findServerNodeModules(serverDir) {
1207
- const local = path5.join(serverDir, "node_modules");
1208
- if (fs6.existsSync(path5.join(local, "hono"))) return local;
1209
- const storeNodeModules = path5.resolve(serverDir, "..", "..");
1210
- if (fs6.existsSync(path5.join(storeNodeModules, "hono")))
949
+ const local = path4.join(serverDir, "node_modules");
950
+ if (fs5.existsSync(path4.join(local, "hono"))) return local;
951
+ const storeNodeModules = path4.resolve(serverDir, "..", "..");
952
+ if (fs5.existsSync(path4.join(storeNodeModules, "hono")))
1211
953
  return storeNodeModules;
1212
954
  let dir = serverDir;
1213
- while (dir !== path5.dirname(dir)) {
1214
- dir = path5.dirname(dir);
1215
- const nm = path5.join(dir, "node_modules");
1216
- if (fs6.existsSync(path5.join(nm, "hono"))) return nm;
955
+ while (dir !== path4.dirname(dir)) {
956
+ dir = path4.dirname(dir);
957
+ const nm = path4.join(dir, "node_modules");
958
+ if (fs5.existsSync(path4.join(nm, "hono"))) return nm;
1217
959
  }
1218
960
  return null;
1219
961
  }
1220
962
  function copyDirSync(src, dest) {
1221
- fs6.mkdirSync(dest, { recursive: true });
1222
- for (const entry of fs6.readdirSync(src, { withFileTypes: true })) {
1223
- const srcPath = path5.join(src, entry.name);
1224
- const destPath = path5.join(dest, entry.name);
963
+ fs5.mkdirSync(dest, { recursive: true });
964
+ for (const entry of fs5.readdirSync(src, { withFileTypes: true })) {
965
+ const srcPath = path4.join(src, entry.name);
966
+ const destPath = path4.join(dest, entry.name);
1225
967
  if (entry.isDirectory()) {
1226
968
  copyDirSync(srcPath, destPath);
1227
969
  } else {
1228
- fs6.copyFileSync(srcPath, destPath);
970
+ fs5.copyFileSync(srcPath, destPath);
1229
971
  }
1230
972
  }
1231
973
  }
@@ -1279,9 +1021,9 @@ function parseDeployUrls(output) {
1279
1021
  };
1280
1022
  }
1281
1023
  function loadDotEnv() {
1282
- const envPath = path5.join(process.cwd(), ".env");
1283
- if (!fs6.existsSync(envPath)) return {};
1284
- const content = fs6.readFileSync(envPath, "utf-8");
1024
+ const envPath = path4.join(process.cwd(), ".env");
1025
+ if (!fs5.existsSync(envPath)) return {};
1026
+ const content = fs5.readFileSync(envPath, "utf-8");
1285
1027
  const tokenMatch = content.match(/^CLOUDFLARE_API_TOKEN=(.+)$/m);
1286
1028
  const accountMatch = content.match(/^CLOUDFLARE_ACCOUNT_ID=(.+)$/m);
1287
1029
  return {
@@ -1386,8 +1128,8 @@ async function runDeploy() {
1386
1128
  }
1387
1129
  const databaseId = dbIdMatch[1];
1388
1130
  printSuccess(`D1 database created (${databaseId})`);
1389
- const wranglerTomlPath = path5.join(stagingDir, "wrangler.toml");
1390
- let tomlContent = fs6.readFileSync(wranglerTomlPath, "utf-8");
1131
+ const wranglerTomlPath = path4.join(stagingDir, "wrangler.toml");
1132
+ let tomlContent = fs5.readFileSync(wranglerTomlPath, "utf-8");
1391
1133
  tomlContent = tomlContent.replace(
1392
1134
  /name = "[^"]*"/,
1393
1135
  `name = "${workerName}"`
@@ -1404,10 +1146,10 @@ async function runDeploy() {
1404
1146
  /ZOOID_SERVER_ID = "[^"]*"/,
1405
1147
  `ZOOID_SERVER_ID = "${serverSlug}"`
1406
1148
  );
1407
- fs6.writeFileSync(wranglerTomlPath, tomlContent);
1149
+ fs5.writeFileSync(wranglerTomlPath, tomlContent);
1408
1150
  printSuccess("Configured wrangler.toml");
1409
- const schemaPath = path5.join(stagingDir, "src/db/schema.sql");
1410
- if (fs6.existsSync(schemaPath)) {
1151
+ const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
1152
+ if (fs5.existsSync(schemaPath)) {
1411
1153
  printStep("Running database schema migration...");
1412
1154
  wrangler(
1413
1155
  `d1 execute ${dbName} --remote --file=${schemaPath}`,
@@ -1462,8 +1204,8 @@ async function runDeploy() {
1462
1204
  console.log("");
1463
1205
  printInfo("Deploy type", "Redeploying existing server");
1464
1206
  console.log("");
1465
- const wranglerTomlPath = path5.join(stagingDir, "wrangler.toml");
1466
- let tomlContent = fs6.readFileSync(wranglerTomlPath, "utf-8");
1207
+ const wranglerTomlPath = path4.join(stagingDir, "wrangler.toml");
1208
+ let tomlContent = fs5.readFileSync(wranglerTomlPath, "utf-8");
1467
1209
  tomlContent = tomlContent.replace(
1468
1210
  /name = "[^"]*"/,
1469
1211
  `name = "${workerName}"`
@@ -1488,9 +1230,9 @@ async function runDeploy() {
1488
1230
  }
1489
1231
  } catch {
1490
1232
  }
1491
- fs6.writeFileSync(wranglerTomlPath, tomlContent);
1492
- const schemaPath = path5.join(stagingDir, "src/db/schema.sql");
1493
- if (fs6.existsSync(schemaPath)) {
1233
+ fs5.writeFileSync(wranglerTomlPath, tomlContent);
1234
+ const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
1235
+ if (fs5.existsSync(schemaPath)) {
1494
1236
  printStep("Running schema migration...");
1495
1237
  wrangler(
1496
1238
  `d1 execute ${dbName} --remote --file=${schemaPath}`,
@@ -1499,29 +1241,21 @@ async function runDeploy() {
1499
1241
  );
1500
1242
  printSuccess("Schema up to date");
1501
1243
  }
1502
- const migrations = [
1503
- "ALTER TABLE events ADD COLUMN publisher_name TEXT",
1504
- "ALTER TABLE channels ADD COLUMN config TEXT"
1505
- ];
1506
- for (const sql of migrations) {
1507
- try {
1508
- wrangler(
1509
- `d1 execute ${dbName} --remote --command="${sql}"`,
1510
- stagingDir,
1511
- creds
1512
- );
1513
- } catch {
1244
+ const migrationsDir = path4.join(stagingDir, "src/db/migrations");
1245
+ if (fs5.existsSync(migrationsDir)) {
1246
+ const migrationFiles = fs5.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
1247
+ for (const file of migrationFiles) {
1248
+ const migrationPath = path4.join(migrationsDir, file);
1249
+ try {
1250
+ wrangler(
1251
+ `d1 execute ${dbName} --remote --file=${migrationPath}`,
1252
+ stagingDir,
1253
+ creds
1254
+ );
1255
+ } catch {
1256
+ }
1514
1257
  }
1515
1258
  }
1516
- try {
1517
- const dataMigrationSql = `UPDATE channels SET config = json_object('types', (SELECT json_group_object(key, json_object('schema', json_each.value)) FROM json_each(schema))) WHERE schema IS NOT NULL AND config IS NULL`;
1518
- wrangler(
1519
- `d1 execute ${dbName} --remote --command="${dataMigrationSql}"`,
1520
- stagingDir,
1521
- creds
1522
- );
1523
- } catch {
1524
- }
1525
1259
  try {
1526
1260
  const keysOutput = wrangler(
1527
1261
  `d1 execute ${dbName} --remote --json --command="SELECT kid FROM trusted_keys WHERE issuer = 'local' LIMIT 1"`,
@@ -1583,9 +1317,7 @@ async function runDeploy() {
1583
1317
  adminToken = existingConfig.admin_token;
1584
1318
  }
1585
1319
  if (!adminToken) {
1586
- printError(
1587
- "No admin token found in ~/.zooid/config.json for this server"
1588
- );
1320
+ printError("No admin token found in ~/.zooid/state.json for this server");
1589
1321
  console.log(
1590
1322
  "If this is a first deploy, remove the D1 database and try again."
1591
1323
  );
@@ -1607,7 +1339,7 @@ async function runDeploy() {
1607
1339
  await new Promise((r) => setTimeout(r, 2e3));
1608
1340
  if (canonicalUrl && adminToken) {
1609
1341
  try {
1610
- const client = new ZooidClient2({
1342
+ const client = new ZooidClient({
1611
1343
  server: canonicalUrl,
1612
1344
  token: adminToken
1613
1345
  });
@@ -1639,7 +1371,7 @@ async function runDeploy() {
1639
1371
  configToSave.channels = {};
1640
1372
  }
1641
1373
  saveConfig(configToSave, canonicalUrl || void 0);
1642
- printSuccess("Saved connection config to ~/.zooid/config.json");
1374
+ printSuccess("Saved connection config to ~/.zooid/state.json");
1643
1375
  cleanup(stagingDir);
1644
1376
  console.log("");
1645
1377
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -1653,7 +1385,7 @@ async function runDeploy() {
1653
1385
  if (isFirstDeploy) {
1654
1386
  printInfo("Admin token", adminToken.slice(0, 20) + "...");
1655
1387
  }
1656
- printInfo("Config", "~/.zooid/config.json");
1388
+ printInfo("Config", "~/.zooid/state.json");
1657
1389
  console.log("");
1658
1390
  if (isFirstDeploy) {
1659
1391
  console.log(" Next steps:");
@@ -1666,7 +1398,7 @@ async function runDeploy() {
1666
1398
  }
1667
1399
  function cleanup(dir) {
1668
1400
  try {
1669
- fs6.rmSync(dir, { recursive: true, force: true });
1401
+ fs5.rmSync(dir, { recursive: true, force: true });
1670
1402
  } catch {
1671
1403
  }
1672
1404
  }
@@ -1694,13 +1426,13 @@ async function resolveAndRecord(channel, opts) {
1694
1426
  return result;
1695
1427
  }
1696
1428
  var program = new Command();
1697
- program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.0.0");
1429
+ program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.2.1");
1698
1430
  var telemetryCtx = { startTime: 0 };
1699
1431
  function setTelemetryChannel(channelId) {
1700
1432
  telemetryCtx.channelId = channelId;
1701
1433
  const config = loadConfig();
1702
1434
  const channelTokens = config.channels?.[channelId];
1703
- const hasChannelToken = !!(channelTokens?.publish_token || channelTokens?.subscribe_token);
1435
+ const hasChannelToken = !!(channelTokens?.token || channelTokens?.publish_token || channelTokens?.subscribe_token);
1704
1436
  telemetryCtx.usedToken = hasChannelToken || !!config.admin_token;
1705
1437
  }
1706
1438
  function getCommandPath(cmd) {
@@ -1804,8 +1536,8 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
1804
1536
  try {
1805
1537
  let config;
1806
1538
  if (opts.schema) {
1807
- const fs7 = await import("fs");
1808
- const raw = fs7.readFileSync(opts.schema, "utf-8");
1539
+ const fs6 = await import("fs");
1540
+ const raw = fs6.readFileSync(opts.schema, "utf-8");
1809
1541
  const parsed = JSON.parse(raw);
1810
1542
  const types = {};
1811
1543
  for (const [eventType, schemaDef] of Object.entries(parsed)) {
@@ -1821,8 +1553,7 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
1821
1553
  config
1822
1554
  });
1823
1555
  printSuccess(`Created channel: ${id}`);
1824
- printInfo("Publish token", result.publish_token);
1825
- printInfo("Subscribe token", result.subscribe_token);
1556
+ printInfo("Token", result.token);
1826
1557
  } catch (err) {
1827
1558
  handleError("channel create", err);
1828
1559
  }
@@ -1840,8 +1571,8 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
1840
1571
  if (opts.public) fields.is_public = true;
1841
1572
  if (opts.private) fields.is_public = false;
1842
1573
  if (opts.schema) {
1843
- const fs7 = await import("fs");
1844
- const raw = fs7.readFileSync(opts.schema, "utf-8");
1574
+ const fs6 = await import("fs");
1575
+ const raw = fs6.readFileSync(opts.schema, "utf-8");
1845
1576
  const parsed = JSON.parse(raw);
1846
1577
  const types = {};
1847
1578
  for (const [eventType, schemaDef] of Object.entries(parsed)) {
@@ -1930,9 +1661,40 @@ program.command("tail <channel>").description("Fetch latest events, or stream li
1930
1661
  "--mode <mode>",
1931
1662
  "Transport mode for follow: auto, ws, or poll",
1932
1663
  "auto"
1933
- ).option("--interval <ms>", "Poll interval in ms for follow mode", "5000").option("--token <token>", "Auth token (for remote/private channels)").action(async (channel, opts) => {
1664
+ ).option("--interval <ms>", "Poll interval in ms for follow mode", "5000").option("--unseen", "Only show events since your last tail").option("--token <token>", "Auth token (for remote/private channels)").action(async (channel, opts) => {
1934
1665
  try {
1935
- const { client, channelId } = await resolveAndRecord(channel, opts);
1666
+ let unseenCursor;
1667
+ let unseenSince;
1668
+ if (opts.unseen) {
1669
+ const file = loadConfigFile();
1670
+ const { parseChannelUrl } = await import("./client-4VMFEFDX.js");
1671
+ const { resolveServer: resolveServer2 } = await import("./config-2KK5GX42.js");
1672
+ const parsed = parseChannelUrl(channel);
1673
+ const channelId2 = parsed?.channelId ?? channel;
1674
+ const serverUrl = parsed?.server ?? resolveServer2();
1675
+ const stats = serverUrl ? file.servers?.[serverUrl]?.channels?.[channelId2]?.stats : void 0;
1676
+ if (!stats) {
1677
+ throw new Error(
1678
+ `No tail history for ${channelId2} \u2014 tail it at least once first`
1679
+ );
1680
+ }
1681
+ if (stats.last_event_id) {
1682
+ unseenCursor = stats.last_event_id;
1683
+ } else if (stats.last_tailed_at) {
1684
+ unseenSince = stats.last_tailed_at;
1685
+ }
1686
+ }
1687
+ const { client, channelId, server } = await resolveAndRecord(
1688
+ channel,
1689
+ opts
1690
+ );
1691
+ if (opts.unseen) {
1692
+ if (unseenCursor) {
1693
+ opts.cursor = unseenCursor;
1694
+ } else if (unseenSince) {
1695
+ opts.since = unseenSince;
1696
+ }
1697
+ }
1936
1698
  if (opts.follow) {
1937
1699
  const mode = opts.mode;
1938
1700
  const transport = mode === "auto" ? "auto (WebSocket \u2192 poll fallback)" : mode;
@@ -1961,6 +1723,10 @@ program.command("tail <channel>").description("Fetch latest events, or stream li
1961
1723
  },
1962
1724
  client
1963
1725
  );
1726
+ if (result.events.length > 0) {
1727
+ const lastEvent = result.events[result.events.length - 1];
1728
+ recordTailHistory(channelId, server, void 0, lastEvent.id);
1729
+ }
1964
1730
  if (result.events.length === 0) {
1965
1731
  console.log("No events.");
1966
1732
  } else {
@@ -2048,22 +1814,25 @@ serverCmd.command("set").description("Update server metadata").option("--name <n
2048
1814
  handleError("server set", err);
2049
1815
  }
2050
1816
  });
2051
- program.command("token <scope>").description("Mint a new token (admin, publish, or subscribe)").argument("[channels...]", "Channels to scope the token to").option("--sub <sub>", "Subject identifier (e.g. publisher ID)").option("--name <name>", "Display name (used for publisher identity)").option("--expires-in <duration>", "Token expiry (e.g. 5m, 1h, 7d, 30d)").action(async (scope, channels, opts) => {
1817
+ program.command("token").description(
1818
+ "Mint a new token. Scopes: admin, pub:<channel>, sub:<channel>. Wildcards: pub:*, sub:prefix-*"
1819
+ ).argument(
1820
+ "<scopes...>",
1821
+ "Scopes to grant (e.g. admin, pub:my-channel, sub:*)"
1822
+ ).option("--sub <sub>", "Subject identifier (e.g. publisher ID)").option("--name <name>", "Display name (used for publisher identity)").option("--expires-in <duration>", "Token expiry (e.g. 5m, 1h, 7d, 30d)").action(async (scopes, opts) => {
2052
1823
  try {
2053
- if (!["admin", "publish", "subscribe"].includes(scope)) {
2054
- throw new Error(
2055
- `Invalid scope "${scope}". Must be one of: admin, publish, subscribe`
2056
- );
2057
- }
2058
- const result = await runTokenMint(
2059
- scope,
2060
- {
2061
- channels: channels.length > 0 ? channels : void 0,
2062
- sub: opts.sub,
2063
- name: opts.name,
2064
- expiresIn: opts.expiresIn
1824
+ for (const s of scopes) {
1825
+ if (s !== "admin" && !s.startsWith("pub:") && !s.startsWith("sub:")) {
1826
+ throw new Error(
1827
+ `Invalid scope "${s}". Must be "admin", "pub:<channel>", or "sub:<channel>"`
1828
+ );
2065
1829
  }
2066
- );
1830
+ }
1831
+ const result = await runTokenMint(scopes, {
1832
+ sub: opts.sub,
1833
+ name: opts.name,
1834
+ expiresIn: opts.expiresIn
1835
+ });
2067
1836
  console.log(result.token);
2068
1837
  } catch (err) {
2069
1838
  handleError("token", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zooid",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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/sdk": "^0.2.0",
28
- "@zooid/server": "^0.2.0",
29
- "@zooid/types": "^0.2.0"
27
+ "@zooid/types": "^0.3.0",
28
+ "@zooid/sdk": "^0.3.0",
29
+ "@zooid/server": "^0.3.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@cloudflare/vitest-pool-workers": "^0.8.34",