@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13

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 (85) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +121 -19
  3. package/dist/index.js +10 -19
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +78 -10
  6. package/dist/src/api-types.test-d.js +10 -0
  7. package/dist/src/channel.js +25 -156
  8. package/dist/src/channel.setup.js +120 -0
  9. package/dist/src/client.js +37 -41
  10. package/dist/src/config.js +75 -17
  11. package/dist/src/inbound.js +79 -61
  12. package/dist/src/login.runtime.js +84 -19
  13. package/dist/src/media-runtime.js +8 -8
  14. package/dist/src/message-mapper.js +1 -1
  15. package/dist/src/mock-transport.js +31 -0
  16. package/dist/src/outbound.js +410 -26
  17. package/dist/src/protocol-types.js +63 -0
  18. package/dist/src/protocol-types.typecheck.js +1 -0
  19. package/dist/src/protocol.js +2 -7
  20. package/dist/src/reply-dispatcher.js +157 -54
  21. package/dist/src/runtime.js +795 -119
  22. package/dist/src/storage.js +689 -0
  23. package/dist/src/tools-schema.js +98 -16
  24. package/dist/src/tools.js +422 -135
  25. package/dist/src/ws-alignment.js +178 -0
  26. package/dist/src/ws-client.js +588 -0
  27. package/dist/src/ws-log.js +19 -0
  28. package/index.ts +10 -22
  29. package/openclaw.plugin.json +37 -2
  30. package/package.json +17 -4
  31. package/setup-entry.ts +4 -0
  32. package/skills/clawchat/SKILL.md +88 -0
  33. package/src/api-client.test.ts +274 -14
  34. package/src/api-client.ts +138 -23
  35. package/src/api-types.test-d.ts +12 -0
  36. package/src/api-types.ts +90 -4
  37. package/src/buffered-stream.test.ts +14 -12
  38. package/src/buffered-stream.ts +1 -1
  39. package/src/channel.outbound.test.ts +269 -60
  40. package/src/channel.setup.ts +146 -0
  41. package/src/channel.test.ts +130 -24
  42. package/src/channel.ts +30 -186
  43. package/src/client.test.ts +197 -11
  44. package/src/client.ts +50 -57
  45. package/src/config.test.ts +108 -6
  46. package/src/config.ts +95 -24
  47. package/src/inbound.test.ts +288 -37
  48. package/src/inbound.ts +96 -84
  49. package/src/login.runtime.test.ts +347 -13
  50. package/src/login.runtime.ts +105 -23
  51. package/src/manifest.test.ts +146 -74
  52. package/src/media-runtime.test.ts +57 -2
  53. package/src/media-runtime.ts +26 -17
  54. package/src/message-mapper.test.ts +2 -2
  55. package/src/message-mapper.ts +2 -2
  56. package/src/mock-transport.test.ts +35 -0
  57. package/src/mock-transport.ts +38 -0
  58. package/src/outbound.test.ts +694 -73
  59. package/src/outbound.ts +484 -31
  60. package/src/plugin-entry.test.ts +1 -0
  61. package/src/protocol-types.test.ts +69 -0
  62. package/src/protocol-types.ts +296 -0
  63. package/src/protocol-types.typecheck.ts +89 -0
  64. package/src/protocol.test.ts +1 -6
  65. package/src/protocol.ts +2 -7
  66. package/src/reply-dispatcher.test.ts +819 -119
  67. package/src/reply-dispatcher.ts +202 -60
  68. package/src/runtime.test.ts +2120 -41
  69. package/src/runtime.ts +935 -142
  70. package/src/scripts.test.ts +85 -0
  71. package/src/storage.test.ts +793 -0
  72. package/src/storage.ts +1095 -0
  73. package/src/streaming.test.ts +9 -8
  74. package/src/streaming.ts +1 -1
  75. package/src/tools-schema.ts +148 -20
  76. package/src/tools.test.ts +377 -50
  77. package/src/tools.ts +574 -154
  78. package/src/ws-alignment.test.ts +103 -0
  79. package/src/ws-alignment.ts +275 -0
  80. package/src/ws-client.test.ts +1218 -0
  81. package/src/ws-client.ts +662 -0
  82. package/src/ws-log.test.ts +32 -0
  83. package/src/ws-log.ts +31 -0
  84. package/skills/clawchat-account-tools/SKILL.md +0 -26
  85. package/skills/clawchat-activate/SKILL.md +0 -47
