@openbrt/weclawbotctl 0.1.5 → 0.1.7

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 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,25 @@ Agent.
86
91
  To install the OpenClaw plugin itself from npm after the package is published:
87
92
 
88
93
  ```bash
89
- openclaw plugins install @openbrt/weclawbotctl --pin
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
+
92
113
  To install the OpenClaw plugin from a local checkout during development:
93
114
 
94
115
  ```bash
@@ -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 || "openclaw",
273
+ spec: DEFAULT_OPENCLAW_PLUGIN_SPEC,
274
+ force: true,
275
+ doctor: true,
276
+ });
277
+ const openclaw = String(options.bin || "openclaw");
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 || "openclaw",
296
+ json: false,
297
+ gateway: true,
298
+ timeout: 20,
299
+ });
300
+ const openclaw = String(options.bin || "openclaw");
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,164 @@ 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
+ function compactText(value) {
587
+ return String(value || "").replace(/\s+/gu, " ").trim().slice(0, 500);
588
+ }
589
+
343
590
  function shellValue(value) {
344
591
  return `'${String(value).replaceAll("'", "'\\''")}'`;
345
592
  }
@@ -357,5 +604,7 @@ function usage() {
357
604
  weclawbotctl unbind --yes
358
605
  weclawbotctl thinking [--ttl seconds] [--id correlation-id]
359
606
  weclawbotctl idle [--id correlation-id]
360
- weclawbotctl screen <document.json> [--force]`);
607
+ weclawbotctl screen <document.json> [--force]
608
+ weclawbotctl openclaw install [--spec @openbrt/weclawbotctl] [--force=false]
609
+ weclawbotctl openclaw doctor [--gateway=false] [--json]`);
361
610
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openbrt/weclawbotctl",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "WeClawBot pairing and screen-control CLI for local AI agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -6,6 +6,7 @@ Wants=network-online.target
6
6
  [Service]
7
7
  Type=simple
8
8
  EnvironmentFile=%h/.config/weclawbot/openclaw-curator.env
9
+ Environment=NODE_EXTRA_CA_CERTS=%h/.openclaw/gateway/tls/gateway-cert.pem
9
10
  ExecStart=%h/.npm-global/bin/weclawbot-openclaw-bridge
10
11
  Restart=always
11
12
  RestartSec=3