@newbase-clawchat/openclaw-clawchat 2026.5.4 → 2026.5.12-13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +121 -19
  3. package/dist/index.js +10 -19
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +78 -10
  6. package/dist/src/api-types.test-d.js +10 -0
  7. package/dist/src/channel.js +25 -156
  8. package/dist/src/channel.setup.js +120 -0
  9. package/dist/src/client.js +37 -41
  10. package/dist/src/config.js +75 -17
  11. package/dist/src/inbound.js +79 -61
  12. package/dist/src/login.runtime.js +84 -19
  13. package/dist/src/media-runtime.js +8 -8
  14. package/dist/src/message-mapper.js +1 -1
  15. package/dist/src/mock-transport.js +31 -0
  16. package/dist/src/outbound.js +410 -26
  17. package/dist/src/protocol-types.js +63 -0
  18. package/dist/src/protocol-types.typecheck.js +1 -0
  19. package/dist/src/protocol.js +2 -7
  20. package/dist/src/reply-dispatcher.js +157 -54
  21. package/dist/src/runtime.js +795 -119
  22. package/dist/src/storage.js +689 -0
  23. package/dist/src/tools-schema.js +98 -16
  24. package/dist/src/tools.js +422 -135
  25. package/dist/src/ws-alignment.js +178 -0
  26. package/dist/src/ws-client.js +588 -0
  27. package/dist/src/ws-log.js +19 -0
  28. package/index.ts +10 -22
  29. package/openclaw.plugin.json +37 -2
  30. package/package.json +17 -4
  31. package/setup-entry.ts +4 -0
  32. package/skills/clawchat/SKILL.md +88 -0
  33. package/src/api-client.test.ts +274 -14
  34. package/src/api-client.ts +138 -23
  35. package/src/api-types.test-d.ts +12 -0
  36. package/src/api-types.ts +90 -4
  37. package/src/buffered-stream.test.ts +14 -12
  38. package/src/buffered-stream.ts +1 -1
  39. package/src/channel.outbound.test.ts +269 -60
  40. package/src/channel.setup.ts +146 -0
  41. package/src/channel.test.ts +130 -24
  42. package/src/channel.ts +30 -186
  43. package/src/client.test.ts +197 -11
  44. package/src/client.ts +50 -57
  45. package/src/config.test.ts +108 -6
  46. package/src/config.ts +95 -24
  47. package/src/inbound.test.ts +288 -37
  48. package/src/inbound.ts +96 -84
  49. package/src/login.runtime.test.ts +347 -13
  50. package/src/login.runtime.ts +105 -23
  51. package/src/manifest.test.ts +146 -74
  52. package/src/media-runtime.test.ts +57 -2
  53. package/src/media-runtime.ts +26 -17
  54. package/src/message-mapper.test.ts +2 -2
  55. package/src/message-mapper.ts +2 -2
  56. package/src/mock-transport.test.ts +35 -0
  57. package/src/mock-transport.ts +38 -0
  58. package/src/outbound.test.ts +694 -73
  59. package/src/outbound.ts +484 -31
  60. package/src/plugin-entry.test.ts +1 -0
  61. package/src/protocol-types.test.ts +69 -0
  62. package/src/protocol-types.ts +296 -0
  63. package/src/protocol-types.typecheck.ts +89 -0
  64. package/src/protocol.test.ts +1 -6
  65. package/src/protocol.ts +2 -7
  66. package/src/reply-dispatcher.test.ts +819 -119
  67. package/src/reply-dispatcher.ts +202 -60
  68. package/src/runtime.test.ts +2120 -41
  69. package/src/runtime.ts +935 -142
  70. package/src/scripts.test.ts +85 -0
  71. package/src/storage.test.ts +793 -0
  72. package/src/storage.ts +1095 -0
  73. package/src/streaming.test.ts +9 -8
  74. package/src/streaming.ts +1 -1
  75. package/src/tools-schema.ts +148 -20
  76. package/src/tools.test.ts +377 -50
  77. package/src/tools.ts +574 -154
  78. package/src/ws-alignment.test.ts +103 -0
  79. package/src/ws-alignment.ts +275 -0
  80. package/src/ws-client.test.ts +1218 -0
  81. package/src/ws-client.ts +662 -0
  82. package/src/ws-log.test.ts +32 -0
  83. package/src/ws-log.ts +31 -0
  84. package/skills/clawchat-account-tools/SKILL.md +0 -26
  85. package/skills/clawchat-activate/SKILL.md +0 -47
