@clawling/clawchat-plugin-openclaw 2026.5.12-28 → 2026.5.12-31

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -50
  3. package/dist/src/api-client.js +37 -4
  4. package/dist/src/channel.js +4 -4
  5. package/dist/src/channel.setup.js +2 -2
  6. package/dist/src/commands.js +102 -0
  7. package/dist/src/config.js +28 -1
  8. package/dist/src/group-message-coalescer.js +21 -37
  9. package/dist/src/login.runtime.js +1 -0
  10. package/dist/src/outbound.js +1 -0
  11. package/dist/src/profile-prompt.js +70 -31
  12. package/dist/src/reply-dispatcher.js +9 -0
  13. package/dist/src/runtime.js +159 -5
  14. package/dist/src/storage.js +19 -0
  15. package/dist/src/tools-schema.js +34 -2
  16. package/dist/src/tools.js +130 -15
  17. package/openclaw.plugin.json +38 -5
  18. package/package.json +8 -3
  19. package/prompts/default-group-bio.md +14 -14
  20. package/prompts/default-owner-behavior.md +22 -22
  21. package/prompts/platform.md +2 -2
  22. package/skills/clawchat/SKILL.md +5 -1
  23. package/src/api-client.ts +61 -5
  24. package/src/api-types.ts +17 -0
  25. package/src/channel.setup.ts +2 -1
  26. package/src/channel.ts +4 -3
  27. package/src/commands.ts +113 -0
  28. package/src/config.ts +48 -1
  29. package/src/group-message-coalescer.ts +35 -36
  30. package/src/inbound.ts +11 -0
  31. package/src/login.runtime.ts +1 -0
  32. package/src/outbound.ts +1 -0
  33. package/src/profile-prompt.ts +83 -31
  34. package/src/reply-dispatcher.ts +11 -0
  35. package/src/runtime.ts +197 -6
  36. package/src/storage.ts +38 -0
  37. package/src/tools-schema.ts +49 -2
  38. package/src/tools.ts +170 -14
  39. package/INSTALL.md +0 -64
  40. package/dist/src/api-types.test-d.js +0 -10
  41. package/dist/src/protocol-types.typecheck.js +0 -1
  42. package/src/api-client.test.ts +0 -827
  43. package/src/channel.outbound.test.ts +0 -433
  44. package/src/channel.test.ts +0 -262
  45. package/src/clawchat-memory.test.ts +0 -480
  46. package/src/clawchat-metadata.test.ts +0 -477
  47. package/src/client.test.ts +0 -169
  48. package/src/commands.test.ts +0 -39
  49. package/src/config.test.ts +0 -344
  50. package/src/group-message-coalescer.test.ts +0 -237
  51. package/src/inbound.test.ts +0 -508
  52. package/src/llm-context-debug.test.ts +0 -55
  53. package/src/login.runtime.test.ts +0 -737
  54. package/src/manifest.test.ts +0 -352
  55. package/src/media-runtime.test.ts +0 -207
  56. package/src/message-mapper.test.ts +0 -201
  57. package/src/mock-transport.test.ts +0 -35
  58. package/src/outbound.test.ts +0 -1269
  59. package/src/plugin-entry.test.ts +0 -38
  60. package/src/plugin-prompts.test.ts +0 -94
  61. package/src/profile-prompt.test.ts +0 -274
  62. package/src/profile-sync.test.ts +0 -539
  63. package/src/prompt-injection.test.ts +0 -39
  64. package/src/protocol-types.test.ts +0 -69
  65. package/src/protocol-types.typecheck.ts +0 -89
  66. package/src/protocol.test.ts +0 -39
  67. package/src/reply-dispatcher.test.ts +0 -1324
  68. package/src/runtime.test.ts +0 -4719
  69. package/src/scripts.test.ts +0 -85
  70. package/src/storage.test.ts +0 -560
  71. package/src/terminal-send.test.ts +0 -81
  72. package/src/tools.test.ts +0 -933
  73. package/src/ws-alignment.test.ts +0 -103
  74. package/src/ws-client.test.ts +0 -1217
  75. package/src/ws-log.test.ts +0 -32
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CLAWLING PTE. LTD.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,12 +5,11 @@ OpenClaw channel plugin that connects an agent to ClawChat over ClawChat Protoco
5
5
  ## Features
