@newbase-clawchat/openclaw-clawchat 2026.4.30 → 2026.5.4-2

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
@@ -10,7 +10,7 @@ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Pro
10
10
  - Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
11
11
  - Typing indicators and filtered forwarding for thinking / tool-call content
12
12
  - Media fragments (image / file / audio / video) in either direction
13
- - Invite-code onboarding via `clawchat_activate` or channel login, plus always-registered `clawchat_*` account/media tools
13
+ - Invite-code onboarding via `clawchat_activate` or `/clawchat-login`, plus always-registered `clawchat_*` account/media tools
14
14
 
15
15
  ## Install
16
16
 
@@ -19,9 +19,10 @@ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Pro
19
19
  npm i @newbase-clawchat/openclaw-clawchat
20
20
  ```
21
21
 
22
- Requires `openclaw >= 2026.4.29` as a peer host.
22
+ Requires `openclaw >= 2026.5.4` as a peer host.
23
23
 
24
- For the OpenClaw plugin install/update flow, see [`INSTALL.md`](./INSTALL.md).
24
+ For the OpenClaw plugin install/update flow, use the R2-hosted tarball install
25
+ command documented in [`INSTALL.md`](./INSTALL.md).
25
26
 
26
27
  Example LLM prompt:
27
28
 
@@ -38,15 +39,31 @@ activate ClawChat with invite code A1B2C3
38
39
  ```
39
40
 
40
41
  If the plugin is already loaded by the Gateway, the activation skill calls the
41
- `clawchat_activate` tool and OpenClaw hot-reloads the updated `channels.*`
42
- credentials. If the tool is not available yet, use channel login as the CLI
43
- fallback:
42
+ `clawchat_activate` tool and persists credentials. If you are activating from
43
+ Telegram or another chat platform and the agent does not call the tool, send the
44
+ runtime slash command in that chat:
45
+
46
+ ```text
47
+ /clawchat-login A1B2C3
48
+ ```
49
+
50
+ The slash command is provided by the loaded plugin. It is not a shell command,
51
+ so `openclaw clawchat-login` is expected to fail.
52
+
53
+ On OpenClaw hosts where the CLI channel catalog includes `openclaw-clawchat`,
54
+ terminal activation can also use:
44
55
 
45
56
  ```bash
46
- openclaw channels login --channel openclaw-clawchat
57
+ CLAWCHAT_INVITE_CODE="A1B2C3"
58
+ openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
47
59
  openclaw channels status --probe
48
60
  ```
49
61
 
62
+ OpenClaw 2026.5.5 can load an npm-installed third-party channel while still
63
+ omitting it from the `channels add` CLI catalog. If `channels add` fails with
64
+ `Unknown channel: openclaw-clawchat`, use `clawchat_activate` or
65
+ `/clawchat-login A1B2C3` after a real Gateway restart.
66
+
50
67
  Restart the Gateway after installing/updating the plugin, when config reload is
51
68
  disabled, or when the channel probe does not become healthy:
52
69
 
@@ -55,17 +72,20 @@ openclaw gateway restart
55
72
  ```
56
73
 
57
74
  If you run the gateway manually instead of as a service and it is not already
58
- running, start it after login:
75
+ running, start it after activation:
59
76
 
60
77
  ```bash
61
78
  openclaw gateway run
