@linkclaw/clawpool 0.1.0 → 0.2.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 +21 -23
- package/clawpool.ts +134 -77
- 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
|
|
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
|
|
23
|
-
|
|
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> -
|
|
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> [-
|
|
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.
|
|
51
|
-
|
|
50
|
+
# 1. Create an instance — dashboard URL is printed automatically
|
|
51
|
+
clawpool create alpha
|
|
52
52
|
|
|
53
|
-
# 2.
|
|
54
|
-
clawpool create
|
|
53
|
+
# 2. Add another with extra env vars
|
|
54
|
+
clawpool create beta -e "OPENAI_API_KEY=sk-..."
|
|
55
55
|
|
|
56
|
-
# 3.
|
|
57
|
-
clawpool create
|
|
56
|
+
# 3. Optionally attach Telegram
|
|
57
|
+
clawpool create gamma --telegram-token "123456:AAH..."
|
|
58
58
|
|
|
59
|
-
# 4.
|
|
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.
|
|
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
|
-
- **
|
|
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
|
|
78
|
-
- **Telegram**:
|
|
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",
|
|
87
|
-
"next_port":
|
|
84
|
+
"image": "openclaw:latest",
|
|
85
|
+
"next_port": 18829,
|
|
88
86
|
"instances": {
|
|
89
87
|
"alpha": {
|
|
90
88
|
"name": "alpha",
|
|
91
|
-
"port":
|
|
92
|
-
"
|
|
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
|
-
|
|
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:
|
|
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)
|
|
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,67 @@ async function syncStatus(
|
|
|
123
144
|
"{{.State.Status}}",
|
|
124
145
|
containerName(name)
|
|
125
146
|
);
|
|
126
|
-
const status =
|
|
127
|
-
state === "running" ? "running" :
|
|
128
|
-
config.instances[name].status = status
|
|
129
|
-
return status
|
|
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
|
|
157
|
+
function openclawConfig(token: string): string {
|
|
158
|
+
return JSON.stringify({
|
|
159
|
+
gateway: {
|
|
160
|
+
auth: { mode: "token", token },
|
|
161
|
+
controlUi: { dangerouslyAllowHostHeaderOriginFallback: true },
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildRunArgs(config: Config, inst: Instance): string[] {
|
|
167
|
+
const ocConfig = openclawConfig(inst.gateway_token);
|
|
168
|
+
// Init as root to fix volume permissions, write config, then drop to node user
|
|
169
|
+
const entrypoint = [
|
|
170
|
+
"sh",
|
|
171
|
+
"-c",
|
|
172
|
+
[
|
|
173
|
+
"mkdir -p /home/node/data/.openclaw",
|
|
174
|
+
"chown -R node:node /home/node/data",
|
|
175
|
+
`echo '${ocConfig}' > /home/node/data/.openclaw/openclaw.json`,
|
|
176
|
+
`exec su -s /bin/sh node -c 'exec node dist/index.js gateway --allow-unconfigured --port ${inst.port} --bind lan'`,
|
|
177
|
+
].join(" && "),
|
|
178
|
+
];
|
|
179
|
+
|
|
137
180
|
const args = [
|
|
181
|
+
"run",
|
|
182
|
+
"-d",
|
|
183
|
+
"--name",
|
|
184
|
+
containerName(inst.name),
|
|
185
|
+
"--restart",
|
|
186
|
+
"unless-stopped",
|
|
187
|
+
"--user",
|
|
188
|
+
"root",
|
|
189
|
+
"-p",
|
|
190
|
+
`${inst.port}:${inst.port}`,
|
|
191
|
+
"-v",
|
|
192
|
+
`${inst.volume}:/home/node/data`,
|
|
138
193
|
"-e",
|
|
139
|
-
`
|
|
194
|
+
`OPENCLAW_HOME=/home/node/data`,
|
|
140
195
|
"-e",
|
|
141
196
|
`INSTANCE_NAME=${inst.name}`,
|
|
142
197
|
];
|
|
198
|
+
|
|
199
|
+
if (inst.telegram_bot_token) {
|
|
200
|
+
args.push("-e", `TELEGRAM_BOT_TOKEN=${inst.telegram_bot_token}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
143
203
|
for (const [k, v] of Object.entries(inst.env)) {
|
|
144
204
|
args.push("-e", `${k}=${v}`);
|
|
145
205
|
}
|
|
206
|
+
|
|
207
|
+
args.push(config.image, ...entrypoint);
|
|
146
208
|
return args;
|
|
147
209
|
}
|
|
148
210
|
|
|
@@ -150,28 +212,9 @@ async function rebuildContainer(
|
|
|
150
212
|
config: Config,
|
|
151
213
|
name: string
|
|
152
214
|
): Promise<string> {
|
|
153
|
-
const inst = config.instances[name];
|
|
154
215
|
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
|
-
|
|
216
|
+
const inst = config.instances[name];
|
|
217
|
+
const cid = await docker(...buildRunArgs(config, inst));
|
|
175
218
|
inst.container_id = cid;
|
|
176
219
|
inst.status = "running";
|
|
177
220
|
return cid;
|
|
@@ -181,20 +224,19 @@ async function rebuildContainer(
|
|
|
181
224
|
|
|
182
225
|
async function cmdCreate(config: Config, args: string[]) {
|
|
183
226
|
let name = "";
|
|
184
|
-
let token = "";
|
|
185
227
|
let port = 0;
|
|
228
|
+
let telegramToken = "";
|
|
186
229
|
const env: Record<string, string> = {};
|
|
187
230
|
|
|
188
231
|
for (let i = 0; i < args.length; i++) {
|
|
189
232
|
switch (args[i]) {
|
|
190
|
-
case "-t":
|
|
191
|
-
case "--token":
|
|
192
|
-
token = args[++i];
|
|
193
|
-
break;
|
|
194
233
|
case "-p":
|
|
195
234
|
case "--port":
|
|
196
235
|
port = parseInt(args[++i]);
|
|
197
236
|
break;
|
|
237
|
+
case "--telegram-token":
|
|
238
|
+
telegramToken = args[++i];
|
|
239
|
+
break;
|
|
198
240
|
case "-e":
|
|
199
241
|
case "--env": {
|
|
200
242
|
const kv = args[++i];
|
|
@@ -210,12 +252,13 @@ async function cmdCreate(config: Config, args: string[]) {
|
|
|
210
252
|
}
|
|
211
253
|
}
|
|
212
254
|
|
|
213
|
-
if (!name
|
|
255
|
+
if (!name) die("usage: clawpool create <name> [--telegram-token <token>] [-p <port>] [-e KEY=VAL ...]");
|
|
214
256
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name))
|
|
215
257
|
die("invalid name: use alphanumerics, dots, hyphens, underscores");
|
|
216
258
|
if (config.instances[name]) die(`instance '${name}' already exists`);
|
|
217
259
|
|
|
218
260
|
if (!port) port = config.next_port;
|
|
261
|
+
const gatewayToken = generateToken();
|
|
219
262
|
|
|
220
263
|
info(`Creating instance '${name}' on port ${port}...`);
|
|
221
264
|
|
|
@@ -224,7 +267,7 @@ async function cmdCreate(config: Config, args: string[]) {
|
|
|
224
267
|
const inst: Instance = {
|
|
225
268
|
name,
|
|
226
269
|
port,
|
|
227
|
-
|
|
270
|
+
gateway_token: gatewayToken,
|
|
228
271
|
status: "running",
|
|
229
272
|
container_id: "",
|
|
230
273
|
volume: volumeName(name),
|
|
@@ -232,13 +275,17 @@ async function cmdCreate(config: Config, args: string[]) {
|
|
|
232
275
|
created_at: new Date().toISOString(),
|
|
233
276
|
};
|
|
234
277
|
|
|
278
|
+
if (telegramToken) inst.telegram_bot_token = telegramToken;
|
|
279
|
+
|
|
235
280
|
config.instances[name] = inst;
|
|
236
|
-
if (config.next_port <= port) config.next_port = port +
|
|
281
|
+
if (config.next_port <= port) config.next_port = port + PORT_STEP;
|
|
237
282
|
|
|
238
283
|
const cid = await rebuildContainer(config, name);
|
|
239
284
|
await saveConfig(config);
|
|
240
285
|
|
|
286
|
+
const host = getLanIP();
|
|
241
287
|
ok(`Instance '${name}' created (port: ${port}, container: ${cid.slice(0, 12)})`);
|
|
288
|
+
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, port, gatewayToken)}`);
|
|
242
289
|
}
|
|
243
290
|
|
|
244
291
|
async function cmdList(config: Config, args: string[]) {
|
|
@@ -247,7 +294,7 @@ async function cmdList(config: Config, args: string[]) {
|
|
|
247
294
|
|
|
248
295
|
if (names.length === 0) {
|
|
249
296
|
console.log(
|
|
250
|
-
"No instances. Create one with: clawpool create <name>
|
|
297
|
+
"No instances. Create one with: clawpool create <name>"
|
|
251
298
|
);
|
|
252
299
|
return;
|
|
253
300
|
}
|
|
@@ -260,7 +307,15 @@ async function cmdList(config: Config, args: string[]) {
|
|
|
260
307
|
return;
|
|
261
308
|
}
|
|
262
309
|
|
|
263
|
-
const
|
|
310
|
+
const host = getLanIP();
|
|
311
|
+
|
|
312
|
+
console.log(
|
|
313
|
+
`${"NAME".padEnd(16)} ${"STATUS".padEnd(10)} ${"PORT".padEnd(7)} DASHBOARD`
|
|
314
|
+
);
|
|
315
|
+
console.log(
|
|
316
|
+
`${"----".padEnd(16)} ${"------".padEnd(10)} ${"----".padEnd(7)} ---------`
|
|
317
|
+
);
|
|
318
|
+
for (const name of names) {
|
|
264
319
|
const inst = config.instances[name];
|
|
265
320
|
const statusCol =
|
|
266
321
|
inst.status === "running"
|
|
@@ -268,19 +323,12 @@ async function cmdList(config: Config, args: string[]) {
|
|
|
268
323
|
: inst.status === "stopped"
|
|
269
324
|
? c.red(inst.status)
|
|
270
325
|
: c.yellow(inst.status);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
326
|
+
const url =
|
|
327
|
+
inst.status === "running"
|
|
328
|
+
? dashboardURL(host, inst.port, inst.gateway_token)
|
|
329
|
+
: c.dim("—");
|
|
282
330
|
console.log(
|
|
283
|
-
`${
|
|
331
|
+
`${name.padEnd(16)} ${statusCol.padEnd(21)} ${String(inst.port).padEnd(7)} ${url}`
|
|
284
332
|
);
|
|
285
333
|
}
|
|
286
334
|
}
|
|
@@ -292,7 +340,10 @@ async function cmdStart(config: Config, args: string[]) {
|
|
|
292
340
|
await docker("start", containerName(name));
|
|
293
341
|
config.instances[name].status = "running";
|
|
294
342
|
await saveConfig(config);
|
|
343
|
+
const host = getLanIP();
|
|
344
|
+
const inst = config.instances[name];
|
|
295
345
|
ok(`Instance '${name}' started`);
|
|
346
|
+
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port, inst.gateway_token)}`);
|
|
296
347
|
}
|
|
297
348
|
|
|
298
349
|
async function cmdStop(config: Config, args: string[]) {
|
|
@@ -312,7 +363,10 @@ async function cmdRestart(config: Config, args: string[]) {
|
|
|
312
363
|
await docker("restart", containerName(name));
|
|
313
364
|
config.instances[name].status = "running";
|
|
314
365
|
await saveConfig(config);
|
|
366
|
+
const host = getLanIP();
|
|
367
|
+
const inst = config.instances[name];
|
|
315
368
|
ok(`Instance '${name}' restarted`);
|
|
369
|
+
console.log(` ${c.bold("Dashboard:")} ${dashboardURL(host, inst.port, inst.gateway_token)}`);
|
|
316
370
|
}
|
|
317
371
|
|
|
318
372
|
async function cmdDelete(config: Config, args: string[]) {
|
|
@@ -350,15 +404,12 @@ async function cmdRename(config: Config, args: string[]) {
|
|
|
350
404
|
|
|
351
405
|
info(`Renaming '${oldName}' → '${newName}'...`);
|
|
352
406
|
|
|
353
|
-
// Copy instance config
|
|
354
407
|
const inst = { ...config.instances[oldName] };
|
|
355
408
|
inst.name = newName;
|
|
356
409
|
inst.volume = volumeName(newName);
|
|
357
410
|
|
|
358
|
-
// Stop old container
|
|
359
411
|
await dockerQuiet("rm", "-f", containerName(oldName));
|
|
360
412
|
|
|
361
|
-
// Migrate volume
|
|
362
413
|
const oldVol = volumeName(oldName);
|
|
363
414
|
const newVol = volumeName(newName);
|
|
364
415
|
await docker("volume", "create", newVol);
|
|
@@ -376,7 +427,6 @@ async function cmdRename(config: Config, args: string[]) {
|
|
|
376
427
|
);
|
|
377
428
|
await dockerQuiet("volume", "rm", oldVol);
|
|
378
429
|
|
|
379
|
-
// Update config and rebuild
|
|
380
430
|
delete config.instances[oldName];
|
|
381
431
|
config.instances[newName] = inst;
|
|
382
432
|
await rebuildContainer(config, newName);
|
|
@@ -388,7 +438,7 @@ async function cmdRename(config: Config, args: string[]) {
|
|
|
388
438
|
async function cmdConfig(config: Config, args: string[]) {
|
|
389
439
|
const name = args[0];
|
|
390
440
|
if (!name || name.startsWith("-"))
|
|
391
|
-
die("usage: clawpool config <name> [-
|
|
441
|
+
die("usage: clawpool config <name> [--telegram-token <token>] [-e KEY=VAL ...]");
|
|
392
442
|
|
|
393
443
|
requireInstance(config, name);
|
|
394
444
|
const inst = config.instances[name];
|
|
@@ -396,10 +446,9 @@ async function cmdConfig(config: Config, args: string[]) {
|
|
|
396
446
|
|
|
397
447
|
for (let i = 1; i < args.length; i++) {
|
|
398
448
|
switch (args[i]) {
|
|
399
|
-
case "-
|
|
400
|
-
case "--token":
|
|
449
|
+
case "--telegram-token":
|
|
401
450
|
inst.telegram_bot_token = args[++i];
|
|
402
|
-
info("
|
|
451
|
+
info("Telegram token updated");
|
|
403
452
|
changed = true;
|
|
404
453
|
break;
|
|
405
454
|
case "-e":
|
|
@@ -417,7 +466,7 @@ async function cmdConfig(config: Config, args: string[]) {
|
|
|
417
466
|
}
|
|
418
467
|
}
|
|
419
468
|
|
|
420
|
-
if (!changed) die("nothing to change — specify --token or --env");
|
|
469
|
+
if (!changed) die("nothing to change — specify --telegram-token or --env");
|
|
421
470
|
|
|
422
471
|
info("Rebuilding container...");
|
|
423
472
|
await rebuildContainer(config, name);
|
|
@@ -426,10 +475,10 @@ async function cmdConfig(config: Config, args: string[]) {
|
|
|
426
475
|
}
|
|
427
476
|
|
|
428
477
|
async function cmdLogs(config: Config, args: string[]) {
|
|
429
|
-
const name = args
|
|
430
|
-
if (!name) return die("usage: clawpool logs <name> [-f]");
|
|
478
|
+
const name = args[0];
|
|
479
|
+
if (!name || name.startsWith("-")) return die("usage: clawpool logs <name> [-f] [--tail N]");
|
|
431
480
|
requireInstance(config, name);
|
|
432
|
-
const flags = args.
|
|
481
|
+
const flags = args.slice(1);
|
|
433
482
|
await dockerExec("logs", ...flags, containerName(name));
|
|
434
483
|
}
|
|
435
484
|
|
|
@@ -447,14 +496,18 @@ async function cmdStatus(config: Config, args: string[]) {
|
|
|
447
496
|
? c.red(inst.status)
|
|
448
497
|
: c.yellow(inst.status);
|
|
449
498
|
|
|
499
|
+
const host = getLanIP();
|
|
450
500
|
console.log(`${c.bold("Instance:")} ${inst.name}`);
|
|
451
501
|
console.log(`${c.bold("Status:")} ${statusCol}`);
|
|
452
502
|
console.log(`${c.bold("Port:")} ${inst.port}`);
|
|
453
503
|
console.log(`${c.bold("Volume:")} ${inst.volume}`);
|
|
454
504
|
console.log(`${c.bold("Created:")} ${inst.created_at}`);
|
|
455
|
-
|
|
456
|
-
`${c.bold("
|
|
457
|
-
|
|
505
|
+
if (inst.status === "running") {
|
|
506
|
+
console.log(`${c.bold("Dashboard:")} ${dashboardURL(host, inst.port, inst.gateway_token)}`);
|
|
507
|
+
}
|
|
508
|
+
if (inst.telegram_bot_token) {
|
|
509
|
+
console.log(`${c.bold("Telegram:")} configured`);
|
|
510
|
+
}
|
|
458
511
|
|
|
459
512
|
const envKeys = Object.keys(inst.env);
|
|
460
513
|
if (envKeys.length > 0) {
|
|
@@ -500,31 +553,35 @@ function cmdHelp() {
|
|
|
500
553
|
console.log(`clawpool — Manage OpenClaw container instances
|
|
501
554
|
|
|
502
555
|
Usage:
|
|
503
|
-
clawpool create <name> -
|
|
556
|
+
clawpool create <name> [--telegram-token <token>] [-p <port>] [-e KEY=VAL ...]
|
|
504
557
|
clawpool list [--json]
|
|
505
558
|
clawpool start <name>
|
|
506
559
|
clawpool stop <name>
|
|
507
560
|
clawpool restart <name>
|
|
508
561
|
clawpool delete <name> [--purge]
|
|
509
562
|
clawpool rename <old_name> <new_name>
|
|
510
|
-
clawpool config <name> [-
|
|
563
|
+
clawpool config <name> [--telegram-token <token>] [-e KEY=VAL ...]
|
|
511
564
|
clawpool logs <name> [-f]
|
|
512
565
|
clawpool status <name>
|
|
513
566
|
clawpool shell <name>
|
|
514
567
|
clawpool image [set <image> | show]
|
|
515
568
|
|
|
516
569
|
Options:
|
|
517
|
-
-
|
|
518
|
-
-p, --port
|
|
519
|
-
-e, --env
|
|
520
|
-
--purge
|
|
521
|
-
--json
|
|
570
|
+
--telegram-token Telegram bot token (optional)
|
|
571
|
+
-p, --port Host port (auto-assigned from ${BASE_PORT}, step ${PORT_STEP})
|
|
572
|
+
-e, --env Extra environment variable (repeatable)
|
|
573
|
+
--purge Also remove data volume on delete
|
|
574
|
+
--json Output in JSON format
|
|
575
|
+
|
|
576
|
+
Each instance gets a Web Dashboard URL with an auto-generated auth token.
|
|
577
|
+
Access it from any browser on your local network.
|
|
522
578
|
|
|
523
579
|
Examples:
|
|
524
|
-
clawpool create alpha
|
|
580
|
+
clawpool create alpha
|
|
581
|
+
clawpool create beta -e "OPENAI_API_KEY=sk-..."
|
|
582
|
+
clawpool create gamma --telegram-token "123456:AAH..."
|
|
525
583
|
clawpool list
|
|
526
584
|
clawpool logs alpha -f
|
|
527
|
-
clawpool config alpha -t "new_token"
|
|
528
585
|
clawpool delete alpha --purge`);
|
|
529
586
|
}
|
|
530
587
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkclaw/clawpool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
|
27
|
+
"description": "CLI tool to manage OpenClaw container instances with Web Dashboard"
|
|
28
28
|
}
|