@openbrt/weclawbotctl 0.1.3 → 0.1.5

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
@@ -15,8 +15,9 @@ Install @openbrt/weclawbotctl and connect to my WeClawBot screen with pairing co
15
15
 
16
16
  Replace `123456` with the six-digit code shown on the screen. The agent should
17
17
  then install this npm package, bind the screen, run the online doctor check,
18
- and use `weclawbotctl thinking`, `weclawbotctl idle`, and `weclawbotctl screen`
19
- when it works.
18
+ and use `weclawbotctl screen` with a pre-rendered pixel document for visible
19
+ screen updates. Use `weclawbotctl thinking` and `weclawbotctl idle` for
20
+ temporary work-state overlays.
20
21
 
21
22
  ## Agent Instructions
22
23
 
@@ -51,19 +52,20 @@ weclawbotctl thinking --id "$task_id" --ttl 45
51
52
  weclawbotctl idle --id "$task_id"
52
53
  ```
53
54
 
54
- To put content on the screen, render a valid WeClawBot `screen_document` JSON
55
- and send it:
55
+ To put text, status, diagrams, or images on the screen, render them into a
56
+ pre-rendered `weclawbot.screen_document.v1` first. The firmware receives pixels;
57
+ it does not lay out text, choose fonts, or split pages for agents.
56
58
 
57
59
  ```bash
58
60
  weclawbotctl screen /path/to/screen-document.json
59
61
  ```
60
62
 
61
63
  The package also includes an OpenClaw integration: the `weclawbot-curator`
62
- skill, a local
63
- `weclawbot_validate_screen_document` tool, a `weclawbot_validate_activity`
64
- tool, and a small outbound bridge
65
- service. The bridge polls `weclawbot.link`; no public HTTP endpoint, port
66
- forwarding, or WeChat credential is required on the OpenClaw host.
64
+ skill, `weclawbot_status`, `weclawbot_validate_screen_document`,
65
+ `weclawbot_publish_screen_document`, `weclawbot_validate_activity`,
66
+ `weclawbot_publish_activity`, and a small outbound bridge service. The bridge
67
+ polls `weclawbot.link`; no public HTTP endpoint, port forwarding, or WeChat
68
+ credential is required on the OpenClaw host.
67
69
 
68
70
  ## Install from npm
69
71
 
@@ -140,10 +142,19 @@ systemctl --user enable --now weclawbot-openclaw-curator
140
142
 
141
143
  The normal bridge is message-triggered by WeChat. Scheduled cards and other
142
144
  agent-originated updates use a separately paired MQTT/TLS control channel;
143
- they are never placed in a gateway mailbox. The plugin already exposes the
144
- validator that a user agent calls before publishing a 1-bit screen document.
145
- It reports `direct_delivery_ready:false` until the physical firmware has been
146
- paired and advertises `agent_transport.available=true`.
145
+ they are never placed in a gateway mailbox. A paired Agent can publish a
146
+ pre-rendered screen document immediately:
147
+
148
+ ```bash
149
+ weclawbotctl doctor --online
150
+ weclawbotctl screen /path/to/screen-document.json
151
+ ```
152
+
153
+ The plugin exposes both validators and publish tools. The validator reports
154
+ `direct_delivery_ready:false` when a supplied event contract says that specific
155
+ inbound event cannot use the live document path, but that does not disable a
156
+ locally paired `weclawbotctl` profile. Before saying direct screen delivery is
157
+ unavailable, run `weclawbotctl status` or `weclawbotctl doctor --online`.
147
158
 
148
159
  The pairing UX deliberately requires no user-supplied Agent endpoint: choose
149
160
  **自定义智能体** in the device configurator, then enter the six-digit code shown
@@ -194,15 +205,24 @@ weclawbotctl thinking --id "$task_id" --ttl 45
194
205
  weclawbotctl idle --id "$task_id"
195
206
  ```
196
207
 
