@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.
@@ -1,4 +1,5 @@
1
1
  import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
2
+ import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
2
3
  import { createOpenclawClawlingApiClient } from "./api-client.js";
3
4
  import { openBufferedStreamingSession, mergeStreamingText, } from "./buffered-stream.js";
4
5
  import { emitFinalStreamReply } from "./client.js";
@@ -256,7 +257,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
256
257
  routing,
257
258
  replyTo: {
258
259
  msgId: inboundMessageId ?? streamingMessageId,
259
- previewId: inboundForFinalReply?.chatId ?? target.chatId,
260
+ previewId: inboundForFinalReply?.senderId ?? target.chatId,
260
261
  nickName: inboundForFinalReply?.senderNickName ?? inboundForFinalReply?.senderId ?? target.chatId,
261
262
  fragments: inboundForFinalReply?.bodyText
262
263
  ? [{ kind: "text", text: inboundForFinalReply.bodyText }]
@@ -271,10 +272,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
271
272
  }
272
273
  if (text)
273
274
  streamText = mergeStreamingText(streamText, text);
274
- const urls = [
275
- ...(payload.mediaUrl ? [payload.mediaUrl] : []),
276
- ...(payload.mediaUrls ?? []),
277
- ].filter((u) => Boolean(u));
275
+ const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
278
276
  for (const url of urls) {
279
277
  if (!accumulatedMediaUrls.includes(url))
280
278
  accumulatedMediaUrls.push(url);
@@ -313,10 +311,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
313
311
  deliver: async (payload, info) => {
314
312
  const richFragment = buildRichInteractionFragment(payload);
315
313
  const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
316
- const urls = [
317
- ...(payload.mediaUrl ? [payload.mediaUrl] : []),
318
- ...(payload.mediaUrls ?? []),
319
- ].filter((u) => Boolean(u));
314
+ const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
320
315
  log?.info?.(`[${account.accountId}] openclaw-clawchat deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`);
321
316
  if (payload.isReasoning) {
322
317
  if (!account.forwardThinking)
@@ -76,9 +76,11 @@ export async function startOpenclawClawlingGateway(params) {
76
76
  // Obtain PluginRuntime from the stored runtime set via setOpenclawClawlingRuntime.
77
77
  const runtime = getOpenclawClawlingRuntime();
78
78
  const accountId = account.accountId;
79
+ log?.info?.(`[${accountId}] openclaw-clawchat runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
79
80
  const client = createOpenclawClawlingClient(account, {
80
81
  ...(params.transport ? { transport: params.transport } : {}),
81
82
  });
83
+ log?.info?.(`[${accountId}] openclaw-clawchat runtime client created`);
82
84
  client.on("state", ({ from, to }) => {
83
85
  log?.info?.(`[${accountId}] openclaw-clawchat state ${from} -> ${to}`);
84
86
  const next = { ...getStatus(), ...mapClawlingStateToStatus(to) };
@@ -138,16 +140,16 @@ export async function startOpenclawClawlingGateway(params) {
138
140
  timestamp: turn.timestamp,
139
141
  ...rt.reply.resolveEnvelopeFormatOptions(cfg),
140
142
  });
143
+ const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
141
144
  const ctxPayload = rt.reply.finalizeInboundContext({
142
145
  Body: body,
143
146
  BodyForAgent: turn.rawBody,
144
147
  RawBody: turn.rawBody,
145
148
  CommandBody: turn.rawBody,
146
- // Clawling v2 routes by chat_id. `senderId` is still preserved as
147
- // structured metadata, but the conversation target must be based on
148
- // `peer.id` so follow-up sends address the active chat, not merely
149
- // the human sender identity.
150
- From: `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`,
149
+ // Clawling v2 routes by chat_id. `OriginatingTo` is what the
150
+ // message tool uses as the implicit current-chat target, so keep it
151
+ // on the conversation id rather than the agent account user id.
152
+ From: conversationTarget,
151
153
  To: `${CHANNEL_ID}:${account.userId}`,
152
154
  SessionKey: route.sessionKey,
153
155
  AccountId: route.accountId ?? accountId,
@@ -160,7 +162,7 @@ export async function startOpenclawClawlingGateway(params) {
160
162
  MessageSidFull: turn.messageId,
161
163
  Timestamp: turn.timestamp,
162
164
  OriginatingChannel: CHANNEL_ID,
163
- OriginatingTo: `${CHANNEL_ID}:${account.userId}`,
165
+ OriginatingTo: conversationTarget,
164
166
  });
165
167
  // Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
166
168
  const inboundPaths = turn.mediaItems.length > 0
@@ -250,7 +252,9 @@ export async function startOpenclawClawlingGateway(params) {
250
252
  // return without throwing (which would make the gateway supervisor
251
253
  // restart us immediately in a tight loop).
252
254
  try {
255
+ log?.info?.(`[${accountId}] openclaw-clawchat runtime calling client.connect()`);
253
256
  await client.connect();
257
+ log?.info?.(`[${accountId}] openclaw-clawchat runtime client.connect() resolved`);
254
258
  }
255
259
  catch (err) {
256
260
  const classified = classifyClawlingClientError(err);
@@ -265,6 +269,7 @@ export async function startOpenclawClawlingGateway(params) {
265
269
  return;
266
270
  }
267
271
  activeClients.set(accountId, client);
272
+ log?.info?.(`[${accountId}] openclaw-clawchat runtime active client registered`);
268
273
  setStatus({
269
274
  ...getStatus(),
270
275
  connected: true,
@@ -273,6 +278,7 @@ export async function startOpenclawClawlingGateway(params) {
273
278
  });
274
279
  log?.info?.(`[${accountId}] openclaw-clawchat connected`);
275
280
  await waitUntilAbort(abortSignal, async () => {
281
+ log?.info?.(`[${accountId}] openclaw-clawchat runtime abort received; closing client`);
276
282
  activeClients.delete(accountId);
277
283
  client.close();
278
284
  setStatus({
package/dist/src/tools.js CHANGED
@@ -118,8 +118,8 @@ export function registerOpenclawClawlingTools(api) {
118
118
  function buildClient() {
119
119
  const acct = resolveCurrent();
120
120
  // `baseUrl` always resolves via the built-in default in config.ts, so we
121
- // only need to gate on `token` here (which is populated by `openclaw
122
- // channel login`).
121
+ // only need to gate on `token` here (which is populated by ClawChat
122
+ // activation/login).
123
123
  if (!acct.token) {
124
124
  return { ok: false, error: configError("openclaw-clawchat: token is required") };
125
125
  }
package/index.ts CHANGED
@@ -1,31 +1,19 @@
1
- // import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
2
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
3
2
  import { openclawClawlingPlugin } from "./src/channel.ts";
4
3
  import { registerOpenclawClawlingCommands } from "./src/commands.ts";
4
+ import { openclawClawlingConfigSchema } from "./src/config.ts";
5
5
  import { setOpenclawClawlingRuntime } from "./src/runtime.ts";
6
6
  import { registerOpenclawClawlingTools } from "./src/tools.ts";
7
- import { openclawClawlingConfigSchema } from "./src/config.ts";
8
7
 
9
- export default {
8
+ export default defineChannelPluginEntry({
10
9
  id: "openclaw-clawchat",
11
10
  name: "Clawling Chat",
12
11
  description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
13
- configSchema: openclawClawlingConfigSchema,
14
- register(api: OpenClawPluginApi) {
15
- setOpenclawClawlingRuntime(api.runtime);
16
- api.registerChannel({ plugin: openclawClawlingPlugin });
12
+ plugin: openclawClawlingPlugin,
13
+ configSchema: { schema: openclawClawlingConfigSchema },
14
+ setRuntime: setOpenclawClawlingRuntime,
15
+ registerFull(api) {
17
16
  registerOpenclawClawlingCommands(api);
18
17
  registerOpenclawClawlingTools(api);
19
- }
20
- }
21
-
22
- // export default defineChannelPluginEntry({
23
- // id: "openclaw-clawchat",
24
- // name: "Clawling Chat",
25
- // description: "Clawling Chat Protocol v2 channel plugin (chat-sdk)",
26
- // plugin: openclawClawlingPlugin,
27
- // setRuntime: setOpenclawClawlingRuntime,
28
- // registerFull(api) {
29
- // registerOpenclawClawlingTools(api);
30
- // },
31
- // });
18
+ },
19
+ });
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-clawchat",
3
+ "kind": "channel",
3
4
  "channels": ["openclaw-clawchat"],
4
5
  "skills": ["./skills"],
5
6
  "activation": {
@@ -7,6 +8,15 @@
7
8
  "onChannels": ["openclaw-clawchat"],
8
9
  "onCommands": ["clawchat-login"]
9
10
  },
11
+ "channelEnvVars": {
12
+ "openclaw-clawchat": [
13
+ "CLAWCHAT_TOKEN",
14
+ "CLAWCHAT_USER_ID",
15
+ "CLAWCHAT_REFRESH_TOKEN",
16
+ "CLAWCHAT_BASE_URL",
17
+ "CLAWCHAT_WEBSOCKET_URL"
18
+ ]
19
+ },
10
20
  "commandAliases": [
11
21
  { "name": "clawchat-login", "kind": "runtime-slash" }
12
22
  ],
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@newbase-clawchat/openclaw-clawchat",
3
- "version": "2026.4.30",
3
+ "version": "2026.5.4-2",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "files": [
6
6
  "dist",
7
7
  "index.ts",
8
+ "setup-entry.ts",
8
9
  "src",
9
10
  "skills",
10
11
  "openclaw.plugin.json",
@@ -15,6 +16,8 @@
15
16
  "build": "tsc -p tsconfig.build.json",
16
17
  "test": "vitest",
17
18
  "test:e2e:install-clawchat-plugin": "bash .e2e/run-install-clawchat-plugin-e2e.sh",
19
+ "test:e2e:install-clawchat-plugin:agent": "bash .e2e/run-install-clawchat-plugin-agent-e2e.sh",
20
+ "test:e2e:install-clawchat-plugin:agent:smoke": "node --test .e2e/run-install-clawchat-plugin-agent-e2e.test.mjs",
18
21
  "test:e2e:install-clawchat-plugin:smoke": "node --test .e2e/run-install-clawchat-plugin-e2e.test.mjs",
19
22
  "dev:openclaw-source": "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
20
23
  "prepack": "npm run build",
@@ -28,12 +31,12 @@
28
31
  },
29
32
  "devDependencies": {
30
33
  "@types/node": "^25.5.0",
31
- "openclaw": "2026.4.29",
34
+ "openclaw": "2026.5.4",
32
35
  "typescript": "^5.4.0",
33
36
  "vitest": "^4.1.5"
34
37
  },
35
38
  "peerDependencies": {
36
- "openclaw": "^2026.4.29"
39
+ "openclaw": ">=2026.5.4"
37
40
  },
38
41
  "peerDependenciesMeta": {
39
42
  "openclaw": {
@@ -50,6 +53,12 @@
50
53
  "runtimeExtensions": [
51
54
  "./dist/index.js"
52
55
  ],
56
+ "setupEntry": "./setup-entry.ts",
57
+ "runtimeSetupEntry": "./dist/setup-entry.js",
58
+ "plugin": {
59
+ "id": "openclaw-clawchat",
60
+ "label": "Clawling Chat"
61
+ },
53
62
  "channel": {
54
63
  "id": "openclaw-clawchat",
55
64
  "label": "Clawling Chat",
@@ -57,12 +66,18 @@
57
66
  "docsPath": "/channels/openclaw-clawchat",
58
67
  "docsLabel": "openclaw-clawchat",
59
68
  "blurb": "Clawling Protocol v2 over WebSocket (chat-sdk).",
60
- "order": 110
69
+ "order": 110,
70
+ "cliAddOptions": [
71
+ {
72
+ "flags": "--token <invite-code>",
73
+ "description": "ClawChat invite code"
74
+ }
75
+ ]
61
76
  },
62
77
  "install": {
63
78
  "npmSpec": "@newbase-clawchat/openclaw-clawchat",
64
79
  "defaultChoice": "npm",
65
- "minHostVersion": ">=2026.4.29"
80
+ "minHostVersion": ">=2026.5.4"
66
81
  }
67
82
  }
68
83
  }
package/setup-entry.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { openclawClawlingSetupPlugin } from "./src/channel.setup.ts";
3
+
4
+ export default defineSetupPluginEntry(openclawClawlingSetupPlugin);
@@ -8,31 +8,36 @@ description: |
8
8
 
9
9
  Activation should use the `clawchat_activate` tool when that tool is available. The tool runs the same invite-code login runtime as OpenClaw channel login and persists credentials into `channels.openclaw-clawchat`.
10
10
 
11
- If the tool is not available because the Gateway has not loaded this plugin yet, fall back to OpenClaw channel login. Do not edit config files manually and do not try to execute slash commands from this skill.
11
+ If the tool is not available because the Gateway has not loaded this plugin yet, restart the Gateway after install/update and retry from a new turn. Do not edit config files manually. On OpenClaw 2026.5.5, npm-installed third-party channels may be loaded by the Gateway but still missing from the `channels add` CLI catalog, which fails with `Unknown channel: openclaw-clawchat`. In that case, ask the user to send the runtime slash command `/clawchat-login A1B2C3` in the chat where OpenClaw is running.
12
12
 
13
13
  ## Workflow
14
14
 
15
15
  1. Check whether the user is trying to activate or log in ClawChat.
16
16
  2. If `clawchat_activate` is available and the invite code is known, call it with the invite code.
17
17
  3. If the invite code is not known, ask the user for it before calling `clawchat_activate`.
18
- 4. If `clawchat_activate` is not available, execute the channel login command:
18
+ 4. If `clawchat_activate` is not available but the plugin is loaded in a chat session, ask the user to send this runtime slash command in that chat:
19
+
20
+ ```text
21
+ /clawchat-login A1B2C3
22
+ ```
23
+ 5. If using a host where `openclaw channels add --help` lists `openclaw-clawchat`, terminal activation can also use:
19
24
 
20
25
  ```bash
21
- openclaw channels login --channel openclaw-clawchat
26
+ openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
22
27
  ```
23
- 5. Do not append the invite code to the command; `openclaw channels login` has no code argument.
24
- 6. When the command prompts for an invite code, provide the user's invite code if one was already present, otherwise ask the user for it.
25
- 7. After activation succeeds, rely on OpenClaw's `channels.*` config hot reload when the plugin is already loaded. Verify channel status if available:
28
+ 6. The token argument is the ClawChat invite code on this setup path. Do not use `channels add` on OpenClaw 2026.5.5 when it reports `Unknown channel: openclaw-clawchat`.
29
+ 7. Use `openclaw channels login --channel openclaw-clawchat` only to refresh credentials later after the channel already exists and the host recognizes the channel.
30
+ 8. After activation succeeds, verify channel status if available:
26
31
 
27
32
  ```bash
28
33
  openclaw channels status --probe
29
34
  ```
30
- 8. Execute the Gateway restart command only when the plugin was just installed/updated, the tool was unavailable because the running Gateway had not loaded the plugin, config reload is disabled, or the probe does not become healthy:
35
+ 9. Execute the Gateway restart command when the plugin was just installed/updated, the tool was unavailable because the running Gateway had not loaded the plugin, config reload is disabled, or the probe does not become healthy:
31
36
 
32
37
  ```bash
33
38
  openclaw gateway restart
34
39
  ```
35
- 9. Tell the user activation completes after login succeeds and either config hot reload/probe succeeds or Gateway restart succeeds.
40
+ 10. Tell the user activation completes after `clawchat_activate`, `/clawchat-login`, or supported channel add/login succeeds and the status probe succeeds after any required Gateway restart.
36
41
 
37
42
  ## Trigger Examples
38
43
 
@@ -42,6 +47,6 @@ openclaw gateway restart
42
47
  - `绑定 ClawChat,邀请码 A1B2C3`
43
48
  - `激活 ClawChat`
44
49
 
45
- Do not ask the user to enter a bare ClawChat command. If activation is requested and `clawchat_activate` is available, call that tool yourself. If the tool is unavailable, execute `openclaw channels login --channel openclaw-clawchat` yourself, then probe channel status and restart the Gateway only when needed.
50
+ Do not ask the user to enter a bare ClawChat command. If activation is requested and `clawchat_activate` is available, call that tool yourself. If the tool is unavailable but the runtime slash command is available in chat, ask the user to send `/clawchat-login A1B2C3`. If the host's CLI channel catalog supports `openclaw-clawchat`, `openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"` is also acceptable; then probe channel status and perform a real Gateway restart if the channel is configured but stopped or disconnected.
46
51
 
47
- When the user asks to activate ClawChat without including a code, ask for the invite code before calling `clawchat_activate`; if falling back to channel login, provide the code when the command needs it.
52
+ When the user asks to activate ClawChat without including a code, ask for the invite code before calling `clawchat_activate` or falling back to channel add.
@@ -88,6 +88,8 @@ describe("openclaw-clawchat channel outbound", () => {
88
88
  uploadOutboundMediaMock.mockResolvedValue([
89
89
  { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
90
90
  ]);
91
+ const mediaReadFile = vi.fn(async () => Buffer.from("host-read"));
92
+ const mediaAccess = { localRoots: ["/tmp"], workspaceDir: "/workspace" };
91
93
 
92
94
  const { openclawClawlingOutbound } = await import("./outbound.ts");
93
95
  const result = await openclawClawlingOutbound.sendMedia!({
@@ -105,7 +107,9 @@ describe("openclaw-clawchat channel outbound", () => {
105
107
  to: "cc:group:room-1",
106
108
  text: "caption",
107
109
  mediaUrl: "/tmp/photo.png",
110
+ mediaAccess,
108
111
  mediaLocalRoots: ["/tmp"],
112
+ mediaReadFile,
109
113
  });
110
114
 
111
115
  expect(createApiClientMock).toHaveBeenCalledWith({
@@ -116,7 +120,9 @@ describe("openclaw-clawchat channel outbound", () => {
116
120
  expect(uploadOutboundMediaMock).toHaveBeenCalledWith(["/tmp/photo.png"], {
117
121
  apiClient,
118
122
  runtime,
123
+ mediaAccess,
119
124
  mediaLocalRoots: ["/tmp"],
125
+ mediaReadFile,
120
126
  });
121
127
  expect(client.sendMessage).toHaveBeenCalledWith(
122
128
  expect.objectContaining({
@@ -0,0 +1,156 @@
1
+ import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
2
+ import type { ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
3
+ import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
4
+ import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
5
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
6
+ import {
7
+ createComputedAccountStatusAdapter,
8
+ createDefaultChannelRuntimeState,
9
+ } from "openclaw/plugin-sdk/status-helpers";
10
+ import {
11
+ CHANNEL_ID,
12
+ listOpenclawClawlingAccountIds,
13
+ mergeOpenclawClawchatToolAllow,
14
+ openclawClawlingConfigSchema,
15
+ resolveOpenclawClawlingAccount,
16
+ type ResolvedOpenclawClawlingAccount,
17
+ } from "./config.ts";
18
+ import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
19
+
20
+ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
21
+ sectionKey: CHANNEL_ID,
22
+ resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
23
+ listAccountIds: () => listOpenclawClawlingAccountIds(),
24
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
25
+ deleteMode: "clear-fields",
26
+ clearBaseFields: [
27
+ "websocketUrl",
28
+ "baseUrl",
29
+ "token",
30
+ "userId",
31
+ "replyMode",
32
+ "forwardThinking",
33
+ "forwardToolCalls",
34
+ "richInteractions",
35
+ "enabled",
36
+ ],
37
+ resolveAllowFrom: (account) => account.allowFrom,
38
+ formatAllowFrom: () => [],
39
+ });
40
+
41
+ /**
42
+ * Invite-code setup adapter used by OpenClaw setup surfaces.
43
+ *
44
+ * `channels add --token` passes the invite code as setup input. The first
45
+ * config write only enables the channel; `afterAccountConfigWritten` exchanges
46
+ * the invite code and persists token/userId through the host runtime mutator.
47
+ */
48
+ const setupAdapter: NonNullable<ChannelPlugin<ResolvedOpenclawClawlingAccount>["setup"]> = {
49
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
50
+ validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
51
+ const inviteCode =
52
+ typeof input.code === "string" && input.code.trim()
53
+ ? input.code.trim()
54
+ : typeof input.token === "string"
55
+ ? input.token.trim()
56
+ : "";
57
+ if (!inviteCode) {
58
+ return "ClawChat invite code is required.";
59
+ }
60
+ return null;
61
+ },
62
+ applyAccountConfig: ({ cfg }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
63
+ const channels = (cfg.channels ?? {}) as Record<string, unknown>;
64
+ const current = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
65
+ return mergeOpenclawClawchatToolAllow({
66
+ ...cfg,
67
+ channels: {
68
+ ...channels,
69
+ [CHANNEL_ID]: { ...current, enabled: true },
70
+ },
71
+ });
72
+ },
73
+ afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
74
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten invoked");
75
+ const code =
76
+ typeof input.code === "string" && input.code.trim()
77
+ ? input.code.trim()
78
+ : typeof input.token === "string"
79
+ ? input.token.trim()
80
+ : "";
81
+ if (!code) {
82
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten skipped: empty invite code");
83
+ return;
84
+ }
85
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
86
+ await runOpenclawClawlingLogin({
87
+ cfg,
88
+ accountId: null,
89
+ runtime: { log: (message: string) => runtime.log(message) },
90
+ readInviteCode: async () => code,
91
+ mutateConfigFile: mutateConfigFile as OpenclawClawchatMutateConfigFile,
92
+ });
93
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten completed");
94
+ },
95
+ };
96
+
97
+ type OpenclawClawlingSetupPlugin = Pick<
98
+ ChannelPlugin<ResolvedOpenclawClawlingAccount>,
99
+ "id" | "meta" | "capabilities" | "reload" | "configSchema" | "config" | "setup" | "status"
100
+ >;
101
+
102
+ export const openclawClawlingSetupPlugin: OpenclawClawlingSetupPlugin = {
103
+ id: CHANNEL_ID,
104
+ meta: {
105
+ id: CHANNEL_ID,
106
+ label: "Clawling Chat",
107
+ selectionLabel: "Clawling Chat",
108
+ docsPath: "/channels/openclaw-clawchat",
109
+ docsLabel: "openclaw-clawchat",
110
+ blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
111
+ order: 110,
112
+ },
113
+ capabilities: {
114
+ chatTypes: ["direct", "group"],
115
+ media: true,
116
+ reactions: false,
117
+ threads: false,
118
+ polls: false,
119
+ blockStreaming: true,
120
+ },
121
+ reload: {
122
+ configPrefixes: [`channels.${CHANNEL_ID}`],
123
+ },
124
+ configSchema: {
125
+ schema: openclawClawlingConfigSchema,
126
+ },
127
+ config: {
128
+ ...configAdapter,
129
+ isConfigured: (account) => account.configured,
130
+ describeAccount: (account) => ({
131
+ accountId: account.accountId,
132
+ name: account.name,
133
+ enabled: account.enabled,
134
+ configured: account.configured,
135
+ }),
136
+ },
137
+ setup: setupAdapter,
138
+ status: createComputedAccountStatusAdapter<ResolvedOpenclawClawlingAccount>({
139
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
140
+ connected: false,
141
+ lastInboundAt: null,
142
+ lastOutboundAt: null,
143
+ }),
144
+ resolveAccountSnapshot: ({ account }) => ({
145
+ accountId: account.accountId,
146
+ name: account.name,
147
+ enabled: account.enabled,
148
+ configured: account.configured,
149
+ extra: {
150
+ websocketUrl: account.websocketUrl || null,
151
+ baseUrl: account.baseUrl || null,
152
+ userId: account.userId || null,
153
+ },
154
+ }),
155
+ }),
156
+ };
@@ -28,9 +28,12 @@ describe("openclaw-clawchat plugin", () => {
28
28
  expect(
29
29
  validate!({ cfg: {}, accountId: "default", input: { code: " " } }),
30
30
  ).toMatch(/invite code is required/i);
31
+ expect(
32
+ validate!({ cfg: {}, accountId: "default", input: { token: " " } }),
33
+ ).toMatch(/invite code is required/i);
31
34
  });
32
35
 
33
- it("setup.validateInput passes when code is present", () => {
36
+ it("setup.validateInput passes when code or token is present", () => {
34
37
  const validate = openclawClawlingPlugin.setup?.validateInput as (args: {
35
38
  cfg: unknown;
36
39
  accountId: string;
@@ -39,6 +42,9 @@ describe("openclaw-clawchat plugin", () => {
39
42
  expect(
40
43
  validate({ cfg: {}, accountId: "default", input: { code: "INV-XXXX" } }),
41
44
  ).toBeNull();
45
+ expect(
46
+ validate({ cfg: {}, accountId: "default", input: { token: "INV-XXXX" } }),
47
+ ).toBeNull();
42
48
  });
43
49
 
44
50
  it("setup.applyAccountConfig marks the channel enabled without touching credentials", () => {
@@ -117,10 +123,32 @@ describe("openclaw-clawchat plugin", () => {
117
123
  expect(normalized).toBe("usr_01KPN6SQFQEGM9HR11CHRHPMMT");
118
124
  });
119
125
 
126
+ it("declares ClawChat target prefixes for OpenClaw channel selection", () => {
127
+ expect(openclawClawlingPlugin.messaging?.targetPrefixes).toEqual([
128
+ "cc",
129
+ "clawchat",
130
+ "openclaw-clawchat",
131
+ ]);
132
+ });
133
+
120
134
  it("parses openclaw-clawchat target prefix as a direct recipient", () => {
121
135
  expect(parseOpenclawRecipient("openclaw-clawchat:usr_01KPN6SQFQEGM9HR11CHRHPMMT")).toEqual({
122
136
  chatId: "usr_01KPN6SQFQEGM9HR11CHRHPMMT",
123
137
  chatType: "direct",
124
138
  });
125
139
  });
140
+
141
+ it("parses host-normalized group targets as group recipients", () => {
142
+ expect(parseOpenclawRecipient("group:cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW")).toEqual({
143
+ chatId: "cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW",
144
+ chatType: "group",
145
+ });
146
+ });
147
+
148
+ it("parses host-normalized direct targets as direct recipients", () => {
149
+ expect(parseOpenclawRecipient("direct:cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW")).toEqual({
150
+ chatId: "cnv_01KR2NBGTKEQ0S0CAYCEQP3YPW",
151
+ chatType: "direct",
152
+ });
153
+ });
126
154
  });