zooid 0.2.0 → 0.2.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.
@@ -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,142 @@
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 createChannelClient(channelId, tokenType) {
21
+ const config = loadConfig();
22
+ const server = config.server;
23
+ if (!server) {
24
+ throw new Error(
25
+ "No server configured. Run: npx zooid config set server <url>"
26
+ );
27
+ }
28
+ const tokenKey = tokenType === "publish" ? "publish_token" : "subscribe_token";
29
+ const channelToken = config.channels?.[channelId]?.[tokenKey];
30
+ return new ZooidClient({ server, token: channelToken ?? config.admin_token });
31
+ }
32
+ var createPublishClient = (channelId) => createChannelClient(channelId, "publish");
33
+ var createSubscribeClient = (channelId) => createChannelClient(channelId, "subscribe");
34
+ 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+)$/;
35
+ function normalizeServerUrl(url) {
36
+ let normalized = url.replace(/\/+$/, "");
37
+ try {
38
+ const parsed = new URL(normalized);
39
+ if (parsed.protocol === "http:" && !PRIVATE_HOST_RE.test(parsed.hostname)) {
40
+ normalized = normalized.replace(/^http:\/\//, "https://");
41
+ }
42
+ } catch {
43
+ }
44
+ return normalized;
45
+ }
46
+ function parseChannelUrl(channel) {
47
+ let raw = channel;
48
+ if (!raw.startsWith("http") && raw.includes("/")) {
49
+ if (PRIVATE_HOST_RE.test(raw.split(/[:/]/)[0])) {
50
+ raw = `http://${raw}`;
51
+ } else if (raw.includes(".")) {
52
+ raw = `https://${raw}`;
53
+ } else if (/^[^/]+:\d+\//.test(raw)) {
54
+ raw = `http://${raw}`;
55
+ }
56
+ }
57
+ if (!raw.startsWith("http")) return null;
58
+ try {
59
+ const url = new URL(raw);
60
+ const channelsMatch = url.pathname.match(/^\/channels\/([^/]+)/);
61
+ if (channelsMatch) {
62
+ return { server: url.origin, channelId: channelsMatch[1] };
63
+ }
64
+ const segments = url.pathname.split("/").filter(Boolean);
65
+ if (segments.length === 1) {
66
+ return { server: url.origin, channelId: segments[0] };
67
+ }
68
+ } catch {
69
+ }
70
+ return null;
71
+ }
72
+ function resolveChannel(channel, opts) {
73
+ const parsed = parseChannelUrl(channel);
74
+ if (parsed) {
75
+ const { server: server2, channelId } = parsed;
76
+ let token2 = opts?.token;
77
+ let tokenSaved2 = false;
78
+ if (token2 && opts?.tokenType) {
79
+ const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
80
+ saveConfig({ channels: { [channelId]: { [tokenKey]: token2 } } }, server2, {
81
+ setCurrent: false
82
+ });
83
+ tokenSaved2 = true;
84
+ }
85
+ if (!token2) {
86
+ const file = loadConfigFile();
87
+ const channelTokens = file.servers?.[server2]?.channels?.[channelId];
88
+ if (opts?.tokenType === "publish") {
89
+ token2 = channelTokens?.publish_token;
90
+ } else {
91
+ token2 = channelTokens?.subscribe_token;
92
+ }
93
+ }
94
+ return {
95
+ client: new ZooidClient({ server: server2, token: token2 }),
96
+ channelId,
97
+ server: server2,
98
+ tokenSaved: tokenSaved2
99
+ };
100
+ }
101
+ const config = loadConfig();
102
+ const server = config.server;
103
+ if (!server) {
104
+ throw new Error(
105
+ "No server configured. Run: npx zooid config set server <url>"
106
+ );
107
+ }
108
+ let token = opts?.token;
109
+ let tokenSaved = false;
110
+ if (token && opts?.tokenType) {
111
+ const tokenKey = opts.tokenType === "publish" ? "publish_token" : "subscribe_token";
112
+ saveConfig({ channels: { [channel]: { [tokenKey]: token } } });
113
+ tokenSaved = true;
114
+ }
115
+ if (!token) {
116
+ const channelTokens = config.channels?.[channel];
117
+ if (opts?.tokenType === "publish") {
118
+ token = channelTokens?.publish_token ?? config.admin_token;
119
+ } else if (opts?.tokenType === "subscribe") {
120
+ token = channelTokens?.subscribe_token ?? config.admin_token;
121
+ } else {
122
+ token = config.admin_token;
123
+ }
124
+ }
125
+ return {
126
+ client: new ZooidClient({ server, token }),
127
+ channelId: channel,
128
+ server,
129
+ tokenSaved
130
+ };
131
+ }
132
+
133
+ export {
134
+ createClient,
135
+ createChannelClient,
136
+ createPublishClient,
137
+ createSubscribeClient,
138
+ PRIVATE_HOST_RE,
139
+ normalizeServerUrl,
140
+ parseChannelUrl,
141
+ resolveChannel
142
+ };
@@ -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-EEA3FCBS.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-EEA3FCBS.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();
@@ -494,20 +240,20 @@ async function runChannelDelete(channelId, client) {
494
240
  const serverUrl = resolveServer();
495
241
  if (serverUrl && file.servers?.[serverUrl]?.channels?.[channelId]) {
496
242
  delete file.servers[serverUrl].channels[channelId];
497
- const fs7 = await import("fs");
498
- fs7.writeFileSync(getConfigPath(), JSON.stringify(file, null, 2) + "\n");
243
+ const fs6 = await import("fs");
244
+ fs6.writeFileSync(getStatePath(), JSON.stringify(file, null, 2) + "\n");
499
245
  }
500
246
  }
501
247
  }
