@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.
- package/README.md +21 -23
- package/clawpool.ts +174 -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,107 @@ 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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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>
|
|
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
|
|
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
|
-
|
|
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
|
|
366
|
+
const url =
|
|
367
|
+
inst.status === "running"
|
|
368
|
+
? dashboardURL(host, inst.port, inst.gateway_token)
|
|
369
|
+
: c.dim("—");
|
|
282
370
|
console.log(
|
|
283
|
-
`${
|
|
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> [-
|
|
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 "-
|
|
400
|
-
case "--token":
|
|
489
|
+
case "--telegram-token":
|
|
401
490
|
inst.telegram_bot_token = args[++i];
|
|
402
|
-
info("
|
|
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
|
|
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.
|
|
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
|
-
|
|
456
|
-
`${c.bold("
|
|
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> -
|
|
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> [-
|
|
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
|
-
-
|
|
518
|
-
-p, --port
|
|
519
|
-
-e, --env
|
|
520
|
-
--purge
|
|
521
|
-
--json
|
|
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
|
|
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
|
|
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
|
|
27
|
+
"description": "CLI tool to manage OpenClaw container instances with Web Dashboard"
|
|
28
28
|
}
|