@openbrt/weclawbotctl 0.1.2 → 0.1.4

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
@@ -5,12 +5,67 @@ any local Agent with a WeClawBot screen. It works from OpenClaw, Hermes,
5
5
  Codex, Claude Code, Gemini CLI, OpenCode, or a shell script through the same
6
6
  local MQTT profile.
7
7
 
8
+ ## Tell Your Agent
9
+
10
+ Give your coding agent one sentence:
11
+
12
+ ```text
13
+ Install @openbrt/weclawbotctl and connect to my WeClawBot screen with pairing code 123456.
14
+ ```
15
+
16
+ Replace `123456` with the six-digit code shown on the screen. The agent should
17
+ then install this npm package, bind the screen, run the online doctor check,
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.
21
+
22
+ ## Agent Instructions
23
+
24
+ If a user asks you to install, enable, connect, or pair WeClawBot and gives a
25
+ six-digit pairing code, do this autonomously:
26
+
27
+ ```bash
28
+ npm install -g @openbrt/weclawbotctl
29
+ weclawbotctl bind 123456 --name "$AGENT_NAME"
30
+ weclawbotctl doctor --online
31
+ ```
32
+
33
+ Use your product name for `$AGENT_NAME`, such as `codex`, `claude-code`,
34
+ `gemini-cli`, `opencode`, `hermes`, or `openclaw`. If global npm installation
35
+ is not available, use the one-shot form:
36
+
37
+ ```bash
38
+ npm exec --package @openbrt/weclawbotctl -- weclawbotctl bind 123456 --name "$AGENT_NAME"
39
+ npm exec --package @openbrt/weclawbotctl -- weclawbotctl doctor --online
40
+ ```
41
+
42
+ Do not ask the user for an MQTT URL, username, password, Wi-Fi password,
43
+ WeChat credential, port, or webhook. The pairing code is enough. The local MQTT
44
+ credential is stored at `~/.config/weclawbot/agent-mqtt.json` with mode `0600`.
45
+
46
+ When doing visible work, publish a temporary activity:
47
+
48
+ ```bash
49
+ task_id="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)"
50
+ weclawbotctl thinking --id "$task_id" --ttl 45
51
+ # Do the work.
52
+ weclawbotctl idle --id "$task_id"
53
+ ```
54
+
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.
58
+
59
+ ```bash
60
+ weclawbotctl screen /path/to/screen-document.json
61
+ ```
62
+
8
63
  The package also includes an OpenClaw integration: the `weclawbot-curator`
9
- skill, a local
10
- `weclawbot_validate_screen_document` tool, a `weclawbot_validate_activity`
11
- tool, and a small outbound bridge
12
- service. The bridge polls `weclawbot.link`; no public HTTP endpoint, port
13
- 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.
14
69
 
15
70
  ## Install from npm
16
71
 
@@ -87,10 +142,19 @@ systemctl --user enable --now weclawbot-openclaw-curator
87
142
 
88
143
  The normal bridge is message-triggered by WeChat. Scheduled cards and other
89
144
  agent-originated updates use a separately paired MQTT/TLS control channel;
90
- they are never placed in a gateway mailbox. The plugin already exposes the
91
- validator that a user agent calls before publishing a 1-bit screen document.
92
- It reports `direct_delivery_ready:false` until the physical firmware has been
93
- 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`.
94
158
 
95
159
  The pairing UX deliberately requires no user-supplied Agent endpoint: choose
96
160
  **自定义智能体** in the device configurator, then enter the six-digit code shown
@@ -141,15 +205,24 @@ weclawbotctl thinking --id "$task_id" --ttl 45
141
205
  weclawbotctl idle --id "$task_id"
142
206
  ```
143
207
 
144
- To put a pre-rendered monochrome document on screen, pass its JSON file to the
145
- same scoped MQTT credential. The document must use the live revision in the
146
- last `device_context` (an empty revision is valid for the first document), one
147
- 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:
148
214
 
149
215
  ```bash
150
216
  weclawbotctl screen /path/to/screen-document.json
151
217
  ```
152
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
+
153
226
  These commands use MQTT/TLS directly, publish QoS 1 without retain, and
154
227
  never create an offline command queue. See
155
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,7 @@
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
5
  "skills": [
6
6
  "./skills"
7
7
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openbrt/weclawbotctl",
3
- "version": "0.1.2",
4
- "description": "WeClawBot MQTT pairing CLI and direct-control tools for local AI agents.",
3
+ "version": "0.1.4",
4
+ "description": "WeClawBot pairing and screen-control CLI for local AI agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -10,7 +10,11 @@
10
10
  "esp32",
11
11
  "e-paper",
12
12
  "openclaw",
13
- "agent"
13
+ "agent",
14
+ "codex",
15
+ "claude-code",
16
+ "gemini-cli",
17
+ "hermes"
14
18
  ],
15
19
  "engines": {
16
20
  "node": ">=20"
@@ -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
 
@@ -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.