197
- To put a pre-rendered monochrome document on screen, pass its JSON file to the
198
- same scoped MQTT credential. The document must use the live revision in the
199
- last `device_context` (an empty revision is valid for the first document), one
200
- to three `mono1` pages, and a future UTC expiry:
208
+ To put content on screen, pass a pre-rendered monochrome document to the same
209
+ credential. The document must use the live revision in the last
210
+ `device_context` (an empty revision is valid for the first document), one to
211
+ three `mono1` pages, and a future UTC expiry. Agents may use PIL, Canvas, SVG
212
+ rasterization, screenshots, or any local renderer, but the MQTT payload must be
213
+ pixels:
201
214
 
202
215
  ```bash
203
216
  weclawbotctl screen /path/to/screen-document.json
204
217
  ```
205
218
 
219
+ When the user explicitly asks to overwrite whatever is currently shown, and
220
+ the firmware supports forced replacement, use:
221
+
222
+ ```bash
223
+ weclawbotctl screen /path/to/screen-document.json --force
224
+ ```
225
+
206
226
  These commands use MQTT/TLS directly, publish QoS 1 without retain, and
207
227
  never create an offline command queue. See
208
228
  `docs/agent-direct-control-protocol.md` in the firmware repository for the
@@ -198,12 +198,16 @@ async function commandUnbind(values) {
198
198
  }
199
199
 
200
200
  async function commandScreen(values) {
201
- const options = parseOptions(values, { credentials: credentialsPath() });
201
+ const options = parseOptions(values, { credentials: credentialsPath(), force: false });
202
202
  const file = String(options._[0] || "").trim();
203
203
  if (!file || options._.length !== 1) {
204
- throw new Error("Usage: weclawbotctl screen <document.json>");
204
+ throw new Error("Usage: weclawbotctl screen <document.json> [--force]");
205
205
  }
206
206
  const document = JSON.parse(await fs.readFile(file, "utf8"));
207
+ if (options.force) {
208
+ document.force_replace = true;
209
+ document.base_revision = "*";
210
+ }
207
211
  const validation = validateScreenDocument(document, {
208
212
  agent_transport: { available: true, screen_document_available: true },
209
213
  });
@@ -216,7 +220,7 @@ async function commandScreen(values) {
216
220
  kind: "screen_document",
217
221
  document,
218
222
  });
219
- console.log(JSON.stringify({ ok: true, id: document.id, pages: document.pages.length }));
223
+ console.log(JSON.stringify({ ok: true, id: document.id, pages: document.pages.length, force_replace: document.force_replace === true }));
220
224
  }
221
225
 
222
226
  async function commandActivity(state, values) {
@@ -353,5 +357,5 @@ function usage() {
353
357
  weclawbotctl unbind --yes
354
358
  weclawbotctl thinking [--ttl seconds] [--id correlation-id]
355
359
  weclawbotctl idle [--id correlation-id]
356
- weclawbotctl screen <document.json>`);
360
+ weclawbotctl screen <document.json> [--force]`);
357
361
  }
package/index.mjs CHANGED
@@ -1,16 +1,54 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
1
6
  import { Type } from "typebox";
2
7
  import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
3
8
 
4
9
  import { validateActivity } from "./lib/activity.mjs";
5
10
  import { validateScreenDocument } from "./lib/direct-control.mjs";
11
+ import { normalizeCredentials, publishControl, testConnection } from "./lib/mqtt-control.mjs";
12
+
13
+ const DEFAULT_CREDENTIALS_PATH = path.join(os.homedir(), ".config", "weclawbot", "agent-mqtt.json");
6
14
 
