@linkclaw/clawpool 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -23
  2. package/clawpool.ts +174 -77
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  CLI tool to manage OpenClaw container instances on a local Docker host (Mac Mini + Colima).
4
4
 
5
- Each instance runs in its own Docker container with a dedicated Telegram bot token, persistent volume, and auto-assigned port.
5
+ Each instance gets its own Web Dashboard with an auto-generated auth token no Telegram or external accounts required.
6
6
 
7
7
  ## Prerequisites
8
8
 
@@ -19,8 +19,8 @@ brew services start colima
19
19
  ## Install
20
20
 
21
21
  ```bash
22
- # Via npx (no install needed)
23
- npx @linkclaw/clawpool list
22
+ # Via bunx (no install needed)
23
+ bunx @linkclaw/clawpool list
24
24
 
25
25
  # Or install globally
26
26
  npm i -g @linkclaw/clawpool
@@ -30,14 +30,14 @@ clawpool list
30
30
  ## Usage
31
31
 
32
32
  ```
33
- clawpool create <name> -t <telegram_bot_token> [-p <port>] [-e KEY=VAL ...]
33
+ clawpool create <name> [--telegram-token <token>] [-p <port>] [-e KEY=VAL ...]
34
34
  clawpool list [--json]
35
35
  clawpool start <name>
36
36
  clawpool stop <name>
37
37
  clawpool restart <name>
38
38
  clawpool delete <name> [--purge]
39
39
  clawpool rename <old_name> <new_name>
40
- clawpool config <name> [-t <new_token>] [-e KEY=VAL ...]
40
+ clawpool config <name> [--telegram-token <token>] [-e KEY=VAL ...]
41
41
  clawpool logs <name> [-f]
42
42
  clawpool status <name>
43
43
  clawpool shell <name>
@@ -47,35 +47,33 @@ clawpool image [set <image> | show]
47
47
  ## Quick Start
48
48
 
49
49
  ```bash
50
- # 1. Build or pull the OpenClaw image
51
- docker build -t openclaw:latest /path/to/apps/agent
50
+ # 1. Create an instance dashboard URL is printed automatically
51
+ clawpool create alpha
52
52
 
53
- # 2. Create an instance with a Telegram bot token
54
- clawpool create alpha -t "123456:AAHxxx..."
53
+ # 2. Add another with extra env vars
54
+ clawpool create beta -e "OPENAI_API_KEY=sk-..."
55
55
 
56
- # 3. Add another instance with extra env vars
57
- clawpool create beta -t "789012:BBXyyy..." -e "OPENAI_API_KEY=sk-..."
56
+ # 3. Optionally attach Telegram
57
+ clawpool create gamma --telegram-token "123456:AAH..."
58
58
 
59
- # 4. Check running instances
59
+ # 4. List all instances with dashboard URLs
60
60
  clawpool list
61
61
 
62
62
  # 5. View logs
63
63
  clawpool logs alpha -f
64
64
 
65
- # 6. Reconfigure
66
- clawpool config alpha -t "new_token_here"
67
-
68
- # 7. Clean up
65
+ # 6. Clean up
69
66
  clawpool delete beta --purge
70
67
  ```
71
68
 
72
69
  ## How It Works
73
70
 
74
- - **Config**: `~/.clawpool/config.json` stores instance metadata (name, port, token, env vars).
71
+ - **Dashboard**: Each instance exposes OpenClaw's built-in Web UI. Auth token is auto-generated and shown in `clawpool list`.
72
+ - **Config**: `~/.clawpool/config.json` stores instance metadata.
75
73
  - **Containers**: Each instance runs as `claw-<name>` with `--restart unless-stopped`.
76
74
  - **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).
75
+ - **Ports**: Auto-assigned starting from 18789 with 20-port spacing (OpenClaw requirement), or manually specified with `-p`.
76
+ - **Telegram**: Optional pass `--telegram-token` to enable Telegram bot integration via long-polling.
79
77
 
80
78
  ## Config File
81
79
 
@@ -83,13 +81,13 @@ Stored at `~/.clawpool/config.json`:
83
81
 
