@meet-ai/cli 0.0.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.
Files changed (2) hide show
  1. package/dist/index.js +403 -0
  2. package/package.json +42 -0
package/dist/index.js ADDED
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/client.ts
4
+ function wsLog(data) {
5
+ const json = JSON.stringify({ ...data, ts: new Date().toISOString() });
6
+ const isSuccess = data.event === "connected" || data.event === "reconnected" || data.event === "catchup";
7
+ console.error(isSuccess ? `\x1B[32m${json}\x1B[0m` : json);
8
+ }
9
+ function isRetryable(error) {
10
+ if (error instanceof TypeError)
11
+ return true;
12
+ if (error instanceof Error && /^HTTP 5\d{2}$/.test(error.message))
13
+ return true;
14
+ return false;
15
+ }
16
+ async function withRetry(fn, options) {
17
+ const maxRetries = options?.maxRetries ?? 3;
18
+ const baseDelay = options?.baseDelay ?? 1000;
19
+ const shouldRetry = options?.shouldRetry ?? isRetryable;
20
+ let lastError;
21
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
22
+ try {
23
+ return await fn();
24
+ } catch (err) {
25
+ lastError = err;
26
+ if (attempt >= maxRetries || !shouldRetry(err))
27
+ throw err;
28
+ const delay = baseDelay * 2 ** attempt;
29
+ console.error(JSON.stringify({
30
+ event: "retry",
31
+ attempt: attempt + 1,
32
+ delay_ms: delay,
33
+ error: err instanceof Error ? err.message : String(err)
34
+ }));
35
+ await new Promise((r) => setTimeout(r, delay));
36
+ }
37
+ }
38
+ throw lastError;
39
+ }
40
+ function createClient(baseUrl, apiKey) {
41
+ function headers(extra) {
42
+ const h = { "Content-Type": "application/json", ...extra };
43
+ if (apiKey) {
44
+ h["Authorization"] = `Bearer ${apiKey}`;
45
+ }
46
+ return h;
47
+ }
48
+ return {
49
+ async createRoom(name) {
50
+ const res = await fetch(`${baseUrl}/api/rooms`, {
51
+ method: "POST",
52
+ headers: headers(),
53
+ body: JSON.stringify({ name })
54
+ });
55
+ if (!res.ok) {
56
+ const err = await res.json();
57
+ throw new Error(err.error ?? `HTTP ${res.status}`);
58
+ }
59
+ return res.json();
60
+ },
61
+ async sendMessage(roomId, sender, content, color) {
62
+ return withRetry(async () => {
63
+ const res = await fetch(`${baseUrl}/api/rooms/${roomId}/messages`, {
64
+ method: "POST",
65
+ headers: headers(),
66
+ body: JSON.stringify({ sender, content, sender_type: "agent", ...color && { color } })
67
+ });
68
+ if (!res.ok) {
69
+ const err = await res.json().catch(() => ({}));
70
+ throw new Error(err.error ?? `HTTP ${res.status}`);
71
+ }
72
+ return res.json();
73
+ });
74
+ },
75
+ async getMessages(roomId, options) {
76
+ const params = new URLSearchParams;
77
+ if (options?.after)
78
+ params.set("after", options.after);
79
+ if (options?.exclude)
80
+ params.set("exclude", options.exclude);
81
+ if (options?.senderType)
82
+ params.set("sender_type", options.senderType);
83
+ const qs = params.toString();
84
+ const url = `${baseUrl}/api/rooms/${roomId}/messages${qs ? `?${qs}` : ""}`;
85
+ const res = await fetch(url, {
86
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined
87
+ });
88
+ if (!res.ok) {
89
+ const err = await res.json();
90
+ throw new Error(err.error ?? `HTTP ${res.status}`);
91
+ }
92
+ return res.json();
93
+ },
94
+ listen(roomId, options) {
95
+ const wsUrl = baseUrl.replace(/^http/, "ws");
96
+ const tokenParam = apiKey ? `?token=${apiKey}` : "";
97
+ let pingInterval = null;
98
+ let reconnectAttempt = 0;
99
+ const seen = new Set;
100
+ let lastSeenId = null;
101
+ function deliver(msg) {
102
+ if (seen.has(msg.id))
103
+ return;
104
+ seen.add(msg.id);
105
+ if (seen.size > 200) {
106
+ const first = seen.values().next().value;
107
+ seen.delete(first);
108
+ }
109
+ lastSeenId = msg.id;
110
+ if (options?.exclude && msg.sender === options.exclude)
111
+ return;
112
+ if (options?.senderType && msg.sender_type !== options.senderType)
113
+ return;
114
+ if (options?.onMessage) {
115
+ options.onMessage(msg);
116
+ } else {
117
+ console.log(JSON.stringify(msg));
118
+ }
119
+ }
120
+ function getReconnectDelay() {
121
+ const delay = Math.min(1000 * 2 ** Math.min(reconnectAttempt, 4), 15000);
122
+ reconnectAttempt++;
123
+ return delay + delay * 0.5 * Math.random();
124
+ }
125
+ const fetchMessages = this.getMessages.bind(this);
126
+ function connect() {
127
+ const ws = new WebSocket(`${wsUrl}/api/rooms/${roomId}/ws${tokenParam}`);
128
+ const connectTimeout = setTimeout(() => {
129
+ if (ws.readyState !== WebSocket.OPEN) {
130
+ wsLog({ event: "timeout", after_ms: 1e4 });
131
+ ws.close();
132
+ }
133
+ }, 1e4);
134
+ ws.onopen = async () => {
135
+ clearTimeout(connectTimeout);
136
+ const wasReconnect = reconnectAttempt > 0;
137
+ reconnectAttempt = 0;
138
+ wsLog({ event: wasReconnect ? "reconnected" : "connected" });
139
+ if (pingInterval)
140
+ clearInterval(pingInterval);
141
+ pingInterval = setInterval(() => {
142
+ if (ws.readyState === WebSocket.OPEN) {
143
+ ws.send(JSON.stringify({ type: "ping" }));
144
+ }
145
+ }, 30000);
146
+ if (lastSeenId) {
147
+ try {
148
+ const missed = await fetchMessages(roomId, { after: lastSeenId });
149
+ if (missed.length)
150
+ wsLog({ event: "catchup", count: missed.length });
151
+ for (const msg of missed)
152
+ deliver(msg);
153
+ } catch {}
154
+ }
155
+ };
156
+ ws.onmessage = (event) => {
157
+ const text = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
158
+ const data = JSON.parse(text);
159
+ if (data.type === "pong")
160
+ return;
161
+ deliver(data);
162
+ };
163
+ ws.onclose = (event) => {
164
+ clearTimeout(connectTimeout);
165
+ if (pingInterval)
166
+ clearInterval(pingInterval);
167
+ const code = event.code;
168
+ if (code === 1000) {
169
+ wsLog({ event: "closed", code: 1000 });
170
+ return;
171
+ }
172
+ const reason = code === 1006 ? "network drop" : code === 1012 ? "service restart" : code === 1013 ? "server back-off" : `code ${code}`;
173
+ const delay = getReconnectDelay();
174
+ wsLog({ event: "disconnected", code, reason });
175
+ wsLog({ event: "reconnecting", attempt: reconnectAttempt, delay_ms: Math.round(delay) });
176
+ setTimeout(connect, delay);
177
+ };
178
+ ws.onerror = () => {};
179
+ return ws;
180
+ }
181
+ return connect();
182
+ },
183
+ async generateKey() {
184
+ const res = await fetch(`${baseUrl}/api/keys`, {
185
+ method: "POST",
186
+ headers: headers()
187
+ });
188
+ if (!res.ok) {
189
+ const err = await res.json();
190
+ throw new Error(err.error ?? `HTTP ${res.status}`);
191
+ }
192
+ return res.json();
193
+ }
194
+ };
195
+ }
196
+
197
+ // src/inbox-router.ts
198
+ import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
199
+ import { dirname } from "path";
200
+ var IDLE_CHECK_INTERVAL_MS = 60000;
201
+ var IDLE_THRESHOLD_MS = 5 * 60 * 1000;
202
+ function appendToInbox(path, entry) {
203
+ mkdirSync(dirname(path), { recursive: true });
204
+ let messages = [];
205
+ try {
206
+ messages = JSON.parse(readFileSync(path, "utf-8"));
207
+ } catch {}
208
+ messages.push(entry);
209
+ writeFileSync(path, JSON.stringify(messages, null, 2));
210
+ }
211
+ function getTeamMembers(teamDir) {
212
+ try {
213
+ const config = JSON.parse(readFileSync(`${teamDir}/config.json`, "utf-8"));
214
+ return new Set(config.members?.map((m) => m.name) || []);
215
+ } catch {
216
+ return new Set;
217
+ }
218
+ }
219
+ function resolveInboxTargets(content, members) {
220
+ const mentions = content.match(/@([\w-]+)/g);
221
+ if (!mentions)
222
+ return null;
223
+ const valid = [...new Set(mentions.map((m) => m.slice(1)))].filter((name) => members.has(name));
224
+ return valid.length > 0 ? valid : null;
225
+ }
226
+ function checkIdleAgents(inboxDir, members, excludeAgent, notified, now = Date.now()) {
227
+ const newlyIdle = [];
228
+ for (const member of members) {
229
+ if (member === excludeAgent)
230
+ continue;
231
+ const inboxPath = `${inboxDir}/${member}.json`;
232
+ let mtime;
233
+ try {
234
+ mtime = statSync(inboxPath).mtimeMs;
235
+ } catch {
236
+ continue;
237
+ }
238
+ const idleMs = now - mtime;
239
+ if (idleMs >= IDLE_THRESHOLD_MS) {
240
+ if (!notified.has(member)) {
241
+ newlyIdle.push(member);
242
+ }
243
+ } else {
244
+ notified.delete(member);
245
+ }
246
+ }
247
+ return newlyIdle;
248
+ }
249
+
250
+ // src/index.ts
251
+ var API_URL = process.env.MEET_AI_URL || "http://localhost:8787";
252
+ var API_KEY = process.env.MEET_AI_KEY;
253
+ var client = createClient(API_URL, API_KEY);
254
+ var [command, ...args] = process.argv.slice(2);
255
+ function parseFlags(args2) {
256
+ const positional = [];
257
+ const flags = {};
258
+ for (let i = 0;i < args2.length; i++) {
259
+ if (args2[i].startsWith("--") && i + 1 < args2.length) {
260
+ flags[args2[i].slice(2)] = args2[i + 1];
261
+ i++;
262
+ } else {
263
+ positional.push(args2[i]);
264
+ }
265
+ }
266
+ return { positional, flags };
267
+ }
268
+ switch (command) {
269
+ case "create-room": {
270
+ const name = args[0];
271
+ if (!name) {
272
+ console.error("Usage: cli create-room <name>");
273
+ process.exit(1);
274
+ }
275
+ const room = await client.createRoom(name);
276
+ console.log(`Room created: ${room.id} (${room.name})`);
277
+ break;
278
+ }
279
+ case "send-message": {
280
+ const { positional: smPos, flags: smFlags } = parseFlags(args);
281
+ const [roomId, sender, ...rest] = smPos;
282
+ const content = rest.join(" ");
283
+ if (!roomId || !sender || !content) {
284
+ console.error("Usage: cli send-message <roomId> <sender> <content> [--color <color>]");
285
+ process.exit(1);
286
+ }
287
+ const msg = await client.sendMessage(roomId, sender, content, smFlags.color);
288
+ console.log(`Message sent: ${msg.id}`);
289
+ break;
290
+ }
291
+ case "poll": {
292
+ const { positional, flags } = parseFlags(args);
293
+ const roomId = positional[0];
294
+ if (!roomId) {
295
+ console.error("Usage: cli poll <roomId> [--after <messageId>] [--exclude <sender>]");
296
+ process.exit(1);
297
+ }
298
+ const messages = await client.getMessages(roomId, {
299
+ after: flags.after,
300
+ exclude: flags.exclude,
301
+ senderType: flags["sender-type"]
302
+ });
303
+ console.log(JSON.stringify(messages));
304
+ break;
305
+ }
306
+ case "listen": {
307
+ let routeToInbox = function(msg) {
308
+ if (!inboxDir)
309
+ return;
310
+ const entry = {
311
+ from: "meet-ai:" + msg.sender,
312
+ text: msg.content,
313
+ timestamp: new Date().toISOString(),
314
+ read: false
315
+ };
316
+ const members = teamDir ? getTeamMembers(teamDir) : new Set;
317
+ const targets = resolveInboxTargets(msg.content, members);
318
+ if (targets) {
319
+ for (const target of targets) {
320
+ appendToInbox(`${inboxDir}/${target}.json`, entry);
321
+ }
322
+ } else if (defaultInboxPath) {
323
+ appendToInbox(defaultInboxPath, entry);
324
+ }
325
+ }, shutdown = function() {
326
+ if (idleCheckTimeout)
327
+ clearTimeout(idleCheckTimeout);
328
+ if (ws.readyState === WebSocket.OPEN) {
329
+ ws.close(1000, "client shutdown");
330
+ }
331
+ process.exit(0);
332
+ };
333
+ const { positional, flags } = parseFlags(args);
334
+ const roomId = positional[0];
335
+ if (!roomId) {
336
+ console.error("Usage: cli listen <roomId> [--exclude <sender>] [--sender-type <type>] [--team <name> --inbox <agent>]");
337
+ process.exit(1);
338
+ }
339
+ const team = flags.team;
340
+ const inbox = flags.inbox;
341
+ const inboxDir = team ? `${process.env.HOME}/.claude/teams/${team}/inboxes` : null;
342
+ const defaultInboxPath = inboxDir && inbox ? `${inboxDir}/${inbox}.json` : null;
343
+ const teamDir = team ? `${process.env.HOME}/.claude/teams/${team}` : null;
344
+ const onMessage = inboxDir ? (msg) => {
345
+ console.log(JSON.stringify(msg));
346
+ routeToInbox(msg);
347
+ } : undefined;
348
+ const ws = client.listen(roomId, { exclude: flags.exclude, senderType: flags["sender-type"], onMessage });
349
+ let idleCheckTimeout = null;
350
+ const idleNotified = new Set;
351
+ if (inboxDir && inbox && teamDir) {
352
+ let scheduleIdleCheck = function() {
353
+ idleCheckTimeout = setTimeout(() => {
354
+ const members = getTeamMembers(teamDir);
355
+ const newlyIdle = checkIdleAgents(inboxDir, members, inbox, idleNotified);
356
+ for (const agent of newlyIdle) {
357
+ idleNotified.add(agent);
358
+ if (defaultInboxPath) {
359
+ appendToInbox(defaultInboxPath, {
360
+ from: "meet-ai:idle-check",
361
+ text: `${agent} idle for 5+ minutes`,
362
+ timestamp: new Date().toISOString(),
363
+ read: false
364
+ });
365
+ }
366
+ }
367
+ scheduleIdleCheck();
368
+ }, IDLE_CHECK_INTERVAL_MS);
369
+ };
370
+ scheduleIdleCheck();
371
+ }
372
+ process.on("SIGINT", shutdown);
373
+ process.on("SIGTERM", shutdown);
374
+ break;
375
+ }
376
+ case "generate-key": {
377
+ const result = await client.generateKey();
378
+ console.log(`API Key: ${result.key}`);
379
+ console.log(`Prefix: ${result.prefix}`);
380
+ break;
381
+ }
382
+ default:
383
+ console.log(`meet-ai CLI
384
+
385
+ Environment variables:
386
+ MEET_AI_URL Server URL (default: http://localhost:8787)
387
+ MEET_AI_KEY API key for authentication (optional for local, required for production)
388
+
389
+ Commands:
390
+ create-room <name> Create a new chat room
391
+ send-message <roomId> <sender> <content> Send a message to a room
392
+ --color <color> Set sender name color (e.g. #ff0000, red)
393
+ poll <roomId> [options] Fetch messages from a room
394
+ --after <id> Only messages after this ID
395
+ --exclude <sender> Exclude messages from sender
396
+ --sender-type <type> Filter by sender_type (human|agent)
397
+ listen <roomId> [options] Stream messages via WebSocket
398
+ --exclude <sender> Exclude messages from sender
399
+ --sender-type <type> Filter by sender_type (human|agent)
400
+ --team <name> Write to Claude Code team inbox
401
+ --inbox <agent> Target agent inbox (requires --team)
402
+ generate-key Generate a new API key`);
403
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@meet-ai/cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI for meet-ai chat rooms — create rooms, send messages, and stream via WebSocket",
5
+ "type": "module",
6
+ "bin": {
7
+ "meet-ai": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "exports": {
11
+ ".": "./dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "start": "bun run src/index.ts",
18
+ "build": "bun build src/index.ts --outdir dist --target node --format esm",
19
+ "prepublishOnly": "bun run build",
20
+ "test": "bun test"
21
+ },
22
+ "keywords": [
23
+ "meet-ai",
24
+ "cli",
25
+ "chat",
26
+ "websocket",
27
+ "real-time"
28
+ ],
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/SoftWare-A-G/meet-ai",
33
+ "directory": "packages/cli"
34
+ },
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "5.9.3"
41
+ }
42
+ }