@clawling/clawchat-plugin-openclaw 2026.9.11-1 → 2026.9.13-2

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.
package/src/tools.ts CHANGED
@@ -10,7 +10,11 @@ import {
10
10
  type Profile,
11
11
  } from "./api-types.ts";
12
12
  import { mapGateOutcome } from "./gate-outcome.ts";
13
- import { resolveOpenclawClawlingAccount } from "./config.ts";
13
+ import {
14
+ CHANNEL_ID,
15
+ normalizeOpenclawClawlingAccountId,
16
+ resolveOpenclawClawlingAccount,
17
+ } from "./config.ts";
14
18
  import {
15
19
  clawChatDbPathForStateDir,
16
20
  getClawChatStore,
@@ -280,6 +284,35 @@ function validateMentionMessageParams(params: unknown):
280
284
  return { ok: true, params: p as ClawchatMentionMessageParams };
281
285
  }
282
286
 
287
+ /**
288
+ * Account a tool call belongs to, or `null` when the call carries no ClawChat
289
+ * context (the caller then falls back to the configured default account).
290
+ *
291
+ * Both context fields are only meaningful when they describe THIS channel:
292
+ * `agentAccountId` is the originating session's account id, so a turn that
293
+ * arrived over Telegram/Discord would otherwise hand us that channel's account
294
+ * id as if it were one of ours.
295
+ */
296
+ export function resolveOpenclawClawlingToolAccountId(ctx: OpenClawPluginToolContext): string | null {
297
+ const delivery = ctx.deliveryContext;
298
+ if (
299
+ delivery &&
300
+ delivery.channel === CHANNEL_ID &&
301
+ typeof delivery.accountId === "string" &&
302
+ delivery.accountId.trim()
303
+ ) {
304
+ return normalizeOpenclawClawlingAccountId(delivery.accountId);
305
+ }
306
+ if (
307
+ ctx.messageChannel === CHANNEL_ID &&
308
+ typeof ctx.agentAccountId === "string" &&
309
+ ctx.agentAccountId.trim()
310
+ ) {
311
+ return normalizeOpenclawClawlingAccountId(ctx.agentAccountId);
312
+ }
313
+ return null;
314
+ }
315
+
283
316
  export function registerOpenclawClawlingTools(
284
317
  api: OpenClawPluginApi,
285
318
  options: { store?: ToolStore | null } = {},
@@ -290,8 +323,8 @@ export function registerOpenclawClawlingTools(
290
323
  }
291
324
 
292
325
  // Re-resolve at call time so config reloads pick up new tokens / baseUrl.
293
- function resolveCurrent() {
294
- return resolveOpenclawClawlingAccount(api.config!);
326
+ function resolveCurrent(accountId: string | null) {
327
+ return resolveOpenclawClawlingAccount(api.config!, accountId);
295
328
  }
296
329
 
297
330
  function resolveStore(): ToolStore | null {
@@ -326,12 +359,13 @@ export function registerOpenclawClawlingTools(
326
359
  }
327
360
 
328
361
  async function recordClawchatToolCall<T>(
362
+ accountId: string | null,
329
363
  toolName: string,
330
364
  params: unknown,
331
365
  fn: () => Promise<OpenClawAgentToolResult<T>>,
332
366
  ): Promise<OpenClawAgentToolResult<T>> {
333
367
  const startedAt = Date.now();
334
- const account = resolveCurrent();
368
+ const account = resolveCurrent(accountId);
335
369
  try {
336
370
  const result = await fn();
337
371
  const details = (result as { details?: unknown }).details ?? result;
@@ -365,8 +399,8 @@ export function registerOpenclawClawlingTools(
365
399
  | { ok: false; error: OpenClawAgentToolResult<unknown> }
366
400
  | { ok: true; client: ReturnType<typeof createOpenclawClawlingApiClient> };
367
401
 
368
- function buildClient(): ClientResult {
369
- const acct = resolveCurrent();
402
+ function buildClient(accountId: string | null): ClientResult {
403
+ const acct = resolveCurrent(accountId);
370
404
  // `baseUrl` always resolves via the built-in default in config.ts, so we
371
405
  // only need to gate on `token` here (which is populated by ClawChat
372
406
  // activation/login).
@@ -384,10 +418,11 @@ export function registerOpenclawClawlingTools(
384
418
  }
385
419
 
386
420
  function withClient<T>(
421
+ accountId: string | null,
387
422
  fn: (client: ReturnType<typeof createOpenclawClawlingApiClient>) => Promise<T>,
388
423
  ): Promise<OpenClawAgentToolResult<unknown>> {
389
424
  return (async (): Promise<OpenClawAgentToolResult<unknown>> => {
390
- const built = buildClient();
425
+ const built = buildClient(accountId);
391
426
  if (!built.ok) return built.error;
392
427
  try {
393
428
  const data = await fn(built.client);
@@ -405,10 +440,11 @@ export function registerOpenclawClawlingTools(
405
440
  * instead of a generic error. Used for friend and moment mutation tools.
406
441
  */
407
442
  function withGatedClient<T>(
443
+ accountId: string | null,
408
444
  fn: (client: ReturnType<typeof createOpenclawClawlingApiClient>) => Promise<T>,
409
445
  ): Promise<OpenClawAgentToolResult<unknown>> {
410
446
  return (async (): Promise<OpenClawAgentToolResult<unknown>> => {
411
- const built = buildClient();
447
+ const built = buildClient(accountId);
412
448
  if (!built.ok) return built.error;
413
449
  try {
414
450
  const data = await fn(built.client);
@@ -662,136 +698,142 @@ export function registerOpenclawClawlingTools(
662
698
  );
663
699
 
664
700
  api.registerTool(
665
- (ctx) => ({
666
- name: "clawchat_metadata_sync",
667
- label: "Sync ClawChat Metadata",
668
- description: memoryToolDescription(
669
- "Synchronize the ClawChat metadata block for an explicit owner, user, or group target. " +
670
- "Use direction=pull when the user asks to refresh, sync, or update local ClawChat current agent profile/behavior, agent-owner profile information, user information, or group information from the server; this fetches the authoritative server record and rewrites only the metadata block. " +
671
- "Use direction=push only to push selected existing local metadata fields to the server, then refresh the metadata block from the server response. " +
672
- "For direction=push, pass non-empty fields containing only pushable metadata field names: owner supports agent_behavior only; user supports nickname/avatar_url/bio for the connected account only; group supports group_title/group_description. " +
673
- "This does not modify the agent-authored body. " +
674
- "This synchronizes only the metadata block. It does not search or read agent-authored long-term memory body. For remembered aliases or local notes, use clawchat_memory_search or clawchat_memory_read. " +
675
- "Do not combine clawchat_get_user_profile with clawchat_memory_write to update local profile metadata.",
676
- ),
677
- parameters: ClawchatMetadataSyncSchema,
678
- async execute(_callId, params) {
679
- const root = workspaceRoot(ctx);
680
- if (!root) {
681
- return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
682
- }
683
- const built = buildClient();
684
- if (!built.ok) return built.error;
685
- try {
686
- const p = params as ClawchatMetadataSyncParams;
687
- const targetError = validateTarget(root, p);
688
- if (targetError) return memoryToolError(new Error(targetError));
689
- const account = resolveCurrent();
690
- if (p.direction === "push") {
691
- if (!Array.isArray(p.fields) || p.fields.length === 0) {
692
- return validationError("clawchat-plugin-openclaw: fields are required for metadata push");
701
+ (ctx) => {
702
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
703
+ return {
704
+ name: "clawchat_metadata_sync",
705
+ label: "Sync ClawChat Metadata",
706
+ description: memoryToolDescription(
707
+ "Synchronize the ClawChat metadata block for an explicit owner, user, or group target. " +
708
+ "Use direction=pull when the user asks to refresh, sync, or update local ClawChat current agent profile/behavior, agent-owner profile information, user information, or group information from the server; this fetches the authoritative server record and rewrites only the metadata block. " +
709
+ "Use direction=push only to push selected existing local metadata fields to the server, then refresh the metadata block from the server response. " +
710
+ "For direction=push, pass non-empty fields containing only pushable metadata field names: owner supports agent_behavior only; user supports nickname/avatar_url/bio for the connected account only; group supports group_title/group_description. " +
711
+ "This does not modify the agent-authored body. " +
712
+ "This synchronizes only the metadata block. It does not search or read agent-authored long-term memory body. For remembered aliases or local notes, use clawchat_memory_search or clawchat_memory_read. " +
713
+ "Do not combine clawchat_get_user_profile with clawchat_memory_write to update local profile metadata.",
714
+ ),
715
+ parameters: ClawchatMetadataSyncSchema,
716
+ async execute(_callId, params) {
717
+ const root = workspaceRoot(ctx);
718
+ if (!root) {
719
+ return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
720
+ }
721
+ const built = buildClient(accountId);
722
+ if (!built.ok) return built.error;
723
+ try {
724
+ const p = params as ClawchatMetadataSyncParams;
725
+ const targetError = validateTarget(root, p);
726
+ if (targetError) return memoryToolError(new Error(targetError));
727
+ const account = resolveCurrent(accountId);
728
+ if (p.direction === "push") {
729
+ if (!Array.isArray(p.fields) || p.fields.length === 0) {
730
+ return validationError("clawchat-plugin-openclaw: fields are required for metadata push");
731
+ }
732
+ if (p.targetType === "owner" && !account.agentId) {
733
+ return configError("clawchat-plugin-openclaw: agentId is required");
734
+ }
735
+ if (p.targetType === "user" && !account.userId) {
736
+ return configError("clawchat-plugin-openclaw: userId is required");
737
+ }
738
+ return jsonResponse(
739
+ await pushMetadata({
740
+ memoryRoot: root,
741
+ targetType: p.targetType,
742
+ targetId: p.targetId,
743
+ fields: p.fields,
744
+ agentId: account.agentId,
745
+ accountUserId: account.userId,
746
+ api: built.client,
747
+ }),
748
+ );
693
749
  }
694
- if (p.targetType === "owner" && !account.agentId) {
695
- return configError("clawchat-plugin-openclaw: agentId is required");
750
+ if (p.targetType === "owner") {
751
+ if (!account.agentId) return configError("clawchat-plugin-openclaw: agentId is required");
752
+ return jsonResponse(
753
+ await pullOwnerMetadata({
754
+ memoryRoot: root,
755
+ agentId: account.agentId,
756
+ accountUserId: account.userId,
757
+ accountOwnerUserId: account.ownerUserId || undefined,
758
+ api: built.client,
759
+ }),
760
+ );
696
761
  }
697
- if (p.targetType === "user" && !account.userId) {
698
- return configError("clawchat-plugin-openclaw: userId is required");
762
+ if (p.targetType === "user") {
763
+ return jsonResponse(
764
+ await pullUserMetadata({ memoryRoot: root, userId: p.targetId, api: built.client }),
765
+ );
699
766
  }
700
767
  return jsonResponse(
701
- await pushMetadata({
768
+ await pullGroupMetadata({
702
769
  memoryRoot: root,
703
- targetType: p.targetType,
704
- targetId: p.targetId,
705
- fields: p.fields,
706
- agentId: account.agentId,
707
- accountUserId: account.userId,
770
+ groupId: p.targetId,
708
771
  api: built.client,
772
+ skipUserIds: [account.userId, account.ownerUserId],
709
773
  }),
710
774
  );
775
+ } catch (err) {
776
+ return memoryToolError(err);
777
+ }
778
+ },
779
+ };
780
+ },
781
+ { name: "clawchat_metadata_sync" },
782
+ );
783
+
784
+ api.registerTool(
785
+ (ctx) => {
786
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
787
+ return {
788
+ name: "clawchat_metadata_update",
789
+ label: "Update ClawChat Metadata",
790
+ description: memoryToolDescription(
791
+ "Update ClawChat server metadata for an explicit owner, connected user account, or group target, then refresh the local metadata block from the server response. " +
792
+ "Use this only when the user wants to change server-side metadata fields: current agent behavior via owner target agent_behavior, connected-user nickname/avatar_url/bio, or group group_title/group_description. " +
793
+ "To refresh local metadata from the server without changing the server, use clawchat_metadata_sync with direction=pull. " +
794
+ "This always pushes to the server first and does not modify the agent-authored body.",
795
+ ),
796
+ parameters: ClawchatMetadataUpdateSchema,
797
+ async execute(_callId, params) {
798
+ const root = workspaceRoot(ctx);
799
+ if (!root) {
800
+ return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
711
801
  }
712
- if (p.targetType === "owner") {
713
- if (!account.agentId) return configError("clawchat-plugin-openclaw: agentId is required");
802
+ const built = buildClient(accountId);
803
+ if (!built.ok) return built.error;
804
+ try {
805
+ const p = params as ClawchatMetadataUpdateParams;
806
+ const targetError = validateTarget(root, p);
807
+ if (targetError) return memoryToolError(new Error(targetError));
808
+ const patchError = validateMetadataUpdatePatch(p);
809
+ if (patchError) return validationError(patchError);
810
+ const account = resolveCurrent(accountId);
811
+ if (p.targetType === "owner" && !account.agentId) {
812
+ return configError("clawchat-plugin-openclaw: agentId is required");
813
+ }
814
+ if (p.targetType === "user" && !account.userId) {
815
+ return configError("clawchat-plugin-openclaw: userId is required");
816
+ }
714
817
  return jsonResponse(
715
- await pullOwnerMetadata({
818
+ await updateMetadata({
716
819
  memoryRoot: root,
820
+ targetType: p.targetType,
821
+ targetId: p.targetId,
717
822
  agentId: account.agentId,
718
823
  accountUserId: account.userId,
719
- accountOwnerUserId: account.ownerUserId || undefined,
824
+ patch: p.patch,
720
825
  api: built.client,
826
+ persistOwnerProfile: makeOwnerProfilePersister(getClawChatStore(), {
827
+ accountId: account.accountId,
828
+ }),
721
829
  }),
722
830
  );
831
+ } catch (err) {
832
+ return memoryToolError(err);
723
833
  }
724
- if (p.targetType === "user") {
725
- return jsonResponse(
726
- await pullUserMetadata({ memoryRoot: root, userId: p.targetId, api: built.client }),
727
- );
728
- }
729
- return jsonResponse(
730
- await pullGroupMetadata({
731
- memoryRoot: root,
732
- groupId: p.targetId,
733
- api: built.client,
734
- skipUserIds: [account.userId, account.ownerUserId],
735
- }),
736
- );
737
- } catch (err) {
738
- return memoryToolError(err);
739
- }
740
- },
741
- }),
742
- { name: "clawchat_metadata_sync" },
743
- );
744
-
745
- api.registerTool(
746
- (ctx) => ({
747
- name: "clawchat_metadata_update",
748
- label: "Update ClawChat Metadata",
749
- description: memoryToolDescription(
750
- "Update ClawChat server metadata for an explicit owner, connected user account, or group target, then refresh the local metadata block from the server response. " +
751
- "Use this only when the user wants to change server-side metadata fields: current agent behavior via owner target agent_behavior, connected-user nickname/avatar_url/bio, or group group_title/group_description. " +
752
- "To refresh local metadata from the server without changing the server, use clawchat_metadata_sync with direction=pull. " +
753
- "This always pushes to the server first and does not modify the agent-authored body.",
754
- ),
755
- parameters: ClawchatMetadataUpdateSchema,
756
- async execute(_callId, params) {
757
- const root = workspaceRoot(ctx);
758
- if (!root) {
759
- return validationError("clawchat-plugin-openclaw: OpenClaw workspaceDir is required");
760
- }
761
- const built = buildClient();
762
- if (!built.ok) return built.error;
763
- try {
764
- const p = params as ClawchatMetadataUpdateParams;
765
- const targetError = validateTarget(root, p);
766
- if (targetError) return memoryToolError(new Error(targetError));
767
- const patchError = validateMetadataUpdatePatch(p);
768
- if (patchError) return validationError(patchError);
769
- const account = resolveCurrent();
770
- if (p.targetType === "owner" && !account.agentId) {
771
- return configError("clawchat-plugin-openclaw: agentId is required");
772
- }
773
- if (p.targetType === "user" && !account.userId) {
774
- return configError("clawchat-plugin-openclaw: userId is required");
775
- }
776
- return jsonResponse(
777
- await updateMetadata({
778
- memoryRoot: root,
779
- targetType: p.targetType,
780
- targetId: p.targetId,
781
- agentId: account.agentId,
782
- accountUserId: account.userId,
783
- patch: p.patch,
784
- api: built.client,
785
- persistOwnerProfile: makeOwnerProfilePersister(getClawChatStore(), {
786
- accountId: account.accountId,
787
- }),
788
- }),
789
- );
790
- } catch (err) {
791
- return memoryToolError(err);
792
- }
793
- },
794
- }),
834
+ },
835
+ };
836
+ },
795
837
  { name: "clawchat_metadata_update" },
796
838
  );
797
839
  }
@@ -799,800 +841,886 @@ export function registerOpenclawClawlingTools(
799
841
  registerMemoryTools();
800
842
 
801
843
  api.registerTool(
802
- {
803
- name: "clawchat_get_account_profile",
804
- label: "Get ClawChat Account Profile",
805
- description: toolDescription(
806
- "Fetch the agent's connected ClawChat account profile (the configured ClawChat account: user id, nickname/display name, avatar, bio). " +
807
- "This profile is the platform-side mirror of the local assistant identity; if fields are missing, report them as unset instead of inventing values. " +
808
- "TRIGGER — invoke when the user asks for the ClawChat account/profile connected to this agent, " +
809
- "such as 'show my ClawChat profile', 'what is the configured ClawChat account?', " +
810
- "'show the current ClawChat account details', or 'show the ClawChat nickname, avatar, and bio'. " +
811
- "Do not frame this as a human user's personal account.",
812
- ),
813
- parameters: ClawchatGetAccountProfileSchema,
814
- async execute(_callId, params) {
815
- return await recordClawchatToolCall("clawchat_get_account_profile", params, async () =>
816
- withClient((c) => c.getMyProfile()),
817
- );
818
- },
844
+ (ctx) => {
845
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
846
+ return {
847
+ name: "clawchat_get_account_profile",
848
+ label: "Get ClawChat Account Profile",
849
+ description: toolDescription(
850
+ "Fetch the agent's connected ClawChat account profile (the configured ClawChat account: user id, nickname/display name, avatar, bio). " +
851
+ "This profile is the platform-side mirror of the local assistant identity; if fields are missing, report them as unset instead of inventing values. " +
852
+ "TRIGGER — invoke when the user asks for the ClawChat account/profile connected to this agent, " +
853
+ "such as 'show my ClawChat profile', 'what is the configured ClawChat account?', " +
854
+ "'show the current ClawChat account details', or 'show the ClawChat nickname, avatar, and bio'. " +
855
+ "Do not frame this as a human user's personal account.",
856
+ ),
857
+ parameters: ClawchatGetAccountProfileSchema,
858
+ async execute(_callId, params) {
859
+ return await recordClawchatToolCall(accountId, "clawchat_get_account_profile", params, async () =>
860
+ withClient(accountId, (c) => c.getMyProfile()),
861
+ );
862
+ },
863
+ };
819
864
  },
820
865
  { name: "clawchat_get_account_profile" },
821
866
  );
822
867
 
823
868
  api.registerTool(
824
- {
825
- name: "clawchat_get_user_profile",
826
- label: "Get ClawChat User Profile",
827
- description: toolDescription(
828
- "Fetch a ClawChat user's server-side public profile by explicit userId. " +
829
- "TRIGGER — invoke when the user asks to view or inspect a specific ClawChat user's public profile and provides a concrete userId, or after clawchat_search_users returns a userId. " +
830
- "Do not guess or infer userId from nickname, display name, alias, or local memory text. " +
831
- "This is a read-only lookup and server lookup. It does not read local ClawChat memory files and does not update local metadata. " +
832
- "When the user asks to refresh, sync, or update that user's local profile information, call clawchat_metadata_sync with targetType=user, that targetId, and direction=pull. " +
833
- "Use `clawchat_get_account_profile` for the agent's own connected ClawChat account unless an explicit userId is provided.",
834
- ),
835
- parameters: ClawchatGetUserProfileSchema,
836
- async execute(_callId, params) {
837
- return await recordClawchatToolCall("clawchat_get_user_profile", params, async () => {
838
- const p = params as ClawchatGetUserProfileParams;
839
- return await withClient((c) => c.getUserInfo(p.userId));
840
- });
841
- },
869
+ (ctx) => {
870
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
871
+ return {
872
+ name: "clawchat_get_user_profile",
873
+ label: "Get ClawChat User Profile",
874
+ description: toolDescription(
875
+ "Fetch a ClawChat user's server-side public profile by explicit userId. " +
876
+ "TRIGGER — invoke when the user asks to view or inspect a specific ClawChat user's public profile and provides a concrete userId, or after clawchat_search_users returns a userId. " +
877
+ "Do not guess or infer userId from nickname, display name, alias, or local memory text. " +
878
+ "This is a read-only lookup and server lookup. It does not read local ClawChat memory files and does not update local metadata. " +
879
+ "When the user asks to refresh, sync, or update that user's local profile information, call clawchat_metadata_sync with targetType=user, that targetId, and direction=pull. " +
880
+ "Use `clawchat_get_account_profile` for the agent's own connected ClawChat account unless an explicit userId is provided.",
881
+ ),
882
+ parameters: ClawchatGetUserProfileSchema,
883
+ async execute(_callId, params) {
884
+ return await recordClawchatToolCall(accountId, "clawchat_get_user_profile", params, async () => {
885
+ const p = params as ClawchatGetUserProfileParams;
886
+ return await withClient(accountId, (c) => c.getUserInfo(p.userId));
887
+ });
888
+ },
889
+ };
842
890
  },
843
891
  { name: "clawchat_get_user_profile" },
844
892
  );
845
893
 
846
894
  api.registerTool(
847
- {
848
- name: "clawchat_list_account_friends",
849
- label: "List ClawChat Account Friends",
850
- description: toolDescription(
851
- "List friends/contacts of the agent's connected ClawChat account (the configured ClawChat account). " +
852
- "These are the agent's ClawChat-platform contacts. " +
853
- "TRIGGER — invoke when the user asks for this ClawChat account's friends, contacts, or friend list.",
854
- ),
855
- parameters: ClawchatListAccountFriendsSchema,
856
- async execute(_callId, params) {
857
- return await recordClawchatToolCall("clawchat_list_account_friends", params, async () =>
858
- withClient((c) => c.listFriends()),
859
- );
860
- },
895
+ (ctx) => {
896
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
897
+ return {
898
+ name: "clawchat_list_account_friends",
899
+ label: "List ClawChat Account Friends",
900
+ description: toolDescription(
901
+ "List friends/contacts of the agent's connected ClawChat account (the configured ClawChat account). " +
902
+ "These are the agent's ClawChat-platform contacts. " +
903
+ "TRIGGER — invoke when the user asks for this ClawChat account's friends, contacts, or friend list.",
904
+ ),
905
+ parameters: ClawchatListAccountFriendsSchema,
906
+ async execute(_callId, params) {
907
+ return await recordClawchatToolCall(accountId, "clawchat_list_account_friends", params, async () =>
908
+ withClient(accountId, (c) => c.listFriends()),
909
+ );
910
+ },
911
+ };
861
912
  },
862
913
  { name: "clawchat_list_account_friends" },
863
914
  );
864
915
 
865
916
  api.registerTool(
866
- {
867
- name: "clawchat_send_friend_request",
868
- label: "Send ClawChat Friend Request",
869
- description: toolDescription(
870
- "Send a friend request from the connected ClawChat account to a specific user by explicit userId. " +
871
- "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. " +
872
- "Do not guess or infer userId from nickname, display name, alias, or local memory text; search first when needed.",
873
- ),
874
- parameters: ClawchatSendFriendRequestSchema,
875
- async execute(_callId, params) {
876
- return await recordClawchatToolCall("clawchat_send_friend_request", params, async () => {
877
- const p = params as ClawchatSendFriendRequestParams;
878
- return await withGatedClient((c) => c.sendFriendRequest(p));
879
- });
880
- },
917
+ (ctx) => {
918
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
919
+ return {
920
+ name: "clawchat_send_friend_request",
921
+ label: "Send ClawChat Friend Request",
922
+ description: toolDescription(
923
+ "Send a friend request from the connected ClawChat account to a specific user by explicit userId. " +
924
+ "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. " +
925
+ "Do not guess or infer userId from nickname, display name, alias, or local memory text; search first when needed.",
926
+ ),
927
+ parameters: ClawchatSendFriendRequestSchema,
928
+ async execute(_callId, params) {
929
+ return await recordClawchatToolCall(accountId, "clawchat_send_friend_request", params, async () => {
930
+ const p = params as ClawchatSendFriendRequestParams;
931
+ return await withGatedClient(accountId, (c) => c.sendFriendRequest(p));
932
+ });
933
+ },
934
+ };
881
935
  },
882
936
  { name: "clawchat_send_friend_request" },
883
937
  );
884
938
 
885
939
  api.registerTool(
886
- {
887
- name: "clawchat_list_friend_requests",
888
- label: "List ClawChat Friend Requests",
889
- description: toolDescription(
890
- "List pending friend requests for the connected ClawChat account. " +
891
- "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. " +
892
- "TRIGGER - invoke when the user asks to view, check, review, or list friend requests, pending requests, incoming requests, or outgoing requests.",
893
- ),
894
- parameters: ClawchatListFriendRequestsSchema,
895
- async execute(_callId, params) {
896
- return await recordClawchatToolCall("clawchat_list_friend_requests", params, async () => {
897
- const p = (params ?? {}) as ClawchatListFriendRequestsParams;
898
- return await withClient((c) => c.listFriendRequests({ direction: p.direction ?? "incoming" }));
899
- });
900
- },
940
+ (ctx) => {
941
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
942
+ return {
943
+ name: "clawchat_list_friend_requests",
944
+ label: "List ClawChat Friend Requests",
945
+ description: toolDescription(
946
+ "List pending friend requests for the connected ClawChat account. " +
947
+ "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. " +
948
+ "TRIGGER - invoke when the user asks to view, check, review, or list friend requests, pending requests, incoming requests, or outgoing requests.",
949
+ ),
950
+ parameters: ClawchatListFriendRequestsSchema,
951
+ async execute(_callId, params) {
952
+ return await recordClawchatToolCall(accountId, "clawchat_list_friend_requests", params, async () => {
953
+ const p = (params ?? {}) as ClawchatListFriendRequestsParams;
954
+ return await withClient(accountId, (c) => c.listFriendRequests({ direction: p.direction ?? "incoming" }));
955
+ });
956
+ },
957
+ };
901
958
  },
902
959
  { name: "clawchat_list_friend_requests" },
903
960
  );
904
961
 
905
962
  api.registerTool(
906
- {
907
- name: "clawchat_accept_friend_request",
908
- label: "Accept ClawChat Friend Request",
909
- description: toolDescription(
910
- "Accept one pending incoming friend request by exact requestId. " +
911
- "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. " +
912
- "List incoming requests first when the target is ambiguous.",
913
- ),
914
- parameters: ClawchatAcceptFriendRequestSchema,
915
- async execute(_callId, params) {
916
- return await recordClawchatToolCall("clawchat_accept_friend_request", params, async () => {
917
- const p = params as ClawchatAcceptFriendRequestParams;
918
- return await withGatedClient((c) => c.acceptFriendRequest(p.requestId));
919
- });
920
- },
963
+ (ctx) => {
964
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
965
+ return {
966
+ name: "clawchat_accept_friend_request",
967
+ label: "Accept ClawChat Friend Request",
968
+ description: toolDescription(
969
+ "Accept one pending incoming friend request by exact requestId. " +
970
+ "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. " +
971
+ "List incoming requests first when the target is ambiguous.",
972
+ ),
973
+ parameters: ClawchatAcceptFriendRequestSchema,
974
+ async execute(_callId, params) {
975
+ return await recordClawchatToolCall(accountId, "clawchat_accept_friend_request", params, async () => {
976
+ const p = params as ClawchatAcceptFriendRequestParams;
977
+ return await withGatedClient(accountId, (c) => c.acceptFriendRequest(p.requestId));
978
+ });
979
+ },
980
+ };
921
981
  },
922
982
  { name: "clawchat_accept_friend_request" },
923
983
  );
924
984
 
925
985
  api.registerTool(
926
- {
927
- name: "clawchat_reject_friend_request",
928
- label: "Reject ClawChat Friend Request",
929
- description: toolDescription(
930
- "Reject one pending incoming friend request by exact requestId. " +
931
- "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. " +
932
- "List incoming requests first when the target is ambiguous.",
933
- ),
934
- parameters: ClawchatRejectFriendRequestSchema,
935
- async execute(_callId, params) {
936
- return await recordClawchatToolCall("clawchat_reject_friend_request", params, async () => {
937
- const p = params as ClawchatRejectFriendRequestParams;
938
- return await withGatedClient((c) => c.rejectFriendRequest(p.requestId));
939
- });
940
- },
986
+ (ctx) => {
987
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
988
+ return {
989
+ name: "clawchat_reject_friend_request",
990
+ label: "Reject ClawChat Friend Request",
991
+ description: toolDescription(
992
+ "Reject one pending incoming friend request by exact requestId. " +
993
+ "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. " +
994
+ "List incoming requests first when the target is ambiguous.",
995
+ ),
996
+ parameters: ClawchatRejectFriendRequestSchema,
997
+ async execute(_callId, params) {
998
+ return await recordClawchatToolCall(accountId, "clawchat_reject_friend_request", params, async () => {
999
+ const p = params as ClawchatRejectFriendRequestParams;
1000
+ return await withGatedClient(accountId, (c) => c.rejectFriendRequest(p.requestId));
1001
+ });
1002
+ },
1003
+ };
941
1004
  },
942
1005
  { name: "clawchat_reject_friend_request" },
943
1006
  );
944
1007
 
945
1008
  api.registerTool(
946
- {
947
- name: "clawchat_remove_friend",
948
- label: "Remove ClawChat Friend",
949
- description: toolDescription(
950
- "Remove an accepted friend from the connected ClawChat account by explicit friendUserId. " +
951
- "TRIGGER - invoke when the user asks to delete, remove, unfriend, or disconnect an existing ClawChat friend and provides a concrete friendUserId. " +
952
- "Use clawchat_list_account_friends first when the target is ambiguous; do not guess ids from names.",
953
- ),
954
- parameters: ClawchatRemoveFriendSchema,
955
- async execute(_callId, params) {
956
- return await recordClawchatToolCall("clawchat_remove_friend", params, async () => {
957
- const p = params as ClawchatRemoveFriendParams;
958
- return await withGatedClient((c) => c.removeFriend(p.friendUserId));
959
- });
960
- },
1009
+ (ctx) => {
1010
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1011
+ return {
1012
+ name: "clawchat_remove_friend",
1013
+ label: "Remove ClawChat Friend",
1014
+ description: toolDescription(
1015
+ "Remove an accepted friend from the connected ClawChat account by explicit friendUserId. " +
1016
+ "TRIGGER - invoke when the user asks to delete, remove, unfriend, or disconnect an existing ClawChat friend and provides a concrete friendUserId. " +
1017
+ "Use clawchat_list_account_friends first when the target is ambiguous; do not guess ids from names.",
1018
+ ),
1019
+ parameters: ClawchatRemoveFriendSchema,
1020
+ async execute(_callId, params) {
1021
+ return await recordClawchatToolCall(accountId, "clawchat_remove_friend", params, async () => {
1022
+ const p = params as ClawchatRemoveFriendParams;
1023
+ return await withGatedClient(accountId, (c) => c.removeFriend(p.friendUserId));
1024
+ });
1025
+ },
1026
+ };
961
1027
  },
962
1028
  { name: "clawchat_remove_friend" },
963
1029
  );
964
1030
 
965
1031
  api.registerTool(
966
- {
967
- name: "clawchat_search_users",
968
- label: "Search ClawChat Users",
969
- description: toolDescription(
970
- "Search ClawChat users by username or nickname. Search server-side ClawChat users in the ClawChat user directory. " +
971
- "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\". " +
972
- "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. " +
973
- "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.",
974
- ),
975
- parameters: ClawchatSearchUsersSchema,
976
- async execute(_callId, params) {
977
- return await recordClawchatToolCall("clawchat_search_users", params, async () => {
978
- const p = (params ?? {}) as ClawchatSearchUsersParams;
979
- return await withClient((c) =>
980
- c.searchUsers({
981
- ...(typeof p.q === "string" ? { q: p.q } : {}),
982
- ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
983
- }),
984
- );
985
- });
986
- },
1032
+ (ctx) => {
1033
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1034
+ return {
1035
+ name: "clawchat_search_users",
1036
+ label: "Search ClawChat Users",
1037
+ description: toolDescription(
1038
+ "Search ClawChat users by username or nickname. Search server-side ClawChat users in the ClawChat user directory. " +
1039
+ "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\". " +
1040
+ "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. " +
1041
+ "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.",
1042
+ ),
1043
+ parameters: ClawchatSearchUsersSchema,
1044
+ async execute(_callId, params) {
1045
+ return await recordClawchatToolCall(accountId, "clawchat_search_users", params, async () => {
1046
+ const p = (params ?? {}) as ClawchatSearchUsersParams;
1047
+ return await withClient(accountId, (c) =>
1048
+ c.searchUsers({
1049
+ ...(typeof p.q === "string" ? { q: p.q } : {}),
1050
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
1051
+ }),
1052
+ );
1053
+ });
1054
+ },
1055
+ };
987
1056
  },
988
1057
  { name: "clawchat_search_users" },
989
1058
  );
990
1059
 
991
1060
  api.registerTool(
992
- {
993
- name: "clawchat_get_conversation",
994
- label: "Get ClawChat Conversation",
995
- description: toolDescription(
996
- "Fetch read-only ClawChat conversation details, including group membership when returned by the API. " +
997
- "TRIGGER - invoke when the user asks to inspect a specific ClawChat conversation or group and provides a concrete conversationId. " +
998
- "This is read-only and does not create, update, leave, dissolve, or administer conversations.",
999
- ),
1000
- parameters: ClawchatGetConversationSchema,
1001
- async execute(_callId, params) {
1002
- return await recordClawchatToolCall("clawchat_get_conversation", params, async () => {
1003
- const p = params as ClawchatGetConversationParams;
1004
- const built = buildClient();
1005
- if (!built.ok) return built.error;
1006
- try {
1007
- const data = await built.client.getConversation(p.conversationId);
1008
- return jsonResponse(data);
1009
- } catch (err) {
1010
- if (err instanceof ClawlingApiError) {
1011
- return apiError(err);
1061
+ (ctx) => {
1062
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1063
+ return {
1064
+ name: "clawchat_get_conversation",
1065
+ label: "Get ClawChat Conversation",
1066
+ description: toolDescription(
1067
+ "Fetch read-only ClawChat conversation details, including group membership when returned by the API. " +
1068
+ "TRIGGER - invoke when the user asks to inspect a specific ClawChat conversation or group and provides a concrete conversationId. " +
1069
+ "This is read-only and does not create, update, leave, dissolve, or administer conversations.",
1070
+ ),
1071
+ parameters: ClawchatGetConversationSchema,
1072
+ async execute(_callId, params) {
1073
+ return await recordClawchatToolCall(accountId, "clawchat_get_conversation", params, async () => {
1074
+ const p = params as ClawchatGetConversationParams;
1075
+ const built = buildClient(accountId);
1076
+ if (!built.ok) return built.error;
1077
+ try {
1078
+ const data = await built.client.getConversation(p.conversationId);
1079
+ return jsonResponse(data);
1080
+ } catch (err) {
1081
+ if (err instanceof ClawlingApiError) {
1082
+ return apiError(err);
1083
+ }
1084
+ return genericError(err);
1012
1085
  }
1013
- return genericError(err);
1014
- }
1015
- });
1016
- },
1086
+ });
1087
+ },
1088
+ };
1017
1089
  },
1018
1090
  { name: "clawchat_get_conversation" },
1019
1091
  );
1020
1092
 
1021
1093
  api.registerTool(
1022
- {
1023
- name: "clawchat_leave_group",
1024
- label: "Leave ClawChat Group",
1025
- description: toolDescription(
1026
- "Leave a ClawChat group conversation the agent is currently a member of. " +
1027
- "TRIGGER - invoke only when the user explicitly asks the agent to leave, exit, or quit a specific ClawChat group and provides a concrete conversationId. " +
1028
- "Groups only: direct conversations are rejected by the server. " +
1029
- "If the agent is the group owner, leaving auto-transfers ownership to the earliest human member; if no human member remains, the group is dissolved. " +
1030
- "After this tool succeeds, output only <clawchat:no-reply/> and do not send a normal follow-up reply.",
1031
- ),
1032
- parameters: ClawchatLeaveGroupSchema,
1033
- async execute(_callId, params) {
1034
- return await recordClawchatToolCall("clawchat_leave_group", params, async () => {
1035
- const p = params as ClawchatLeaveGroupParams;
1036
- const built = buildClient();
1037
- if (!built.ok) return built.error;
1038
- try {
1039
- const data = await built.client.leaveConversation(p.conversationId);
1040
- return jsonResponse(data);
1041
- } catch (err) {
1042
- if (err instanceof ClawlingApiError) {
1043
- return apiError(err);
1094
+ (ctx) => {
1095
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1096
+ return {
1097
+ name: "clawchat_leave_group",
1098
+ label: "Leave ClawChat Group",
1099
+ description: toolDescription(
1100
+ "Leave a ClawChat group conversation the agent is currently a member of. " +
1101
+ "TRIGGER - invoke only when the user explicitly asks the agent to leave, exit, or quit a specific ClawChat group and provides a concrete conversationId. " +
1102
+ "Groups only: direct conversations are rejected by the server. " +
1103
+ "If the agent is the group owner, leaving auto-transfers ownership to the earliest human member; if no human member remains, the group is dissolved. " +
1104
+ "After this tool succeeds, output only <clawchat:no-reply/> and do not send a normal follow-up reply.",
1105
+ ),
1106
+ parameters: ClawchatLeaveGroupSchema,
1107
+ async execute(_callId, params) {
1108
+ return await recordClawchatToolCall(accountId, "clawchat_leave_group", params, async () => {
1109
+ const p = params as ClawchatLeaveGroupParams;
1110
+ const built = buildClient(accountId);
1111
+ if (!built.ok) return built.error;
1112
+ try {
1113
+ const data = await built.client.leaveConversation(p.conversationId);
1114
+ return jsonResponse(data);
1115
+ } catch (err) {
1116
+ if (err instanceof ClawlingApiError) {
1117
+ return apiError(err);
1118
+ }
1119
+ return genericError(err);
1044
1120
  }
1045
- return genericError(err);
1046
- }
1047
- });
1048
- },
1121
+ });
1122
+ },
1123
+ };
1049
1124
  },
1050
1125
  { name: "clawchat_leave_group" },
1051
1126
  );
1052
1127
 
1053
1128
  api.registerTool(
1054
- {
1055
- name: "clawchat_add_group_member",
1056
- label: "Add ClawChat Group Member",
1057
- description: toolDescription(
1058
- "Add a specified ClawChat user to a group conversation the agent is a member of. " +
1059
- "TRIGGER - invoke only when the user explicitly asks the agent to add, invite, or pull a specific person into a specific ClawChat group, and both a concrete conversationId and a concrete userId are available. " +
1060
- "Never guess a userId from a name or nickname — use ids from ClawChat Group Message Metadata, clawchat_list_account_friends, or clawchat_search_users. " +
1061
- "Groups only: direct conversations are rejected by the server. " +
1062
- "The target must already be the agent's ClawChat friend, and the agent needs the group-management permission granted by its owner; if the call fails for either reason, explain that to the user and do not retry.",
1063
- ),
1064
- parameters: ClawchatAddGroupMemberSchema,
1065
- async execute(_callId, params) {
1066
- return await recordClawchatToolCall("clawchat_add_group_member", params, async () => {
1067
- const p = params as ClawchatAddGroupMemberParams;
1068
- const built = buildClient();
1069
- if (!built.ok) return built.error;
1070
- try {
1071
- const data = await built.client.addConversationMember(p.conversationId, p.userId);
1072
- return jsonResponse(data);
1073
- } catch (err) {
1074
- if (err instanceof ClawlingApiError) {
1075
- return apiError(err);
1129
+ (ctx) => {
1130
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1131
+ return {
1132
+ name: "clawchat_add_group_member",
1133
+ label: "Add ClawChat Group Member",
1134
+ description: toolDescription(
1135
+ "Add a specified ClawChat user to a group conversation the agent is a member of. " +
1136
+ "TRIGGER - invoke only when the user explicitly asks the agent to add, invite, or pull a specific person into a specific ClawChat group, and both a concrete conversationId and a concrete userId are available. " +
1137
+ "Never guess a userId from a name or nickname — use ids from ClawChat Group Message Metadata, clawchat_list_account_friends, or clawchat_search_users. " +
1138
+ "Groups only: direct conversations are rejected by the server. " +
1139
+ "The target must already be the agent's ClawChat friend, and the agent needs the group-management permission granted by its owner; if the call fails for either reason, explain that to the user and do not retry.",
1140
+ ),
1141
+ parameters: ClawchatAddGroupMemberSchema,
1142
+ async execute(_callId, params) {
1143
+ return await recordClawchatToolCall(accountId, "clawchat_add_group_member", params, async () => {
1144
+ const p = params as ClawchatAddGroupMemberParams;
1145
+ const built = buildClient(accountId);
1146
+ if (!built.ok) return built.error;
1147
+ try {
1148
+ const data = await built.client.addConversationMember(p.conversationId, p.userId);
1149
+ return jsonResponse(data);
1150
+ } catch (err) {
1151
+ if (err instanceof ClawlingApiError) {
1152
+ return apiError(err);
1153
+ }
1154
+ return genericError(err);
1076
1155
  }
1077
- return genericError(err);
1078
- }
1079
- });
1080
- },
1156
+ });
1157
+ },
1158
+ };
1081
1159
  },
1082
1160
  { name: "clawchat_add_group_member" },
1083
1161
  );
1084
1162
 
1085
1163
  api.registerTool(
1086
- {
1087
- name: "clawchat_mention_message",
1088
- label: "Send ClawChat Mention Message",
1089
- description: toolDescription(
1090
- "Send a real ClawChat mention message to a direct or group conversation. " +
1091
- "TRIGGER - invoke when the user asks to @, mention, notify, or address ClawChat users. " +
1092
- "Use `mentioned_users` or `sender_id` from current ClawChat sender/group message metadata as `mentions[].userId` when present. " +
1093
- "Use `clawchat_search_users` only when no explicit or locally available id exists. " +
1094
- "Never guess userId from names, nicknames, or plain @name text. Plain @name is not a real mention. " +
1095
- "Pass the visible label as `mentions[].display` when known. Do not copy the visible @mention into `text`; put only the message body in `text`. If display is unknown but a concrete userId is available and a human-readable label matters, call clawchat_get_user_profile first and use the returned nickname. " +
1096
- "After this tool succeeds, the mention message has already been sent; output only <clawchat:no-reply/> and do not send a normal follow-up reply.",
1097
- ),
1098
- parameters: ClawchatMentionMessageSchema,
1099
- async execute(_callId, params) {
1100
- return await recordClawchatToolCall("clawchat_mention_message", params, async () => {
1101
- const validated = validateMentionMessageParams(params);
1102
- if (!validated.ok) return validationError(validated.message);
1103
- const account = resolveCurrent();
1104
- if (!account.configured) {
1105
- return configError("clawchat-plugin-openclaw: account is not configured");
1106
- }
1107
- try {
1108
- const p = validated.params;
1109
- const client = getOpenclawClawlingClient(account.accountId);
1110
- if (!client) {
1111
- return configError("ClawChat websocket client is not ready");
1164
+ (ctx) => {
1165
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1166
+ return {
1167
+ name: "clawchat_mention_message",
1168
+ label: "Send ClawChat Mention Message",
1169
+ description: toolDescription(
1170
+ "Send a real ClawChat mention message to a direct or group conversation. " +
1171
+ "TRIGGER - invoke when the user asks to @, mention, notify, or address ClawChat users. " +
1172
+ "Use `mentioned_users` or `sender_id` from current ClawChat sender/group message metadata as `mentions[].userId` when present. " +
1173
+ "Use `clawchat_search_users` only when no explicit or locally available id exists. " +
1174
+ "Never guess userId from names, nicknames, or plain @name text. Plain @name is not a real mention. " +
1175
+ "Pass the visible label as `mentions[].display` when known. Do not copy the visible @mention into `text`; put only the message body in `text`. If display is unknown but a concrete userId is available and a human-readable label matters, call clawchat_get_user_profile first and use the returned nickname. " +
1176
+ "After this tool succeeds, the mention message has already been sent; output only <clawchat:no-reply/> and do not send a normal follow-up reply.",
1177
+ ),
1178
+ parameters: ClawchatMentionMessageSchema,
1179
+ async execute(_callId, params) {
1180
+ return await recordClawchatToolCall(accountId, "clawchat_mention_message", params, async () => {
1181
+ const validated = validateMentionMessageParams(params);
1182
+ if (!validated.ok) return validationError(validated.message);
1183
+ const account = resolveCurrent(accountId);
1184
+ if (!account.configured) {
1185
+ return configError("clawchat-plugin-openclaw: account is not configured");
1112
1186
  }
1113
- const result = await sendOpenclawClawlingMentionMessage({
1114
- client,
1115
- account,
1116
- to: {
1187
+ try {
1188
+ const p = validated.params;
1189
+ const client = getOpenclawClawlingClient(account.accountId);
1190
+ if (!client) {
1191
+ return configError("ClawChat websocket client is not ready");
1192
+ }
1193
+ const result = await sendOpenclawClawlingMentionMessage({
1194
+ client,
1195
+ account,
1196
+ to: {
1197
+ chatId: p.chatId.trim(),
1198
+ chatType: p.chatType ?? "group",
1199
+ },
1200
+ ...(p.text !== undefined ? { text: p.text } : {}),
1201
+ mentions: p.mentions,
1202
+ ...(p.replyToMessageId
1203
+ ? {
1204
+ replyCtx: {
1205
+ replyToMessageId: p.replyToMessageId,
1206
+ },
1207
+ }
1208
+ : {}),
1209
+ log: api.logger,
1210
+ });
1211
+ if (!result) return jsonResponse(result);
1212
+ markTerminalClawChatSend({
1213
+ accountId: account.accountId,
1117
1214
  chatId: p.chatId.trim(),
1118
- chatType: p.chatType ?? "group",
1119
- },
1120
- ...(p.text !== undefined ? { text: p.text } : {}),
1121
- mentions: p.mentions,
1122
- ...(p.replyToMessageId
1123
- ? {
1124
- replyCtx: {
1125
- replyToMessageId: p.replyToMessageId,
1126
- },
1127
- }
1128
- : {}),
1129
- log: api.logger,
1130
- });
1131
- if (!result) return jsonResponse(result);
1132
- markTerminalClawChatSend({
1133
- accountId: account.accountId,
1134
- chatId: p.chatId.trim(),
1135
- messageId: result.messageId,
1136
- });
1137
- return jsonResponse({
1138
- sent: true,
1139
- terminal: true,
1140
- noFollowupReply: true,
1141
- instruction:
1142
- "The mention message has already been sent to ClawChat. If a follow-up response is required, output only <clawchat:no-reply/> and do not send a normal follow-up reply.",
1143
- ...result,
1144
- });
1145
- } catch (err) {
1146
- return genericError(err);
1147
- }
1148
- });
1149
- },
1215
+ messageId: result.messageId,
1216
+ });
1217
+ return jsonResponse({
1218
+ sent: true,
1219
+ terminal: true,
1220
+ noFollowupReply: true,
1221
+ instruction:
1222
+ "The mention message has already been sent to ClawChat. If a follow-up response is required, output only <clawchat:no-reply/> and do not send a normal follow-up reply.",
1223
+ ...result,
1224
+ });
1225
+ } catch (err) {
1226
+ return genericError(err);
1227
+ }
1228
+ });
1229
+ },
1230
+ };
1150
1231
  },
1151
1232
  { name: "clawchat_mention_message" },
1152
1233
  );
1153
1234
 
1154
1235
  api.registerTool(
1155
- {
1156
- name: "clawchat_react_message",
1157
- label: "React to ClawChat Message",
1158
- description: toolDescription(
1159
- "React to a ClawChat message with a single quick emoji (the bubble long-press reaction), instead of sending a text message. " +
1160
- "TRIGGER - invoke when a short acknowledgement or emotional beat (agreement, thanks, laughter, celebration, sympathy) reads more naturally as an emoji on the message than as a sentence, e.g. a simple 👍 to \"done\" or ❤️ to good news. " +
1161
- "Omit targetMessageId and chatId to react to the message that triggered the current turn; pass targetMessageId to react to an earlier message. " +
1162
- "Set remove:true to take back your reaction. " +
1163
- "Prefer the quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔. " +
1164
- "When a reaction is all you want to send, do not also send a normal text reply.",
1165
- ),
1166
- parameters: ClawchatReactMessageSchema,
1167
- async execute(_callId, params) {
1168
- return await recordClawchatToolCall("clawchat_react_message", params, async () => {
1169
- const p = params as ClawchatReactMessageParams;
1170
- if (!p.emoji?.trim()) {
1171
- return validationError("clawchat-plugin-openclaw: emoji is required");
1172
- }
1173
- const account = resolveCurrent();
1174
- if (!account.configured) {
1175
- return configError("clawchat-plugin-openclaw: account is not configured");
1176
- }
1177
- const scope = getCurrentTerminalSendScope();
1178
- const chatId = p.chatId?.trim() || scope?.chatId;
1179
- const targetMessageId = p.targetMessageId?.trim() || scope?.messageId;
1180
- if (!chatId) {
1181
- return validationError(
1182
- "clawchat-plugin-openclaw: chatId is required (no current-turn chat to default from)",
1183
- );
1184
- }
1185
- if (!targetMessageId) {
1186
- return validationError(
1187
- "clawchat-plugin-openclaw: targetMessageId is required (no current-turn message to default from)",
1188
- );
1189
- }
1190
- const client = getOpenclawClawlingClient(account.accountId);
1191
- if (!client) {
1192
- return configError("ClawChat websocket client is not ready");
1193
- }
1194
- const emoji = p.emoji.trim();
1195
- const removed = p.remove === true;
1196
- try {
1197
- sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
1198
- return jsonResponse({ reacted: true, targetMessageId, emoji, removed });
1199
- } catch (err) {
1200
- return genericError(err);
1201
- }
1202
- });
1203
- },
1236
+ (ctx) => {
1237
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1238
+ return {
1239
+ name: "clawchat_react_message",
1240
+ label: "React to ClawChat Message",
1241
+ description: toolDescription(
1242
+ "React to a ClawChat message with a single quick emoji (the bubble long-press reaction), instead of sending a text message. " +
1243
+ "TRIGGER - invoke when a short acknowledgement or emotional beat (agreement, thanks, laughter, celebration, sympathy) reads more naturally as an emoji on the message than as a sentence, e.g. a simple 👍 to \"done\" or ❤️ to good news. " +
1244
+ "Omit targetMessageId and chatId to react to the message that triggered the current turn; pass targetMessageId to react to an earlier message. " +
1245
+ "Set remove:true to take back your reaction. " +
1246
+ "Prefer the quick set 👍 ❤️ 😂 😮 😢 🙏 🎉 👏 🔥 😍 🤔. " +
1247
+ "When a reaction is all you want to send, do not also send a normal text reply.",
1248
+ ),
1249
+ parameters: ClawchatReactMessageSchema,
1250
+ async execute(_callId, params) {
1251
+ return await recordClawchatToolCall(accountId, "clawchat_react_message", params, async () => {
1252
+ const p = params as ClawchatReactMessageParams;
1253
+ if (!p.emoji?.trim()) {
1254
+ return validationError("clawchat-plugin-openclaw: emoji is required");
1255
+ }
1256
+ const account = resolveCurrent(accountId);
1257
+ if (!account.configured) {
1258
+ return configError("clawchat-plugin-openclaw: account is not configured");
1259
+ }
1260
+ const scope = getCurrentTerminalSendScope();
1261
+ const chatId = p.chatId?.trim() || scope?.chatId;
1262
+ const targetMessageId = p.targetMessageId?.trim() || scope?.messageId;
1263
+ if (!chatId) {
1264
+ return validationError(
1265
+ "clawchat-plugin-openclaw: chatId is required (no current-turn chat to default from)",
1266
+ );
1267
+ }
1268
+ if (!targetMessageId) {
1269
+ return validationError(
1270
+ "clawchat-plugin-openclaw: targetMessageId is required (no current-turn message to default from)",
1271
+ );
1272
+ }
1273
+ const client = getOpenclawClawlingClient(account.accountId);
1274
+ if (!client) {
1275
+ return configError("ClawChat websocket client is not ready");
1276
+ }
1277
+ const emoji = p.emoji.trim();
1278
+ const removed = p.remove === true;
1279
+ try {
1280
+ sendOpenclawClawlingReaction({ client, chatId, targetMessageId, emoji, removed, log: api.logger });
1281
+ return jsonResponse({ reacted: true, targetMessageId, emoji, removed });
1282
+ } catch (err) {
1283
+ return genericError(err);
1284
+ }
1285
+ });
1286
+ },
1287
+ };
1204
1288
  },
1205
1289
  { name: "clawchat_react_message" },
1206
1290
  );
1207
1291
 
1208
1292
  api.registerTool(
1209
- {
1210
- name: "clawchat_list_moments",
1211
- label: "List ClawChat Moments",
1212
- description: toolDescription(
1213
- "List the configured ClawChat account's visible moments feed, including moments from the account and its friends. " +
1214
- "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\". " +
1215
- "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.",
1216
- ),
1217
- parameters: ClawchatListMomentsSchema,
1218
- async execute(_callId, params) {
1219
- return await recordClawchatToolCall("clawchat_list_moments", params, async () => {
1220
- const p = (params ?? {}) as ClawchatListMomentsParams;
1221
- return await withClient((c) =>
1222
- c.listMoments({
1223
- ...(typeof p.before === "number" ? { before: p.before } : {}),
1224
- ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
1225
- }),
1226
- );
1227
- });
1228
- },
1293
+ (ctx) => {
1294
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1295
+ return {
1296
+ name: "clawchat_list_moments",
1297
+ label: "List ClawChat Moments",
1298
+ description: toolDescription(
1299
+ "List the configured ClawChat account's visible moments feed, including moments from the account and its friends. " +
1300
+ "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\". " +
1301
+ "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.",
1302
+ ),
1303
+ parameters: ClawchatListMomentsSchema,
1304
+ async execute(_callId, params) {
1305
+ return await recordClawchatToolCall(accountId, "clawchat_list_moments", params, async () => {
1306
+ const p = (params ?? {}) as ClawchatListMomentsParams;
1307
+ return await withClient(accountId, (c) =>
1308
+ c.listMoments({
1309
+ ...(typeof p.before === "number" ? { before: p.before } : {}),
1310
+ ...(typeof p.limit === "number" ? { limit: p.limit } : {}),
1311
+ }),
1312
+ );
1313
+ });
1314
+ },
1315
+ };
1229
1316
  },
1230
1317
  { name: "clawchat_list_moments" },
1231
1318
  );
1232
1319
 
1233
1320
  api.registerTool(
1234
- {
1235
- name: "clawchat_get_moment",
1236
- label: "Get ClawChat Moment",
1237
- description: toolDescription(
1238
- "Fetch a single ClawChat moment by id together with the comments visible to this agent. TRIGGER - after a moment.comment.created / moment.comment.replied awareness note, call this with the moment id to read the new comment before deciding whether to reply.",
1239
- ),
1240
- parameters: ClawchatGetMomentSchema,
1241
- async execute(_callId, params) {
1242
- return await recordClawchatToolCall("clawchat_get_moment", params, async () => {
1243
- const p = (params ?? {}) as ClawchatGetMomentParams;
1244
- return await withClient((c) => c.getMoment(p.momentId));
1245
- });
1246
- },
1321
+ (ctx) => {
1322
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1323
+ return {
1324
+ name: "clawchat_get_moment",
1325
+ label: "Get ClawChat Moment",
1326
+ description: toolDescription(
1327
+ "Fetch a single ClawChat moment by id together with the comments visible to this agent. TRIGGER - after a moment.comment.created / moment.comment.replied awareness note, call this with the moment id to read the new comment before deciding whether to reply.",
1328
+ ),
1329
+ parameters: ClawchatGetMomentSchema,
1330
+ async execute(_callId, params) {
1331
+ return await recordClawchatToolCall(accountId, "clawchat_get_moment", params, async () => {
1332
+ const p = (params ?? {}) as ClawchatGetMomentParams;
1333
+ return await withClient(accountId, (c) => c.getMoment(p.momentId));
1334
+ });
1335
+ },
1336
+ };
1247
1337
  },
1248
1338
  { name: "clawchat_get_moment" },
1249
1339
  );
1250
1340
 
1251
1341
  api.registerTool(
1252
- {
1253
- name: "clawchat_create_moment",
1254
- label: "Create ClawChat Moment",
1255
- description: toolDescription(
1256
- "Create a new ClawChat moment/dynamic for the configured ClawChat account. " +
1257
- "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 ...\". " +
1258
- "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.",
1259
- ),
1260
- parameters: ClawchatCreateMomentSchema,
1261
- async execute(_callId, params) {
1262
- return await recordClawchatToolCall("clawchat_create_moment", params, async () => {
1263
- const p = (params ?? {}) as ClawchatCreateMomentParams;
1264
- const text = typeof p.text === "string" ? p.text : undefined;
1265
- const images = Array.isArray(p.images) ? p.images : undefined;
1266
- if (!text && (!images || images.length === 0)) {
1267
- return validationError("clawchat-plugin-openclaw: at least one of text or images is required");
1268
- }
1269
- return await withGatedClient((c) =>
1270
- c.createMoment({
1271
- ...(text !== undefined ? { text } : {}),
1272
- ...(images !== undefined ? { images } : {}),
1273
- }),
1274
- );
1275
- });
1276
- },
1342
+ (ctx) => {
1343
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1344
+ return {
1345
+ name: "clawchat_create_moment",
1346
+ label: "Create ClawChat Moment",
1347
+ description: toolDescription(
1348
+ "Create a new ClawChat moment/dynamic for the configured ClawChat account. " +
1349
+ "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 ...\". " +
1350
+ "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.",
1351
+ ),
1352
+ parameters: ClawchatCreateMomentSchema,
1353
+ async execute(_callId, params) {
1354
+ return await recordClawchatToolCall(accountId, "clawchat_create_moment", params, async () => {
1355
+ const p = (params ?? {}) as ClawchatCreateMomentParams;
1356
+ const text = typeof p.text === "string" ? p.text : undefined;
1357
+ const images = Array.isArray(p.images) ? p.images : undefined;
1358
+ if (!text && (!images || images.length === 0)) {
1359
+ return validationError("clawchat-plugin-openclaw: at least one of text or images is required");
1360
+ }
1361
+ return await withGatedClient(accountId, (c) =>
1362
+ c.createMoment({
1363
+ ...(text !== undefined ? { text } : {}),
1364
+ ...(images !== undefined ? { images } : {}),
1365
+ }),
1366
+ );
1367
+ });
1368
+ },
1369
+ };
1277
1370
  },
1278
1371
  { name: "clawchat_create_moment" },
1279
1372
  );
1280
1373
 
1281
1374
  api.registerTool(
1282
- {
1283
- name: "clawchat_delete_moment",
1284
- label: "Delete ClawChat Moment",
1285
- description: toolDescription(
1286
- "Delete a ClawChat moment by moment id. " +
1287
- "TRIGGER - invoke when the user asks to delete/remove one of the configured account's ClawChat moments/dynamics and provides or selects a concrete moment id. " +
1288
- "Only the moment author can delete it. Do not guess the id; list moments first if the user refers to a moment ambiguously.",
1289
- ),
1290
- parameters: ClawchatDeleteMomentSchema,
1291
- async execute(_callId, params) {
1292
- return await recordClawchatToolCall("clawchat_delete_moment", params, async () => {
1293
- const p = params as ClawchatDeleteMomentParams;
1294
- return await withGatedClient((c) => c.deleteMoment(p.momentId));
1295
- });
1296
- },
1375
+ (ctx) => {
1376
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1377
+ return {
1378
+ name: "clawchat_delete_moment",
1379
+ label: "Delete ClawChat Moment",
1380
+ description: toolDescription(
1381
+ "Delete a ClawChat moment by moment id. " +
1382
+ "TRIGGER - invoke when the user asks to delete/remove one of the configured account's ClawChat moments/dynamics and provides or selects a concrete moment id. " +
1383
+ "Only the moment author can delete it. Do not guess the id; list moments first if the user refers to a moment ambiguously.",
1384
+ ),
1385
+ parameters: ClawchatDeleteMomentSchema,
1386
+ async execute(_callId, params) {
1387
+ return await recordClawchatToolCall(accountId, "clawchat_delete_moment", params, async () => {
1388
+ const p = params as ClawchatDeleteMomentParams;
1389
+ return await withGatedClient(accountId, (c) => c.deleteMoment(p.momentId));
1390
+ });
1391
+ },
1392
+ };
1297
1393
  },
1298
1394
  { name: "clawchat_delete_moment" },
1299
1395
  );
1300
1396
 
1301
1397
  api.registerTool(
1302
- {
1303
- name: "clawchat_toggle_moment_reaction",
1304
- label: "Toggle ClawChat Moment Reaction",
1305
- description: toolDescription(
1306
- "Toggle an emoji reaction on a ClawChat moment. " +
1307
- "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\". " +
1308
- "The API adds the reaction if missing and removes it if already present. Require a concrete moment id and emoji.",
1309
- ),
1310
- parameters: ClawchatToggleMomentReactionSchema,
1311
- async execute(_callId, params) {
1312
- return await recordClawchatToolCall("clawchat_toggle_moment_reaction", params, async () => {
1313
- const p = params as ClawchatToggleMomentReactionParams;
1314
- if (!p.emoji?.trim()) {
1315
- return validationError("clawchat-plugin-openclaw: emoji is required");
1316
- }
1317
- return await withGatedClient((c) =>
1318
- c.toggleMomentReaction({ momentId: p.momentId, emoji: p.emoji }),
1319
- );
1320
- });
1321
- },
1398
+ (ctx) => {
1399
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1400
+ return {
1401
+ name: "clawchat_toggle_moment_reaction",
1402
+ label: "Toggle ClawChat Moment Reaction",
1403
+ description: toolDescription(
1404
+ "Toggle an emoji reaction on a ClawChat moment. " +
1405
+ "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\". " +
1406
+ "The API adds the reaction if missing and removes it if already present. Require a concrete moment id and emoji.",
1407
+ ),
1408
+ parameters: ClawchatToggleMomentReactionSchema,
1409
+ async execute(_callId, params) {
1410
+ return await recordClawchatToolCall(accountId, "clawchat_toggle_moment_reaction", params, async () => {
1411
+ const p = params as ClawchatToggleMomentReactionParams;
1412
+ if (!p.emoji?.trim()) {
1413
+ return validationError("clawchat-plugin-openclaw: emoji is required");
1414
+ }
1415
+ return await withGatedClient(accountId, (c) =>
1416
+ c.toggleMomentReaction({ momentId: p.momentId, emoji: p.emoji }),
1417
+ );
1418
+ });
1419
+ },
1420
+ };
1322
1421
  },
1323
1422
  { name: "clawchat_toggle_moment_reaction" },
1324
1423
  );
1325
1424
 
1326
1425
  api.registerTool(
1327
- {
1328
- name: "clawchat_create_moment_comment",
1329
- label: "Create ClawChat Moment Comment",
1330
- description: toolDescription(
1331
- "Create a top-level comment on a ClawChat moment. " +
1332
- "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 ...\". " +
1333
- "Require a concrete moment id and non-empty text. Use clawchat_reply_moment_comment when the user is replying to another user's comment.",
1334
- ),
1335
- parameters: ClawchatCreateMomentCommentSchema,
1336
- async execute(_callId, params) {
1337
- return await recordClawchatToolCall("clawchat_create_moment_comment", params, async () => {
1338
- const p = params as ClawchatCreateMomentCommentParams;
1339
- if (!p.text?.trim()) {
1340
- return validationError("clawchat-plugin-openclaw: text is required");
1341
- }
1342
- return await withGatedClient((c) =>
1343
- c.createMomentComment({ momentId: p.momentId, text: p.text }),
1344
- );
1345
- });
1346
- },
1426
+ (ctx) => {
1427
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1428
+ return {
1429
+ name: "clawchat_create_moment_comment",
1430
+ label: "Create ClawChat Moment Comment",
1431
+ description: toolDescription(
1432
+ "Create a top-level comment on a ClawChat moment. " +
1433
+ "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 ...\". " +
1434
+ "Require a concrete moment id and non-empty text. Use clawchat_reply_moment_comment when the user is replying to another user's comment.",
1435
+ ),
1436
+ parameters: ClawchatCreateMomentCommentSchema,
1437
+ async execute(_callId, params) {
1438
+ return await recordClawchatToolCall(accountId, "clawchat_create_moment_comment", params, async () => {
1439
+ const p = params as ClawchatCreateMomentCommentParams;
1440
+ if (!p.text?.trim()) {
1441
+ return validationError("clawchat-plugin-openclaw: text is required");
1442
+ }
1443
+ return await withGatedClient(accountId, (c) =>
1444
+ c.createMomentComment({ momentId: p.momentId, text: p.text }),
1445
+ );
1446
+ });
1447
+ },
1448
+ };
1347
1449
  },
1348
1450
  { name: "clawchat_create_moment_comment" },
1349
1451
  );
1350
1452
 
1351
1453
  api.registerTool(
1352
- {
1353
- name: "clawchat_reply_moment_comment",
1354
- label: "Reply To ClawChat Moment Comment",
1355
- description: toolDescription(
1356
- "Reply to an existing ClawChat moment comment with a single-level reply. " +
1357
- "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 ...\". " +
1358
- "Require concrete moment and comment ids; do not use this for top-level comments.",
1359
- ),
1360
- parameters: ClawchatReplyMomentCommentSchema,
1361
- async execute(_callId, params) {
1362
- return await recordClawchatToolCall("clawchat_reply_moment_comment", params, async () => {
1363
- const p = params as ClawchatReplyMomentCommentParams;
1364
- if (!p.text?.trim()) {
1365
- return validationError("clawchat-plugin-openclaw: text is required");
1366
- }
1367
- return await withGatedClient((c) =>
1368
- c.replyMomentComment({
1369
- momentId: p.momentId,
1370
- replyToCommentId: p.replyToCommentId,
1371
- text: p.text,
1372
- }),
1373
- );
1374
- });
1375
- },
1454
+ (ctx) => {
1455
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1456
+ return {
1457
+ name: "clawchat_reply_moment_comment",
1458
+ label: "Reply To ClawChat Moment Comment",
1459
+ description: toolDescription(
1460
+ "Reply to an existing ClawChat moment comment with a single-level reply. " +
1461
+ "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 ...\". " +
1462
+ "Require concrete moment and comment ids; do not use this for top-level comments.",
1463
+ ),
1464
+ parameters: ClawchatReplyMomentCommentSchema,
1465
+ async execute(_callId, params) {
1466
+ return await recordClawchatToolCall(accountId, "clawchat_reply_moment_comment", params, async () => {
1467
+ const p = params as ClawchatReplyMomentCommentParams;
1468
+ if (!p.text?.trim()) {
1469
+ return validationError("clawchat-plugin-openclaw: text is required");
1470
+ }
1471
+ return await withGatedClient(accountId, (c) =>
1472
+ c.replyMomentComment({
1473
+ momentId: p.momentId,
1474
+ replyToCommentId: p.replyToCommentId,
1475
+ text: p.text,
1476
+ }),
1477
+ );
1478
+ });
1479
+ },
1480
+ };
1376
1481
  },
1377
1482
  { name: "clawchat_reply_moment_comment" },
1378
1483
  );
1379
1484
 
1380
1485
  api.registerTool(
1381
- {
1382
- name: "clawchat_delete_moment_comment",
1383
- label: "Delete ClawChat Moment Comment",
1384
- description: toolDescription(
1385
- "Delete a comment on a ClawChat moment. " +
1386
- "TRIGGER - invoke when the user asks to delete/remove a specific comment or reply from a ClawChat moment/dynamic and provides concrete moment and comment ids. " +
1387
- "The caller may delete comments they authored or comments on moments they authored. Do not guess ids; list moments first if needed.",
1388
- ),
1389
- parameters: ClawchatDeleteMomentCommentSchema,
1390
- async execute(_callId, params) {
1391
- return await recordClawchatToolCall("clawchat_delete_moment_comment", params, async () => {
1392
- const p = params as ClawchatDeleteMomentCommentParams;
1393
- return await withGatedClient((c) =>
1394
- c.deleteMomentComment({ momentId: p.momentId, commentId: p.commentId }),
1395
- );
1396
- });
1397
- },
1486
+ (ctx) => {
1487
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1488
+ return {
1489
+ name: "clawchat_delete_moment_comment",
1490
+ label: "Delete ClawChat Moment Comment",
1491
+ description: toolDescription(
1492
+ "Delete a comment on a ClawChat moment. " +
1493
+ "TRIGGER - invoke when the user asks to delete/remove a specific comment or reply from a ClawChat moment/dynamic and provides concrete moment and comment ids. " +
1494
+ "The caller may delete comments they authored or comments on moments they authored. Do not guess ids; list moments first if needed.",
1495
+ ),
1496
+ parameters: ClawchatDeleteMomentCommentSchema,
1497
+ async execute(_callId, params) {
1498
+ return await recordClawchatToolCall(accountId, "clawchat_delete_moment_comment", params, async () => {
1499
+ const p = params as ClawchatDeleteMomentCommentParams;
1500
+ return await withGatedClient(accountId, (c) =>
1501
+ c.deleteMomentComment({ momentId: p.momentId, commentId: p.commentId }),
1502
+ );
1503
+ });
1504
+ },
1505
+ };
1398
1506
  },
1399
1507
  { name: "clawchat_delete_moment_comment" },
1400
1508
  );
1401
1509
 
1402
1510
  api.registerTool(
1403
- {
1404
- name: "clawchat_update_account_profile",
1405
- label: "Update ClawChat Account Profile",
1406
- description: toolDescription(
1407
- "Update nickname/avatar_url/bio on the agent's connected ClawChat account (the configured ClawChat account), which mirrors the local assistant identity. " +
1408
- "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: " +
1409
- "(1) ClawChat account nickname/name change: 'change the ClawChat account nickname to X', " +
1410
- "'set this assistant name to X', 'change the assistant nickname to X', 'set the account name to X' " +
1411
- "→ call with `nickname = X`; " +
1412
- "(2) ClawChat account avatar/profile-picture change: 'change the ClawChat account avatar', " +
1413
- "'use this image as the assistant profile picture', 'change the account avatar to this image' " +
1414
- "→ first obtain the avatar URL (upload via `clawchat_upload_avatar_image`, OR use a provided URL directly), " +
1415
- "then call this tool with `avatar_url = <url>`; " +
1416
- "(3) ClawChat account bio/self-introduction change: 'update the ClawChat bio', " +
1417
- "'set the assistant self-introduction to X', 'change the account bio to X' " +
1418
- "→ call with `bio = X`. " +
1419
- "You can pass `nickname`, `avatar_url`, and `bio` together in one call, or just one of them. " +
1420
- "At least one of the three must be present. Do not frame this as updating a human user's personal account.",
1421
- ),
1422
- parameters: ClawchatUpdateAccountProfileSchema,
1423
- async execute(_callId, params) {
1424
- return await recordClawchatToolCall("clawchat_update_account_profile", params, async () => {
1425
- const p = (params ?? {}) as ClawchatUpdateAccountProfileParams;
1426
- const patch: { nickname?: string; avatar_url?: string; bio?: string } = {};
1427
- if (typeof p.nickname === "string") patch.nickname = p.nickname;
1428
- if (typeof p.avatar_url === "string") patch.avatar_url = p.avatar_url;
1429
- if (typeof p.bio === "string") patch.bio = p.bio;
1430
- if (Object.keys(patch).length === 0) {
1431
- return validationError(
1432
- "clawchat-plugin-openclaw: at least one of nickname / avatar / bio is required",
1433
- );
1434
- }
1435
- return await withClient((c): Promise<Profile> => c.updateMyProfile(patch));
1436
- });
1437
- },
1511
+ (ctx) => {
1512
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1513
+ return {
1514
+ name: "clawchat_update_account_profile",
1515
+ label: "Update ClawChat Account Profile",
1516
+ description: toolDescription(
1517
+ "Update nickname/avatar_url/bio on the agent's connected ClawChat account (the configured ClawChat account), which mirrors the local assistant identity. " +
1518
+ "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: " +
1519
+ "(1) ClawChat account nickname/name change: 'change the ClawChat account nickname to X', " +
1520
+ "'set this assistant name to X', 'change the assistant nickname to X', 'set the account name to X' " +
1521
+ "→ call with `nickname = X`; " +
1522
+ "(2) ClawChat account avatar/profile-picture change: 'change the ClawChat account avatar', " +
1523
+ "'use this image as the assistant profile picture', 'change the account avatar to this image' " +
1524
+ "→ first obtain the avatar URL (upload via `clawchat_upload_avatar_image`, OR use a provided URL directly), " +
1525
+ "then call this tool with `avatar_url = <url>`; " +
1526
+ "(3) ClawChat account bio/self-introduction change: 'update the ClawChat bio', " +
1527
+ "'set the assistant self-introduction to X', 'change the account bio to X' " +
1528
+ "→ call with `bio = X`. " +
1529
+ "You can pass `nickname`, `avatar_url`, and `bio` together in one call, or just one of them. " +
1530
+ "At least one of the three must be present. Do not frame this as updating a human user's personal account.",
1531
+ ),
1532
+ parameters: ClawchatUpdateAccountProfileSchema,
1533
+ async execute(_callId, params) {
1534
+ return await recordClawchatToolCall(accountId, "clawchat_update_account_profile", params, async () => {
1535
+ const p = (params ?? {}) as ClawchatUpdateAccountProfileParams;
1536
+ const patch: { nickname?: string; avatar_url?: string; bio?: string } = {};
1537
+ if (typeof p.nickname === "string") patch.nickname = p.nickname;
1538
+ if (typeof p.avatar_url === "string") patch.avatar_url = p.avatar_url;
1539
+ if (typeof p.bio === "string") patch.bio = p.bio;
1540
+ if (Object.keys(patch).length === 0) {
1541
+ return validationError(
1542
+ "clawchat-plugin-openclaw: at least one of nickname / avatar / bio is required",
1543
+ );
1544
+ }
1545
+ return await withClient(accountId, (c): Promise<Profile> => c.updateMyProfile(patch));
1546
+ });
1547
+ },
1548
+ };
1438
1549
  },
1439
1550
  { name: "clawchat_update_account_profile" },
1440
1551
  );
1441
1552
 
1442
1553
  api.registerTool(
1443
- {
1444
- name: "clawchat_upload_avatar_image",
1445
- label: "Upload ClawChat Avatar Image",
1446
- description: toolDescription(
1447
- "Upload an absolute local image path for use as the agent's connected ClawChat account avatar (max 20MB), returning a hosted avatar URL. " +
1448
- "TRIGGER — invoke when the user provides an absolute local image path and asks to upload it for the ClawChat account avatar/profile picture. " +
1449
- "This tool does not update or set the account avatar by itself; when the user asked to set or sync the avatar, call `clawchat_update_account_profile` with `avatar_url` after this tool returns a URL.",
1450
- ),
1451
- parameters: ClawchatUploadAvatarImageSchema,
1452
- async execute(_callId, params) {
1453
- return await recordClawchatToolCall("clawchat_upload_avatar_image", params, async () => {
1454
- const p = params as ClawchatUploadAvatarImageParams;
1455
- if (!p.filePath || !path.isAbsolute(p.filePath)) {
1456
- return validationError("clawchat-plugin-openclaw: filePath must be an absolute local path");
1457
- }
1458
- let stat: fs.Stats;
1459
- try {
1460
- stat = fs.statSync(p.filePath);
1461
- } catch (err) {
1462
- return validationError(
1463
- `clawchat-plugin-openclaw: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`,
1464
- );
1465
- }
1466
- if (!stat.isFile()) {
1467
- return validationError(`clawchat-plugin-openclaw: ${p.filePath} is not a regular file`);
1468
- }
1469
- if (stat.size > MAX_UPLOAD_BYTES) {
1470
- return validationError(
1471
- `clawchat-plugin-openclaw: file too large (${stat.size} bytes; max 20MB)`,
1472
- );
1473
- }
1474
- const buffer = fs.readFileSync(p.filePath);
1475
- const filename = path.basename(p.filePath);
1476
- const mime = inferMimeFromPath(p.filePath);
1477
- return await withClient((c) => c.uploadAvatar({ buffer, filename, mime }));
1478
- });
1479
- },
1554
+ (ctx) => {
1555
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1556
+ return {
1557
+ name: "clawchat_upload_avatar_image",
1558
+ label: "Upload ClawChat Avatar Image",
1559
+ description: toolDescription(
1560
+ "Upload an absolute local image path for use as the agent's connected ClawChat account avatar (max 20MB), returning a hosted avatar URL. " +
1561
+ "TRIGGER — invoke when the user provides an absolute local image path and asks to upload it for the ClawChat account avatar/profile picture. " +
1562
+ "This tool does not update or set the account avatar by itself; when the user asked to set or sync the avatar, call `clawchat_update_account_profile` with `avatar_url` after this tool returns a URL.",
1563
+ ),
1564
+ parameters: ClawchatUploadAvatarImageSchema,
1565
+ async execute(_callId, params) {
1566
+ return await recordClawchatToolCall(accountId, "clawchat_upload_avatar_image", params, async () => {
1567
+ const p = params as ClawchatUploadAvatarImageParams;
1568
+ if (!p.filePath || !path.isAbsolute(p.filePath)) {
1569
+ return validationError("clawchat-plugin-openclaw: filePath must be an absolute local path");
1570
+ }
1571
+ let stat: fs.Stats;
1572
+ try {
1573
+ stat = fs.statSync(p.filePath);
1574
+ } catch (err) {
1575
+ return validationError(
1576
+ `clawchat-plugin-openclaw: cannot stat ${p.filePath}: ${err instanceof Error ? err.message : String(err)}`,
1577
+ );
1578
+ }
1579
+ if (!stat.isFile()) {
1580
+ return validationError(`clawchat-plugin-openclaw: ${p.filePath} is not a regular file`);
1581
+ }
1582
+ if (stat.size > MAX_UPLOAD_BYTES) {
1583
+ return validationError(
1584
+ `clawchat-plugin-openclaw: file too large (${stat.size} bytes; max 20MB)`,
1585
+ );
1586
+ }
1587
+ const buffer = fs.readFileSync(p.filePath);
1588
+ const filename = path.basename(p.filePath);
1589
+ const mime = inferMimeFromPath(p.filePath);
1590
+ return await withClient(accountId, (c) => c.uploadAvatar({ buffer, filename, mime }));
1591
+ });
1592
+ },
1593
+ };
1480
1594
  },
1481
1595
  { name: "clawchat_upload_avatar_image" },
1482
1596
  );
1483
1597
 
1484
1598
  api.registerTool(
1485
- {
1486
- name: "clawchat_register_app",
1487
- label: "Register ClawChat App",
1488
- description: toolDescription(
1489
- "Register a liveware-tunneled web app to ClawChat so it appears in the owner's chat with this agent. " +
1490
- "Call AFTER `liveware tunnel bind` returns a public URL. Params: name, appId (liveware app id), url (public URL).",
1491
- ),
1492
- parameters: ClawchatRegisterAppSchema,
1493
- async execute(_callId, params) {
1494
- return await recordClawchatToolCall("clawchat_register_app", params, async () => {
1495
- const p = params as ClawchatRegisterAppParams;
1496
- return await withClient((c) => c.registerApp({ name: p.name, appId: p.appId, url: p.url }));
1497
- });
1498
- },
1599
+ (ctx) => {
1600
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1601
+ return {
1602
+ name: "clawchat_register_app",
1603
+ label: "Register ClawChat App",
1604
+ description: toolDescription(
1605
+ "Register a liveware-tunneled web app to ClawChat so it appears in the owner's chat with this agent. " +
1606
+ "Call AFTER `liveware tunnel bind` returns a public URL. Params: name, appId (liveware app id), url (public URL).",
1607
+ ),
1608
+ parameters: ClawchatRegisterAppSchema,
1609
+ async execute(_callId, params) {
1610
+ return await recordClawchatToolCall(accountId, "clawchat_register_app", params, async () => {
1611
+ const p = params as ClawchatRegisterAppParams;
1612
+ return await withClient(accountId, (c) => c.registerApp({ name: p.name, appId: p.appId, url: p.url }));
1613
+ });
1614
+ },
1615
+ };
1499
1616
  },
1500
1617
  { name: "clawchat_register_app" },
1501
1618
  );
1502
1619
 
1503
1620
  api.registerTool(
1504
- {
1505
- name: "clawchat_list_apps",
1506
- label: "List ClawChat Apps",
1507
- description: toolDescription("List the liveware web apps this agent has registered to ClawChat."),
1508
- parameters: ClawchatListAppsSchema,
1509
- async execute(_callId, params) {
1510
- return await recordClawchatToolCall("clawchat_list_apps", params, async () => withClient((c) => c.listApps()));
1511
- },
1621
+ (ctx) => {
1622
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1623
+ return {
1624
+ name: "clawchat_list_apps",
1625
+ label: "List ClawChat Apps",
1626
+ description: toolDescription("List the liveware web apps this agent has registered to ClawChat."),
1627
+ parameters: ClawchatListAppsSchema,
1628
+ async execute(_callId, params) {
1629
+ return await recordClawchatToolCall(accountId, "clawchat_list_apps", params, async () =>
1630
+ withClient(accountId, (c) => c.listApps()),
1631
+ );
1632
+ },
1633
+ };
1512
1634
  },
1513
1635
  { name: "clawchat_list_apps" },
1514
1636
  );
1515
1637
 
1516
1638
  api.registerTool(
1517
- {
1518
- name: "clawchat_unregister_app",
1519
- label: "Unregister ClawChat App",
1520
- description: toolDescription("Unregister a previously registered liveware app from ClawChat. Param: appId (liveware app id)."),
1521
- parameters: ClawchatUnregisterAppSchema,
1522
- async execute(_callId, params) {
1523
- return await recordClawchatToolCall("clawchat_unregister_app", params, async () => {
1524
- const p = params as ClawchatUnregisterAppParams;
1525
- return await withClient((c) => c.unregisterApp(p.appId));
1526
- });
1527
- },
1639
+ (ctx) => {
1640
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1641
+ return {
1642
+ name: "clawchat_unregister_app",
1643
+ label: "Unregister ClawChat App",
1644
+ description: toolDescription("Unregister a previously registered liveware app from ClawChat. Param: appId (liveware app id)."),
1645
+ parameters: ClawchatUnregisterAppSchema,
1646
+ async execute(_callId, params) {
1647
+ return await recordClawchatToolCall(accountId, "clawchat_unregister_app", params, async () => {
1648
+ const p = params as ClawchatUnregisterAppParams;
1649
+ return await withClient(accountId, (c) => c.unregisterApp(p.appId));
1650
+ });
1651
+ },
1652
+ };
1528
1653
  },
1529
1654
  { name: "clawchat_unregister_app" },
1530
1655
  );
1531
1656
 
1532
1657
  api.registerTool(
1533
- {
1534
- name: "clawchat_liveware_login",
1535
- label: "Login to Liveware",
1536
- description: toolDescription(
1537
- "Log in to liveware CLI using the plugin-held ClawChat access token. " +
1538
- "The token is resolved from the plugin's own credential store and is never passed through the LLM context. " +
1539
- "Call this BEFORE any liveware tunnel or app commands; it is idempotent. " +
1540
- "Returns { ok: true } on success. Returns an error if liveware is not installed, the ClawChat account is not activated, or login fails.",
1541
- ),
1542
- parameters: ClawchatLivewareLoginSchema,
1543
- async execute(_callId, params) {
1544
- return await recordClawchatToolCall("clawchat_liveware_login", params as ClawchatLivewareLoginParams, async () => {
1545
- const acct = resolveCurrent();
1546
- const token = acct.token;
1547
- if (!token) {
1548
- return configError("clawchat-plugin-openclaw: token is required");
1549
- }
1550
-
1551
- return await new Promise<ReturnType<typeof jsonResponse>>((resolve) => {
1552
- const livewarePath = resolveLivewarePath(api.runtime.state?.resolveStateDir?.());
1553
- if (!livewarePath) {
1554
- resolve(jsonResponse({
1555
- error: "config",
1556
- message: "liveware CLI not found in PATH; install liveware before calling this tool",
1557
- }));
1558
- return;
1658
+ (ctx) => {
1659
+ const accountId = resolveOpenclawClawlingToolAccountId(ctx);
1660
+ return {
1661
+ name: "clawchat_liveware_login",
1662
+ label: "Login to Liveware",
1663
+ description: toolDescription(
1664
+ "Log in to liveware CLI using the plugin-held ClawChat access token. " +
1665
+ "The token is resolved from the plugin's own credential store and is never passed through the LLM context. " +
1666
+ "Call this BEFORE any liveware tunnel or app commands; it is idempotent. " +
1667
+ "Returns { ok: true } on success. Returns an error if liveware is not installed, the ClawChat account is not activated, or login fails.",
1668
+ ),
1669
+ parameters: ClawchatLivewareLoginSchema,
1670
+ async execute(_callId, params) {
1671
+ return await recordClawchatToolCall(accountId, "clawchat_liveware_login", params as ClawchatLivewareLoginParams, async () => {
1672
+ const acct = resolveCurrent(accountId);
1673
+ const token = acct.token;
1674
+ if (!token) {
1675
+ return configError("clawchat-plugin-openclaw: token is required");
1559
1676
  }
1560
- // Security: the token is passed as a child process argv value using
1561
- // liveware's documented --access-token interface. It is in the child
1562
- // argv briefly during process creation; env-var or stdin delivery
1563
- // would be preferred if liveware supported it.
1564
- // NEVER log the argv array; scrub token from any captured output below.
1565
- execFile(
1566
- livewarePath,
1567
- ["login", "--access-token", token],
1568
- { timeout: 30000 },
1569
- (err, stdout, stderr) => {
1570
- const scrub = (s: string) => (token ? s.split(token).join("***") : s);
1571
- const safeStdout = scrub(stdout ?? "").trim();
1572
- const safeStderr = scrub(stderr ?? "").trim();
1573
-
1574
- if (err) {
1575
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
1677
+
1678
+ return await new Promise<ReturnType<typeof jsonResponse>>((resolve) => {
1679
+ const livewarePath = resolveLivewarePath(api.runtime.state?.resolveStateDir?.());
1680
+ if (!livewarePath) {
1681
+ resolve(jsonResponse({
1682
+ error: "config",
1683
+ message: "liveware CLI not found in PATH; install liveware before calling this tool",
1684
+ }));
1685
+ return;
1686
+ }
1687
+ // Security: the token is passed as a child process argv value using
1688
+ // liveware's documented --access-token interface. It is in the child
1689
+ // argv briefly during process creation; env-var or stdin delivery
1690
+ // would be preferred if liveware supported it.
1691
+ // NEVER log the argv array; scrub token from any captured output below.
1692
+ execFile(
1693
+ livewarePath,
1694
+ ["login", "--access-token", token],
1695
+ { timeout: 30000 },
1696
+ (err, stdout, stderr) => {
1697
+ const scrub = (s: string) => (token ? s.split(token).join("***") : s);
1698
+ const safeStdout = scrub(stdout ?? "").trim();
1699
+ const safeStderr = scrub(stderr ?? "").trim();
1700
+
1701
+ if (err) {
1702
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
1703
+ resolve(jsonResponse({
1704
+ error: "config",
1705
+ message: "liveware CLI not found in PATH; install liveware before calling this tool",
1706
+ }));
1707
+ return;
1708
+ }
1709
+ const detail = [safeStderr, safeStdout].filter(Boolean).join("\n").trim();
1576
1710
  resolve(jsonResponse({
1577
- error: "config",
1578
- message: "liveware CLI not found in PATH; install liveware before calling this tool",
1711
+ error: "liveware_login_failed",
1712
+ message: "liveware login failed" + (detail ? `: ${detail}` : ""),
1579
1713
  }));
1580
1714
  return;
1581
1715
  }
1582
- const detail = [safeStderr, safeStdout].filter(Boolean).join("\n").trim();
1583
- resolve(jsonResponse({
1584
- error: "liveware_login_failed",
1585
- message: "liveware login failed" + (detail ? `: ${detail}` : ""),
1586
- }));
1587
- return;
1588
- }
1589
1716
 
1590
- resolve(jsonResponse({ ok: true }));
1591
- },
1592
- );
1717
+ resolve(jsonResponse({ ok: true }));
1718
+ },
1719
+ );
1720
+ });
1593
1721
  });
1594
- });
1595
- },
1722
+ },
1723
+ };
1596
1724
  },
1597
1725
  { name: "clawchat_liveware_login" },
1598
1726
  );