@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.test.ts CHANGED
@@ -1,34 +1,49 @@
1
1
  import fs from "node:fs";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
5
  import { registerOpenclawClawlingTools } from "./tools.ts";
4
6
 
5
- const loginRuntime = vi.hoisted(() => ({
6
- runOpenclawClawlingLogin: vi.fn(),
7
- }));
8
-
9
- vi.mock("./login.runtime.ts", () => loginRuntime);
10
-
11
7
  interface RegisteredTool {
12
8
  name: string;
13
9
  execute: (callId: string, params: unknown) => Promise<unknown>;
14
10
  }
15
11
 
16
12
  const ALWAYS_VISIBLE_TOOL_NAMES = [
17
- "clawchat_activate",
13
+ "clawchat_create_moment",
14
+ "clawchat_create_moment_comment",
15
+ "clawchat_delete_moment",
16
+ "clawchat_delete_moment_comment",
18
17
  "clawchat_get_account_profile",
18
+ "clawchat_get_conversation",
19
19
  "clawchat_get_user_profile",
20
20
  "clawchat_list_account_friends",
21
+ "clawchat_list_conversations",
22
+ "clawchat_list_moments",
23
+ "clawchat_reply_moment_comment",
24
+ "clawchat_search_users",
25
+ "clawchat_toggle_moment_reaction",
21
26
  "clawchat_update_account_profile",
22
27
  "clawchat_upload_avatar_image",
23
28
  "clawchat_upload_media_file",
24
29
  ];
25
30
 
