@clawling/clawchat-plugin-openclaw 2026.5.12-28

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