84
82
  ```jsonc
85
83
  {
86
- "image": "openclaw:latest", // default image for new instances
87
- "next_port": 8083, // next auto-assigned port
84
+ "image": "openclaw:latest",
85
+ "next_port": 18829,
88
86
  "instances": {
89
87
  "alpha": {
90
88
  "name": "alpha",
91
- "port": 8081,
92
- "telegram_bot_token": "123456:AAH...",
89
+ "port": 18789,
90
+ "gateway_token": "abc123...",
93
91
  "status": "running",
94
92
  "container_id": "a1b2c3...",
95
93
  "volume": "claw-alpha-data",
package/clawpool.ts CHANGED
@@ -1,19 +1,23 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { existsSync, mkdirSync } from "fs";
4
- import { homedir } from "os";
4
+ import { homedir, networkInterfaces } from "os";
5
5
  import { join } from "path";
6
+ import { randomBytes } from "crypto";
6
7
 
7
8
  // ── Config ──────────────────────────────────────────────────────────────────
8
9
 
9
10
  const CLAWPOOL_DIR = process.env.CLAWPOOL_DIR ?? join(homedir(), ".clawpool");
10
11
  const CONFIG_FILE = join(CLAWPOOL_DIR, "config.json");
11
12
  const CONTAINER_PREFIX = "claw";
13
+ const BASE_PORT = 18789;
14
+ const PORT_STEP = 20; // OpenClaw requires at least 20-port spacing
12
15
 
13
16
  interface Instance {
14
17
  name: string;
15
18
  port: number;
16
- telegram_bot_token: string;
19
+ gateway_token: string;
20
+ telegram_bot_token?: string;
17
21
  status: "running" | "stopped" | "removed";
18
22
  container_id: string;
19
23
  volume: string;
@@ -35,6 +39,7 @@ const c = {
35
39
  yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
36
40
  cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
37
41
  bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
42
+ dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
38
43
  };
39
44
 
40
45
  const die = (msg: string): never => {
@@ -46,6 +51,21 @@ const ok = (msg: string) => console.log(`${c.green("✓")} ${msg}`);
46
51
 
47
52
  const containerName = (name: string) => `${CONTAINER_PREFIX}-${name}`;
48
53
  const volumeName = (name: string) => `${CONTAINER_PREFIX}-${name}-data`;
54
+ const generateToken = () => randomBytes(24).toString("base64url");
55
+
56
+ function getLanIP(): string {
57
+ const nets = networkInterfaces();
58
+ for (const addrs of Object.values(nets)) {
59
+ for (const addr of addrs ?? []) {
60
+ if (addr.family === "IPv4" && !addr.internal) return addr.address;
61
+ }
62
+ }
63
+ return "localhost";
64
+ }
65
+
66
+ function dashboardURL(host: string, port: number, token: string): string {
67
+ return `http://${host}:${port}?token=${token}`;
68
+ }
49
69
 