502
248
 
503
249
  // src/commands/publish.ts
504
- import fs3 from "fs";
250
+ import fs2 from "fs";
505
251
  async function runPublish(channelId, options, client) {
506
252
  const c = client ?? createPublishClient(channelId);
507
253
  let type;
508
254
  let data;
509
255
  if (options.file) {
510
- const raw = fs3.readFileSync(options.file, "utf-8");
256
+ const raw = fs2.readFileSync(options.file, "utf-8");
511
257
  const parsed = JSON.parse(raw);
512
258
  type = parsed.type;
513
259
  data = parsed.data ?? parsed;
@@ -708,13 +454,13 @@ async function deviceAuth() {
708
454
  "Authorization timed out. You need your human to authorize you. Run `npx zooid share` again to retry."
709
455
  );
710
456
  }
711
- async function directoryFetch(path6, options = {}) {
457
+ async function directoryFetch(path5, options = {}) {
712
458
  let token = await ensureDirectoryToken();
713
459
  const doFetch = (t) => {
714
460
  const headers = new Headers(options.headers);
715
461
  headers.set("Authorization", `Bearer ${t}`);
716
462
  headers.set("Content-Type", "application/json");
717
- return fetch(`${DIRECTORY_BASE_URL}${path6}`, { ...options, headers });
463
+ return fetch(`${DIRECTORY_BASE_URL}${path5}`, { ...options, headers });
718
464
  };
719
465
  let res = await doFetch(token);
720
466
  if (res.status === 401) {
@@ -958,10 +704,10 @@ async function runTokenMint(scope, options) {
958
704
  }
959
705
 
960
706
  // src/commands/dev.ts
961
- import { execSync, spawn } from "child_process";
707
+ import { execSync, spawn as spawn2 } from "child_process";
962
708
  import crypto2 from "crypto";
963
- import fs4 from "fs";
964
- import path3 from "path";
709
+ import fs3 from "fs";
710
+ import path2 from "path";
965
711
  import { fileURLToPath } from "url";
966
712
 
967
713
  // src/lib/crypto.ts
@@ -1027,25 +773,25 @@ async function createEdDSAAdminToken(privateKeyJwk, kid) {
1027
773
 
1028
774
  // src/commands/dev.ts
1029
775
  function findServerDir() {
1030
- const cliDir = path3.dirname(fileURLToPath(import.meta.url));
1031
- return path3.resolve(cliDir, "../../server");
776
+ const cliDir = path2.dirname(fileURLToPath(import.meta.url));
777
+ return path2.resolve(cliDir, "../../server");
1032
778
  }
1033
779
  async function runDev(port = 8787) {
1034
780
  const serverDir = findServerDir();
1035
- if (!fs4.existsSync(path3.join(serverDir, "wrangler.toml"))) {
781
+ if (!fs3.existsSync(path2.join(serverDir, "wrangler.toml"))) {
1036
782
  throw new Error(
1037
783
  `Server directory not found at ${serverDir}. Make sure you're running from the zooid monorepo.`
1038
784
  );
1039
785
  }
1040
786
  const jwtSecret = crypto2.randomUUID();
1041
- const devVarsPath = path3.join(serverDir, ".dev.vars");
1042
- fs4.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
787
+ const devVarsPath = path2.join(serverDir, ".dev.vars");
788
+ fs3.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
1043
789
  `);
1044
790
  const adminToken = await createAdminToken(jwtSecret);
1045
791
  const serverUrl = `http://localhost:${port}`;
1046
792
  saveConfig({ admin_token: adminToken }, serverUrl);
1047
- const schemaPath = path3.join(serverDir, "src/db/schema.sql");
1048
- if (fs4.existsSync(schemaPath)) {
793
+ const schemaPath = path2.join(serverDir, "src/db/schema.sql");
794
+ if (fs3.existsSync(schemaPath)) {
1049
795
  try {
1050
796
  execSync(
1051
797
  `npx wrangler d1 execute zooid-db --local --file=${schemaPath}`,
@@ -1062,11 +808,11 @@ async function runDev(port = 8787) {
1062
808
  printSuccess("Local server configured");
1063
809
  printInfo("Server", serverUrl);
1064
810
  printInfo("Admin token", adminToken.slice(0, 20) + "...");
1065
- printInfo("Config saved to", "~/.zooid/config.json");
811
+ printInfo("Config saved to", "~/.zooid/state.json");
1066
812
  console.log("");
1067
813
  console.log("Starting wrangler dev...");
1068
814
  console.log("");
1069
- const child = spawn(
815
+ const child = spawn2(
1070
816
  "npx",
1071
817
  ["wrangler", "dev", "--local", "--port", String(port)],
1072
818
  {
@@ -1085,25 +831,25 @@ async function runDev(port = 8787) {
1085
831
  }
1086
832
 
1087
833
  // src/commands/init.ts
1088
- import fs5 from "fs";
1089
- import path4 from "path";
834
+ import fs4 from "fs";
835
+ import path3 from "path";
1090
836
  import readline2 from "readline/promises";
1091
- var CONFIG_FILENAME2 = "zooid.json";
1092
- function getConfigPath2() {
1093
- return path4.join(process.cwd(), CONFIG_FILENAME2);
837
+ var CONFIG_FILENAME = "zooid.json";
838
+ function getConfigPath() {
839
+ return path3.join(process.cwd(), CONFIG_FILENAME);
1094
840
  }
1095
841
  function loadServerConfig() {
1096
- const configPath = getConfigPath2();
1097
- if (!fs5.existsSync(configPath)) return null;
1098
- const raw = fs5.readFileSync(configPath, "utf-8");
842
+ const configPath = getConfigPath();
843
+ if (!fs4.existsSync(configPath)) return null;
844
+ const raw = fs4.readFileSync(configPath, "utf-8");
1099
845
  return JSON.parse(raw);
1100
846
  }
1101
847
  function saveServerConfig(config) {
1102
- const configPath = getConfigPath2();
1103
- fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
848
+ const configPath = getConfigPath();
849
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1104
850
  }
1105
851
  async function runInit() {
1106
- const configPath = getConfigPath2();
852
+ const configPath = getConfigPath();
1107
853
  const existing = loadServerConfig();
1108
854
  if (existing) {
1109
855
  printInfo("Found existing", configPath);
@@ -1146,9 +892,9 @@ async function runInit() {
1146
892
  tags,
1147
893
  url
1148
894
  };
1149
- fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
895
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1150
896
  console.log("");
1151
- printSuccess(`Saved ${CONFIG_FILENAME2}`);
897
+ printSuccess(`Saved ${CONFIG_FILENAME}`);
1152
898
  console.log("");
1153
899
  printInfo("Name", config.name || "(not set)");
1154
900
  printInfo("Description", config.description || "(not set)");
@@ -1169,63 +915,63 @@ async function runInit() {
1169
915
  // src/commands/deploy.ts
1170
916
  import { execSync as execSync2, spawnSync } from "child_process";
1171
917
  import crypto3 from "crypto";
1172
- import fs6 from "fs";
1173
- import os2 from "os";
1174
- import path5 from "path";
918
+ import fs5 from "fs";
919
+ import os from "os";
920
+ import path4 from "path";
1175
921
  import readline3 from "readline/promises";
1176
922
  import { createRequire } from "module";
1177
- import { ZooidClient as ZooidClient2 } from "@zooid/sdk";
923
+ import { ZooidClient } from "@zooid/sdk";
1178
924
  var require2 = createRequire(import.meta.url);
1179
925
  function resolvePackageDir(packageName) {
1180
926
  const pkgJson = require2.resolve(`${packageName}/package.json`);
1181
- return path5.dirname(pkgJson);
927
+ return path4.dirname(pkgJson);
1182
928
  }
1183
929
  function prepareStagingDir() {
1184
930
  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"))) {
931
+ const serverRequire = createRequire(path4.join(serverDir, "package.json"));
932
+ const webDir = path4.dirname(serverRequire.resolve("@zooid/web/package.json"));
933
+ const webDistDir = path4.join(webDir, "dist");
934
+ if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
1189
935
  throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
1190
936
  }
1191
- if (!fs6.existsSync(webDistDir)) {
937
+ if (!fs5.existsSync(webDistDir)) {
1192
938
  throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
1193
939
  }
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");
940
+ const tmpDir = fs5.mkdtempSync(path4.join(os.tmpdir(), "zooid-deploy-"));
941
+ copyDirSync(path4.join(serverDir, "src"), path4.join(tmpDir, "src"));
942
+ copyDirSync(webDistDir, path4.join(tmpDir, "web-dist"));
943
+ let toml = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
1198
944
  toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
1199
- fs6.writeFileSync(path5.join(tmpDir, "wrangler.toml"), toml);
945
+ fs5.writeFileSync(path4.join(tmpDir, "wrangler.toml"), toml);
1200
946
  const nodeModules = findServerNodeModules(serverDir);
1201
947
  if (nodeModules) {
1202
- fs6.symlinkSync(nodeModules, path5.join(tmpDir, "node_modules"), "junction");
948
+ fs5.symlinkSync(nodeModules, path4.join(tmpDir, "node_modules"), "junction");
1203
949
  }
1204
950
  return tmpDir;
1205
951
  }
1206
952
  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")))
953
+ const local = path4.join(serverDir, "node_modules");
954
+ if (fs5.existsSync(path4.join(local, "hono"))) return local;
955
+ const storeNodeModules = path4.resolve(serverDir, "..", "..");
956
+ if (fs5.existsSync(path4.join(storeNodeModules, "hono")))
1211
957
  return storeNodeModules;
1212
958
  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;
959
+ while (dir !== path4.dirname(dir)) {
960
+ dir = path4.dirname(dir);
961
+ const nm = path4.join(dir, "node_modules");
962
+ if (fs5.existsSync(path4.join(nm, "hono"))) return nm;
1217
963
  }
1218
964
  return null;
1219
965
  }
1220
966
  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);
967
+ fs5.mkdirSync(dest, { recursive: true });
968
+ for (const entry of fs5.readdirSync(src, { withFileTypes: true })) {
969
+ const srcPath = path4.join(src, entry.name);
970
+ const destPath = path4.join(dest, entry.name);
1225
971
  if (entry.isDirectory()) {
1226
972
  copyDirSync(srcPath, destPath);
1227
973
  } else {
1228
- fs6.copyFileSync(srcPath, destPath);
974
+ fs5.copyFileSync(srcPath, destPath);
1229
975
  }
1230
976
  }
1231
977
  }
@@ -1279,9 +1025,9 @@ function parseDeployUrls(output) {
1279
1025
  };
1280
1026
  }
1281
1027
  function loadDotEnv() {
1282
- const envPath = path5.join(process.cwd(), ".env");
1283
- if (!fs6.existsSync(envPath)) return {};
1284
- const content = fs6.readFileSync(envPath, "utf-8");
1028
+ const envPath = path4.join(process.cwd(), ".env");
1029
+ if (!fs5.existsSync(envPath)) return {};
1030
+ const content = fs5.readFileSync(envPath, "utf-8");
1285
1031
  const tokenMatch = content.match(/^CLOUDFLARE_API_TOKEN=(.+)$/m);
1286
1032
  const accountMatch = content.match(/^CLOUDFLARE_ACCOUNT_ID=(.+)$/m);
1287
1033
  return {
@@ -1386,8 +1132,8 @@ async function runDeploy() {
1386
1132
  }
1387
1133
  const databaseId = dbIdMatch[1];
1388
1134
  printSuccess(`D1 database created (${databaseId})`);
1389
- const wranglerTomlPath = path5.join(stagingDir, "wrangler.toml");
1390
- let tomlContent = fs6.readFileSync(wranglerTomlPath, "utf-8");
1135
+ const wranglerTomlPath = path4.join(stagingDir, "wrangler.toml");
1136
+ let tomlContent = fs5.readFileSync(wranglerTomlPath, "utf-8");
1391
1137
  tomlContent = tomlContent.replace(
1392
1138
  /name = "[^"]*"/,
1393
1139
  `name = "${workerName}"`
@@ -1404,10 +1150,10 @@ async function runDeploy() {
1404
1150
  /ZOOID_SERVER_ID = "[^"]*"/,
1405
1151
  `ZOOID_SERVER_ID = "${serverSlug}"`
1406
1152
  );
1407
- fs6.writeFileSync(wranglerTomlPath, tomlContent);
1153
+ fs5.writeFileSync(wranglerTomlPath, tomlContent);
1408
1154
  printSuccess("Configured wrangler.toml");
1409
- const schemaPath = path5.join(stagingDir, "src/db/schema.sql");
1410
- if (fs6.existsSync(schemaPath)) {
1155
+ const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
1156
+ if (fs5.existsSync(schemaPath)) {
1411
1157
  printStep("Running database schema migration...");
1412
1158
  wrangler(
1413
1159
  `d1 execute ${dbName} --remote --file=${schemaPath}`,
@@ -1462,8 +1208,8 @@ async function runDeploy() {
1462
1208
  console.log("");
1463
1209
  printInfo("Deploy type", "Redeploying existing server");
1464
1210
  console.log("");
1465
- const wranglerTomlPath = path5.join(stagingDir, "wrangler.toml");
1466
- let tomlContent = fs6.readFileSync(wranglerTomlPath, "utf-8");
1211
+ const wranglerTomlPath = path4.join(stagingDir, "wrangler.toml");
1212
+ let tomlContent = fs5.readFileSync(wranglerTomlPath, "utf-8");
1467
1213
  tomlContent = tomlContent.replace(
1468
1214
  /name = "[^"]*"/,
1469
1215
  `name = "${workerName}"`
@@ -1488,9 +1234,9 @@ async function runDeploy() {
1488
1234
  }
1489
1235
  } catch {
1490
1236
  }
1491
- fs6.writeFileSync(wranglerTomlPath, tomlContent);
1492
- const schemaPath = path5.join(stagingDir, "src/db/schema.sql");
1493
- if (fs6.existsSync(schemaPath)) {
1237
+ fs5.writeFileSync(wranglerTomlPath, tomlContent);
1238
+ const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
1239
+ if (fs5.existsSync(schemaPath)) {
1494
1240
  printStep("Running schema migration...");
1495
1241
  wrangler(
1496
1242
  `d1 execute ${dbName} --remote --file=${schemaPath}`,
@@ -1583,9 +1329,7 @@ async function runDeploy() {
1583
1329
  adminToken = existingConfig.admin_token;
1584
1330
  }
1585
1331
  if (!adminToken) {
1586
- printError(
1587
- "No admin token found in ~/.zooid/config.json for this server"
1588
- );
1332
+ printError("No admin token found in ~/.zooid/state.json for this server");
1589
1333
  console.log(
1590
1334
  "If this is a first deploy, remove the D1 database and try again."
1591
1335
  );
@@ -1607,7 +1351,7 @@ async function runDeploy() {
1607
1351
  await new Promise((r) => setTimeout(r, 2e3));
1608
1352
  if (canonicalUrl && adminToken) {
1609
1353
  try {
1610
- const client = new ZooidClient2({
1354
+ const client = new ZooidClient({
1611
1355
  server: canonicalUrl,
1612
1356
  token: adminToken
1613
1357
  });
@@ -1639,7 +1383,7 @@ async function runDeploy() {
1639
1383
  configToSave.channels = {};
1640
1384
  }
1641
1385
  saveConfig(configToSave, canonicalUrl || void 0);
1642
- printSuccess("Saved connection config to ~/.zooid/config.json");
1386
+ printSuccess("Saved connection config to ~/.zooid/state.json");
1643
1387
  cleanup(stagingDir);
1644
1388
  console.log("");
1645
1389
  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 +1397,7 @@ async function runDeploy() {
1653
1397
  if (isFirstDeploy) {
1654
1398
  printInfo("Admin token", adminToken.slice(0, 20) + "...");
1655
1399
  }
1656
- printInfo("Config", "~/.zooid/config.json");
1400
+ printInfo("Config", "~/.zooid/state.json");
1657
1401
  console.log("");
1658
1402
  if (isFirstDeploy) {
1659
1403
  console.log(" Next steps:");
@@ -1666,7 +1410,7 @@ async function runDeploy() {
1666
1410
  }
1667
1411
  function cleanup(dir) {
1668
1412
  try {
1669
- fs6.rmSync(dir, { recursive: true, force: true });
1413
+ fs5.rmSync(dir, { recursive: true, force: true });
1670
1414
  } catch {
1671
1415
  }
1672
1416
  }
@@ -1694,7 +1438,7 @@ async function resolveAndRecord(channel, opts) {
1694
1438
  return result;
1695
1439
  }
1696
1440
  var program = new Command();
1697
- program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.0.0");
1441
+ program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.2.0");
1698
1442
  var telemetryCtx = { startTime: 0 };
1699
1443
  function setTelemetryChannel(channelId) {
1700
1444
  telemetryCtx.channelId = channelId;
@@ -1804,8 +1548,8 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
1804
1548
  try {
1805
1549
  let config;
1806
1550
  if (opts.schema) {
1807
- const fs7 = await import("fs");
1808
- const raw = fs7.readFileSync(opts.schema, "utf-8");
1551
+ const fs6 = await import("fs");
1552
+ const raw = fs6.readFileSync(opts.schema, "utf-8");
1809
1553
  const parsed = JSON.parse(raw);
1810
1554
  const types = {};
1811
1555
  for (const [eventType, schemaDef] of Object.entries(parsed)) {
@@ -1840,8 +1584,8 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
1840
1584
  if (opts.public) fields.is_public = true;
1841
1585
  if (opts.private) fields.is_public = false;
1842
1586
  if (opts.schema) {
1843
- const fs7 = await import("fs");
1844
- const raw = fs7.readFileSync(opts.schema, "utf-8");
1587
+ const fs6 = await import("fs");
1588
+ const raw = fs6.readFileSync(opts.schema, "utf-8");
1845
1589
  const parsed = JSON.parse(raw);
1846
1590
  const types = {};
1847
1591
  for (const [eventType, schemaDef] of Object.entries(parsed)) {
@@ -1930,9 +1674,40 @@ program.command("tail <channel>").description("Fetch latest events, or stream li
1930
1674
  "--mode <mode>",
1931
1675
  "Transport mode for follow: auto, ws, or poll",
1932
1676
  "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) => {
1677
+ ).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
1678
  try {
1935
- const { client, channelId } = await resolveAndRecord(channel, opts);
1679
+ let unseenCursor;
1680
+ let unseenSince;
1681
+ if (opts.unseen) {
1682
+ const file = loadConfigFile();
1683
+ const { parseChannelUrl } = await import("./client-QPT54SNG.js");
1684
+ const { resolveServer: resolveServer2 } = await import("./config-2KK5GX42.js");
1685
+ const parsed = parseChannelUrl(channel);
1686
+ const channelId2 = parsed?.channelId ?? channel;
1687
+ const serverUrl = parsed?.server ?? resolveServer2();
1688
+ const stats = serverUrl ? file.servers?.[serverUrl]?.channels?.[channelId2]?.stats : void 0;
1689
+ if (!stats) {
1690
+ throw new Error(
1691
+ `No tail history for ${channelId2} \u2014 tail it at least once first`
1692
+ );
1693
+ }
1694
+ if (stats.last_event_id) {
1695
+ unseenCursor = stats.last_event_id;
1696
+ } else if (stats.last_tailed_at) {
1697
+ unseenSince = stats.last_tailed_at;
1698
+ }
1699
+ }
1700
+ const { client, channelId, server } = await resolveAndRecord(
1701
+ channel,
1702
+ opts
1703
+ );
1704
+ if (opts.unseen) {
1705
+ if (unseenCursor) {
1706
+ opts.cursor = unseenCursor;
1707
+ } else if (unseenSince) {
1708
+ opts.since = unseenSince;
1709
+ }
1710
+ }
1936
1711
  if (opts.follow) {
1937
1712
  const mode = opts.mode;
1938
1713
  const transport = mode === "auto" ? "auto (WebSocket \u2192 poll fallback)" : mode;
@@ -1961,6 +1736,10 @@ program.command("tail <channel>").description("Fetch latest events, or stream li
1961
1736
  },
1962
1737
  client
1963
1738
  );
1739
+ if (result.events.length > 0) {
1740
+ const lastEvent = result.events[result.events.length - 1];
1741
+ recordTailHistory(channelId, server, void 0, lastEvent.id);
1742
+ }
1964
1743
  if (result.events.length === 0) {
1965
1744
  console.log("No events.");
1966
1745
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zooid",
3
- "version": "0.2.0",
3
+ "version": "0.2.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",
@@ -25,8 +25,8 @@
25
25
  "@inquirer/checkbox": "^5.0.7",
26
26
  "commander": "^14.0.3",
27
27
  "@zooid/sdk": "^0.2.0",
28
- "@zooid/server": "^0.2.0",
29
- "@zooid/types": "^0.2.0"
28
+ "@zooid/types": "^0.2.0",
29
+ "@zooid/server": "^0.2.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@cloudflare/vitest-pool-workers": "^0.8.34",