@newbase-clawchat/openclaw-clawchat 2026.4.24 → 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 (56) hide show
  1. package/README.md +66 -16
  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/index.ts +2 -1
  22. package/openclaw.plugin.json +81 -1
  23. package/package.json +21 -9
  24. package/skills/clawchat-account-tools/SKILL.md +26 -0
  25. package/skills/clawchat-activate/SKILL.md +47 -0
  26. package/src/api-client.test.ts +6 -5
  27. package/src/api-client.ts +8 -3
  28. package/src/buffered-stream.test.ts +14 -4
  29. package/src/buffered-stream.ts +19 -11
  30. package/src/channel.outbound.test.ts +49 -35
  31. package/src/channel.test.ts +45 -10
  32. package/src/channel.ts +26 -17
  33. package/src/client.test.ts +9 -1
  34. package/src/client.ts +48 -21
  35. package/src/commands.test.ts +39 -0
  36. package/src/commands.ts +41 -0
  37. package/src/config.test.ts +40 -3
  38. package/src/config.ts +60 -4
  39. package/src/inbound.test.ts +9 -6
  40. package/src/inbound.ts +51 -16
  41. package/src/login.runtime.test.ts +142 -3
  42. package/src/login.runtime.ts +59 -26
  43. package/src/manifest.test.ts +183 -5
  44. package/src/outbound.test.ts +10 -7
  45. package/src/outbound.ts +8 -7
  46. package/src/plugin-entry.test.ts +27 -0
  47. package/src/protocol.ts +5 -0
  48. package/src/reply-dispatcher.test.ts +420 -3
  49. package/src/reply-dispatcher.ts +137 -12
  50. package/src/runtime.test.ts +23 -7
  51. package/src/runtime.ts +13 -1
  52. package/src/streaming.test.ts +12 -9
  53. package/src/streaming.ts +22 -12
  54. package/src/tools-schema.ts +28 -19
  55. package/src/tools.test.ts +181 -40
  56. package/src/tools.ts +107 -95
@@ -1,12 +1,29 @@
1
+ import fs from "node:fs";
1
2
  import { describe, expect, it } from "vitest";
2
3
  import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
3
4
  import packageJson from "../package.json" with { type: "json" };
4
5
 
5
6
  interface PackageJsonWithOpenclaw {
6
7
  name: string;
8
+ files: string[];
9
+ scripts: Record<string, string>;
10
+ devDependencies: Record<string, string>;
11
+ peerDependencies: Record<string, string>;
7
12
  openclaw: {
8
- channel: { id: string };
9
- install: { npmSpec: string };
13
+ extensions: string[];
14
+ runtimeExtensions?: string[];
15
+ setupEntry?: string;
16
+ channel?: {
17
+ id: string;
18
+ label: string;
19
+ selectionLabel?: string;
20
+ docsPath?: string;
21
+ docsLabel?: string;
22
+ blurb: string;
23
+ order?: number;
24
+ aliases?: string[];
25
+ };
26
+ install: { npmSpec: string; minHostVersion: string };
10
27
  };
11
28
  }
12
29
 