50
70
  function humanTime(iso: string): string {
51
71
  const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
@@ -62,7 +82,7 @@ async function loadConfig(): Promise<Config> {
62
82
  if (!existsSync(CONFIG_FILE)) {
63
83
  const initial: Config = {
64
84
  image: "openclaw:latest",
65
- next_port: 8081,
85
+ next_port: BASE_PORT,
66
86
  instances: {},
67
87
  };
68
88
  await Bun.write(CONFIG_FILE, JSON.stringify(initial, null, 2));
@@ -91,7 +111,8 @@ async function docker(...args: string[]): Promise<string> {
91
111
  const stdout = await new Response(proc.stdout).text();
92
112
  const stderr = await new Response(proc.stderr).text();
93
113
  const code = await proc.exited;
94
- if (code !== 0) throw new Error(stderr.trim() || `docker ${args[0]} failed`);
114
+ if (code !== 0)
115
+ throw new Error(stderr.trim() || `docker ${args[0]} failed`);
95
116
  return stdout.trim();
96
117
  }
97
118
 
@@ -123,26 +144,107 @@ async function syncStatus(
123
144
  "{{.State.Status}}",
124
145
  containerName(name)
125
146
  );
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";
147
+ const status: "running" | "stopped" =
148
+ state === "running" ? "running" : "stopped";
149
+ config.instances[name].status = status;
150
+ return status;
130
151
  } catch {
131
152
  config.instances[name].status = "removed";
132
153
  return "removed";
133
154
  }
134
155
  }
135
156
 
136
- function buildEnvArgs(inst: Instance): string[] {
157
+ const HOST_OC_CONFIG = join(homedir(), ".openclaw", "openclaw.json");
158
+
159
+ function loadHostOpenClawConfig(): Record<string, unknown> {
160
+ if (!existsSync(HOST_OC_CONFIG)) return {};
161
+ try {
162
+ const raw = require("fs").readFileSync(HOST_OC_CONFIG, "utf-8");
163
+ return JSON.parse(raw);
164
+ } catch {
165
+ return {};
166
+ }
167
+ }
168
+
169
+ function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
170
+ const result = { ...target };
171
+ for (const key of Object.keys(source)) {
172
+ if (
173
+ source[key] &&
174
+ typeof source[key] === "object" &&
175
+ !Array.isArray(source[key]) &&
176
+ target[key] &&
177
+ typeof target[key] === "object" &&
178
+ !Array.isArray(target[key])
179
+ ) {
180
+ result[key] = deepMerge(
181
+ target[key] as Record<string, unknown>,
182
+ source[key] as Record<string, unknown>
183
+ );
184
+ } else {
185
+ result[key] = source[key];
186
+ }
187
+ }
188
+ return result;
189
+ }
190
+
191
+ function buildOpenClawConfig(token: string): string {
192
+ const hostConfig = loadHostOpenClawConfig();
193
+ // Gateway config is always overridden per-instance
194
+ const instanceOverrides: Record<string, unknown> = {
195
+ gateway: {
196
+ auth: { mode: "token", token },
197
+ controlUi: { dangerouslyAllowHostHeaderOriginFallback: true },
198
+ },
199
+ };
200
+ // Strip host gateway settings (we manage those), keep everything else
201
+ const { gateway: _gw, meta: _meta, ...hostSettings } = hostConfig;
202
+ const merged = deepMerge(hostSettings, instanceOverrides);
203
+ return JSON.stringify(merged);
204
+ }
205
+
206
+ function buildRunArgs(config: Config, inst: Instance): string[] {
207
+ const ocConfig = buildOpenClawConfig(inst.gateway_token);
208
+ // Always write config on start — merges host ~/.openclaw/openclaw.json with per-instance gateway auth
209
+ const entrypoint = [
210
+ "sh",
211
+ "-c",
212
+ [
213
+ "mkdir -p /home/node/data/.openclaw",
214
+ "chown -R node:node /home/node/data",
215
+ `echo '${ocConfig}' > /home/node/data/.openclaw/openclaw.json`,
216
+ `exec su -s /bin/sh node -c 'exec node dist/index.js gateway --allow-unconfigured --port ${inst.port} --bind lan'`,
217
+ ].join(" && "),
218
+ ];
219
+
137
220
  const args = [
221
+ "run",
222
+ "-d",
223
+ "--name",
224
+ containerName(inst.name),
225
+ "--restart",
226
+ "unless-stopped",
227
+ "--user",
228
+ "root",
229
+ "-p",
230
+ `${inst.port}:${inst.port}`,
231
+ "-v",
232
+ `${inst.volume}:/home/node/data`,
138
233
  "-e",
139
- `TELEGRAM_BOT_TOKEN=${inst.telegram_bot_token}`,
234
+ `OPENCLAW_HOME=/home/node/data`,
140
235
  "-e",
141
236
  `INSTANCE_NAME=${inst.name}`,
142
237
  ];
238
+
239
+ if (inst.telegram_bot_token) {
240
+ args.push("-e", `TELEGRAM_BOT_TOKEN=${inst.telegram_bot_token}`);
241
+ }
242
+
143
243
  for (const [k, v] of Object.entries(inst.env)) {
144
244
  args.push("-e", `${k}=${v}`);
145
245
  }
246
+
247
+ args.push(config.image, ...entrypoint);
146
248
  return args;
147
249
  }
148
250
 
@@ -150,28 +252,9 @@ async function rebuildContainer(
150
252
  config: Config,
151
253
  name: string
152
254
  ): Promise<string> {
153
- const inst = config.instances[name];
154
255
  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
-
256
+ const inst = config.instances[name];
257
+ const cid = await docker(...buildRunArgs(config, inst));
175
258
  inst.container_id = cid;
176
259
  inst.status = "running";
177
260
  return cid;
@@ -181,20 +264,19 @@ async function rebuildContainer(
181
264
 
182
265
  async function cmdCreate(config: Config, args: string[]) {
183
266
  let name = "";
184
- let token = "";
185
267
  let port = 0;
268
+ let telegramToken = "";
186
269
  const env: Record<string, string> = {};
187
270
 
188
271
  for (let i = 0; i < args.length; i++) {
189
272
  switch (args[i]) {
190
- case "-t":
191
- case "--token":
192
- token = args[++i];
193
- break;
194
273
  case "-p":
195
274
  case "--port":
196
275
  port = parseInt(args[++i]);
197
276
  break;
277
+ case "--telegram-token":
278
+ telegramToken = args[++i];
279
+ break;
198
280
  case "-e":
199
281
  case "--env": {
200
282
  const kv = args[++i];
@@ -210,12 +292,13 @@ async function cmdCreate(config: Config, args: string[]) {
210
292
  }
211
293
  }
212
294
 
213
- if (!name || !token) die("usage: clawpool create <name> -t <token>");
295
+ if (!name) die("usage: clawpool create <name> [--telegram-token <token>] [-p <port>] [-e KEY=VAL ...]");
214
296
  if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name))
215
297
  die("invalid name: use alphanumerics, dots, hyphens, underscores");
216
298
  if (config.instances[name]) die(`instance '${name}' already exists`);
217
299
 
218
300
  if (!port) port = config.next_port;
301
+ const gatewayToken = generateToken();
219
302
 
220
303
  info(`Creating instance '${name}' on port ${port}...`);
221
304
 
@@ -224,7 +307,7 @@ async function cmdCreate(config: Config, args: string[]) {
224
307
  const inst: Instance = {
225
308
  name,
226
309
  port,
227
- telegram_bot_token: token,
310
+ gateway_token: gatewayToken,
228
311
  status: "running",
229
312
  container_id: "",
230
313
  volume: volumeName(name),
@@ -232,13 +315,17 @@ async function cmdCreate(config: Config, args: string[]) {
232
315
  created_at: new Date().toISOString(),
233
316
  };
234
317
 
318
+ if (telegramToken) inst.telegram_bot_token = telegramToken;
319
+
235
320
  config.instances[name] = inst;
236
- if (config.next_port <= port) config.next_port = port + 1;
321
+ if (config.next_port <= port) config.next_port = port + PORT_STEP;
237
322
 
238
323
  const cid = await rebuildContainer(config, name);
239
324
  await saveConfig(config);
240
325
 
326
+ const host = getLanIP();
241
327
  ok(`Instance '${name}' created (port: ${port}, container: ${cid.slice(0, 12)})`);
328
+ console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, port, gatewayToken)}`);
242
329
  }
243
330
 
244
331
  async function cmdList(config: Config, args: string[]) {
@@ -247,7 +334,7 @@ async function cmdList(config: Config, args: string[]) {
247
334
 
248
335
  if (names.length === 0) {
249
336
  console.log(
250
- "No instances. Create one with: clawpool create <name> -t <token>"
337
+ "No instances. Create one with: clawpool create <name>"
251
338
  );
252
339
  return;
253
340
  }
@@ -260,7 +347,15 @@ async function cmdList(config: Config, args: string[]) {
260
347
  return;
261
348
  }
262
349
 
263
- const rows = names.map((name) => {
350
+ const host = getLanIP();
351
+
352
+ console.log(
353
+ `${"NAME".padEnd(16)} ${"STATUS".padEnd(10)} ${"PORT".padEnd(7)} DASHBOARD`
354
+ );
355
+ console.log(
356
+ `${"----".padEnd(16)} ${"------".padEnd(10)} ${"----".padEnd(7)} ---------`
357
+ );
358
+ for (const name of names) {
264
359
  const inst = config.instances[name];
265
360
  const statusCol =
266
361
  inst.status === "running"
@@ -268,19 +363,12 @@ async function cmdList(config: Config, args: string[]) {
268
363
  : inst.status === "stopped"
269
364
  ? c.red(inst.status)
270
365
  : 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
366
+ const url =
367
+ inst.status === "running"
368
+ ? dashboardURL(host, inst.port, inst.gateway_token)
369
+ : c.dim("—");
282
370
  console.log(
283
- `${r.name.padEnd(16)} ${r.status.padEnd(21)} ${String(r.port).padEnd(6)} ${r.created}`
371
+ `${name.padEnd(16)} ${statusCol.padEnd(21)} ${String(inst.port).padEnd(7)} ${url}`
284
372
  );
285
373
  }
286
374
  }
@@ -292,7 +380,10 @@ async function cmdStart(config: Config, args: string[]) {
292
380
  await docker("start", containerName(name));
293
381
  config.instances[name].status = "running";
294
382
  await saveConfig(config);
383
+ const host = getLanIP();
384
+ const inst = config.instances[name];
295
385
  ok(`Instance '${name}' started`);
386
+ console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port, inst.gateway_token)}`);
296
387
  }
297
388
 
298
389
  async function cmdStop(config: Config, args: string[]) {
@@ -312,7 +403,10 @@ async function cmdRestart(config: Config, args: string[]) {
312
403
  await docker("restart", containerName(name));
313
404
  config.instances[name].status = "running";
314
405
  await saveConfig(config);
406
+ const host = getLanIP();
407
+ const inst = config.instances[name];
315
408
  ok(`Instance '${name}' restarted`);
409
+ console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port, inst.gateway_token)}`);
316
410
  }
317
411
 
318
412
  async function cmdDelete(config: Config, args: string[]) {
@@ -350,15 +444,12 @@ async function cmdRename(config: Config, args: string[]) {
350
444
 
351
445
  info(`Renaming '${oldName}' → '${newName}'...`);
352
446
 
353
- // Copy instance config
354
447
  const inst = { ...config.instances[oldName] };
355
448
  inst.name = newName;
356
449
  inst.volume = volumeName(newName);
357
450
 
358
- // Stop old container
359
451
  await dockerQuiet("rm", "-f", containerName(oldName));
360
452
 
361
- // Migrate volume
362
453
  const oldVol = volumeName(oldName);
363
454
  const newVol = volumeName(newName);
364
455
  await docker("volume", "create", newVol);
@@ -376,7 +467,6 @@ async function cmdRename(config: Config, args: string[]) {
376
467
  );
377
468
  await dockerQuiet("volume", "rm", oldVol);
378
469
 
379
- // Update config and rebuild
380
470
  delete config.instances[oldName];
381
471
  config.instances[newName] = inst;
382
472
  await rebuildContainer(config, newName);
@@ -388,7 +478,7 @@ async function cmdRename(config: Config, args: string[]) {
388
478
  async function cmdConfig(config: Config, args: string[]) {
389
479
  const name = args[0];
390
480
  if (!name || name.startsWith("-"))
391
- die("usage: clawpool config <name> [-t <token>] [-e KEY=VAL ...]");
481
+ die("usage: clawpool config <name> [--telegram-token <token>] [-e KEY=VAL ...]");
392
482
 
393
483
  requireInstance(config, name);
394
484
  const inst = config.instances[name];
@@ -396,10 +486,9 @@ async function cmdConfig(config: Config, args: string[]) {
396
486
 
397
487
  for (let i = 1; i < args.length; i++) {
398
488
  switch (args[i]) {
399
- case "-t":
400
- case "--token":
489
+ case "--telegram-token":
401
490
  inst.telegram_bot_token = args[++i];
402
- info("Token updated");
491
+ info("Telegram token updated");
403
492
  changed = true;
404
493
  break;
405
494
  case "-e":
@@ -417,7 +506,7 @@ async function cmdConfig(config: Config, args: string[]) {
417
506
  }
418
507
  }
419
508
 
420
- if (!changed) die("nothing to change — specify --token or --env");
509
+ if (!changed) die("nothing to change — specify --telegram-token or --env");
421
510
 
422
511
  info("Rebuilding container...");
423
512
  await rebuildContainer(config, name);
@@ -426,10 +515,10 @@ async function cmdConfig(config: Config, args: string[]) {
426
515
  }
427
516
 
428
517
  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]");
518
+ const name = args[0];
519
+ if (!name || name.startsWith("-")) return die("usage: clawpool logs <name> [-f] [--tail N]");
431
520
  requireInstance(config, name);
432
- const flags = args.filter((a) => a.startsWith("-"));
521
+ const flags = args.slice(1);
433
522
  await dockerExec("logs", ...flags, containerName(name));
434
523
  }
435
524
 
@@ -447,14 +536,18 @@ async function cmdStatus(config: Config, args: string[]) {
447
536
  ? c.red(inst.status)
448
537
  : c.yellow(inst.status);
449
538
 
539
+ const host = getLanIP();
450
540
  console.log(`${c.bold("Instance:")} ${inst.name}`);
451
541
  console.log(`${c.bold("Status:")} ${statusCol}`);
452
542
  console.log(`${c.bold("Port:")} ${inst.port}`);
453
543
  console.log(`${c.bold("Volume:")} ${inst.volume}`);
454
544
  console.log(`${c.bold("Created:")} ${inst.created_at}`);
455
- console.log(
456
- `${c.bold("Token:")} ${inst.telegram_bot_token.slice(0, 20)}...`
457
- );
545
+ if (inst.status === "running") {
546
+ console.log(`${c.bold("Dashboard:")} ${dashboardURL(host, inst.port, inst.gateway_token)}`);
547
+ }
548
+ if (inst.telegram_bot_token) {
549
+ console.log(`${c.bold("Telegram:")} configured`);
550
+ }
458
551
 
459
552
  const envKeys = Object.keys(inst.env);
460
553
  if (envKeys.length > 0) {
@@ -500,31 +593,35 @@ function cmdHelp() {
500
593
  console.log(`clawpool — Manage OpenClaw container instances
501
594
 
502
595
  Usage:
503
- clawpool create <name> -t <telegram_bot_token> [-p <port>] [-e KEY=VAL ...]
596
+ clawpool create <name> [--telegram-token <token>] [-p <port>] [-e KEY=VAL ...]
504
597
  clawpool list [--json]
505
598
  clawpool start <name>
506
599
  clawpool stop <name>
507
600
  clawpool restart <name>
508
601
  clawpool delete <name> [--purge]
509
602
  clawpool rename <old_name> <new_name>
510
- clawpool config <name> [-t <new_token>] [-e KEY=VAL ...]
603
+ clawpool config <name> [--telegram-token <token>] [-e KEY=VAL ...]
511
604
  clawpool logs <name> [-f]
512
605
  clawpool status <name>
513
606
  clawpool shell <name>
514
607
  clawpool image [set <image> | show]
515
608
 
516
609
  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
610
+ --telegram-token Telegram bot token (optional)
611
+ -p, --port Host port (auto-assigned from ${BASE_PORT}, step ${PORT_STEP})
612
+ -e, --env Extra environment variable (repeatable)
613
+ --purge Also remove data volume on delete
614
+ --json Output in JSON format
615
+
616
+ Each instance gets a Web Dashboard URL with an auto-generated auth token.
617
+ Access it from any browser on your local network.
522
618
 
523
619
  Examples:
524
- clawpool create alpha -t "123456:AAH..." -e "OPENAI_API_KEY=sk-..."
620
+ clawpool create alpha
621
+ clawpool create beta -e "OPENAI_API_KEY=sk-..."
622
+ clawpool create gamma --telegram-token "123456:AAH..."
525
623
  clawpool list
526
624
  clawpool logs alpha -f
527
- clawpool config alpha -t "new_token"
528
625
  clawpool delete alpha --purge`);
529
626
  }
530
627
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkclaw/clawpool",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "clawpool": "./clawpool.ts"
@@ -24,5 +24,5 @@
24
24
  "url": "https://github.com/linkclaw-lab/clawpool.git"
25
25
  },
26
26
  "license": "MIT",
27
- "description": "CLI tool to manage OpenClaw container instances with Telegram integration"
27
+ "description": "CLI tool to manage OpenClaw container instances with Web Dashboard"
28
28
  }