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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +39 -17
  2. package/dist/index.js +3 -1
  3. package/dist/src/api-client.js +71 -12
  4. package/dist/src/api-types.test-d.js +10 -0
  5. package/dist/src/channel.js +5 -5
  6. package/dist/src/channel.setup.js +4 -17
  7. package/dist/src/clawchat-memory.js +290 -0
  8. package/dist/src/clawchat-metadata.js +235 -0
  9. package/dist/src/client.js +31 -93
  10. package/dist/src/commands.js +3 -3
  11. package/dist/src/config.js +58 -3
  12. package/dist/src/group-message-coalescer.js +107 -0
  13. package/dist/src/inbound.js +24 -28
  14. package/dist/src/login.runtime.js +82 -19
  15. package/dist/src/media-runtime.js +2 -3
  16. package/dist/src/message-mapper.js +1 -1
  17. package/dist/src/mock-transport.js +31 -0
  18. package/dist/src/outbound.js +281 -56
  19. package/dist/src/plugin-prompts.js +76 -0
  20. package/dist/src/profile-prompt.js +150 -0
  21. package/dist/src/profile-sync.js +169 -0
  22. package/dist/src/prompt-injection.js +25 -0
  23. package/dist/src/protocol-types.js +63 -0
  24. package/dist/src/protocol-types.typecheck.js +1 -0
  25. package/dist/src/protocol.js +2 -2
  26. package/dist/src/reply-dispatcher.js +143 -40
  27. package/dist/src/runtime.js +813 -109
  28. package/dist/src/storage.js +636 -0
  29. package/dist/src/tools-schema.js +70 -10
  30. package/dist/src/tools.js +600 -112
  31. package/dist/src/ws-alignment.js +8 -0
  32. package/dist/src/ws-client.js +588 -0
  33. package/index.ts +6 -1
  34. package/openclaw.plugin.json +44 -4
  35. package/package.json +4 -3
  36. package/prompts/platform.md +7 -0
  37. package/skills/clawchat/SKILL.md +90 -0
  38. package/src/api-client.test.ts +360 -15
  39. package/src/api-client.ts +127 -25
  40. package/src/api-types.test-d.ts +12 -0
  41. package/src/api-types.ts +71 -4
  42. package/src/buffered-stream.test.ts +1 -1
  43. package/src/buffered-stream.ts +1 -1
  44. package/src/channel.outbound.test.ts +270 -60
  45. package/src/channel.setup.ts +9 -18
  46. package/src/channel.test.ts +33 -25
  47. package/src/channel.ts +5 -7
  48. package/src/clawchat-memory.test.ts +372 -0
  49. package/src/clawchat-memory.ts +363 -0
  50. package/src/clawchat-metadata.test.ts +350 -0
  51. package/src/clawchat-metadata.ts +352 -0
  52. package/src/client.test.ts +57 -48
  53. package/src/client.ts +37 -129
  54. package/src/commands.test.ts +2 -2
  55. package/src/commands.ts +3 -3
  56. package/src/config.test.ts +169 -4
  57. package/src/config.ts +86 -6
  58. package/src/group-message-coalescer.test.ts +223 -0
  59. package/src/group-message-coalescer.ts +154 -0
  60. package/src/inbound.test.ts +106 -19
  61. package/src/inbound.ts +31 -35
  62. package/src/login.runtime.test.ts +294 -11
  63. package/src/login.runtime.ts +90 -21
  64. package/src/manifest.test.ts +86 -14
  65. package/src/media-runtime.test.ts +31 -2
  66. package/src/media-runtime.ts +7 -10
  67. package/src/message-mapper.test.ts +2 -2
  68. package/src/message-mapper.ts +2 -2
  69. package/src/mock-transport.test.ts +35 -0
  70. package/src/mock-transport.ts +38 -0
  71. package/src/outbound.test.ts +811 -95
  72. package/src/outbound.ts +332 -65
  73. package/src/plugin-entry.test.ts +3 -1
  74. package/src/plugin-prompts.test.ts +78 -0
  75. package/src/plugin-prompts.ts +92 -0
  76. package/src/profile-prompt.test.ts +435 -0
  77. package/src/profile-prompt.ts +208 -0
  78. package/src/profile-sync.test.ts +611 -0
  79. package/src/profile-sync.ts +268 -0
  80. package/src/prompt-injection.test.ts +39 -0
  81. package/src/prompt-injection.ts +45 -0
  82. package/src/protocol-types.test.ts +69 -0
  83. package/src/protocol-types.ts +296 -0
  84. package/src/protocol-types.typecheck.ts +89 -0
  85. package/src/protocol.ts +2 -2
  86. package/src/reply-dispatcher.test.ts +720 -135
  87. package/src/reply-dispatcher.ts +174 -42
  88. package/src/runtime.test.ts +3884 -337
  89. package/src/runtime.ts +956 -128
  90. package/src/storage.test.ts +692 -0
  91. package/src/storage.ts +989 -0
  92. package/src/streaming.test.ts +1 -1
  93. package/src/streaming.ts +1 -1
  94. package/src/tools-schema.ts +115 -13
  95. package/src/tools.test.ts +501 -10
  96. package/src/tools.ts +739 -133
  97. package/src/ws-alignment.ts +9 -0
  98. package/src/ws-client.test.ts +1218 -0
  99. package/src/ws-client.ts +662 -0
