@linkclaw/clawpool 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.
Files changed (3) hide show
  1. package/README.md +107 -0
  2. package/clawpool.ts +583 -0
  3. package/package.json +28 -0
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # clawpool
2
+
3
+ CLI tool to manage OpenClaw container instances on a local Docker host (Mac Mini + Colima).
4
+
5
+ Each instance runs in its own Docker container with a dedicated Telegram bot token, persistent volume, and auto-assigned port.
6
+
7
+ ## Prerequisites
8
+
9
+ ```bash
10
+ brew install colima docker bun
11
+
12
+ # Start Colima (first time)
13
+ colima start --cpu 8 --memory 12 --disk 100 --vm-type vz
14
+
15
+ # Auto-start on boot
16
+ brew services start colima
17
+ ```
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ # Via npx (no install needed)
23
+ npx @linkclaw/clawpool list
24
+
25
+ # Or install globally
26
+ npm i -g @linkclaw/clawpool
27
+ clawpool list
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```
33
+ clawpool create <name> -t <telegram_bot_token> [-p <port>] [-e KEY=VAL ...]
34
+ clawpool list [--json]
35
+ clawpool start <name>
36
+ clawpool stop <name>
37
+ clawpool restart <name>
38
+ clawpool delete <name> [--purge]
39
+ clawpool rename <old_name> <new_name>
40
+ clawpool config <name> [-t <new_token>] [-e KEY=VAL ...]
41
+ clawpool logs <name> [-f]
42
+ clawpool status <name>
43
+ clawpool shell <name>
44
+ clawpool image [set <image> | show]
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ```bash
50
+ # 1. Build or pull the OpenClaw image
51
+ docker build -t openclaw:latest /path/to/apps/agent
52
+
53
+ # 2. Create an instance with a Telegram bot token
54
+ clawpool create alpha -t "123456:AAHxxx..."
55
+
56
+ # 3. Add another instance with extra env vars
57
+ clawpool create beta -t "789012:BBXyyy..." -e "OPENAI_API_KEY=sk-..."
58
+
59
+ # 4. Check running instances
60
+ clawpool list
61
+
62
+ # 5. View logs
63
+ clawpool logs alpha -f
64
+
65
+ # 6. Reconfigure
66
+ clawpool config alpha -t "new_token_here"
67
+
68
+ # 7. Clean up
69
+ clawpool delete beta --purge
70
+ ```
71
+
72
+ ## How It Works
73
+
74
+ - **Config**: `~/.clawpool/config.json` stores instance metadata (name, port, token, env vars).
75
+ - **Containers**: Each instance runs as `claw-<name>` with `--restart unless-stopped`.
76
+ - **Volumes**: Data persisted in Docker named volumes (`claw-<name>-data` mounted at `/home/claw`).
77
+ - **Ports**: Auto-assigned starting from 8081, or manually specified with `-p`.
78
+ - **Telegram**: Each instance connects via long-polling (no public IP needed).
79
+
80
+ ## Config File
81
+
82
+ Stored at `~/.clawpool/config.json`:
83
+
84
+ ```jsonc
85
+ {
86
+ "image": "openclaw:latest", // default image for new instances
87
+ "next_port": 8083, // next auto-assigned port
88
+ "instances": {
89
+ "alpha": {
90
+ "name": "alpha",
91
+ "port": 8081,
92
+ "telegram_bot_token": "123456:AAH...",
93
+ "status": "running",
94
+ "container_id": "a1b2c3...",
95
+ "volume": "claw-alpha-data",
96
+ "env": { "OPENAI_API_KEY": "sk-..." },
97
+ "created_at": "2026-03-19T10:00:00Z"
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Environment
104
+
105
+ | Variable | Purpose |
106
+ |---|---|
107
+ | `CLAWPOOL_DIR` | Override config directory (default: `~/.clawpool`) |
package/clawpool.ts ADDED
@@ -0,0 +1,583 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { existsSync, mkdirSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+
7
+ // ── Config ──────────────────────────────────────────────────────────────────
8
+
9
+ const CLAWPOOL_DIR = process.env.CLAWPOOL_DIR ?? join(homedir(), ".clawpool");
10
+ const CONFIG_FILE = join(CLAWPOOL_DIR, "config.json");
11
+ const CONTAINER_PREFIX = "claw";
12
+
13
+ interface Instance {
14
+ name: string;
15
+ port: number;
16
+ telegram_bot_token: string;
17
+ status: "running" | "stopped" | "removed";
18
+ container_id: string;
19
+ volume: string;
20
+ env: Record<string, string>;
21
+ created_at: string;
22
+ }
23
+
24
+ interface Config {
25
+ image: string;
26
+ next_port: number;
27
+ instances: Record<string, Instance>;
28
+ }
29
+
30
+ // ── Helpers ─────────────────────────────────────────────────────────────────
31
+
32
+ const c = {
33
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
34
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
35
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
36
+ cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
37
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
38
+ };
39
+
40
+ const die = (msg: string): never => {
41
+ console.error(`${c.red("Error:")} ${msg}`);
42
+ process.exit(1);
43
+ };
44
+ const info = (msg: string) => console.log(`${c.cyan("==>")} ${msg}`);
45
+ const ok = (msg: string) => console.log(`${c.green("✓")} ${msg}`);
46
+
47
+ const containerName = (name: string) => `${CONTAINER_PREFIX}-${name}`;
48
+ const volumeName = (name: string) => `${CONTAINER_PREFIX}-${name}-data`;
49
+
50
+ function humanTime(iso: string): string {
51
+ const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
52
+ if (diff < 60) return `${diff}s ago`;
53
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
54
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
55
+ return `${Math.floor(diff / 86400)}d ago`;
56
+ }
57
+
58
+ // ── Config I/O ──────────────────────────────────────────────────────────────
59
+
60
+ async function loadConfig(): Promise<Config> {
61
+ mkdirSync(CLAWPOOL_DIR, { recursive: true });
62
+ if (!existsSync(CONFIG_FILE)) {
63
+ const initial: Config = {
64
+ image: "openclaw:latest",
65
+ next_port: 8081,
66
+ instances: {},
67
+ };
68
+ await Bun.write(CONFIG_FILE, JSON.stringify(initial, null, 2));
69
+ return initial;
70
+ }
71
+ return Bun.file(CONFIG_FILE).json();
72
+ }
73
+
74
+ async function saveConfig(config: Config) {
75
+ await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
76
+ }
77
+
78
+ function requireInstance(config: Config, name: string): Instance {
79
+ const inst = config.instances[name];
80
+ if (!inst) die(`instance '${name}' does not exist`);
81
+ return inst;
82
+ }
83
+
84
+ // ── Docker helpers ──────────────────────────────────────────────────────────
85
+
86
+ async function docker(...args: string[]): Promise<string> {
87
+ const proc = Bun.spawn(["docker", ...args], {
88
+ stdout: "pipe",
89
+ stderr: "pipe",
90
+ });
91
+ const stdout = await new Response(proc.stdout).text();
92
+ const stderr = await new Response(proc.stderr).text();
93
+ const code = await proc.exited;
94
+ if (code !== 0) throw new Error(stderr.trim() || `docker ${args[0]} failed`);
95
+ return stdout.trim();
96
+ }
97
+
98
+ async function dockerQuiet(...args: string[]): Promise<boolean> {
99
+ const proc = Bun.spawn(["docker", ...args], {
100
+ stdout: "ignore",
101
+ stderr: "ignore",
102
+ });
103
+ return (await proc.exited) === 0;
104
+ }
105
+
106
+ async function dockerExec(...args: string[]): Promise<void> {
107
+ const proc = Bun.spawn(["docker", ...args], {
108
+ stdout: "inherit",
109
+ stderr: "inherit",
110
+ stdin: "inherit",
111
+ });
112
+ await proc.exited;
113
+ }
114
+
115
+ async function syncStatus(
116
+ config: Config,
117
+ name: string
118
+ ): Promise<"running" | "stopped" | "removed"> {
119
+ try {
120
+ const state = await docker(
121
+ "inspect",
122
+ "-f",
123
+ "{{.State.Status}}",
124
+ containerName(name)
125
+ );
126
+ const status =
127
+ state === "running" ? "running" : (("stopped" as const) satisfies string);
128
+ config.instances[name].status = status as "running" | "stopped";
129
+ return status as "running" | "stopped";
130
+ } catch {
131
+ config.instances[name].status = "removed";
132
+ return "removed";
133
+ }
134
+ }
135
+
136
+ function buildEnvArgs(inst: Instance): string[] {
137
+ const args = [
138
+ "-e",
139
+ `TELEGRAM_BOT_TOKEN=${inst.telegram_bot_token}`,
140
+ "-e",
141
+ `INSTANCE_NAME=${inst.name}`,
142
+ ];
143
+ for (const [k, v] of Object.entries(inst.env)) {
144
+ args.push("-e", `${k}=${v}`);
145
+ }
146
+ return args;
147
+ }
148
+
149
+ async function rebuildContainer(
150
+ config: Config,
151
+ name: string
152
+ ): Promise<string> {
153
+ const inst = config.instances[name];
154
+ await dockerQuiet("rm", "-f", containerName(name));
155
+
156
+ const cid = await docker(
157
+ "run",
158
+ "-d",
159
+ "--name",
160
+ containerName(name),
161
+ "--restart",
162
+ "unless-stopped",
163
+ "-p",
164
+ `${inst.port}:8080`,
165
+ "-v",
166
+ `${inst.volume}:/home/claw`,
167
+ ...buildEnvArgs(inst),
168
+ config.image,
169
+ "node",
170
+ "dist/index.js",
171
+ "gateway",
172
+ "--allow-unconfigured"
173
+ );
174
+
175
+ inst.container_id = cid;
176
+ inst.status = "running";
177
+ return cid;
178
+ }
179
+
180
+ // ── Commands ────────────────────────────────────────────────────────────────
181
+
182
+ async function cmdCreate(config: Config, args: string[]) {
183
+ let name = "";
184
+ let token = "";
185
+ let port = 0;
186
+ const env: Record<string, string> = {};
187
+
188
+ for (let i = 0; i < args.length; i++) {
189
+ switch (args[i]) {
190
+ case "-t":
191
+ case "--token":
192
+ token = args[++i];
193
+ break;
194
+ case "-p":
195
+ case "--port":
196
+ port = parseInt(args[++i]);
197
+ break;
198
+ case "-e":
199
+ case "--env": {
200
+ const kv = args[++i];
201
+ const eq = kv.indexOf("=");
202
+ if (eq === -1) die(`invalid env format: ${kv} (expected KEY=VAL)`);
203
+ env[kv.slice(0, eq)] = kv.slice(eq + 1);
204
+ break;
205
+ }
206
+ default:
207
+ if (args[i].startsWith("-")) die(`unknown option: ${args[i]}`);
208
+ if (!name) name = args[i];
209
+ else die(`unexpected argument: ${args[i]}`);
210
+ }
211
+ }
212
+
213
+ if (!name || !token) die("usage: clawpool create <name> -t <token>");
214
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name))
215
+ die("invalid name: use alphanumerics, dots, hyphens, underscores");
216
+ if (config.instances[name]) die(`instance '${name}' already exists`);
217
+
218
+ if (!port) port = config.next_port;
219
+
220
+ info(`Creating instance '${name}' on port ${port}...`);
221
+
222
+ await docker("volume", "create", volumeName(name));
223
+
224
+ const inst: Instance = {
225
+ name,
226
+ port,
227
+ telegram_bot_token: token,
228
+ status: "running",
229
+ container_id: "",
230
+ volume: volumeName(name),
231
+ env,
232
+ created_at: new Date().toISOString(),
233
+ };
234
+
235
+ config.instances[name] = inst;
236
+ if (config.next_port <= port) config.next_port = port + 1;
237
+
238
+ const cid = await rebuildContainer(config, name);
239
+ await saveConfig(config);
240
+
241
+ ok(`Instance '${name}' created (port: ${port}, container: ${cid.slice(0, 12)})`);
242
+ }
243
+
244
+ async function cmdList(config: Config, args: string[]) {
245
+ const jsonMode = args.includes("--json");
246
+ const names = Object.keys(config.instances);
247
+
248
+ if (names.length === 0) {
249
+ console.log(
250
+ "No instances. Create one with: clawpool create <name> -t <token>"
251
+ );
252
+ return;
253
+ }
254
+
255
+ for (const name of names) await syncStatus(config, name);
256
+ await saveConfig(config);
257
+
258
+ if (jsonMode) {
259
+ console.log(JSON.stringify(config.instances, null, 2));
260
+ return;
261
+ }
262
+
263
+ const rows = names.map((name) => {
264
+ const inst = config.instances[name];
265
+ const statusCol =
266
+ inst.status === "running"
267
+ ? c.green(inst.status)
268
+ : inst.status === "stopped"
269
+ ? c.red(inst.status)
270
+ : c.yellow(inst.status);
271
+ return { name, status: statusCol, port: inst.port, created: humanTime(inst.created_at) };
272
+ });
273
+
274
+ console.log(
275
+ `${"NAME".padEnd(16)} ${"STATUS".padEnd(10)} ${"PORT".padEnd(6)} CREATED`
276
+ );
277
+ console.log(
278
+ `${"----".padEnd(16)} ${"------".padEnd(10)} ${"----".padEnd(6)} -------`
279
+ );
280
+ for (const r of rows) {
281
+ // status has ANSI codes so pad the raw length
282
+ console.log(
283
+ `${r.name.padEnd(16)} ${r.status.padEnd(21)} ${String(r.port).padEnd(6)} ${r.created}`
284
+ );
285
+ }
286
+ }
287
+
288
+ async function cmdStart(config: Config, args: string[]) {
289
+ const name = args[0] ?? die("usage: clawpool start <name>");
290
+ requireInstance(config, name);
291
+ info(`Starting '${name}'...`);
292
+ await docker("start", containerName(name));
293
+ config.instances[name].status = "running";
294
+ await saveConfig(config);
295
+ ok(`Instance '${name}' started`);
296
+ }
297
+
298
+ async function cmdStop(config: Config, args: string[]) {
299
+ const name = args[0] ?? die("usage: clawpool stop <name>");
300
+ requireInstance(config, name);
301
+ info(`Stopping '${name}'...`);
302
+ await docker("stop", containerName(name));
303
+ config.instances[name].status = "stopped";
304
+ await saveConfig(config);
305
+ ok(`Instance '${name}' stopped`);
306
+ }
307
+
308
+ async function cmdRestart(config: Config, args: string[]) {
309
+ const name = args[0] ?? die("usage: clawpool restart <name>");
310
+ requireInstance(config, name);
311
+ info(`Restarting '${name}'...`);
312
+ await docker("restart", containerName(name));
313
+ config.instances[name].status = "running";
314
+ await saveConfig(config);
315
+ ok(`Instance '${name}' restarted`);
316
+ }
317
+
318
+ async function cmdDelete(config: Config, args: string[]) {
319
+ let name = "";
320
+ let purge = false;
321
+ for (const a of args) {
322
+ if (a === "--purge") purge = true;
323
+ else if (!a.startsWith("-")) name = a;
324
+ else die(`unknown option: ${a}`);
325
+ }
326
+ if (!name) die("usage: clawpool delete <name> [--purge]");
327
+ requireInstance(config, name);
328
+
329
+ info(`Deleting '${name}'...`);
330
+ await dockerQuiet("rm", "-f", containerName(name));
331
+
332
+ if (purge) {
333
+ await dockerQuiet("volume", "rm", volumeName(name));
334
+ info("Volume purged");
335
+ }
336
+
337
+ delete config.instances[name];
338
+ await saveConfig(config);
339
+ ok(`Instance '${name}' deleted`);
340
+ }
341
+
342
+ async function cmdRename(config: Config, args: string[]) {
343
+ const oldName = args[0] ?? die("usage: clawpool rename <old> <new>");
344
+ const newName = args[1] ?? die("usage: clawpool rename <old> <new>");
345
+
346
+ requireInstance(config, oldName);
347
+ if (config.instances[newName]) die(`instance '${newName}' already exists`);
348
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(newName))
349
+ die("invalid name: use alphanumerics, dots, hyphens, underscores");
350
+
351
+ info(`Renaming '${oldName}' → '${newName}'...`);
352
+
353
+ // Copy instance config
354
+ const inst = { ...config.instances[oldName] };
355
+ inst.name = newName;
356
+ inst.volume = volumeName(newName);
357
+
358
+ // Stop old container
359
+ await dockerQuiet("rm", "-f", containerName(oldName));
360
+
361
+ // Migrate volume
362
+ const oldVol = volumeName(oldName);
363
+ const newVol = volumeName(newName);
364
+ await docker("volume", "create", newVol);
365
+ await dockerQuiet(
366
+ "run",
367
+ "--rm",
368
+ "-v",
369
+ `${oldVol}:/from`,
370
+ "-v",
371
+ `${newVol}:/to`,
372
+ "alpine",
373
+ "sh",
374
+ "-c",
375
+ "cp -a /from/. /to/"
376
+ );
377
+ await dockerQuiet("volume", "rm", oldVol);
378
+
379
+ // Update config and rebuild
380
+ delete config.instances[oldName];
381
+ config.instances[newName] = inst;
382
+ await rebuildContainer(config, newName);
383
+ await saveConfig(config);
384
+
385
+ ok(`Renamed '${oldName}' → '${newName}'`);
386
+ }
387
+
388
+ async function cmdConfig(config: Config, args: string[]) {
389
+ const name = args[0];
390
+ if (!name || name.startsWith("-"))
391
+ die("usage: clawpool config <name> [-t <token>] [-e KEY=VAL ...]");
392
+
393
+ requireInstance(config, name);
394
+ const inst = config.instances[name];
395
+ let changed = false;
396
+
397
+ for (let i = 1; i < args.length; i++) {
398
+ switch (args[i]) {
399
+ case "-t":
400
+ case "--token":
401
+ inst.telegram_bot_token = args[++i];
402
+ info("Token updated");
403
+ changed = true;
404
+ break;
405
+ case "-e":
406
+ case "--env": {
407
+ const kv = args[++i];
408
+ const eq = kv.indexOf("=");
409
+ if (eq === -1) die(`invalid env format: ${kv}`);
410
+ inst.env[kv.slice(0, eq)] = kv.slice(eq + 1);
411
+ info(`Env ${kv.slice(0, eq)} set`);
412
+ changed = true;
413
+ break;
414
+ }
415
+ default:
416
+ die(`unknown option: ${args[i]}`);
417
+ }
418
+ }
419
+
420
+ if (!changed) die("nothing to change — specify --token or --env");
421
+
422
+ info("Rebuilding container...");
423
+ await rebuildContainer(config, name);
424
+ await saveConfig(config);
425
+ ok(`Instance '${name}' reconfigured and restarted`);
426
+ }
427
+
428
+ async function cmdLogs(config: Config, args: string[]) {
429
+ const name = args.find((a): a is string => !a.startsWith("-"));
430
+ if (!name) return die("usage: clawpool logs <name> [-f]");
431
+ requireInstance(config, name);
432
+ const flags = args.filter((a) => a.startsWith("-"));
433
+ await dockerExec("logs", ...flags, containerName(name));
434
+ }
435
+
436
+ async function cmdStatus(config: Config, args: string[]) {
437
+ const name = args[0] ?? die("usage: clawpool status <name>");
438
+ requireInstance(config, name);
439
+ await syncStatus(config, name);
440
+ await saveConfig(config);
441
+
442
+ const inst = config.instances[name];
443
+ const statusCol =
444
+ inst.status === "running"
445
+ ? c.green(inst.status)
446
+ : inst.status === "stopped"
447
+ ? c.red(inst.status)
448
+ : c.yellow(inst.status);
449
+
450
+ console.log(`${c.bold("Instance:")} ${inst.name}`);
451
+ console.log(`${c.bold("Status:")} ${statusCol}`);
452
+ console.log(`${c.bold("Port:")} ${inst.port}`);
453
+ console.log(`${c.bold("Volume:")} ${inst.volume}`);
454
+ console.log(`${c.bold("Created:")} ${inst.created_at}`);
455
+ console.log(
456
+ `${c.bold("Token:")} ${inst.telegram_bot_token.slice(0, 20)}...`
457
+ );
458
+
459
+ const envKeys = Object.keys(inst.env);
460
+ if (envKeys.length > 0) {
461
+ console.log(`${c.bold("Env vars:")} ${envKeys.length}`);
462
+ for (const [k, v] of Object.entries(inst.env)) {
463
+ console.log(` ${k}=${v.slice(0, 20)}...`);
464
+ }
465
+ }
466
+
467
+ if (inst.status === "running") {
468
+ console.log("");
469
+ await dockerExec(
470
+ "stats",
471
+ "--no-stream",
472
+ "--format",
473
+ `${c.bold("CPU:")} {{.CPUPerc}} ${c.bold("Mem:")} {{.MemUsage}}`,
474
+ containerName(name)
475
+ );
476
+ }
477
+ }
478
+
479
+ async function cmdShell(config: Config, args: string[]) {
480
+ const name = args[0] ?? die("usage: clawpool shell <name>");
481
+ requireInstance(config, name);
482
+ await dockerExec("exec", "-it", containerName(name), "/bin/sh");
483
+ }
484
+
485
+ async function cmdImage(config: Config, args: string[]) {
486
+ const sub = args[0] ?? "show";
487
+ if (sub === "set") {
488
+ const img = args[1] ?? die("usage: clawpool image set <image>");
489
+ config.image = img;
490
+ await saveConfig(config);
491
+ ok(`Default image set to '${img}'`);
492
+ } else if (sub === "show") {
493
+ console.log(config.image);
494
+ } else {
495
+ die("usage: clawpool image [set <image> | show]");
496
+ }
497
+ }
498
+
499
+ function cmdHelp() {
500
+ console.log(`clawpool — Manage OpenClaw container instances
501
+
502
+ Usage:
503
+ clawpool create <name> -t <telegram_bot_token> [-p <port>] [-e KEY=VAL ...]
504
+ clawpool list [--json]
505
+ clawpool start <name>
506
+ clawpool stop <name>
507
+ clawpool restart <name>
508
+ clawpool delete <name> [--purge]
509
+ clawpool rename <old_name> <new_name>
510
+ clawpool config <name> [-t <new_token>] [-e KEY=VAL ...]
511
+ clawpool logs <name> [-f]
512
+ clawpool status <name>
513
+ clawpool shell <name>
514
+ clawpool image [set <image> | show]
515
+
516
+ Options:
517
+ -t, --token Telegram bot token
518
+ -p, --port Host port (auto-assigned if omitted)
519
+ -e, --env Extra environment variable (repeatable)
520
+ --purge Also remove data volume on delete
521
+ --json Output in JSON format
522
+
523
+ Examples:
524
+ clawpool create alpha -t "123456:AAH..." -e "OPENAI_API_KEY=sk-..."
525
+ clawpool list
526
+ clawpool logs alpha -f
527
+ clawpool config alpha -t "new_token"
528
+ clawpool delete alpha --purge`);
529
+ }
530
+
531
+ // ── Main ────────────────────────────────────────────────────────────────────
532
+
533
+ const config = await loadConfig();
534
+ const [cmd, ...args] = process.argv.slice(2);
535
+
536
+ switch (cmd ?? "help") {
537
+ case "create":
538
+ await cmdCreate(config, args);
539
+ break;
540
+ case "list":
541
+ case "ls":
542
+ await cmdList(config, args);
543
+ break;
544
+ case "start":
545
+ await cmdStart(config, args);
546
+ break;
547
+ case "stop":
548
+ await cmdStop(config, args);
549
+ break;
550
+ case "restart":
551
+ await cmdRestart(config, args);
552
+ break;
553
+ case "delete":
554
+ case "rm":
555
+ await cmdDelete(config, args);
556
+ break;
557
+ case "rename":
558
+ await cmdRename(config, args);
559
+ break;
560
+ case "config":
561
+ await cmdConfig(config, args);
562
+ break;
563
+ case "logs":
564
+ await cmdLogs(config, args);
565
+ break;
566
+ case "status":
567
+ await cmdStatus(config, args);
568
+ break;
569
+ case "shell":
570
+ case "sh":
571
+ await cmdShell(config, args);
572
+ break;
573
+ case "image":
574
+ await cmdImage(config, args);
575
+ break;
576
+ case "help":
577
+ case "-h":
578
+ case "--help":
579
+ cmdHelp();
580
+ break;
581
+ default:
582
+ die(`unknown command: ${cmd} (run 'clawpool help' for usage)`);
583
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@linkclaw/clawpool",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "clawpool": "./clawpool.ts"
7
+ },
8
+ "files": [
9
+ "clawpool.ts"
10
+ ],
11
+ "scripts": {
12
+ "typecheck": "tsc --noEmit",
13
+ "start": "bun run clawpool.ts"
14
+ },
15
+ "devDependencies": {
16
+ "bun-types": "latest",
17
+ "typescript": "^5"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/linkclaw-lab/clawpool.git"
25
+ },
26
+ "license": "MIT",
27
+ "description": "CLI tool to manage OpenClaw container instances with Telegram integration"
28
+ }