@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21

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 (99) hide show
  1. package/README.md +39 -17
  2. package/dist/index.js +3 -1
  3. package/dist/src/api-client.js +71 -12
  4. package/dist/src/api-types.test-d.js +10 -0
  5. package/dist/src/channel.js +5 -5
  6. package/dist/src/channel.setup.js +4 -17
  7. package/dist/src/clawchat-memory.js +290 -0
  8. package/dist/src/clawchat-metadata.js +235 -0
  9. package/dist/src/client.js +31 -93
  10. package/dist/src/commands.js +3 -3
  11. package/dist/src/config.js +58 -3
  12. package/dist/src/group-message-coalescer.js +107 -0
  13. package/dist/src/inbound.js +24 -28
  14. package/dist/src/login.runtime.js +82 -19
  15. package/dist/src/media-runtime.js +2 -3
  16. package/dist/src/message-mapper.js +1 -1
  17. package/dist/src/mock-transport.js +31 -0
  18. package/dist/src/outbound.js +281 -56
  19. package/dist/src/plugin-prompts.js +76 -0
  20. package/dist/src/profile-prompt.js +150 -0
  21. package/dist/src/profile-sync.js +169 -0
  22. package/dist/src/prompt-injection.js +25 -0
  23. package/dist/src/protocol-types.js +63 -0
  24. package/dist/src/protocol-types.typecheck.js +1 -0
  25. package/dist/src/protocol.js +2 -2
  26. package/dist/src/reply-dispatcher.js +143 -40
  27. package/dist/src/runtime.js +813 -109
  28. package/dist/src/storage.js +636 -0
  29. package/dist/src/tools-schema.js +70 -10
  30. package/dist/src/tools.js +600 -112
  31. package/dist/src/ws-alignment.js +8 -0
  32. package/dist/src/ws-client.js +588 -0
  33. package/index.ts +6 -1
  34. package/openclaw.plugin.json +44 -4
  35. package/package.json +4 -3
  36. package/prompts/platform.md +7 -0
  37. package/skills/clawchat/SKILL.md +90 -0
  38. package/src/api-client.test.ts +360 -15
  39. package/src/api-client.ts +127 -25
  40. package/src/api-types.test-d.ts +12 -0
  41. package/src/api-types.ts +71 -4
  42. package/src/buffered-stream.test.ts +1 -1
  43. package/src/buffered-stream.ts +1 -1
  44. package/src/channel.outbound.test.ts +270 -60
  45. package/src/channel.setup.ts +9 -18
  46. package/src/channel.test.ts +33 -25
  47. package/src/channel.ts +5 -7
  48. package/src/clawchat-memory.test.ts +372 -0
  49. package/src/clawchat-memory.ts +363 -0
  50. package/src/clawchat-metadata.test.ts +350 -0
  51. package/src/clawchat-metadata.ts +352 -0
  52. package/src/client.test.ts +57 -48
  53. package/src/client.ts +37 -129
  54. package/src/commands.test.ts +2 -2
  55. package/src/commands.ts +3 -3
  56. package/src/config.test.ts +169 -4
  57. package/src/config.ts +86 -6
  58. package/src/group-message-coalescer.test.ts +223 -0
  59. package/src/group-message-coalescer.ts +154 -0
  60. package/src/inbound.test.ts +106 -19
  61. package/src/inbound.ts +31 -35
  62. package/src/login.runtime.test.ts +294 -11
  63. package/src/login.runtime.ts +90 -21
  64. package/src/manifest.test.ts +86 -14
  65. package/src/media-runtime.test.ts +31 -2
  66. package/src/media-runtime.ts +7 -10
  67. package/src/message-mapper.test.ts +2 -2
  68. package/src/message-mapper.ts +2 -2
  69. package/src/mock-transport.test.ts +35 -0
  70. package/src/mock-transport.ts +38 -0
  71. package/src/outbound.test.ts +811 -95
  72. package/src/outbound.ts +332 -65
  73. package/src/plugin-entry.test.ts +3 -1
  74. package/src/plugin-prompts.test.ts +78 -0
  75. package/src/plugin-prompts.ts +92 -0
  76. package/src/profile-prompt.test.ts +435 -0
  77. package/src/profile-prompt.ts +208 -0
  78. package/src/profile-sync.test.ts +611 -0
  79. package/src/profile-sync.ts +268 -0
  80. package/src/prompt-injection.test.ts +39 -0
  81. package/src/prompt-injection.ts +45 -0
  82. package/src/protocol-types.test.ts +69 -0
  83. package/src/protocol-types.ts +296 -0
  84. package/src/protocol-types.typecheck.ts +89 -0
  85. package/src/protocol.ts +2 -2
  86. package/src/reply-dispatcher.test.ts +720 -135
  87. package/src/reply-dispatcher.ts +174 -42
  88. package/src/runtime.test.ts +3884 -337
  89. package/src/runtime.ts +956 -128
  90. package/src/storage.test.ts +692 -0
  91. package/src/storage.ts +989 -0
  92. package/src/streaming.test.ts +1 -1
  93. package/src/streaming.ts +1 -1
  94. package/src/tools-schema.ts +115 -13
  95. package/src/tools.test.ts +501 -10
  96. package/src/tools.ts +739 -133
  97. package/src/ws-alignment.ts +9 -0
  98. package/src/ws-client.test.ts +1218 -0
  99. package/src/ws-client.ts +662 -0
