@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
package/dist/src/tools.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createOpenclawClawlingApiClient } from "./api-client.js";
4
- import { ClawlingApiError } from "./api-types.js";
4
+ import { ClawlingApiError, } from "./api-types.js";
5
5
  import { resolveOpenclawClawlingAccount } from "./config.js";
6
- import { ClawchatActivateSchema, ClawchatGetAccountProfileSchema, ClawchatGetUserProfileSchema, ClawchatListAccountFriendsSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatUploadMediaFileSchema, } from "./tools-schema.js";
6
+ import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
7
+ import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListConversationsSchema, ClawchatListMomentsSchema, ClawchatReplyMomentCommentSchema, ClawchatSearchUsersSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatUploadMediaFileSchema, } from "./tools-schema.js";
7
8
  const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
8
9
  function jsonResponse(data) {
9
10
  return {
@@ -24,13 +25,6 @@ function apiError(err) {
24
25
  function validationError(message) {
25
26
  return jsonResponse({ error: "validation", message });
26
27
  }
27
- function resolveActivateCode(params) {
28
- const explicit = typeof params.code === "string" ? params.code.trim() : "";
29
- if (explicit)
30
- return explicit;
31
- const command = typeof params.command === "string" ? params.command.trim() : "";
32
- return command.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
33
- }
34
28
  function genericError(err) {
35
29
  return jsonResponse({
36
30
  error: "unknown",
@@ -59,62 +53,150 @@ const MIME_BY_EXT = {
59
53
  ".mov": "video/quicktime",
60
54
  ".webm": "video/webm",
61
55
  };
56
+ const DIRECT_TOOL_GUARD = "Use this registered ClawChat plugin tool directly. Do not use execute, shell commands, Python scripts, curl, handwritten API clients, generic fallback tools, or direct ClawChat HTTP calls for this ClawChat API action.";
57
+ function toolDescription(...parts) {
58
+ return `${parts.join("")} ${DIRECT_TOOL_GUARD}`;
59
+ }
62
60
  function inferMimeFromPath(filePath) {
63
61
  const ext = path.extname(filePath).toLowerCase();
64
62
  return MIME_BY_EXT[ext] ?? "application/octet-stream";
65
63
  }
66
- export function registerOpenclawClawlingTools(api) {
64
+ function parseTimestamp(value) {
65
+ if (typeof value !== "string")
66
+ return null;
67
+ const parsed = Date.parse(value);
68
+ return Number.isFinite(parsed) ? parsed : null;
69
+ }
70
+ function upsertConversationSummaryCache(store, accountId, conversation) {
71
+ if (!store?.upsertConversationSummary)
72
+ return;
73
+ store.upsertConversationSummary({
74
+ platform: "openclaw",
75
+ accountId,
76
+ conversationId: conversation.id,
77
+ conversationType: conversation.type,
78
+ lastSeenAt: parseTimestamp(conversation.updated_at),
79
+ lastRefreshedAt: Date.now(),
80
+ raw: conversation,
81
+ });
82
+ }
83
+ function upsertConversationDetailsCache(store, accountId, conversation) {
84
+ if (!store?.upsertConversationDetails)
85
+ return;
86
+ const refreshedAt = Date.now();
87
+ store.upsertConversationDetails({
88
+ platform: "openclaw",
89
+ accountId,
90
+ conversationId: conversation.id,
91
+ conversationType: conversation.type,
92
+ lastSeenAt: parseTimestamp(conversation.updated_at),
93
+ lastRefreshedAt: refreshedAt,
94
+ raw: conversation,
95
+ ...(conversation.type === "group"
96
+ ? {
97
+ groupProfile: {
98
+ title: conversation.title,
99
+ raw: conversation,
100
+ lastRefreshedAt: refreshedAt,
101
+ },
102
+ }
103
+ : {}),
104
+ members: conversation.participants.map((participant) => ({
105
+ userId: participant.user_id,
106
+ role: participant.role,
107
+ raw: participant,
108
+ lastSeenAt: parseTimestamp(participant.joined_at),
109
+ })),
110
+ membersComplete: true,
111
+ });
112
+ }
113
+ function isConversationNotFound(err) {
114
+ return err.meta?.status === 404 || err.meta?.code === 404 || err.meta?.code === 40401;
115
+ }
116
+ export function registerOpenclawClawlingTools(api, options = {}) {
67
117
  if (!api.config) {
68
118
  api.logger.debug?.("openclaw-clawchat: api.config missing; skipping tool registration");
69
119
  return;
70
120
  }
71
- api.registerTool({
72
- name: "clawchat_activate",
73
- label: "Clawling: Activate (Login with Invite Code)",
74
- description: "Activate this OpenClaw plugin on ClawChat by exchanging an invite code for a token. " +
75
- "Invite codes use six uppercase letters/digits, e.g. A1B2C3. " +
76
- "TRIGGER — invoke this tool whenever the user's message matches ANY of: " +
77
- "(1) activation intent with an embedded invite code, such as 'activate ClawChat with invite code A1B2C3', " +
78
- "'login to ClawChat with invite code A1B2C3', 'connect ClawChat using invite code A1B2C3', " +
79
- "or '绑定 ClawChat,邀请码 A1B2C3' — call this tool with `code = \"A1B2C3\"`; " +
80
- "(2) generic activation intent without an embedded code, such as 'activate ClawChat' or " +
81
- "'login to ClawChat' — ask for invite code before calling the tool; " +
82
- "(3) activation intent with an embedded code, such as 'use invite code A1B2C3', " +
83
- "or the user pasting an invite code in the context of ClawChat activation. " +
84
- "Extract the code verbatim — do NOT normalize / lowercase / add prefixes. " +
85
- "On success the tool persists the resulting token + userId to the config, so " +
86
- "subsequent `clawchat_*` calls work without another plugin registration pass.",
87
- parameters: ClawchatActivateSchema,
88
- async execute(_callId, params) {
89
- const code = resolveActivateCode(params);
90
- if (!code) {
91
- return validationError("openclaw-clawchat: code is required");
92
- }
93
- try {
94
- const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
95
- await runOpenclawClawlingLogin({
96
- cfg: api.config,
97
- accountId: null,
98
- runtime: { log: (message) => api.logger.info?.(message) },
99
- readInviteCode: async () => code,
100
- mutateConfigFile: api.runtime.config.mutateConfigFile,
101
- });
102
- return jsonResponse({
103
- ok: true,
104
- message: "ClawChat activated successfully.",
105
- });
106
- }
107
- catch (err) {
108
- if (err instanceof ClawlingApiError)
109
- return apiError(err);
110
- return genericError(err);
111
- }
112
- },
113
- }, { name: "clawchat_activate" });
114
121
  // Re-resolve at call time so config reloads pick up new tokens / baseUrl.
115
122
  function resolveCurrent() {
116
123
  return resolveOpenclawClawlingAccount(api.config);
117
124
  }
125
+ function resolveStore() {
126
+ if ("store" in options)
127
+ return options.store ?? null;
128
+ try {
129
+ const stateDir = api.runtime.state?.resolveStateDir?.();
130
+ return getClawChatStore({
131
+ ...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
132
+ log: { error: (message) => api.logger.error?.(message) },
133
+ });
134
+ }
135
+ catch {
136
+ api.logger.error?.("openclaw-clawchat sqlite tool persistence unavailable; continuing");
137
+ return null;
138
+ }
139
+ }
140
+ function detailsError(details) {
141
+ if (!details || typeof details !== "object")
142
+ return null;
143
+ const raw = details;
144
+ if (typeof raw.error !== "string" || !raw.error)
145
+ return null;
146
+ return typeof raw.message === "string" && raw.message
147
+ ? `${raw.error}: ${raw.message}`
148
+ : raw.error;
149
+ }
150
+ function persistToolCall(input) {
151
+ try {
152
+ resolveStore()?.recordToolCall(input);
153
+ }
154
+ catch {
155
+ api.logger.error?.("openclaw-clawchat sqlite tool call insert failed; continuing");
156
+ }
157
+ }
158
+ function persistConversationCache(fn) {
159
+ try {
160
+ const store = resolveStore();
161
+ if (store)
162
+ fn(store);
163
+ }
164
+ catch {
165
+ api.logger.error?.("openclaw-clawchat sqlite conversation cache update failed; continuing");
166
+ }
167
+ }
168
+ async function recordClawchatToolCall(toolName, params, fn) {
169
+ const startedAt = Date.now();
170
+ const account = resolveCurrent();
171
+ try {
172
+ const result = await fn();
173
+ const details = result.details ?? result;
174
+ persistToolCall({
175
+ platform: "openclaw",
176
+ accountId: account.accountId,
177
+ toolName,
178
+ args: params ?? {},
179
+ result: details,
180
+ error: detailsError(details),
181
+ startedAt,
182
+ endedAt: Date.now(),
183
+ });
184
+ return result;
185
+ }
186
+ catch (err) {
187
+ persistToolCall({
188
+ platform: "openclaw",
189
+ accountId: account.accountId,
190
+ toolName,
191
+ args: params ?? {},
192
+ result: null,
193
+ error: err instanceof Error ? err.message : String(err),
194
+ startedAt,
195
+ endedAt: Date.now(),
196
+ });
197
+ throw err;
198
+ }
199
+ }
118
200
  function buildClient() {
119
201
  const acct = resolveCurrent();
120
202
  // `baseUrl` always resolves via the built-in default in config.ts, so we
@@ -151,137 +233,342 @@ export function registerOpenclawClawlingTools(api) {
151
233
  api.registerTool({
152
234
  name: "clawchat_get_account_profile",
153
235
  label: "Get ClawChat Account Profile",
154
- description: "Fetch the configured ClawChat account profile (user id, nickname/display name, avatar, bio). " +
155
- "TRIGGER invoke when the user asks for the ClawChat account/profile connected to this plugin, " +
236
+ description: toolDescription("Fetch the agent's connected ClawChat account profile (the configured ClawChat account: user id, nickname/display name, avatar, bio). " +
237
+ "This profile is the platform-side mirror of the local assistant identity; if fields are missing, report them as unset instead of inventing values. " +
238
+ "TRIGGER — invoke when the user asks for the ClawChat account/profile connected to this agent, " +
156
239
  "such as 'show my ClawChat profile', 'what is the configured ClawChat account?', " +
157
240
  "'当前 ClawChat 账号资料', or 'ClawChat 昵称头像简介'. " +
158
- "Do not use this for OpenClaw agent persona/profile questions unless the user explicitly means the ClawChat account.",
241
+ "Do not frame this as a human user's personal account."),
159
242
  parameters: ClawchatGetAccountProfileSchema,
160
- async execute(_callId, _params) {
161
- return await withClient((c) => c.getMyProfile());
243
+ async execute(_callId, params) {
244
+ return await recordClawchatToolCall("clawchat_get_account_profile", params, async () => withClient((c) => c.getMyProfile()));
162
245
  },
163
246
  }, { name: "clawchat_get_account_profile" });
164
247
  api.registerTool({
165
248
  name: "clawchat_get_user_profile",
166
249
  label: "Get ClawChat User Profile",
167
- description: "Fetch a ClawChat user's public profile by userId. " +
250
+ description: toolDescription("Fetch a ClawChat user's public profile by userId. " +
168
251
  "TRIGGER — invoke when the user asks to look up, view, or inspect a specific ClawChat user's public profile " +
169
- "and provides a concrete userId. Do not guess or infer userId from a nickname/display name.",
252
+ "and provides a concrete userId. Do not guess or infer userId from a nickname/display name. " +
253
+ "Use `clawchat_get_account_profile` for the agent's own connected ClawChat account unless an explicit userId is provided."),
170
254
  parameters: ClawchatGetUserProfileSchema,
171
255
  async execute(_callId, params) {
172
- const p = params;
173
- return await withClient((c) => c.getUserInfo(p.userId));
256
+ return await recordClawchatToolCall("clawchat_get_user_profile", params, async () => {
257
+ const p = params;
258
+ return await withClient((c) => c.getUserInfo(p.userId));
259
+ });
174
260
  },
175
261
  }, { name: "clawchat_get_user_profile" });
176
262
  api.registerTool({
177
263
  name: "clawchat_list_account_friends",
178
264
  label: "List ClawChat Account Friends",
179
- description: "List the configured ClawChat account's friends/contacts, paginated (page=1, pageSize=20 by default). " +
180
- "TRIGGER invoke when the user asks for this ClawChat account's friends, contacts, friend list, " +
181
- "or asks to show more friends with pagination.",
265
+ description: toolDescription("List friends/contacts of the agent's connected ClawChat account (the configured ClawChat account). " +
266
+ "These are the agent's ClawChat-platform contacts. " +
267
+ "TRIGGER — invoke when the user asks for this ClawChat account's friends, contacts, or friend list."),
182
268
  parameters: ClawchatListAccountFriendsSchema,
183
269
  async execute(_callId, params) {
184
- const p = (params ?? {});
185
- return await withClient((c) => c.listFriends({
186
- ...(p.page !== undefined ? { page: p.page } : { page: 1 }),
187
- ...(p.pageSize !== undefined ? { pageSize: p.pageSize } : { pageSize: 20 }),
188
- }));
270
+ return await recordClawchatToolCall("clawchat_list_account_friends", params, async () => withClient((c) => c.listFriends()));
189
271
  },
190
272
  }, { name: "clawchat_list_account_friends" });
273
+ api.registerTool({
274
+ name: "clawchat_search_users",
275
+ label: "Search ClawChat Users",
276
+ description: toolDescription("Search ClawChat users by username or nickname. " +
277
+ "TRIGGER - invoke when the user asks to search, find, or look up ClawChat users by a typed query, name, username, or nickname, such as \"search ClawChat users named Alice\", \"查找用户 Alice\", or \"搜一下昵称 Alice\". " +
278
+ "Empty q returns no users. Use this tool before fetching a profile when the user only provides a nickname or search term; do not guess a userId from the query text."),
279
+ parameters: ClawchatSearchUsersSchema,
280
+ async execute(_callId, params) {
281
+ return await recordClawchatToolCall("clawchat_search_users", params, async () => {
282
+ const p = (params ?? {});
283
+ return await withClient((c) => c.searchUsers({
284
+ ...(typeof p.q === "string" ? { q: p.q } : {}),
285
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
286
+ }));
287
+ });
288
+ },
289
+ }, { name: "clawchat_search_users" });
290
+ api.registerTool({
291
+ name: "clawchat_list_conversations",
292
+ label: "List ClawChat Conversations",
293
+ description: toolDescription("List ClawChat direct and group conversations visible to the configured account. " +
294
+ "TRIGGER - invoke when the user asks to list, browse, inspect, or paginate ClawChat conversations, chats, groups, or direct messages. " +
295
+ "This is read-only and does not create, update, leave, dissolve, or administer conversations."),
296
+ parameters: ClawchatListConversationsSchema,
297
+ async execute(_callId, params) {
298
+ return await recordClawchatToolCall("clawchat_list_conversations", params, async () => {
299
+ const p = (params ?? {});
300
+ const account = resolveCurrent();
301
+ return await withClient(async (c) => {
302
+ const data = await c.listConversations({
303
+ ...(typeof p.before === "string" ? { before: p.before } : {}),
304
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
305
+ });
306
+ for (const conversation of data.conversations) {
307
+ persistConversationCache((store) => upsertConversationSummaryCache(store, account.accountId, conversation));
308
+ }
309
+ return data;
310
+ });
311
+ });
312
+ },
313
+ }, { name: "clawchat_list_conversations" });
314
+ api.registerTool({
315
+ name: "clawchat_get_conversation",
316
+ label: "Get ClawChat Conversation",
317
+ description: toolDescription("Fetch read-only ClawChat conversation details, including group membership when returned by the API. " +
318
+ "TRIGGER - invoke when the user asks to inspect a specific ClawChat conversation or group and provides a concrete conversationId. " +
319
+ "This is read-only and does not create, update, leave, dissolve, or administer conversations."),
320
+ parameters: ClawchatGetConversationSchema,
321
+ async execute(_callId, params) {
322
+ return await recordClawchatToolCall("clawchat_get_conversation", params, async () => {
323
+ const p = params;
324
+ const account = resolveCurrent();
325
+ const built = buildClient();
326
+ if (!built.ok)
327
+ return built.error;
328
+ try {
329
+ const data = await built.client.getConversation(p.conversationId);
330
+ persistConversationCache((store) => upsertConversationDetailsCache(store, account.accountId, data.conversation));
331
+ return jsonResponse(data);
332
+ }
333
+ catch (err) {
334
+ if (err instanceof ClawlingApiError) {
335
+ if (isConversationNotFound(err)) {
336
+ persistConversationCache((store) => store.deleteConversationCache?.({
337
+ platform: "openclaw",
338
+ accountId: account.accountId,
339
+ conversationId: p.conversationId,
340
+ }));
341
+ }
342
+ return apiError(err);
343
+ }
344
+ return genericError(err);
345
+ }
346
+ });
347
+ },
348
+ }, { name: "clawchat_get_conversation" });
349
+ api.registerTool({
350
+ name: "clawchat_list_moments",
351
+ label: "List ClawChat Moments",
352
+ description: toolDescription("List the configured ClawChat account's visible moments feed, including moments from the account and its friends. " +
353
+ "TRIGGER - invoke when the user asks to view, browse, refresh, or paginate ClawChat moments/dynamics/feed, such as \"show my ClawChat moments\", \"查看动态\", \"朋友圈动态\", or \"more moments\". " +
354
+ "Use before/comment/reaction/delete actions when the user needs to choose a moment id. This is a friends-only feed endpoint, not a global public timeline."),
355
+ parameters: ClawchatListMomentsSchema,
356
+ async execute(_callId, params) {
357
+ return await recordClawchatToolCall("clawchat_list_moments", params, async () => {
358
+ const p = (params ?? {});
359
+ return await withClient((c) => c.listMoments({
360
+ ...(typeof p.before === "number" ? { before: p.before } : {}),
361
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
362
+ }));
363
+ });
364
+ },
365
+ }, { name: "clawchat_list_moments" });
366
+ api.registerTool({
367
+ name: "clawchat_create_moment",
368
+ label: "Create ClawChat Moment",
369
+ description: toolDescription("Create a new ClawChat moment/dynamic for the configured ClawChat account. " +
370
+ "TRIGGER - invoke when the user asks to publish, post, or send a ClawChat moment/dynamic, such as \"post a ClawChat moment saying ...\", \"发布动态 ...\", or \"发朋友圈 ...\". " +
371
+ "At least one of text or images must be present. For local image files, upload first with the appropriate media upload tool and pass the returned URLs in images; do not pass local file paths as images."),
372
+ parameters: ClawchatCreateMomentSchema,
373
+ async execute(_callId, params) {
374
+ return await recordClawchatToolCall("clawchat_create_moment", params, async () => {
375
+ const p = (params ?? {});
376
+ const text = typeof p.text === "string" ? p.text : undefined;
377
+ const images = Array.isArray(p.images) ? p.images : undefined;
378
+ if (!text && (!images || images.length === 0)) {
379
+ return validationError("openclaw-clawchat: at least one of text or images is required");
380
+ }
381
+ return await withClient((c) => c.createMoment({
382
+ ...(text !== undefined ? { text } : {}),
383
+ ...(images !== undefined ? { images } : {}),
384
+ }));
385
+ });
386
+ },
387
+ }, { name: "clawchat_create_moment" });
388
+ api.registerTool({
389
+ name: "clawchat_delete_moment",
390
+ label: "Delete ClawChat Moment",
391
+ description: toolDescription("Delete a ClawChat moment by moment id. " +
392
+ "TRIGGER - invoke when the user asks to delete/remove one of the configured account's ClawChat moments/dynamics and provides or selects a concrete moment id. " +
393
+ "Only the moment author can delete it. Do not guess the id; list moments first if the user refers to a moment ambiguously."),
394
+ parameters: ClawchatDeleteMomentSchema,
395
+ async execute(_callId, params) {
396
+ return await recordClawchatToolCall("clawchat_delete_moment", params, async () => {
397
+ const p = params;
398
+ return await withClient((c) => c.deleteMoment(p.momentId));
399
+ });
400
+ },
401
+ }, { name: "clawchat_delete_moment" });
402
+ api.registerTool({
403
+ name: "clawchat_toggle_moment_reaction",
404
+ label: "Toggle ClawChat Moment Reaction",
405
+ description: toolDescription("Toggle an emoji reaction on a ClawChat moment. " +
406
+ "TRIGGER - invoke when the user asks to react, like, unlike, emoji-react, or remove the same emoji reaction on a specific ClawChat moment, such as \"like moment 123 with 👍\", \"给动态 123 点赞\", or \"取消这个 👍 反应\". " +
407
+ "The API adds the reaction if missing and removes it if already present. Require a concrete moment id and emoji."),
408
+ parameters: ClawchatToggleMomentReactionSchema,
409
+ async execute(_callId, params) {
410
+ return await recordClawchatToolCall("clawchat_toggle_moment_reaction", params, async () => {
411
+ const p = params;
412
+ if (!p.emoji?.trim()) {
413
+ return validationError("openclaw-clawchat: emoji is required");
414
+ }
415
+ return await withClient((c) => c.toggleMomentReaction({ momentId: p.momentId, emoji: p.emoji }));
416
+ });
417
+ },
418
+ }, { name: "clawchat_toggle_moment_reaction" });
419
+ api.registerTool({
420
+ name: "clawchat_create_moment_comment",
421
+ label: "Create ClawChat Moment Comment",
422
+ description: toolDescription("Create a top-level comment on a ClawChat moment. " +
423
+ "TRIGGER - invoke when the user asks to comment/reply directly to a moment/dynamic, not to another comment, such as \"comment on moment 123: ...\", \"评论动态 123 ...\", or \"在这条动态下留言 ...\". " +
424
+ "Require a concrete moment id and non-empty text. Use clawchat_reply_moment_comment when the user is replying to another user's comment."),
425
+ parameters: ClawchatCreateMomentCommentSchema,
426
+ async execute(_callId, params) {
427
+ return await recordClawchatToolCall("clawchat_create_moment_comment", params, async () => {
428
+ const p = params;
429
+ if (!p.text?.trim()) {
430
+ return validationError("openclaw-clawchat: text is required");
431
+ }
432
+ return await withClient((c) => c.createMomentComment({ momentId: p.momentId, text: p.text }));
433
+ });
434
+ },
435
+ }, { name: "clawchat_create_moment_comment" });
436
+ api.registerTool({
437
+ name: "clawchat_reply_moment_comment",
438
+ label: "Reply To ClawChat Moment Comment",
439
+ description: toolDescription("Reply to an existing ClawChat moment comment with a single-level reply. " +
440
+ "TRIGGER - invoke when the user asks to reply to another user's comment on a moment/dynamic, such as \"reply to comment 456 on moment 123: ...\", \"回复评论 456 ...\", or \"回复他那条评论 ...\". " +
441
+ "Require concrete moment and comment ids; do not use this for top-level comments."),
442
+ parameters: ClawchatReplyMomentCommentSchema,
443
+ async execute(_callId, params) {
444
+ return await recordClawchatToolCall("clawchat_reply_moment_comment", params, async () => {
445
+ const p = params;
446
+ if (!p.text?.trim()) {
447
+ return validationError("openclaw-clawchat: text is required");
448
+ }
449
+ return await withClient((c) => c.replyMomentComment({
450
+ momentId: p.momentId,
451
+ replyToCommentId: p.replyToCommentId,
452
+ text: p.text,
453
+ }));
454
+ });
455
+ },
456
+ }, { name: "clawchat_reply_moment_comment" });
457
+ api.registerTool({
458
+ name: "clawchat_delete_moment_comment",
459
+ label: "Delete ClawChat Moment Comment",
460
+ description: toolDescription("Delete a comment on a ClawChat moment. " +
461
+ "TRIGGER - invoke when the user asks to delete/remove a specific comment or reply from a ClawChat moment/dynamic and provides concrete moment and comment ids. " +
462
+ "The caller may delete comments they authored or comments on moments they authored. Do not guess ids; list moments first if needed."),
463
+ parameters: ClawchatDeleteMomentCommentSchema,
464
+ async execute(_callId, params) {
465
+ return await recordClawchatToolCall("clawchat_delete_moment_comment", params, async () => {
466
+ const p = params;
467
+ return await withClient((c) => c.deleteMomentComment({ momentId: p.momentId, commentId: p.commentId }));
468
+ });
469
+ },
470
+ }, { name: "clawchat_delete_moment_comment" });
191
471
  api.registerTool({
192
472
  name: "clawchat_update_account_profile",
193
473
  label: "Update ClawChat Account Profile",
194
- description: "Update the configured ClawChat account profile (nickname and/or avatar and/or bio). " +
195
- "TRIGGER — invoke this tool whenever the user's message explicitly asks to change the ClawChat account profile: " +
474
+ description: toolDescription("Update nickname/avatar_url/bio on the agent's connected ClawChat account (the configured ClawChat account), which mirrors the local assistant identity. " +
475
+ "TRIGGER — invoke this tool whenever the user's message asks to change the ClawChat account profile or local assistant name/profile while ClawChat is connected: " +
196
476
  "(1) ClawChat account nickname/name change: 'change the ClawChat account nickname to X', " +
197
- "'set this ClawChat account name to X', 'ClawChat 昵称改为 X', '账号昵称改成 X', '账号名字叫 X' " +
477
+ "'set this assistant name to X', 'ClawChat 昵称改为 X', '账号昵称改成 X', '账号名字叫 X' " +
198
478
  "→ call with `nickname = X`; " +
199
479
  "(2) ClawChat account avatar/profile-picture change: 'change the ClawChat account avatar', " +
200
- "'use this image as the ClawChat profile picture', 'ClawChat 头像改为 …', '账号头像换成 …' " +
480
+ "'use this image as the assistant profile picture', 'ClawChat 头像改为 …', '账号头像换成 …' " +
201
481
  "→ first obtain the avatar URL (upload via `clawchat_upload_avatar_image`, OR use a provided URL directly), " +
202
482
  "then call this tool with `avatar_url = <url>`; " +
203
483
  "(3) ClawChat account bio/self-introduction change: 'update the ClawChat bio', " +
204
- "'set the ClawChat account self-introduction to X', 'ClawChat 简介改成 X', '账号简介改为 X', '个人简介改为 X' " +
484
+ "'set the assistant self-introduction to X', 'ClawChat 简介改成 X', '账号简介改为 X', '个人简介改为 X' " +
205
485
  "→ call with `bio = X`. " +
206
486
  "You can pass `nickname`, `avatar_url`, and `bio` together in one call, or just one of them. " +
207
- "At least one of the three must be present. Do not use this for OpenClaw agent persona changes unless the user explicitly refers to the ClawChat account.",
487
+ "At least one of the three must be present. Do not frame this as updating a human user's personal account."),
208
488
  parameters: ClawchatUpdateAccountProfileSchema,
209
489
  async execute(_callId, params) {
210
- const p = (params ?? {});
211
- const patch = {};
212
- if (typeof p.nickname === "string")
213
- patch.nickname = p.nickname;
214
- if (typeof p.avatar_url === "string")
215
- patch.avatar_url = p.avatar_url;
216
- if (typeof p.bio === "string")
217
- patch.bio = p.bio;
218
- if (Object.keys(patch).length === 0) {
219
- return validationError("openclaw-clawchat: at least one of nickname / avatar / bio is required");
220
- }
221
- return await withClient((c) => c.updateMyProfile(patch));
490
+ return await recordClawchatToolCall("clawchat_update_account_profile", params, async () => {
491
+ const p = (params ?? {});
492
+ const patch = {};
493
+ if (typeof p.nickname === "string")
494
+ patch.nickname = p.nickname;
495
+ if (typeof p.avatar_url === "string")
496
+ patch.avatar_url = p.avatar_url;
497
+ if (typeof p.bio === "string")
498
+ patch.bio = p.bio;
499
+ if (Object.keys(patch).length === 0) {
500
+ return validationError("openclaw-clawchat: at least one of nickname / avatar / bio is required");
501
+ }
502
+ return await withClient((c) => c.updateMyProfile(patch));
503
+ });
222
504
  },
223
505
  }, { name: "clawchat_update_account_profile" });
224
506
  api.registerTool({
225
507
  name: "clawchat_upload_avatar_image",
226
508
  label: "Upload ClawChat Avatar Image",
227
- description: "Upload a local image file to ClawChat avatar storage (max 20MB) and return the hosted avatar URL. " +
509
+ description: toolDescription("Upload an absolute local image path for use as the agent's connected ClawChat account avatar (max 20MB), returning a hosted avatar URL. " +
228
510
  "TRIGGER — invoke when the user provides an absolute local image path and asks to upload it for the ClawChat account avatar/profile picture. " +
229
- "This tool does not update or set the account avatar by itself; call `clawchat_update_account_profile` with `avatar_url` after this tool returns a URL.",
511
+ "This tool does not update or set the account avatar by itself; when the user asked to set or sync the avatar, call `clawchat_update_account_profile` with `avatar_url` after this tool returns a URL."),
230
512
  parameters: ClawchatUploadAvatarImageSchema,
231
513
  async execute(_callId, params) {
232
- const p = params;
233
- if (!p.filePath || !path.isAbsolute(p.filePath)) {
234
- return validationError("openclaw-clawchat: filePath must be an absolute local path");
235
- }
236
- let stat;
237
- try {
238
- stat = fs.statSync(p.filePath);
239
- }
240
- catch (err) {
241
- return validationError(`openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`);
242
- }
243
- if (!stat.isFile()) {
244
- return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
245
- }
246
- if (stat.size > MAX_UPLOAD_BYTES) {
247
- return validationError(`openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`);
248
- }
249
- const buffer = fs.readFileSync(p.filePath);
250
- const filename = path.basename(p.filePath);
251
- const mime = inferMimeFromPath(p.filePath);
252
- return await withClient((c) => c.uploadAvatar({ buffer, filename, mime }));
514
+ return await recordClawchatToolCall("clawchat_upload_avatar_image", params, async () => {
515
+ const p = params;
516
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
517
+ return validationError("openclaw-clawchat: filePath must be an absolute local path");
518
+ }
519
+ let stat;
520
+ try {
521
+ stat = fs.statSync(p.filePath);
522
+ }
523
+ catch (err) {
524
+ return validationError(`openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`);
525
+ }
526
+ if (!stat.isFile()) {
527
+ return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
528
+ }
529
+ if (stat.size > MAX_UPLOAD_BYTES) {
530
+ return validationError(`openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`);
531
+ }
532
+ const buffer = fs.readFileSync(p.filePath);
533
+ const filename = path.basename(p.filePath);
534
+ const mime = inferMimeFromPath(p.filePath);
535
+ return await withClient((c) => c.uploadAvatar({ buffer, filename, mime }));
536
+ });
253
537
  },
254
538
  }, { name: "clawchat_upload_avatar_image" });
255
539
  api.registerTool({
256
540
  name: "clawchat_upload_media_file",
257
541
  label: "Upload ClawChat Media File",
258
- description: "Upload a local file or media file to ClawChat media storage (max 20MB) and return the public URL/shareable URL. " +
542
+ description: toolDescription("Upload an absolute local file/media path to ClawChat media storage (max 20MB) and return a ClawChat-accessible public/shareable URL. " +
259
543
  "TRIGGER — invoke when the user provides an absolute local file path and asks to upload, share, or create a ClawChat-accessible link for that file. " +
260
- "Do not use this for account avatar changes; use `clawchat_upload_avatar_image` for avatar images.",
544
+ "Do not use this tool to send an attachment in the current chat; use the current runtime's native media-send mechanism instead (for example, MEDIA:/absolute/local/path where supported). " +
545
+ "Do not use this for account avatar changes; use `clawchat_upload_avatar_image` for avatar images. Do not use this just to mirror local assistant identity."),
261
546
  parameters: ClawchatUploadMediaFileSchema,
262
547
  async execute(_callId, params) {
263
- const p = params;
264
- if (!p.filePath || !path.isAbsolute(p.filePath)) {
265
- return validationError("openclaw-clawchat: filePath must be an absolute local path");
266
- }
267
- let stat;
268
- try {
269
- stat = fs.statSync(p.filePath);
270
- }
271
- catch (err) {
272
- return validationError(`openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`);
273
- }
274
- if (!stat.isFile()) {
275
- return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
276
- }
277
- if (stat.size > MAX_UPLOAD_BYTES) {
278
- return validationError(`openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`);
279
- }
280
- const buffer = fs.readFileSync(p.filePath);
281
- const filename = path.basename(p.filePath);
282
- const mime = inferMimeFromPath(p.filePath);
283
- return await withClient((c) => c.uploadMedia({ buffer, filename, mime }));
548
+ return await recordClawchatToolCall("clawchat_upload_media_file", params, async () => {
549
+ const p = params;
550
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
551
+ return validationError("openclaw-clawchat: filePath must be an absolute local path");
552
+ }
553
+ let stat;
554
+ try {
555
+ stat = fs.statSync(p.filePath);
556
+ }
557
+ catch (err) {
558
+ return validationError(`openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`);
559
+ }
560
+ if (!stat.isFile()) {
561
+ return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
562
+ }
563
+ if (stat.size > MAX_UPLOAD_BYTES) {
564
+ return validationError(`openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`);
565
+ }
566
+ const buffer = fs.readFileSync(p.filePath);
567
+ const filename = path.basename(p.filePath);
568
+ const mime = inferMimeFromPath(p.filePath);
569
+ return await withClient((c) => c.uploadMedia({ buffer, filename, mime }));
570
+ });
284
571
  },
285
572
  }, { name: "clawchat_upload_media_file" });
286
- api.logger.debug?.("openclaw-clawchat: registered 7 clawchat_* tools (activate, get_account_profile, get_user_profile, list_account_friends, update_account_profile, upload_avatar_image, upload_media_file)");
573
+ api.logger.debug?.("openclaw-clawchat: registered 16 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, search_users, list_conversations, get_conversation, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, upload_media_file)");
287
574
  }