@@ -4,9 +4,11 @@ import { createOpenclawClawlingApiClient } from "./api-client.ts";
4
4
  import { ClawlingApiError, type AgentConnectResult } from "./api-types.ts";
5
5
  import {
6
6
  CHANNEL_ID,
7
+ mergeOpenclawClawchatRuntimePluginActivation,
7
8
  mergeOpenclawClawchatToolAllow,
8
9
  resolveOpenclawClawlingAccount,
9
10
  } from "./config.ts";
11
+ import { getClawChatStore, type ClawChatStore } from "./storage.ts";
10
12
 
11
13
  /**
12
14
  * Platform tag sent to `/v1/agents/connect`. Identifies the host of this
@@ -42,6 +44,10 @@ export interface LoginParams {
42
44
  mutateConfigFile?: OpenclawClawchatMutateConfigFile;
43
45
  /** Test-only config persistence override. */
44
46
  persistConfig?: (cfg: OpenClawConfig) => Promise<void> | void;
47
+ /** Test/runtime override for best-effort activation persistence. */
48
+ store?: Pick<ClawChatStore, "upsertActivation">;
49
+ /** Optional database path resolved by the host runtime. */
50
+ dbPath?: string;
45
51
  }
46
52
 
47
53
  /**
@@ -72,19 +78,28 @@ async function promptInviteCodeFromStdin(runtime: {
72
78
  function buildLoginConfig(cfg: OpenClawConfig, result: AgentConnectResult): OpenClawConfig {
73
79
  const channels = (cfg.channels ?? {}) as Record<string, unknown>;
74
80
  const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
81
+ const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
82
+ ? existing.groupMode
83
+ : "all";
75
84
  const nextSection: Record<string, unknown> = {
76
85
  ...existing,
77
86
  enabled: true,
87
+ groupMode,
78
88
  token: result.access_token,
79
89
  userId: result.agent.user_id,
90
+ ownerUserId: result.agent.owner_id,
80
91
  };
81
92
  if (result.refresh_token) {
82
93
  nextSection.refreshToken = result.refresh_token;
94
+ } else {
95
+ delete nextSection.refreshToken;
83
96
  }
84
- return mergeOpenclawClawchatToolAllow({
85
- ...cfg,
86
- channels: { ...channels, [CHANNEL_ID]: nextSection },
87
- });
97
+ return mergeOpenclawClawchatRuntimePluginActivation(
98
+ mergeOpenclawClawchatToolAllow({
99
+ ...cfg,
100
+ channels: { ...channels, [CHANNEL_ID]: nextSection },
101
+ }),
102
+ );
88
103
  }
89
104
 
90
105
  async function persistLoginConfig(
@@ -92,25 +107,58 @@ async function persistLoginConfig(
92
107
  result: AgentConnectResult,
93
108
  ): Promise<void> {
94
109
  if (params.mutateConfigFile) {
110
+ params.runtime.log(
111
+ `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`,
112
+ );
95
113
  await params.mutateConfigFile({
96
- afterWrite: { mode: "auto" },
114
+ afterWrite: {
115
+ mode: "restart",
116
+ reason: "openclaw-clawchat credentials changed",
117
+ },
97
118
  mutate(draft) {
98
119
  Object.assign(draft, buildLoginConfig(draft, result));
99
120
  },
100
121
  });
122
+ params.runtime.log(
123
+ `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
124
+ );
101
125
  return;
102
126
  }
103
127
 
104
128
  if (params.persistConfig) {
129
+ params.runtime.log(
130
+ `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
131
+ );
105
132
  await params.persistConfig(buildLoginConfig(params.cfg, result));
133
+ params.runtime.log(
134
+ `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
135
+ );
106
136
  return;
107
137
  }
108
138
 
109
139
  throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
110
140
  }
111
141
 
142
+ function requireConnectString(value: unknown, fieldName: string): string {
143
+ if (typeof value !== "string") {
144
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
145
+ }
146
+ const trimmed = value.trim();
147
+ if (!trimmed) {
148
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
149
+ }
150
+ return trimmed;
151
+ }
152
+
153
+ function readOptionalConnectString(value: unknown, fieldName: string): string | undefined {
154
+ if (value == null) {
155
+ return undefined;
156
+ }
157
+ return requireConnectString(value, fieldName);
158
+ }
159
+
112
160
  /**
113
- * Run the invite-code credential exchange used by `clawchat_activate`,
161
+ * Run the invite-code credential exchange used by `/clawchat-login`,
114
162
  * `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
115
163
  * and `openclaw channels login --channel openclaw-clawchat`:
116
164
  * 1. Read the existing channel section; require `baseUrl` to be set so we
@@ -160,29 +208,63 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
160
208
  throw err;
161
209
  }
162
210
 
163
- if (!result?.access_token || !result?.agent?.user_id) {
164
- throw new Error(
165
- `agents/connect response missing required fields (access_token / agent.user_id): ${JSON.stringify(result)}`,
166
- );
211
+ const accessToken = requireConnectString(result?.access_token, "access_token");
212
+ const agentUserId = requireConnectString(result?.agent?.user_id, "agent.user_id");
213
+ const ownerUserId = requireConnectString(result?.agent?.owner_id, "agent.owner_id");
214
+ const agentId = readOptionalConnectString(result?.agent?.id, "agent.id");
215
+
216
+ let conversationId: string | null = null;
217
+ if (result?.conversation != null) {
218
+ conversationId = requireConnectString(result.conversation.id, "conversation.id");
167
219
  }
168
220
 
169
- const tokenPreview = redactToken(result.access_token);
221
+ const normalizedResult: AgentConnectResult = {
222
+ ...result,
223
+ access_token: accessToken,
224
+ refresh_token: typeof result?.refresh_token === "string" ? result.refresh_token.trim() : "",
225
+ agent: {
226
+ ...result.agent,
227
+ ...(agentId ? { id: agentId } : {}),
228
+ owner_id: ownerUserId,
229
+ user_id: agentUserId,
230
+ },
231
+ ...(conversationId
232
+ ? {
233
+ conversation: {
234
+ ...result.conversation,
235
+ id: conversationId,
236
+ },
237
+ }
238
+ : {}),
239
+ };
240
+
170
241
  runtime.log(
171
- `Updating config: channels.${CHANNEL_ID}.token=${tokenPreview} userId=${result.agent.user_id}${
172
- result.refresh_token ? " refreshToken=***" : ""
173
- } …`,
242
+ `Updating config: channels.${CHANNEL_ID}.token=[REDACTED] userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${
243
+ normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""
244
+ } plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`,
174
245
  );
175
- await persistLoginConfig(params, result);
246
+ await persistLoginConfig(params, normalizedResult);
247
+ try {
248
+ const store =
249
+ params.store ??
250
+ getClawChatStore({
251
+ ...(params.dbPath ? { dbPath: params.dbPath } : {}),
252
+ log: { error: runtime.log },
253
+ });
254
+ store.upsertActivation({
255
+ platform: "openclaw",
256
+ accountId: account.accountId,
257
+ userId: normalizedResult.agent.user_id,
258
+ ownerUserId: normalizedResult.agent.owner_id,
259
+ conversationId: normalizedResult.conversation?.id ?? null,
260
+ loginMethod: "login",
261
+ });
262
+ } catch {
263
+ runtime.log("openclaw-clawchat sqlite activation persistence failed; login continues.");
264
+ }
176
265
  runtime.log(`Config file updated.`);
177
266
 
178
267
  runtime.log(
179
- `openclaw-clawchat login succeeded (user_id=${result.agent.user_id}, nickname=${result.agent.nickname || "-"}).`,
268
+ `openclaw-clawchat login succeeded (user_id=${normalizedResult.agent.user_id}, owner_user_id=${normalizedResult.agent.owner_id}, nickname=${normalizedResult.agent.nickname || "-"}).`,
180
269
  );
181
270
  }
182
-
183
- /** Shortens a token for display logs without revealing the full secret. */
184
- function redactToken(token: string): string {
185
- if (!token) return "(empty)";
186
- if (token.length <= 8) return "***";
187
- return `${token.slice(0, 4)}…${token.slice(-4)}`;
188
- }
@@ -2,6 +2,10 @@ import fs from "node:fs";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
4
4
  import packageJson from "../package.json" with { type: "json" };