@@ -1,10 +1,46 @@
1
+ import { EventEmitter } from "node:events";
1
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { Envelope, MessageAckPayload } from "./protocol-types.ts";
2
4
 
3
5
  const getClientMock = vi.hoisted(() => vi.fn());
4
6
  const getRuntimeMock = vi.hoisted(() => vi.fn());
5
7
  const waitForClientMock = vi.hoisted(() => vi.fn());
6
8
  const uploadOutboundMediaMock = vi.hoisted(() => vi.fn());
7
9
  const createApiClientMock = vi.hoisted(() => vi.fn());
10
+ const getStoreMock = vi.hoisted(() => vi.fn());
11
+ const clawChatDbPathForStateDirMock = vi.hoisted(() => vi.fn((stateDir: string) => `${stateDir}/clawchat.sqlite`));
12
+
13
+ function mockClient() {
14
+ let trace = 0;
15
+ const sent: string[] = [];
16
+ const client = Object.assign(new EventEmitter(), {
17
+ sent,
18
+ state: "connected",
19
+ nextTraceId: vi.fn(() => `trace-${++trace}`),
20
+ sendWire: vi.fn((wire: string) => {
21
+ sent.push(wire);
22
+ }),
23
+ typing: vi.fn(),
24
+ emitRaw: vi.fn(),
25
+ sendRawEnvelope: vi.fn(),
26
+ });
27
+ Object.defineProperty(client, "transportState", { get: () => "open" });
28
+ return client;
29
+ }
30
+
31
+ function emitAck(
32
+ client: ReturnType<typeof mockClient>,
33
+ traceId: string,
34
+ payload: MessageAckPayload,
35
+ ) {
36
+ client.emit("raw", {
37
+ version: "2",
38
+ event: "message.ack",
39
+ trace_id: traceId,
40
+ emitted_at: Date.now(),
41
+ payload,
42
+ } satisfies Envelope<MessageAckPayload>);
43
+ }
8
44
 