62
79
  ```
63
80
 
64
- The invite code is not a token; token fields are written only after login. The
65
- plugin registers `clawchat_activate` and the six account/media tools at plugin
66
- load time so they stay visible before activation. Before login, account/media
81
+ The `--token` value above is the ClawChat invite code for OpenClaw's generic
82
+ `channels add` CLI surface on hosts that expose this plugin in the channel
83
+ catalog; persisted token fields are written only after the invite code exchange
84
+ succeeds. The plugin registers `clawchat_activate` and the six account/media tools at plugin
85
+ load time so they stay visible before activation. Before activation, account/media
67
86
  tools return a config error instead of disappearing; after activation/login, the
68
- channel is enabled and the same tools read the hot-reloaded token/userId.
87
+ channel is enabled and the same tools read the persisted token/userId after the
88
+ Gateway restarts.
69
89
 
70
90
  After activation/login, the channel section is enabled and has credentials:
71
91
 
@@ -157,6 +177,38 @@ npm run dev:openclaw-source
157
177
  This checkout is local-only. It is ignored by git and is not required to run the
158
178
  plugin tests or publish the package.
159
179
 
180
+ ## R2 package scripts
181
+
182
+ Create and upload the OpenClaw plugin tarball to the R2 `openclaw/` prefix:
183
+
184
+ ```bash
185
+ ./package_openclaw_plugin.sh
186
+ ```
187
+
188
+ The script runs `npm pack`, uploads the generated `.tgz` to the configured R2
189
+ bucket, and prints the public URL. R2 credentials are read from `.env.r2`, which
190
+ is ignored by git. Use `--no-upload` to build the tarball without uploading it.
191
+
192
+ ```bash
193
+ AWS_ACCESS_KEY_ID=...
194
+ AWS_SECRET_ACCESS_KEY=...
195
+ AWS_DEFAULT_REGION=auto
196
+ R2_ENDPOINT=https://...
197
+ R2_BUCKET=...
198
+ ```
199
+
200
+ Install the R2-hosted tarball on a device or container with OpenClaw available:
201
+
202
+ ```bash
203
+ ./install_openclaw.sh
204
+ ```
205
+
206
+ To install a specific uploaded tarball, pass its URL explicitly:
207
+
208
+ ```bash
209
+ ./install_openclaw.sh https://dddddddddddddtest.clawling.chat/openclaw/newbase-clawchat-openclaw-clawchat-2026.5.4-2.tgz
210
+ ```
211
+
160
212
  ## License
161
213
 
162
214
  See the repository root.
package/dist/index.js CHANGED
@@ -1,27 +1,18 @@
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
1
2
  import { openclawClawlingPlugin } from "./src/channel.js";
2
3
  import { registerOpenclawClawlingCommands } from "./src/commands.js";
4
+ import { openclawClawlingConfigSchema } from "./src/config.js";
3
5
  import { setOpenclawClawlingRuntime } from "./src/runtime.js";
4
6
  import { registerOpenclawClawlingTools } from "./src/tools.js";
5
- import { openclawClawlingConfigSchema } from "./src/config.js";
6
- export default {
7
+ export default defineChannelPluginEntry({
7
8
  id: "openclaw-clawchat",
8
9
  name: "Clawling Chat",
9
10
  description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
10
- configSchema: openclawClawlingConfigSchema,
11
- register(api) {
12
- setOpenclawClawlingRuntime(api.runtime);
13
- api.registerChannel({ plugin: openclawClawlingPlugin });
11
+ plugin: openclawClawlingPlugin,
12
+ configSchema: { schema: openclawClawlingConfigSchema },
13
+ setRuntime: setOpenclawClawlingRuntime,
14
+ registerFull(api) {
14
15
  registerOpenclawClawlingCommands(api);
15
16
  registerOpenclawClawlingTools(api);
16
- }
17
- };
18
- // export default defineChannelPluginEntry({
19
- // id: "openclaw-clawchat",
20
- // name: "Clawling Chat",
21
- // description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
22
- // plugin: openclawClawlingPlugin,
23
- // setRuntime: setOpenclawClawlingRuntime,
24
- // registerFull(api) {
25
- // registerOpenclawClawlingTools(api);
26
- // },
27
- // });
17
+ },
18
+ });
@@ -0,0 +1,3 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { openclawClawlingSetupPlugin } from "./src/channel.setup.js";
3
+ export default defineSetupPluginEntry(openclawClawlingSetupPlugin);
@@ -1,145 +1,15 @@
1
- import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
2
- import { createChatChannelPlugin, } from "openclaw/plugin-sdk/core";
1
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
3
2
  import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
4
- import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
5
- import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
6
- import { CHANNEL_ID, listOpenclawClawlingAccountIds, mergeOpenclawClawchatToolAllow, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
3
+ import { CHANNEL_ID, resolveOpenclawClawlingAccount, } from "./config.js";
7
4
  import { openclawClawlingOutbound } from "./outbound.js";
8
5
  import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.js";
9
- const configAdapter = createTopLevelChannelConfigAdapter({
10
- sectionKey: CHANNEL_ID,
11
- resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
12
- listAccountIds: () => listOpenclawClawlingAccountIds(),
13
- defaultAccountId: () => DEFAULT_ACCOUNT_ID,
14
- deleteMode: "clear-fields",
15
- clearBaseFields: [
16
- "websocketUrl",
17
- "baseUrl",
18
- "token",
19
- "userId",
20
- "replyMode",
21
- "forwardThinking",
22
- "forwardToolCalls",
23
- "richInteractions",
24
- "enabled",
25
- ],
26
- resolveAllowFrom: (account) => account.allowFrom,
27
- formatAllowFrom: () => [],
28
- });
29
- /**
30
- * Invite-code setup adapter used by OpenClaw setup surfaces that already have
31
- * a concrete plugin instance. This plugin does not advertise catalog-driven
32
- * one-shot setup metadata because current hosts do not discover channels from
33
- * `plugins.load.paths`.
34
- *
35
- * Setup takes exactly ONE input: `code` (an invite code). URL + token +
36
- * userId come from the login flow which is triggered automatically in
37
- * `afterAccountConfigWritten`.
38
- *
39
- * `applyAccountConfig` itself only marks the section `enabled: true`;
40
- * credentials are written by `runOpenclawClawlingLogin` via the runtime config
41
- * mutator after the `/v1/agents/connect` response lands.
42
- */
43
- const setupAdapter = {
44
- resolveAccountId: () => DEFAULT_ACCOUNT_ID,
45
- validateInput: ({ input }) => {
46
- if (!input.code?.trim()) {
47
- return "ClawChat invite code is required.";
48
- }
49
- return null;
50
- },
51
- applyAccountConfig: ({ cfg, }) => {
52
- // Base config: just enable the channel. Credentials arrive via
53
- // `afterAccountConfigWritten` → `runOpenclawClawlingLogin`.
54
- const channels = (cfg.channels ?? {});
55
- const current = (channels[CHANNEL_ID] ?? {});
56
- return mergeOpenclawClawchatToolAllow({
57
- ...cfg,
58
- channels: {
59
- ...channels,
60
- [CHANNEL_ID]: { ...current, enabled: true },
61
- },
62
- });
63
- },
64
- afterAccountConfigWritten: async ({ cfg, input, runtime, }) => {
65
- const code = input.code?.trim();
66
- if (!code)
67
- return;
68
- // Lazy-import the login runtime to keep @clack/prompts / readline /
69
- // config-runtime off the plugin's cold-start path. `readInviteCode`
70
- // feeds the fixed code so the stdin prompt is skipped entirely.
71
- const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
72
- await runOpenclawClawlingLogin({
73
- cfg,
74
- accountId: null,
75
- runtime: { log: (message) => runtime.log(message) },
76
- readInviteCode: async () => code,
77
- mutateConfigFile: getOpenclawClawlingRuntime().config.mutateConfigFile,
78
- });
79
- },
80
- };
6
+ import { openclawClawlingSetupPlugin } from "./channel.setup.js";
81
7
  export const openclawClawlingPlugin = createChatChannelPlugin({
82
8
  base: {
83
- id: CHANNEL_ID,
84
- meta: {
85
- id: CHANNEL_ID,
86
- label: "Clawling Chat",
87
- selectionLabel: "Clawling Chat",
88
- docsPath: "/channels/openclaw-clawchat",
89
- docsLabel: "openclaw-clawchat",
90
- blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
91
- order: 110,
92
- },
93
- capabilities: {
94
- chatTypes: ["direct", "group"],
95
- media: true,
96
- reactions: false,
97
- threads: false,
98
- polls: false,
99
- blockStreaming: true,
100
- },
101
- reload: {
102
- configPrefixes: [`channels.${CHANNEL_ID}`],
103
- },
104
- configSchema: {
105
- schema: openclawClawlingConfigSchema,
106
- },
107
- config: {
108
- ...configAdapter,
109
- isConfigured: (account) => account.configured,
110
- describeAccount: (account) => ({
111
- accountId: account.accountId,
112
- name: account.name,
113
- enabled: account.enabled,
114
- configured: account.configured,
115
- }),
116
- },
9
+ ...openclawClawlingSetupPlugin,
117
10
  directory: createEmptyChannelDirectoryAdapter(),
118
- setup: setupAdapter,
119
- status: createComputedAccountStatusAdapter({
120
- defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
121
- connected: false,
122
- lastInboundAt: null,
123
- lastOutboundAt: null,
124
- }),
125
- resolveAccountSnapshot: ({ account }) => ({
126
- accountId: account.accountId,
127
- name: account.name,
128
- enabled: account.enabled,
129
- configured: account.configured,
130
- extra: {
131
- websocketUrl: account.websocketUrl || null,
132
- baseUrl: account.baseUrl || null,
133
- userId: account.userId || null,
134
- },
135
- }),
136
- }),
137
11
  auth: {
138
12
  login: async ({ cfg, accountId, runtime }) => {
139
- // Lazy-load login.runtime: it pulls in @clack/prompts and other
140
- // heavy modules that have no business loading on every plugin
141
- // boot. Only the rare `openclaw channels login --channel
142
- // openclaw-clawchat` invocation pays the import cost.
143
13
  const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