@@ -14,9 +31,170 @@ describe("openclaw-clawchat manifest", () => {
14
31
  it("keeps plugin id / channel id / package name aligned", () => {
15
32
  expect(pluginManifest.id).toBe("openclaw-clawchat");
16
33
  expect(pluginManifest.channels).toContain("openclaw-clawchat");
17
- expect(packageJson.name).toBe("openclaw-clawchat");
34
+ expect(pluginManifest.skills).toContain("./skills");
35
+ expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.label).toBe(
36
+ "Clawling Chat",
37
+ );
38
+ expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.schema?.properties).toHaveProperty(
39
+ "token",
40
+ );
41
+ expect(packageJson.name).toBe("@newbase-clawchat/openclaw-clawchat");
18
42
  const pkg = packageJson as PackageJsonWithOpenclaw;
19
- expect(pkg.openclaw.channel.id).toBe("openclaw-clawchat");
20
- expect(pkg.openclaw.install.npmSpec).toBe("openclaw-clawchat");
43
+ expect(pkg.openclaw.extensions).toContain("./index.ts");
44
+ expect(pkg.openclaw.install.npmSpec).toBe("@newbase-clawchat/openclaw-clawchat");
45
+ });
46
+
47
+ it("requires an OpenClaw host with runtime config mutation support", () => {
48
+ const pkg = packageJson as PackageJsonWithOpenclaw;
49
+ expect(pkg.peerDependencies.openclaw).toBe("^2026.4.29");
50
+ expect(pkg.devDependencies.openclaw).toBe("2026.4.29");
51
+ expect(pkg.openclaw.install.minHostVersion).toBe(">=2026.4.29");
52
+ });
53
+
54
+ it("publishes compiled runtime entrypoints for npm plugin installs", () => {
55
+ const pkg = packageJson as PackageJsonWithOpenclaw;
56
+ expect(pkg.openclaw.extensions).toEqual(["./index.ts"]);
57
+ expect(pkg.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
58
+ expect(pkg.files).toContain("dist");
59
+ expect(pkg.scripts.build).toBe("tsc -p tsconfig.build.json");
60
+ expect(pkg.scripts.prepack).toBe("npm run build");
61
+ expect(fs.existsSync(new URL("../tsconfig.build.json", import.meta.url))).toBe(true);
62
+ });
63
+
64
+ it("publishes channel catalog metadata for OpenClaw CLI discovery", () => {
65
+ const pkg = packageJson as PackageJsonWithOpenclaw;
66
+ expect(pkg.openclaw.channel).toEqual({
67
+ id: "openclaw-clawchat",
68
+ label: "Clawling Chat",
69
+ selectionLabel: "Clawling Chat",
70
+ docsPath: "/channels/openclaw-clawchat",
71
+ docsLabel: "openclaw-clawchat",
72
+ blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
73
+ order: 110,
74
+ });
75
+ });
76
+
77
+ it("declares supported channel/command activation hints for plugin loading", () => {
78
+ expect(pluginManifest.activation).toEqual({
79
+ onStartup: true,
80
+ onChannels: ["openclaw-clawchat"],
81
+ onCommands: ["clawchat-login"],
82
+ });
83
+ expect(pluginManifest.commandAliases).toEqual([
84
+ { name: "clawchat-login", kind: "runtime-slash" },
85
+ ]);
86
+ });
87
+
88
+ it("does not publish setup migration or setup-runtime entry metadata", () => {
89
+ const pkg = packageJson as PackageJsonWithOpenclaw;
90
+ expect(pkg.files).not.toContain("setup-api.ts");
91
+ expect(pkg.files).not.toContain("setup-entry.ts");
92
+ expect(pkg.openclaw.setupEntry).toBeUndefined();
93
+ expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
94
+ expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
95
+ });
96
+
97
+ it("does not document channels add as an activation path", () => {
98
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
99
+ const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
100
+ expect(readme).not.toMatch(/channels add --channel openclaw-clawchat/i);
101
+ expect(docs).not.toMatch(/channels add --channel openclaw-clawchat/i);
102
+ });
103
+
104
+ it("publishes a ClawChat account tools skill for non-activation workflows", () => {
105
+ const skill = fs.readFileSync(new URL("../skills/clawchat-account-tools/SKILL.md", import.meta.url), "utf8");
106
+ expect(skill).toMatch(/^---\nname: clawchat-account-tools\n/m);
107
+ expect(skill).toMatch(/description: .*Use when/i);
108
+ expect(skill).toMatch(/clawchat_get_account_profile/);
109
+ expect(skill).toMatch(/clawchat_get_user_profile/);
110
+ expect(skill).toMatch(/clawchat_list_account_friends/);
111
+ expect(skill).toMatch(/clawchat_update_account_profile/);
112
+ expect(skill).toMatch(/clawchat_upload_avatar_image/);
113
+ expect(skill).toMatch(/clawchat_upload_media_file/);
114
+ expect(skill).toMatch(/configured ClawChat account/i);
115
+ expect(skill).not.toMatch(/clawchat_activate/);
116
+ });
117
+
118
+ it("declares ownership of registered ClawChat agent tools", () => {
119
+ expect(pluginManifest.contracts?.tools).toEqual([
120
+ "clawchat_activate",
121
+ "clawchat_get_account_profile",
122
+ "clawchat_get_user_profile",
123
+ "clawchat_list_account_friends",
124
+ "clawchat_update_account_profile",
125
+ "clawchat_upload_avatar_image",
126
+ "clawchat_upload_media_file",
127
+ ]);
128
+ });
129
+
130
+ it("keeps the optional OpenClaw source checkout local-only", () => {
131
+ expect(fs.existsSync(new URL("../.gitmodules", import.meta.url))).toBe(false);
132
+
133
+ const gitignore = fs.readFileSync(new URL("../.gitignore", import.meta.url), "utf8");
134
+ expect(gitignore).toMatch(/^tmp\/openclaw\/$/m);
135
+
136
+ const pkg = packageJson as PackageJsonWithOpenclaw;
137
+ expect(pkg.scripts["dev:openclaw-source"]).toBe(
138
+ "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
139
+ );
140
+
141
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
142
+ expect(readme).toMatch(/npm run dev:openclaw-source/);
143
+ expect(readme).toMatch(
144
+ /git clone --depth=1 https:\/\/github\.com\/openclaw\/openclaw\.git tmp\/openclaw/,
145
+ );
146
+ expect(readme).toMatch(/local-only/i);
147
+ });
148
+
149
+ it("keeps default Vitest discovery scoped to plugin sources", () => {
150
+ const configUrl = new URL("../vitest.config.ts", import.meta.url);
151
+ expect(fs.existsSync(configUrl)).toBe(true);
152
+ const config = fs.readFileSync(configUrl, "utf8");
153
+ expect(config).toMatch(/include:\s*\["src\/\*\*\/\*\.test\.ts"\]/);
154
+ expect(config).toMatch(/"tmp\/\*\*"/);
155
+ expect(config).toMatch(/"\.e2e\/\*\*"/);
156
+ });
157
+
158
+ it("keeps the activation skill on clawchat_activate with channel-login fallback", () => {
159
+ const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
160
+ expect(skill).toMatch(/name:\s*clawchat-activate/);
161
+ expect(skill).toMatch(/clawchat_activate/);
162
+ expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
163
+ expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
164
+ expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
165
+ expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
166
+ expect(skill).not.toMatch(/\/clawchat-login A1B2C3/);
167
+ expect(skill).toMatch(/openclaw channels login --channel openclaw-clawchat/);
168
+ expect(skill).toMatch(/do not append/i);
169
+ expect(skill).toMatch(/prompt[^\n]+invite code[^\n]+provide/i);
170
+ expect(skill).toMatch(/channel login/i);
171
+ expect(skill).toMatch(/openclaw channels status --probe/);
172
+ expect(skill).toMatch(/openclaw gateway restart/);
173
+ expect(skill).not.toMatch(/ask the user to send/i);
174
+ expect(skill).not.toMatch(/give the exact/i);
175
+ expect(skill).toMatch(/restart[^\n]+only when/i);
176
+ });
177
+
178
+ it("documents clawchat_activate as the natural-language activation path with CLI fallback", () => {
179
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
180
+ const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
181
+ expect(readme).toMatch(/clawchat_activate/i);
182
+ expect(docs).toMatch(/clawchat_activate/i);
183
+ expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
184
+ expect(docs).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
185
+ expect(readme).toMatch(/openclaw channels status --probe/i);
186
+ expect(docs).toMatch(/openclaw channels status --probe/i);
187
+ expect(readme).toMatch(/openclaw gateway restart/i);
188
+ expect(docs).toMatch(/openclaw gateway restart/i);
189
+ expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
190
+ expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
191
+ expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
192
+ expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
193
+ expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
194
+ expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
195
+ expect(readme).not.toMatch(/direct users to/i);
196
+ expect(docs).not.toMatch(/direct the\s+user/i);
197
+ expect(readme).toMatch(/activation skill calls/i);
198
+ expect(docs).toMatch(/Natural-language activation requests should call `clawchat_activate`/i);
21
199
  });
22
200
  });
