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 +4 -1
- package/dist/chunk-67ZRMVHO.js +174 -0
- package/dist/chunk-VBGU2NST.js +139 -0
- package/dist/client-4VMFEFDX.js +22 -0
- package/dist/config-2KK5GX42.js +27 -0
- package/dist/index.js +190 -421
- package/package.json +4 -4
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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/
|
|
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
|
-
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
189
61
|
let queue = [];
|
|
190
62
|
try {
|
|
191
|
-
const raw =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
95
|
+
const configPath = getStatePath();
|
|
225
96
|
try {
|
|
226
|
-
const raw =
|
|
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
|
|
107
|
+
return path.join(getConfigDir(), QUEUE_FILENAME);
|
|
237
108
|
}
|
|
238
109
|
function readTelemetryFlag() {
|
|
239
|
-
const configPath =
|
|
110
|
+
const configPath = getStatePath();
|
|
240
111
|
try {
|
|
241
|
-
const raw =
|
|
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 =
|
|
126
|
+
const configPath = getStatePath();
|
|
256
127
|
try {
|
|
257
|
-
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
258
129
|
let config = {};
|
|
259
130
|
try {
|
|
260
|
-
const raw =
|
|
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
|
-
|
|
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
|
|
498
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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}${
|
|
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(
|
|
693
|
+
async function runTokenMint(scopes, options) {
|
|
951
694
|
const client = createClient();
|
|
952
|
-
const body = {
|
|
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
|
|
964
|
-
import
|
|
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 =
|
|
1031
|
-
return
|
|
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 (!
|
|
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 =
|
|
1042
|
-
|
|
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 =
|
|
1048
|
-
if (
|
|
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/
|
|
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 =
|
|
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
|
|
1089
|
-
import
|
|
830
|
+
import fs4 from "fs";
|
|
831
|
+
import path3 from "path";
|
|
1090
832
|
import readline2 from "readline/promises";
|
|
1091
|
-
var
|
|
1092
|
-
function
|
|
1093
|
-
return
|
|
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 =
|
|
1097
|
-
if (!
|
|
1098
|
-
const raw =
|
|
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 =
|
|
1103
|
-
|
|
844
|
+
const configPath = getConfigPath();
|
|
845
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1104
846
|
}
|
|
1105
847
|
async function runInit() {
|
|
1106
|
-
const configPath =
|
|
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
|
-
|
|
891
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1150
892
|
console.log("");
|
|
1151
|
-
printSuccess(`Saved ${
|
|
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
|
|
1173
|
-
import
|
|
1174
|
-
import
|
|
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
|
|
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
|
|
923
|
+
return path4.dirname(pkgJson);
|
|
1182
924
|
}
|
|
1183
925
|
function prepareStagingDir() {
|
|
1184
926
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
1185
|
-
const serverRequire = createRequire(
|
|
1186
|
-
const webDir =
|
|
1187
|
-
const webDistDir =
|
|
1188
|
-
if (!
|
|
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 (!
|
|
933
|
+
if (!fs5.existsSync(webDistDir)) {
|
|
1192
934
|
throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
|
|
1193
935
|
}
|
|
1194
|
-
const tmpDir =
|
|
1195
|
-
copyDirSync(
|
|
1196
|
-
copyDirSync(webDistDir,
|
|
1197
|
-
let toml =
|
|
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
|
-
|
|
941
|
+
fs5.writeFileSync(path4.join(tmpDir, "wrangler.toml"), toml);
|
|
1200
942
|
const nodeModules = findServerNodeModules(serverDir);
|
|
1201
943
|
if (nodeModules) {
|
|
1202
|
-
|
|
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 =
|
|
1208
|
-
if (
|
|
1209
|
-
const storeNodeModules =
|
|
1210
|
-
if (
|
|
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 !==
|
|
1214
|
-
dir =
|
|
1215
|
-
const nm =
|
|
1216
|
-
if (
|
|
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
|
-
|
|
1222
|
-
for (const entry of
|
|
1223
|
-
const srcPath =
|
|
1224
|
-
const destPath =
|
|
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
|
-
|
|
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 =
|
|
1283
|
-
if (!
|
|
1284
|
-
const content =
|
|
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 =
|
|
1390
|
-
let tomlContent =
|
|
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
|
-
|
|
1149
|
+
fs5.writeFileSync(wranglerTomlPath, tomlContent);
|
|
1408
1150
|
printSuccess("Configured wrangler.toml");
|
|
1409
|
-
const schemaPath =
|
|
1410
|
-
if (
|
|
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 =
|
|
1466
|
-
let tomlContent =
|
|
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
|
-
|
|
1492
|
-
const schemaPath =
|
|
1493
|
-
if (
|
|
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
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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
|
|
1808
|
-
const raw =
|
|
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("
|
|
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
|
|
1844
|
-
const raw =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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.
|
|
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/
|
|
28
|
-
"@zooid/
|
|
29
|
-
"@zooid/
|
|
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",
|