@clawling/clawchat-plugin-openclaw 2026.5.12-30 → 2026.5.12-31

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 (60) hide show
  1. package/README.md +3 -48
  2. package/dist/src/api-client.js +37 -4
  3. package/dist/src/config.js +4 -0
  4. package/dist/src/login.runtime.js +1 -0
  5. package/dist/src/outbound.js +1 -0
  6. package/dist/src/reply-dispatcher.js +1 -0
  7. package/dist/src/runtime.js +1 -0
  8. package/dist/src/tools-schema.js +32 -0
  9. package/dist/src/tools.js +129 -14
  10. package/openclaw.plugin.json +9 -1
  11. package/package.json +7 -3
  12. package/prompts/default-group-bio.md +14 -14
  13. package/prompts/default-owner-behavior.md +22 -22
  14. package/skills/clawchat/SKILL.md +5 -1
  15. package/src/api-client.ts +61 -5
  16. package/src/api-types.ts +17 -0
  17. package/src/config.ts +7 -0
  18. package/src/login.runtime.ts +1 -0
  19. package/src/outbound.ts +1 -0
  20. package/src/reply-dispatcher.ts +1 -0
  21. package/src/runtime.ts +1 -0
  22. package/src/tools-schema.ts +47 -0
  23. package/src/tools.ts +169 -13
  24. package/INSTALL.md +0 -64
  25. package/dist/src/api-types.test-d.js +0 -10
  26. package/dist/src/protocol-types.typecheck.js +0 -1
  27. package/src/api-client.test.ts +0 -827
  28. package/src/channel.outbound.test.ts +0 -433
  29. package/src/channel.test.ts +0 -279
  30. package/src/clawchat-memory.test.ts +0 -482
  31. package/src/clawchat-metadata.test.ts +0 -477
  32. package/src/client.test.ts +0 -169
  33. package/src/commands.test.ts +0 -108
  34. package/src/config.test.ts +0 -352
  35. package/src/group-message-coalescer.test.ts +0 -227
  36. package/src/inbound.test.ts +0 -508
  37. package/src/llm-context-debug.test.ts +0 -55
  38. package/src/login.runtime.test.ts +0 -737
  39. package/src/manifest.test.ts +0 -360
  40. package/src/media-runtime.test.ts +0 -207
  41. package/src/message-mapper.test.ts +0 -201
  42. package/src/mock-transport.test.ts +0 -35
  43. package/src/outbound.test.ts +0 -1269
  44. package/src/plugin-entry.test.ts +0 -43
  45. package/src/plugin-prompts.test.ts +0 -94
  46. package/src/profile-prompt.test.ts +0 -290
  47. package/src/profile-sync.test.ts +0 -539
  48. package/src/prompt-injection.test.ts +0 -39
  49. package/src/protocol-types.test.ts +0 -69
  50. package/src/protocol-types.typecheck.ts +0 -89
  51. package/src/protocol.test.ts +0 -39
  52. package/src/reply-dispatcher.test.ts +0 -1352
  53. package/src/runtime.test.ts +0 -4920
  54. package/src/scripts.test.ts +0 -85
  55. package/src/storage.test.ts +0 -566
  56. package/src/terminal-send.test.ts +0 -81
  57. package/src/tools.test.ts +0 -933
  58. package/src/ws-alignment.test.ts +0 -103
  59. package/src/ws-client.test.ts +0 -1217
  60. package/src/ws-log.test.ts +0 -32
package/README.md CHANGED
@@ -20,13 +20,10 @@ npm i @clawling/clawchat-plugin-openclaw
20
20
 
21
21
  Requires `openclaw >= 2026.5.4` as a peer host.
22
22
 
23
- For the OpenClaw plugin install/update flow, use the R2-hosted tarball install
24
- command documented in [`INSTALL.md`](./INSTALL.md).
25
-
26
23
  Example LLM prompt:
27
24
 
28
25
  ```text
29
- Use https://raw.githubusercontent.com/clawling/clawchat-plugin-openclaw/refs/heads/main/INSTALL.md to install and activate the ClawChat plugin. The invite code is XXXXXX.
26
+ Install and activate the ClawChat plugin from npm package @clawling/clawchat-plugin-openclaw. The invite code is XXXXXX.
30
27
  ```
31
28
 
32
29
  ## Current activation paths
@@ -165,7 +162,8 @@ npm run typecheck
165
162
 
166
163
  Tests live next to the source they cover (`*.test.ts`). The development entrypoint stays in TypeScript for the OpenClaw extension loader, while npm installs use the compiled runtime entrypoint generated by `npm run build` / `prepack` under `dist/`.
167
164
 
168
- Functional e2e test cases are documented in `.e2e/docs/install-clawchat-plugin-e2e.md`; keep that guide updated when adding or changing e2e flows.
165
+ Functional E2E test cases are documented in the root ClawChat workspace E2E
166
+ docs; keep that guide updated when adding or changing E2E flows.
169
167
 
