@newbase-clawchat/openclaw-clawchat 2026.4.29 → 2026.4.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +33 -10
  2. package/dist/index.js +27 -0
  3. package/dist/src/api-client.js +156 -0
  4. package/dist/src/api-types.js +17 -0
  5. package/dist/src/buffered-stream.js +177 -0
  6. package/dist/src/channel.js +191 -0
  7. package/dist/src/client.js +176 -0
  8. package/dist/src/commands.js +35 -0
  9. package/dist/src/config.js +214 -0
  10. package/dist/src/inbound.js +133 -0
  11. package/dist/src/login.runtime.js +130 -0
  12. package/dist/src/media-runtime.js +85 -0
  13. package/dist/src/message-mapper.js +82 -0
  14. package/dist/src/outbound.js +181 -0
  15. package/dist/src/protocol.js +38 -0
  16. package/dist/src/reply-dispatcher.js +440 -0
  17. package/dist/src/runtime.js +288 -0
  18. package/dist/src/streaming.js +65 -0
  19. package/dist/src/tools-schema.js +38 -0
  20. package/dist/src/tools.js +287 -0
  21. package/openclaw.plugin.json +12 -0
  22. package/package.json +25 -5
  23. package/skills/clawchat-activate/SKILL.md +17 -8
  24. package/src/buffered-stream.test.ts +10 -0
  25. package/src/buffered-stream.ts +6 -6
  26. package/src/channel.outbound.test.ts +3 -3
  27. package/src/channel.ts +11 -3
  28. package/src/client.test.ts +8 -1
  29. package/src/client.ts +11 -10
  30. package/src/commands.test.ts +6 -0
  31. package/src/commands.ts +5 -1
  32. package/src/config.test.ts +3 -0
  33. package/src/config.ts +7 -0
  34. package/src/inbound.test.ts +4 -1
  35. package/src/inbound.ts +11 -10
  36. package/src/login.runtime.test.ts +36 -0
  37. package/src/login.runtime.ts +54 -26
  38. package/src/manifest.test.ts +98 -22
  39. package/src/outbound.test.ts +6 -5
  40. package/src/outbound.ts +8 -7
  41. package/src/reply-dispatcher.test.ts +418 -3
  42. package/src/reply-dispatcher.ts +137 -12
  43. package/src/runtime.ts +1 -0
  44. package/src/streaming.test.ts +12 -9
  45. package/src/streaming.ts +6 -6
  46. package/src/tools.test.ts +81 -18
  47. package/src/tools.ts +63 -72
package/src/runtime.ts CHANGED
@@ -262,6 +262,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
262
262
  ...(replyCtx ? { replyCtx } : {}),
263
263
  inboundMessageId: turn.messageId,
264
264
  inboundForFinalReply: {
265
+ chatId: turn.peer.id,
265
266
  senderId: turn.senderId,
266
267
  senderNickName: turn.senderNickName || turn.senderId,
267
268
  bodyText: turn.rawBody,
@@ -37,12 +37,12 @@ describe("openclaw-clawchat streaming", () => {
37
37
  "message.done",
38
38
  ]);
39
39
  expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
40
- ["u1", true, "direct"],
41
- ["u1", false, "direct"],
40
+ ["u1", true],
41
+ ["u1", false],
42
42
  ]);
43
43
  });
44
44
 