9
45
  vi.mock("./runtime.ts", () => ({
10
46
  getOpenclawClawlingClient: getClientMock,
@@ -21,28 +57,44 @@ vi.mock("./api-client.ts", () => ({
21
57
  createOpenclawClawlingApiClient: createApiClientMock,
22
58
  }));
23
59
 
60
+ vi.mock("./storage.ts", () => ({
61
+ clawChatDbPathForStateDir: clawChatDbPathForStateDirMock,
62
+ getClawChatStore: getStoreMock,
63
+ }));
64
+
65
+ function configureClaim(result: true | false | null, runtimeExtras: Record<string, unknown> = {}) {
66
+ const claimMessageOnce = vi.fn(() => result);
67
+ const runtime = {
68
+ ...runtimeExtras,
69
+ state: { resolveStateDir: vi.fn(() => "/state") },
70
+ };
71
+ getRuntimeMock.mockReturnValue(runtime);
72
+ getStoreMock.mockReturnValue({ claimMessageOnce });
73
+ return { claimMessageOnce, runtime };
74
+ }
75
+
24
76
  describe("openclaw-clawchat channel outbound", () => {
25
77
  beforeEach(() => {
78
+ vi.useRealTimers();
26
79
  vi.resetModules();
27
80
  getClientMock.mockReset();
28
81
  getRuntimeMock.mockReset();
29
82
  waitForClientMock.mockReset();
30
83
  uploadOutboundMediaMock.mockReset();
31
84
  createApiClientMock.mockReset();
85
+ getStoreMock.mockReset();
86
+ clawChatDbPathForStateDirMock.mockClear();
87
+ clawChatDbPathForStateDirMock.mockImplementation((stateDir: string) => `${stateDir}/clawchat.sqlite`);
32
88
  });
33
89
 
34
- it("sendText waits for client activation when no active client exists yet", async () => {
35
- const client = {
36
- sendMessage: vi.fn().mockResolvedValue({
37
- payload: { message_id: "m-2", accepted_at: 456 },
38
- trace_id: "trace-2",
39
- }),
40
- };
90
+ it("sendText claims a local message_id before sending", async () => {
91
+ const client = mockClient();
41
92
  getClientMock.mockReturnValue(undefined);
42
93
  waitForClientMock.mockResolvedValue(client);
94
+ const { claimMessageOnce } = configureClaim(true);
43
95
 
44
96
  const { openclawClawlingOutbound } = await import("./outbound.ts");
45
- const result = await openclawClawlingOutbound.sendText!({
97
+ const send = openclawClawlingOutbound.sendText!({
46
98
  cfg: {
47
99
  channels: {
48
100
  "openclaw-clawchat": {
@@ -58,39 +110,119 @@ describe("openclaw-clawchat channel outbound", () => {
58
110
  text: "hello",
59
111
  });
60
112
 
113
+ await vi.waitFor(() => expect(client.sent).toHaveLength(1));
114
+ const frame = JSON.parse(client.sent[0]!) as Envelope;
115
+ const claimedInput = claimMessageOnce.mock.calls[0]?.[0] as { messageId?: string } | undefined;
116
+ const claimedMessageId = claimedInput?.messageId ?? "server-id";
117
+ emitAck(client, frame.trace_id, { message_id: claimedMessageId, accepted_at: 456 });
118
+ const result = await send;
119
+
61
120
  expect(waitForClientMock).toHaveBeenCalledWith("default");
62
- expect(client.sendMessage).toHaveBeenCalledWith(
63
- expect.objectContaining({
64
- chat_id: "user-1",
65
- body: { fragments: [{ kind: "text", text: "hello" }] },
66
- }),
67
- );
68
- expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
121
+ expect(getStoreMock).toHaveBeenCalledWith({ dbPath: "/state/clawchat.sqlite" });
122
+ expect(claimedInput).toEqual(expect.objectContaining({
123
+ kind: "message",
124
+ direction: "outbound",
125
+ eventType: "message.send",
126
+ messageId: claimedMessageId,
127
+ text: "hello",
128
+ }));
129
+ expect(frame).toMatchObject({
130
+ event: "message.send",
131
+ chat_id: "user-1",
132
+ payload: {
133
+ message_id: claimedMessageId,
134
+ message: { body: { fragments: [{ kind: "text", text: "hello" }] } },
135
+ },
136
+ });
137
+ expect(frame).not.toHaveProperty("chat_type");
69
138
  expect(result).toEqual({
70
139
  channel: "openclaw-clawchat",
71
140
  to: "cc:user-1",
72
- messageId: "m-2",
141
+ messageId: claimedMessageId,
73
142
  });
74
143
  });
75
144
 
76
- it("sendMedia uploads mediaUrl and sends resulting fragments", async () => {
77
- const client = {
78
- sendMessage: vi.fn().mockResolvedValue({
79
- payload: { message_id: "m-1", accepted_at: 123 },
80
- trace_id: "trace-1",
81
- }),
82
- };
83
- const runtime = { media: { loadWebMedia: vi.fn() } };
145
+ it("sendText rejects empty text instead of returning an unsent message id", async () => {
146
+ const client = mockClient();
147
+ getClientMock.mockReturnValue(client);
148
+ const { claimMessageOnce } = configureClaim(true);
149
+
150
+ const { openclawClawlingOutbound } = await import("./outbound.ts");
151
+ const send = openclawClawlingOutbound.sendText!({
152
+ cfg: {
153
+ channels: {
154
+ "openclaw-clawchat": {
155
+ enabled: true,
156
+ websocketUrl: "ws://t",
157
+ baseUrl: "https://api.example.com",
158
+ token: "tk",
159
+ userId: "agent-1",
160
+ },
161
+ },
162
+ } as never,
163
+ to: "cc:user-1",
164
+ text: " ",
165
+ });
166
+
167
+ await expect(send).rejects.toThrow("openclaw-clawchat sendText requires non-empty text");
168
+ expect(claimMessageOnce).not.toHaveBeenCalled();
169
+ expect(client.sent).toHaveLength(0);
170
+ });
171
+
172
+ it("sendText rejects when the storage claim is duplicate or unavailable", async () => {
173
+ for (const [claimResult, errorMessage] of [
174
+ [false, "openclaw-clawchat outbound duplicate claim; message not sent"],
175
+ [null, "openclaw-clawchat outbound message claim failed"],
176
+ ] as const) {
177
+ const client = mockClient();
178
+ getClientMock.mockReturnValue(client);
179
+ const { claimMessageOnce } = configureClaim(claimResult);
180
+ const { openclawClawlingOutbound } = await import("./outbound.ts");
181
+
182
+ const send = openclawClawlingOutbound.sendText!({
183
+ cfg: {
184
+ channels: {
185
+ "openclaw-clawchat": {
186
+ enabled: true,
187
+ websocketUrl: "ws://t",
188
+ baseUrl: "https://api.example.com",
189
+ token: "tk",
190
+ userId: "agent-1",
191
+ },
192
+ },
193
+ } as never,
194
+ to: "cc:user-1",
195
+ text: "hello",
196
+ });
197
+ await expect(send).rejects.toThrow(errorMessage);
198
+
199
+ expect(claimMessageOnce).toHaveBeenCalledWith(expect.objectContaining({
200
+ kind: "message",
201
+ direction: "outbound",
202
+ }));
203
+ expect(client.sent).toHaveLength(0);
204
+ vi.resetModules();
205
+ getClientMock.mockReset();
206
+ getRuntimeMock.mockReset();
207
+ getStoreMock.mockReset();
208
+ }
209
+ });
210
+
211
+ it("sendMedia claims a local message_id before sending resulting fragments", async () => {
212
+ const client = mockClient();
213
+ const runtimeExtras = { media: { loadWebMedia: vi.fn() } };
84
214
  const apiClient = { uploadMedia: vi.fn() };
85
215
  getClientMock.mockReturnValue(client);
86
- getRuntimeMock.mockReturnValue(runtime);
216
+ const { claimMessageOnce, runtime } = configureClaim(true, runtimeExtras);
87
217
  createApiClientMock.mockReturnValue(apiClient);
88
218
  uploadOutboundMediaMock.mockResolvedValue([
89
219
  { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
90
220
  ]);
221
+ const mediaReadFile = vi.fn(async () => Buffer.from("host-read"));
222
+ const mediaAccess = { localRoots: ["/tmp"], workspaceDir: "/workspace" };
91
223
 
92
224
  const { openclawClawlingOutbound } = await import("./outbound.ts");
93
- const result = await openclawClawlingOutbound.sendMedia!({
225
+ const send = openclawClawlingOutbound.sendMedia!({
94
226
  cfg: {
95
227
  channels: {
96
228
  "openclaw-clawchat": {
@@ -105,9 +237,18 @@ describe("openclaw-clawchat channel outbound", () => {
105
237
  to: "cc:group:room-1",
106
238
  text: "caption",
107
239
  mediaUrl: "/tmp/photo.png",
240
+ mediaAccess,
108
241
  mediaLocalRoots: ["/tmp"],
242
+ mediaReadFile,
109
243
  });
110
244
 
245
+ await vi.waitFor(() => expect(client.sent).toHaveLength(1));
246
+ const frame = JSON.parse(client.sent[0]!) as Envelope;
247
+ const claimedInput = claimMessageOnce.mock.calls[0]?.[0] as { messageId?: string } | undefined;
248
+ const claimedMessageId = claimedInput?.messageId ?? "server-id";
249
+ emitAck(client, frame.trace_id, { message_id: claimedMessageId, accepted_at: 123 });
250
+ const result = await send;
251
+
111
252
  expect(createApiClientMock).toHaveBeenCalledWith({
112
253
  baseUrl: "https://api.example.com",
113
254
  token: "tk",
@@ -116,29 +257,91 @@ describe("openclaw-clawchat channel outbound", () => {
116
257
  expect(uploadOutboundMediaMock).toHaveBeenCalledWith(["/tmp/photo.png"], {
117
258
  apiClient,
118
259
  runtime,
260
+ mediaAccess,
119
261
  mediaLocalRoots: ["/tmp"],
262
+ mediaReadFile,
120
263
  });
121
- expect(client.sendMessage).toHaveBeenCalledWith(
122
- expect.objectContaining({
123
- chat_id: "room-1",
124
- body: {
125
- fragments: [
126
- { kind: "text", text: "caption" },
127
- { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
128
- ],
264
+ expect(claimedInput).toEqual(expect.objectContaining({
265
+ kind: "message",
266
+ direction: "outbound",
267
+ eventType: "message.send",
268
+ messageId: claimedMessageId,
269
+ text: "caption",
270
+ }));
271
+ expect(frame).toMatchObject({
272
+ event: "message.send",
273
+ chat_id: "room-1",
274
+ payload: {
275
+ message_id: claimedMessageId,
276
+ message: {
277
+ body: {
278
+ fragments: [
279
+ { kind: "text", text: "caption" },
280
+ { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
281
+ ],
282
+ },
129
283
  },
130
- }),
131
- );
132
- expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
284
+ },
285
+ });
286
+ expect(frame).not.toHaveProperty("chat_type");
133
287
  expect(result).toEqual({
134
288
  channel: "openclaw-clawchat",
135
289
  to: "cc:group:room-1",
136
- messageId: "m-1",
290
+ messageId: claimedMessageId,
137
291
  });
138
292
  });
139
293
 
294
+ it("sendMedia rejects when the storage claim is duplicate or unavailable", async () => {
295
+ for (const [claimResult, errorMessage] of [
296
+ [false, "openclaw-clawchat outbound duplicate claim; message not sent"],
297
+ [null, "openclaw-clawchat outbound message claim failed"],
298
+ ] as const) {
299
+ const client = mockClient();
300
+ const runtimeExtras = { media: { loadWebMedia: vi.fn() } };
301
+ const apiClient = { uploadMedia: vi.fn() };
302
+ getClientMock.mockReturnValue(client);
303
+ const { claimMessageOnce } = configureClaim(claimResult, runtimeExtras);
304
+ createApiClientMock.mockReturnValue(apiClient);
305
+ uploadOutboundMediaMock.mockResolvedValue([
306
+ { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
307
+ ]);
308
+ const { openclawClawlingOutbound } = await import("./outbound.ts");
309
+
310
+ const send = openclawClawlingOutbound.sendMedia!({
311
+ cfg: {
312
+ channels: {
313
+ "openclaw-clawchat": {
314
+ enabled: true,
315
+ websocketUrl: "ws://t",
316
+ baseUrl: "https://api.example.com",
317
+ token: "tk",
318
+ userId: "agent-1",
319
+ },
320
+ },
321
+ } as never,
322
+ to: "cc:group:room-1",
323
+ text: "caption",
324
+ mediaUrl: "/tmp/photo.png",
325
+ });
326
+ await expect(send).rejects.toThrow(errorMessage);
327
+
328
+ expect(uploadOutboundMediaMock).toHaveBeenCalled();
329
+ expect(claimMessageOnce).toHaveBeenCalledWith(expect.objectContaining({
330
+ kind: "message",
331
+ direction: "outbound",
332
+ }));
333
+ expect(client.sent).toHaveLength(0);
334
+ vi.resetModules();
335
+ getClientMock.mockReset();
336
+ getRuntimeMock.mockReset();
337
+ getStoreMock.mockReset();
338
+ uploadOutboundMediaMock.mockReset();
339
+ createApiClientMock.mockReset();
340
+ }
341
+ });
342
+
140
343
  it("sendMedia rejects missing mediaUrl", async () => {
141
- getClientMock.mockReturnValue({ sendMessage: vi.fn() });
344
+ getClientMock.mockReturnValue(mockClient());
142
345
  const { openclawClawlingOutbound } = await import("./outbound.ts");
143
346
  await expect(
144
347
  openclawClawlingOutbound.sendMedia!({
@@ -160,24 +363,19 @@ describe("openclaw-clawchat channel outbound", () => {
160
363
  });
161
364
 
162
365
  it("sendMedia waits for client activation when no active client exists yet", async () => {
163
- const client = {
164
- sendMessage: vi.fn().mockResolvedValue({
165
- payload: { message_id: "m-3", accepted_at: 789 },
166
- trace_id: "trace-3",
167
- }),
168
- };
169
- const runtime = { media: { loadWebMedia: vi.fn() } };
366
+ const client = mockClient();
367
+ const runtimeExtras = { media: { loadWebMedia: vi.fn() } };
170
368
  const apiClient = { uploadMedia: vi.fn() };
171
369
  getClientMock.mockReturnValue(undefined);
172
370
  waitForClientMock.mockResolvedValue(client);
173
- getRuntimeMock.mockReturnValue(runtime);
371
+ const { claimMessageOnce, runtime } = configureClaim(true, runtimeExtras);
174
372
  createApiClientMock.mockReturnValue(apiClient);
175
373
  uploadOutboundMediaMock.mockResolvedValue([
176
374
  { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
177
375
  ]);
178
376
 
179
377
  const { openclawClawlingOutbound } = await import("./outbound.ts");
180
- const result = await openclawClawlingOutbound.sendMedia!({
378
+ const send = openclawClawlingOutbound.sendMedia!({
181
379
  cfg: {
182
380
  channels: {
183
381
  "openclaw-clawchat": {
@@ -195,23 +393,34 @@ describe("openclaw-clawchat channel outbound", () => {
195
393
  mediaLocalRoots: ["/tmp"],
196
394
  });
197
395
 
396
+ await vi.waitFor(() => expect(client.sent).toHaveLength(1));
397
+ const frame = JSON.parse(client.sent[0]!) as Envelope;
398
+ const claimedInput = claimMessageOnce.mock.calls[0]?.[0] as { messageId?: string } | undefined;
399
+ const claimedMessageId = claimedInput?.messageId ?? "server-id";
400
+ emitAck(client, frame.trace_id, { message_id: claimedMessageId, accepted_at: 789 });
401
+ const result = await send;
402
+
198
403
  expect(waitForClientMock).toHaveBeenCalledWith("default");
199
- expect(client.sendMessage).toHaveBeenCalledWith(
200
- expect.objectContaining({
201
- chat_id: "room-1",
202
- body: {
203
- fragments: [
204
- { kind: "text", text: "caption" },
205
- { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
206
- ],
404
+ expect(frame).toMatchObject({
405
+ event: "message.send",
406
+ chat_id: "room-1",
407
+ payload: {
408
+ message_id: claimedMessageId,
409
+ message: {
410
+ body: {
411
+ fragments: [
412
+ { kind: "text", text: "caption" },
413
+ { kind: "image", url: "https://cdn/uploaded.png", mime: "image/png" },
414
+ ],
415
+ },
207
416
  },
208
- }),
209
- );
210
- expect(client.sendMessage.mock.calls[0][0]).not.toHaveProperty("chat_type");
417
+ },
418
+ });
419
+ expect(frame).not.toHaveProperty("chat_type");
211
420
  expect(result).toEqual({
212
421
  channel: "openclaw-clawchat",
213
422
  to: "cc:group:room-1",
214
- messageId: "m-3",
423
+ messageId: claimedMessageId,
215
424
  });
216
425
  });
217
426
  });
@@ -0,0 +1,146 @@
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
+ openclawClawlingConfigSchema,
14
+ resolveOpenclawClawlingAccount,
15
+ type ResolvedOpenclawClawlingAccount,
16
+ } from "./config.ts";
17
+ import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
18
+
19
+ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
20
+ sectionKey: CHANNEL_ID,
21
+ resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
22
+ listAccountIds: () => listOpenclawClawlingAccountIds(),
23
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
24
+ deleteMode: "clear-fields",
25
+ clearBaseFields: [
26
+ "websocketUrl",
27
+ "baseUrl",
28
+ "token",
29
+ "userId",
30
+ "replyMode",
31
+ "forwardThinking",
32
+ "forwardToolCalls",
33
+ "richInteractions",
34
+ "enabled",
35
+ ],
36
+ resolveAllowFrom: (account) => account.allowFrom,
37
+ formatAllowFrom: () => [],
38
+ });
39
+
40
+ /**
41
+ * Invite-code setup adapter used by OpenClaw setup surfaces.
42
+ *
43
+ * `channels add --token` passes the invite code as setup input. The setup
44
+ * write leaves channel config unchanged; `afterAccountConfigWritten` exchanges
45
+ * the invite code and persists token/userId through the host runtime mutator.
46
+ */
47
+ const setupAdapter: NonNullable<ChannelPlugin<ResolvedOpenclawClawlingAccount>["setup"]> = {
48
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
49
+ validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) => {
50
+ const inviteCode =
51
+ typeof input.code === "string" && input.code.trim()
52
+ ? input.code.trim()
53
+ : typeof input.token === "string"
54
+ ? input.token.trim()
55
+ : "";
56
+ if (!inviteCode) {
57
+ return "ClawChat invite code is required.";
58
+ }
59
+ return null;
60
+ },
61
+ applyAccountConfig: ({ cfg }: {
62
+ cfg: OpenClawConfig;
63
+ accountId: string;
64
+ input: ChannelSetupInput;
65
+ }) => cfg,
66
+ afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
67
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten invoked");
68
+ const code =
69
+ typeof input.code === "string" && input.code.trim()
70
+ ? input.code.trim()
71
+ : typeof input.token === "string"
72
+ ? input.token.trim()
73
+ : "";
74
+ if (!code) {
75
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten skipped: empty invite code");
76
+ return;
77
+ }
78
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
79
+ await runOpenclawClawlingLogin({
80
+ cfg,
81
+ accountId: null,
82
+ runtime: { log: (message: string) => runtime.log(message) },
83
+ readInviteCode: async () => code,
84
+ mutateConfigFile: mutateConfigFile as OpenclawClawchatMutateConfigFile,
85
+ });
86
+ runtime.log("[default] openclaw-clawchat setup afterAccountConfigWritten completed");
87
+ },
88
+ };
89
+
90
+ type OpenclawClawlingSetupPlugin = Pick<
91
+ ChannelPlugin<ResolvedOpenclawClawlingAccount>,
92
+ "id" | "meta" | "capabilities" | "configSchema" | "config" | "setup" | "status"
93
+ >;
94
+
95
+ export const openclawClawlingSetupPlugin: OpenclawClawlingSetupPlugin = {
96
+ id: CHANNEL_ID,
97
+ meta: {
98
+ id: CHANNEL_ID,
99
+ label: "Clawling Chat",
100
+ selectionLabel: "Clawling Chat",
101
+ docsPath: "/channels/openclaw-clawchat",
102
+ docsLabel: "openclaw-clawchat",
103
+ blurb: "ClawChat Protocol v2 over WebSocket.",
104
+ order: 110,
105
+ },
106
+ capabilities: {
107
+ chatTypes: ["direct", "group"],
108
+ media: true,
109
+ reactions: false,
110
+ threads: false,
111
+ polls: false,
112
+ blockStreaming: true,
113
+ },
114
+ configSchema: {
115
+ schema: openclawClawlingConfigSchema,
116
+ },
117
+ config: {
118
+ ...configAdapter,
119
+ isConfigured: (account) => account.configured,
120
+ describeAccount: (account) => ({
121
+ accountId: account.accountId,
122
+ name: account.name,
123
+ enabled: account.enabled,
124
+ configured: account.configured,
125
+ }),
126
+ },
127
+ setup: setupAdapter,
128
+ status: createComputedAccountStatusAdapter<ResolvedOpenclawClawlingAccount>({
129
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
130
+ connected: false,
131
+ lastInboundAt: null,
132
+ lastOutboundAt: null,
133
+ }),
134
+ resolveAccountSnapshot: ({ account }) => ({
135
+ accountId: account.accountId,
136
+ name: account.name,
137
+ enabled: account.enabled,
138
+ configured: account.configured,
139
+ extra: {
140
+ websocketUrl: account.websocketUrl || null,
141
+ baseUrl: account.baseUrl || null,
142
+ userId: account.userId || null,
143
+ },
144
+ }),
145
+ }),
146
+ };