31
+ const tempRoots: string[] = [];
32
+
33
+ afterEach(() => {
34
+ for (const dir of tempRoots.splice(0)) {
35
+ fs.rmSync(dir, { recursive: true, force: true });
36
+ }
37
+ });
38
+
26
39
  function buildApi(opts: {
27
40
  configChannel?: Record<string, unknown> | null;
28
41
  configTools?: Record<string, unknown>;
29
42
  registerTool?: (tool: { name: string }, options?: { name: string }) => void;
30
43
  }) {
31
44
  const registered: RegisteredTool[] = [];
45
+ const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-clawchat-tools-"));
46
+ tempRoots.push(stateDir);
32
47
  const api = {
33
48
  config:
34
49
  opts.configChannel === null
@@ -44,6 +59,9 @@ function buildApi(opts: {
44
59
  error: vi.fn(),
45
60
  },
46
61
  runtime: {
62
+ state: {
63
+ resolveStateDir: () => stateDir,
64
+ },
47
65
  config: {
48
66
  mutateConfigFile: vi.fn(),
49
67
  },
@@ -65,10 +83,6 @@ function configuredChannel(extra: Record<string, unknown> = {}) {
65
83
  }
66
84
 
67
85
  describe("registerOpenclawClawlingTools", () => {
68
- beforeEach(() => {
69
- vi.clearAllMocks();
70
- });
71
-
72
86
  it("uses OpenClaw SDK tool result types instead of direct pi-agent-core imports", () => {
73
87
  const source = fs.readFileSync(new URL("./tools.ts", import.meta.url), "utf8");
74
88
  expect(source).not.toMatch(/@mariozechner\/pi-agent-core/);
@@ -98,44 +112,33 @@ describe("registerOpenclawClawlingTools", () => {
98
112
  });
99
113
  });
100
114
 
101
- it("registers clawchat_activate for invite-code onboarding", async () => {
115
+ it("does not register invite-code activation as an agent tool", () => {
102
116
  const { api, registered } = buildApi({
103
117
  configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
104
118
  });
105
119
 
106
120
  registerOpenclawClawlingTools(api);
107
121
 
108
- const tool = registered.find((t) => t.name === "clawchat_activate");
109
- expect(tool).toBeDefined();
110
- loginRuntime.runOpenclawClawlingLogin.mockResolvedValueOnce(undefined);
111
-
112
- const result = await tool!.execute("call-1", { code: "A1B2C3" });
113
-
114
- expect(loginRuntime.runOpenclawClawlingLogin).toHaveBeenCalledTimes(1);
115
- const params = loginRuntime.runOpenclawClawlingLogin.mock.calls[0]?.[0];
116
- expect(params.cfg).toBe(api.config);
117
- expect(params.mutateConfigFile).toBe(api.runtime.config.mutateConfigFile);
118
- await expect(params.readInviteCode()).resolves.toBe("A1B2C3");
119
- const text = (result as { content: { text: string }[] }).content[0]!.text;
120
- const parsed = JSON.parse(text) as { ok?: boolean; message?: string };
121
- expect(parsed.ok).toBe(true);
122
- expect(parsed.message).toMatch(/activated successfully/i);
122
+ expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
123
123
  });
124
124
 
125
- it("clawchat_activate rejects empty invite codes", async () => {
125
+ it("registers only read-only conversation tools", () => {
126
126
  const { api, registered } = buildApi({
127
- configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
127
+ configChannel: configuredChannel(),
128
128
  });
129
129
 
130
130
  registerOpenclawClawlingTools(api);
131
- const tool = registered.find((t) => t.name === "clawchat_activate")!;
132
- const result = await tool.execute("call-1", { code: " " });
133
131
 
134
- expect(loginRuntime.runOpenclawClawlingLogin).not.toHaveBeenCalled();
135
- const text = (result as { content: { text: string }[] }).content[0]!.text;
136
- const parsed = JSON.parse(text) as { error?: string; message?: string };
137
- expect(parsed.error).toBe("validation");
138
- expect(parsed.message).toMatch(/code is required/i);
132
+ const names = registered.map((t) => t.name);
133
+ expect(names).toContain("clawchat_list_conversations");
134
+ expect(names).toContain("clawchat_get_conversation");
135
+ expect(names).not.toContain("clawchat_create_group_conversation");
136
+ expect(names).not.toContain("clawchat_update_conversation");
137
+ expect(names).not.toContain("clawchat_leave_conversation");
138
+ expect(names).not.toContain("clawchat_dissolve_conversation");
139
+ expect(names).not.toContain("clawchat_add_conversation_member");
140
+ expect(names).not.toContain("clawchat_remove_conversation_member");
141
+ expect(names).not.toContain("clawchat_list_conversation_users");
139
142
  });
140
143
 
141
144
  it("skips registration when api.config is undefined", () => {
@@ -144,7 +147,7 @@ describe("registerOpenclawClawlingTools", () => {
144
147
  expect(registered).toHaveLength(0);
145
148
  });
146
149
 
147
- it("registers all seven ClawChat tools when configured (regardless of baseUrl)", () => {
150
+ it("registers all account/media/search/moment ClawChat tools when configured (regardless of baseUrl)", () => {
148
151
  const { api, registered } = buildApi({
149
152
  configChannel: configuredChannel(/* no baseUrl */),
150
153
  });
@@ -166,18 +169,10 @@ describe("registerOpenclawClawlingTools", () => {
166
169
 
167
170
  expect(logger.info).not.toHaveBeenCalled();
168
171
  expect(logger.debug).toHaveBeenCalledWith(
169
- "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)",
172
+ "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)",
170
173
  );
171
174
  });
172
175
 
173
- it("registers clawchat_activate when configured", () => {
174
- const { api, registered } = buildApi({
175
- configChannel: configuredChannel(),
176
- });
177
- registerOpenclawClawlingTools(api);
178
- expect(registered.some((t) => t.name === "clawchat_activate")).toBe(true);
179
- });
180
-
181
176
  it("account tools return a config error before activation instead of disappearing", async () => {
182
177
  const { api, registered } = buildApi({
183
178
  configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
@@ -193,6 +188,283 @@ describe("registerOpenclawClawlingTools", () => {
193
188
  expect(parsed.message).toMatch(/token is required/i);
194
189
  });
195
190
 
191
+ it("records successful clawchat tool calls without changing the returned result", async () => {
192
+ const store = { recordToolCall: vi.fn() };
193
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
194
+ new Response(
195
+ JSON.stringify({
196
+ code: 0,
197
+ msg: "ok",
198
+ data: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
199
+ }),
200
+ { status: 200, headers: { "content-type": "application/json" } },
201
+ ),
202
+ );
203
+ try {
204
+ const { api, registered } = buildApi({
205
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
206
+ });
207
+ registerOpenclawClawlingTools(api, { store });
208
+ const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
209
+
210
+ const result = await tool.execute("call-1", {});
211
+
212
+ expect((result as { details: unknown }).details).toEqual({
213
+ user_id: "u",
214
+ nickname: "Bot",
215
+ avatar_url: "",
216
+ bio: "",
217
+ });
218
+ expect(store.recordToolCall).toHaveBeenCalledTimes(1);
219
+ expect(store.recordToolCall).toHaveBeenCalledWith(
220
+ expect.objectContaining({
221
+ platform: "openclaw",
222
+ accountId: "default",
223
+ toolName: "clawchat_get_account_profile",
224
+ args: {},
225
+ result: { user_id: "u", nickname: "Bot", avatar_url: "", bio: "" },
226
+ error: null,
227
+ startedAt: expect.any(Number),
228
+ endedAt: expect.any(Number),
229
+ }),
230
+ );
231
+ } finally {
232
+ fetchMock.mockRestore();
233
+ }
234
+ });
235
+
236
+ it("lists conversations, upserts returned summaries, and does not fetch or delete extra cache entries", async () => {
237
+ const store = {
238
+ recordToolCall: vi.fn(),
239
+ upsertConversationSummary: vi.fn(),
240
+ upsertConversationDetails: vi.fn(),
241
+ deleteConversationCache: vi.fn(),
242
+ };
243
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
244
+ new Response(
245
+ JSON.stringify({
246
+ code: 0,
247
+ msg: "ok",
248
+ data: {
249
+ conversations: [
250
+ {
251
+ id: "cnv_1",
252
+ type: "direct",
253
+ title: "Alice",
254
+ created_at: "2026-05-01T00:00:00Z",
255
+ updated_at: "2026-05-02T00:00:00Z",
256
+ peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
257
+ },
258
+ ],
259
+ next_before: "cursor-2",
260
+ },
261
+ }),
262
+ { status: 200, headers: { "content-type": "application/json" } },
263
+ ),
264
+ );
265
+ try {
266
+ const { api, registered } = buildApi({
267
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
268
+ });
269
+ registerOpenclawClawlingTools(api, { store });
270
+ const tool = registered.find((t) => t.name === "clawchat_list_conversations")!;
271
+
272
+ const result = await tool.execute("call-1", { before: "cursor-1", limit: 10 });
273
+
274
+ expect((result as { details: unknown }).details).toEqual({
275
+ conversations: [
276
+ {
277
+ id: "cnv_1",
278
+ type: "direct",
279
+ title: "Alice",
280
+ created_at: "2026-05-01T00:00:00Z",
281
+ updated_at: "2026-05-02T00:00:00Z",
282
+ peer: { id: "alice", type: "user", nickname: "Alice", avatar_url: "" },
283
+ },
284
+ ],
285
+ next_before: "cursor-2",
286
+ });
287
+ expect(fetchMock).toHaveBeenCalledTimes(1);
288
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations?before=cursor-1&limit=10");
289
+ expect(store.upsertConversationSummary).toHaveBeenCalledWith(
290
+ expect.objectContaining({
291
+ platform: "openclaw",
292
+ accountId: "default",
293
+ conversationId: "cnv_1",
294
+ conversationType: "direct",
295
+ raw: expect.objectContaining({ id: "cnv_1" }),
296
+ }),
297
+ );
298
+ expect(store.upsertConversationDetails).not.toHaveBeenCalled();
299
+ expect(store.deleteConversationCache).not.toHaveBeenCalled();
300
+ } finally {
301
+ fetchMock.mockRestore();
302
+ }
303
+ });
304
+
305
+ it("gets a conversation and upserts full details", async () => {
306
+ const store = {
307
+ recordToolCall: vi.fn(),
308
+ upsertConversationSummary: vi.fn(),
309
+ upsertConversationDetails: vi.fn(),
310
+ deleteConversationCache: vi.fn(),
311
+ };
312
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
313
+ new Response(
314
+ JSON.stringify({
315
+ code: 0,
316
+ msg: "ok",
317
+ data: {
318
+ conversation: {
319
+ id: "group_1",
320
+ type: "group",
321
+ title: "Project Group",
322
+ creator_id: "owner",
323
+ created_at: "2026-05-01T00:00:00Z",
324
+ updated_at: "2026-05-02T00:00:00Z",
325
+ participants: [
326
+ { conversation_id: "group_1", user_id: "owner", role: "owner", joined_at: "2026-05-01T00:00:00Z" },
327
+ ],
328
+ },
329
+ },
330
+ }),
331
+ { status: 200, headers: { "content-type": "application/json" } },
332
+ ),
333
+ );
334
+ try {
335
+ const { api, registered } = buildApi({
336
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
337
+ });
338
+ registerOpenclawClawlingTools(api, { store });
339
+ const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
340
+
341
+ await tool.execute("call-1", { conversationId: "group_1" });
342
+
343
+ expect(fetchMock).toHaveBeenCalledTimes(1);
344
+ expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.example.com/v1/conversations/group_1");
345
+ expect(store.upsertConversationDetails).toHaveBeenCalledWith(
346
+ expect.objectContaining({
347
+ platform: "openclaw",
348
+ accountId: "default",
349
+ conversationId: "group_1",
350
+ conversationType: "group",
351
+ raw: expect.objectContaining({ id: "group_1" }),
352
+ groupProfile: expect.objectContaining({ title: "Project Group" }),
353
+ members: [expect.objectContaining({ userId: "owner", role: "owner" })],
354
+ membersComplete: true,
355
+ }),
356
+ );
357
+ } finally {
358
+ fetchMock.mockRestore();
359
+ }
360
+ });
361
+
362
+ it("deletes conversation-scoped cache and returns the standard error result when get conversation is not found", async () => {
363
+ const store = {
364
+ recordToolCall: vi.fn(),
365
+ upsertConversationSummary: vi.fn(),
366
+ upsertConversationDetails: vi.fn(),
367
+ deleteConversationCache: vi.fn(),
368
+ };
369
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
370
+ new Response(JSON.stringify({ code: 40401, msg: "conversation not found", data: null }), {
371
+ status: 200,
372
+ headers: { "content-type": "application/json" },
373
+ }),
374
+ );
375
+ try {
376
+ const { api, registered } = buildApi({
377
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
378
+ });
379
+ registerOpenclawClawlingTools(api, { store });
380
+ const tool = registered.find((t) => t.name === "clawchat_get_conversation")!;
381
+
382
+ const result = await tool.execute("call-1", { conversationId: "missing" });
383
+
384
+ const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
385
+ error?: string;
386
+ message?: string;
387
+ meta?: { code?: number };
388
+ };
389
+ expect(parsed).toMatchObject({
390
+ error: "api",
391
+ message: "conversation not found",
392
+ meta: { code: 40401 },
393
+ });
394
+ expect(store.deleteConversationCache).toHaveBeenCalledWith({
395
+ platform: "openclaw",
396
+ accountId: "default",
397
+ conversationId: "missing",
398
+ });
399
+ expect(store.upsertConversationSummary).not.toHaveBeenCalled();
400
+ expect(store.upsertConversationDetails).not.toHaveBeenCalled();
401
+ } finally {
402
+ fetchMock.mockRestore();
403
+ }
404
+ });
405
+
406
+ it("does not write conversation cache from account, user, friend, search, avatar, or media tools", async () => {
407
+ const store = {
408
+ recordToolCall: vi.fn(),
409
+ upsertConversationSummary: vi.fn(),
410
+ upsertConversationDetails: vi.fn(),
411
+ deleteConversationCache: vi.fn(),
412
+ };
413
+ const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue(
414
+ new Response(
415
+ JSON.stringify({
416
+ code: 0,
417
+ msg: "ok",
418
+ data: { id: "u", nickname: "Bot", avatar_url: "", bio: "" },
419
+ }),
420
+ { status: 200, headers: { "content-type": "application/json" } },
421
+ ),
422
+ );
423
+ try {
424
+ const { api, registered } = buildApi({
425
+ configChannel: configuredChannel({ baseUrl: "https://api.example.com" }),
426
+ });
427
+ registerOpenclawClawlingTools(api, { store });
428
+ const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
429
+
430
+ await tool.execute("call-1", {});
431
+
432
+ expect(store.upsertConversationSummary).not.toHaveBeenCalled();
433
+ expect(store.upsertConversationDetails).not.toHaveBeenCalled();
434
+ expect(store.deleteConversationCache).not.toHaveBeenCalled();
435
+ } finally {
436
+ fetchMock.mockRestore();
437
+ }
438
+ });
439
+
440
+ it("records clawchat tool failures while preserving the returned error result", async () => {
441
+ const store = { recordToolCall: vi.fn() };
442
+ const { api, registered } = buildApi({
443
+ configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
444
+ });
445
+ registerOpenclawClawlingTools(api, { store });
446
+ const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
447
+
448
+ const result = await tool.execute("call-1", {});
449
+
450
+ const parsed = JSON.parse((result as { content: { text: string }[] }).content[0]!.text) as {
451
+ error?: string;
452
+ message?: string;
453
+ };
454
+ expect(parsed).toMatchObject({ error: "config" });
455
+ expect(store.recordToolCall).toHaveBeenCalledWith(
456
+ expect.objectContaining({
457
+ platform: "openclaw",
458
+ accountId: "default",
459
+ toolName: "clawchat_get_account_profile",
460
+ args: {},
461
+ result: parsed,
462
+ error: expect.stringContaining("config"),
463
+ }),
464
+ );
465
+ expect(registered.every((tool) => tool.name.startsWith("clawchat_"))).toBe(true);
466
+ });
467
+
196
468
  it("clawchat_update_account_profile description names account profile triggers (EN + ZH)", () => {
197
469
  const { api } = buildApi({ configChannel: configuredChannel() });
198
470
  const fullTools: Array<{ name: string; description?: string }> = [];
@@ -217,8 +489,16 @@ describe("registerOpenclawClawlingTools", () => {
217
489
 
218
490
  it("account query and upload tool descriptions include precise trigger semantics", () => {
219
491
  const { api } = buildApi({ configChannel: configuredChannel() });
220
- const fullTools: Array<{ name: string; description?: string }> = [];
221
- api.registerTool = (tool: { name: string; description?: string }) => {
492
+ const fullTools: Array<{
493
+ name: string;
494
+ description?: string;
495
+ parameters?: { properties?: Record<string, unknown> };
496
+ }> = [];
497
+ api.registerTool = (tool: {
498
+ name: string;
499
+ description?: string;
500
+ parameters?: { properties?: Record<string, unknown> };
501
+ }) => {
222
502
  fullTools.push(tool);
223
503
  };
224
504
  registerOpenclawClawlingTools(api);
@@ -239,7 +519,8 @@ describe("registerOpenclawClawlingTools", () => {
239
519
 
240
520
  expect(friends.description).toMatch(/configured ClawChat account|logged-in ClawChat account/i);
241
521
  expect(friends.description).toMatch(/friends|contacts/i);
242
- expect(friends.description).toMatch(/page=1|pageSize=20/);
522
+ expect(friends.description).not.toMatch(/paginated|page=1|pageSize=20/i);
523
+ expect(friends.parameters?.properties ?? {}).toEqual({});
243
524
 
244
525
  expect(avatar.description).toMatch(/local image/i);
245
526
  expect(avatar.description).toMatch(/avatar URL|hosted avatar URL|public URL/i);
@@ -251,6 +532,52 @@ describe("registerOpenclawClawlingTools", () => {
251
532
  expect(media.description).toMatch(/not.*avatar|do not use.*avatar/i);
252
533
  });
253
534
 
535
+ it("search and moment tool descriptions match reviewed trigger semantics", () => {
536
+ const { api } = buildApi({ configChannel: configuredChannel() });
537
+ const fullTools: Array<{ name: string; description?: string }> = [];
538
+ api.registerTool = (tool: { name: string; description?: string }) => {
539
+ fullTools.push(tool);
540
+ };
541
+ registerOpenclawClawlingTools(api);
542
+
543
+ const search = fullTools.find((t) => t.name === "clawchat_search_users")!;
544
+ const listMoments = fullTools.find((t) => t.name === "clawchat_list_moments")!;
545
+ const createMoment = fullTools.find((t) => t.name === "clawchat_create_moment")!;
546
+ const deleteMoment = fullTools.find((t) => t.name === "clawchat_delete_moment")!;
547
+ const reaction = fullTools.find((t) => t.name === "clawchat_toggle_moment_reaction")!;
548
+ const comment = fullTools.find((t) => t.name === "clawchat_create_moment_comment")!;
549
+ const reply = fullTools.find((t) => t.name === "clawchat_reply_moment_comment")!;
550
+ const deleteComment = fullTools.find((t) => t.name === "clawchat_delete_moment_comment")!;
551
+
552
+ for (const tool of [
553
+ search,
554
+ listMoments,
555
+ createMoment,
556
+ deleteMoment,
557
+ reaction,
558
+ comment,
559
+ reply,
560
+ deleteComment,
561
+ ]) {
562
+ expect(tool.description).toMatch(/Do not use execute/);
563
+ expect(tool.description).toMatch(/direct ClawChat HTTP calls/);
564
+ }
565
+
566
+ expect(search.description).toMatch(/Search ClawChat users by username or nickname/);
567
+ expect(search.description).toMatch(/do not guess a userId/);
568
+ expect(listMoments.description).toMatch(/moments\/dynamics\/feed/);
569
+ expect(listMoments.description).toMatch(/friends-only feed endpoint/);
570
+ expect(createMoment.description).toMatch(/At least one of text or images/);
571
+ expect(createMoment.description).toMatch(/do not pass local file paths as images/);
572
+ expect(deleteMoment.description).toMatch(/Do not guess the id/);
573
+ expect(reaction.description).toMatch(/adds the reaction if missing and removes it/);
574
+ expect(comment.description).toMatch(/top-level comment/);
575
+ expect(comment.description).toMatch(/Use clawchat_reply_moment_comment/);
576
+ expect(reply.description).toMatch(/single-level reply/);
577
+ expect(reply.description).toMatch(/do not use this for top-level comments/);
578
+ expect(deleteComment.description).toMatch(/moment and comment ids/);
579
+ });
580
+
254
581
  it("clawchat_upload_avatar_image rejects oversized files before upload", async () => {
255
582
  const fs = await import("node:fs/promises");
256
583
  const path = await import("node:path");