45
- it("message.created payload is minimal (just message_id); adds carry monotonic sequences", async () => {
45
+ it("message.created payload is minimal (just message_id); adds carry zero-based monotonic sequences", async () => {
46
46
  const { client, sent } = mockClient();
47
47
  await sendStreamingText({
48
48
  client,
@@ -54,10 +54,10 @@ describe("openclaw-clawchat streaming", () => {
54
54
 
55
55
  // created payload is { message_id } only — no embedded message / streaming.
56
56
  expect(sent[0]!.payload).toEqual({ message_id: "m1" });
57
- expect(sent[1]!.payload.sequence).toBe(1);
58
- expect(sent[2]!.payload.sequence).toBe(2);
59
- expect(sent[3]!.payload.sequence).toBe(3);
60
- expect((sent[4]!.payload.streaming as { sequence: number }).sequence).toBe(3);
57
+ expect(sent[1]!.payload.sequence).toBe(0);
58
+ expect(sent[2]!.payload.sequence).toBe(1);
59
+ expect(sent[3]!.payload.sequence).toBe(2);
60
+ expect((sent[4]!.payload.streaming as { sequence: number }).sequence).toBe(2);
61
61
  });
62
62
 
63
63
  it("each message.add carries fragments: [{ text: cumulative, delta: new }]", async () => {
@@ -107,10 +107,13 @@ describe("openclaw-clawchat streaming", () => {
107
107
  reason: "boom",
108
108
  });
109
109
  expect(sent[0]!.event).toBe("message.failed");
110
- expect(sent[0]!.payload.sequence).toBe(3);
110
+ expect(sent[0]!.payload.sequence).toBe(2);
111
111
  expect(sent[0]!.payload.reason).toBe("boom");
112
+ expect(sent[0]!.payload).toHaveProperty("completed_at");
113
+ expect(sent[0]!.payload).not.toHaveProperty("failed_at");
114
+ expect(sent[0]!.payload.fragments).toEqual([{ kind: "text", text: "boom" }]);
112
115
  expect((client.typing as ReturnType<typeof vi.fn>).mock.calls).toEqual([
113
- ["u1", false, "direct"],
116
+ ["u1", false],
114
117
  ]);
115
118
  });
116
119
  });
package/src/streaming.ts CHANGED
@@ -45,13 +45,13 @@ export async function sendStreamingText(params: StreamingSendParams): Promise<vo
45
45
  const routing = resolveRouting(params);
46
46
  const emitTyping = params.emitTyping !== false;
47
47
  if (emitTyping) {
48
- params.client.typing(routing.chatId, true, routing.chatType);
48
+ params.client.typing(routing.chatId, true);
49
49
  }
50
50
  emitStreamCreated(params.client, {
51
51
  messageId: params.messageId,
52
52
  routing,
53
53
  });
54
- let sequence = 0;
54
+ let sequence = -1;
55
55
  let fullText = "";
56
56
  for (const chunk of params.chunks) {
57
57
  sequence += 1;
@@ -67,11 +67,11 @@ export async function sendStreamingText(params: StreamingSendParams): Promise<vo
67
67
  emitStreamDone(params.client, {
68
68
  messageId: params.messageId,
69
69
  routing,
70
- finalSequence: sequence,
70
+ finalSequence: Math.max(sequence, 0),
71
71
  finalText: fullText,
72
72
  });
73
73
  if (emitTyping) {
74
- params.client.typing(routing.chatId, false, routing.chatType);
74
+ params.client.typing(routing.chatId, false);
75
75
  }
76
76
  }
77
77
 
@@ -90,10 +90,10 @@ export async function sendStreamingFailure(params: StreamingFailureParams): Prom
90
90
  emitStreamFailed(params.client, {
91
91
  messageId: params.messageId,
92
92
  routing,
93
- sequence: params.currentSequence + 1,
93
+ sequence: params.currentSequence,
94
94
  reason: params.reason,
95
95
  });
96
96
  if (params.emitTyping !== false) {
97
- params.client.typing(routing.chatId, false, routing.chatType);
97
+ params.client.typing(routing.chatId, false);
98
98
  }
99
99
  }
package/src/tools.test.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { describe, expect, it, vi } from "vitest";
1
+ import fs from "node:fs";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
3
  import { registerOpenclawClawlingTools } from "./tools.ts";
3
4
 
4
5
  const loginRuntime = vi.hoisted(() => ({
@@ -12,6 +13,16 @@ interface RegisteredTool {
12
13
  execute: (callId: string, params: unknown) => Promise<unknown>;
13
14
  }
14
15
 
16
+ const ALWAYS_VISIBLE_TOOL_NAMES = [
17
+ "clawchat_activate",
18
+ "clawchat_get_account_profile",
19
+ "clawchat_get_user_profile",
20
+ "clawchat_list_account_friends",
21
+ "clawchat_update_account_profile",
22
+ "clawchat_upload_avatar_image",
23
+ "clawchat_upload_media_file",
24
+ ];
25
+
15
26
  function buildApi(opts: {
16
27
  configChannel?: Record<string, unknown> | null;
17
28
  configTools?: Record<string, unknown>;
@@ -32,6 +43,11 @@ function buildApi(opts: {
32
43
  warn: vi.fn(),
33
44
  error: vi.fn(),
34
45
  },
46
+ runtime: {
47
+ config: {
48
+ mutateConfigFile: vi.fn(),
49
+ },
50
+ },
35
51
  registerTool: (tool: RegisteredTool, _options?: { name: string }) => {
36
52
  registered.push(tool);
37
53
  },
@@ -49,12 +65,22 @@ function configuredChannel(extra: Record<string, unknown> = {}) {
49
65
  }
50
66
 
51
67
  describe("registerOpenclawClawlingTools", () => {
52
- it("registers no tools when account.configured is false", () => {
68
+ beforeEach(() => {
69
+ vi.clearAllMocks();
70
+ });
71
+
72
+ it("uses OpenClaw SDK tool result types instead of direct pi-agent-core imports", () => {
73
+ const source = fs.readFileSync(new URL("./tools.ts", import.meta.url), "utf8");
74
+ expect(source).not.toMatch(/@mariozechner\/pi-agent-core/);
75
+ expect(source).toMatch(/openclaw\/plugin-sdk\/agent-harness-runtime/);
76
+ });
77
+
78
+ it("registers all ClawChat tools even when account.configured is false", () => {
53
79
  const { api, registered } = buildApi({
54
80
  configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
55
81
  });
56
82
  registerOpenclawClawlingTools(api);
57
- expect(registered.map((t) => t.name)).toEqual([]);
83
+ expect(registered.map((t) => t.name).sort()).toEqual(ALWAYS_VISIBLE_TOOL_NAMES);
58
84
  });
59
85
 
60
86
  it("does not mutate tool policy during registration before account activation", () => {
@@ -65,22 +91,51 @@ describe("registerOpenclawClawlingTools", () => {
65
91
 
66
92
  registerOpenclawClawlingTools(api);
67
93
 
68
- expect(registered.map((t) => t.name)).toEqual([]);
94
+ expect(registered.map((t) => t.name).sort()).toEqual(ALWAYS_VISIBLE_TOOL_NAMES);
69
95
  expect(api.config?.tools).toEqual({
70
96
  profile: "coding",
71
97
  allow: [],
72
98
  });
73
99
  });
74
100
 
75
- it("does not register clawchat_activate for invite-code onboarding", async () => {
101
+ it("registers clawchat_activate for invite-code onboarding", async () => {
102
+ const { api, registered } = buildApi({
103
+ configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
104
+ });
105
+
106
+ registerOpenclawClawlingTools(api);
107
+
108
+ const tool = registered.find((t) => t.name === "clawchat_activate");
109
+ expect(tool).toBeDefined();
110
+ loginRuntime.runOpenclawClawlingLogin.mockResolvedValueOnce(undefined);
111
+
112
+ const result = await tool!.execute("call-1", { code: "A1B2C3" });
113
+
114
+ expect(loginRuntime.runOpenclawClawlingLogin).toHaveBeenCalledTimes(1);
115
+ const params = loginRuntime.runOpenclawClawlingLogin.mock.calls[0]?.[0];
116
+ expect(params.cfg).toBe(api.config);
117
+ expect(params.mutateConfigFile).toBe(api.runtime.config.mutateConfigFile);
118
+ await expect(params.readInviteCode()).resolves.toBe("A1B2C3");
119
+ const text = (result as { content: { text: string }[] }).content[0]!.text;
120
+ const parsed = JSON.parse(text) as { ok?: boolean; message?: string };
121
+ expect(parsed.ok).toBe(true);
122
+ expect(parsed.message).toMatch(/activated successfully/i);
123
+ });
124
+
125
+ it("clawchat_activate rejects empty invite codes", async () => {
76
126
  const { api, registered } = buildApi({
77
127
  configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
78
128
  });
79
129
 
80
130
  registerOpenclawClawlingTools(api);
131
+ const tool = registered.find((t) => t.name === "clawchat_activate")!;
132
+ const result = await tool.execute("call-1", { code: " " });
81
133
 
82
- expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
83
134
  expect(loginRuntime.runOpenclawClawlingLogin).not.toHaveBeenCalled();
135
+ const text = (result as { content: { text: string }[] }).content[0]!.text;
136
+ const parsed = JSON.parse(text) as { error?: string; message?: string };
137
+ expect(parsed.error).toBe("validation");
138
+ expect(parsed.message).toMatch(/code is required/i);
84
139
  });
85
140
 
86
141
  it("skips registration when api.config is undefined", () => {
@@ -89,20 +144,13 @@ describe("registerOpenclawClawlingTools", () => {
89
144
  expect(registered).toHaveLength(0);
90
145
  });
91
146
 
92
- it("registers all six account tools when configured (regardless of baseUrl)", () => {
147
+ it("registers all seven ClawChat tools when configured (regardless of baseUrl)", () => {
93
148
  const { api, registered } = buildApi({
94
149
  configChannel: configuredChannel(/* no baseUrl */),
95
150
  });
96
151
  registerOpenclawClawlingTools(api);
97
152
  const names = registered.map((t) => t.name).sort();
98
- expect(names).toEqual([
99
- "clawchat_get_account_profile",
100
- "clawchat_get_user_profile",
101
- "clawchat_list_account_friends",
102
- "clawchat_update_account_profile",
103
- "clawchat_upload_avatar_image",
104
- "clawchat_upload_media_file",
105
- ]);
153
+ expect(names).toEqual(ALWAYS_VISIBLE_TOOL_NAMES);
106
154
  });
107
155
 
108
156
  it("logs configured tool registration at debug level only", () => {
@@ -118,16 +166,31 @@ describe("registerOpenclawClawlingTools", () => {
118
166
 
119
167
  expect(logger.info).not.toHaveBeenCalled();
120
168
  expect(logger.debug).toHaveBeenCalledWith(
121
- "openclaw-clawchat: registered 6 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, update_account_profile, upload_avatar_image, upload_media_file)",
169
+ "openclaw-clawchat: registered 7 clawchat_* tools (activate, get_account_profile, get_user_profile, list_account_friends, update_account_profile, upload_avatar_image, upload_media_file)",
122
170
  );
123
171
  });
124
172
 
125
- it("does not register clawchat_activate when configured", () => {
173
+ it("registers clawchat_activate when configured", () => {
126
174
  const { api, registered } = buildApi({
127
175
  configChannel: configuredChannel(),
128
176
  });
129
177
  registerOpenclawClawlingTools(api);
130
- expect(registered.some((t) => t.name === "clawchat_activate")).toBe(false);
178
+ expect(registered.some((t) => t.name === "clawchat_activate")).toBe(true);
179
+ });
180
+
181
+ it("account tools return a config error before activation instead of disappearing", async () => {
182
+ const { api, registered } = buildApi({
183
+ configChannel: { websocketUrl: "wss://w" /* token / userId missing */ },
184
+ });
185
+
186
+ registerOpenclawClawlingTools(api);
187
+ const tool = registered.find((t) => t.name === "clawchat_get_account_profile")!;
188
+ const result = await tool.execute("call-1", {});
189
+
190
+ const text = (result as { content: { text: string }[] }).content[0]!.text;
191
+ const parsed = JSON.parse(text) as { error?: string; message?: string };
192
+ expect(parsed.error).toBe("config");
193
+ expect(parsed.message).toMatch(/token is required/i);
131
194
  });
132
195
 
133
196
  it("clawchat_update_account_profile description names account profile triggers (EN + ZH)", () => {
package/src/tools.ts CHANGED
@@ -1,19 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { OpenClawAgentToolResult } from "openclaw/plugin-sdk/agent-harness-runtime";
4
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
4
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
5
+ import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
5
6
  import { createOpenclawClawlingApiClient } from "./api-client.ts";
6
7
  import { ClawlingApiError, type Profile } from "./api-types.ts";
7
8
  import { resolveOpenclawClawlingAccount } from "./config.ts";
8
9
  import {
9
- // ClawchatActivateSchema,
10
+ ClawchatActivateSchema,
10
11
  ClawchatGetAccountProfileSchema,
11
12
  ClawchatGetUserProfileSchema,
12
13
  ClawchatListAccountFriendsSchema,
13
14
  ClawchatUpdateAccountProfileSchema,
14
15
  ClawchatUploadAvatarImageSchema,
15
16
  ClawchatUploadMediaFileSchema,
16
- // type ClawchatActivateParams,
17
+ type ClawchatActivateParams,
17
18
  type ClawchatGetUserProfileParams,
18
19
  type ClawchatListAccountFriendsParams,
19
20
  type ClawchatUpdateAccountProfileParams,
@@ -46,12 +47,12 @@ function validationError(message: string) {
46
47
  return jsonResponse({ error: "validation", message });
47
48
  }
48
49
 
49
- // function resolveActivateCode(params: ClawchatActivateParams & { command?: unknown }): string {
50
- // const explicit = typeof params.code === "string" ? params.code.trim() : "";
51
- // if (explicit) return explicit;
52
- // const command = typeof params.command === "string" ? params.command.trim() : "";
53
- // return command.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
54
- // }
50
+ function resolveActivateCode(params: ClawchatActivateParams & { command?: unknown }): string {
51
+ const explicit = typeof params.code === "string" ? params.code.trim() : "";
52
+ if (explicit) return explicit;
53
+ const command = typeof params.command === "string" ? params.command.trim() : "";
54
+ return command.match(/\b[A-Z0-9]{6}\b/u)?.[0] ?? "";
55
+ }
55
56
 
56
57
  function genericError(err: unknown) {
57
58
  return jsonResponse({
@@ -94,64 +95,53 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
94
95
  return;
95
96
  }
96
97
 
97
- // -----------------------------------------------------------------------
98
- // `clawchat_activate` tool disabled: activation now goes through OpenClaw
99
- // channel login.
100
- // -----------------------------------------------------------------------
101
- // api.registerTool(
102
- // {
103
- // name: "clawchat_activate",
104
- // label: "Clawling: Activate (Login with Invite Code)",
105
- // description:
106
- // "Activate this OpenClaw plugin on ClawChat by exchanging an invite code for a token. " +
107
- // "Invite codes use six uppercase letters/digits, e.g. A1B2C3. " +
108
- // "TRIGGER invoke this tool whenever the user's message matches ANY of: " +
109
- // "(1) activation intent with an embedded invite code, such as 'activate ClawChat with invite code A1B2C3', " +
110
- // "'login to ClawChat with invite code A1B2C3', 'connect ClawChat using invite code A1B2C3', " +
111
- // "or '绑定 ClawChat,邀请码 A1B2C3' call this tool with `code = \"A1B2C3\"`; " +
112
- // "(2) generic activation intent without an embedded code, such as 'activate ClawChat' or " +
113
- // "'login to ClawChat' ask for invite code before calling the tool; " +
114
- // "(3) activation intent with an embedded code, such as 'use invite code A1B2C3', " +
115
- // "or the user pasting an invite code in the context of ClawChat activation. " +
116
- // "Do not ask the user to enter a bare ClawChat command; if an invite code is present, call this tool directly. " +
117
- // "Extract the code verbatim do NOT normalize / lowercase / add prefixes. " +
118
- // "On success the tool persists the resulting token + userId to the config, so " +
119
- // "subsequent `clawchat_*` calls work without any other setup.",
120
- // parameters: ClawchatActivateSchema,
121
- // async execute(_callId, params) {
122
- // const code = resolveActivateCode(params as ClawchatActivateParams & { command?: unknown });
123
- // if (!code) {
124
- // return validationError("openclaw-clawchat: code is required");
125
- // }
126
- // try {
127
- // const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
128
- // await runOpenclawClawlingLogin({
129
- // cfg: api.config!,
130
- // accountId: null,
131
- // runtime: { log: (message: string) => api.logger.info?.(message) },
132
- // readInviteCode: async () => code,
133
- // });
134
- // return jsonResponse({
135
- // ok: true,
136
- // message: "✅ ClawChat activated successfully.",
137
- // });
138
- // } catch (err) {
139
- // if (err instanceof ClawlingApiError) return apiError(err);
140
- // return genericError(err);
141
- // }
142
- // },
143
- // },
144
- // { name: "clawchat_activate" },
145
- // );
146
-
147
- const account = resolveOpenclawClawlingAccount(api.config);
148
- if (!account.configured) {
149
- api.logger.debug?.(
150
- "openclaw-clawchat: account not yet configured; account tools will register " +
151
- "on the next config reload after channel login.",
152
- );
153
- return;
154
- }
98
+ api.registerTool(
99
+ {
100
+ name: "clawchat_activate",
101
+ label: "Clawling: Activate (Login with Invite Code)",
102
+ description:
103
+ "Activate this OpenClaw plugin on ClawChat by exchanging an invite code for a token. " +
104
+ "Invite codes use six uppercase letters/digits, e.g. A1B2C3. " +
105
+ "TRIGGER invoke this tool whenever the user's message matches ANY of: " +
106
+ "(1) activation intent with an embedded invite code, such as 'activate ClawChat with invite code A1B2C3', " +
107
+ "'login to ClawChat with invite code A1B2C3', 'connect ClawChat using invite code A1B2C3', " +
108
+ "or '绑定 ClawChat,邀请码 A1B2C3' call this tool with `code = \"A1B2C3\"`; " +
109
+ "(2) generic activation intent without an embedded code, such as 'activate ClawChat' or " +
110
+ "'login to ClawChat' ask for invite code before calling the tool; " +
111
+ "(3) activation intent with an embedded code, such as 'use invite code A1B2C3', " +
112
+ "or the user pasting an invite code in the context of ClawChat activation. " +
113
+ "Extract the code verbatim do NOT normalize / lowercase / add prefixes. " +
114
+ "On success the tool persists the resulting token + userId to the config, so " +
115
+ "subsequent `clawchat_*` calls work without another plugin registration pass.",
116
+ parameters: ClawchatActivateSchema,
117
+ async execute(_callId, params) {
118
+ const code = resolveActivateCode(params as ClawchatActivateParams & { command?: unknown });
119
+ if (!code) {
120
+ return validationError("openclaw-clawchat: code is required");
121
+ }
122
+ try {
123
+ const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
124
+ await runOpenclawClawlingLogin({
125
+ cfg: api.config!,
126
+ accountId: null,
127
+ runtime: { log: (message: string) => api.logger.info?.(message) },
128
+ readInviteCode: async () => code,
129
+ mutateConfigFile: (api.runtime.config as unknown as {
130
+ mutateConfigFile: OpenclawClawchatMutateConfigFile;
131
+ }).mutateConfigFile,
132
+ });
133
+ return jsonResponse({
134
+ ok: true,
135
+ message: "ClawChat activated successfully.",
136
+ });
137
+ } catch (err) {
138
+ if (err instanceof ClawlingApiError) return apiError(err);
139
+ return genericError(err);
140
+ }
141
+ },
142
+ },
143
+ { name: "clawchat_activate" },
144
+ );
155
145
 
156
146
  // Re-resolve at call time so config reloads pick up new tokens / baseUrl.
157
147
  function resolveCurrent() {
@@ -159,8 +149,8 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
159
149
  }
160
150
 
161
151
  type ClientResult =
162
- | { error: OpenClawAgentToolResult<unknown>; client?: never }
163
- | { client: ReturnType<typeof createOpenclawClawlingApiClient>; error?: never };
152
+ | { ok: false; error: OpenClawAgentToolResult<unknown> }
153
+ | { ok: true; client: ReturnType<typeof createOpenclawClawlingApiClient> };
164
154
 
165
155
  function buildClient(): ClientResult {
166
156
  const acct = resolveCurrent();
@@ -168,9 +158,10 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
168
158
  // only need to gate on `token` here (which is populated by `openclaw
169
159
  // channel login`).
170
160
  if (!acct.token) {
171
- return { error: configError("openclaw-clawchat: token is required") };
161
+ return { ok: false, error: configError("openclaw-clawchat: token is required") };
172
162
  }
173
163
  return {
164
+ ok: true,
174
165
  client: createOpenclawClawlingApiClient({
175
166
  baseUrl: acct.baseUrl,
176
167
  token: acct.token,
@@ -184,7 +175,7 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
184
175
  ): Promise<OpenClawAgentToolResult<unknown>> {
185
176
  return (async (): Promise<OpenClawAgentToolResult<unknown>> => {
186
177
  const built = buildClient();
187
- if (built.error !== undefined) return built.error;
178
+ if (!built.ok) return built.error;
188
179
  try {
189
180
  const data = await fn(built.client);
190
181
  return jsonResponse(data);
@@ -368,6 +359,6 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
368
359
  );
369
360
 
370
361
  api.logger.debug?.(
371
- "openclaw-clawchat: registered 6 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, update_account_profile, upload_avatar_image, upload_media_file)",
362
+ "openclaw-clawchat: registered 7 clawchat_* tools (activate, get_account_profile, get_user_profile, list_account_friends, update_account_profile, upload_avatar_image, upload_media_file)",
372
363
  );
373
364
  }