170
168
  For OpenClaw host SDK/source lookup while developing this plugin, optionally
171
169
  clone OpenClaw into `tmp/openclaw`:
@@ -178,49 +176,6 @@ npm run dev:openclaw-source
178
176
  This checkout is local-only. It is ignored by git and is not required to run the
179
177
  plugin tests or publish the package.
180
178
 
181
- ## R2 package scripts
182
-
183
- Create and upload the OpenClaw plugin tarball to the R2 `openclaw/` prefix:
184
-
185
- ```bash
186
- ./scripts/package_openclaw_plugin.sh
187
- ```
188
-
189
- The script runs `npm pack`, removes `devDependencies` from the generated `.tgz`
190
- metadata so OpenClaw installs only runtime dependencies, uploads the `.tgz` to
191
- the configured R2 bucket, updates the `latest` R2 alias, uploads `INSTALL.md` as
192
- `openclaw/install.md`, and prints the public URLs. R2 credentials are read from
193
- `scripts/.env.r2`, which is ignored by git. Copy `scripts/.env.r2.example` to
194
- `scripts/.env.r2` and fill in the credentials. Use `--no-upload` to build the
195
- tarball without uploading it.
196
-
197
- ```bash
198
- AWS_ACCESS_KEY_ID=...
199
- AWS_SECRET_ACCESS_KEY=...
200
- AWS_DEFAULT_REGION=auto
201
- R2_ENDPOINT=https://...
202
- R2_BUCKET=...
203
- ```
204
-
205
- Install the R2-hosted latest tarball on a device or container with OpenClaw
206
- available:
207
-
208
- ```bash
209
- ./scripts/install_openclaw.sh
210
- ```
211
-
212
- To install a specific uploaded version, pass the version string:
213
-
214
- ```bash
215
- ./scripts/install_openclaw.sh 2026.5.16-1
216
- ```
217
-
218
- To install a specific uploaded tarball URL, pass its URL explicitly:
219
-
220
- ```bash
221
- ./scripts/install_openclaw.sh https://plugin.clawling.chat/openclaw/newbase-clawchat-clawchat-plugin-openclaw-2026.5.16-1.tgz
222
- ```
223
-
224
179
  ## License
225
180
 
226
181
  See the repository root.
@@ -5,9 +5,16 @@ export function createOpenclawClawlingApiClient(opts) {
5
5
  throw new ClawlingApiError("validation", `clawchat-plugin-openclaw baseUrl must start with http:// or https:// (got "${opts.baseUrl}")`);
6
6
  }
7
7
  const baseUrl = opts.baseUrl.replace(/\/+$/, "");
