openclaw-elys 1.3.2 → 1.4.0

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
@@ -1,8 +1,8 @@
1
1
  # openclaw-elys
2
2
 
3
- OpenClaw channel plugin for [Elys App](https://elys.ai) — connects your local OpenClaw agent to the Elys mobile app via MQTT gateway.
3
+ OpenClaw channel plugin for [Elys App](https://elys.ai) — connects your local OpenClaw agent to the Elys mobile app.
4
4
 
5
- Elys App 的 OpenClaw 频道插件 — 通过 MQTT 网关将本地 OpenClaw 智能体连接到 Elys 手机应用。
5
+ Elys App 的 OpenClaw 频道插件 — 将本地 OpenClaw 智能体连接到 Elys 手机应用。
6
6
 
7
7
  ## Install / 安装
8
8
 
@@ -27,24 +27,15 @@ Example / 示例:
27
27
  npx openclaw-elys setup https://your-gateway.com reg_abc123def456
28
28
  # Device registered successfully!
29
29
  # Device ID: d_xxxxxxxxxxxx
30
- # MQTT Broker: mqtts://xxx.emqxcloud.cn:8883
31
30
  # Credentials saved to ~/.elys/config.json
31
+ # OpenClaw config updated: channels.elys.gatewayUrl = https://your-gateway.com
32
+ #
33
+ # Restart OpenClaw gateway to activate: openclaw gateway restart
32
34
  ```
33
35
 
34
- Then add the channel config to your OpenClaw config (`~/.openclaw/openclaw.json`):
36
+ The setup command automatically configures `channels.elys` in `~/.openclaw/openclaw.json`.
35
37
 
36
- 然后在 OpenClaw 配置文件中添加频道配置:
37
-
38
- ```json
39
- {
40
- "channels": {
41
- "elys": {
42
- "enabled": true,
43
- "gatewayUrl": "https://your-gateway.com"
44
- }
45
- }
46
- }
47
- ```
38
+ setup 命令会自动配置 `~/.openclaw/openclaw.json` 中的 `channels.elys`。
48
39
 
49
40
  Restart the OpenClaw gateway to activate:
50
41
 
@@ -78,18 +69,6 @@ Then remove the plugin:
78
69
  openclaw plugins uninstall openclaw-elys
79
70
  ```
80
71
 
81
- ## How it works / 工作原理
82
-
83
- ```
84
- Elys App -> Backend -> Gateway (HTTP) -> EMQX (MQTT) -> OpenClaw Plugin
85
- |
86
- OpenClaw Plugin -> EMQX (MQTT) -> Gateway -> Backend -> Elys App
87
- ```
88
-
89
- - **Downlink** / 下行 (App -> Device): Backend calls Gateway HTTP API, Gateway publishes to MQTT
90
- - **Uplink** / 上行 (Device -> App): Plugin publishes to MQTT, Gateway receives and returns to Backend
91
- - **Streaming** / 流式: Plugin sends AI reply chunks in real-time via MQTT stream messages
92
-
93
72
  ## License
94
73
 
95
74
  MIT
package/dist/src/cli.js CHANGED
@@ -1,6 +1,52 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
2
5
  import { registerDevice } from "./register.js";
3
6
  import { loadCredentials, loadGatewayUrl, deleteCredentials, } from "./config.js";
7
+ const OPENCLAW_CONFIG = path.join(os.homedir(), ".openclaw", "openclaw.json");
8
+ /**
9
+ * Auto-configure channels.elys in ~/.openclaw/openclaw.json
10
+ */
11
+ function configureOpenClawChannel(gatewayUrl) {
12
+ try {
13
+ if (!fs.existsSync(OPENCLAW_CONFIG))
14
+ return false;
15
+ const raw = fs.readFileSync(OPENCLAW_CONFIG, "utf-8");
16
+ const cfg = JSON.parse(raw);
17
+ if (!cfg.channels)
18
+ cfg.channels = {};
19
+ cfg.channels.elys = {
20
+ enabled: true,
21
+ gatewayUrl,
22
+ };
23
+ fs.writeFileSync(OPENCLAW_CONFIG, JSON.stringify(cfg, null, 2), "utf-8");
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Remove channels.elys from ~/.openclaw/openclaw.json
32
+ */
33
+ function removeOpenClawChannel() {
34
+ try {
35
+ if (!fs.existsSync(OPENCLAW_CONFIG))
36
+ return false;
37
+ const raw = fs.readFileSync(OPENCLAW_CONFIG, "utf-8");
38
+ const cfg = JSON.parse(raw);
39
+ if (cfg.channels?.elys) {
40
+ delete cfg.channels.elys;
41
+ fs.writeFileSync(OPENCLAW_CONFIG, JSON.stringify(cfg, null, 2), "utf-8");
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
4
50
  const args = process.argv.slice(2);
5
51
  const command = args[0];
6
52
  if (command === "setup") {
@@ -16,8 +62,19 @@ if (command === "setup") {
16
62
  .then((creds) => {
17
63
  console.log("Device registered successfully!");
18
64
  console.log(` Device ID: ${creds.deviceId}`);
19
- console.log(` MQTT Broker: ${creds.mqttBroker}`);
20
65
  console.log(` Credentials saved to ~/.elys/config.json`);
66
+ // Auto-configure OpenClaw channel
67
+ if (configureOpenClawChannel(gatewayUrl)) {
68
+ console.log(` OpenClaw config updated: channels.elys.gatewayUrl = ${gatewayUrl}`);
69
+ console.log("");
70
+ console.log("Restart OpenClaw gateway to activate: openclaw gateway restart");
71
+ }
72
+ else {
73
+ console.log("");
74
+ console.log("Add to ~/.openclaw/openclaw.json:");
75
+ console.log(` "channels": { "elys": { "enabled": true, "gatewayUrl": "${gatewayUrl}" } }`);
76
+ console.log("Then restart: openclaw gateway restart");
77
+ }
21
78
  })
22
79
  .catch((err) => {
23
80
  console.error("Registration failed:", err.message);
@@ -35,6 +92,7 @@ else if (command === "uninstall") {
35
92
  if (!gatewayUrl) {
36
93
  console.log("No gateway URL found. Cleaning up local credentials only.");
37
94
  deleteCredentials();
95
+ removeOpenClawChannel();
38
96
  console.log("Local credentials removed.");
39
97
  process.exit(0);
40
98
  }
@@ -59,6 +117,9 @@ else if (command === "uninstall") {
59
117
  })
60
118
  .finally(() => {
61
119
  deleteCredentials();
120
+ if (removeOpenClawChannel()) {
121
+ console.log("OpenClaw channel config removed.");
122
+ }
62
123
  console.log("Local credentials removed.");
63
124
  });
64
125
  }
@@ -67,7 +128,6 @@ else if (command === "status") {
67
128
  if (creds) {
68
129
  console.log("Elys device credentials:");
69
130
  console.log(` Device ID: ${creds.deviceId}`);
70
- console.log(` MQTT Broker: ${creds.mqttBroker}`);
71
131
  console.log(` Config file: ~/.elys/config.json`);
72
132
  }
73
133
  else {
@@ -5,7 +5,7 @@ export interface MonitorElysOpts {
5
5
  accountId?: string;
6
6
  channelRuntime?: ChannelRuntimeReply;
7
7
  }
8
- /** Subset of OpenClaw's channelRuntime.reply API that we use */
8
+ /** Subset of OpenClaw's PluginRuntime["channel"] that we use */
9
9
  type ReplyPayload = {
10
10
  text?: string;
11
11
  mediaUrl?: string;
@@ -18,9 +18,12 @@ type ChannelRuntimeReply = {
18
18
  ctx: Record<string, unknown>;
19
19
  cfg: Record<string, unknown>;
20
20
  dispatcherOptions: {
21
- deliver: (payload: ReplyPayload) => Promise<void>;
22
- deliverBlock?: (payload: ReplyPayload) => Promise<void>;
23
- deliverToolResult?: (payload: ReplyPayload) => Promise<void>;
21
+ deliver: (payload: ReplyPayload, info: {
22
+ kind: "tool" | "block" | "final";
23
+ }) => Promise<void>;
24
+ onError?: (err: unknown, info: {
25
+ kind: string;
26
+ }) => void;
24
27
  };
25
28
  replyOptions?: Record<string, unknown>;
26
29
  }) => Promise<{
@@ -30,6 +30,7 @@ export async function monitorElysProvider(opts) {
30
30
  const channelRuntime = opts.channelRuntime;
31
31
  const dispatchReply = channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
32
32
  const finalizeCtx = channelRuntime?.reply?.finalizeInboundContext;
33
+ log(`[elys] channelRuntime available: ${!!channelRuntime}, dispatchReply: ${!!dispatchReply}, finalizeCtx: ${!!finalizeCtx}`);
33
34
  const commandHandler = async (cmd) => {
34
35
  log(`[elys] executing command: ${cmd.command}`, cmd.args);
35
36
  // If we have the full OpenClaw channelRuntime, use the standard dispatch path
@@ -51,37 +52,36 @@ export async function monitorElysProvider(opts) {
51
52
  AccountId: "default",
52
53
  SessionKey: `elys:${credentials.deviceId}`,
53
54
  CommandAuthorized: true,
55
+ OriginatingChannel: "elys",
56
+ OriginatingTo: credentials.deviceId,
54
57
  });
55
58
  await dispatchReply({
56
59
  ctx: inboundCtx,
57
60
  cfg: opts.config,
58
61
  dispatcherOptions: {
59
- // Block (streaming) delivery send each chunk via MQTT
60
- deliverBlock: async (payload) => {
62
+ // Single deliver callback with kind discriminator (matches OpenClaw API)
63
+ deliver: async (payload, info) => {
61
64
  if (payload.text) {
62
65
  fullText += payload.text;
63
66
  seq++;
64
- mqttClient.publishStreamChunk(cmd.id, payload.text, seq, false);
65
- log(`[elys] stream chunk #${seq}: ${payload.text.slice(0, 80)}...`);
67
+ const done = info.kind === "final";
68
+ mqttClient.publishStreamChunk(cmd.id, payload.text, seq, done);
69
+ if (info.kind === "block") {
70
+ log(`[elys] stream chunk #${seq}: ${payload.text.slice(0, 80)}...`);
71
+ }
72
+ else if (info.kind === "final") {
73
+ log(`[elys] final reply delivered`);
74
+ }
66
75
  }
67
- },
68
- // Final delivery
69
- deliver: async (payload) => {
70
- if (payload.text) {
71
- fullText += payload.text;
72
- }
73
- // Send final stream marker
74
- seq++;
75
- mqttClient.publishStreamChunk(cmd.id, payload.text ?? "", seq, true);
76
- log(`[elys] final reply delivered`);
77
- },
78
- // Tool result delivery (optional)
79
- deliverToolResult: async (payload) => {
80
- if (payload.text) {
76
+ else if (info.kind === "final") {
81
77
  seq++;
82
- mqttClient.publishStreamChunk(cmd.id, payload.text, seq, false);
78
+ mqttClient.publishStreamChunk(cmd.id, "", seq, true);
79
+ log(`[elys] final reply delivered (empty)`);
83
80
  }
84
81
  },
82
+ onError: (err, info) => {
83
+ log(`[elys] dispatch error (${info.kind}):`, err);
84
+ },
85
85
  },
86
86
  });
87
87
  return {
@@ -93,6 +93,7 @@ export async function monitorElysProvider(opts) {
93
93
  };
94
94
  }
95
95
  catch (err) {
96
+ log(`[elys] dispatch error:`, err);
96
97
  return {
97
98
  id: cmd.id,
98
99
  type: "result",
@@ -103,6 +104,7 @@ export async function monitorElysProvider(opts) {
103
104
  }
104
105
  }
105
106
  // Fallback: echo the command back (no channelRuntime available)
107
+ log(`[elys] no channelRuntime — using fallback echo handler`);
106
108
  return {
107
109
  id: cmd.id,
108
110
  type: "result",
@@ -121,6 +123,9 @@ export async function monitorElysProvider(opts) {
121
123
  }
122
124
  }
123
125
  function formatCommandAsText(cmd) {
126
+ if (cmd.args?.text && typeof cmd.args.text === "string") {
127
+ return cmd.args.text;
128
+ }
124
129
  const parts = [cmd.command];
125
130
  if (cmd.args) {
126
131
  for (const [k, v] of Object.entries(cmd.args)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-elys",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "OpenClaw Elys channel plugin — connects to Elys App",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",