6
6
 
7
7
  - Plugin-owned WebSocket transport with auto-reconnect (exponential backoff + jitter), heartbeat, and ack tracking
8
- - Invite-code onboarding — no raw credentials required
9
8
  - Inbound `message.send` / `message.reply` with reply context
10
9
  - Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
11
10
  - Typing indicators and filtered forwarding for thinking / tool-call content
12
11
  - Media fragments (image / file / audio / video) in either direction
13
- - Invite-code onboarding via `/clawchat-activate` or supported `openclaw channels add`, plus always-registered `clawchat_*` account/media tools
12
+ - Invite-code onboarding (no raw credentials) via `/clawchat-activate` or supported `openclaw channels add`, plus always-registered `clawchat_*` account/media tools
14
13
 
15
14
  ## Install
16
15
 
@@ -21,13 +20,10 @@ npm i @clawling/clawchat-plugin-openclaw
21
20
 
22
21
  Requires `openclaw >= 2026.5.4` as a peer host.
23
22
 
24
- For the OpenClaw plugin install/update flow, use the R2-hosted tarball install
25
- command documented in [`INSTALL.md`](./INSTALL.md).
26
-
27
23
  Example LLM prompt:
28
24
 
29
25
  ```text
30
- Use https://raw.githubusercontent.com/clawling/clawchat-plugin-openclaw/refs/heads/main/INSTALL.md to install and activate the ClawChat plugin. The invite code is XXXXXX.
26
+ Install and activate the ClawChat plugin from npm package @clawling/clawchat-plugin-openclaw. The invite code is XXXXXX.
31
27
  ```
32
28
 
33
29
  ## Current activation paths
@@ -166,7 +162,8 @@ npm run typecheck
166
162
 
167
163
  Tests live next to the source they cover (`*.test.ts`). The development entrypoint stays in TypeScript for the OpenClaw extension loader, while npm installs use the compiled runtime entrypoint generated by `npm run build` / `prepack` under `dist/`.
168
164
 
169
- Functional e2e test cases are documented in `.e2e/docs/install-clawchat-plugin-e2e.md`; keep that guide updated when adding or changing e2e flows.
165
+ Functional E2E test cases are documented in the root ClawChat workspace E2E
166
+ docs; keep that guide updated when adding or changing E2E flows.
170
167
 
171
168
  For OpenClaw host SDK/source lookup while developing this plugin, optionally
172
169
  clone OpenClaw into `tmp/openclaw`:
@@ -179,49 +176,6 @@ npm run dev:openclaw-source
179
176
  This checkout is local-only. It is ignored by git and is not required to run the
180
177
  plugin tests or publish the package.
181
178
 
182
- ## R2 package scripts
183
-
184
- Create and upload the OpenClaw plugin tarball to the R2 `openclaw/` prefix:
185
-
186
- ```bash
187
- ./scripts/package_openclaw_plugin.sh
188
- ```
189
-
190
- The script runs `npm pack`, removes `devDependencies` from the generated `.tgz`
191
- metadata so OpenClaw installs only runtime dependencies, uploads the `.tgz` to
192
- the configured R2 bucket, updates the `latest` R2 alias, uploads `INSTALL.md` as
193
- `openclaw/install.md`, and prints the public URLs. R2 credentials are read from
194
- `scripts/.env.r2`, which is ignored by git. Copy `scripts/.env.r2.example` to
195
- `scripts/.env.r2` and fill in the credentials. Use `--no-upload` to build the
196
- tarball without uploading it.
197
-
198
- ```bash
199
- AWS_ACCESS_KEY_ID=...
200
- AWS_SECRET_ACCESS_KEY=...
201
- AWS_DEFAULT_REGION=auto
202
- R2_ENDPOINT=https://...
203
- R2_BUCKET=...
204
- ```
205
-
206
- Install the R2-hosted latest tarball on a device or container with OpenClaw
207
- available:
208
-
209
- ```bash
210
- ./scripts/install_openclaw.sh
211
- ```
212
-
213
- To install a specific uploaded version, pass the version string:
214
-
215
- ```bash
216
- ./scripts/install_openclaw.sh 2026.5.16-1
217
- ```
218
-
219
- To install a specific uploaded tarball URL, pass its URL explicitly:
220
-
221
- ```bash
222
- ./scripts/install_openclaw.sh https://plugin.clawling.chat/openclaw/newbase-clawchat-clawchat-plugin-openclaw-2026.5.16-1.tgz
223
- ```
224
-
225
179
  ## License