144
14
  await runOpenclawClawlingLogin({
145
15
  cfg,
@@ -152,17 +22,24 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
152
22
  gateway: {
153
23
  startAccount: async (ctx) => {
154
24
  const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
25
+ ctx.log?.info?.(`[${account.accountId}] openclaw-clawchat lifecycle startAccount invoked configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
155
26
  if (!account.configured) {
27
+ ctx.log?.error?.(`[${account.accountId}] openclaw-clawchat lifecycle startAccount refused: websocketUrl/token/userId are required`);
156
28
  throw new Error("Clawling Chat websocketUrl/token/userId are required");
157
29
  }
158
- return await startOpenclawClawlingGateway({
159
- cfg: ctx.cfg,
160
- account,
161
- abortSignal: ctx.abortSignal,
162
- setStatus: ctx.setStatus,
163
- getStatus: ctx.getStatus,
164
- log: ctx.log,
165
- });
30
+ try {
31
+ await startOpenclawClawlingGateway({
32
+ cfg: ctx.cfg,
33
+ account,
34
+ abortSignal: ctx.abortSignal,
35
+ setStatus: ctx.setStatus,
36
+ getStatus: ctx.getStatus,
37
+ log: ctx.log,
38
+ });
39
+ }
40
+ finally {
41
+ ctx.log?.info?.(`[${account.accountId}] openclaw-clawchat lifecycle startAccount completed/stopped`);
42
+ }
166
43
  },
167
44
  },
168
45
  agentPrompt: {
@@ -176,6 +53,7 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
176
53
  ],
177
54
  },
178
55
  messaging: {
56
+ targetPrefixes: ["cc", "clawchat", CHANNEL_ID],
179
57
  normalizeTarget: (target) => target
180
58
  .trim()
181
59
  .replace(/^openclaw-clawchat:/i, "")
@@ -0,0 +1,133 @@
1
+ import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
2
+ import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
3
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
4
+ import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
5
+ import { CHANNEL_ID, listOpenclawClawlingAccountIds, mergeOpenclawClawchatToolAllow, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
6
+ const configAdapter = createTopLevelChannelConfigAdapter({
7
+ sectionKey: CHANNEL_ID,
8
+ resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
9
+ listAccountIds: () => listOpenclawClawlingAccountIds(),
10
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
11
+ deleteMode: "clear-fields",
12
+ clearBaseFields: [
13
+ "websocketUrl",
14
+ "baseUrl",
15
+ "token",
16
+ "userId",
17
+ "replyMode",
18
+ "forwardThinking",
19
+ "forwardToolCalls",
20
+ "richInteractions",
21
+ "enabled",
22
+ ],
23
+ resolveAllowFrom: (account) => account.allowFrom,
24
+ formatAllowFrom: () => [],
25
+ });
26
+ /**
27
+ * Invite-code setup adapter used by OpenClaw setup surfaces.
28
+ *
29
+ * `channels add --token` passes the invite code as setup input. The first
30
+ * config write only enables the channel; `afterAccountConfigWritten` exchanges
31
+ * the invite code and persists token/userId through the host runtime mutator.
32
+ */
33
+ const setupAdapter = {
34
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
35
+ validateInput: ({ input }) => {
36
+ const inviteCode = typeof input.code === "string" && input.code.trim()
37
+ ? input.code.trim()
38
+ : typeof input.token === "string"
39
+ ? input.token.trim()
40
+ : "";
41
+ if (!inviteCode) {
42
+ return "ClawChat invite code is required.";
43
+ }
44
+ return null;
45
+ },
46
+ applyAccountConfig: ({ cfg }) => {
47
+ const channels = (cfg.channels ?? {});
48
+ const current = (channels[CHANNEL_ID] ?? {});
49
+ return mergeOpenclawClawchatToolAllow({
50
+ ...cfg,
51
+ channels: {
52
+ ...channels,
53
+ [CHANNEL_ID]: { ...current, enabled: true },
54
+ },
55
+ });
56
+ },
57
+ afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
58
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten invoked");
59
+ const code = typeof input.code === "string" && input.code.trim()
60
+ ? input.code.trim()
61
+ : typeof input.token === "string"
62
+ ? input.token.trim()
63
+ : "";
64
+ if (!code) {
65
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten skipped: empty invite code");
66
+ return;
67
+ }
68
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
69
+ await runOpenclawClawlingLogin({
70
+ cfg,
71
+ accountId: null,
72
+ runtime: { log: (message) => runtime.log(message) },
73
+ readInviteCode: async () => code,
74
+ mutateConfigFile: mutateConfigFile,
75
+ });
76
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten completed");
77
+ },
78
+ };
79
+ export const openclawClawlingSetupPlugin = {
80
+ id: CHANNEL_ID,
81
+ meta: {
82
+ id: CHANNEL_ID,
83
+ label: "Clawling Chat",
84
+ selectionLabel: "Clawling Chat",
85
+ docsPath: "/channels/openclaw-clawchat",
86
+ docsLabel: "openclaw-clawchat",
87
+ blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
88
+ order: 110,
89
+ },
90
+ capabilities: {
91
+ chatTypes: ["direct", "group"],
92
+ media: true,
93
+ reactions: false,
94
+ threads: false,
95
+ polls: false,
96
+ blockStreaming: true,
97
+ },
98
+ reload: {
99
+ configPrefixes: [`channels.${CHANNEL_ID}`],
100
+ },
101
+ configSchema: {
102
+ schema: openclawClawlingConfigSchema,
103
+ },
104
+ config: {
105
+ ...configAdapter,
106
+ isConfigured: (account) => account.configured,
107
+ describeAccount: (account) => ({
108
+ accountId: account.accountId,
109
+ name: account.name,
110
+ enabled: account.enabled,
111
+ configured: account.configured,
112
+ }),
113
+ },
114
+ setup: setupAdapter,
115
+ status: createComputedAccountStatusAdapter({
116
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
117
+ connected: false,
118
+ lastInboundAt: null,
119
+ lastOutboundAt: null,
120
+ }),
121
+ resolveAccountSnapshot: ({ account }) => ({
122
+ accountId: account.accountId,
123
+ name: account.name,
124
+ enabled: account.enabled,
125
+ configured: account.configured,
126
+ extra: {
127
+ websocketUrl: account.websocketUrl || null,
128
+ baseUrl: account.baseUrl || null,
129
+ userId: account.userId || null,
130
+ },
131
+ }),
132
+ }),
133
+ };
@@ -44,12 +44,19 @@ function normalizeRouting(params) {
44
44
  * Emit a raw v2 envelope directly over the transport so we can carry top-level
45
45
  * `chat_id` routing without SDK-injected `to` metadata.
46
46
  */
47
- function emitEnvelope(client, event, payload, routing) {
47
+ function emitEnvelope(client, event, payload, routing, options = {}) {
48
48
  const inner = client;
49
- if (!inner.opts?.transport) {
50
- inner.emitRaw?.(event, payload, { to: { id: routing.chatId, type: routing.chatType } });
49
+ if (!options.forceRawTransport && inner.emitRaw) {
50
+ inner.emitRaw(event, payload, { chat_id: routing.chatId });
51
51
  return;
52
52
  }
53
+ if (!inner.opts?.transport) {
54
+ if (!options.forceRawTransport && inner.emitRaw) {
55
+ inner.emitRaw(event, payload, { chat_id: routing.chatId });
56
+ return;
57
+ }
58
+ throw new Error("openclaw-clawchat streaming emit requires SDK raw transport");
59
+ }
53
60
  const env = {
54
61
  version: "2",
55
62
  event,
@@ -150,7 +157,7 @@ export function emitFinalStreamReply(client, params) {
150
157
  },
151
158
  },
152
159
  },
153
- }, routing);
160
+ }, routing, { forceRawTransport: true });
154
161
  }
155
162
  export function emitStreamFailed(client, params) {
156
163
  const now = Date.now();
@@ -1,5 +1,10 @@
1
1
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
2
2
  export const CHANNEL_ID = "openclaw-clawchat";
3
+ export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN";
4
+ export const CLAWCHAT_USER_ID_ENV = "CLAWCHAT_USER_ID";
5
+ export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
6
+ export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
7
+ export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
3
8
  /**
4
9
  * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
5
10
  * login` works out of the box without requiring a prior `openclaw channel
@@ -134,6 +139,9 @@ function readChannelSection(cfg) {
134
139
  function readOptionalString(value) {
135
140
  return typeof value === "string" ? value.trim() : "";
136
141
  }
142
+ function readEnvString(env, key) {
143
+ return readOptionalString(env[key]);
144
+ }
137
145
  function readReplyMode(value) {
138
146
  return value === "stream" ? "stream" : "static";
139
147
  }
@@ -175,13 +183,17 @@ function readAck(raw) {
175
183
  : DEFAULT_ACK.autoResendOnTimeout,
176
184
  };
177
185
  }
178
- export function resolveOpenclawClawlingAccount(cfg) {
186
+ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
179
187
  const channel = readChannelSection(cfg);
180
188
  // Apply built-in defaults so login/gateway work without prior setup.
181
- const websocketUrl = readOptionalString(channel.websocketUrl) || DEFAULT_WEBSOCKET_URL;
182
- const baseUrl = readOptionalString(channel.baseUrl) || DEFAULT_BASE_URL;
183
- const token = readOptionalString(channel.token);
184
- const userId = readOptionalString(channel.userId);
189
+ const websocketUrl = readOptionalString(channel.websocketUrl) ||
190
+ readEnvString(env, CLAWCHAT_WEBSOCKET_URL_ENV) ||
191
+ DEFAULT_WEBSOCKET_URL;
192
+ const baseUrl = readOptionalString(channel.baseUrl) ||
193
+ readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
194
+ DEFAULT_BASE_URL;
195
+ const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
196
+ const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
185
197
  const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
186
198
  const replyMode = readReplyMode(channel.replyMode);
187
199
  const groupMode = readGroupMode(channel.groupMode);
@@ -54,22 +54,31 @@ function buildLoginConfig(cfg, result) {
54
54
  }
55
55
  async function persistLoginConfig(params, result) {
56
56
  if (params.mutateConfigFile) {
57
+ params.runtime.log(`Persisting ClawChat credentials for userId=${result.agent.user_id} with Gateway restart intent.`);
57
58
  await params.mutateConfigFile({
58
- afterWrite: { mode: "auto" },
59
+ afterWrite: {
60
+ mode: "restart",
61
+ reason: "openclaw-clawchat credentials changed",
62
+ },
59
63
  mutate(draft) {
60
64
  Object.assign(draft, buildLoginConfig(draft, result));
61
65
  },
62
66
  });
67
+ params.runtime.log(`ClawChat credentials persisted for userId=${result.agent.user_id}.`);
63
68
  return;
64
69
  }
65
70
  if (params.persistConfig) {
71
+ params.runtime.log(`Persisting ClawChat credentials for userId=${result.agent.user_id}.`);
66
72
  await params.persistConfig(buildLoginConfig(params.cfg, result));
73
+ params.runtime.log(`ClawChat credentials persisted for userId=${result.agent.user_id}.`);
67
74
  return;
68
75
  }
69
76
  throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
70
77
  }
71
78
  /**
72
- * Run the `openclaw channels login --channel openclaw-clawchat` flow:
79
+ * Run the invite-code credential exchange used by `clawchat_activate`,
80
+ * `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
81
+ * and `openclaw channels login --channel openclaw-clawchat`:
73
82
  * 1. Read the existing channel section; require `baseUrl` to be set so we
74
83
  * know which server to hit.
75
84
  * 2. Prompt the user for an invite code on stdin.
@@ -1,3 +1,4 @@
1
+ import { buildOutboundMediaLoadOptions, } from "openclaw/plugin-sdk/media-runtime";
1
2
  export function inferMediaKindFromMime(mime) {
2
3
  if (!mime)
3
4
  return "file";
@@ -56,12 +57,12 @@ export async function uploadOutboundMedia(urls, ctx) {
56
57
  const out = [];
57
58
  for (const url of urls) {
58
59
  try {
59
- const loaded = await ctx.runtime.media.loadWebMedia(url, {
60
+ const loaded = await ctx.runtime.media.loadWebMedia(url, buildOutboundMediaLoadOptions({
60
61
  maxBytes,
61
- ...(ctx.mediaLocalRoots && ctx.mediaLocalRoots.length > 0
62
- ? { localRoots: ctx.mediaLocalRoots }
63
- : {}),
64
- });
62
+ ...(ctx.mediaAccess ? { mediaAccess: ctx.mediaAccess } : {}),
63
+ ...(ctx.mediaLocalRoots ? { mediaLocalRoots: ctx.mediaLocalRoots } : {}),
64
+ ...(ctx.mediaReadFile ? { mediaReadFile: ctx.mediaReadFile } : {}),
65
+ }));
65
66
  const uploaded = await ctx.apiClient.uploadMedia({
66
67
  buffer: loaded.buffer,
67
68
  filename: loaded.fileName ?? "upload.bin",
@@ -19,6 +19,8 @@ import { getOpenclawClawlingClient, getOpenclawClawlingRuntime, waitForOpenclawC
19
19
  * - `clawchat:group:{chat_id}` → group
20
20
  * - `openclaw-clawchat:direct:{chat_id}` → direct
21
21
  * - `openclaw-clawchat:group:{chat_id}` → group
22
+ * - `direct:{chat_id}` → direct (host-normalized)
23
+ * - `group:{chat_id}` → group (host-normalized)
22
24
  * - bare `{chat_id}` → direct (backward compat)
23
25
  */
24
26
  export function parseOpenclawRecipient(to) {
@@ -30,6 +32,12 @@ export function parseOpenclawRecipient(to) {
30
32
  return { chatId: raw, chatType: "direct" };
31
33
  const scheme = raw.slice(0, firstColon).toLowerCase();
32
34
  const rest = raw.slice(firstColon + 1);
35
+ if (scheme === "direct" || scheme === "group") {
36
+ const chatId = rest.trim();
37
+ if (!chatId)
38
+ throw new Error(`openclaw-clawchat: missing chat_id in "${to}"`);
39
+ return { chatId, chatType: scheme };
40
+ }
33
41
  if (scheme !== "cc" && scheme !== "clawchat" && scheme !== CHANNEL_ID) {
34
42
  return { chatId: raw, chatType: "direct" };
35
43
  }
@@ -74,7 +82,7 @@ export async function sendOpenclawClawlingText(params) {
74
82
  mode: "normal",
75
83
  replyTo: {
76
84
  msgId: params.replyCtx.replyToMessageId,
77
- senderId: params.replyCtx.replyPreviewChatId ?? params.replyCtx.replyPreviewSenderId,
85
+ senderId: params.replyCtx.replyPreviewSenderId,
78
86
  nickName: params.replyCtx.replyPreviewNickName,
79
87
  fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
80
88
  },
@@ -144,7 +152,7 @@ export const openclawClawlingOutbound = {
144
152
  messageId: result?.messageId ?? `${account.userId}-${Date.now()}`,
145
153
  };
146
154
  },
147
- sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots }) => {
155
+ sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
148
156
  const account = resolveOpenclawClawlingAccount(cfg);
149
157
  const client = getOpenclawClawlingClient(account.accountId) ??
150
158
  (await waitForOpenclawClawlingClient(account.accountId));
@@ -160,7 +168,9 @@ export const openclawClawlingOutbound = {
160
168
  const mediaFragments = await uploadOutboundMedia([mediaUrl.trim()], {
161
169
  apiClient,
162
170
  runtime,
171
+ ...(mediaAccess ? { mediaAccess } : {}),
163
172
  ...(mediaLocalRoots ? { mediaLocalRoots } : {}),
173
+ ...(mediaReadFile ? { mediaReadFile } : {}),
164
174
  });
165
175
  if (mediaFragments.length === 0) {
166
176
  throw new Error(`openclaw-clawchat failed to upload media: ${mediaUrl}`);