@@ -64,9 +64,10 @@ describe("openclaw-clawchat outbound", () => {
64
64
  text: "hello",
65
65
  });
66
66
  expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
67
- to: { id: "user-1", type: "direct" },
67
+ chat_id: "user-1",
68
68
  body: { fragments: [{ kind: "text", text: "hello" }] },
69
69
  });
70
+ expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
70
71
  expect(client.replyMessage).not.toHaveBeenCalled();
71
72
  expect(result?.messageId).toBe("server-m1");
72
73
  expect(result?.acceptedAt).toBe(1234);
@@ -77,25 +78,27 @@ describe("openclaw-clawchat outbound", () => {
77
78
  await sendOpenclawClawlingText({
78
79
  client,
79
80
  account: baseAccount(),
80
- to: { chatId: "user-1", chatType: "direct" },
81
+ to: { chatId: "chat-1", chatType: "direct" },
81
82
  text: "reply",
82
83
  replyCtx: {
83
84
  replyToMessageId: "m-orig",
85
+ replyPreviewChatId: "chat-1",
84
86
  replyPreviewSenderId: "user-2",
85
- replyPreviewDisplayName: "Sender",
87
+ replyPreviewNickName: "Sender",
86
88
  replyPreviewText: "original",
87
89
  },
88
90
  });
89
91
  expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
90
- to: { id: "user-1", type: "direct" },
92
+ chat_id: "chat-1",
91
93
  replyTo: {
92
94
  msgId: "m-orig",
93
- senderId: "user-2",
94
- displayName: "Sender",
95
+ senderId: "chat-1",
96
+ nickName: "Sender",
95
97
  fragments: [{ kind: "text", text: "original" }],
96
98
  },
97
99
  body: { fragments: [{ kind: "text", text: "reply" }] },
98
100
  });