7
- // The long-running curator bridge remains a separate service. This plugin's
8
- // tool is local, deterministic, and contains no credential or network access.
15
+ // The long-running curator bridge remains a separate service. These tools keep
16
+ // the local agent path explicit: validate first, then publish only with the
17
+ // user's paired MQTT credential.
9
18
  export default defineToolPlugin({
10
19
  id: "weclawbot",
11
20
  name: "WeClawBot",
12
- description: "WeClawBot screen-curation skill pack and direct-control validator.",
21
+ description: "WeClawBot screen-curation skill pack and direct MQTT control tools.",
13
22
  tools: (tool) => [
23
+ tool({
24
+ name: "weclawbot_status",
25
+ label: "Check WeClawBot pairing",
26
+ description: "Read the local WeClawBot MQTT pairing profile and optionally test the live MQTT connection.",
27
+ parameters: Type.Object({
28
+ credentials_path: Type.Optional(Type.String()),
29
+ online: Type.Optional(Type.Boolean()),
30
+ }, { additionalProperties: false }),
31
+ execute: async ({ credentials_path, online }) => {
32
+ const file = expandPath(credentials_path || DEFAULT_CREDENTIALS_PATH);
33
+ const payload = await readCredentials(file);
34
+ if (!payload) {
35
+ return { ok: false, paired: false, credentials_path: file, error: "not_paired" };
36
+ }
37
+ const config = normalizeCredentials(payload);
38
+ const result = {
39
+ ok: true,
40
+ paired: true,
41
+ credentials_path: file,
42
+ binding: payload.binding || {},
43
+ mqtt: maskedMqtt(config, payload.mqtt?.topics || {}),
44
+ };
45
+ if (online) {
46
+ await testConnection(payload);
47
+ result.online = true;
48
+ }
49
+ return result;
50
+ },
51
+ }),
14
52
  tool({
15
53
  name: "weclawbot_validate_screen_document",
16
54
  label: "Validate WeClawBot screen document",
@@ -21,6 +59,43 @@ export default defineToolPlugin({
21
59
  }, { additionalProperties: false }),
22
60
  execute: ({ document, device_context }) => validateScreenDocument(document, device_context),
23
61
  }),
62
+ tool({
63
+ name: "weclawbot_publish_screen_document",
64
+ label: "Publish WeClawBot screen document",
65
+ description: "Validate and publish a pre-rendered mono1 screen document through the paired local MQTT profile. The document must already contain pixels; this tool does not lay out text.",
66
+ parameters: Type.Object({
67
+ document: Type.Any(),
68
+ device_context: Type.Optional(Type.Any()),
69
+ credentials_path: Type.Optional(Type.String()),
70
+ force_replace: Type.Optional(Type.Boolean()),
71
+ }, { additionalProperties: false }),
72
+ execute: async ({ document, device_context, credentials_path, force_replace }) => {
73
+ const outbound = cloneObject(document);
74
+ if (force_replace) {
75
+ outbound.force_replace = true;
76
+ outbound.base_revision = "*";
77
+ }
78
+ const validation = validateScreenDocument(outbound, device_context || {
79
+ agent_transport: { available: true, screen_document_available: true },
80
+ });
81
+ if (!validation.ok) {
82
+ return { ok: false, published: false, errors: validation.errors, validation };
83
+ }
84
+ await publishControl(await requireCredentials(credentials_path), {
85
+ schema: "weclawbot.control.v1",
86
+ id: `screen_${crypto.randomUUID()}`,
87
+ kind: "screen_document",
88
+ document: outbound,
89
+ });
90
+ return {
91
+ ok: true,
92
+ published: true,
93
+ id: outbound.id,
94
+ pages: outbound.pages.length,
95
+ force_replace: outbound.force_replace === true,
96
+ };
97
+ },
98
+ }),
24
99
  tool({
25
100
  name: "weclawbot_validate_activity",
26
101
  label: "Validate WeClawBot activity",
@@ -31,5 +106,75 @@ export default defineToolPlugin({
31
106
  }, { additionalProperties: false }),
32
107
  execute: ({ activity, device_context }) => validateActivity(activity, device_context),
33
108
  }),
109
+ tool({
110
+ name: "weclawbot_publish_activity",
111
+ label: "Publish WeClawBot activity",
112
+ description: "Validate and publish a short-lived thinking or idle activity through the paired local MQTT profile.",
113
+ parameters: Type.Object({
114
+ activity: Type.Any(),
115
+ device_context: Type.Optional(Type.Any()),
116
+ credentials_path: Type.Optional(Type.String()),
117
+ }, { additionalProperties: false }),
118
+ execute: async ({ activity, device_context, credentials_path }) => {
119
+ const validation = validateActivity(activity, device_context || {
120
+ agent_transport: { available: true, activity_available: true },
121
+ });
122
+ if (!validation.ok) {
123
+ return { ok: false, published: false, errors: validation.errors, validation };
124
+ }
125
+ await publishControl(await requireCredentials(credentials_path), {
126
+ schema: "weclawbot.control.v1",
127
+ id: `activity_${crypto.randomUUID()}`,
128
+ kind: "activity",
129
+ activity,
130
+ });
131
+ return {
132
+ ok: true,
133
+ published: true,
134
+ state: activity.state,
135
+ correlation_id: activity.correlation_id,
136
+ };
137
+ },
138
+ }),
34
139
  ],
35
140
  });
