@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/src/tools.ts CHANGED
@@ -2,21 +2,47 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { OpenClawAgentToolResult } from "openclaw/plugin-sdk/agent-harness-runtime";
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
5
- import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
6
5
  import { createOpenclawClawlingApiClient } from "./api-client.ts";
7
- import { ClawlingApiError, type Profile } from "./api-types.ts";
6
+ import {
7
+ ClawlingApiError,
8
+ type ConversationDetails,
9
+ type ConversationListItem,
10
+ type Profile,
11
+ } from "./api-types.ts";
8
12
  import { resolveOpenclawClawlingAccount } from "./config.ts";
9
13
  import {
10
- ClawchatActivateSchema,
14
+ clawChatDbPathForStateDir,
15
+ getClawChatStore,
16
+ type ClawChatStore,
17
+ } from "./storage.ts";
18
+ import {
11
19
  ClawchatGetAccountProfileSchema,
20
+ ClawchatGetConversationSchema,
12
21
  ClawchatGetUserProfileSchema,
22
+ ClawchatCreateMomentCommentSchema,
23
+ ClawchatCreateMomentSchema,
24
+ ClawchatDeleteMomentCommentSchema,
25
+ ClawchatDeleteMomentSchema,
13
26
  ClawchatListAccountFriendsSchema,
27
+ ClawchatListConversationsSchema,
28
+ ClawchatListMomentsSchema,
29
+ ClawchatReplyMomentCommentSchema,
30
+ ClawchatSearchUsersSchema,
31
+ ClawchatToggleMomentReactionSchema,
14
32
  ClawchatUpdateAccountProfileSchema,
15
33
  ClawchatUploadAvatarImageSchema,
16
34
  ClawchatUploadMediaFileSchema,
17
- type ClawchatActivateParams,
35
+ type ClawchatCreateMomentCommentParams,
36
+ type ClawchatCreateMomentParams,
37
+ type ClawchatDeleteMomentCommentParams,
38
+ type ClawchatDeleteMomentParams,
39
+ type ClawchatGetConversationParams,
18
40
  type ClawchatGetUserProfileParams,
19
- type ClawchatListAccountFriendsParams,
41
+ type ClawchatListConversationsParams,
42
+ type ClawchatListMomentsParams,
43
+ type ClawchatReplyMomentCommentParams,
44
+ type ClawchatSearchUsersParams,
45
+ type ClawchatToggleMomentReactionParams,
20
46
  type ClawchatUpdateAccountProfileParams,
21
47
  type ClawchatUploadAvatarImageParams,
22
48
  type ClawchatUploadMediaFileParams,
@@ -47,13 +73,6 @@ function validationError(message: string) {
47
73
  return jsonResponse({ error: "validation", message });
48
74
  }
49
75
 
50
- function resolveActivateCode(params: ClawchatActivateParams & { command?: unknown }): string {
51
- const explicit = typeof params.code === "string" ? params.code.trim() : "";
52
- if (explicit) return explicit;
53
- const command = typeof params.command === "string" ? params.command.trim() : "";
54
- return command.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
55
- }
56
-
57
76
  function genericError(err: unknown) {
58
77
  return jsonResponse({
59
78
  error: "unknown",
@@ -84,70 +103,180 @@ const MIME_BY_EXT: Record<string, string> = {
84
103
  ".webm": "video/webm",
85
104
  };
86
105
 
106
+ const DIRECT_TOOL_GUARD =
107
+ "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.";
108
+
109
+ type ToolStore = Pick<
110
+ ClawChatStore,
111
+ "recordToolCall"
112
+ > &
113
+ Partial<
114
+ Pick<
115
+ ClawChatStore,
116
+ "upsertConversationSummary" | "upsertConversationDetails" | "deleteConversationCache"
117
+ >
118
+ >;
119
+
120
+ function toolDescription(...parts: string[]): string {
121
+ return `${parts.join("")} ${DIRECT_TOOL_GUARD}`;
122
+ }
123
+
87
124
  function inferMimeFromPath(filePath: string): string {
88
125
  const ext = path.extname(filePath).toLowerCase();
89
126
  return MIME_BY_EXT[ext] ?? "application/octet-stream";
90
127
  }
91
128
 
92
- export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
129
+ function parseTimestamp(value: unknown): number | null {
130
+ if (typeof value !== "string") return null;
131
+ const parsed = Date.parse(value);
132
+ return Number.isFinite(parsed) ? parsed : null;
133
+ }
134
+
135
+ function upsertConversationSummaryCache(
136
+ store: ToolStore | null,
137
+ accountId: string,
138
+ conversation: ConversationListItem,
139
+ ): void {
140
+ if (!store?.upsertConversationSummary) return;
141
+ store.upsertConversationSummary({
142
+ platform: "openclaw",
143
+ accountId,
144
+ conversationId: conversation.id,
145
+ conversationType: conversation.type,
146
+ lastSeenAt: parseTimestamp(conversation.updated_at),
147
+ lastRefreshedAt: Date.now(),
148
+ raw: conversation,
149
+ });
150
+ }
151
+
152
+ function upsertConversationDetailsCache(
153
+ store: ToolStore | null,
154
+ accountId: string,
155
+ conversation: ConversationDetails,
156
+ ): void {
157
+ if (!store?.upsertConversationDetails) return;
158
+ const refreshedAt = Date.now();
159
+ store.upsertConversationDetails({
160
+ platform: "openclaw",
161
+ accountId,
162
+ conversationId: conversation.id,
163
+ conversationType: conversation.type,
164
+ lastSeenAt: parseTimestamp(conversation.updated_at),
165
+ lastRefreshedAt: refreshedAt,
166
+ raw: conversation,
167
+ ...(conversation.type === "group"
168
+ ? {
169
+ groupProfile: {
170
+ title: conversation.title,
171
+ raw: conversation,
172
+ lastRefreshedAt: refreshedAt,
173
+ },
174
+ }
175
+ : {}),
176
+ members: conversation.participants.map((participant) => ({
177
+ userId: participant.user_id,
178
+ role: participant.role,
179
+ raw: participant,
180
+ lastSeenAt: parseTimestamp(participant.joined_at),
181
+ })),
182
+ membersComplete: true,
183
+ });
184
+ }
185
+
186
+ function isConversationNotFound(err: ClawlingApiError): boolean {
187
+ return err.meta?.status === 404 || err.meta?.code === 404 || err.meta?.code === 40401;
188
+ }
189
+
190
+ export function registerOpenclawClawlingTools(
191
+ api: OpenClawPluginApi,
192
+ options: { store?: ToolStore | null } = {},
193
+ ): void {
93
194
  if (!api.config) {
94
195
  api.logger.debug?.("openclaw-clawchat: api.config missing; skipping tool registration");
95
196
  return;
96
197
  }
97
198
 
98
- api.registerTool(
99
- {
100
- name: "clawchat_activate",
101
- label: "Clawling: Activate (Login with Invite Code)",
102
- description:
103
- "Activate this OpenClaw plugin on ClawChat by exchanging an invite code for a token. " +
104
- "Invite codes use six uppercase letters/digits, e.g. A1B2C3. " +
105
- "TRIGGER — invoke this tool whenever the user's message matches ANY of: " +
106
- "(1) activation intent with an embedded invite code, such as 'activate ClawChat with invite code A1B2C3', " +
107
- "'login to ClawChat with invite code A1B2C3', 'connect ClawChat using invite code A1B2C3', " +
108
- "or '绑定 ClawChat,邀请码 A1B2C3' — call this tool with `code = \"A1B2C3\"`; " +
109
- "(2) generic activation intent without an embedded code, such as 'activate ClawChat' or " +
110
- "'login to ClawChat' — ask for invite code before calling the tool; " +
111
- "(3) activation intent with an embedded code, such as 'use invite code A1B2C3', " +
112
- "or the user pasting an invite code in the context of ClawChat activation. " +
113
- "Extract the code verbatim — do NOT normalize / lowercase / add prefixes. " +
114
- "On success the tool persists the resulting token + userId to the config, so " +
115
- "subsequent `clawchat_*` calls work without another plugin registration pass.",
116
- parameters: ClawchatActivateSchema,
117
- async execute(_callId, params) {
118
- const code = resolveActivateCode(params as ClawchatActivateParams & { command?: unknown });
119
- if (!code) {
120
- return validationError("openclaw-clawchat: code is required");
121
- }
122
- try {
123
- const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
124
- await runOpenclawClawlingLogin({
125
- cfg: api.config!,
126
- accountId: null,
127
- runtime: { log: (message: string) => api.logger.info?.(message) },
128
- readInviteCode: async () => code,
129
- mutateConfigFile: (api.runtime.config as unknown as {
130
- mutateConfigFile: OpenclawClawchatMutateConfigFile;
131
- }).mutateConfigFile,
132
- });
133
- return jsonResponse({
134
- ok: true,
135
- message: "ClawChat activated successfully.",
136
- });
137
- } catch (err) {
138
- if (err instanceof ClawlingApiError) return apiError(err);
139
- return genericError(err);
140
- }
141
- },
142
- },
143
- { name: "clawchat_activate" },
144
- );
145
-
146
199
  // Re-resolve at call time so config reloads pick up new tokens / baseUrl.
147
200
  function resolveCurrent() {
148
201
  return resolveOpenclawClawlingAccount(api.config!);
149
202
  }
150
203
 
204
+ function resolveStore(): ToolStore | null {
205
+ if ("store" in options) return options.store ?? null;
206
+ try {
207
+ const stateDir = api.runtime.state?.resolveStateDir?.();
208
+ return getClawChatStore({
209
+ ...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
210
+ log: { error: (message) => api.logger.error?.(message) },
211
+ });
212
+ } catch {
213
+ api.logger.error?.("openclaw-clawchat sqlite tool persistence unavailable; continuing");
214
+ return null;
215
+ }
216
+ }
217
+
218
+ function detailsError(details: unknown): string | null {
219
+ if (!details || typeof details !== "object") return null;
220
+ const raw = details as { error?: unknown; message?: unknown };
221
+ if (typeof raw.error !== "string" || !raw.error) return null;
222
+ return typeof raw.message === "string" && raw.message
223
+ ? `${raw.error}: ${raw.message}`
224
+ : raw.error;
225
+ }
226
+
227
+ function persistToolCall(input: Parameters<ToolStore["recordToolCall"]>[0]): void {
228
+ try {
229
+ resolveStore()?.recordToolCall(input);
230
+ } catch {
231
+ api.logger.error?.("openclaw-clawchat sqlite tool call insert failed; continuing");
232
+ }
233
+ }
234
+
235
+ function persistConversationCache(fn: (store: ToolStore) => void): void {
236
+ try {
237
+ const store = resolveStore();
238
+ if (store) fn(store);
239
+ } catch {
240
+ api.logger.error?.("openclaw-clawchat sqlite conversation cache update failed; continuing");
241
+ }
242
+ }
243
+
244
+ async function recordClawchatToolCall<T>(
245
+ toolName: string,
246
+ params: unknown,
247
+ fn: () => Promise<OpenClawAgentToolResult<T>>,
248
+ ): Promise<OpenClawAgentToolResult<T>> {
249
+ const startedAt = Date.now();
250
+ const account = resolveCurrent();
251
+ try {
252
+ const result = await fn();
253
+ const details = (result as { details?: unknown }).details ?? result;
254
+ persistToolCall({
255
+ platform: "openclaw",
256
+ accountId: account.accountId,
257
+ toolName,
258
+ args: params ?? {},
259
+ result: details,
260
+ error: detailsError(details),
261
+ startedAt,
262
+ endedAt: Date.now(),
263
+ });
264
+ return result;
265
+ } catch (err) {
266
+ persistToolCall({
267
+ platform: "openclaw",
268
+ accountId: account.accountId,
269
+ toolName,
270
+ args: params ?? {},
271
+ result: null,
272
+ error: err instanceof Error ? err.message : String(err),
273
+ startedAt,
274
+ endedAt: Date.now(),
275
+ });
276
+ throw err;
277
+ }
278
+ }
279
+
151
280
  type ClientResult =
152
281
  | { ok: false; error: OpenClawAgentToolResult<unknown> }
153
282
  | { ok: true; client: ReturnType<typeof createOpenclawClawlingApiClient> };
@@ -190,15 +319,19 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
190
319
  {
191
320
  name: "clawchat_get_account_profile",
192
321
  label: "Get ClawChat Account Profile",
193
- description:
194
- "Fetch the configured ClawChat account profile (user id, nickname/display name, avatar, bio). " +
195
- "TRIGGER invoke when the user asks for the ClawChat account/profile connected to this plugin, " +
322
+ description: toolDescription(
323
+ "Fetch the agent's connected ClawChat account profile (the configured ClawChat account: user id, nickname/display name, avatar, bio). " +
324
+ "This profile is the platform-side mirror of the local assistant identity; if fields are missing, report them as unset instead of inventing values. " +
325
+ "TRIGGER — invoke when the user asks for the ClawChat account/profile connected to this agent, " +
196
326
  "such as 'show my ClawChat profile', 'what is the configured ClawChat account?', " +
197
327
  "'当前 ClawChat 账号资料', or 'ClawChat 昵称头像简介'. " +
198
- "Do not use this for OpenClaw agent persona/profile questions unless the user explicitly means the ClawChat account.",
328
+ "Do not frame this as a human user's personal account.",
329
+ ),
199
330
  parameters: ClawchatGetAccountProfileSchema,
200
- async execute(_callId, _params) {
201
- return await withClient((c) => c.getMyProfile());
331
+ async execute(_callId, params) {
332
+ return await recordClawchatToolCall("clawchat_get_account_profile", params, async () =>
333
+ withClient((c) => c.getMyProfile()),
334
+ );
202
335
  },
203
336
  },
204
337
  { name: "clawchat_get_account_profile" },
@@ -208,14 +341,18 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
208
341
  {
209
342
  name: "clawchat_get_user_profile",
210
343
  label: "Get ClawChat User Profile",
211
- description:
344
+ description: toolDescription(
212
345
  "Fetch a ClawChat user's public profile by userId. " +
213
346
  "TRIGGER — invoke when the user asks to look up, view, or inspect a specific ClawChat user's public profile " +
214
- "and provides a concrete userId. Do not guess or infer userId from a nickname/display name.",
347
+ "and provides a concrete userId. Do not guess or infer userId from a nickname/display name. " +
348
+ "Use `clawchat_get_account_profile` for the agent's own connected ClawChat account unless an explicit userId is provided.",
349
+ ),
215
350
  parameters: ClawchatGetUserProfileSchema,
216
351
  async execute(_callId, params) {
217
- const p = params as ClawchatGetUserProfileParams;
218
- return await withClient((c) => c.getUserInfo(p.userId));
352
+ return await recordClawchatToolCall("clawchat_get_user_profile", params, async () => {
353
+ const p = params as ClawchatGetUserProfileParams;
354
+ return await withClient((c) => c.getUserInfo(p.userId));
355
+ });
219
356
  },
220
357
  },
221
358
  { name: "clawchat_get_user_profile" },
@@ -225,56 +362,332 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
225
362
  {
226
363
  name: "clawchat_list_account_friends",
227
364
  label: "List ClawChat Account Friends",
228
- description:
229
- "List the configured ClawChat account's friends/contacts, paginated (page=1, pageSize=20 by default). " +
230
- "TRIGGER invoke when the user asks for this ClawChat account's friends, contacts, friend list, " +
231
- "or asks to show more friends with pagination.",
365
+ description: toolDescription(
366
+ "List friends/contacts of the agent's connected ClawChat account (the configured ClawChat account). " +
367
+ "These are the agent's ClawChat-platform contacts. " +
368
+ "TRIGGER — invoke when the user asks for this ClawChat account's friends, contacts, or friend list.",
369
+ ),
232
370
  parameters: ClawchatListAccountFriendsSchema,
233
371
  async execute(_callId, params) {
234
- const p = (params ?? {}) as ClawchatListAccountFriendsParams;
235
- return await withClient((c) =>
236
- c.listFriends({
237
- ...(p.page !== undefined ? { page: p.page } : { page: 1 }),
238
- ...(p.pageSize !== undefined ? { pageSize: p.pageSize } : { pageSize: 20 }),
239
- }),
372
+ return await recordClawchatToolCall("clawchat_list_account_friends", params, async () =>
373
+ withClient((c) => c.listFriends()),
240
374
  );
241
375
  },
242
376
  },
243
377
  { name: "clawchat_list_account_friends" },
244
378
  );
245
379
 
380
+ api.registerTool(
381
+ {
382
+ name: "clawchat_search_users",
383
+ label: "Search ClawChat Users",
384
+ description: toolDescription(
385
+ "Search ClawChat users by username or nickname. " +
386
+ "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\". " +
387
+ "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.",
388
+ ),
389
+ parameters: ClawchatSearchUsersSchema,
390
+ async execute(_callId, params) {
391
+ return await recordClawchatToolCall("clawchat_search_users", params, async () => {
392
+ const p = (params ?? {}) as ClawchatSearchUsersParams;
393
+ return await withClient((c) =>
394
+ c.searchUsers({
395
+ ...(typeof p.q === "string" ? { q: p.q } : {}),
396
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
397
+ }),
398
+ );
399
+ });
400
+ },
401
+ },
402
+ { name: "clawchat_search_users" },
403
+ );
404
+
405
+ api.registerTool(
406
+ {
407
+ name: "clawchat_list_conversations",
408
+ label: "List ClawChat Conversations",
409
+ description: toolDescription(
410
+ "List ClawChat direct and group conversations visible to the configured account. " +
411
+ "TRIGGER - invoke when the user asks to list, browse, inspect, or paginate ClawChat conversations, chats, groups, or direct messages. " +
412
+ "This is read-only and does not create, update, leave, dissolve, or administer conversations.",
413
+ ),
414
+ parameters: ClawchatListConversationsSchema,
415
+ async execute(_callId, params) {
416
+ return await recordClawchatToolCall("clawchat_list_conversations", params, async () => {
417
+ const p = (params ?? {}) as ClawchatListConversationsParams;
418
+ const account = resolveCurrent();
419
+ return await withClient(async (c) => {
420
+ const data = await c.listConversations({
421
+ ...(typeof p.before === "string" ? { before: p.before } : {}),
422
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
423
+ });
424
+ for (const conversation of data.conversations) {
425
+ persistConversationCache((store) =>
426
+ upsertConversationSummaryCache(store, account.accountId, conversation),
427
+ );
428
+ }
429
+ return data;
430
+ });
431
+ });
432
+ },
433
+ },
434
+ { name: "clawchat_list_conversations" },
435
+ );
436
+
437
+ api.registerTool(
438
+ {
439
+ name: "clawchat_get_conversation",
440
+ label: "Get ClawChat Conversation",
441
+ description: toolDescription(
442
+ "Fetch read-only ClawChat conversation details, including group membership when returned by the API. " +
443
+ "TRIGGER - invoke when the user asks to inspect a specific ClawChat conversation or group and provides a concrete conversationId. " +
444
+ "This is read-only and does not create, update, leave, dissolve, or administer conversations.",
445
+ ),
446
+ parameters: ClawchatGetConversationSchema,
447
+ async execute(_callId, params) {
448
+ return await recordClawchatToolCall("clawchat_get_conversation", params, async () => {
449
+ const p = params as ClawchatGetConversationParams;
450
+ const account = resolveCurrent();
451
+ const built = buildClient();
452
+ if (!built.ok) return built.error;
453
+ try {
454
+ const data = await built.client.getConversation(p.conversationId);
455
+ persistConversationCache((store) =>
456
+ upsertConversationDetailsCache(store, account.accountId, data.conversation),
457
+ );
458
+ return jsonResponse(data);
459
+ } catch (err) {
460
+ if (err instanceof ClawlingApiError) {
461
+ if (isConversationNotFound(err)) {
462
+ persistConversationCache((store) =>
463
+ store.deleteConversationCache?.({
464
+ platform: "openclaw",
465
+ accountId: account.accountId,
466
+ conversationId: p.conversationId,
467
+ }),
468
+ );
469
+ }
470
+ return apiError(err);
471
+ }
472
+ return genericError(err);
473
+ }
474
+ });
475
+ },
476
+ },
477
+ { name: "clawchat_get_conversation" },
478
+ );
479
+
480
+ api.registerTool(
481
+ {
482
+ name: "clawchat_list_moments",
483
+ label: "List ClawChat Moments",
484
+ description: toolDescription(
485
+ "List the configured ClawChat account's visible moments feed, including moments from the account and its friends. " +
486
+ "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\". " +
487
+ "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.",
488
+ ),
489
+ parameters: ClawchatListMomentsSchema,
490
+ async execute(_callId, params) {
491
+ return await recordClawchatToolCall("clawchat_list_moments", params, async () => {
492
+ const p = (params ?? {}) as ClawchatListMomentsParams;
493
+ return await withClient((c) =>
494
+ c.listMoments({
495
+ ...(typeof p.before === "number" ? { before: p.before } : {}),
496
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
497
+ }),
498
+ );
499
+ });
500
+ },
501
+ },
502
+ { name: "clawchat_list_moments" },
503
+ );
504
+
505
+ api.registerTool(
506
+ {
507
+ name: "clawchat_create_moment",
508
+ label: "Create ClawChat Moment",
509
+ description: toolDescription(
510
+ "Create a new ClawChat moment/dynamic for the configured ClawChat account. " +
511
+ "TRIGGER - invoke when the user asks to publish, post, or send a ClawChat moment/dynamic, such as \"post a ClawChat moment saying ...\", \"发布动态 ...\", or \"发朋友圈 ...\". " +
512
+ "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.",
513
+ ),
514
+ parameters: ClawchatCreateMomentSchema,
515
+ async execute(_callId, params) {
516
+ return await recordClawchatToolCall("clawchat_create_moment", params, async () => {
517
+ const p = (params ?? {}) as ClawchatCreateMomentParams;
518
+ const text = typeof p.text === "string" ? p.text : undefined;
519
+ const images = Array.isArray(p.images) ? p.images : undefined;
520
+ if (!text && (!images || images.length === 0)) {
521
+ return validationError("openclaw-clawchat: at least one of text or images is required");
522
+ }
523
+ return await withClient((c) =>
524
+ c.createMoment({
525
+ ...(text !== undefined ? { text } : {}),
526
+ ...(images !== undefined ? { images } : {}),
527
+ }),
528
+ );
529
+ });
530
+ },
531
+ },
532
+ { name: "clawchat_create_moment" },
533
+ );
534
+
535
+ api.registerTool(
536
+ {
537
+ name: "clawchat_delete_moment",
538
+ label: "Delete ClawChat Moment",
539
+ description: toolDescription(
540
+ "Delete a ClawChat moment by moment id. " +
541
+ "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. " +
542
+ "Only the moment author can delete it. Do not guess the id; list moments first if the user refers to a moment ambiguously.",
543
+ ),
544
+ parameters: ClawchatDeleteMomentSchema,
545
+ async execute(_callId, params) {
546
+ return await recordClawchatToolCall("clawchat_delete_moment", params, async () => {
547
+ const p = params as ClawchatDeleteMomentParams;
548
+ return await withClient((c) => c.deleteMoment(p.momentId));
549
+ });
550
+ },
551
+ },
552
+ { name: "clawchat_delete_moment" },
553
+ );
554
+
555
+ api.registerTool(
556
+ {
557
+ name: "clawchat_toggle_moment_reaction",
558
+ label: "Toggle ClawChat Moment Reaction",
559
+ description: toolDescription(
560
+ "Toggle an emoji reaction on a ClawChat moment. " +
561
+ "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 \"取消这个 👍 反应\". " +
562
+ "The API adds the reaction if missing and removes it if already present. Require a concrete moment id and emoji.",
563
+ ),
564
+ parameters: ClawchatToggleMomentReactionSchema,
565
+ async execute(_callId, params) {
566
+ return await recordClawchatToolCall("clawchat_toggle_moment_reaction", params, async () => {
567
+ const p = params as ClawchatToggleMomentReactionParams;
568
+ if (!p.emoji?.trim()) {
569
+ return validationError("openclaw-clawchat: emoji is required");
570
+ }
571
+ return await withClient((c) =>
572
+ c.toggleMomentReaction({ momentId: p.momentId, emoji: p.emoji }),
573
+ );
574
+ });
575
+ },
576
+ },
577
+ { name: "clawchat_toggle_moment_reaction" },
578
+ );
579
+
580
+ api.registerTool(
581
+ {
582
+ name: "clawchat_create_moment_comment",
583
+ label: "Create ClawChat Moment Comment",
584
+ description: toolDescription(
585
+ "Create a top-level comment on a ClawChat moment. " +
586
+ "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 \"在这条动态下留言 ...\". " +
587
+ "Require a concrete moment id and non-empty text. Use clawchat_reply_moment_comment when the user is replying to another user's comment.",
588
+ ),
589
+ parameters: ClawchatCreateMomentCommentSchema,
590
+ async execute(_callId, params) {
591
+ return await recordClawchatToolCall("clawchat_create_moment_comment", params, async () => {
592
+ const p = params as ClawchatCreateMomentCommentParams;
593
+ if (!p.text?.trim()) {
594
+ return validationError("openclaw-clawchat: text is required");
595
+ }
596
+ return await withClient((c) =>
597
+ c.createMomentComment({ momentId: p.momentId, text: p.text }),
598
+ );
599
+ });
600
+ },
601
+ },
602
+ { name: "clawchat_create_moment_comment" },
603
+ );
604
+
605
+ api.registerTool(
606
+ {
607
+ name: "clawchat_reply_moment_comment",
608
+ label: "Reply To ClawChat Moment Comment",
609
+ description: toolDescription(
610
+ "Reply to an existing ClawChat moment comment with a single-level reply. " +
611
+ "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 \"回复他那条评论 ...\". " +
612
+ "Require concrete moment and comment ids; do not use this for top-level comments.",
613
+ ),
614
+ parameters: ClawchatReplyMomentCommentSchema,
615
+ async execute(_callId, params) {
616
+ return await recordClawchatToolCall("clawchat_reply_moment_comment", params, async () => {
617
+ const p = params as ClawchatReplyMomentCommentParams;
618
+ if (!p.text?.trim()) {
619
+ return validationError("openclaw-clawchat: text is required");
620
+ }
621
+ return await withClient((c) =>
622
+ c.replyMomentComment({
623
+ momentId: p.momentId,
624
+ replyToCommentId: p.replyToCommentId,
625
+ text: p.text,
626
+ }),
627
+ );
628
+ });
629
+ },
630
+ },
631
+ { name: "clawchat_reply_moment_comment" },
632
+ );
633
+
634
+ api.registerTool(
635
+ {
636
+ name: "clawchat_delete_moment_comment",
637
+ label: "Delete ClawChat Moment Comment",
638
+ description: toolDescription(
639
+ "Delete a comment on a ClawChat moment. " +
640
+ "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. " +
641
+ "The caller may delete comments they authored or comments on moments they authored. Do not guess ids; list moments first if needed.",
642
+ ),
643
+ parameters: ClawchatDeleteMomentCommentSchema,
644
+ async execute(_callId, params) {
645
+ return await recordClawchatToolCall("clawchat_delete_moment_comment", params, async () => {
646
+ const p = params as ClawchatDeleteMomentCommentParams;
647
+ return await withClient((c) =>
648
+ c.deleteMomentComment({ momentId: p.momentId, commentId: p.commentId }),
649
+ );
650
+ });
651
+ },
652
+ },
653
+ { name: "clawchat_delete_moment_comment" },
654
+ );
655
+
246
656
  api.registerTool(
247
657
  {
248
658
  name: "clawchat_update_account_profile",
249
659
  label: "Update ClawChat Account Profile",
250
- description:
251
- "Update the configured ClawChat account profile (nickname and/or avatar and/or bio). " +
252
- "TRIGGER — invoke this tool whenever the user's message explicitly asks to change the ClawChat account profile: " +
660
+ description: toolDescription(
661
+ "Update nickname/avatar_url/bio on the agent's connected ClawChat account (the configured ClawChat account), which mirrors the local assistant identity. " +
662
+ "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: " +
253
663
  "(1) ClawChat account nickname/name change: 'change the ClawChat account nickname to X', " +
254
- "'set this ClawChat account name to X', 'ClawChat 昵称改为 X', '账号昵称改成 X', '账号名字叫 X' " +
664
+ "'set this assistant name to X', 'ClawChat 昵称改为 X', '账号昵称改成 X', '账号名字叫 X' " +
255
665
  "→ call with `nickname = X`; " +
256
666
  "(2) ClawChat account avatar/profile-picture change: 'change the ClawChat account avatar', " +
257
- "'use this image as the ClawChat profile picture', 'ClawChat 头像改为 …', '账号头像换成 …' " +
667
+ "'use this image as the assistant profile picture', 'ClawChat 头像改为 …', '账号头像换成 …' " +
258
668
  "→ first obtain the avatar URL (upload via `clawchat_upload_avatar_image`, OR use a provided URL directly), " +
259
669
  "then call this tool with `avatar_url = <url>`; " +
260
670
  "(3) ClawChat account bio/self-introduction change: 'update the ClawChat bio', " +
261
- "'set the ClawChat account self-introduction to X', 'ClawChat 简介改成 X', '账号简介改为 X', '个人简介改为 X' " +
671
+ "'set the assistant self-introduction to X', 'ClawChat 简介改成 X', '账号简介改为 X', '个人简介改为 X' " +
262
672
  "→ call with `bio = X`. " +
263
673
  "You can pass `nickname`, `avatar_url`, and `bio` together in one call, or just one of them. " +
264
- "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.",
674
+ "At least one of the three must be present. Do not frame this as updating a human user's personal account.",
675
+ ),
265
676
  parameters: ClawchatUpdateAccountProfileSchema,
266
677
  async execute(_callId, params) {
267
- const p = (params ?? {}) as ClawchatUpdateAccountProfileParams;
268
- const patch: { nickname?: string; avatar_url?: string; bio?: string } = {};
269
- if (typeof p.nickname === "string") patch.nickname = p.nickname;
270
- if (typeof p.avatar_url === "string") patch.avatar_url = p.avatar_url;
271
- if (typeof p.bio === "string") patch.bio = p.bio;
272
- if (Object.keys(patch).length === 0) {
273
- return validationError(
274
- "openclaw-clawchat: at least one of nickname / avatar / bio is required",
275
- );
276
- }
277
- return await withClient((c): Promise<Profile> => c.updateMyProfile(patch));
678
+ return await recordClawchatToolCall("clawchat_update_account_profile", params, async () => {
679
+ const p = (params ?? {}) as ClawchatUpdateAccountProfileParams;
680
+ const patch: { nickname?: string; avatar_url?: string; bio?: string } = {};
681
+ if (typeof p.nickname === "string") patch.nickname = p.nickname;
682
+ if (typeof p.avatar_url === "string") patch.avatar_url = p.avatar_url;
683
+ if (typeof p.bio === "string") patch.bio = p.bio;
684
+ if (Object.keys(patch).length === 0) {
685
+ return validationError(
686
+ "openclaw-clawchat: at least one of nickname / avatar / bio is required",
687
+ );
688
+ }
689
+ return await withClient((c): Promise<Profile> => c.updateMyProfile(patch));
690
+ });
278
691
  },
279
692
  },
280
693
  { name: "clawchat_update_account_profile" },
@@ -284,36 +697,39 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
284
697
  {
285
698
  name: "clawchat_upload_avatar_image",
286
699
  label: "Upload ClawChat Avatar Image",
287
- description:
288
- "Upload a local image file to ClawChat avatar storage (max 20MB) and return the hosted avatar URL. " +
700
+ description: toolDescription(
701
+ "Upload an absolute local image path for use as the agent's connected ClawChat account avatar (max 20MB), returning a hosted avatar URL. " +
289
702
  "TRIGGER — invoke when the user provides an absolute local image path and asks to upload it for the ClawChat account avatar/profile picture. " +
290
- "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.",
703
+ "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.",
704
+ ),
291
705
  parameters: ClawchatUploadAvatarImageSchema,
292
706
  async execute(_callId, params) {
293
- const p = params as ClawchatUploadAvatarImageParams;
294
- if (!p.filePath || !path.isAbsolute(p.filePath)) {
295
- return validationError("openclaw-clawchat: filePath must be an absolute local path");
296
- }
297
- let stat: fs.Stats;
298
- try {
299
- stat = fs.statSync(p.filePath);
300
- } catch (err) {
301
- return validationError(
302
- `openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`,
303
- );
304
- }
305
- if (!stat.isFile()) {
306
- return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
307
- }
308
- if (stat.size > MAX_UPLOAD_BYTES) {
309
- return validationError(
310
- `openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`,
311
- );
312
- }
313
- const buffer = fs.readFileSync(p.filePath);
314
- const filename = path.basename(p.filePath);
315
- const mime = inferMimeFromPath(p.filePath);
316
- return await withClient((c) => c.uploadAvatar({ buffer, filename, mime }));
707
+ return await recordClawchatToolCall("clawchat_upload_avatar_image", params, async () => {
708
+ const p = params as ClawchatUploadAvatarImageParams;
709
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
710
+ return validationError("openclaw-clawchat: filePath must be an absolute local path");
711
+ }
712
+ let stat: fs.Stats;
713
+ try {
714
+ stat = fs.statSync(p.filePath);
715
+ } catch (err) {
716
+ return validationError(
717
+ `openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`,
718
+ );
719
+ }
720
+ if (!stat.isFile()) {
721
+ return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
722
+ }
723
+ if (stat.size > MAX_UPLOAD_BYTES) {
724
+ return validationError(
725
+ `openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`,
726
+ );
727
+ }
728
+ const buffer = fs.readFileSync(p.filePath);
729
+ const filename = path.basename(p.filePath);
730
+ const mime = inferMimeFromPath(p.filePath);
731
+ return await withClient((c) => c.uploadAvatar({ buffer, filename, mime }));
732
+ });
317
733
  },
318
734
  },
319
735
  { name: "clawchat_upload_avatar_image" },
@@ -323,42 +739,46 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
323
739
  {
324
740
  name: "clawchat_upload_media_file",
325
741
  label: "Upload ClawChat Media File",
326
- description:
327
- "Upload a local file or media file to ClawChat media storage (max 20MB) and return the public URL/shareable URL. " +
742
+ description: toolDescription(
743
+ "Upload an absolute local file/media path to ClawChat media storage (max 20MB) and return a ClawChat-accessible public/shareable URL. " +
328
744
  "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. " +
329
- "Do not use this for account avatar changes; use `clawchat_upload_avatar_image` for avatar images.",
745
+ "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). " +
746
+ "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.",
747
+ ),
330
748
  parameters: ClawchatUploadMediaFileSchema,
331
749
  async execute(_callId, params) {
332
- const p = params as ClawchatUploadMediaFileParams;
333
- if (!p.filePath || !path.isAbsolute(p.filePath)) {
334
- return validationError("openclaw-clawchat: filePath must be an absolute local path");
335
- }
336
- let stat: fs.Stats;
337
- try {
338
- stat = fs.statSync(p.filePath);
339
- } catch (err) {
340
- return validationError(
341
- `openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`,
342
- );
343
- }
344
- if (!stat.isFile()) {
345
- return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
346
- }
347
- if (stat.size > MAX_UPLOAD_BYTES) {
348
- return validationError(
349
- `openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`,
350
- );
351
- }
352
- const buffer = fs.readFileSync(p.filePath);
353
- const filename = path.basename(p.filePath);
354
- const mime = inferMimeFromPath(p.filePath);
355
- return await withClient((c) => c.uploadMedia({ buffer, filename, mime }));
750
+ return await recordClawchatToolCall("clawchat_upload_media_file", params, async () => {
751
+ const p = params as ClawchatUploadMediaFileParams;
752
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
753
+ return validationError("openclaw-clawchat: filePath must be an absolute local path");
754
+ }
755
+ let stat: fs.Stats;
756
+ try {
757
+ stat = fs.statSync(p.filePath);
758
+ } catch (err) {
759
+ return validationError(
760
+ `openclaw-clawchat: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`,
761
+ );
762
+ }
763
+ if (!stat.isFile()) {
764
+ return validationError(`openclaw-clawchat: ${p.filePath} is not a regular file`);
765
+ }
766
+ if (stat.size > MAX_UPLOAD_BYTES) {
767
+ return validationError(
768
+ `openclaw-clawchat: file too large (${stat.size} bytes; max 20MB)`,
769
+ );
770
+ }
771
+ const buffer = fs.readFileSync(p.filePath);
772
+ const filename = path.basename(p.filePath);
773
+ const mime = inferMimeFromPath(p.filePath);
774
+ return await withClient((c) => c.uploadMedia({ buffer, filename, mime }));
775
+ });
356
776
  },
357
777
  },
358
778
  { name: "clawchat_upload_media_file" },
359
779
  );
360
780
 
361
781
  api.logger.debug?.(
362
- "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)",
782
+ "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)",
363
783
  );
364
784
  }