226
180
 
227
181
  See the repository root.
@@ -5,9 +5,16 @@ export function createOpenclawClawlingApiClient(opts) {
5
5
  throw new ClawlingApiError("validation", `clawchat-plugin-openclaw baseUrl must start with http:// or https:// (got "${opts.baseUrl}")`);
6
6
  }
7
7
  const baseUrl = opts.baseUrl.replace(/\/+$/, "");
8
+ let mediaBaseUrl = baseUrl;
9
+ if (opts.mediaBaseUrl && opts.mediaBaseUrl.trim()) {
10
+ if (!/^https?:\/\//i.test(opts.mediaBaseUrl)) {
11
+ throw new ClawlingApiError("validation", `clawchat-plugin-openclaw mediaBaseUrl must start with http:// or https:// (got "${opts.mediaBaseUrl}")`);
12
+ }
13
+ mediaBaseUrl = opts.mediaBaseUrl.replace(/\/+$/, "");
14
+ }
8
15
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
9
- function url(path) {
10
- return `${baseUrl}${path}`;
16
+ function url(path, base = baseUrl) {
17
+ return `${base}${path}`;
11
18
  }
12
19
  function authHeaders(extra = {}) {
13
20
  // `X-Device-Id` is sent on every request so the server can correlate
@@ -75,7 +82,7 @@ export function createOpenclawClawlingApiClient(opts) {
75
82
  if (init?.body !== undefined) {
76
83
  requestInit.body = init.body;
77
84
  }
78
- res = await fetchImpl(url(path), requestInit);
85
+ res = await fetchImpl(url(path, init?.baseUrl || undefined), requestInit);
79
86
  }
80
87
  catch (err) {
81
88
  throw new ClawlingApiError("transport", `fetch failed: ${err instanceof Error ? err.message : String(err)}`, { path });
@@ -148,6 +155,32 @@ export function createOpenclawClawlingApiClient(opts) {
148
155
  async listFriends() {
149
156
  return await call("GET", "/v1/friendships");
150
157
  },
158
+ async sendFriendRequest(params) {
159
+ assertNonBlankId(params.userId, "sendFriendRequest: userId");
160
+ return await call("POST", "/v1/friendships", {
161
+ body: JSON.stringify({
162
+ user_id: params.userId,
163
+ ...(typeof params.greeting === "string" ? { greeting: params.greeting } : {}),
164
+ }),
165
+ headers: { "content-type": "application/json" },
166
+ });
167
+ },
168
+ async listFriendRequests(params) {
169
+ if (params.direction !== "incoming" && params.direction !== "outgoing") {
170
+ throw new ClawlingApiError("validation", "listFriendRequests: direction must be incoming or outgoing");
171
+ }
172
+ return await call("GET", `/v1/friendships/requests/${params.direction}`);
173
+ },
174
+ async acceptFriendRequest(requestId) {
175
+ return await call("POST", `/v1/friendships/requests/${encodeURIComponent(String(requestId))}/accept`);
176
+ },
177
+ async rejectFriendRequest(requestId) {
178
+ return await call("POST", `/v1/friendships/requests/${encodeURIComponent(String(requestId))}/reject`);
179
+ },
180
+ async removeFriend(friendUserId) {
181
+ assertNonBlankId(friendUserId, "removeFriend: friendUserId");
182
+ return await call("DELETE", `/v1/friendships/${encodeURIComponent(friendUserId)}`);
183
+ },
151
184
  async searchUsers(params) {
152
185
  const sp = new URLSearchParams();
153
186
  if (typeof params.q === "string")
@@ -245,7 +278,7 @@ export function createOpenclawClawlingApiClient(opts) {
245
278
  });
246
279
  const fd = new FormData();
247
280
  fd.set("file", file);
248
- const data = await call("POST", "/media/upload", { body: fd });
281
+ const data = await call("POST", "/media/upload", { body: fd, baseUrl: mediaBaseUrl });
249
282
  return parseUploadResult(data, "/media/upload");
250
283
  },
251
284
  async uploadAvatar(params) {
@@ -1,6 +1,6 @@
1
1
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
2
2
  import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
3
- import { CHANNEL_ID, resolveOpenclawClawlingAccount, } from "./config.js";
3
+ import { CHANNEL_ID, canStartOpenclawClawlingAccount, resolveOpenclawClawlingAccount, } from "./config.js";
4
4
  import { openclawClawlingOutbound } from "./outbound.js";
5
5
  import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.js";
6
6
  import { openclawClawlingSetupPlugin } from "./channel.setup.js";
@@ -27,9 +27,9 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
27
27
  startAccount: async (ctx) => {
28
28
  const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
29
29
  ctx.log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle START_ACCOUNT_CALLED configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
30
- if (!account.configured) {
31
- ctx.log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle startAccount refused: websocketUrl/token/userId are required`);
32
- throw new Error("Clawling Chat websocketUrl/token/userId are required");
30
+ if (!canStartOpenclawClawlingAccount(account)) {
31
+ ctx.log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle startAccount refused: websocketUrl is required`);
32
+ throw new Error("Clawling Chat websocketUrl is required");
33
33
  }
34
34
  try {
35
35
  await startOpenclawClawlingGateway({
@@ -2,7 +2,7 @@ import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-
2
2
  import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
3
3
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
4
4
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
5
- import { CHANNEL_ID, listOpenclawClawlingAccountIds, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
5
+ import { CHANNEL_ID, canStartOpenclawClawlingAccount, listOpenclawClawlingAccountIds, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
6
6
  const configAdapter = createTopLevelChannelConfigAdapter({
7
7
  sectionKey: CHANNEL_ID,
8
8
  resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
@@ -89,7 +89,7 @@ export const openclawClawlingSetupPlugin = {
89
89
  },
90
90
  config: {
91
91
  ...configAdapter,
92
- isConfigured: (account) => account.configured,
92
+ isConfigured: (account) => canStartOpenclawClawlingAccount(account),
93
93
  describeAccount: (account) => ({
94
94
  accountId: account.accountId,
95
95
  name: account.name,
@@ -1,3 +1,4 @@
1
+ import { CHANNEL_ID } from "./config.js";
1
2
  function extractInviteCode(value) {
2
3
  const raw = typeof value === "string" ? value.trim() : "";
3
4
  return raw.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
@@ -5,6 +6,80 @@ function extractInviteCode(value) {
5
6
  function errorMessage(err) {
6
7
  return err instanceof Error ? err.message : String(err);
7
8
  }
9
+ function readRecord(value) {
10
+ return value && typeof value === "object" && !Array.isArray(value)
11
+ ? value
12
+ : {};
13
+ }
14
+ function extractOutputVisibility(value) {
15
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
16
+ const token = raw.split(/\s+/, 1)[0] ?? "";
17
+ return token === "minimal" || token === "normal" || token === "full" ? token : null;
18
+ }
19
+ function stripChannelPrefix(value) {
20
+ const raw = value.trim();
21
+ const prefix = `${CHANNEL_ID}:`;
22
+ return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
23
+ }
24
+ function extractChatIdFromRoute(value) {
25
+ if (typeof value !== "string")
26
+ return "";
27
+ const stripped = stripChannelPrefix(value);
28
+ if (!stripped)
29
+ return "";
30
+ if (stripped.startsWith("group:"))
31
+ return stripped.slice("group:".length).trim();
32
+ if (stripped.startsWith("direct:"))
33
+ return stripped.slice("direct:".length).trim();
34
+ return stripped.trim();
35
+ }
36
+ function resolveCommandChatId(ctx) {
37
+ return extractChatIdFromRoute(ctx.from)
38
+ || extractChatIdFromRoute(ctx.to)
39
+ || (typeof ctx.threadParentId === "string" ? ctx.threadParentId.trim() : "")
40
+ || (typeof ctx.messageThreadId === "string" || typeof ctx.messageThreadId === "number"
41
+ ? String(ctx.messageThreadId).trim()
42
+ : "");
43
+ }
44
+ function persistOutputVisibility(draft, chatId, outputVisibility) {
45
+ const channels = readRecord(draft.channels);
46
+ const channel = readRecord(channels[CHANNEL_ID]);
47
+ const chats = readRecord(channel.chats);
48
+ const chat = readRecord(chats[chatId]);
49
+ Object.assign(draft, {
50
+ ...draft,
51
+ channels: {
52
+ ...channels,
53
+ [CHANNEL_ID]: {
54
+ ...channel,
55
+ chats: {
56
+ ...chats,
57
+ [chatId]: {
58
+ ...chat,
59
+ outputVisibility,
60
+ },
61
+ },
62
+ },
63
+ },
64
+ });
65
+ }
66
+ function formatOutputVisibilityResult(outputVisibility) {
67
+ const runtimeStatus = outputVisibility === "full" ? "on" : "off";
68
+ const detailLevel = {
69
+ minimal: "quiet",
70
+ normal: "normal",
71
+ full: "verbose",
72
+ };
73
+ return [
74
+ "**ClawChat output updated**",
75
+ "",
76
+ `- visibility: \`${outputVisibility}\``,
77
+ `- runtime status: \`${runtimeStatus}\``,
78
+ `- detail level: \`${detailLevel[outputVisibility]}\``,
79
+ "",
80
+ "Applies to new ClawChat messages.",
81
+ ].join("\n");
82
+ }
8
83
  export function registerOpenclawClawlingCommands(api) {
9
84
  api.registerCommand({
10
85
  name: "clawchat-activate",
@@ -32,4 +107,31 @@ export function registerOpenclawClawlingCommands(api) {
32
107
  }
33
108
  },
34
109
  });
110
+ api.registerCommand({
111
+ name: "clawchat-output",
112
+ description: "Set ClawChat output visibility for the current conversation.",
113
+ acceptsArgs: true,
114
+ requireAuth: true,
115
+ async handler(ctx) {
116
+ const outputVisibility = extractOutputVisibility(ctx.args ?? ctx.commandBody);
117
+ if (!outputVisibility) {
118
+ return { text: "Usage: /clawchat-output minimal|normal|full" };
119
+ }
120
+ const chatId = resolveCommandChatId(ctx);
121
+ if (!chatId) {
122
+ return { text: "Unable to determine the current ClawChat conversation for /clawchat-output." };
123
+ }
124
+ const mutateConfigFile = api.runtime.config.mutateConfigFile;
125
+ if (!mutateConfigFile) {
126
+ return { text: "OpenClaw runtime config mutation is unavailable for /clawchat-output." };
127
+ }
128
+ await mutateConfigFile({
129
+ afterWrite: { mode: "auto" },
130
+ mutate(draft) {
131
+ persistOutputVisibility(draft, chatId, outputVisibility);
132
+ },
133
+ });
134
+ return { text: formatOutputVisibilityResult(outputVisibility) };
135
+ },
136
+ });
35
137
  }
@@ -7,6 +7,7 @@ export const CLAWCHAT_OWNER_USER_ID_ENV = "CLAWCHAT_OWNER_USER_ID";
7
7
  export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
8
8
  export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
9
9
  export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
10
+ export const CLAWCHAT_MEDIA_BASE_URL_ENV = "CLAWCHAT_MEDIA_BASE_URL";
10
11
  /**
11
12
  * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
12
13
  * login` works out of the box without requiring a prior `openclaw channel
@@ -46,11 +47,23 @@ export const openclawClawlingConfigSchema = {
46
47
  enabled: { type: "boolean" },
47
48
  websocketUrl: { type: "string" },
48
49
  baseUrl: { type: "string" },
50
+ mediaBaseUrl: { type: "string" },
49
51
  token: { type: "string" },
50
52
  refreshToken: { type: "string" },
51
53
  agentId: { type: "string" },
52
54
  userId: { type: "string" },
53
55
  ownerUserId: { type: "string" },
56
+ outputVisibility: { type: "string", enum: ["minimal", "normal", "full"] },
57
+ chats: {
58
+ type: "object",
59
+ additionalProperties: {
60
+ type: "object",
61
+ additionalProperties: false,
62
+ properties: {
63
+ outputVisibility: { type: "string", enum: ["minimal", "normal", "full"] },
64
+ },
65
+ },
66
+ },
54
67
  groupMode: { type: "string", enum: ["mention", "all"] },
55
68
  groupCommandMode: { type: "string", enum: ["owner", "all", "off"] },
56
69
  groups: {
@@ -61,6 +74,7 @@ export const openclawClawlingConfigSchema = {
61
74
  properties: {
62
75
  groupMode: { type: "string", enum: ["mention", "all"] },
63
76
  groupCommandMode: { type: "string", enum: ["owner", "all", "off"] },
77
+ outputVisibility: { type: "string", enum: ["minimal", "normal", "full"] },
64
78
  },
65
79
  },
66
80
  },
@@ -154,6 +168,12 @@ export function mergeOpenclawClawchatRuntimePluginActivation(cfg) {
154
168
  plugins: nextPlugins,
155
169
  };
156
170
  }
171
+ export function hasOpenclawClawlingConnectCredentials(account) {
172
+ return Boolean(account.websocketUrl && account.token && account.userId && account.ownerUserId);
173
+ }
174
+ export function canStartOpenclawClawlingAccount(account) {
175
+ return Boolean(account.enabled && account.websocketUrl);
176
+ }
157
177
  function readChannelSection(cfg) {
158
178
  const channels = (cfg.channels ?? {});
159
179
  const channel = channels[CHANNEL_ID];
@@ -235,6 +255,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
235
255
  const baseUrl = readOptionalString(channel.baseUrl) ||
236
256
  readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
237
257
  DEFAULT_BASE_URL;
258
+ const mediaBaseUrl = readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
238
259
  const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
239
260
  const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
240
261
  const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
@@ -250,9 +271,15 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
250
271
  accountId: DEFAULT_ACCOUNT_ID,
251
272
  name: CHANNEL_ID,
252
273
  enabled,
253
- configured: Boolean(websocketUrl && token && userId && ownerUserId),
274
+ configured: hasOpenclawClawlingConnectCredentials({
275
+ websocketUrl,
276
+ token,
277
+ userId,
278
+ ownerUserId,
279
+ }),
254
280
  websocketUrl,
255
281
  baseUrl,
282
+ mediaBaseUrl,
256
283
  token,
257
284
  agentId,
258
285
  userId,
@@ -1,11 +1,3 @@
1
- function formatTurnTime(timestamp) {
2
- if (!Number.isFinite(timestamp))
3
- return "unknown-time";
4
- const time = new Date(timestamp);
5
- if (Number.isNaN(time.getTime()))
6
- return "unknown-time";
7
- return time.toISOString();
8
- }
9
1
  function formatSenderRelation(turn) {
10
2
  return turn.senderRelation || "peer_user";
11
3
  }
@@ -21,40 +13,31 @@ function formatMessageBody(rawBody) {
21
13
  function formatField(value) {
22
14
  return value.replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
23
15
  }
24
- function formatMentionedUsers(turn) {
25
- const mentionedUsers = turn.mentionedUsers && turn.mentionedUsers.length > 0
26
- ? turn.mentionedUsers
27
- : turn.mentionedUserIds.map((id) => ({ id }));
28
- if (mentionedUsers.length === 0)
29
- return "-";
30
- return mentionedUsers.map((mention) => {
31
- const id = formatField(mention.id);
32
- const display = mention.display?.trim();
33
- return display ? `${id}(${formatField(display)})` : id;
34
- }).join(",");
35
- }
36
16
  export function formatCoalescedGroupBody(turns, timing = { idleSeconds: 10, maxWaitSeconds: 30 }) {
37
- const header = `ClawChat group batch (${turns.length} ${turns.length === 1 ? "message" : "messages"}, ${timing.idleSeconds}s idle, ${timing.maxWaitSeconds}s max):`;
17
+ void timing;
38
18
  return [
39
- header,
40
- turns.map((turn) => {
19
+ "ClawChat group messages:",
20
+ turns.map((turn, index) => {
41
21
  const senderName = turn.senderNickName || turn.senderId;
42
- const senderIsAgentOwner = turn.senderIsOwner ?? formatSenderRelation(turn) === "owner";
43
- return [
44
- "[message]",
45
- `sender_id: ${formatField(turn.senderId)}`,
46
- `sender_name: ${formatField(senderName)}`,
47
- `sender_profile_type: ${formatField(formatSenderProfileType(turn))}`,
48
- `sender_is_agent_owner: ${senderIsAgentOwner ? "true" : "false"}`,
49
- `sender_is_group_owner: ${turn.senderIsGroupOwner ? "true" : "false"}`,
50
- `mentions_current_agent: ${turn.wasMentioned ? "true" : "false"}`,
51
- `mentioned_users: ${formatMentionedUsers(turn)}`,
52
- "text:",
53
- formatMessageBody(turn.rawBody),
54
- ].join("\n");
55
- }).join("\n\n"),
22
+ const label = `[message ${index + 1}] ${formatField(senderName)}:`;
23
+ const body = formatMessageBody(turn.rawBody);
24
+ return body.includes("\n") ? `${label}\n${body}` : `${label} ${body}`;
25
+ }).join("\n"),
56
26
  ].join("\n");
57
27
  }
28
+ function groupMessageForPrompt(turn) {
29
+ return {
30
+ senderId: turn.senderId,
31
+ senderName: turn.senderNickName || turn.senderId,
32
+ senderRelation: turn.senderRelation,
33
+ senderProfileType: formatSenderProfileType(turn),
34
+ senderIsOwner: turn.senderIsOwner ?? formatSenderRelation(turn) === "owner",
35
+ senderIsGroupOwner: turn.senderIsGroupOwner,
36
+ wasMentioned: turn.wasMentioned,
37
+ mentionedUserIds: turn.mentionedUserIds,
38
+ mentionedUsers: turn.mentionedUsers,
39
+ };
40
+ }
58
41
  export function mergeGroupTurns(turns, timing = { idleSeconds: 10, maxWaitSeconds: 30 }) {
59
42
  if (turns.length === 0)
60
43
  throw new Error("cannot merge empty group turn batch");
@@ -62,6 +45,7 @@ export function mergeGroupTurns(turns, timing = { idleSeconds: 10, maxWaitSecond
62
45
  return {
63
46
  ...latest,
64
47
  rawBody: formatCoalescedGroupBody(turns, timing),
48
+ groupMessages: turns.map(groupMessageForPrompt),
65
49
  mediaItems: turns.flatMap((turn) => turn.mediaItems),
66
50
  wasMentioned: turns.some((turn) => turn.wasMentioned),
67
51
  mentionedUserIds: Array.from(new Set(turns.flatMap((turn) => turn.mentionedUserIds))),
@@ -131,6 +131,7 @@ export async function runOpenclawClawlingLogin(params) {
131
131
  }
132
132
  const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
133
133
  baseUrl: account.baseUrl,
134
+ mediaBaseUrl: account.mediaBaseUrl,
134
135
  // Pre-login we may not have a token yet. Send the current one (or empty)
135
136
  // — the server should accept an unauthenticated invite-code exchange.
136
137
  token: account.token || "",
@@ -581,6 +581,7 @@ export const openclawClawlingOutbound = {
581
581
  const runtime = getOpenclawClawlingRuntime();
582
582
  const apiClient = createOpenclawClawlingApiClient({
583
583
  baseUrl: account.baseUrl,
584
+ mediaBaseUrl: account.mediaBaseUrl,
584
585
  token: account.token,
585
586
  userId: account.userId,
586
587
  });