@boxcrew/cli 0.1.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,605 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/login.ts
7
+ import { createInterface } from "readline";
8
+ import { createServer } from "http";
9
+ import open from "open";
10
+
11
+ // src/config.ts
12
+ import Conf from "conf";
13
+ var config = new Conf({
14
+ projectName: "boxcrew",
15
+ defaults: {
16
+ apiUrl: "https://api.boxcrew.ai"
17
+ }
18
+ });
19
+
20
+ // src/commands/login.ts
21
+ var DEFAULT_FRONTEND_URL = "https://boxcrew.ai";
22
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
23
+ function registerLoginCommand(program2) {
24
+ program2.command("login").description("Authenticate with BoxCrew").option("--api-url <url>", "BoxCrew API URL").option(
25
+ "--frontend-url <url>",
26
+ "BoxCrew frontend URL",
27
+ process.env.BOXCREW_FRONTEND_URL || DEFAULT_FRONTEND_URL
28
+ ).option("--paste", "Use paste-based login (for headless environments)").action(
29
+ async (options) => {
30
+ if (options.apiUrl) {
31
+ config.set("apiUrl", options.apiUrl);
32
+ }
33
+ if (options.paste) {
34
+ await pasteLogin();
35
+ } else {
36
+ await browserLogin(options.frontendUrl);
37
+ }
38
+ }
39
+ );
40
+ }
41
+ async function pasteLogin() {
42
+ console.log(
43
+ "Create an API key in the BoxCrew dashboard (Settings > API Keys),"
44
+ );
45
+ console.log("then paste it below.\n");
46
+ const rl = createInterface({
47
+ input: process.stdin,
48
+ output: process.stdout
49
+ });
50
+ const apiKey = await new Promise((resolve) => {
51
+ rl.question("API key: ", (answer) => {
52
+ rl.close();
53
+ resolve(answer.trim());
54
+ });
55
+ });
56
+ if (!apiKey.startsWith("bxk_")) {
57
+ console.error('Invalid API key. Keys should start with "bxk_".');
58
+ process.exit(1);
59
+ }
60
+ config.set("apiKey", apiKey);
61
+ console.log("\nAuthenticated successfully! Credentials stored.");
62
+ console.log(`API URL: ${config.get("apiUrl")}`);
63
+ }
64
+ async function browserLogin(frontendUrl) {
65
+ return new Promise((resolve, reject) => {
66
+ const server = createServer((req, res) => {
67
+ const url = new URL(req.url || "/", `http://localhost`);
68
+ if (url.pathname === "/callback") {
69
+ const key = url.searchParams.get("key");
70
+ if (key && key.startsWith("bxk_")) {
71
+ config.set("apiKey", key);
72
+ res.writeHead(200, { "Content-Type": "text/html" });
73
+ res.end(`
74
+ <!DOCTYPE html>
75
+ <html>
76
+ <head><title>BoxCrew CLI</title></head>
77
+ <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa;">
78
+ <div style="text-align: center;">
79
+ <h2>Authenticated!</h2>
80
+ <p>You can close this tab and return to your terminal.</p>
81
+ </div>
82
+ </body>
83
+ </html>
84
+ `);
85
+ console.log("\nAuthenticated successfully! Credentials stored.");
86
+ console.log(`API URL: ${config.get("apiUrl")}`);
87
+ server.close();
88
+ clearTimeout(timeout);
89
+ resolve();
90
+ } else {
91
+ res.writeHead(400, { "Content-Type": "text/plain" });
92
+ res.end("Invalid key");
93
+ }
94
+ } else {
95
+ res.writeHead(404, { "Content-Type": "text/plain" });
96
+ res.end("Not found");
97
+ }
98
+ });
99
+ server.listen(0, "127.0.0.1", () => {
100
+ const addr = server.address();
101
+ if (!addr || typeof addr === "string") {
102
+ reject(new Error("Failed to start local server"));
103
+ return;
104
+ }
105
+ const port = addr.port;
106
+ const authUrl = `${frontendUrl}/cli-auth?port=${port}`;
107
+ console.log("Opening browser to authenticate...");
108
+ console.log(`If the browser doesn't open, visit: ${authUrl}
109
+ `);
110
+ console.log("Waiting for authentication...");
111
+ open(authUrl).catch(() => {
112
+ });
113
+ });
114
+ const timeout = setTimeout(() => {
115
+ console.error(
116
+ '\nLogin timed out. Use "bx login --paste" to authenticate manually.'
117
+ );
118
+ server.close();
119
+ process.exit(1);
120
+ }, LOGIN_TIMEOUT_MS);
121
+ });
122
+ }
123
+
124
+ // src/commands/logout.ts
125
+ function registerLogoutCommand(program2) {
126
+ program2.command("logout").description("Remove stored credentials").action(() => {
127
+ config.delete("apiKey");
128
+ console.log("Credentials removed.");
129
+ });
130
+ }
131
+
132
+ // src/auth.ts
133
+ function getAuthToken() {
134
+ if (process.env.BOXCREW_API_KEY) return process.env.BOXCREW_API_KEY;
135
+ return config.get("apiKey") ?? null;
136
+ }
137
+ function requireAuth() {
138
+ const token = getAuthToken();
139
+ if (!token) {
140
+ console.error(
141
+ "Not authenticated. Run `bx login` or set the BOXCREW_API_KEY environment variable."
142
+ );
143
+ process.exit(1);
144
+ }
145
+ return token;
146
+ }
147
+
148
+ // src/client.ts
149
+ function getBaseUrl() {
150
+ return process.env.BOXCREW_API_URL || config.get("apiUrl");
151
+ }
152
+ async function apiFetch(path, options = {}) {
153
+ const token = requireAuth();
154
+ const baseUrl = getBaseUrl();
155
+ const url = `${baseUrl}${path}`;
156
+ const headers = {
157
+ Authorization: `Bearer ${token}`,
158
+ ...options.headers ?? {}
159
+ };
160
+ if (options.body && typeof options.body === "string" && !headers["Content-Type"]) {
161
+ headers["Content-Type"] = "application/json";
162
+ }
163
+ const response = await fetch(url, {
164
+ ...options,
165
+ headers
166
+ });
167
+ return response;
168
+ }
169
+ async function apiFetchJson(path, options = {}) {
170
+ const response = await apiFetch(path, options);
171
+ if (!response.ok) {
172
+ const text = await response.text();
173
+ let message;
174
+ try {
175
+ const json = JSON.parse(text);
176
+ message = json.message || json.error || text;
177
+ } catch {
178
+ message = text;
179
+ }
180
+ console.error(`API error (${response.status}): ${message}`);
181
+ process.exit(1);
182
+ }
183
+ return response.json();
184
+ }
185
+
186
+ // src/commands/agents.ts
187
+ var TRANSIENT_CODES = [
188
+ "ETIMEDOUT",
189
+ "ECONNRESET",
190
+ "ECONNREFUSED",
191
+ "UND_ERR_CONNECT_TIMEOUT"
192
+ ];
193
+ function isTransientError(err) {
194
+ if (!(err instanceof Error)) return false;
195
+ const code = err.code;
196
+ if (code && TRANSIENT_CODES.includes(code)) return true;
197
+ if (err instanceof TypeError && /terminated|network/i.test(err.message))
198
+ return true;
199
+ return false;
200
+ }
201
+ function registerAgentsCommands(program2) {
202
+ const agents = program2.command("agents").description("Manage agents");
203
+ agents.command("list").description("List all agents").action(async () => {
204
+ const data = await apiFetchJson("/agents");
205
+ if (data.length === 0) {
206
+ console.log("No agents found.");
207
+ return;
208
+ }
209
+ console.log("");
210
+ for (const agent of data) {
211
+ const model = agent.model ? ` (${agent.model})` : "";
212
+ console.log(` ${agent.name} ${agent.runtime}${model}`);
213
+ }
214
+ console.log("");
215
+ });
216
+ agents.command("create <name>").description("Create a new agent").option("-r, --runtime <runtime>", "Agent runtime", "claude-code").option("-m, --model <model>", "Model override").option("-i, --instructions <text>", "Agent instructions").action(
217
+ async (name, options) => {
218
+ const body = {
219
+ name,
220
+ runtime: options.runtime
221
+ };
222
+ if (options.model) body.model = options.model;
223
+ if (options.instructions) body.instructions = options.instructions;
224
+ const agent = await apiFetchJson("/agents", {
225
+ method: "POST",
226
+ body: JSON.stringify(body)
227
+ });
228
+ console.log(`Agent "${agent.name}" created (${agent.runtime}).`);
229
+ }
230
+ );
231
+ agents.command("delete <name>").description("Delete an agent").action(async (name) => {
232
+ const allAgents = await apiFetchJson("/agents");
233
+ const agent = allAgents.find((a) => a.name === name);
234
+ if (!agent) {
235
+ console.error(`Agent "${name}" not found.`);
236
+ process.exit(1);
237
+ }
238
+ const response = await apiFetch(`/agents/${agent.id}`, {
239
+ method: "DELETE"
240
+ });
241
+ if (!response.ok) {
242
+ console.error(`Failed to delete agent: ${response.statusText}`);
243
+ process.exit(1);
244
+ }
245
+ console.log(`Agent "${name}" deleted.`);
246
+ });
247
+ agents.command("chat <name> <message>").description("Send a message to an agent (streaming)").action(async (name, message) => {
248
+ const MAX_RETRIES = 1;
249
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
250
+ try {
251
+ const response = await apiFetch(`/agents/${name}/chat`, {
252
+ method: "POST",
253
+ body: JSON.stringify({ message })
254
+ });
255
+ if (!response.ok) {
256
+ const text = await response.text();
257
+ console.error(`Chat error (${response.status}): ${text}`);
258
+ process.exit(1);
259
+ }
260
+ if (!response.body) {
261
+ console.error("No response body");
262
+ process.exit(1);
263
+ }
264
+ const reader = response.body.getReader();
265
+ const decoder = new TextDecoder();
266
+ let buffer = "";
267
+ let receivedText = false;
268
+ while (true) {
269
+ const { done, value } = await reader.read();
270
+ if (done) break;
271
+ buffer += decoder.decode(value, { stream: true });
272
+ const lines = buffer.split("\n");
273
+ buffer = lines.pop() ?? "";
274
+ for (const line of lines) {
275
+ if (!line.startsWith("data: ")) continue;
276
+ const jsonStr = line.slice(6);
277
+ if (!jsonStr || jsonStr === "[DONE]") continue;
278
+ try {
279
+ const event = JSON.parse(jsonStr);
280
+ if (event.type === "response.output_text.delta" && event.delta) {
281
+ receivedText = true;
282
+ process.stdout.write(event.delta);
283
+ } else if (event.type === "response.completed") {
284
+ process.stdout.write("\n");
285
+ }
286
+ } catch {
287
+ }
288
+ }
289
+ }
290
+ if (!receivedText) {
291
+ console.error("No response from agent. Try again.");
292
+ process.exit(1);
293
+ }
294
+ break;
295
+ } catch (err) {
296
+ if (attempt < MAX_RETRIES && isTransientError(err)) {
297
+ console.error("\nConnection lost. Retrying...");
298
+ continue;
299
+ }
300
+ const msg = err instanceof Error ? err.message : String(err);
301
+ console.error(`
302
+ Connection error: ${msg}`);
303
+ process.exit(1);
304
+ }
305
+ }
306
+ });
307
+ }
308
+
309
+ // src/commands/keys.ts
310
+ function registerKeysCommands(program2) {
311
+ const keys = program2.command("keys").description("Manage API keys");
312
+ keys.command("list").description("List all API keys").action(async () => {
313
+ const data = await apiFetchJson("/api-keys");
314
+ if (data.length === 0) {
315
+ console.log("No API keys found.");
316
+ return;
317
+ }
318
+ console.log("");
319
+ for (const key of data) {
320
+ const lastUsed = key.lastUsedAt ? `last used ${new Date(key.lastUsedAt).toLocaleDateString()}` : "never used";
321
+ const expires = key.expiresAt ? `expires ${new Date(key.expiresAt).toLocaleDateString()}` : "no expiry";
322
+ console.log(
323
+ ` ${key.keyPrefix}... ${key.name} (${lastUsed}, ${expires})`
324
+ );
325
+ console.log(` ID: ${key.id}`);
326
+ console.log("");
327
+ }
328
+ });
329
+ keys.command("create").description("Create a new API key").requiredOption("-n, --name <name>", "Key name").option("-e, --expires <date>", "Expiration date (ISO 8601)").action(async (options) => {
330
+ const body = { name: options.name };
331
+ if (options.expires) body.expiresAt = options.expires;
332
+ const data = await apiFetchJson("/api-keys", {
333
+ method: "POST",
334
+ body: JSON.stringify(body)
335
+ });
336
+ console.log("");
337
+ console.log("API key created successfully!");
338
+ console.log("");
339
+ console.log(` ${data.key}`);
340
+ console.log("");
341
+ console.log(
342
+ "Store this key securely \u2014 it will not be shown again."
343
+ );
344
+ });
345
+ keys.command("revoke <id>").description("Revoke an API key").action(async (id) => {
346
+ const response = await apiFetch(`/api-keys/${id}`, {
347
+ method: "DELETE"
348
+ });
349
+ if (!response.ok) {
350
+ const text = await response.text();
351
+ console.error(`Failed to revoke key: ${text}`);
352
+ process.exit(1);
353
+ }
354
+ console.log("API key revoked.");
355
+ });
356
+ }
357
+
358
+ // src/commands/api.ts
359
+ function registerApiCommand(program2) {
360
+ program2.command("api <method> <path>").description("Make a raw API request (like gh api)").option("-d, --data <json>", "JSON request body").option("-H, --header <header...>", "Additional headers (key:value)").action(
361
+ async (method, path, options) => {
362
+ const init = {
363
+ method: method.toUpperCase()
364
+ };
365
+ if (options.data) {
366
+ init.body = options.data;
367
+ }
368
+ if (options.header) {
369
+ const extra = {};
370
+ for (const h of options.header) {
371
+ const colonIdx = h.indexOf(":");
372
+ if (colonIdx === -1) {
373
+ console.error(`Invalid header format: "${h}". Use "key:value".`);
374
+ process.exit(1);
375
+ }
376
+ extra[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
377
+ }
378
+ init.headers = extra;
379
+ }
380
+ const apiPath = path.startsWith("/") ? path : `/${path}`;
381
+ const response = await apiFetch(apiPath, init);
382
+ const contentType = response.headers.get("content-type") ?? "";
383
+ const text = await response.text();
384
+ if (contentType.includes("json") && text) {
385
+ try {
386
+ const json = JSON.parse(text);
387
+ console.log(JSON.stringify(json, null, 2));
388
+ } catch {
389
+ process.stdout.write(text);
390
+ }
391
+ } else if (text) {
392
+ process.stdout.write(text);
393
+ }
394
+ if (!response.ok) {
395
+ process.exit(1);
396
+ }
397
+ }
398
+ );
399
+ }
400
+
401
+ // src/commands/connect.ts
402
+ import { spawn } from "child_process";
403
+ import { createInterface as createInterface2 } from "readline";
404
+ import WebSocket from "ws";
405
+ var RECONNECT_BASE_MS = 1e3;
406
+ var RECONNECT_MAX_MS = 3e4;
407
+ function parseStreamJsonLine(line) {
408
+ let obj;
409
+ try {
410
+ obj = JSON.parse(line);
411
+ } catch {
412
+ return null;
413
+ }
414
+ const type = obj?.type;
415
+ if (type === "system" && obj.subtype === "init" && obj.session_id) {
416
+ return { kind: "session_id", sessionId: obj.session_id };
417
+ }
418
+ if (type === "assistant") {
419
+ const content = obj.message?.content;
420
+ if (Array.isArray(content)) {
421
+ for (const block of content) {
422
+ if (block.type === "text" && block.text) {
423
+ return { kind: "text", text: block.text };
424
+ }
425
+ }
426
+ }
427
+ if (obj.session_id) {
428
+ return { kind: "session_id", sessionId: obj.session_id };
429
+ }
430
+ return { kind: "raw", raw: obj, rawType: "assistant" };
431
+ }
432
+ if (type === "content_block_delta") {
433
+ const delta = obj.delta;
434
+ if (delta?.type === "text_delta" && delta.text) {
435
+ return { kind: "text", text: delta.text };
436
+ }
437
+ if (delta?.type === "thinking_delta" && delta.thinking) {
438
+ return { kind: "raw", raw: { type: "content_block_delta", delta }, rawType: "thinking" };
439
+ }
440
+ if (delta?.type === "input_json_delta") {
441
+ return { kind: "raw", raw: obj, rawType: "tool_use" };
442
+ }
443
+ }
444
+ if (type === "content_block_start") {
445
+ const cb = obj.content_block;
446
+ if (cb?.type === "tool_use") return { kind: "raw", raw: obj, rawType: "tool_use" };
447
+ if (cb?.type === "thinking") return { kind: "raw", raw: obj, rawType: "thinking" };
448
+ }
449
+ if (type === "content_block_stop") {
450
+ return { kind: "raw", raw: obj, rawType: "content_block_stop" };
451
+ }
452
+ if (type === "message_start" && obj.message?.id) {
453
+ return { kind: "session_id", sessionId: obj.message.id };
454
+ }
455
+ if (type === "message_stop") {
456
+ return { kind: "done" };
457
+ }
458
+ if (type === "result") {
459
+ if (obj.session_id) {
460
+ return { kind: "session_id", sessionId: obj.session_id };
461
+ }
462
+ return { kind: "done" };
463
+ }
464
+ if (type) {
465
+ return { kind: "raw", raw: obj, rawType: type };
466
+ }
467
+ return null;
468
+ }
469
+ function registerConnectCommand(program2) {
470
+ program2.command("connect <agent-name>").description(
471
+ "Connect a local Claude Code instance to a BoxCrew agent.\nRequires authentication \u2014 run `bx login` first."
472
+ ).option("--claude-path <path>", "Path to claude CLI binary", "claude").action(async (agentName, options) => {
473
+ console.log(`Fetching connection config for agent "${agentName}"...`);
474
+ const config2 = await apiFetchJson(
475
+ `/agents/${encodeURIComponent(agentName)}/connection-config`
476
+ );
477
+ const wsUrl = config2.websocket_url;
478
+ console.log(`Agent: ${config2.agent_name} (${config2.agent_id.slice(0, 8)}...)`);
479
+ console.log(`Proxy: ${config2.proxy_base_url}`);
480
+ const claudePath = options.claudePath;
481
+ let activeProcess = null;
482
+ let sendToServer = null;
483
+ let reconnectAttempt = 0;
484
+ let shouldReconnect = true;
485
+ const handleChat = (msg) => {
486
+ if (activeProcess) {
487
+ activeProcess.kill("SIGTERM");
488
+ activeProcess = null;
489
+ }
490
+ const { messageId, message, sessionId } = msg;
491
+ console.log(`
492
+ Chat ${messageId.slice(0, 8)}: ${message.slice(0, 120)}${message.length > 120 ? "..." : ""}`);
493
+ const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
494
+ if (sessionId) args.push("--resume", sessionId);
495
+ const childEnv = { ...process.env };
496
+ delete childEnv.CLAUDECODE;
497
+ const child = spawn(claudePath, args, {
498
+ stdio: ["ignore", "pipe", "pipe"],
499
+ env: childEnv
500
+ });
501
+ activeProcess = child;
502
+ const rl = createInterface2({ input: child.stdout });
503
+ rl.on("line", (line) => {
504
+ const event = parseStreamJsonLine(line);
505
+ if (event && sendToServer) {
506
+ sendToServer({ type: "event", messageId, event });
507
+ }
508
+ });
509
+ if (child.stderr) {
510
+ const stderrRl = createInterface2({ input: child.stderr });
511
+ stderrRl.on("line", (line) => {
512
+ if (line.trim()) console.error(` [claude] ${line}`);
513
+ });
514
+ }
515
+ child.on("exit", (code) => {
516
+ if (activeProcess === child) activeProcess = null;
517
+ if (sendToServer) {
518
+ sendToServer({ type: "event", messageId, event: { kind: "done" } });
519
+ }
520
+ if (code && code !== 0) {
521
+ console.error(`Claude Code exited with code ${code}`);
522
+ sendToServer?.({ type: "error", messageId, error: `Claude Code exited with code ${code}` });
523
+ }
524
+ });
525
+ child.on("error", (err) => {
526
+ console.error(`Failed to spawn Claude Code: ${err.message}`);
527
+ sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
528
+ });
529
+ };
530
+ const connect = () => {
531
+ console.log("Connecting to BoxCrew...");
532
+ const ws = new WebSocket(wsUrl);
533
+ const send = (msg) => {
534
+ if (ws.readyState === WebSocket.OPEN) {
535
+ ws.send(JSON.stringify(msg));
536
+ }
537
+ };
538
+ ws.on("open", () => {
539
+ reconnectAttempt = 0;
540
+ sendToServer = send;
541
+ console.log("Connected. Waiting for messages...");
542
+ });
543
+ ws.on("message", (data) => {
544
+ let msg;
545
+ try {
546
+ msg = JSON.parse(data.toString());
547
+ } catch {
548
+ return;
549
+ }
550
+ if (msg.type === "ping") {
551
+ send({ type: "pong" });
552
+ return;
553
+ }
554
+ if (msg.type === "stop" && activeProcess) {
555
+ console.log(`Stopping chat ${msg.messageId.slice(0, 8)}...`);
556
+ activeProcess.kill("SIGINT");
557
+ return;
558
+ }
559
+ if (msg.type === "chat") {
560
+ handleChat(msg);
561
+ }
562
+ });
563
+ ws.on("close", () => {
564
+ sendToServer = null;
565
+ if (activeProcess) {
566
+ activeProcess.kill("SIGTERM");
567
+ activeProcess = null;
568
+ }
569
+ if (shouldReconnect) {
570
+ const delay = Math.min(
571
+ RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt),
572
+ RECONNECT_MAX_MS
573
+ );
574
+ reconnectAttempt++;
575
+ console.log(`Disconnected. Reconnecting in ${delay / 1e3}s...`);
576
+ setTimeout(connect, delay);
577
+ }
578
+ });
579
+ ws.on("error", (err) => {
580
+ console.error("WebSocket error:", err.message);
581
+ });
582
+ const shutdown = () => {
583
+ shouldReconnect = false;
584
+ if (activeProcess) activeProcess.kill("SIGTERM");
585
+ ws.close();
586
+ process.exit(0);
587
+ };
588
+ process.removeAllListeners("SIGINT");
589
+ process.removeAllListeners("SIGTERM");
590
+ process.on("SIGINT", shutdown);
591
+ process.on("SIGTERM", shutdown);
592
+ };
593
+ connect();
594
+ });
595
+ }
596
+
597
+ // src/index.ts
598
+ var program = new Command().name("bx").description("BoxCrew CLI \u2014 manage your agents from the terminal").version("0.1.0");
599
+ registerLoginCommand(program);
600
+ registerLogoutCommand(program);
601
+ registerAgentsCommands(program);
602
+ registerKeysCommands(program);
603
+ registerApiCommand(program);
604
+ registerConnectCommand(program);
605
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@boxcrew/cli",
3
+ "version": "0.1.0",
4
+ "description": "BoxCrew CLI — manage your agents from the terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "bx": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup src/index.ts --format esm --dts --clean",
11
+ "dev": "tsx src/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "chalk": "^5.4.0",
15
+ "commander": "^13.0.0",
16
+ "conf": "^13.0.0",
17
+ "eventsource-parser": "^3.0.0",
18
+ "open": "^10.0.0",
19
+ "ora": "^8.0.0",
20
+ "ws": "^8.19.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.0.0",
24
+ "@types/ws": "^8.18.1",
25
+ "tsup": "^8.0.0",
26
+ "tsx": "^4.0.0",
27
+ "typescript": "^5.7.0"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20.0.0"
34
+ }
35
+ }