@@ -1,9 +1,7 @@
1
- import { EVENT, } from "@newbase-clawchat/sdk";
1
+ import { EVENT, } from "./protocol-types.js";
2
+ import { effectiveGroupMode } from "./config.js";
2
3
  import { extractMediaFragments, fragmentsToText } from "./message-mapper.js";
3
4
  import { hasRenderableText, isInboundMessagePayload } from "./protocol.js";
4
- const DEDUP_MAX = 256;
5
- const dedupSeen = [];
6
- const dedupSet = new Set();
7
5
  function normalizeSender(sender) {
8
6
  if (!sender || typeof sender !== "object")
9
7
  return null;
@@ -12,7 +10,8 @@ function normalizeSender(sender) {
12
10
  if (!id)
13
11
  return null;
14
12
  const nickName = typeof s.nick_name === "string" ? s.nick_name : id;
15
- return { id, nickName };
13
+ const profileType = s.type === "agent" || s.type === "user" ? s.type : null;
14
+ return { id, nickName, profileType };
16
15
  }
17
16
  function isStreamDonePayload(payload) {
18
17
  if (!payload || typeof payload !== "object")
@@ -39,21 +38,18 @@ function extractMentionIds(fragments) {
39
38
  .map((fragment) => fragment.kind === "mention" ? fragment.user_id : undefined)
40
39
  .filter((userId) => typeof userId === "string" && userId.length > 0);
41
40
  }
42
- export function _resetDedupForTest() {
43
- dedupSeen.length = 0;
44
- dedupSet.clear();
45
- }
46
- function rememberAndCheck(messageId) {
47
- if (dedupSet.has(messageId))
48
- return true;
49
- dedupSet.add(messageId);
50
- dedupSeen.push(messageId);
51
- if (dedupSeen.length > DEDUP_MAX) {
52
- const evict = dedupSeen.shift();
53
- if (evict)
54
- dedupSet.delete(evict);
55
- }
56
- return false;
41
+ function normalizeMentionIds(mentions) {
42
+ return mentions
43
+ .map((mention) => {
44
+ if (typeof mention === "string")
45
+ return mention;
46
+ if (mention && typeof mention === "object") {
47
+ const userId = mention.user_id;
48
+ return typeof userId === "string" ? userId : undefined;
49
+ }
50
+ return undefined;
51
+ })
52
+ .filter((userId) => typeof userId === "string" && userId.length > 0);
57
53
  }
58
54
  /**
59
55
  * Exported for direct unit testing. Direct chats always count as addressed;
@@ -62,7 +58,7 @@ function rememberAndCheck(messageId) {
62
58
  export function detectMention(params) {
63
59
  if (params.chatType === "direct")
64
60
  return true;
65
- return params.mentions.includes(params.userId);
61
+ return normalizeMentionIds(params.mentions).includes(params.userId);
66
62
  }
67
63
  export async function dispatchOpenclawClawlingInbound(params) {
68
64
  const { envelope, account, log } = params;
@@ -107,23 +103,21 @@ export async function dispatchOpenclawClawlingInbound(params) {
107
103
  log?.info?.(`[${account.accountId}] openclaw-clawchat skip empty msg=${payload.message_id}`);
108
104
  return;
109
105
  }
110
- if (rememberAndCheck(payload.message_id)) {
111
- log?.info?.(`[${account.accountId}] openclaw-clawchat skip duplicate msg=${payload.message_id}`);
112
- return;
113
- }
106
+ const mentionIds = normalizeMentionIds(message.context.mentions);
114
107
  const rawBody = fragmentsToText(message.body.fragments, {
115
- mentionFallbackIds: message.context.mentions,
108
+ mentionFallbackIds: mentionIds,
116
109
  });
117
110
  const mediaItems = extractMediaFragments(message.body.fragments);
118
111
  const wasMentioned = detectMention({
119
- mentions: message.context.mentions,
112
+ mentions: mentionIds,
120
113
  chatType,
121
114
  userId: account.userId,
122
115
  });
123
116
  // Group trigger policy: in "mention" mode we only handle group messages
124
117
  // that @-mention us; "all" listens open and processes every group msg.
125
118
  // Direct chats are unaffected (detectMention returns true).
126
- if (isGroup && account.groupMode === "mention" && !wasMentioned) {
119
+ const groupMode = isGroup ? effectiveGroupMode(account, chatId) : account.groupMode;
120
+ if (isGroup && groupMode === "mention" && !wasMentioned) {
127
121
  log?.info?.(`[${account.accountId}] openclaw-clawchat skip group (no mention) msg=${payload.message_id}`);
128
122
  return;
129
123
  }
@@ -143,11 +137,13 @@ export async function dispatchOpenclawClawlingInbound(params) {
143
137
  peer: { kind: isGroup ? "group" : "direct", id: chatId },
144
138
  senderId: sender.id,
145
139
  senderNickName: sender.nickName,
140
+ ...(sender.profileType ? { senderProfileType: sender.profileType } : {}),
146
141
  rawBody,
147
142
  messageId: payload.message_id,
148
143
  traceId: envelope.trace_id,
149
144
  timestamp: envelope.emitted_at,
150
145
  wasMentioned,
146
+ mentionedUserIds: mentionIds,
151
147
  mediaItems,
152
148
  ...(replyCtx ? { replyCtx } : {}),
153
149
  cfg: params.cfg,
@@ -2,6 +2,7 @@ import { createInterface } from "node:readline/promises";
2
2
  import { createOpenclawClawlingApiClient } from "./api-client.js";
3
3
  import { ClawlingApiError } from "./api-types.js";
4
4
  import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, resolveOpenclawClawlingAccount, } from "./config.js";
5
+ import { getClawChatStore } from "./storage.js";
5
6
  /**
6
7
  * Platform tag sent to `/v1/agents/connect`. Identifies the host of this
7
8
  * agent runtime — openclaw's bundled clawchat channel.
@@ -38,15 +39,28 @@ async function promptInviteCodeFromStdin(runtime) {
38
39
  function buildLoginConfig(cfg, result) {
39
40
  const channels = (cfg.channels ?? {});
40
41
  const existing = (channels[CHANNEL_ID] ?? {});
42
+ const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
43
+ ? existing.groupMode
44
+ : "all";
45
+ const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
46
+ ? existing.groupCommandMode
47
+ : "owner";
41
48
  const nextSection = {
42
49
  ...existing,
43
50
  enabled: true,
51
+ groupMode,
52
+ groupCommandMode,
44
53
  token: result.access_token,
54
+ ...(result.agent.id ? { agentId: result.agent.id } : {}),
45
55
  userId: result.agent.user_id,
56
+ ownerUserId: result.agent.owner_id,
46
57
  };
47
58
  if (result.refresh_token) {
48
59
  nextSection.refreshToken = result.refresh_token;
49
60
  }
61
+ else {
62
+ delete nextSection.refreshToken;
63
+ }
50
64
  return mergeOpenclawClawchatRuntimePluginActivation(mergeOpenclawClawchatToolAllow({
51
65
  ...cfg,
52
66
  channels: { ...channels, [CHANNEL_ID]: nextSection },
@@ -54,7 +68,7 @@ function buildLoginConfig(cfg, result) {
54
68
  }
55
69
  async function persistLoginConfig(params, result) {
56
70
  if (params.mutateConfigFile) {
57
- params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} with Gateway restart intent.`);
71
+ params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`);
58
72
  await params.mutateConfigFile({
59
73
  afterWrite: {
60
74
  mode: "restart",
@@ -64,19 +78,35 @@ async function persistLoginConfig(params, result) {
64
78
  Object.assign(draft, buildLoginConfig(draft, result));
65
79
  },
66
80
  });
67
- params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`);
81
+ params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
68
82
  return;
69
83
  }
70
84
  if (params.persistConfig) {
71
- params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id}.`);
85
+ params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
72
86
  await params.persistConfig(buildLoginConfig(params.cfg, result));
73
- params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`);
87
+ params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
74
88
  return;
75
89
  }
76
90
  throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
77
91
  }
92
+ function requireConnectString(value, fieldName) {
93
+ if (typeof value !== "string") {
94
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
95
+ }
96
+ const trimmed = value.trim();
97
+ if (!trimmed) {
98
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
99
+ }
100
+ return trimmed;
101
+ }
102
+ function readOptionalConnectString(value, fieldName) {
103
+ if (value == null) {
104
+ return undefined;
105
+ }
106
+ return requireConnectString(value, fieldName);
107
+ }
78
108
  /**
79
- * Run the invite-code credential exchange used by `/clawchat-login`,
109
+ * Run the invite-code credential exchange used by `/clawchat-activate`,
80
110
  * `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
81
111
  * and `openclaw channels login --channel openclaw-clawchat`:
82
112
  * 1. Read the existing channel section; require `baseUrl` to be set so we
@@ -120,20 +150,53 @@ export async function runOpenclawClawlingLogin(params) {
120
150
  }
121
151
  throw err;
122
152
  }
123
- if (!result?.access_token || !result?.agent?.user_id) {
124
- throw new Error(`agents/connect response missing required fields (access_token / agent.user_id): ${JSON.stringify(result)}`);
153
+ const accessToken = requireConnectString(result?.access_token, "access_token");
154
+ const agentUserId = requireConnectString(result?.agent?.user_id, "agent.user_id");
155
+ const ownerUserId = requireConnectString(result?.agent?.owner_id, "agent.owner_id");
156
+ const agentId = readOptionalConnectString(result?.agent?.id, "agent.id");
157
+ let conversationId = null;
158
+ if (result?.conversation != null) {
159
+ conversationId = requireConnectString(result.conversation.id, "conversation.id");
160
+ }
161
+ const normalizedResult = {
162
+ ...result,
163
+ access_token: accessToken,
164
+ refresh_token: typeof result?.refresh_token === "string" ? result.refresh_token.trim() : "",
165
+ agent: {
166
+ ...result.agent,
167
+ ...(agentId ? { id: agentId } : {}),
168
+ owner_id: ownerUserId,
169
+ user_id: agentUserId,
170
+ },
171
+ ...(conversationId
172
+ ? {
173
+ conversation: {
174
+ ...result.conversation,
175
+ id: conversationId,
176
+ },
177
+ }
178
+ : {}),
179
+ };
180
+ runtime.log(`Updating config: channels.${CHANNEL_ID}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`);
181
+ await persistLoginConfig(params, normalizedResult);
182
+ try {
183
+ const store = params.store ??
184
+ getClawChatStore({
185
+ ...(params.dbPath ? { dbPath: params.dbPath } : {}),
186
+ log: { error: runtime.log },
187
+ });
188
+ store.upsertActivation({
189
+ platform: "openclaw",
190
+ accountId: account.accountId,
191
+ userId: normalizedResult.agent.user_id,
192
+ ownerUserId: normalizedResult.agent.owner_id,
193
+ conversationId: normalizedResult.conversation?.id ?? null,
194
+ loginMethod: "login",
195
+ });
196
+ }
197
+ catch {
198
+ runtime.log("openclaw-clawchat sqlite activation persistence failed; login continues.");
125
199
  }
126
- const tokenPreview = redactToken(result.access_token);
127
- runtime.log(`Updating config: channels.${CHANNEL_ID}.token=${tokenPreview} userId=${result.agent.user_id}${result.refresh_token ? " refreshToken=***" : ""} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`);
128
- await persistLoginConfig(params, result);
129
200
  runtime.log(`Config file updated.`);
130
- runtime.log(`openclaw-clawchat login succeeded (user_id=${result.agent.user_id}, nickname=${result.agent.nickname || "-"}).`);
131
- }
132
- /** Shortens a token for display logs without revealing the full secret. */
133
- function redactToken(token) {
134
- if (!token)
135
- return "(empty)";
136
- if (token.length <= 8)
137
- return "***";
138
- return `${token.slice(0, 4)}…${token.slice(-4)}`;
201
+ runtime.log(`openclaw-clawchat login succeeded (user_id=${normalizedResult.agent.user_id}, owner_user_id=${normalizedResult.agent.owner_id}, nickname=${normalizedResult.agent.nickname || "-"}).`);
139
202
  }
@@ -69,13 +69,12 @@ export async function uploadOutboundMedia(urls, ctx) {
69
69
  mime: loaded.contentType,
70
70
  });
71
71
  const fragment = {
72
- kind: inferMediaKindFromMime(uploaded.mime),
72
+ kind: uploaded.kind,
73
73
  url: uploaded.url,
74
+ name: uploaded.name,
74
75
  mime: uploaded.mime,
75
76
  size: uploaded.size,
76
77
  };
77
- if (loaded.fileName)
78
- fragment.name = loaded.fileName;
79
78
  out.push(fragment);
80
79
  }
81
80
  catch (err) {
@@ -53,7 +53,7 @@ export function textToFragments(text) {
53
53
  /**
54
54
  * Extract media fragments from a body (image/file/audio/video). Skips
55
55
  * entries missing `url`. Preserves all optional metadata fields the
56
- * SDK passes through (mime/size/width/height/duration/name).
56
+ * protocol carries through (mime/size/width/height/duration/name).
57
57
  */
58
58
  export function extractMediaFragments(fragments) {
59
59
  const out = [];
@@ -0,0 +1,31 @@
1
+ export class MockTransport {
2
+ handlers;
3
+ currentState = "closed";
4
+ sent = [];
5
+ get state() {
6
+ return this.currentState;
7
+ }
8
+ async connect(_url, handlers) {
9
+ this.handlers = handlers;
10
+ this.currentState = "open";
11
+ handlers.onOpen();
12
+ }
13
+ send(data) {
14
+ if (this.currentState !== "open") {
15
+ throw new Error("transport is not open");
16
+ }
17
+ this.sent.push(data);
18
+ }
19
+ close(code = 1000, reason = "client close") {
20
+ if (this.currentState === "closed")
21
+ return;
22
+ this.currentState = "closed";
23
+ this.handlers?.onClose(code, reason);
24
+ }
25
+ emitInbound(data) {
26
+ this.handlers?.onMessage(data);
27
+ }
28
+ emitError(err) {
29
+ this.handlers?.onError(err);
30
+ }
31
+ }