@openbrt/weclawbotctl 0.1.6 → 0.1.8
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 +32 -1
- package/bin/weclawbotctl.mjs +267 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,11 @@ skill, `weclawbot_status`, `weclawbot_validate_screen_document`,
|
|
|
67
67
|
polls `weclawbot.link`; no public HTTP endpoint, port forwarding, or WeChat
|
|
68
68
|
credential is required on the OpenClaw host.
|
|
69
69
|
|
|
70
|
+
OpenClaw plugin tools require OpenClaw `2026.6.9` or newer because the plugin
|
|
71
|
+
declares `contracts.tools`. Older OpenClaw builds can still run
|
|
72
|
+
`weclawbotctl` as a shell command, but the agent tool injection will not be
|
|
73
|
+
reliable.
|
|
74
|
+
|
|
70
75
|
## Install from npm
|
|
71
76
|
|
|
72
77
|
```bash
|
|
@@ -86,9 +91,35 @@ Agent.
|
|
|
86
91
|
To install the OpenClaw plugin itself from npm after the package is published:
|
|
87
92
|
|
|
88
93
|
```bash
|
|
89
|
-
openclaw
|
|
94
|
+
weclawbotctl openclaw install
|
|
95
|
+
weclawbotctl openclaw doctor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This wraps:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
openclaw plugins install @openbrt/weclawbotctl --pin --force
|
|
102
|
+
openclaw plugins enable weclawbot
|
|
90
103
|
```
|
|
91
104
|
|
|
105
|
+
Restart the OpenClaw gateway or app after installation so it reloads plugin
|
|
106
|
+
tools. The doctor checks the OpenClaw version, plugin installation, plugin
|
|
107
|
+
diagnostics, and local gateway reachability. If a local WSS gateway uses a
|
|
108
|
+
self-signed certificate, the doctor will suggest `NODE_EXTRA_CA_CERTS`. If the
|
|
109
|
+
certificate lacks `localhost` or `127.0.0.1` SANs, fix the gateway certificate
|
|
110
|
+
or use a certificate trusted by Node. The package does not rewrite other
|
|
111
|
+
users' OpenClaw gateway certificates automatically.
|
|
112
|
+
|
|
113
|
+
If OpenClaw is installed outside `PATH`, pass it explicitly:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
weclawbotctl openclaw doctor --bin /path/to/openclaw
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
When `weclawbotctl` is installed globally, it also checks the same npm global
|
|
120
|
+
`bin` directory for `openclaw`, which helps non-interactive SSH and systemd
|
|
121
|
+
environments where shell startup files are not loaded.
|
|
122
|
+
|
|
92
123
|
To install the OpenClaw plugin from a local checkout during development:
|
|
93
124
|
|
|
94
125
|
```bash
|
package/bin/weclawbotctl.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
4
5
|
import fs from "node:fs/promises";
|
|
5
6
|
import os from "node:os";
|
|
6
7
|
import path from "node:path";
|
|
@@ -12,9 +13,11 @@ import { normalizeCredentials, publishControl, testConnection } from "../lib/mqt
|
|
|
12
13
|
|
|
13
14
|
const DEFAULT_ENDPOINT = "https://weclawbot.link/byoa";
|
|
14
15
|
const DEFAULT_CREDENTIALS_PATH = path.join(os.homedir(), ".config", "weclawbot", "agent-mqtt.json");
|
|
16
|
+
const DEFAULT_OPENCLAW_PLUGIN_SPEC = "@openbrt/weclawbotctl";
|
|
17
|
+
const MIN_OPENCLAW_VERSION = "2026.6.9";
|
|
15
18
|
|
|
16
19
|
const [command, ...args] = process.argv.slice(2);
|
|
17
|
-
const commands = new Set(["bind", "status", "doctor", "export", "unbind", "thinking", "idle", "screen"]);
|
|
20
|
+
const commands = new Set(["bind", "status", "doctor", "export", "unbind", "thinking", "idle", "screen", "openclaw"]);
|
|
18
21
|
if (!commands.has(command)) {
|
|
19
22
|
usage();
|
|
20
23
|
process.exit(64);
|
|
@@ -27,6 +30,7 @@ try {
|
|
|
27
30
|
else if (command === "export") await commandExport(args);
|
|
28
31
|
else if (command === "unbind") await commandUnbind(args);
|
|
29
32
|
else if (command === "screen") await commandScreen(args);
|
|
33
|
+
else if (command === "openclaw") await commandOpenClaw(args);
|
|
30
34
|
else await commandActivity(command, args);
|
|
31
35
|
} catch (error) {
|
|
32
36
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -250,6 +254,91 @@ async function commandActivity(state, values) {
|
|
|
250
254
|
console.log(JSON.stringify({ ok: true, state: activity.state, correlation_id: correlationId }));
|
|
251
255
|
}
|
|
252
256
|
|
|
257
|
+
async function commandOpenClaw(values) {
|
|
258
|
+
const first = String(values[0] || "");
|
|
259
|
+
const subcommand = first && !first.startsWith("--") ? first : "doctor";
|
|
260
|
+
const rest = first && !first.startsWith("--") ? values.slice(1) : values;
|
|
261
|
+
if (subcommand === "install") {
|
|
262
|
+
await commandOpenClawInstall(rest);
|
|
263
|
+
} else if (subcommand === "doctor") {
|
|
264
|
+
await commandOpenClawDoctor(rest);
|
|
265
|
+
} else {
|
|
266
|
+
throw new Error("Usage: weclawbotctl openclaw install|doctor");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function commandOpenClawInstall(values) {
|
|
271
|
+
const options = parseOptions(values, {
|
|
272
|
+
bin: process.env.OPENCLAW_BIN || "",
|
|
273
|
+
spec: DEFAULT_OPENCLAW_PLUGIN_SPEC,
|
|
274
|
+
force: true,
|
|
275
|
+
doctor: true,
|
|
276
|
+
});
|
|
277
|
+
const openclaw = await resolveOpenClawBin(options.bin);
|
|
278
|
+
const spec = String(options.spec || DEFAULT_OPENCLAW_PLUGIN_SPEC);
|
|
279
|
+
const version = await runCaptured(openclaw, ["--version"], { timeoutMs: 10_000 });
|
|
280
|
+
const versionCheck = openClawVersionCheck(version.stdout);
|
|
281
|
+
if (version.code !== 0) throw new Error(`OpenClaw CLI not found. Install OpenClaw or set OPENCLAW_BIN=/path/to/openclaw.`);
|
|
282
|
+
if (!versionCheck.ok) throw new Error(`${versionCheck.detail}. ${versionCheck.hint}`);
|
|
283
|
+
const installArgs = ["plugins", "install", spec, "--pin"];
|
|
284
|
+
if (options.force) installArgs.push("--force");
|
|
285
|
+
await runRequired(openclaw, installArgs);
|
|
286
|
+
await runRequired(openclaw, ["plugins", "enable", "weclawbot"]);
|
|
287
|
+
console.log("OpenClaw plugin installed and enabled. Restart the OpenClaw gateway or app so it reloads plugins.");
|
|
288
|
+
if (options.doctor) {
|
|
289
|
+
await commandOpenClawDoctor(["--bin", openclaw, "--gateway=false"]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function commandOpenClawDoctor(values) {
|
|
294
|
+
const options = parseOptions(values, {
|
|
295
|
+
bin: process.env.OPENCLAW_BIN || "",
|
|
296
|
+
json: false,
|
|
297
|
+
gateway: true,
|
|
298
|
+
timeout: 20,
|
|
299
|
+
});
|
|
300
|
+
const openclaw = await resolveOpenClawBin(options.bin);
|
|
301
|
+
const timeoutMs = Math.max(1, Number(options.timeout) || 20) * 1000;
|
|
302
|
+
const checks = [];
|
|
303
|
+
|
|
304
|
+
const version = await runCaptured(openclaw, ["--version"], { timeoutMs });
|
|
305
|
+
pushProcessCheck(checks, "openclaw_bin", version, "OpenClaw CLI found");
|
|
306
|
+
if (version.code === 0) checks.push(openClawVersionCheck(version.stdout));
|
|
307
|
+
|
|
308
|
+
const inspect = await runCaptured(openclaw, ["plugins", "inspect", "weclawbot"], { timeoutMs });
|
|
309
|
+
checks.push({
|
|
310
|
+
name: "openclaw_plugin",
|
|
311
|
+
ok: inspect.code === 0,
|
|
312
|
+
detail: inspect.code === 0 ? summarizeOpenClawPlugin(inspect.stdout) : compactText(inspect.stderr || inspect.stdout || "not installed"),
|
|
313
|
+
hint: inspect.code === 0 ? "" : `Run: weclawbotctl openclaw install`,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const pluginDoctor = await runCaptured(openclaw, ["plugins", "doctor"], { timeoutMs });
|
|
317
|
+
const doctorText = `${pluginDoctor.stdout}\n${pluginDoctor.stderr}`;
|
|
318
|
+
const weclawbotIssue = /(^|\n)\s*-\s*weclawbot:|weclawbot.*contracts\.tools|weclawbot.*plugin/i.test(doctorText);
|
|
319
|
+
checks.push({
|
|
320
|
+
name: "openclaw_plugin_doctor",
|
|
321
|
+
ok: pluginDoctor.code === 0 && !weclawbotIssue,
|
|
322
|
+
detail: pluginDoctor.code === 0 && !weclawbotIssue
|
|
323
|
+
? "no WeClawBot plugin issues detected"
|
|
324
|
+
: compactText(doctorText || `exit ${pluginDoctor.code}`),
|
|
325
|
+
hint: weclawbotIssue ? "Upgrade and reinstall: weclawbotctl openclaw install" : "",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (options.gateway) {
|
|
329
|
+
const gatewayEnv = { ...process.env };
|
|
330
|
+
const defaultCert = path.join(os.homedir(), ".openclaw", "gateway", "tls", "gateway-cert.pem");
|
|
331
|
+
if (!gatewayEnv.NODE_EXTRA_CA_CERTS && await fileExists(defaultCert)) {
|
|
332
|
+
gatewayEnv.NODE_EXTRA_CA_CERTS = defaultCert;
|
|
333
|
+
}
|
|
334
|
+
const status = await runCaptured(openclaw, ["status", "--json"], { env: gatewayEnv, timeoutMs });
|
|
335
|
+
checks.push(gatewayStatusCheck(status, gatewayEnv.NODE_EXTRA_CA_CERTS || ""));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
printOpenClawDoctor(checks, options.json);
|
|
339
|
+
if (checks.some((check) => !check.ok)) process.exitCode = 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
253
342
|
function parseOptions(values, defaults = {}) {
|
|
254
343
|
const options = { ...defaults, _: [] };
|
|
255
344
|
for (let index = 0; index < values.length; index += 1) {
|
|
@@ -340,6 +429,180 @@ function printDoctor(checks, json) {
|
|
|
340
429
|
}
|
|
341
430
|
}
|
|
342
431
|
|
|
432
|
+
function printOpenClawDoctor(checks, json) {
|
|
433
|
+
if (json) {
|
|
434
|
+
console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
for (const check of checks) {
|
|
438
|
+
console.log(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.detail}`);
|
|
439
|
+
if (!check.ok && check.hint) console.log(`hint ${check.name}: ${check.hint}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function runRequired(file, args) {
|
|
444
|
+
const result = await runProcess(file, args, { stdio: "inherit" });
|
|
445
|
+
if (result.code !== 0) {
|
|
446
|
+
throw new Error(`${file} ${args.join(" ")} failed with exit ${result.code}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function runCaptured(file, args, options = {}) {
|
|
451
|
+
return runProcess(file, args, { ...options, stdio: "pipe" });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function runProcess(file, args, options = {}) {
|
|
455
|
+
return new Promise((resolve) => {
|
|
456
|
+
const child = spawn(file, args, {
|
|
457
|
+
env: options.env || process.env,
|
|
458
|
+
stdio: options.stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
459
|
+
});
|
|
460
|
+
let stdout = "";
|
|
461
|
+
let stderr = "";
|
|
462
|
+
let timedOut = false;
|
|
463
|
+
const timeoutMs = Number(options.timeoutMs || 0);
|
|
464
|
+
const timeout = timeoutMs > 0 ? setTimeout(() => {
|
|
465
|
+
timedOut = true;
|
|
466
|
+
child.kill("SIGTERM");
|
|
467
|
+
}, timeoutMs) : null;
|
|
468
|
+
if (child.stdout) child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
469
|
+
if (child.stderr) child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
470
|
+
child.on("error", (error) => {
|
|
471
|
+
if (timeout) clearTimeout(timeout);
|
|
472
|
+
resolve({ code: 127, stdout, stderr: error.message, timedOut });
|
|
473
|
+
});
|
|
474
|
+
child.on("close", (code) => {
|
|
475
|
+
if (timeout) clearTimeout(timeout);
|
|
476
|
+
resolve({ code: timedOut ? 124 : (code ?? 1), stdout, stderr, timedOut });
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function pushProcessCheck(checks, name, result, okDetail) {
|
|
482
|
+
checks.push({
|
|
483
|
+
name,
|
|
484
|
+
ok: result.code === 0,
|
|
485
|
+
detail: result.code === 0 ? compactText(result.stdout || okDetail) : compactText(result.stderr || result.stdout || `exit ${result.code}`),
|
|
486
|
+
hint: result.code === 127 ? "Install OpenClaw or set OPENCLAW_BIN=/path/to/openclaw." : "",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function openClawVersionCheck(output) {
|
|
491
|
+
const version = parseOpenClawVersion(output);
|
|
492
|
+
if (!version) {
|
|
493
|
+
return {
|
|
494
|
+
name: "openclaw_version",
|
|
495
|
+
ok: true,
|
|
496
|
+
detail: compactText(output || "version unknown"),
|
|
497
|
+
hint: "",
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const ok = compareDottedVersion(version, MIN_OPENCLAW_VERSION) >= 0;
|
|
501
|
+
return {
|
|
502
|
+
name: "openclaw_version",
|
|
503
|
+
ok,
|
|
504
|
+
detail: `OpenClaw ${version} (requires >= ${MIN_OPENCLAW_VERSION})`,
|
|
505
|
+
hint: ok ? "" : "Upgrade OpenClaw before installing the WeClawBot plugin tools.",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function parseOpenClawVersion(output) {
|
|
510
|
+
const match = String(output || "").match(/OpenClaw\s+([0-9]+(?:\.[0-9]+){1,3})/u);
|
|
511
|
+
return match ? match[1] : "";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function compareDottedVersion(left, right) {
|
|
515
|
+
const a = String(left).split(".").map((part) => Number(part) || 0);
|
|
516
|
+
const b = String(right).split(".").map((part) => Number(part) || 0);
|
|
517
|
+
const length = Math.max(a.length, b.length);
|
|
518
|
+
for (let index = 0; index < length; index += 1) {
|
|
519
|
+
const diff = (a[index] || 0) - (b[index] || 0);
|
|
520
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
521
|
+
}
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function summarizeOpenClawPlugin(output) {
|
|
526
|
+
const lines = String(output || "").split(/\r?\n/u);
|
|
527
|
+
const wanted = [];
|
|
528
|
+
for (const line of lines) {
|
|
529
|
+
if (/^(Status|Version|Source|Install path|Spec):/u.test(line.trim())) wanted.push(line.trim());
|
|
530
|
+
}
|
|
531
|
+
return wanted.length ? wanted.join("; ") : "installed";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function gatewayStatusCheck(result, extraCaCerts) {
|
|
535
|
+
const text = `${result.stdout}\n${result.stderr}`;
|
|
536
|
+
if (result.code !== 0) {
|
|
537
|
+
return {
|
|
538
|
+
name: "openclaw_gateway_status",
|
|
539
|
+
ok: false,
|
|
540
|
+
detail: compactText(text || `exit ${result.code}`),
|
|
541
|
+
hint: tlsHint(text, extraCaCerts),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const status = JSON.parse(result.stdout);
|
|
546
|
+
const gateway = status.gateway || {};
|
|
547
|
+
const ok = gateway.reachable === true;
|
|
548
|
+
return {
|
|
549
|
+
name: "openclaw_gateway_status",
|
|
550
|
+
ok,
|
|
551
|
+
detail: ok ? `reachable ${gateway.url || ""}`.trim() : compactText(gateway.error || "gateway not reachable"),
|
|
552
|
+
hint: ok ? "" : tlsHint(gateway.error || text, extraCaCerts),
|
|
553
|
+
};
|
|
554
|
+
} catch {
|
|
555
|
+
return {
|
|
556
|
+
name: "openclaw_gateway_status",
|
|
557
|
+
ok: false,
|
|
558
|
+
detail: compactText(text || "status JSON parse failed"),
|
|
559
|
+
hint: tlsHint(text, extraCaCerts),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function tlsHint(text, extraCaCerts) {
|
|
565
|
+
const value = String(text || "");
|
|
566
|
+
if (/self-signed certificate/i.test(value)) {
|
|
567
|
+
return extraCaCerts
|
|
568
|
+
? `Retry with NODE_EXTRA_CA_CERTS=${extraCaCerts}, or use a gateway certificate trusted by Node.`
|
|
569
|
+
: "If your OpenClaw gateway uses a self-signed cert, set NODE_EXTRA_CA_CERTS to its CA/cert path.";
|
|
570
|
+
}
|
|
571
|
+
if (/ALTNAME|Hostname\/IP does not match certificate/i.test(value)) {
|
|
572
|
+
return "Regenerate the OpenClaw gateway certificate with SANs for localhost and 127.0.0.1, or configure OpenClaw to use a valid certificate.";
|
|
573
|
+
}
|
|
574
|
+
return "";
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function fileExists(file) {
|
|
578
|
+
try {
|
|
579
|
+
await fs.access(file);
|
|
580
|
+
return true;
|
|
581
|
+
} catch {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function resolveOpenClawBin(value) {
|
|
587
|
+
if (value) return String(value);
|
|
588
|
+
const scriptDir = path.dirname(process.argv[1] || "");
|
|
589
|
+
const candidates = [
|
|
590
|
+
scriptDir ? path.join(scriptDir, "openclaw") : "",
|
|
591
|
+
path.join(os.homedir(), ".npm-global", "bin", "openclaw"),
|
|
592
|
+
path.join(os.homedir(), ".local", "bin", "openclaw"),
|
|
593
|
+
"/usr/local/bin/openclaw",
|
|
594
|
+
"/opt/homebrew/bin/openclaw",
|
|
595
|
+
].filter(Boolean);
|
|
596
|
+
for (const candidate of candidates) {
|
|
597
|
+
if (await fileExists(candidate)) return candidate;
|
|
598
|
+
}
|
|
599
|
+
return "openclaw";
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function compactText(value) {
|
|
603
|
+
return String(value || "").replace(/\s+/gu, " ").trim().slice(0, 500);
|
|
604
|
+
}
|
|
605
|
+
|
|
343
606
|
function shellValue(value) {
|
|
344
607
|
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
345
608
|
}
|
|
@@ -357,5 +620,7 @@ function usage() {
|
|
|
357
620
|
weclawbotctl unbind --yes
|
|
358
621
|
weclawbotctl thinking [--ttl seconds] [--id correlation-id]
|
|
359
622
|
weclawbotctl idle [--id correlation-id]
|
|
360
|
-
weclawbotctl screen <document.json> [--force]
|
|
623
|
+
weclawbotctl screen <document.json> [--force]
|
|
624
|
+
weclawbotctl openclaw install [--spec @openbrt/weclawbotctl] [--bin /path/to/openclaw] [--force=false]
|
|
625
|
+
weclawbotctl openclaw doctor [--gateway=false] [--bin /path/to/openclaw] [--json]`);
|
|
361
626
|
}
|