5
+ import {
6
+ CLAWCHAT_OWNER_USER_ID_ENV,
7
+ openclawClawlingConfigSchema,
8
+ } from "./config.ts";
5
9
 
6
10
  interface PackageJsonWithOpenclaw {
7
11
  name: string;
@@ -13,6 +17,11 @@ interface PackageJsonWithOpenclaw {
13
17
  extensions: string[];
14
18
  runtimeExtensions?: string[];
15
19
  setupEntry?: string;
20
+ runtimeSetupEntry?: string;
21
+ plugin?: {
22
+ id: string;
23
+ label: string;
24
+ };
16
25
  channel?: {
17
26
  id: string;
18
27
  label: string;
@@ -22,6 +31,7 @@ interface PackageJsonWithOpenclaw {
22
31
  blurb: string;
23
32
  order?: number;
24
33
  aliases?: string[];
34
+ cliAddOptions?: Array<{ flags: string; description: string }>;
25
35
  };
26
36
  install: { npmSpec: string; minHostVersion: string };
27
37
  };
@@ -31,7 +41,7 @@ describe("openclaw-clawchat manifest", () => {
31
41
  it("keeps plugin id / channel id / package name aligned", () => {
32
42
  expect(pluginManifest.id).toBe("openclaw-clawchat");
33
43
  expect(pluginManifest.channels).toContain("openclaw-clawchat");
34
- expect(pluginManifest.skills).toContain("./skills");
44
+ expect(pluginManifest.skills).toEqual(["./skills"]);
35
45
  expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.label).toBe(
36
46
  "Clawling Chat",
37
47
  );
@@ -55,22 +65,38 @@ describe("openclaw-clawchat manifest", () => {
55
65
  const pkg = packageJson as PackageJsonWithOpenclaw;
56
66
  expect(pkg.openclaw.extensions).toEqual(["./index.ts"]);
57
67
  expect(pkg.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
68
+ expect(pkg.openclaw.setupEntry).toBe("./setup-entry.ts");
69
+ expect(pkg.openclaw.runtimeSetupEntry).toBe("./dist/setup-entry.js");
58
70
  expect(pkg.files).toContain("dist");
71
+ expect(pkg.files).toContain("setup-entry.ts");
72
+ expect(pkg.files).toContain("skills");
73
+ expect(pkg.files).toContain("INSTALL.md");
59
74
  expect(pkg.scripts.build).toBe("tsc -p tsconfig.build.json");
60
75
  expect(pkg.scripts.prepack).toBe("npm run build");
61
76
  expect(fs.existsSync(new URL("../tsconfig.build.json", import.meta.url))).toBe(true);
77
+ expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(true);
62
78
  });
63
79
 
64
80
  it("publishes channel catalog metadata for OpenClaw CLI discovery", () => {
65
81
  const pkg = packageJson as PackageJsonWithOpenclaw;
82
+ expect(pkg.openclaw.plugin).toEqual({
83
+ id: "openclaw-clawchat",
84
+ label: "Clawling Chat",
85
+ });
66
86
  expect(pkg.openclaw.channel).toEqual({
67
87
  id: "openclaw-clawchat",
68
88
  label: "Clawling Chat",
69
89
  selectionLabel: "Clawling Chat",
70
90
  docsPath: "/channels/openclaw-clawchat",
71
91
  docsLabel: "openclaw-clawchat",
72
- blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
92
+ blurb: "ClawChat Protocol v2 over WebSocket.",
73
93
  order: 110,
94
+ cliAddOptions: [
95
+ {
96
+ flags: "--token <invite-code>",
97
+ description: "ClawChat invite code",
98
+ },
99
+ ],
74
100
  });
75
101
  });
76
102
 
@@ -90,6 +116,7 @@ describe("openclaw-clawchat manifest", () => {
90
116
  "openclaw-clawchat": [
91
117
  "CLAWCHAT_TOKEN",
92
118
  "CLAWCHAT_USER_ID",
119
+ CLAWCHAT_OWNER_USER_ID_ENV,
93
120
  "CLAWCHAT_REFRESH_TOKEN",
94
121
  "CLAWCHAT_BASE_URL",
95
122
  "CLAWCHAT_WEBSOCKET_URL",
@@ -97,42 +124,105 @@ describe("openclaw-clawchat manifest", () => {
97
124
  });
98
125
  });
99
126
 
100
- it("does not publish setup migration or setup-runtime entry metadata", () => {
127
+ it("keeps host manifest channel schemas aligned with runtime config schema", () => {
128
+ expect(pluginManifest.configSchema).toEqual(openclawClawlingConfigSchema);
129
+ expect(pluginManifest.channelConfigs?.["openclaw-clawchat"]?.schema).toEqual(
130
+ openclawClawlingConfigSchema,
131
+ );
132
+ });
133
+
134
+ it("keeps setup entry on a lightweight setup surface", () => {
101
135
  const pkg = packageJson as PackageJsonWithOpenclaw;
102
136
  expect(pkg.files).not.toContain("setup-api.ts");
103
- expect(pkg.files).not.toContain("setup-entry.ts");
104
- expect(pkg.openclaw.setupEntry).toBeUndefined();
105
137
  expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
106
- expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
138
+ const setupEntry = fs.readFileSync(new URL("../setup-entry.ts", import.meta.url), "utf8");
139
+ expect(setupEntry).toMatch(/defineSetupPluginEntry/);
140
+ expect(setupEntry).toMatch(/openclawClawlingSetupPlugin/);
141
+ expect(setupEntry).not.toMatch(/\.\/src\/channel\.ts/);
142
+ expect(setupEntry).not.toMatch(/\.\/src\/runtime(?:\.ts)?/);
143
+ expect(setupEntry).not.toMatch(/\.\/src\/outbound(?:\.ts)?/);
144
+ });
145
+
146
+ it("uses the OpenClaw channel entry helper for registration-mode splitting", () => {
147
+ const entry = fs.readFileSync(new URL("../index.ts", import.meta.url), "utf8");
148
+ expect(entry).toMatch(/defineChannelPluginEntry/);
149
+ expect(entry).toMatch(/registerFull/);
150
+ expect(entry).toMatch(/registerOpenclawClawlingCommands/);
151
+ expect(entry).toMatch(/registerOpenclawClawlingTools/);
152
+ expect(entry).not.toMatch(/register\(api: OpenClawPluginApi\)/);
107
153
  });
108
154
 
109
- it("documents channels add --token as the first-time CLI activation path", () => {
155
+ it("documents runtime activation as the reliable first-time activation path", () => {
110
156
  const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
111
157
  const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
112
- expect(readme).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
113
- expect(docs).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
158
+ expect(readme).not.toMatch(/clawchat_activate/i);
159
+ expect(docs).not.toMatch(/clawchat_activate/i);
160
+ expect(readme).toMatch(/\/clawchat-login A1B2C3/i);
161
+ expect(docs).toMatch(/\/clawchat-login A1B2C3/i);
162
+ expect(readme).toMatch(/OpenClaw 2026\.5\.5/i);
163
+ expect(docs).toMatch(/OpenClaw 2026\.5\.5/i);
164
+ expect(readme).toMatch(/Unknown channel: openclaw-clawchat/i);
165
+ expect(docs).toMatch(/Unknown channel: openclaw-clawchat/i);
114
166
  });
115
167
 
116
- it("publishes a ClawChat account tools skill for non-activation workflows", () => {
117
- const skill = fs.readFileSync(new URL("../skills/clawchat-account-tools/SKILL.md", import.meta.url), "utf8");
118
- expect(skill).toMatch(/^---\nname: clawchat-account-tools\n/m);
119
- expect(skill).toMatch(/description: .*Use when/i);
168
+ it("publishes the repository-provided ClawChat skill", () => {
169
+ const pkg = packageJson as PackageJsonWithOpenclaw;
170
+ expect(pluginManifest.skills).toEqual(["./skills"]);
171
+ expect(pkg.files).toContain("skills");
172
+
173
+ const skillUrl = new URL("../skills/clawchat/SKILL.md", import.meta.url);
174
+ expect(fs.existsSync(skillUrl)).toBe(true);
175
+ const skill = fs.readFileSync(skillUrl, "utf8");
176
+ expect(skill).toMatch(
177
+ /^---\nname: clawchat\ndescription: Use when a request involves ClawChat profile, friends, user search, moments\/dynamics, comments, reactions, avatar, media, or read-only conversation lookup\.\n---/m,
178
+ );
179
+ expect(skill).toMatch(
180
+ /This skill guides agent behavior for ClawChat-aware tasks\. Use the registered ClawChat tools for profile, friends, user search, moments, comments, reactions, avatar, media, and read-only conversation list\/get operations instead of direct HTTP calls, shell scripts, or handwritten clients\./,
181
+ );
120
182
  expect(skill).toMatch(/clawchat_get_account_profile/);
121
- expect(skill).toMatch(/clawchat_get_user_profile/);
122
- expect(skill).toMatch(/clawchat_list_account_friends/);
123
- expect(skill).toMatch(/clawchat_update_account_profile/);
183
+ expect(skill).toMatch(/clawchat_search_users/);
124
184
  expect(skill).toMatch(/clawchat_upload_avatar_image/);
125
185
  expect(skill).toMatch(/clawchat_upload_media_file/);
126
- expect(skill).toMatch(/configured ClawChat account/i);
127
- expect(skill).not.toMatch(/clawchat_activate/);
186
+ expect(skill).toMatch(/clawchat_list_conversations/);
187
+ expect(skill).toMatch(/clawchat_get_conversation/);
188
+ expect(skill).toMatch(/conversations?\/groups?.*read-only/i);
189
+ expect(skill).not.toMatch(
190
+ /conversations?\/groups?.*(?:create|update|leave|dissolve|add members|remove members|administer)/i,
191
+ );
192
+ expect(skill).not.toMatch(/clawchat_create_group_conversation/);
193
+ expect(skill).not.toMatch(/clawchat_update_conversation/);
194
+ expect(skill).not.toMatch(/clawchat_leave_conversation/);
195
+ expect(skill).not.toMatch(/clawchat_dissolve_conversation/);
196
+ expect(skill).not.toMatch(/clawchat_add_conversation_member/);
197
+ expect(skill).not.toMatch(/clawchat_remove_conversation_member/);
198
+ expect(skill).not.toMatch(/clawchat_list_conversation_users/);
199
+ expect(skill).toMatch(/## Profile And Identity Sync/);
200
+ expect(skill).toMatch(/When updating the OpenClaw agent identity file/);
201
+ expect(skill).toMatch(/display name \/ nickname \| `clawchat_update_account_profile` with `nickname`/);
202
+ expect(skill).toMatch(/bio \/ self-introduction \| `clawchat_update_account_profile` with `bio`/);
203
+ expect(skill).toMatch(/local avatar image \| `clawchat_upload_avatar_image`, then `clawchat_update_account_profile` with `avatar_url`/);
204
+ expect(skill).toMatch(/Do not invent invite codes, tokens, moment ids, comment ids, user ids, emoji reactions, image URLs, or file paths/);
205
+ expect(skill).not.toMatch(/hermes/i);
206
+ expect(skill).not.toMatch(/target hermes/i);
207
+ expect(skill).not.toMatch(/choosing among registered clawchat_\*/);
208
+ expect(skill).not.toMatch(/\b(?:whe|regis|plu)\s*$/m);
128
209
  });
129
210
 
130
211
  it("declares ownership of registered ClawChat agent tools", () => {
131
212
  expect(pluginManifest.contracts?.tools).toEqual([
132
- "clawchat_activate",
133
213
  "clawchat_get_account_profile",
134
214
  "clawchat_get_user_profile",
135
215
  "clawchat_list_account_friends",
216
+ "clawchat_search_users",
217
+ "clawchat_list_conversations",
218
+ "clawchat_get_conversation",
219
+ "clawchat_list_moments",
220
+ "clawchat_create_moment",
221
+ "clawchat_delete_moment",
222
+ "clawchat_toggle_moment_reaction",
223
+ "clawchat_create_moment_comment",
224
+ "clawchat_reply_moment_comment",
225
+ "clawchat_delete_moment_comment",
136
226
  "clawchat_update_account_profile",
137
227
  "clawchat_upload_avatar_image",
138
228
  "clawchat_upload_media_file",
@@ -167,84 +257,66 @@ describe("openclaw-clawchat manifest", () => {
167
257
  expect(config).toMatch(/"\.e2e\/\*\*"/);
168
258
  });
169
259
 
170
- it("keeps the activation skill on clawchat_activate with channels-add fallback", () => {
171
- const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
172
- expect(skill).toMatch(/name:\s*clawchat-activate/);
173
- expect(skill).toMatch(/clawchat_activate/);
174
- expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
175
- expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
176
- expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
177
- expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
178
- expect(skill).not.toMatch(/\/clawchat-login A1B2C3/);
179
- expect(skill).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
180
- expect(skill).toMatch(/first-time CLI activation/i);
181
- expect(skill).toMatch(/channel add/i);
182
- expect(skill).toMatch(/channel login/i);
183
- expect(skill).toMatch(/openclaw channels status --probe/);
184
- expect(skill).toMatch(/openclaw gateway restart/);
185
- expect(skill).not.toMatch(/ask the user to send/i);
186
- expect(skill).not.toMatch(/give the exact/i);
187
- expect(skill).toMatch(/restart[^\n]+only when/i);
188
- });
189
-
190
- it("documents clawchat_activate as the natural-language activation path with channels-add CLI fallback", () => {
260
+ it("documents slash command as the chat activation path", () => {
191
261
  const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
192
262
  const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
193
- expect(readme).toMatch(/clawchat_activate/i);
194
- expect(docs).toMatch(/clawchat_activate/i);
195
- expect(readme).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
196
- expect(docs).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
263
+ expect(readme).toMatch(/Current activation paths/i);
264
+ expect(docs).toMatch(/Current activation paths/i);
265
+ expect(readme).not.toMatch(/clawchat_activate/i);
266
+ expect(docs).not.toMatch(/clawchat_activate/i);
267
+ expect(readme).toMatch(/\/clawchat-login A1B2C3/i);
268
+ expect(docs).toMatch(/\/clawchat-login A1B2C3/i);
269
+ expect(readme).toMatch(/openclaw channels add --channel openclaw-clawchat --token "\$CLAWCHAT_INVITE_CODE"/i);
270
+ expect(docs).toMatch(/openclaw channels add --channel openclaw-clawchat --token "\$CLAWCHAT_INVITE_CODE"/i);
271
+ expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
272
+ expect(docs).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
273
+ expect(readme).toMatch(/refresh credentials/i);
274
+ expect(docs).toMatch(/refresh credentials/i);
275
+ expect(readme).toMatch(/OpenClaw 2026\.5\.5/i);
276
+ expect(docs).toMatch(/OpenClaw 2026\.5\.5/i);
277
+ expect(readme).toMatch(/Unknown channel: openclaw-clawchat/i);
278
+ expect(docs).toMatch(/Unknown channel: openclaw-clawchat/i);
197
279
  expect(readme).toMatch(/openclaw channels status --probe/i);
198
280
  expect(docs).toMatch(/openclaw channels status --probe/i);
199
281
  expect(readme).toMatch(/openclaw gateway restart/i);
200
282
  expect(docs).toMatch(/openclaw gateway restart/i);
201
- expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
202
- expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
203
- expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
204
- expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
205
283
  expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
206
284
  expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
207
- expect(readme).not.toMatch(/direct users to/i);
208
- expect(docs).not.toMatch(/direct the\s+user/i);
209
- expect(readme).toMatch(/activation skill calls/i);
210
- expect(docs).toMatch(/Natural-language activation requests should call `clawchat_activate`/i);
285
+ expect(readme).toMatch(/runtime slash command/i);
286
+ expect(docs).toMatch(/runtime slash command/i);
211
287
  });
212
288
 
213
- it("documents gateway restart as the required next step after plugin install or update", () => {
289
+ it("documents the numbered install restart step", () => {
214
290
  const install = fs.readFileSync(new URL("../INSTALL.md", import.meta.url), "utf8");
215
- const installOrUpdate = install.indexOf("## Install or Update the Plugin");
216
- const activate = install.indexOf("## Activate the Channel");
217
- const installSection = install.slice(installOrUpdate, activate);
291
+ const restart = install.indexOf("## 3. Restart");
292
+ const activate = install.indexOf("## 4. Activate");
293
+ const installSection = install.slice(restart, activate);
218
294
 
219
- expect(installSection).toMatch(
220
- /After installing or updating the plugin, restart the OpenClaw Gateway\. This\s+restart is required before OpenClaw can load the installed or updated ClawChat\s+plugin\.\n\n```bash\nopenclaw gateway restart/i,
221
- );
222
295
  expect(installSection).toMatch(/openclaw gateway restart/);
223
- expect(installSection).toMatch(/If restarting the Gateway interrupts the current agent\/session/i);
224
- expect(installSection).toMatch(/continue from \*\*Activate the Channel\*\*/i);
225
- expect(installSection).not.toMatch(/runtime imports the plugin/i);
226
- expect(installSection).not.toMatch(/If the Gateway is already running/i);
296
+ expect(installSection).toMatch(/First restart completed/);
297
+ expect(installSection).not.toMatch(/kill -TERM 1/);
298
+ expect(installSection).not.toMatch(/docker restart <container>/);
227
299
  });
228
300
 
229
- it("documents activation as a direct channels-add command after restarting the Openclaw Gateway", () => {
301
+ it("documents numbered CLI activation, hot reload verification, and restart fallback", () => {
230
302
  const install = fs.readFileSync(new URL("../INSTALL.md", import.meta.url), "utf8");
231
- const activate = install.indexOf("## Activate the Channel");
232
- const verify = install.indexOf("## Verify");
303
+ const activate = install.indexOf("## 4. Activate");
304
+ const verify = install.indexOf("## 5. Verify");
233
305
  const activateSection = install.slice(activate, verify);
234
306
 
235
- expect(activateSection).toMatch(/After the OpenClaw Gateway has restarted and is reachable, activate ClawChat by\s+adding the channel with the invite code/i);
236
307
  expect(activateSection).toMatch(/openclaw channels add --channel openclaw-clawchat --token "\$CLAWCHAT_INVITE_CODE"/);
237
- expect(activateSection).toMatch(/First-time CLI activation uses `channels add`/i);
238
- expect(activateSection).toMatch(/refresh\s+credentials later/i);
239
- expect(activateSection).not.toMatch(/clawchat_activate/i);
240
- expect(activateSection).not.toMatch(/tools are visible/i);
308
+ expect(activateSection).toMatch(/Activation completed/);
241
309
  expect(activateSection).not.toMatch(/openclaw channels status --probe/i);
242
- expect(activateSection).not.toMatch(/verify the channel/i);
310
+ expect(activateSection).not.toMatch(/openclaw gateway restart/i);
311
+ expect(install).not.toMatch(/## 5\. Restart Again/i);
312
+ expect(install).not.toMatch(/Second restart completed/);
243
313
 
244
314
  const verifySection = install.slice(verify);
315
+ expect(verifySection).toMatch(/sleep 5/);
245
316
  expect(verifySection).toMatch(/openclaw channels status --probe/i);
317
+ expect(verifySection).toMatch(/Verification completed/);
246
318
  expect(verifySection).toMatch(/enabled, configured, running, and\s+connected/i);
247
- expect(verifySection).toMatch(/enabled, not configured, stopped, disconnected/i);
248
- expect(verifySection).toMatch(/channel hot reload/i);
319
+ expect(verifySection).toMatch(/restart OpenClaw/i);
320
+ expect(verifySection).toMatch(/installation flow is complete/i);
249
321
  });
250
322
  });
@@ -85,7 +85,9 @@ describe("uploadOutboundMedia", () => {
85
85
  function buildApiClient() {
86
86
  return {
87
87
  uploadMedia: vi.fn().mockResolvedValue({
88
+ kind: "image",
88
89
  url: "https://cdn/uploaded.png",
90
+ name: "uploaded.png",
89
91
  size: 12,
90
92
  mime: "image/png",
91
93
  }),
@@ -118,18 +120,71 @@ describe("uploadOutboundMedia", () => {
118
120
  url: "https://cdn/uploaded.png",
119
121
  mime: "image/png",
120
122
  size: 12,
121
- name: "img.png",
123
+ name: "uploaded.png",
122
124
  },
123
125
  {
124
126
  kind: "image",
125
127
  url: "https://cdn/uploaded.png",
126
128
  mime: "image/png",
127
129
  size: 12,
128
- name: "img.png",
130
+ name: "uploaded.png",
131
+ },
132
+ ]);
133
+ });
134
+
135
+ it("uses server-returned kind and name for uploaded media fragments", async () => {
136
+ const { runtime } = buildRuntime();
137
+ const apiClient = buildApiClient();
138
+ (apiClient.uploadMedia as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
139
+ kind: "file",
140
+ url: "https://cdn/server.bin",
141
+ name: "server.bin",
142
+ size: 12,
143
+ mime: "image/png",
144
+ });
145
+
146
+ const fragments = await uploadOutboundMedia(["https://cdn/in.png"], {
147
+ apiClient,
148
+ runtime,
149
+ });
150
+
151
+ expect(fragments).toEqual([
152
+ {
153
+ kind: "file",
154
+ url: "https://cdn/server.bin",
155
+ mime: "image/png",
156
+ size: 12,
157
+ name: "server.bin",
129
158
  },
130
159
  ]);
131
160
  });
132
161
 
162
+ it("passes host media access options to loadWebMedia", async () => {
163
+ const { runtime, loadWebMedia } = buildRuntime();
164
+ const apiClient = buildApiClient();
165
+ const readFile = vi.fn(async () => Buffer.from("host-read"));
166
+
167
+ await uploadOutboundMedia(["relative/image.png"], {
168
+ apiClient,
169
+ runtime,
170
+ mediaAccess: {
171
+ localRoots: ["/workspace"],
172
+ readFile,
173
+ workspaceDir: "/workspace",
174
+ },
175
+ });
176
+
177
+ expect(loadWebMedia).toHaveBeenCalledWith(
178
+ "relative/image.png",
179
+ expect.objectContaining({
180
+ localRoots: ["/workspace"],
181
+ readFile,
182
+ hostReadCapability: true,
183
+ workspaceDir: "/workspace",
184
+ }),
185
+ );
186
+ });
187
+
133
188
  it("drops a single failed upload, returns the rest", async () => {
134
189
  const { runtime } = buildRuntime();
135
190
  const apiClient = buildApiClient();