101
+ expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
99
102
  expect(client.sendMessage).not.toHaveBeenCalled();
100
103
  });
101
104
 
@@ -168,7 +171,7 @@ describe("openclaw-clawchat outbound", () => {
168
171
  replyCtx: {
169
172
  replyToMessageId: "m-orig",
170
173
  replyPreviewSenderId: "user-2",
171
- replyPreviewDisplayName: "Sender",
174
+ replyPreviewNickName: "Sender",
172
175
  replyPreviewText: "original",
173
176
  },
174
177
  mediaFragments: [{ kind: "image", url: "https://cdn/x.png" }],
package/src/outbound.ts CHANGED
@@ -22,6 +22,7 @@ export interface OutboundTarget {
22
22
 
23
23
  export interface OutboundReplyCtx {
24
24
  replyToMessageId: string;
25
+ replyPreviewChatId?: string;
25
26
  replyPreviewSenderId: string;
26
27
  replyPreviewNickName: string;
27
28
  replyPreviewText: string;
@@ -38,6 +39,7 @@ export interface SendParams {
38
39
  to: OutboundTarget;
39
40
  text: string;
40
41
  replyCtx?: OutboundReplyCtx;
42
+ richFragments?: Fragment[];
41
43
  mediaFragments?: ClawlingMediaFragment[];
42
44
  mentions?: string[];
43
45
  log?: LogSink;
@@ -92,8 +94,9 @@ export function parseOpenclawRecipient(to: string): { chatId: string; chatType:
92
94
 
93
95
  export async function sendOpenclawClawlingText(params: SendParams): Promise<SendResult | null> {
94
96
  const text = (params.text ?? "").trim();
97
+ const richFragments = params.richFragments ?? [];
95
98
  const mediaFragments = params.mediaFragments ?? [];
96
- if (!text && mediaFragments.length === 0) {
99
+ if (!text && richFragments.length === 0 && mediaFragments.length === 0) {
97
100
  params.log?.info?.(
98
101
  `[${params.account.accountId}] openclaw-clawchat outbound suppressed: empty text and no media`,
99
102
  );
@@ -106,7 +109,7 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
106
109
  // with one of the SDK's narrow Fragment members (ImageFragment / FileFragment /
107
110
  // AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
108
111
  // shape lets us build a single uniform array without a per-kind switch.
109
- const fragments = [...textFragments, ...mediaFragments] as Fragment[];
112
+ const fragments = [...textFragments, ...richFragments, ...mediaFragments] as Fragment[];
110
113
 
111
114
  const useReply = params.replyCtx && mediaFragments.length === 0;
112
115
  if (params.replyCtx && mediaFragments.length > 0) {
@@ -121,26 +124,24 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
121
124
  mode = "reply";
122
125
  ack = await params.client.replyMessage({
123
126
  chat_id: params.to.chatId,
124
- chat_type: params.to.chatType,
125
127
  mode: "normal",
126
128
  replyTo: {
127
129
  msgId: params.replyCtx.replyToMessageId,
128
- senderId: params.replyCtx.replyPreviewSenderId,
130
+ senderId: params.replyCtx.replyPreviewChatId ?? params.replyCtx.replyPreviewSenderId,
129
131
  nickName: params.replyCtx.replyPreviewNickName,
130
132
  fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
131
133
  },
132
134
  body: { fragments },
133
135
  context: { mentions },
134
- });
136
+ } as Parameters<ClawlingChatClient["replyMessage"]>[0]);
135
137
  } else {
136
138
  mode = "send";
137
139
  ack = await params.client.sendMessage({
138
140
  chat_id: params.to.chatId,
139
- chat_type: params.to.chatType,
140
141
  mode: "normal",
141
142
  body: { fragments },
142
143
  context: { mentions, reply: null },
143
- });
144
+ } as Parameters<ClawlingChatClient["sendMessage"]>[0]);
144
145
  }
145
146
  params.log?.info?.(
146
147
  `[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`,
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import pluginEntry from "../index.ts";
3
+
4
+ describe("openclaw-clawchat plugin entry", () => {
5
+ it("registers the channel/tools and native activation command without bootstrap migration", () => {
6
+ const api = {
7
+ config: {},
8
+ runtime: {},
9
+ logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
10
+ registerChannel: vi.fn(),
11
+ registerCommand: vi.fn(),
12
+ registerConfigMigration: vi.fn(),
13
+ registerTool: vi.fn(),
14
+ } as never;
15
+
16
+ pluginEntry.register(api);
17
+
18
+ expect(api.registerChannel).toHaveBeenCalledTimes(1);
19
+ expect(api.registerConfigMigration).not.toHaveBeenCalled();
20
+ expect(api.registerCommand).toHaveBeenCalledTimes(1);
21
+ expect(api.registerCommand).toHaveBeenCalledWith(
22
+ expect.objectContaining({
23
+ name: "clawchat-login",
24
+ }),
25
+ );
26
+ });
27
+ });
package/src/protocol.ts CHANGED
@@ -40,3 +40,8 @@ export function hasRenderableText(message: {
40
40
  (f as { url: string }).url.trim().length > 0)),
41
41
  );
42
42
  }
43
+
44
+ export function isGroupSender(sender: unknown): boolean {
45
+ if (!sender || typeof sender !== "object") return false;
46
+ return (sender as { type?: unknown }).type === "group";
47
+ }