@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,422 @@
1
+ import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
2
+ import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
3
+ import { createOpenclawClawlingApiClient } from "./api-client.js";
4
+ import { uploadOutboundMedia } from "./media-runtime.js";
5
+ import { sendOpenclawClawlingText, } from "./outbound.js";
6
+ import { isClawChatNoopResponseText } from "./profile-prompt.js";
7
+ import { consumeTerminalClawChatSend } from "./terminal-send.js";
8
+ import { openclawLlmContextDebug } from "./llm-context-debug.js";
9
+ const GROUP_OWNER_ATTENTION_TITLE = "requires owner attention";
10
+ function ownerAttentionText(groupId, fallbackText) {
11
+ const body = fallbackText.trim();
12
+ return `ClawChat group ${groupId} ${GROUP_OWNER_ATTENTION_TITLE}.${body ? `\n\n${body}` : ""}`;
13
+ }
14
+ function normalizeReplyErrorText(error) {
15
+ const raw = String(error);
16
+ const retryWrapped = raw.match(/^Error: Retry failed for delivery [^:]+:\s*(.+)$/s);
17
+ if (retryWrapped?.[1]?.trim())
18
+ return retryWrapped[1].trim();
19
+ const retryWrappedBare = raw.match(/^Retry failed for delivery [^:]+:\s*(.+)$/s);
20
+ if (retryWrappedBare?.[1]?.trim())
21
+ return retryWrappedBare[1].trim();
22
+ return raw;
23
+ }
24
+ function isMessagePresentation(value) {
25
+ return Boolean(value &&
26
+ typeof value === "object" &&
27
+ Array.isArray(value.blocks));
28
+ }
29
+ function resolvePresentation(payload) {
30
+ if (isMessagePresentation(payload.presentation))
31
+ return payload.presentation;
32
+ if (payload.interactive)
33
+ return interactiveReplyToPresentation(payload.interactive);
34
+ return undefined;
35
+ }
36
+ function normalizeActionId(value, label, index) {
37
+ const raw = value?.trim() || label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-");
38
+ return raw.replace(/^-+|-+$/g, "") || `action-${index + 1}`;
39
+ }
40
+ function collectPresentationActions(blocks) {
41
+ const actions = [];
42
+ for (const block of blocks) {
43
+ if (block.type === "buttons") {
44
+ for (const button of block.buttons) {
45
+ const value = button.value?.trim();
46
+ const url = button.url?.trim();
47
+ const action = {
48
+ id: normalizeActionId(value ?? url, button.label, actions.length),
49
+ label: button.label,
50
+ ...(button.style ? { style: button.style } : {}),
51
+ ...(value || url ? { payload: { ...(value ? { value } : {}), ...(url ? { url } : {}) } } : {}),
52
+ };
53
+ actions.push(action);
54
+ }
55
+ }
56
+ if (block.type === "select") {
57
+ for (const option of block.options) {
58
+ actions.push({
59
+ id: normalizeActionId(option.value, option.label, actions.length),
60
+ label: option.label,
61
+ style: "secondary",
62
+ payload: { value: option.value },
63
+ });
64
+ }
65
+ }
66
+ }
67
+ return actions;
68
+ }
69
+ function looksLikeApproval(actions, presentation) {
70
+ if (presentation.tone === "warning" || presentation.tone === "danger")
71
+ return true;
72
+ const ids = new Set(actions.map((action) => action.id.toLowerCase()));
73
+ return ids.has("approve") || ids.has("deny") || ids.has("reject");
74
+ }
75
+ function buildRichInteractionFragment(payload) {
76
+ const presentation = resolvePresentation(payload);
77
+ if (!presentation)
78
+ return null;
79
+ const actions = collectPresentationActions(presentation.blocks);
80
+ if (actions.length === 0)
81
+ return null;
82
+ const fallbackText = renderMessagePresentationFallbackText({
83
+ presentation,
84
+ text: payload.text ?? null,
85
+ }).trim();
86
+ if (!fallbackText)
87
+ return null;
88
+ return {
89
+ kind: looksLikeApproval(actions, presentation) ? "approval_request" : "action_card",
90
+ ...(presentation.title?.trim() ? { title: presentation.title.trim() } : {}),
91
+ fallback_text: fallbackText,
92
+ state: "pending",
93
+ actions,
94
+ };
95
+ }
96
+ function resolvePayloadText(payload) {
97
+ const presentation = resolvePresentation(payload);
98
+ if (!presentation)
99
+ return payload.text ?? "";
100
+ return renderMessagePresentationFallbackText({ presentation, text: payload.text ?? null });
101
+ }
102
+ /**
103
+ * Reply dispatcher for clawchat-plugin-openclaw.
104
+ *
105
+ * The plugin intentionally forces complete-message delivery. It sets
106
+ * `disableBlockStreaming: true` in reply options so OpenClaw does not split
107
+ * deliver blocks for this channel. If the host still delivers non-final
108
+ * blocks, the dispatcher buffers or ignores them and only emits materialized
109
+ * `message.send` / `message.reply` frames for the final reply.
110
+ */
111
+ export function createOpenclawClawlingReplyDispatcher(options) {
112
+ const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, log, } = options;
113
+ const isGroupTarget = target.chatType === "group";
114
+ const ownerDirectTarget = () => {
115
+ const ownerUserId = account.ownerUserId?.trim();
116
+ return ownerUserId ? { chatId: ownerUserId, chatType: "direct" } : null;
117
+ };
118
+ const humanDelay = runtime.channel.reply.resolveHumanDelayConfig(cfg, account.userId);
119
+ const buildApiClient = () => {
120
+ if (!account.baseUrl || !account.token)
121
+ return null;
122
+ return createOpenclawClawlingApiClient({
123
+ baseUrl: account.baseUrl,
124
+ token: account.token,
125
+ userId: account.userId,
126
+ });
127
+ };
128
+ async function uploadMediaUrls(urls) {
129
+ if (urls.length === 0)
130
+ return [];
131
+ const apiClient = buildApiClient();
132
+ if (!apiClient) {
133
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw outbound media skipped: baseUrl not configured`);
134
+ return [];
135
+ }
136
+ return await uploadOutboundMedia(urls, { apiClient, runtime, log });
137
+ }
138
+ // ----- Reply state ------------------------------------------------------
139
+ let reasoningText = "";
140
+ let runDone = false;
141
+ let typingActive = false;
142
+ let terminalReplySuppressed = false;
143
+ const outboundEventType = () => (replyCtx ? "message.reply" : "message.send");
144
+ const outboundRaw = () => ({ target, replyCtx: replyCtx ?? null });
145
+ const terminalSendScopeId = options.terminalSendScopeId ?? null;
146
+ const consumeTerminalSend = (reason) => {
147
+ if (terminalReplySuppressed)
148
+ return true;
149
+ if (!terminalSendScopeId)
150
+ return false;
151
+ const terminal = consumeTerminalClawChatSend({
152
+ accountId: account.accountId,
153
+ chatId: target.chatId,
154
+ scopeId: terminalSendScopeId,
155
+ });
156
+ if (!terminal)
157
+ return false;
158
+ terminalReplySuppressed = true;
159
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw suppressing ${reason} reply after terminal tool send msg=${terminal.messageId} to=${target.chatId}`);
160
+ return true;
161
+ };
162
+ const claimOutbound = (eventType, messageId, text, raw) => {
163
+ if (!store || !messageId)
164
+ return null;
165
+ try {
166
+ return store.claimMessageOnce({
167
+ platform: "openclaw",
168
+ accountId: account.accountId,
169
+ kind: "message",
170
+ direction: "outbound",
171
+ eventType,
172
+ chatId: target.chatId,
173
+ messageId,
174
+ text,
175
+ raw,
176
+ });
177
+ }
178
+ catch {
179
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw sqlite outbound claim failed`);
180
+ return null;
181
+ }
182
+ };
183
+ const recordOutbound = (kind, messageId, text) => {
184
+ if (!store || !messageId)
185
+ return;
186
+ try {
187
+ store.insertMessage({
188
+ platform: "openclaw",
189
+ accountId: account.accountId,
190
+ kind,
191
+ direction: "outbound",
192
+ eventType: outboundEventType(),
193
+ chatId: target.chatId,
194
+ messageId,
195
+ text,
196
+ raw: outboundRaw(),
197
+ });
198
+ }
199
+ catch {
200
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw sqlite outbound insert failed; continuing`);
201
+ }
202
+ };
203
+ const recordOutboundAck = (kind, messageId, result) => {
204
+ if (!store || !messageId || !result?.messageId)
205
+ return;
206
+ try {
207
+ store.markMessageAcknowledged?.({
208
+ accountId: account.accountId,
209
+ kind,
210
+ direction: "outbound",
211
+ messageId,
212
+ protocolMessageId: result.messageId,
213
+ ackedAt: result.acceptedAt ?? Date.now(),
214
+ });
215
+ }
216
+ catch {
217
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw sqlite outbound ack update failed`);
218
+ }
219
+ };
220
+ const recordThinkingIfLinked = (messageId) => {
221
+ const thinkingText = reasoningText.trim();
222
+ if (!thinkingText)
223
+ return;
224
+ recordOutbound("thinking", messageId, thinkingText);
225
+ reasoningText = "";
226
+ };
227
+ const mintStaticMessageId = () => `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
228
+ const emitTyping = (isTyping) => {
229
+ if (!isTyping && !typingActive)
230
+ return;
231
+ try {
232
+ client.typing(target.chatId, isTyping);
233
+ typingActive = isTyping;
234
+ }
235
+ catch (error) {
236
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw typing update failed: ${String(error)}`);
237
+ }
238
+ };
239
+ const sendOwnerAttention = async (fallbackText, richFragment = null) => {
240
+ const ownerTarget = ownerDirectTarget();
241
+ if (!ownerTarget) {
242
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason=missing_owner_user_id group=${target.chatId}`);
243
+ return;
244
+ }
245
+ await sendOpenclawClawlingText({
246
+ client,
247
+ account,
248
+ to: ownerTarget,
249
+ text: ownerAttentionText(target.chatId, fallbackText),
250
+ messageId: mintStaticMessageId(),
251
+ ...(richFragment ? { richFragments: [richFragment] } : {}),
252
+ log,
253
+ });
254
+ };
255
+ // ----- Static send ------------------------------------------------------
256
+ const sendStatic = async (text, mediaFragments = [], richFragments = [], options = {}) => {
257
+ if (isClawChatNoopResponseText(text) &&
258
+ mediaFragments.length === 0 &&
259
+ richFragments.length === 0) {
260
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw outbound suppressed: no-reply token`);
261
+ openclawLlmContextDebug.writeSnapshot({
262
+ visibility: "host_event",
263
+ trace: {
264
+ messageId: inboundMessageId ?? "unknown",
265
+ chatId: target.chatId,
266
+ chatType: target.chatType,
267
+ },
268
+ input: { injectedPrompt: "", eventText: "" },
269
+ output: {
270
+ rawModelOutput: text,
271
+ finalAssistantText: text,
272
+ adapterFilteredText: "",
273
+ outboundClawChatMessage: null,
274
+ suppressed: true,
275
+ suppressionReason: "no-reply token",
276
+ },
277
+ });
278
+ return null;
279
+ }
280
+ if (!text.trim() && mediaFragments.length === 0 && richFragments.length === 0)
281
+ return null;
282
+ if (consumeTerminalSend("static"))
283
+ return null;
284
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw sending static text_len=${text.length} media=${mediaFragments.length} rich=${richFragments.length} to=${target.chatId}`);
285
+ const messageId = mintStaticMessageId();
286
+ const raw = { target, replyCtx: replyCtx ?? null, mode: "static" };
287
+ const claimed = options.recordMessage
288
+ ? claimOutbound(outboundEventType(), messageId, text, raw)
289
+ : true;
290
+ if (claimed === false) {
291
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw outbound duplicate skipped msg=${messageId}`);
292
+ return null;
293
+ }
294
+ if (claimed === null) {
295
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw outbound skipped msg=${messageId} reason=claim_unavailable`);
296
+ return null;
297
+ }
298
+ const result = await sendOpenclawClawlingText({
299
+ client,
300
+ account,
301
+ to: target,
302
+ text,
303
+ messageId,
304
+ ...(replyCtx ? { replyCtx } : {}),
305
+ ...(richFragments.length > 0 ? { richFragments } : {}),
306
+ ...(mediaFragments.length > 0 ? { mediaFragments } : {}),
307
+ log,
308
+ });
309
+ recordOutboundAck("message", messageId, result);
310
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw send complete to=${target.chatId}`);
311
+ openclawLlmContextDebug.writeSnapshot({
312
+ visibility: "host_event",
313
+ trace: {
314
+ messageId,
315
+ chatId: target.chatId,
316
+ chatType: target.chatType,
317
+ },
318
+ input: { injectedPrompt: "", eventText: "" },
319
+ output: {
320
+ rawModelOutput: text,
321
+ finalAssistantText: text,
322
+ adapterFilteredText: text,
323
+ outboundClawChatMessage: {
324
+ messageId: result?.messageId ?? messageId,
325
+ chatId: target.chatId,
326
+ chatType: target.chatType,
327
+ },
328
+ suppressed: false,
329
+ suppressionReason: null,
330
+ },
331
+ });
332
+ return result;
333
+ };
334
+ // ----- Dispatcher -------------------------------------------------------
335
+ const base = runtime.channel.reply.createReplyDispatcherWithTyping({
336
+ humanDelay,
337
+ onReplyStart: async () => {
338
+ emitTyping(true);
339
+ reasoningText = "";
340
+ runDone = false;
341
+ },
342
+ deliver: async (payload, info) => {
343
+ if (consumeTerminalSend(info?.kind ?? "unknown"))
344
+ return;
345
+ const richFragment = buildRichInteractionFragment(payload);
346
+ const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
347
+ const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
348
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`);
349
+ if (isGroupTarget && richFragment) {
350
+ if (info?.kind !== "final")
351
+ return;
352
+ await sendOwnerAttention(resolvePayloadText(payload), richFragment);
353
+ return;
354
+ }
355
+ if (payload.isReasoning) {
356
+ if (isGroupTarget || !account.forwardThinking)
357
+ return;
358
+ reasoningText = text;
359
+ return;
360
+ }
361
+ if (info?.kind === "tool")
362
+ return;
363
+ if (info?.kind === "final") {
364
+ if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
365
+ log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
366
+ openclawLlmContextDebug.writeSnapshot({
367
+ visibility: "host_event",
368
+ trace: {
369
+ messageId: inboundMessageId ?? "unknown",
370
+ chatId: target.chatId,
371
+ chatType: target.chatType,
372
+ },
373
+ input: { injectedPrompt: "", eventText: "" },
374
+ output: {
375
+ rawModelOutput: text,
376
+ finalAssistantText: text,
377
+ adapterFilteredText: "",
378
+ outboundClawChatMessage: null,
379
+ suppressed: true,
380
+ suppressionReason: "no-reply token",
381
+ },
382
+ });
383
+ return;
384
+ }
385
+ const mediaFragments = await uploadMediaUrls(urls);
386
+ const result = await sendStatic(text, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
387
+ if (result?.messageId)
388
+ recordThinkingIfLinked(result.messageId);
389
+ return;
390
+ }
391
+ // kind === "block" or unknown: OpenClaw may still call this path while
392
+ // the model is producing output. ClawChat gets only the final materialized
393
+ // reply.
394
+ },
395
+ onError: (error, info) => {
396
+ const errorText = normalizeReplyErrorText(error);
397
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw ${info.kind} reply failed: ${errorText}`);
398
+ if (isGroupTarget) {
399
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw group runtime failure suppressed from ClawChat clients group=${target.chatId}`);
400
+ return;
401
+ }
402
+ },
403
+ onIdle: async () => {
404
+ emitTyping(false);
405
+ if (runDone)
406
+ return;
407
+ runDone = true;
408
+ },
409
+ onCleanup: () => {
410
+ emitTyping(false);
411
+ },
412
+ });
413
+ return {
414
+ dispatcher: base.dispatcher,
415
+ replyOptions: {
416
+ ...base.replyOptions,
417
+ sourceReplyDeliveryMode: "automatic",
418
+ disableBlockStreaming: true,
419
+ },
420
+ markDispatchIdle: base.markDispatchIdle,
421
+ };
422
+ }