@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,177 @@
1
+ import { emitStreamAdd, emitStreamCreated, emitStreamDone, emitStreamFailed, } from "./client.js";
2
+ /**
3
+ * Merge two views of the same progressively-revealed text.
4
+ *
5
+ * The agent runner may give us either:
6
+ * - full snapshots ("Hel", "Hello", "Hello, world") where each item is
7
+ * a superset of the previous; or
8
+ * - overlapping slices ("hello ", "world hello ") that don't share a
9
+ * prefix but share an overlap at the join.
10
+ *
11
+ * This helper returns a longest-sensible combined string. Ported from
12
+ * `clawling-channel/src/reply-dispatcher.ts`.
13
+ */
14
+ export function mergeStreamingText(previousText, nextText) {
15
+ const currentSnapshot = typeof previousText === "string" ? previousText : "";
16
+ const incomingText = typeof nextText === "string" ? nextText : "";
17
+ if (!incomingText)
18
+ return currentSnapshot;
19
+ if (!currentSnapshot || incomingText === currentSnapshot)
20
+ return incomingText;
21
+ if (incomingText.startsWith(currentSnapshot))
22
+ return incomingText;
23
+ if (currentSnapshot.startsWith(incomingText))
24
+ return currentSnapshot;
25
+ if (incomingText.includes(currentSnapshot))
26
+ return incomingText;
27
+ if (currentSnapshot.includes(incomingText))
28
+ return currentSnapshot;
29
+ const maxOverlap = Math.min(currentSnapshot.length, incomingText.length);
30
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
31
+ if (currentSnapshot.slice(-overlap) === incomingText.slice(0, overlap)) {
32
+ return `${currentSnapshot}${incomingText.slice(overlap)}`;
33
+ }
34
+ }
35
+ return `${currentSnapshot}${incomingText}`;
36
+ }
37
+ function resolveRouting(options) {
38
+ if (options.routing)
39
+ return options.routing;
40
+ if (options.to)
41
+ return { chatId: options.to.id, chatType: options.to.type };
42
+ throw new Error("openclaw-clawchat buffered stream requires routing");
43
+ }
44
+ /**
45
+ * Build a streaming session wrapper around message.created/add/done events.
46
+ *
47
+ * Usage pattern (matching clawling-channel):
48
+ * const session = openBufferedStreamingSession({...});
49
+ * await session.queueSnapshot("Hel");
50
+ * await session.queueSnapshot("Hello");
51
+ * await session.queueDelta(", world");
52
+ * await session.done();
53
+ */
54
+ export function openBufferedStreamingSession(options) {
55
+ const routing = resolveRouting(options);
56
+ const emitTyping = options.emitTyping !== false;
57
+ if (emitTyping)
58
+ options.client.typing(routing.chatId, true);
59
+ emitStreamCreated(options.client, {
60
+ messageId: options.messageId,
61
+ routing,
62
+ });
63
+ let bufferedSnapshot = "";
64
+ let flushedSnapshot = "";
65
+ let sequence = -1;
66
+ let flushTimer = null;
67
+ let pendingFlush = Promise.resolve();
68
+ let closed = false;
69
+ const clearTimer = () => {
70
+ if (flushTimer) {
71
+ clearTimeout(flushTimer);
72
+ flushTimer = null;
73
+ }
74
+ };
75
+ const performFlush = async () => {
76
+ clearTimer();
77
+ if (closed)
78
+ return;
79
+ if (bufferedSnapshot === flushedSnapshot)
80
+ return;
81
+ const snapshot = bufferedSnapshot;
82
+ const delta = snapshot.slice(flushedSnapshot.length);
83
+ if (!delta)
84
+ return;
85
+ sequence += 1;
86
+ emitStreamAdd(options.client, {
87
+ messageId: options.messageId,
88
+ routing,
89
+ sequence,
90
+ fullText: snapshot,
91
+ textDelta: delta,
92
+ });
93
+ flushedSnapshot = snapshot;
94
+ };
95
+ const flush = async () => {
96
+ pendingFlush = pendingFlush.then(performFlush);
97
+ await pendingFlush;
98
+ };
99
+ const scheduleFlush = () => {
100
+ if (flushTimer || closed)
101
+ return;
102
+ flushTimer = setTimeout(() => {
103
+ flushTimer = null;
104
+ void flush();
105
+ }, options.flushIntervalMs);
106
+ };
107
+ const queueSnapshot = async (snapshot) => {
108
+ if (closed || !snapshot)
109
+ return;
110
+ const base = bufferedSnapshot.length >= flushedSnapshot.length ? bufferedSnapshot : flushedSnapshot;
111
+ const merged = mergeStreamingText(base, snapshot);
112
+ if (merged === bufferedSnapshot)
113
+ return;
114
+ bufferedSnapshot = merged;
115
+ const deltaChars = Math.max(0, bufferedSnapshot.length - flushedSnapshot.length);
116
+ if (deltaChars >= options.minChunkChars || bufferedSnapshot.length >= options.maxBufferChars) {
117
+ await flush();
118
+ }
119
+ else {
120
+ scheduleFlush();
121
+ }
122
+ };
123
+ const queueDelta = async (delta) => {
124
+ if (closed || !delta)
125
+ return;
126
+ bufferedSnapshot = `${bufferedSnapshot}${delta}`;
127
+ const deltaChars = Math.max(0, bufferedSnapshot.length - flushedSnapshot.length);
128
+ if (deltaChars >= options.minChunkChars || bufferedSnapshot.length >= options.maxBufferChars) {
129
+ await flush();
130
+ }
131
+ else {
132
+ scheduleFlush();
133
+ }
134
+ };
135
+ const done = async () => {
136
+ if (closed)
137
+ return;
138
+ await flush();
139
+ closed = true;
140
+ clearTimer();
141
+ emitStreamDone(options.client, {
142
+ messageId: options.messageId,
143
+ routing,
144
+ finalSequence: Math.max(sequence, 0),
145
+ finalText: bufferedSnapshot,
146
+ });
147
+ if (emitTyping)
148
+ options.client.typing(routing.chatId, false);
149
+ };
150
+ const fail = async (reason) => {
151
+ if (closed)
152
+ return;
153
+ closed = true;
154
+ clearTimer();
155
+ emitStreamFailed(options.client, {
156
+ messageId: options.messageId,
157
+ routing,
158
+ sequence: Math.max(sequence, 0),
159
+ ...(reason !== undefined ? { reason } : {}),
160
+ });
161
+ if (emitTyping)
162
+ options.client.typing(routing.chatId, false);
163
+ };
164
+ return {
165
+ get currentText() {
166
+ return bufferedSnapshot;
167
+ },
168
+ get flushedText() {
169
+ return flushedSnapshot;
170
+ },
171
+ queueSnapshot,
172
+ queueDelta,
173
+ flush,
174
+ done,
175
+ fail,
176
+ };
177
+ }
@@ -0,0 +1,66 @@
1
+ import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
2
+ import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
3
+ import { CHANNEL_ID, resolveOpenclawClawlingAccount, } from "./config.js";
4
+ import { openclawClawlingOutbound } from "./outbound.js";
5
+ import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.js";
6
+ import { openclawClawlingSetupPlugin } from "./channel.setup.js";
7
+ import { getClawChatPlatformPrompt } from "./plugin-prompts.js";
8
+ export const openclawClawlingPlugin = createChatChannelPlugin({
9
+ base: {
10
+ ...openclawClawlingSetupPlugin,
11
+ reload: {
12
+ configPrefixes: [`channels.${CHANNEL_ID}`],
13
+ },
14
+ directory: createEmptyChannelDirectoryAdapter(),
15
+ auth: {
16
+ login: async ({ cfg, accountId, runtime }) => {
17
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
18
+ await runOpenclawClawlingLogin({
19
+ cfg,
20
+ accountId: accountId ?? null,
21
+ runtime: { log: (message) => runtime.log(message) },
22
+ mutateConfigFile: getOpenclawClawlingRuntime().config.mutateConfigFile,
23
+ });
24
+ },
25
+ },
26
+ gateway: {
27
+ startAccount: async (ctx) => {
28
+ const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
29
+ ctx.log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle START_ACCOUNT_CALLED configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
30
+ if (!account.configured) {
31
+ ctx.log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle startAccount refused: websocketUrl/token/userId are required`);
32
+ throw new Error("Clawling Chat websocketUrl/token/userId are required");
33
+ }
34
+ try {
35
+ await startOpenclawClawlingGateway({
36
+ cfg: ctx.cfg,
37
+ account,
38
+ abortSignal: ctx.abortSignal,
39
+ setStatus: ctx.setStatus,
40
+ getStatus: ctx.getStatus,
41
+ log: ctx.log,
42
+ });
43
+ }
44
+ finally {
45
+ ctx.log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle startAccount completed/stopped`);
46
+ }
47
+ },
48
+ },
49
+ agentPrompt: {
50
+ messageToolHints: () => [getClawChatPlatformPrompt()],
51
+ },
52
+ messaging: {
53
+ targetPrefixes: ["cc", "clawchat", CHANNEL_ID],
54
+ normalizeTarget: (target) => target
55
+ .trim()
56
+ .replace(/^clawchat-plugin-openclaw:/i, "")
57
+ .replace(/^clawchat:/i, "")
58
+ .replace(/^cc:/i, ""),
59
+ targetResolver: {
60
+ looksLikeId: (raw, normalized) => Boolean((normalized ?? raw).trim()),
61
+ hint: "active-session",
62
+ },
63
+ },
64
+ },
65
+ outbound: openclawClawlingOutbound,
66
+ });
@@ -0,0 +1,119 @@
1
+ import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
2
+ import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
3
+ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
4
+ import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
5
+ import { CHANNEL_ID, listOpenclawClawlingAccountIds, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
6
+ const configAdapter = createTopLevelChannelConfigAdapter({
7
+ sectionKey: CHANNEL_ID,
8
+ resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
9
+ listAccountIds: () => listOpenclawClawlingAccountIds(),
10
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
11
+ deleteMode: "clear-fields",
12
+ clearBaseFields: [
13
+ "websocketUrl",
14
+ "baseUrl",
15
+ "token",
16
+ "userId",
17
+ "forwardThinking",
18
+ "forwardToolCalls",
19
+ "richInteractions",
20
+ "enabled",
21
+ ],
22
+ resolveAllowFrom: (account) => account.allowFrom,
23
+ formatAllowFrom: () => [],
24
+ });
25
+ /**
26
+ * Invite-code setup adapter used by OpenClaw setup surfaces.
27
+ *
28
+ * `channels add --token` passes the invite code as setup input. The setup
29
+ * write leaves channel config unchanged; `afterAccountConfigWritten` exchanges
30
+ * the invite code and persists token/userId through the host runtime mutator.
31
+ */
32
+ const setupAdapter = {
33
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
34
+ validateInput: ({ input }) => {
35
+ const inviteCode = typeof input.code === "string" && input.code.trim()
36
+ ? input.code.trim()
37
+ : typeof input.token === "string"
38
+ ? input.token.trim()
39
+ : "";
40
+ if (!inviteCode) {
41
+ return "ClawChat invite code is required.";
42
+ }
43
+ return null;
44
+ },
45
+ applyAccountConfig: ({ cfg }) => cfg,
46
+ afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
47
+ runtime.log("[default] clawchat-plugin-openclaw setup afterAccountConfigWritten invoked");
48
+ const code = typeof input.code === "string" && input.code.trim()
49
+ ? input.code.trim()
50
+ : typeof input.token === "string"
51
+ ? input.token.trim()
52
+ : "";
53
+ if (!code) {
54
+ runtime.log("[default] clawchat-plugin-openclaw setup afterAccountConfigWritten skipped: empty invite code");
55
+ return;
56
+ }
57
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
58
+ await runOpenclawClawlingLogin({
59
+ cfg,
60
+ accountId: null,
61
+ runtime: { log: (message) => runtime.log(message) },
62
+ readInviteCode: async () => code,
63
+ mutateConfigFile: mutateConfigFile,
64
+ });
65
+ runtime.log("[default] clawchat-plugin-openclaw setup afterAccountConfigWritten completed");
66
+ },
67
+ };
68
+ export const openclawClawlingSetupPlugin = {
69
+ id: CHANNEL_ID,
70
+ meta: {
71
+ id: CHANNEL_ID,
72
+ label: "Clawling Chat",
73
+ selectionLabel: "Clawling Chat",
74
+ docsPath: "/channels/clawchat-plugin-openclaw",
75
+ docsLabel: "clawchat-plugin-openclaw",
76
+ blurb: "ClawChat Protocol v2 over WebSocket.",
77
+ order: 110,
78
+ },
79
+ capabilities: {
80
+ chatTypes: ["direct", "group"],
81
+ media: true,
82
+ reactions: false,
83
+ threads: false,
84
+ polls: false,
85
+ blockStreaming: true,
86
+ },
87
+ configSchema: {
88
+ schema: openclawClawlingConfigSchema,
89
+ },
90
+ config: {
91
+ ...configAdapter,
92
+ isConfigured: (account) => account.configured,
93
+ describeAccount: (account) => ({
94
+ accountId: account.accountId,
95
+ name: account.name,
96
+ enabled: account.enabled,
97
+ configured: account.configured,
98
+ }),
99
+ },
100
+ setup: setupAdapter,
101
+ status: createComputedAccountStatusAdapter({
102
+ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
103
+ connected: false,
104
+ lastInboundAt: null,
105
+ lastOutboundAt: null,
106
+ }),
107
+ resolveAccountSnapshot: ({ account }) => ({
108
+ accountId: account.accountId,
109
+ name: account.name,
110
+ enabled: account.enabled,
111
+ configured: account.configured,
112
+ extra: {
113
+ websocketUrl: account.websocketUrl || null,
114
+ baseUrl: account.baseUrl || null,
115
+ userId: account.userId || null,
116
+ },
117
+ }),
118
+ }),
119
+ };