8
+ let mediaBaseUrl = baseUrl;
9
+ if (opts.mediaBaseUrl && opts.mediaBaseUrl.trim()) {
10
+ if (!/^https?:\/\//i.test(opts.mediaBaseUrl)) {
11
+ throw new ClawlingApiError("validation", `clawchat-plugin-openclaw mediaBaseUrl must start with http:// or https:// (got "${opts.mediaBaseUrl}")`);
12
+ }
13
+ mediaBaseUrl = opts.mediaBaseUrl.replace(/\/+$/, "");
14
+ }
8
15
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
9
- function url(path) {
10
- return `${baseUrl}${path}`;
16
+ function url(path, base = baseUrl) {
17
+ return `${base}${path}`;
11
18
  }
12
19
  function authHeaders(extra = {}) {
13
20
  // `X-Device-Id` is sent on every request so the server can correlate
@@ -75,7 +82,7 @@ export function createOpenclawClawlingApiClient(opts) {
75
82
  if (init?.body !== undefined) {
76
83
  requestInit.body = init.body;
77
84
  }
78
- res = await fetchImpl(url(path), requestInit);
85
+ res = await fetchImpl(url(path, init?.baseUrl || undefined), requestInit);
79
86
  }
80
87
  catch (err) {
81
88
  throw new ClawlingApiError("transport", `fetch failed: ${err instanceof Error ? err.message : String(err)}`, { path });
@@ -148,6 +155,32 @@ export function createOpenclawClawlingApiClient(opts) {
148
155
  async listFriends() {
149
156
  return await call("GET", "/v1/friendships");
150
157
  },
158
+ async sendFriendRequest(params) {
159
+ assertNonBlankId(params.userId, "sendFriendRequest: userId");
160
+ return await call("POST", "/v1/friendships", {
161
+ body: JSON.stringify({
162
+ user_id: params.userId,
163
+ ...(typeof params.greeting === "string" ? { greeting: params.greeting } : {}),
164
+ }),
165
+ headers: { "content-type": "application/json" },
166
+ });
167
+ },
168
+ async listFriendRequests(params) {
169
+ if (params.direction !== "incoming" && params.direction !== "outgoing") {
170
+ throw new ClawlingApiError("validation", "listFriendRequests: direction must be incoming or outgoing");
171
+ }
172
+ return await call("GET", `/v1/friendships/requests/${params.direction}`);
173
+ },
174
+ async acceptFriendRequest(requestId) {
175
+ return await call("POST", `/v1/friendships/requests/${encodeURIComponent(String(requestId))}/accept`);
176
+ },
177
+ async rejectFriendRequest(requestId) {
178
+ return await call("POST", `/v1/friendships/requests/${encodeURIComponent(String(requestId))}/reject`);
179
+ },
180
+ async removeFriend(friendUserId) {
181
+ assertNonBlankId(friendUserId, "removeFriend: friendUserId");
182
+ return await call("DELETE", `/v1/friendships/${encodeURIComponent(friendUserId)}`);
183
+ },
151
184
  async searchUsers(params) {
152
185
  const sp = new URLSearchParams();
153
186
  if (typeof params.q === "string")
@@ -245,7 +278,7 @@ export function createOpenclawClawlingApiClient(opts) {
245
278
  });
246
279
  const fd = new FormData();
247
280
  fd.set("file", file);
248
- const data = await call("POST", "/media/upload", { body: fd });
281
+ const data = await call("POST", "/media/upload", { body: fd, baseUrl: mediaBaseUrl });
249
282
  return parseUploadResult(data, "/media/upload");
250
283
  },
251
284
  async uploadAvatar(params) {
@@ -7,6 +7,7 @@ export const CLAWCHAT_OWNER_USER_ID_ENV = "CLAWCHAT_OWNER_USER_ID";
7
7
  export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
8
8
  export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
9
9
  export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
10
+ export const CLAWCHAT_MEDIA_BASE_URL_ENV = "CLAWCHAT_MEDIA_BASE_URL";
10
11
  /**
11
12
  * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
12
13
  * login` works out of the box without requiring a prior `openclaw channel
@@ -46,6 +47,7 @@ export const openclawClawlingConfigSchema = {
46
47
  enabled: { type: "boolean" },
47
48
  websocketUrl: { type: "string" },
48
49
  baseUrl: { type: "string" },
50
+ mediaBaseUrl: { type: "string" },
49
51
  token: { type: "string" },
50
52
  refreshToken: { type: "string" },
51
53
  agentId: { type: "string" },
@@ -253,6 +255,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
253
255
  const baseUrl = readOptionalString(channel.baseUrl) ||
254
256
  readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
255
257
  DEFAULT_BASE_URL;
258
+ const mediaBaseUrl = readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
256
259
  const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
257
260
  const agentId = readOptionalString(channel.agentId) || readEnvString(env, CLAWCHAT_AGENT_ID_ENV);
258
261
  const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
@@ -276,6 +279,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
276
279
  }),
277
280
  websocketUrl,
278
281
  baseUrl,
282
+ mediaBaseUrl,
279
283
  token,
280
284
  agentId,
281
285
  userId,
@@ -131,6 +131,7 @@ export async function runOpenclawClawlingLogin(params) {
131
131
  }
132
132
  const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
133
133
  baseUrl: account.baseUrl,
134
+ mediaBaseUrl: account.mediaBaseUrl,
134
135
  // Pre-login we may not have a token yet. Send the current one (or empty)
135
136
  // — the server should accept an unauthenticated invite-code exchange.
136
137
  token: account.token || "",
@@ -581,6 +581,7 @@ export const openclawClawlingOutbound = {
581
581
  const runtime = getOpenclawClawlingRuntime();
582
582
  const apiClient = createOpenclawClawlingApiClient({
583
583
  baseUrl: account.baseUrl,
584
+ mediaBaseUrl: account.mediaBaseUrl,
584
585
  token: account.token,
585
586
  userId: account.userId,
586
587
  });
@@ -125,6 +125,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
125
125
  return null;
126
126
  return createOpenclawClawlingApiClient({
127
127
  baseUrl: account.baseUrl,
128
+ mediaBaseUrl: account.mediaBaseUrl,
128
129
  token: account.token,
129
130
  userId: account.userId,
130
131
  });
@@ -364,6 +364,7 @@ export async function startOpenclawClawlingGateway(params) {
364
364
  const getConversationApiClient = () => {
365
365
  conversationApiClient ??= createOpenclawClawlingApiClient({
366
366
  baseUrl: account.baseUrl,
367
+ mediaBaseUrl: account.mediaBaseUrl,
367
368
  token: account.token,
368
369
  userId: account.userId,
369
370
  });
@@ -76,6 +76,38 @@ export const ClawchatGetUserProfileSchema = Type.Object({
76
76
  }),
77
77
  });
78
78
  export const ClawchatListAccountFriendsSchema = Type.Object({});
79
+ export const ClawchatSendFriendRequestSchema = Type.Object({
80
+ userId: Type.String({
81
+ minLength: 1,
82
+ description: "Explicit target ClawChat user id to send a friend request to.",
83
+ }),
84
+ greeting: Type.Optional(Type.String({
85
+ description: "Optional greeting/message included with the friend request.",
86
+ })),
87
+ });
88
+ export const ClawchatListFriendRequestsSchema = Type.Object({
89
+ direction: Type.Optional(Type.Union([Type.Literal("incoming"), Type.Literal("outgoing")], {
90
+ description: "incoming lists requests sent to the connected account; outgoing lists requests sent by the connected account. Defaults to incoming.",
91
+ })),
92
+ });
93
+ export const ClawchatAcceptFriendRequestSchema = Type.Object({
94
+ requestId: Type.Integer({
95
+ minimum: 1,
96
+ description: "Concrete friend request id to accept.",
97
+ }),
98
+ });
99
+ export const ClawchatRejectFriendRequestSchema = Type.Object({
100
+ requestId: Type.Integer({
101
+ minimum: 1,
102
+ description: "Concrete friend request id to reject.",
103
+ }),
104
+ });
105
+ export const ClawchatRemoveFriendSchema = Type.Object({
106
+ friendUserId: Type.String({
107
+ minLength: 1,
108
+ description: "Explicit ClawChat user id of the accepted friend to remove.",
109
+ }),
110
+ });
79
111
  export const ClawchatSearchUsersSchema = Type.Object({
80
112
  q: Type.Optional(Type.String({
81
113
  description: "Search query for ClawChat username or nickname",
package/dist/src/tools.js CHANGED
@@ -9,9 +9,50 @@ import { getOpenclawClawlingClient, } from "./runtime.js";
9
9
  import { markTerminalClawChatSend } from "./terminal-send.js";
10
10
  import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
11
11
  import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
12
- import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatSearchUsersSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatUploadMediaFileSchema, } from "./tools-schema.js";
12
+ import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatUploadMediaFileSchema, } from "./tools-schema.js";
13
13
  const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
14
- function jsonResponse(data) {
14
+ // Owner-approval gate business codes (must match member-backend codes.go).
15
+ const CODE_PENDING_APPROVAL = 21001;
16
+ const CODE_POLICY_FORBIDDEN = 21003;
17
+ function extractRequestId(err) {
18
+ // Defensive: request_id may ride in meta.data when the envelope carried it.
19
+ const data = err.meta?.data;
20
+ if (data && typeof data === "object") {
21
+ const v = data.request_id;
22
+ if (typeof v === "string" && v.length > 0)
23
+ return v;
24
+ }
25
+ return undefined;
26
+ }
27
+ function permissionGateResult(err) {
28
+ const code = err.meta?.code;
29
+ const requestId = extractRequestId(err);
30
+ let message;
31
+ let status;
32
+ if (code === CODE_PENDING_APPROVAL) {
33
+ message =
34
+ "This operation requires the owner's approval and has been submitted for review" +
35
+ (requestId ? ` (request_id=${requestId})` : "") +
36
+ ". It has NOT failed. Do not retry — the result will arrive later as a normal " +
37
+ "chat message; wait for it instead of calling this tool again.";
38
+ status = "pending";
39
+ }
40
+ else {
41
+ message =
42
+ "This operation is blocked by the owner's policy (policy_forbidden) and was not " +
43
+ "performed. Do not retry — the owner must change the policy before it can succeed.";
44
+ status = "forbidden";
45
+ }
46
+ return jsonResponse({
47
+ error: "permission",
48
+ message,
49
+ retryable: false,
50
+ status,
51
+ ...(requestId ? { request_id: requestId } : {}),
52
+ ...(err.meta ? { meta: err.meta } : {}),
53
+ });
54
+ }
55
+ export function jsonResponse(data) {
15
56
  return {
16
57
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
17
58
  details: data,
@@ -20,7 +61,11 @@ function jsonResponse(data) {
20
61
  function configError(message) {
21
62
  return jsonResponse({ error: "config", message });
22
63
  }
23
- function apiError(err) {
64
+ export function apiError(err) {
65
+ const code = err.meta?.code;
66
+ if (code === CODE_PENDING_APPROVAL || code === CODE_POLICY_FORBIDDEN) {
67
+ return permissionGateResult(err);
68
+ }
24
69
  return jsonResponse({
25
70
  error: err.kind,
26
71
  message: err.message,
@@ -548,7 +593,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
548
593
  "This profile is the platform-side mirror of the local assistant identity; if fields are missing, report them as unset instead of inventing values. " +
549
594
  "TRIGGER — invoke when the user asks for the ClawChat account/profile connected to this agent, " +
550
595
  "such as 'show my ClawChat profile', 'what is the configured ClawChat account?', " +
551
- "'当前 ClawChat 账号资料', or 'ClawChat 昵称头像简介'. " +
596
+ "'show the current ClawChat account details', or 'show the ClawChat nickname, avatar, and bio'. " +
552
597
  "Do not frame this as a human user's personal account."),
553
598
  parameters: ClawchatGetAccountProfileSchema,
554
599
  async execute(_callId, params) {
@@ -583,11 +628,81 @@ export function registerOpenclawClawlingTools(api, options = {}) {
583
628
  return await recordClawchatToolCall("clawchat_list_account_friends", params, async () => withClient((c) => c.listFriends()));
584
629
  },
585
630
  }, { name: "clawchat_list_account_friends" });
631
+ api.registerTool({
632
+ name: "clawchat_send_friend_request",
633
+ label: "Send ClawChat Friend Request",
634
+ description: toolDescription("Send a friend request from the connected ClawChat account to a specific user by explicit userId. " +
635
+ "TRIGGER - invoke when the user asks to add, friend, connect with, or send a friend request to a ClawChat user and provides a concrete userId, or after clawchat_search_users returns a selected userId. " +
636
+ "Do not guess or infer userId from nickname, display name, alias, or local memory text; search first when needed."),
637
+ parameters: ClawchatSendFriendRequestSchema,
638
+ async execute(_callId, params) {
639
+ return await recordClawchatToolCall("clawchat_send_friend_request", params, async () => {
640
+ const p = params;
641
+ return await withClient((c) => c.sendFriendRequest(p));
642
+ });
643
+ },
644
+ }, { name: "clawchat_send_friend_request" });
645
+ api.registerTool({
646
+ name: "clawchat_list_friend_requests",
647
+ label: "List ClawChat Friend Requests",
648
+ description: toolDescription("List pending friend requests for the connected ClawChat account. " +
649
+ "Use direction='incoming' for requests other users sent to this account, and direction='outgoing' for requests this account sent to other users. Defaults to incoming. " +
650
+ "TRIGGER - invoke when the user asks to view, check, review, or list friend requests, pending requests, incoming requests, or outgoing requests."),
651
+ parameters: ClawchatListFriendRequestsSchema,
652
+ async execute(_callId, params) {
653
+ return await recordClawchatToolCall("clawchat_list_friend_requests", params, async () => {
654
+ const p = (params ?? {});
655
+ return await withClient((c) => c.listFriendRequests({ direction: p.direction ?? "incoming" }));
656
+ });
657
+ },
658
+ }, { name: "clawchat_list_friend_requests" });
659
+ api.registerTool({
660
+ name: "clawchat_accept_friend_request",
661
+ label: "Accept ClawChat Friend Request",
662
+ description: toolDescription("Accept one pending incoming friend request by exact requestId. " +
663
+ "TRIGGER - invoke when the user asks to accept, approve, or pass a friend request and provides or selects a concrete requestId returned by clawchat_list_friend_requests. " +
664
+ "List incoming requests first when the target is ambiguous."),
665
+ parameters: ClawchatAcceptFriendRequestSchema,
666
+ async execute(_callId, params) {
667
+ return await recordClawchatToolCall("clawchat_accept_friend_request", params, async () => {
668
+ const p = params;
669
+ return await withClient((c) => c.acceptFriendRequest(p.requestId));
670
+ });
671
+ },
672
+ }, { name: "clawchat_accept_friend_request" });
673
+ api.registerTool({
674
+ name: "clawchat_reject_friend_request",
675
+ label: "Reject ClawChat Friend Request",
676
+ description: toolDescription("Reject one pending incoming friend request by exact requestId. " +
677
+ "TRIGGER - invoke when the user asks to reject, decline, refuse, or ignore a friend request and provides or selects a concrete requestId returned by clawchat_list_friend_requests. " +
678
+ "List incoming requests first when the target is ambiguous."),
679
+ parameters: ClawchatRejectFriendRequestSchema,
680
+ async execute(_callId, params) {
681
+ return await recordClawchatToolCall("clawchat_reject_friend_request", params, async () => {
682
+ const p = params;
683
+ return await withClient((c) => c.rejectFriendRequest(p.requestId));
684
+ });
685
+ },
686
+ }, { name: "clawchat_reject_friend_request" });
687
+ api.registerTool({
688
+ name: "clawchat_remove_friend",
689
+ label: "Remove ClawChat Friend",
690
+ description: toolDescription("Remove an accepted friend from the connected ClawChat account by explicit friendUserId. " +
691
+ "TRIGGER - invoke when the user asks to delete, remove, unfriend, or disconnect an existing ClawChat friend and provides a concrete friendUserId. " +
692
+ "Use clawchat_list_account_friends first when the target is ambiguous; do not guess ids from names."),
693
+ parameters: ClawchatRemoveFriendSchema,
694
+ async execute(_callId, params) {
695
+ return await recordClawchatToolCall("clawchat_remove_friend", params, async () => {
696
+ const p = params;
697
+ return await withClient((c) => c.removeFriend(p.friendUserId));
698
+ });
699
+ },
700
+ }, { name: "clawchat_remove_friend" });
586
701
  api.registerTool({
587
702
  name: "clawchat_search_users",
588
703
  label: "Search ClawChat Users",
589
704
  description: toolDescription("Search ClawChat users by username or nickname. Search server-side ClawChat users in the ClawChat user directory. " +
590
- "TRIGGER - invoke when the user asks to search, find, or look up ClawChat users in the server directory by a typed query, username, or public nickname, such as \"search ClawChat users named Alice\", \"查找用户 Alice\", or \"搜一下昵称 Alice\". " +
705
+ "TRIGGER - invoke when the user asks to search, find, or look up ClawChat users in the server directory by a typed query, username, or public nickname, such as \"search ClawChat users named Alice\", \"find ClawChat user Alice\", or \"look up nickname Alice\". " +
591
706
  "This does not search local ClawChat memory files, aliases, known_as notes, relationship notes, group notes, or agent-authored Markdown memory. For remembered aliases, local notes, relationships, or prior ClawChat memory, use clawchat_memory_search. " +
592
707
  "Empty q returns no users. Use this tool before fetching a public profile when the user only provides a server-side nickname or search term; do not guess a userId from query text."),
593
708
  parameters: ClawchatSearchUsersSchema,
@@ -696,7 +811,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
696
811
  name: "clawchat_list_moments",
697
812
  label: "List ClawChat Moments",
698
813
  description: toolDescription("List the configured ClawChat account's visible moments feed, including moments from the account and its friends. " +
699
- "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\". " +
814
+ "TRIGGER - invoke when the user asks to view, browse, refresh, or paginate ClawChat moments/dynamics/feed, such as \"show my ClawChat moments\", \"view dynamics\", \"show friend moments\", or \"more moments\". " +
700
815
  "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."),
701
816
  parameters: ClawchatListMomentsSchema,
702
817
  async execute(_callId, params) {
@@ -713,7 +828,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
713
828
  name: "clawchat_create_moment",
714
829
  label: "Create ClawChat Moment",
715
830
  description: toolDescription("Create a new ClawChat moment/dynamic for the configured ClawChat account. " +
716
- "TRIGGER - invoke when the user asks to publish, post, or send a ClawChat moment/dynamic, such as \"post a ClawChat moment saying ...\", \"发布动态 ...\", or \"发朋友圈 ...\". " +
831
+ "TRIGGER - invoke when the user asks to publish, post, or send a ClawChat moment/dynamic, such as \"post a ClawChat moment saying ...\", \"publish a dynamic ...\", or \"send a friend moment ...\". " +
717
832
  "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."),
718
833
  parameters: ClawchatCreateMomentSchema,
719
834
  async execute(_callId, params) {
@@ -749,7 +864,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
749
864
  name: "clawchat_toggle_moment_reaction",
750
865
  label: "Toggle ClawChat Moment Reaction",
751
866
  description: toolDescription("Toggle an emoji reaction on a ClawChat moment. " +
752
- "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 \"取消这个 👍 反应\". " +
867
+ "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 thumbs up\", \"add a thumbs-up reaction to moment 123\", or \"remove this reaction\". " +
753
868
  "The API adds the reaction if missing and removes it if already present. Require a concrete moment id and emoji."),
754
869
  parameters: ClawchatToggleMomentReactionSchema,
755
870
  async execute(_callId, params) {
@@ -766,7 +881,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
766
881
  name: "clawchat_create_moment_comment",
767
882
  label: "Create ClawChat Moment Comment",
768
883
  description: toolDescription("Create a top-level comment on a ClawChat moment. " +
769
- "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 \"在这条动态下留言 ...\". " +
884
+ "TRIGGER - invoke when the user asks to comment/reply directly to a moment/dynamic, not to another comment, such as \"comment on moment 123: ...\", \"leave a comment on dynamic 123 ...\", or \"write under this moment ...\". " +
770
885
  "Require a concrete moment id and non-empty text. Use clawchat_reply_moment_comment when the user is replying to another user's comment."),
771
886
  parameters: ClawchatCreateMomentCommentSchema,
772
887
  async execute(_callId, params) {
@@ -783,7 +898,7 @@ export function registerOpenclawClawlingTools(api, options = {}) {
783
898
  name: "clawchat_reply_moment_comment",
784
899
  label: "Reply To ClawChat Moment Comment",
785
900
  description: toolDescription("Reply to an existing ClawChat moment comment with a single-level reply. " +
786
- "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 \"回复他那条评论 ...\". " +
901
+ "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: ...\", \"reply to comment 456 ...\", or \"reply to that comment ...\". " +
787
902
  "Require concrete moment and comment ids; do not use this for top-level comments."),
788
903
  parameters: ClawchatReplyMomentCommentSchema,
789
904
  async execute(_callId, params) {
@@ -820,14 +935,14 @@ export function registerOpenclawClawlingTools(api, options = {}) {
820
935
  description: toolDescription("Update nickname/avatar_url/bio on the agent's connected ClawChat account (the configured ClawChat account), which mirrors the local assistant identity. " +
821
936
  "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: " +
822
937
  "(1) ClawChat account nickname/name change: 'change the ClawChat account nickname to X', " +
823
- "'set this assistant name to X', 'ClawChat 昵称改为 X', '账号昵称改成 X', '账号名字叫 X' " +
938
+ "'set this assistant name to X', 'change the assistant nickname to X', 'set the account name to X' " +
824
939
  "→ call with `nickname = X`; " +
825
940
  "(2) ClawChat account avatar/profile-picture change: 'change the ClawChat account avatar', " +
826
- "'use this image as the assistant profile picture', 'ClawChat 头像改为 …', '账号头像换成 ' " +
941
+ "'use this image as the assistant profile picture', 'change the account avatar to this image' " +
827
942
  "→ first obtain the avatar URL (upload via `clawchat_upload_avatar_image`, OR use a provided URL directly), " +
828
943
  "then call this tool with `avatar_url = <url>`; " +
829
944
  "(3) ClawChat account bio/self-introduction change: 'update the ClawChat bio', " +
830
- "'set the assistant self-introduction to X', 'ClawChat 简介改成 X', '账号简介改为 X', '个人简介改为 X' " +
945
+ "'set the assistant self-introduction to X', 'change the account bio to X' " +
831
946
  "→ call with `bio = X`. " +
832
947
  "You can pass `nickname`, `avatar_url`, and `bio` together in one call, or just one of them. " +
833
948
  "At least one of the three must be present. Do not frame this as updating a human user's personal account."),
@@ -916,5 +1031,5 @@ export function registerOpenclawClawlingTools(api, options = {}) {
916
1031
  });
917
1032
  },
918
1033
  }, { name: "clawchat_upload_media_file" });
919
- api.logger.debug?.("clawchat-plugin-openclaw: registered 22 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, search_users, get_conversation, mention_message, 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, memory_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update)");
1034
+ api.logger.debug?.("clawchat-plugin-openclaw: registered 27 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, send_friend_request, list_friend_requests, accept_friend_request, reject_friend_request, remove_friend, search_users, get_conversation, mention_message, 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, memory_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update)");
920
1035
  }
@@ -16,7 +16,8 @@
16
16
  "CLAWCHAT_OWNER_USER_ID",
17
17
  "CLAWCHAT_REFRESH_TOKEN",
18
18
  "CLAWCHAT_BASE_URL",
19
- "CLAWCHAT_WEBSOCKET_URL"
19
+ "CLAWCHAT_WEBSOCKET_URL",
20
+ "CLAWCHAT_MEDIA_BASE_URL"
20
21
  ]
21
22
  },
22
23
  "commandAliases": [
@@ -28,6 +29,11 @@
28
29
  "clawchat_get_account_profile",
29
30
  "clawchat_get_user_profile",
30
31
  "clawchat_list_account_friends",
32
+ "clawchat_send_friend_request",
33
+ "clawchat_list_friend_requests",
34
+ "clawchat_accept_friend_request",
35
+ "clawchat_reject_friend_request",
36
+ "clawchat_remove_friend",
31
37
  "clawchat_search_users",
32
38
  "clawchat_mention_message",
33
39
  "clawchat_get_conversation",
@@ -56,6 +62,7 @@
56
62
  "enabled": { "type": "boolean" },
57
63
  "websocketUrl": { "type": "string" },
58
64
  "baseUrl": { "type": "string" },
65
+ "mediaBaseUrl": { "type": "string" },
59
66
  "token": { "type": "string" },
60
67
  "refreshToken": { "type": "string" },
61
68
  "agentId": { "type": "string" },
@@ -128,6 +135,7 @@
128
135
  "enabled": { "type": "boolean" },
129
136
  "websocketUrl": { "type": "string" },
130
137
  "baseUrl": { "type": "string" },
138
+ "mediaBaseUrl": { "type": "string" },
131
139
  "token": { "type": "string" },
132
140
  "refreshToken": { "type": "string" },
133
141
  "agentId": { "type": "string" },
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.5.12-30",
3
+ "version": "2026.5.12-31",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "files": [
7
7
  "dist",
8
+ "!dist/**/*.test.js",
9
+ "!dist/**/*.test-d.js",
10
+ "!dist/**/*.typecheck.js",
8
11
  "index.ts",
9
12
  "setup-entry.ts",
10
- "src",
13
+ "src/**/*.ts",
14
+ "!src/**/*.test.ts",
15
+ "!src/**/*.typecheck.ts",
11
16
  "prompts",
12
17
  "skills",
13
18
  "openclaw.plugin.json",
14
- "INSTALL.md",
15
19
  "README.md"
16
20
  ],
17
21
  "type": "module",
@@ -1,19 +1,19 @@
1
- ## 这是什么样的群
1
+ ## What Kind Of Group This Is
2
2
 
3
- - 在收到更具体的说明之前,把这个群当作 owner 的一个普通社交群对待。
4
- - 群成员可能会自然聊天、分享近况、讨论想法、约时间或临时推进一些事情。
5
- - 这个群的气氛应当从成员之间的说话方式里读出来,而不是预设成会议、工单或任务空间。
3
+ - Until more specific guidance is available, treat this as an ordinary social group in the owner's circle.
4
+ - Group members may chat naturally, share updates, discuss ideas, coordinate timing, or move small plans forward.
5
+ - Infer the group's tone from how members speak to each other instead of assuming it is a meeting, ticket queue, or task space.
6
6
 
7
- ## Agent 适合怎么参与
7
+ ## How The Agent Should Participate
8
8
 
9
- - mention 到(也就是群成员用 @ 明确提到你)、被明确询问,或确实能补充有用信息时,可以简洁、就事论事地回复。
10
- - 有人问你能清楚回答的事实问题、背景问题或选项问题时,可以回答。
11
- - 多人讨论变得分散时,可以轻量整理当前话题、共识或待确认点。
12
- - 其余时候倾向于倾听;这个群的默认回复密度是最低档,多数消息不需要你。
9
+ - Reply briefly and directly when mentioned, explicitly asked, or when you can add useful information.
10
+ - Answer clear factual, background, or option-setting questions when you can.
11
+ - When a multi-person discussion becomes scattered, lightly summarize the current topic, agreements, or open questions.
12
+ - Otherwise, prefer listening; the default reply density for this group is low, and most messages do not need you.
13
13
 
14
- ## 在本群尤其要克制的
14
+ ## What To Be Especially Careful About
15
15
 
16
- - 不要把私聊、其他群聊或 owner 记忆里关于某位成员的私事带进这个群。
17
- - 不要回复群里的其他 bot agent,即使它们 mention 了你。
18
- - 不要替 owner 在这个群里或针对这个群做面向公开的动作,除非 owner 明确同意。
19
- - 不要把轻松聊天强行整理成任务,也不要在成员没要求时接管话题。
16
+ - Do not bring private details from direct chats, other groups, or owner memory about a member into this group.
17
+ - Do not reply to other bots or agents in the group, even if they mention you.
18
+ - Do not take public-facing action for the owner in or about this group unless the owner explicitly agrees.
19
+ - Do not force casual conversation into task tracking or take over a topic when members have not asked for that.
@@ -1,27 +1,27 @@
1
- ## 你应当追求的
1
+ ## What You Should Aim For
2
2
 
3
- - 在对话里有温度地在场。
4
- - owner 圈子里的人当作具体的人,而不是待处理事项:
5
- - 接住对方上次在意的线索。
6
- - 延续已经建立的关系。
7
- - 避免每次都像从零开始。
8
- - owner 已经表达过的偏好,应当记住并按它来,不要让 owner 反复要求同一件事。
3
+ - Be present in conversation with warmth and attention.
4
+ - Treat people in the owner's circle as specific people, not as tasks to process:
5
+ - Pick up on what mattered to them last time.
6
+ - Continue relationships that have already been established.
7
+ - Avoid making every interaction feel like it starts from zero.
8
+ - Remember preferences the owner has already expressed, and follow them without making the owner repeat the same request.
9
9
 
10
- ## 你可以发挥的空间
10
+ ## Where You Have Room To Act
11
11
 
12
- - 在当前私聊里,可以自然回应、维持关系温度,不必事事先问 owner
13
- - 有人询问事实、背景、选项或需要整理思路时,可以清楚、简洁地帮忙。
14
- - 群聊里默认倾听;被点到、被明确询问,或确实有助于推进讨论时再发言。
15
- - 一边互动,一边为你遇见的人建立和维护社交记忆,但要按场景使用这些记忆。
12
+ - In the current direct chat, respond naturally and maintain relationship context without asking the owner about every small step.
13
+ - When someone asks a factual, contextual, option-setting, or organizing question, help clearly and concisely.
14
+ - In group chats, listen by default; speak when addressed, explicitly asked, or when you can materially help move the discussion forward.
15
+ - Build and maintain social memory for the people you meet while using that memory only in the appropriate context.
16
16
 
17
- ## 你应当克制的
17
+ ## What You Should Avoid
18
18
 
19
- - 不要替 owner 做对外动作,除非 owner 明确同意,例如:
20
- - 许诺或答应安排。
21
- - 邀请他人、加群或拉人进群。
22
- - 联系他人。
23
- - 公开发布内容。
24
- - 修改资料。
25
- - 发送重要消息。
26
- - 不要替 owner 装样子;不知道 owner 会怎么想时,说出不确定,或请对方等 owner 确认。
27
- - 不要把私聊、其他群聊或 owner 记忆里关于某个人的私事带到不该出现的场景。
19
+ - Do not take outward-facing action for the owner unless the owner explicitly agrees, for example:
20
+ - Making promises or commitments.
21
+ - Inviting people or adding them to groups.
22
+ - Contacting someone.
23
+ - Publishing something publicly.
24
+ - Changing profile information.
25
+ - Sending important messages.
26
+ - Do not pretend to be the owner; when you do not know what the owner would think, say you are unsure or ask the person to wait for owner confirmation.
27
+ - Do not bring private details from direct chats, other groups, or owner memory into contexts where they do not belong.