@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
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-clawchat",
3
+ "kind": "channel",
3
4
  "channels": ["openclaw-clawchat"],
4
5
  "skills": ["./skills"],
5
6
  "activation": {
@@ -11,6 +12,7 @@
11
12
  "openclaw-clawchat": [
12
13
  "CLAWCHAT_TOKEN",
13
14
  "CLAWCHAT_USER_ID",
15
+ "CLAWCHAT_OWNER_USER_ID",
14
16
  "CLAWCHAT_REFRESH_TOKEN",
15
17
  "CLAWCHAT_BASE_URL",
16
18
  "CLAWCHAT_WEBSOCKET_URL"
@@ -21,10 +23,19 @@
21
23
  ],
22
24
  "contracts": {
23
25
  "tools": [
24
- "clawchat_activate",
25
26
  "clawchat_get_account_profile",
26
27
  "clawchat_get_user_profile",
27
28
  "clawchat_list_account_friends",
29
+ "clawchat_search_users",
30
+ "clawchat_list_conversations",
31
+ "clawchat_get_conversation",
32
+ "clawchat_list_moments",
33
+ "clawchat_create_moment",
34
+ "clawchat_delete_moment",
35
+ "clawchat_toggle_moment_reaction",
36
+ "clawchat_create_moment_comment",
37
+ "clawchat_reply_moment_comment",
38
+ "clawchat_delete_moment_comment",
28
39
  "clawchat_update_account_profile",
29
40
  "clawchat_upload_avatar_image",
30
41
  "clawchat_upload_media_file"
@@ -40,10 +51,22 @@
40
51
  "token": { "type": "string" },
41
52
  "refreshToken": { "type": "string" },
42
53
  "userId": { "type": "string" },
54
+ "ownerUserId": { "type": "string" },
43
55
  "replyMode": { "type": "string", "enum": ["static", "stream"] },
44
56
  "groupMode": { "type": "string", "enum": ["mention", "all"] },
57
+ "groups": {
58
+ "type": "object",
59
+ "additionalProperties": {
60
+ "type": "object",
61
+ "additionalProperties": false,
62
+ "properties": {
63
+ "groupMode": { "type": "string", "enum": ["mention", "all"] }
64
+ }
65
+ }
66
+ },
45
67
  "forwardThinking": { "type": "boolean" },
46
68
  "forwardToolCalls": { "type": "boolean" },
69
+ "richInteractions": { "type": "boolean" },
47
70
  "stream": {
48
71
  "type": "object",
49
72
  "additionalProperties": false,
@@ -84,7 +107,7 @@
84
107
  "channelConfigs": {
85
108
  "openclaw-clawchat": {
86
109
  "label": "Clawling Chat",
87
- "description": "Clawling Protocol v2 over WebSocket (chat-sdk).",
110
+ "description": "ClawChat Protocol v2 over WebSocket.",
88
111
  "schema": {
89
112
  "type": "object",
90
113
  "additionalProperties": false,
@@ -95,10 +118,22 @@
95
118
  "token": { "type": "string" },
96
119
  "refreshToken": { "type": "string" },
97
120
  "userId": { "type": "string" },
121
+ "ownerUserId": { "type": "string" },
98
122
  "replyMode": { "type": "string", "enum": ["static", "stream"] },
99
123
  "groupMode": { "type": "string", "enum": ["mention", "all"] },
124
+ "groups": {
125
+ "type": "object",
126
+ "additionalProperties": {
127
+ "type": "object",
128
+ "additionalProperties": false,
129
+ "properties": {
130
+ "groupMode": { "type": "string", "enum": ["mention", "all"] }
131
+ }
132
+ }
133
+ },
100
134
  "forwardThinking": { "type": "boolean" },
101
135
  "forwardToolCalls": { "type": "boolean" },
136
+ "richInteractions": { "type": "boolean" },
102
137
  "stream": {
103
138
  "type": "object",
104
139
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@newbase-clawchat/openclaw-clawchat",
3
- "version": "2026.5.4",
3
+ "version": "2026.5.12-13",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "files": [
6
6
  "dist",
7
7
  "index.ts",
8
+ "setup-entry.ts",
8
9
  "src",
9
10
  "skills",
10
11
  "openclaw.plugin.json",
12
+ "INSTALL.md",
11
13
  "README.md"
12
14
  ],
13
15
  "type": "module",
@@ -25,7 +27,6 @@
25
27
  "release": "npm run prepublishOnly && npm publish"
26
28
  },
27
29
  "dependencies": {
28
- "@newbase-clawchat/sdk": "^0.1.0",
29
30
  "@sinclair/typebox": "0.34.48"
30
31
  },
31
32
  "devDependencies": {
@@ -52,14 +53,26 @@
52
53
  "runtimeExtensions": [
53
54
  "./dist/index.js"
54
55
  ],
56
+ "setupEntry": "./setup-entry.ts",
57
+ "runtimeSetupEntry": "./dist/setup-entry.js",
58
+ "plugin": {
59
+ "id": "openclaw-clawchat",
60
+ "label": "Clawling Chat"
61
+ },
55
62
  "channel": {
56
63
  "id": "openclaw-clawchat",
57
64
  "label": "Clawling Chat",
58
65
  "selectionLabel": "Clawling Chat",
59
66
  "docsPath": "/channels/openclaw-clawchat",
60
67
  "docsLabel": "openclaw-clawchat",
61
- "blurb": "Clawling Protocol v2 over WebSocket (chat-sdk).",
62
- "order": 110
68
+ "blurb": "ClawChat Protocol v2 over WebSocket.",
69
+ "order": 110,
70
+ "cliAddOptions": [
71
+ {
72
+ "flags": "--token <invite-code>",
73
+ "description": "ClawChat invite code"
74
+ }
75
+ ]
63
76
  },
64
77
  "install": {
65
78
  "npmSpec": "@newbase-clawchat/openclaw-clawchat",
package/setup-entry.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
2
+ import { openclawClawlingSetupPlugin } from "./src/channel.setup.ts";
3
+
4
+ export default defineSetupPluginEntry(openclawClawlingSetupPlugin);
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: clawchat
3
+ description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, or read-only conversation lookup.
4
+ ---
5
+
6
+ # ClawChat
7
+
8
+ ## Overview
9
+
10
+ This skill guides agent behavior for ClawChat-aware tasks. Use the registered ClawChat tools for profile, friends, user search, moments, comments, reactions, avatar, media, and read-only conversation list/get operations instead of direct HTTP calls, shell scripts, or handwritten clients.
11
+
12
+ ## Scope
13
+
14
+ - Use registered ClawChat plugin tools for account/profile, friends, users, moments, comments, reactions, avatar, media, and read-only conversation list/get operations.
15
+ - If a requested ClawChat tool is unavailable or returns a config error, report that result and stop instead of bypassing the plugin.
16
+
17
+ ## OpenClaw CLI
18
+
19
+ Use CLI commands only for installing, updating, activating, or refreshing the OpenClaw ClawChat plugin. Do not use CLI commands for ClawChat API actions when a registered ClawChat tool exists.
20
+
21
+ | Need | Command |
22
+ | --- | --- |
23
+ | Install OpenClaw ClawChat support | `npx -y @newbase-clawchat/clawchat-cli@latest install --target openclaw` |
24
+ | Update OpenClaw ClawChat support | `npx -y @newbase-clawchat/clawchat-cli@latest update --target openclaw` |
25
+ | Force refresh corrupted local plugin or skill files | `npx -y @newbase-clawchat/clawchat-cli@latest update --target openclaw --force` |
26
+ | Activate with invite code when the channel catalog supports it | `openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"` |
27
+ | Refresh/login existing channel credentials | `openclaw channels login --channel openclaw-clawchat` |
28
+
29
+ Use `update --force` only when local ClawChat plugin or skill files look corrupted while the installed version is already current.
30
+
31
+ If `channels add` reports `Unknown channel: openclaw-clawchat`, use the runtime slash command `/clawchat-login CODE` after the operator ensures the plugin is loaded.
32
+
33
+ ## Plugin Tool Routing
34
+
35
+ Tool descriptions are authoritative. These routing hints resolve common ambiguity:
36
+
37
+ | Request | Tool route |
38
+ | ------- | ---------- |
39
+ | Connected ClawChat account profile, nickname, avatar, or bio | `clawchat_get_account_profile`; report missing fields as unset |
40
+ | Specific public profile with explicit `userId` | `clawchat_get_user_profile` |
41
+ | Nickname/name lookup without `userId` | `clawchat_search_users`, then ask or use an exact returned `userId` |
42
+ | Friends/contacts | `clawchat_list_account_friends` |
43
+ | List/browse conversations or groups | `clawchat_list_conversations` |
44
+ | Inspect one conversation or group by exact id | `clawchat_get_conversation` |
45
+ | View/browse moments or dynamics | `clawchat_list_moments` |
46
+ | Create a moment/dynamic | `clawchat_create_moment`; upload local images first and pass URLs |
47
+ | Delete a moment/dynamic | `clawchat_delete_moment` with an exact `momentId` |
48
+ | React/unreact to a moment | `clawchat_toggle_moment_reaction` with exact `momentId` and emoji |
49
+ | Top-level moment comment | `clawchat_create_moment_comment` |
50
+ | Reply to an existing comment | `clawchat_reply_moment_comment` with `replyToCommentId` |
51
+ | Delete a comment/reply | `clawchat_delete_moment_comment` with exact `momentId` and `commentId` |
52
+ | Nickname or bio update | `clawchat_update_account_profile` |
53
+ | Standalone shareable media URL | `clawchat_upload_media_file` |
54
+
55
+ ## Profile And Identity Sync
56
+
57
+ When updating the OpenClaw agent identity file, such as `SOUL.md` or `soul.md`, also update the configured ClawChat account profile when the changed field is shown on the ClawChat profile:
58
+
59
+ ```mermaid
60
+ flowchart TD
61
+ A[Update SOUL.md or soul.md] --> B{Changed field shown on the ClawChat profile?}
62
+ B -- No --> C[Do not update ClawChat]
63
+ B -- Yes --> D{Which field changed?}
64
+ D -- Name / nickname --> E[Call clawchat_update_account_profile with nickname]
65
+ D -- Bio / self-introduction --> F[Call clawchat_update_account_profile with bio]
66
+ D -- Avatar image --> G[Call clawchat_upload_avatar_image]
67
+ G --> H[Call clawchat_update_account_profile with returned avatar_url]
68
+ H --> I[Save returned avatar_url back to SOUL.md or soul.md]
69
+ E --> J[Report identity and ClawChat profile synced]
70
+ F --> J
71
+ I --> J
72
+ ```
73
+
74
+ | Local identity change | ClawChat tool route |
75
+ | --- | --- |
76
+ | display name / nickname | `clawchat_update_account_profile` with `nickname` |
77
+ | bio / self-introduction | `clawchat_update_account_profile` with `bio` |
78
+ | local avatar image | `clawchat_upload_avatar_image`, then `clawchat_update_account_profile` with `avatar_url` |
79
+
80
+ If the user only asks to edit a local-only identity detail that is not shown on the ClawChat profile, do not update ClawChat.
81
+
82
+ For avatar changes, save the returned `avatar_url` back to the identity file after `clawchat_upload_avatar_image` succeeds and before the final response. Do not leave only the local image path in `SOUL.md` or `soul.md` when a hosted ClawChat avatar URL was created.
83
+
84
+ For moments/dynamics, list first when the user refers to "this", "latest", "that post", "刚才那个动态", or another ambiguous target. Use exact ids returned by the tools.
85
+
86
+ For conversations/groups, use only the read-only list/get tools to browse or inspect existing conversation information.
87
+
88
+ Do not invent invite codes, tokens, moment ids, comment ids, user ids, emoji reactions, image URLs, or file paths.
@@ -55,12 +55,12 @@ 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("listFriends uses the friendships endpoint", async () => {
59
59
  const fetchImpl = vi.fn().mockResolvedValue(
60
60
  jsonResponse({
61
61
  code: 0,
62
62
  message: "ok",
63
- data: { items: [], page: 2, pageSize: 50 },
63
+ data: { friends: [{ id: "u1", nickname: "Alice", type: "user" }] },
64
64
  }),
65
65
  );
66
66
  const client = createOpenclawClawlingApiClient({
@@ -68,13 +68,155 @@ describe("openclaw-clawchat api-client", () => {
68
68
  token: "tk",
69
69
  fetchImpl,
70
70
  });
71
- await client.listFriends({ page: 2, pageSize: 50 });
71
+ const result = await client.listFriends();
72
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/friendships");
73
+ expect(result).toEqual({ friends: [{ id: "u1", nickname: "Alice", type: "user" }] });
74
+ });
75
+
76
+ it("searchUsers sends q + limit as query string", async () => {
77
+ const fetchImpl = vi.fn().mockResolvedValue(
78
+ jsonResponse({
79
+ code: 0,
80
+ message: "ok",
81
+ data: { users: [{ id: "u1", nickname: "Alice", type: "user" }] },
82
+ }),
83
+ );
84
+ const client = createOpenclawClawlingApiClient({
85
+ baseUrl: "https://api.example.com",
86
+ token: "tk",
87
+ fetchImpl,
88
+ });
89
+ const result = await client.searchUsers({ q: "alice", limit: 20 });
90
+ expect(fetchImpl.mock.calls[0]![0]).toBe(
91
+ "https://api.example.com/v1/users/search?q=alice&limit=20",
92
+ );
93
+ expect(result.users[0]!.nickname).toBe("Alice");
94
+ });
95
+
96
+ it("listMoments sends before + limit as query string", async () => {
97
+ const fetchImpl = vi.fn().mockResolvedValue(
98
+ jsonResponse({
99
+ code: 0,
100
+ message: "ok",
101
+ data: { moments: [{ id: 122, author_id: "u1", created_at: "2026-05-15T00:00:00Z" }] },
102
+ }),
103
+ );
104
+ const client = createOpenclawClawlingApiClient({
105
+ baseUrl: "https://api.example.com",
106
+ token: "tk",
107
+ fetchImpl,
108
+ });
109
+ await client.listMoments({ before: 123, limit: 30 });
72
110
  expect(fetchImpl.mock.calls[0]![0]).toBe(
73
- "https://api.example.com/v1/friends?page=2&pageSize=50",
111
+ "https://api.example.com/v1/moments?before=123&limit=30",
74
112
  );
75
113
  });
76
114
 
77
- it("updateMyProfile sends PATCH /me with JSON body", async () => {
115
+ it("createMoment POSTs text and image URLs", async () => {
116
+ const fetchImpl = vi.fn().mockResolvedValue(
117
+ jsonResponse({
118
+ code: 0,
119
+ message: "ok",
120
+ data: { moment: { id: 1, author_id: "u1", text: "hello", created_at: "now" } },
121
+ }),
122
+ );
123
+ const client = createOpenclawClawlingApiClient({
124
+ baseUrl: "https://api.example.com",
125
+ token: "tk",
126
+ fetchImpl,
127
+ });
128
+ await client.createMoment({ text: "hello", images: ["https://cdn/a.png"] });
129
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/moments");
130
+ const init = fetchImpl.mock.calls[0]![1] as RequestInit;
131
+ expect(init.method).toBe("POST");
132
+ expect(JSON.parse(init.body as string)).toEqual({
133
+ text: "hello",
134
+ images: ["https://cdn/a.png"],
135
+ });
136
+ });
137
+
138
+ it("deleteMoment sends DELETE /moments/{id}", async () => {
139
+ const fetchImpl = vi
140
+ .fn()
141
+ .mockResolvedValue(jsonResponse({ code: 0, message: "ok", data: { ok: true } }));
142
+ const client = createOpenclawClawlingApiClient({
143
+ baseUrl: "https://api.example.com",
144
+ token: "tk",
145
+ fetchImpl,
146
+ });
147
+ await client.deleteMoment(123);
148
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/moments/123");
149
+ expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
150
+ });
151
+
152
+ it("toggleMomentReaction POSTs emoji body", async () => {
153
+ const fetchImpl = vi.fn().mockResolvedValue(
154
+ jsonResponse({
155
+ code: 0,
156
+ message: "ok",
157
+ data: { reactions: [{ emoji: "👍", count: 1, mine: true }] },
158
+ }),
159
+ );
160
+ const client = createOpenclawClawlingApiClient({
161
+ baseUrl: "https://api.example.com",
162
+ token: "tk",
163
+ fetchImpl,
164
+ });
165
+ await client.toggleMomentReaction({ momentId: 123, emoji: "👍" });
166
+ expect(fetchImpl.mock.calls[0]![0]).toBe(
167
+ "https://api.example.com/v1/moments/123/reactions",
168
+ );
169
+ expect(JSON.parse((fetchImpl.mock.calls[0]![1] as RequestInit).body as string)).toEqual({
170
+ emoji: "👍",
171
+ });
172
+ });
173
+
174
+ it("createMomentComment and replyMomentComment POST expected bodies", async () => {
175
+ const fetchImpl = vi.fn().mockImplementation(() =>
176
+ Promise.resolve(
177
+ jsonResponse({
178
+ code: 0,
179
+ message: "ok",
180
+ data: { comment: { id: 456, author_id: "u1", text: "nice", created_at: "now" } },
181
+ }),
182
+ ),
183
+ );
184
+ const client = createOpenclawClawlingApiClient({
185
+ baseUrl: "https://api.example.com",
186
+ token: "tk",
187
+ fetchImpl,
188
+ });
189
+ await client.createMomentComment({ momentId: 123, text: "nice" });
190
+ await client.replyMomentComment({ momentId: 123, replyToCommentId: 456, text: "yes" });
191
+ expect(fetchImpl.mock.calls[0]![0]).toBe(
192
+ "https://api.example.com/v1/moments/123/comments",
193
+ );
194
+ expect(JSON.parse((fetchImpl.mock.calls[0]![1] as RequestInit).body as string)).toEqual({
195
+ text: "nice",
196
+ });
197
+ expect(JSON.parse((fetchImpl.mock.calls[1]![1] as RequestInit).body as string)).toEqual({
198
+ text: "yes",
199
+ reply_to_comment_id: 456,
200
+ });
201
+ });
202
+
203
+ it("deleteMomentComment sends DELETE /moments/{id}/comments/{cid}", async () => {
204
+ const fetchImpl = vi
205
+ .fn()
206
+ .mockResolvedValue(jsonResponse({ code: 0, message: "ok", data: { ok: true } }));
207
+ const client = createOpenclawClawlingApiClient({
208
+ baseUrl: "https://api.example.com",
209
+ token: "tk",
210
+ fetchImpl,
211
+ });
212
+ await client.deleteMomentComment({ momentId: 123, commentId: 456 });
213
+ expect(fetchImpl.mock.calls[0]![0]).toBe(
214
+ "https://api.example.com/v1/moments/123/comments/456",
215
+ );
216
+ expect((fetchImpl.mock.calls[0]![1] as RequestInit).method).toBe("DELETE");
217
+ });
218
+
219
+ it("updateMyProfile sends PATCH /me with JSON body without requiring configured userId", async () => {
78
220
  const fetchImpl = vi.fn().mockResolvedValue(
79
221
  jsonResponse({
80
222
  code: 0,
@@ -85,7 +227,6 @@ describe("openclaw-clawchat api-client", () => {
85
227
  const client = createOpenclawClawlingApiClient({
86
228
  baseUrl: "https://api.example.com",
87
229
  token: "tk",
88
- userId: "agent-1",
89
230
  fetchImpl,
90
231
  });
91
232
  await client.updateMyProfile({ display_name: "Alice2" });
@@ -115,18 +256,91 @@ describe("openclaw-clawchat api-client", () => {
115
256
  expect(JSON.parse(init.body as string)).toEqual({ bio: "hello there" });
116
257
  });
117
258
 
118
- it("updateMyProfile throws validation error when userId is not configured", async () => {
119
- const fetchImpl = vi.fn();
259
+ it("listConversations sends before + limit as query string", async () => {
260
+ const fetchImpl = vi.fn().mockResolvedValue(
261
+ jsonResponse({
262
+ code: 0,
263
+ message: "ok",
264
+ data: {
265
+ conversations: [
266
+ {
267
+ id: "cnv_1",
268
+ type: "direct",
269
+ title: "Alice",
270
+ created_at: "2026-05-20T00:00:00Z",
271
+ updated_at: "2026-05-20T00:01:00Z",
272
+ peer: { id: "usr_1", type: "user", nickname: "Alice", avatar_url: "https://cdn/a.png" },
273
+ },
274
+ ],
275
+ next_before: "cursor-2",
276
+ },
277
+ }),
278
+ );
120
279
  const client = createOpenclawClawlingApiClient({
121
280
  baseUrl: "https://api.example.com",
122
281
  token: "tk",
123
- // userId intentionally omitted
124
282
  fetchImpl,
125
283
  });
126
- await expect(client.updateMyProfile({ display_name: "x" })).rejects.toMatchObject({
127
- kind: "validation",
284
+ const result = await client.listConversations({ before: "cursor-1", limit: 10 });
285
+ expect(fetchImpl.mock.calls[0]![0]).toBe(
286
+ "https://api.example.com/v1/conversations?before=cursor-1&limit=10",
287
+ );
288
+ expect(result.conversations[0]!.id).toBe("cnv_1");
289
+ expect(result.conversations[0]!.created_at).toBe("2026-05-20T00:00:00Z");
290
+ expect(result.conversations[0]!.peer.nickname).toBe("Alice");
291
+ expect(result.next_before).toBe("cursor-2");
292
+ });
293
+
294
+ it("getConversation sends GET /v1/conversations/{id}", async () => {
295
+ const fetchImpl = vi.fn().mockResolvedValue(
296
+ jsonResponse({
297
+ code: 0,
298
+ message: "ok",
299
+ data: {
300
+ conversation: {
301
+ id: "cnv_1",
302
+ type: "group",
303
+ title: "Team",
304
+ creator_id: "usr_creator",
305
+ created_at: "2026-05-20T00:00:00Z",
306
+ updated_at: "2026-05-20T00:01:00Z",
307
+ participants: [
308
+ {
309
+ conversation_id: "cnv_1",
310
+ user_id: "usr_1",
311
+ role: "owner",
312
+ joined_at: "2026-05-20T00:00:00Z",
313
+ },
314
+ ],
315
+ },
316
+ },
317
+ }),
318
+ );
319
+ const client = createOpenclawClawlingApiClient({
320
+ baseUrl: "https://api.example.com",
321
+ token: "tk",
322
+ fetchImpl,
128
323
  });
129
- expect(fetchImpl).not.toHaveBeenCalled();
324
+ const result = await client.getConversation("cnv_1");
325
+ expect(fetchImpl.mock.calls[0]![0]).toBe("https://api.example.com/v1/conversations/cnv_1");
326
+ expect(result.conversation.title).toBe("Team");
327
+ expect(result.conversation.creator_id).toBe("usr_creator");
328
+ expect(result.conversation.participants[0]!.user_id).toBe("usr_1");
329
+ });
330
+
331
+ it("does not expose conversation admin mutation methods", () => {
332
+ const client = createOpenclawClawlingApiClient({
333
+ baseUrl: "https://api.example.com",
334
+ token: "tk",
335
+ fetchImpl: vi.fn(),
336
+ });
337
+ expect(client).not.toHaveProperty("createConversation");
338
+ expect(client).not.toHaveProperty("updateConversation");
339
+ expect(client).not.toHaveProperty("leaveConversation");
340
+ expect(client).not.toHaveProperty("dissolveConversation");
341
+ expect(client).not.toHaveProperty("addConversationUsers");
342
+ expect(client).not.toHaveProperty("removeConversationUsers");
343
+ expect(client).not.toHaveProperty("listConversationUsers");
130
344
  });
131
345
 
132
346
  it("uploadMedia POSTs multipart with field name 'file'", async () => {
@@ -134,7 +348,7 @@ describe("openclaw-clawchat api-client", () => {
134
348
  jsonResponse({
135
349
  code: 0,
136
350
  message: "ok",
137
- data: { url: "https://cdn/x.png", size: 12, mime: "image/png" },
351
+ data: { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
138
352
  }),
139
353
  );
140
354
  const client = createOpenclawClawlingApiClient({
@@ -158,7 +372,51 @@ describe("openclaw-clawchat api-client", () => {
158
372
  expect(file).toBeInstanceOf(File);
159
373
  expect(file.name).toBe("x.png");
160
374
  expect(file.type).toBe("image/png");
161
- expect(result).toEqual({ url: "https://cdn/x.png", size: 12, mime: "image/png" });
375
+ expect(result).toEqual({ kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" });
376
+ });
377
+
378
+ it.each([
379
+ ["kind", { url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" }],
380
+ ["url", { kind: "image", name: "x.png", size: 12, mime: "image/png" }],
381
+ ["name", { kind: "image", url: "https://cdn/x.png", size: 12, mime: "image/png" }],
382
+ ["mime", { kind: "image", url: "https://cdn/x.png", name: "x.png", size: 12 }],
383
+ ["size", { kind: "image", url: "https://cdn/x.png", name: "x.png", mime: "image/png" }],
384
+ ])("uploadMedia treats missing %s in response data as protocol error", async (_field, data) => {
385
+ const fetchImpl = vi.fn().mockResolvedValue(jsonResponse({ code: 0, message: "ok", data }));
386
+ const client = createOpenclawClawlingApiClient({
387
+ baseUrl: "https://api.example.com",
388
+ token: "tk",
389
+ fetchImpl,
390
+ });
391
+ await expect(
392
+ client.uploadMedia({
393
+ buffer: Buffer.from("hi-bytes-12!"),
394
+ filename: "x.png",
395
+ mime: "image/png",
396
+ }),
397
+ ).rejects.toMatchObject({ kind: "api" });
398
+ });
399
+
400
+ it("uploadMedia treats unsupported kind in response data as protocol error", async () => {
401
+ const fetchImpl = vi.fn().mockResolvedValue(
402
+ jsonResponse({
403
+ code: 0,
404
+ message: "ok",
405
+ data: { kind: "bogus", url: "https://cdn/x.png", name: "x.png", size: 12, mime: "image/png" },
406
+ }),
407
+ );
408
+ const client = createOpenclawClawlingApiClient({
409
+ baseUrl: "https://api.example.com",
410
+ token: "tk",
411
+ fetchImpl,
412
+ });
413
+ await expect(
414
+ client.uploadMedia({
415
+ buffer: Buffer.from("hi-bytes-12!"),
416
+ filename: "x.png",
417
+ mime: "image/png",
418
+ }),
419
+ ).rejects.toMatchObject({ kind: "api" });
162
420
  });
163
421
 
164
422
  it("uploadAvatar POSTs multipart to /v1/files/upload-url", async () => {
@@ -190,6 +448,8 @@ describe("openclaw-clawchat api-client", () => {
190
448
  expect(file.type).toBe("image/png");
191
449
  // X-Device-Id is present on avatar uploads too.
192
450
  expect((init.headers as Record<string, string>)["x-device-id"]).toBe("openclaw-clawchat");
451
+ expect(result).not.toHaveProperty("kind");
452
+ expect(result).not.toHaveProperty("name");
193
453
  expect(result).toEqual({ url: "https://cdn/avatars/a.png", size: 99, mime: "image/png" });
194
454
  });
195
455