141
+
142
+ async function requireCredentials(credentialsPath) {
143
+ const file = expandPath(credentialsPath || DEFAULT_CREDENTIALS_PATH);
144
+ const payload = await readCredentials(file);
145
+ if (!payload) throw new Error(`WeClawBot is not paired. Run: weclawbotctl bind <six-digit-code>`);
146
+ return payload;
147
+ }
148
+
149
+ async function readCredentials(file) {
150
+ try {
151
+ const payload = JSON.parse(await fs.readFile(file, "utf8"));
152
+ return payload && typeof payload === "object" ? payload : null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ function expandPath(value) {
159
+ const raw = String(value || DEFAULT_CREDENTIALS_PATH);
160
+ return raw.startsWith("~/") ? path.join(os.homedir(), raw.slice(2)) : raw;
161
+ }
162
+
163
+ function cloneObject(value) {
164
+ if (!value || typeof value !== "object") throw new Error("document must be an object");
165
+ return JSON.parse(JSON.stringify(value));
166
+ }
167
+
168
+ function maskedMqtt(config, topics) {
169
+ return {
170
+ url: config.url,
171
+ client_id: config.clientId,
172
+ username: config.username,
173
+ password: "********",
174
+ topics: {
175
+ control: config.controlTopic,
176
+ events: topics.events || "",
177
+ status: topics.status || "",
178
+ },
179
+ };
180
+ }
@@ -41,6 +41,9 @@ export function validateScreenDocument(value, suppliedContext) {
41
41
  if (document.kind !== "replace") errors.push("kind must be replace");
42
42
  if (!string(document.id)) errors.push("id is required");
43
43
  if (typeof document.base_revision !== "string") errors.push("base_revision must be a string (empty for the first document)");
44
+ if ("force_replace" in document && typeof document.force_replace !== "boolean") {
45
+ errors.push("force_replace must be a boolean when present");
46
+ }
44
47
  if (!isFutureIso(document.expires_at)) errors.push("expires_at must be a future UTC RFC3339 timestamp");
45
48
  if (!Array.isArray(document.pages) || document.pages.length < 1 || document.pages.length > viewport.max_pages) {
46
49
  errors.push(`pages must contain 1..${viewport.max_pages} items`);
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "id": "weclawbot",
3
3
  "name": "WeClawBot",
4
- "description": "Lets OpenClaw curate WeChat messages and validate live screen documents for a paired WeClawBot screen.",
4
+ "description": "Lets OpenClaw pair with and push content to a WeClawBot screen over MQTT.",
5
+ "contracts": {
6
+ "tools": [
7
+ "weclawbot_status",
8
+ "weclawbot_validate_screen_document",
9
+ "weclawbot_publish_screen_document",
10
+ "weclawbot_validate_activity",
11
+ "weclawbot_publish_activity"
12
+ ]
13
+ },
5
14
  "skills": [
6
15
  "./skills"
7
16
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openbrt/weclawbotctl",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "WeClawBot pairing and screen-control CLI for local AI agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,7 @@
35
35
  ],
36
36
  "bin": {
37
37
  "weclawbot-byoa-bind": "bin/weclawbot-byoa-bind.mjs",
38
+ "weclawbot-openclaw-bridge": "bin/weclawbot-openclaw-bridge.mjs",
38
39
  "weclawbotctl": "bin/weclawbotctl.mjs"
39
40
  },
40
41
  "exports": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: weclawbot-curator
3
- description: Curate WeChat messages into thoughtful monochrome WeClawBot screen decisions when a WeClawBot curator event is supplied.
3
+ description: Curate WeClawBot screen events and guide direct MQTT pushes for paired physical screens.
4
4
  metadata: { "openclaw": { "always": true } }
5
5
  ---
6
6
 
@@ -35,11 +35,32 @@ away the relationship.
35
35
 
36
36
  ## Direct agent control
37
37
 
38
- `wechat_transport` describes the existing iLink long-poll event path. It is
39
- inbound only. Never claim it lets an agent push a periodic or scheduled card.
38
+ `wechat_transport` describes the official-mode iLink long-poll event path. It
39
+ is inbound only. In BYOA mode it may be advertised as `mode=disabled` and
40
+ `direction=ignored`; do not use firmware WeChat as an ingress or reply channel.
41
+ Never claim it lets an agent push a periodic or scheduled card.
42
+
43
+ `agent_transport.available` inside an inbound curator event is the live
44
+ firmware contract for that event. It is not the authority for a locally paired
45
+ `weclawbotctl` profile. If a user directly asks to send text, a status card, a
46
+ timer, or other agent-originated content to the screen, first run
47
+ `weclawbotctl status` or `weclawbotctl doctor --online`, or call
48
+ `weclawbot_status` with `online:true`. If the profile is paired and online,
49
+ render the content into a `weclawbot.screen_document.v1` pixel document and
50
+ publish it with:
51
+
52
+ ```bash
53
+ weclawbotctl screen /path/to/screen-document.json
54
+ ```
55
+
56
+ or call `weclawbot_publish_screen_document` with the same document. Do not use
57
+ OpenClaw Canvas for requests that mention WeClawBot, the physical screen, or
58
+ “屏上”; Canvas is an OpenClaw UI surface, not the ESP32 e-paper display. Do
59
+ not send raw text to firmware. The agent owns text layout, font choice, image
60
+ rasterization, and page splitting; the device consumes pixels.
40
61
 
41
- When `agent_transport.available` is `false`, return the normal WeChat decision
42
- shape below. Do not claim that a direct update reached the device.
62
+ Only return the normal WeChat decision shape below when processing an explicit
63
+ `WECLAWBOT_CURATOR_EVENT` envelope.
43
64
 
44
65
  When a paired device advertises `agent_transport.available=true`, an external
45
66
  agent may publish a complete `weclawbot.screen_document.v1` over the live
@@ -56,8 +77,8 @@ model API key.
56
77
  Treat “发到屏上” as one capability regardless of its origin. A WeChat event
57
78
  has a reply target: return `user_reply` only when it genuinely helps the
58
79
  sender. A timer, automation, or direct Agent request has no WeChat reply
59
- target: publish the same screen document and use the device `applied` or
60
- `rejected` event as its result.
80
+ target: publish a validated screen document and use the device `applied` or
81
+ `rejected` event as the result.
61
82
 
62
83
  ## Thinking activity
63
84
 
@@ -6,7 +6,7 @@ Wants=network-online.target
6
6
  [Service]
7
7
  Type=simple
8
8
  EnvironmentFile=%h/.config/weclawbot/openclaw-curator.env
9
- ExecStart=/usr/bin/node %h/.openclaw/extensions/weclawbot/bin/weclawbot-openclaw-bridge.mjs
9
+ ExecStart=%h/.npm-global/bin/weclawbot-openclaw-bridge
10
10
  Restart=always
11
11
  RestartSec=3
12
12
  TimeoutStopSec=30
@@ -17,3 +17,26 @@ details in `note.body`; do not replace `note` with `content`, `note_name`, or a
17
17
  wrapper object. For a list, preserve existing categories and group new items by
18
18
  meaning. For greetings, acknowledgements, or content with no future value, use
19
19
  `ignore` or `reply_only`.
20
+
21
+ ## Direct Screen Requests
22
+
23
+ When the user asks to put something on the WeClawBot screen, treat WeClawBot
24
+ as the physical ESP32 e-paper display, not OpenClaw Canvas. First check the
25
+ local pairing:
26
+
27
+ ```bash
28
+ weclawbotctl status
29
+ weclawbotctl doctor --online
30
+ ```
31
+
32
+ If it is paired and online, render the requested text, status, image, or chart
33
+ into a `weclawbot.screen_document.v1` with 1-bit pages for the firmware
34
+ content viewport, then publish it:
35
+
36
+ ```bash
37
+ weclawbotctl screen /path/to/screen-document.json
38
+ ```
39
+
40
+ The firmware receives pixels. Do not send raw text to firmware, and do not
41
+ answer that direct delivery is unavailable before checking the local
42
+ `weclawbotctl` profile.