@@ -55,12 +55,28 @@ describe("openclaw-clawchat api-client", () => {
55
55
  expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/u%2F2");
56
56
  });
57
57
 
58
- it("listFriends sends page + pageSize as query string", async () => {
58
+ it("getUserProfile aliases GET /v1/users/{id}", async () => {
59
+ const fetchImpl = vi.fn().mockResolvedValue(
60
+ jsonResponse({ code: 0, message: "ok", data: { id: "u/2", nickname: "Bob" } }),
61
+ );
62
+ const client = createOpenclawClawlingApiClient({
63
+ baseUrl: "https://api.example.com",
64
+ token: "tk",
65
+ fetchImpl,
66
+ });
67
+
68
+ const profile = await client.getUserProfile("u/2");
69
+
70
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/u%2F2");
71
+ expect(profile).toEqual({ id: "u/2", nickname: "Bob" });
72
+ });
73
+
74
+ it("getAgentDetail calls GET /v1/agents/{agent_id}", async () => {
59
75
  const fetchImpl = vi.fn().mockResolvedValue(
60
76
  jsonResponse({
61
77
  code: 0,
62
78
  message: "ok",
63
- data: { items: [], page: 2, pageSize: 50 },
79
+ data: { agent: { user_id: "agt/1", owner_id: "owner", type: "agent" } },
64
80
  }),
65
81
  );
66
82
  const client = createOpenclawClawlingApiClient({
@@ -68,10 +84,221 @@ describe("openclaw-clawchat api-client", () => {
68
84
  token: "tk",
69
85
  fetchImpl,
70
86
  });
71
- await client.listFriends({ page: 2, pageSize: 50 });
72
- expect(fetchImpl.mock.calls[0]![0]).toBe(
73
- "https://api.example.com/v1/friends?page=2&pageSize=50",
87
+
88
+ await client.getAgentDetail("agt/1");
89
+
90
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/agt%2F1");
91
+ });
92
+
93
+ describe("metadata REST helpers", () => {
94
+ it("patchAgent sends only nickname/avatar_url/bio/behavior to PATCH /v1/agents/{agent_id}", async () => {
95
+ const fetchImpl = vi.fn().mockResolvedValue(
96
+ jsonResponse({
97
+ code: 0,
98
+ message: "ok",
99
+ data: {
100
+ agent: {
101
+ id: "agt/1",
102
+ owner_id: "owner",
103
+ user_id: "agent-user",
104
+ type: "bot",
105
+ nickname: "OpenClaw Bot",
106
+ avatar_url: "https://cdn/agent.png",
107
+ bio: "metadata bio",
108
+ visibility: "public",
109
+ status: "active",
110
+ platform: "openclaw",
111
+ created_at: "2026-05-24T00:00:00Z",
112
+ },
113
+ },
114
+ }),
115
+ );
116
+ const client = createOpenclawClawlingApiClient({
117
+ baseUrl: "https://api.example.com",
118
+ token: "tk",
119
+ fetchImpl,
120
+ });
121
+
122
+ const result = await client.patchAgent("agt/1", {
123
+ nickname: "OpenClaw Bot",
124
+ avatar_url: "https://cdn/agent.png",
125
+ bio: "metadata bio",
126
+ behavior: "do not forward",
127
+ filePath: "owner.md",
128
+ } as never);
129
+
130
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/agents/agt%2F1");
131
+ const init = fetchImpl.mock.calls[0]![1] as RequestInit;
132
+ expect(init.method).toBe("PATCH");
133
+ expect(JSON.parse(init.body as string)).toEqual({
134
+ nickname: "OpenClaw Bot",
135
+ avatar_url: "https://cdn/agent.png",
136
+ bio: "metadata bio",
137
+ behavior: "do not forward",
138
+ });
139
+ expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
140
+ expect(result.agent.nickname).toBe("OpenClaw Bot");
141
+ });
142
+
143
+ it("patchConversation sends only title/description to PATCH /v1/conversations/{id}", async () => {
144
+ const fetchImpl = vi.fn().mockResolvedValue(
145
+ jsonResponse({
146
+ code: 0,
147
+ message: "ok",
148
+ data: {
149
+ conversation: {
150
+ id: "cnv/1",
151
+ type: "group",
152
+ title: "Metadata Team",
153
+ description: "Group metadata",
154
+ creator_id: "usr_creator",
155
+ created_at: "2026-05-20T00:00:00Z",
156
+ updated_at: "2026-05-24T00:00:00Z",
157
+ participants: [],
158
+ },
159
+ },
160
+ }),
161
+ );
162
+ const client = createOpenclawClawlingApiClient({
163
+ baseUrl: "https://api.example.com",
164
+ token: "tk",
165
+ fetchImpl,
166
+ });
167
+
168
+ const result = await client.patchConversation("cnv/1", {
169
+ title: "Metadata Team",
170
+ description: "Group metadata",
171
+ filePath: "groups/cnv_1.md",
172
+ } as never);
173
+
174
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv%2F1");
175
+ const init = fetchImpl.mock.calls[0]![1] as RequestInit;
176
+ expect(init.method).toBe("PATCH");
177
+ expect(JSON.parse(init.body as string)).toEqual({
178
+ title: "Metadata Team",
179
+ description: "Group metadata",
180
+ });
181
+ expect((init.headers as Record<string, string>)["content-type"]).toBe("application/json");
182
+ expect(result.conversation.description).toBe("Group metadata");
183
+ });
184
+
185
+ it("metadata user updates still use updateMyProfile with PATCH /v1/users/me", async () => {
186
+ const fetchImpl = vi.fn().mockResolvedValue(
187
+ jsonResponse({
188
+ code: 0,
189
+ message: "ok",
190
+ data: { id: "usr_1", nickname: "Alice", avatar_url: "https://cdn/a.png", bio: "hello" },
191
+ }),
192
+ );
193
+ const client = createOpenclawClawlingApiClient({
194
+ baseUrl: "https://api.example.com",
195
+ token: "tk",
196
+ fetchImpl,
197
+ });
198
+
199
+ await client.updateMyProfile({
200
+ nickname: "Alice",
201
+ avatar_url: "https://cdn/a.png",
202
+ bio: "hello",
203
+ });
204
+
205
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/users/me");
206
+ const init = fetchImpl.mock.calls[0]![1] as RequestInit;
207
+ expect(init.method).toBe("PATCH");
208
+ expect(JSON.parse(init.body as string)).toEqual({
209
+ nickname: "Alice",
210
+ avatar_url: "https://cdn/a.png",
211
+ bio: "hello",
212
+ });
213
+ });
214
+
215
+ it("metadata getConversation preserves participants for caller mapping", async () => {
216
+ const participants = [
217
+ {
218
+ conversation_id: "cnv_1",
219
+ user_id: "usr_1",
220
+ role: "owner",
221
+ joined_at: "2026-05-20T00:00:00Z",
222
+ user: { id: "usr_1", nickname: "Alice", avatar_url: "https://cdn/a.png" },
223
+ },
224
+ {
225
+ conversation_id: "cnv_1",
226
+ user_id: "usr_2",
227
+ role: "member",
228
+ joined_at: "2026-05-21T00:00:00Z",
229
+ user: { id: "usr_2", nickname: "Bob", avatar_url: "https://cdn/b.png" },
230
+ },
231
+ ];
232
+ const fetchImpl = vi.fn().mockResolvedValue(
233
+ jsonResponse({
234
+ code: 0,
235
+ message: "ok",
236
+ data: {
237
+ conversation: {
238
+ id: "cnv_1",
239
+ type: "group",
240
+ title: "Team",
241
+ creator_id: "usr_1",
242
+ created_at: "2026-05-20T00:00:00Z",
243
+ updated_at: "2026-05-24T00:00:00Z",
244
+ participants,
245
+ },
246
+ },
247
+ }),
248
+ );
249
+ const client = createOpenclawClawlingApiClient({
250
+ baseUrl: "https://api.example.com",
251
+ token: "tk",
252
+ fetchImpl,
253
+ });
254
+
255
+ const result = await client.getConversation("cnv_1");
256
+
257
+ expect(result.conversation.participants).toEqual(participants);
258
+ });
259
+
260
+ it("metadata patch helpers reject blank ids and empty allowed patches before sending", async () => {
261
+ const fetchImpl = vi.fn();
262
+ const client = createOpenclawClawlingApiClient({
263
+ baseUrl: "https://api.example.com",
264
+ token: "tk",
265
+ fetchImpl,
266
+ });
267
+
268
+ await expect(client.patchAgent(" ", { nickname: "Bot" })).rejects.toMatchObject({
269
+ kind: "validation",
270
+ });
271
+ await expect(client.patchAgent("agt_1", {})).rejects.toMatchObject({
272
+ kind: "validation",
273
+ });
274
+ await expect(client.patchConversation("", { title: "Team" })).rejects.toMatchObject({
275
+ kind: "validation",
276
+ });
277
+ await expect(
278
+ client.patchConversation("cnv_1", { filePath: "groups/cnv_1.md" } as never),
279
+ ).rejects.toMatchObject({
280
+ kind: "validation",
281
+ });
282
+ expect(fetchImpl).not.toHaveBeenCalled();
283
+ });
284
+ });
285
+
286
+ it("listFriends uses the friendships endpoint", async () => {
287
+ const fetchImpl = vi.fn().mockResolvedValue(
288
+ jsonResponse({
289
+ code: 0,
290
+ message: "ok",
291
+ data: { friends: [{ id: "u1", nickname: "Alice", type: "user" }] },
292
+ }),
74
293
  );
294
+ const client = createOpenclawClawlingApiClient({
295
+ baseUrl: "https://api.example.com",
296
+ token: "tk",
297
+ fetchImpl,
298
+ });
299
+ const result = await client.listFriends();
300
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/friendships");
301
+ expect(result).toEqual({ friends: [{ id: "u1", nickname: "Alice", type: "user" }] });
75
302
  });
76
303
 
77
304
  it("searchUsers sends q + limit as query string", async () => {
@@ -217,7 +444,7 @@ describe("openclaw-clawchat api-client", () => {
217
444
  expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
218
445
  });
219
446
 
220
- it("updateMyProfile sends PATCH /me with JSON body", async () => {
447
+ it("updateMyProfile sends PATCH /me with JSON body without requiring configured userId", async () => {
221
448
  const fetchImpl = vi.fn().mockResolvedValue(
222
449
  jsonResponse({
223
450
  code: 0,
@@ -228,7 +455,6 @@ describe("openclaw-clawchat api-client", () => {
228
455
  const client = createOpenclawClawlingApiClient({
229
456
  baseUrl: "https://api.example.com",
230
457
  token: "tk",
231
- userId: "agent-1",
232
458
  fetchImpl,
233
459
  });
234
460
  await client.updateMyProfile({ display_name: "Alice2" });
@@ -258,18 +484,91 @@ describe("openclaw-clawchat api-client", () => {
258
484
  expect(JSON.parse(init.body as string)).toEqual({ bio: "hello there" });
259
485
  });
260
486
 
261
- it("updateMyProfile throws validation error when userId is not configured", async () => {
262
- const fetchImpl = vi.fn();
487
+ it("listConversations sends before + limit as query string", async () => {
488
+ const fetchImpl = vi.fn().mockResolvedValue(
489
+ jsonResponse({
490
+ code: 0,
491
+ message: "ok",
492
+ data: {
493
+ conversations: [
494
+ {
495
+ id: "cnv_1",
496
+ type: "direct",
497
+ title: "Alice",
498
+ created_at: "2026-05-20T00:00:00Z",
499
+ updated_at: "2026-05-20T00:01:00Z",
500
+ peer: { id: "usr_1", type: "user", nickname: "Alice", avatar_url: "https://cdn/a.png" },
501
+ },
502
+ ],
503
+ next_before: "cursor-2",
504
+ },
505
+ }),
506
+ );
263
507
  const client = createOpenclawClawlingApiClient({
264
508
  baseUrl: "https://api.example.com",
265
509
  token: "tk",
266
- // userId intentionally omitted
267
510
  fetchImpl,
268
511
  });
269
- await expect(client.updateMyProfile({ display_name: "x" })).rejects.toMatchObject({
270
- kind: "validation",
512
+ const result = await client.listConversations({ before: "cursor-1", limit: 10 });
513
+ expect(fetchImpl.mock.calls[0]![0]).toBe(
514
+ "https://api.example.com/v1/conversations?before=cursor-1&limit=10",
515
+ );
516
+ expect(result.conversations[0]!.id).toBe("cnv_1");
517
+ expect(result.conversations[0]!.created_at).toBe("2026-05-20T00:00:00Z");
518
+ expect(result.conversations[0]!.peer.nickname).toBe("Alice");
519
+ expect(result.next_before).toBe("cursor-2");
520
+ });
521
+
522
+ it("getConversation sends GET /v1/conversations/{id}", async () => {
523
+ const fetchImpl = vi.fn().mockResolvedValue(
524
+ jsonResponse({
525
+ code: 0,
526
+ message: "ok",
527
+ data: {
528
+ conversation: {
529
+ id: "cnv_1",
530
+ type: "group",
531
+ title: "Team",
532
+ creator_id: "usr_creator",
533
+ created_at: "2026-05-20T00:00:00Z",
534
+ updated_at: "2026-05-20T00:01:00Z",
535
+ participants: [
536
+ {
537
+ conversation_id: "cnv_1",
538
+ user_id: "usr_1",
539
+ role: "owner",
540
+ joined_at: "2026-05-20T00:00:00Z",
541
+ },
542
+ ],
543
+ },
544
+ },
545
+ }),
546
+ );
547
+ const client = createOpenclawClawlingApiClient({
548
+ baseUrl: "https://api.example.com",
549
+ token: "tk",
550
+ fetchImpl,
271
551
  });
272
- expect(fetchImpl).not.toHaveBeenCalled();
552
+ const result = await client.getConversation("cnv_1");
553
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv_1");
554
+ expect(result.conversation.title).toBe("Team");
555
+ expect(result.conversation.creator_id).toBe("usr_creator");
556
+ expect(result.conversation.participants[0]!.user_id).toBe("usr_1");
557
+ });
558
+
559
+ it("does not expose conversation admin mutation methods", () => {
560
+ const client = createOpenclawClawlingApiClient({
561
+ baseUrl: "https://api.example.com",
562
+ token: "tk",
563
+ fetchImpl: vi.fn(),
564
+ });
565
+ expect(client).not.toHaveProperty("createConversation");
566
+ expect(client).not.toHaveProperty("updateConversation");
567
+ expect(client).not.toHaveProperty("leaveConversation");
568
+ expect(client).not.toHaveProperty("dissolveConversation");
569
+ expect(client).not.toHaveProperty("addConversationUsers");
570
+ expect(client).not.toHaveProperty("removeConversationUsers");
571
+ expect(client).not.toHaveProperty("listConversationUsers");
273
572
  });
274
573
 
275
574
  it("uploadMedia POSTs multipart with field name 'file'", async () => {
@@ -277,7 +576,7 @@ describe("openclaw-clawchat api-client", () => {
277
576
  jsonResponse({
278
577
  code: 0,
279
578
  message: "ok",
280
- data: { url: "https://cdn/x.png", size: 12, mime: "image/png" },
579
+ data: { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
281
580
  }),
282
581
  );
283
582
  const client = createOpenclawClawlingApiClient({
@@ -301,7 +600,51 @@ describe("openclaw-clawchat api-client", () => {
301
600
  expect(file).toBeInstanceOf(File);
302
601
  expect(file.name).toBe("x.png");
303
602
  expect(file.type).toBe("image/png");
304
- expect(result).toEqual({ url: "https://cdn/x.png", size: 12, mime: "image/png" });
603
+ expect(result).toEqual({ kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" });
604
+ });
605
+
606
+ it.each([
607
+ ["kind", { url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" }],
608
+ ["url", { kind: "image", name: "x.png", size: 12, mime: "image/png" }],
609
+ ["name", { kind: "image", url: "https://cdn/x.png", size: 12, mime: "image/png" }],
610
+ ["mime", { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12 }],
611
+ ["size", { kind: "image", url: "https://cdn/x.png", name: "x.png", mime: "image/png" }],
612
+ ])("uploadMedia treats missing %s in response data as protocol error", async (_field, data) => {
613
+ const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ code: 0, message: "ok", data }));
614
+ const client = createOpenclawClawlingApiClient({
615
+ baseUrl: "https://api.example.com",
616
+ token: "tk",
617
+ fetchImpl,
618
+ });
619
+ await expect(
620
+ client.uploadMedia({
621
+ buffer: Buffer.from("hi-bytes-12!"),
622
+ filename: "x.png",
623
+ mime: "image/png",
624
+ }),
625
+ ).rejects.toMatchObject({ kind: "api" });
626
+ });
627
+
628
+ it("uploadMedia treats unsupported kind in response data as protocol error", async () => {
629
+ const fetchImpl = vi.fn().mockResolvedValue(
630
+ jsonResponse({
631
+ code: 0,
632
+ message: "ok",
633
+ data: { kind: "bogus", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
634
+ }),
635
+ );
636
+ const client = createOpenclawClawlingApiClient({
637
+ baseUrl: "https://api.example.com",
638
+ token: "tk",
639
+ fetchImpl,
640
+ });
641
+ await expect(
642
+ client.uploadMedia({
643
+ buffer: Buffer.from("hi-bytes-12!"),
644
+ filename: "x.png",
645
+ mime: "image/png",
646
+ }),
647
+ ).rejects.toMatchObject({ kind: "api" });
305
648
  });
306
649
 
307
650
  it("uploadAvatar POSTs multipart to /v1/files/upload-url", async () => {
@@ -333,6 +676,8 @@ describe("openclaw-clawchat api-client", () => {
333
676
  expect(file.type).toBe("image/png");
334
677
  // X-Device-Id is present on avatar uploads too.
335
678
  expect((init.headers as Record<string, string>)["x-device-id"]).toBe("openclaw-clawchat");
679
+ expect(result).not.toHaveProperty("kind");
680
+ expect(result).not.toHaveProperty("name");
336
681
  expect(result).toEqual({ url: "https://cdn/avatars/a.png", size: 99, mime: "image/png" });
337
682
  });
338
683
 
package/src/api-client.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import {
2
2
  ClawlingApiError,
3
+ type AgentMetadataPatch,
4
+ type AgentProfile,
3
5
  type AgentConnectResult,
6
+ type AvatarUploadResult,
7
+ type ConversationDetails,
8
+ type ConversationListResult,
9
+ type ConversationMetadataPatch,
4
10
  type FriendList,
5
11
  type MomentComment,
6
12
  type MomentReaction,
@@ -14,11 +20,7 @@ import { CHANNEL_ID } from "./config.ts";
14
20
  export interface ApiClientOptions {
15
21
  baseUrl: string;
16
22
  token: string;
17
- /**
18
- * Logged-in agent's `user_id`. Required for `updateMyProfile`, which
19
- * targets `/v1/agents/{userId}`. Safe to omit for unauthenticated /
20
- * pre-login calls (e.g. `agentsConnect`).
21
- */
23
+ /** Logged-in agent's `user_id`, used by WebSocket setup but not REST `/me` calls. */
22
24
  userId?: string;
23
25
  /** Test override only. Defaults to global `fetch`. */
24
26
  fetchImpl?: typeof fetch;
@@ -26,8 +28,12 @@ export interface ApiClientOptions {
26
28
 
27
29
  export interface OpenclawClawlingApiClient {
28
30
  getMyProfile(): Promise<Profile>;
31
+ getAgentProfile(agentId: string): Promise<{ agent: AgentProfile }>;
32
+ getAgentDetail(agentId: string): Promise<{ agent: AgentProfile }>;
33
+ patchAgent(agentId: string, patch: AgentMetadataPatch): Promise<{ agent: AgentProfile }>;
29
34
  getUserInfo(userId: string): Promise<Profile>;
30
- listFriends(params: { page?: number; pageSize?: number }): Promise<FriendList>;
35
+ getUserProfile(userId: string): Promise<Profile>;
36
+ listFriends(): Promise<FriendList>;
31
37
  searchUsers(params: { q?: string; limit?: number }): Promise<{ users: UserSearchHit[] }>;
32
38
  listMoments(params: { before?: number; limit?: number }): Promise<{ moments: MomentView[] }>;
33
39
  createMoment(body: { text?: string; images?: string[] }): Promise<{ moment: MomentView }>;
@@ -46,6 +52,12 @@ export interface OpenclawClawlingApiClient {
46
52
  text: string;
47
53
  }): Promise<{ comment: MomentComment }>;
48
54
  deleteMomentComment(params: { momentId: number; commentId: number }): Promise<{ ok: boolean }>;
55
+ listConversations(params: { before?: string; limit?: number }): Promise<ConversationListResult>;
56
+ getConversation(conversationId: string): Promise<{ conversation: ConversationDetails }>;
57
+ patchConversation(
58
+ conversationId: string,
59
+ patch: ConversationMetadataPatch,
60
+ ): Promise<{ conversation: ConversationDetails }>;
49
61
  updateMyProfile(patch: { nickname?: string; avatar_url?: string; bio?: string }): Promise<Profile>;
50
62
  uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
51
63
  /**
@@ -61,15 +73,14 @@ export interface OpenclawClawlingApiClient {
61
73
  type: string;
62
74
  }): Promise<AgentConnectResult>;
63
75
  /**
64
- * Upload an avatar image via `POST /v1/files/upload-url`. Returns the
65
- * same `UploadResult` shape as `uploadMedia` the resulting `url` is
66
- * what you then pass to `updateMyProfile({ avatar: url })`.
76
+ * Upload an avatar image via `POST /v1/files/upload-url`. The resulting
77
+ * `url` is what you then pass to `updateMyProfile({ avatar_url: url })`.
67
78
  */
68
79
  uploadAvatar(params: {
69
80
  buffer: Buffer;
70
81
  filename: string;
71
82
  mime?: string;
72
- }): Promise<UploadResult>;
83
+ }): Promise<AvatarUploadResult>;
73
84
  }
74
85
 
75
86
  export function createOpenclawClawlingApiClient(opts: ApiClientOptions): OpenclawClawlingApiClient {
@@ -173,6 +184,50 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
173
184
  return await readEnvelope<T>(res, path);
174
185
  }
175
186
 
187
+ function parseUploadResult(data: unknown, path: string): UploadResult {
188
+ const obj = data as Partial<UploadResult> | null;
189
+ const validKind =
190
+ obj?.kind === "image" || obj?.kind === "file" || obj?.kind === "audio" || obj?.kind === "video";
191
+ if (
192
+ !obj ||
193
+ !validKind ||
194
+ typeof obj.url !== "string" ||
195
+ typeof obj.name !== "string" ||
196
+ typeof obj.mime !== "string" ||
197
+ typeof obj.size !== "number"
198
+ ) {
199
+ throw new ClawlingApiError(
200
+ "api",
201
+ "invalid upload response: missing required media fields",
202
+ { path },
203
+ );
204
+ }
205
+ return obj as UploadResult;
206
+ }
207
+
208
+ function assertNonBlankId(value: string, label: string): void {
209
+ if (!value.trim()) {
210
+ throw new ClawlingApiError("validation", `${label} is required`);
211
+ }
212
+ }
213
+
214
+ function pickPatch<T extends object>(
215
+ patch: Record<string, unknown>,
216
+ keys: readonly (keyof T & string)[],
217
+ label: string,
218
+ ): T {
219
+ const body: Record<string, unknown> = {};
220
+ for (const key of keys) {
221
+ if (Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== undefined) {
222
+ body[key] = patch[key];
223
+ }
224
+ }
225
+ if (Object.keys(body).length === 0) {
226
+ throw new ClawlingApiError("validation", `${label} patch must include at least one mutable field`);
227
+ }
228
+ return body as T;
229
+ }
230
+
176
231
  // All JSON API endpoints live under `/v1/...`. Media upload is the one
177
232
  // intentional exception — the upstream server mounts it at `/media/upload`
178
233
  // without the version prefix.
@@ -180,15 +235,32 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
180
235
  async getMyProfile(): Promise<Profile> {
181
236
  return await call<Profile>("GET", "/v1/users/me");
182
237
  },
238
+ async getAgentProfile(agentId: string): Promise<{ agent: AgentProfile }> {
239
+ return await call<{ agent: AgentProfile }>("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
240
+ },
241
+ async getAgentDetail(agentId: string): Promise<{ agent: AgentProfile }> {
242
+ return await call<{ agent: AgentProfile }>("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
243
+ },
244
+ async patchAgent(agentId: string, patch: AgentMetadataPatch): Promise<{ agent: AgentProfile }> {
245
+ assertNonBlankId(agentId, "patchAgent: agentId");
246
+ const body = pickPatch<AgentMetadataPatch>(
247
+ patch as Record<string, unknown>,
248
+ ["nickname", "avatar_url", "bio", "behavior"],
249
+ "patchAgent",
250
+ );
251
+ return await call<{ agent: AgentProfile }>("PATCH", `/v1/agents/${encodeURIComponent(agentId)}`, {
252
+ body: JSON.stringify(body),
253
+ headers: { "content-type": "application/json" },
254
+ });
255
+ },
183
256
  async getUserInfo(userId: string): Promise<Profile> {
184
257
  return await call<Profile>("GET", `/v1/users/${encodeURIComponent(userId)}`);
185
258
  },
186
- async listFriends(params): Promise<FriendList> {
187
- const sp = new URLSearchParams();
188
- if (typeof params.page === "number") sp.set("page", String(params.page));
189
- if (typeof params.pageSize === "number") sp.set("pageSize", String(params.pageSize));
190
- const q = sp.toString();
191
- return await call<FriendList>("GET", q ? `/v1/friends?${q}` : "/v1/friends");
259
+ async getUserProfile(userId: string): Promise<Profile> {
260
+ return await call<Profile>("GET", `/v1/users/${encodeURIComponent(userId)}`);
261
+ },
262
+ async listFriends(): Promise<FriendList> {
263
+ return await call<FriendList>("GET", "/v1/friendships");
192
264
  },
193
265
  async searchUsers(params): Promise<{ users: UserSearchHit[] }> {
194
266
  const sp = new URLSearchParams();
@@ -255,13 +327,42 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
255
327
  `/v1/moments/${encodeURIComponent(String(params.momentId))}/comments/${encodeURIComponent(String(params.commentId))}`,
256
328
  );
257
329
  },
330
+ async listConversations(params): Promise<ConversationListResult> {
331
+ const sp = new URLSearchParams();
332
+ if (typeof params.before === "string") sp.set("before", params.before);
333
+ if (typeof params.limit === "number") sp.set("limit", String(params.limit));
334
+ const q = sp.toString();
335
+ return await call<ConversationListResult>(
336
+ "GET",
337
+ q ? `/v1/conversations?${q}` : "/v1/conversations",
338
+ );
339
+ },
340
+ async getConversation(conversationId): Promise<{ conversation: ConversationDetails }> {
341
+ return await call<{ conversation: ConversationDetails }>(
342
+ "GET",
343
+ `/v1/conversations/${encodeURIComponent(conversationId)}`,
344
+ );
345
+ },
346
+ async patchConversation(
347
+ conversationId: string,
348
+ patch: ConversationMetadataPatch,
349
+ ): Promise<{ conversation: ConversationDetails }> {
350
+ assertNonBlankId(conversationId, "patchConversation: conversationId");
351
+ const body = pickPatch<ConversationMetadataPatch>(
352
+ patch as Record<string, unknown>,
353
+ ["title", "description"],
354
+ "patchConversation",
355
+ );
356
+ return await call<{ conversation: ConversationDetails }>(
357
+ "PATCH",
358
+ `/v1/conversations/${encodeURIComponent(conversationId)}`,
359
+ {
360
+ body: JSON.stringify(body),
361
+ headers: { "content-type": "application/json" },
362
+ },
363
+ );
364
+ },
258
365
  async updateMyProfile(patch): Promise<Profile> {
259
- if (!opts.userId?.trim()) {
260
- throw new ClawlingApiError(
261
- "validation",
262
- "updateMyProfile: userId is required to target /v1/agents/{userId}",
263
- );
264
- }
265
366
  return await call<Profile>(
266
367
  "PATCH",
267
368
  `/v1/users/me`,
@@ -300,9 +401,10 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
300
401
  });
301
402
  const fd = new FormData();
302
403
  fd.set("file", file);
303
- return await call<UploadResult>("POST", "/media/upload", { body: fd });
404
+ const data = await call<unknown>("POST", "/media/upload", { body: fd });
405
+ return parseUploadResult(data, "/media/upload");
304
406
  },
305
- async uploadAvatar(params): Promise<UploadResult> {
407
+ async uploadAvatar(params): Promise<AvatarUploadResult> {
306
408
  const blob = new Blob([new Uint8Array(params.buffer)], {
307
409
  type: params.mime ?? "application/octet-stream",
308
410
  });
@@ -311,7 +413,7 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
311
413
  });
312
414
  const fd = new FormData();
313
415
  fd.set("file", file);
314
- return await call<UploadResult>("POST", "/v1/files/upload-url", { body: fd });
416
+ return await call<AvatarUploadResult>("POST", "/v1/files/upload-url", { body: fd });
315
417
  },
316
418
  };
317
419
  }
@@ -0,0 +1,12 @@
1
+ import type { ConversationListItem } from "./api-types.ts";
2
+
3
+ const listItemWithSoftDeletedPeer: ConversationListItem = {
4
+ id: "cnv_1",
5
+ type: "direct",
6
+ title: "Deleted peer",
7
+ created_at: "2026-05-20T00:00:00Z",
8
+ updated_at: "2026-05-20T00:01:00Z",
9
+ peer: null,
10
+ };
11
+
12
+ void listItemWithSoftDeletedPeer;