@clawling/clawchat-plugin-openclaw 2026.5.12-28

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 (114) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +227 -0
  3. package/dist/index.js +20 -0
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +263 -0
  6. package/dist/src/api-types.js +17 -0
  7. package/dist/src/api-types.test-d.js +10 -0
  8. package/dist/src/buffered-stream.js +177 -0
  9. package/dist/src/channel.js +66 -0
  10. package/dist/src/channel.setup.js +119 -0
  11. package/dist/src/clawchat-memory.js +403 -0
  12. package/dist/src/clawchat-metadata.js +310 -0
  13. package/dist/src/client.js +35 -0
  14. package/dist/src/commands.js +35 -0
  15. package/dist/src/config.js +274 -0
  16. package/dist/src/group-message-coalescer.js +119 -0
  17. package/dist/src/inbound.js +170 -0
  18. package/dist/src/llm-context-debug.js +86 -0
  19. package/dist/src/login.runtime.js +204 -0
  20. package/dist/src/media-runtime.js +85 -0
  21. package/dist/src/message-mapper.js +146 -0
  22. package/dist/src/mock-transport.js +31 -0
  23. package/dist/src/outbound.js +628 -0
  24. package/dist/src/plugin-prompts.js +89 -0
  25. package/dist/src/profile-prompt.js +269 -0
  26. package/dist/src/profile-sync.js +110 -0
  27. package/dist/src/prompt-injection.js +25 -0
  28. package/dist/src/protocol-types.js +63 -0
  29. package/dist/src/protocol-types.typecheck.js +1 -0
  30. package/dist/src/protocol.js +33 -0
  31. package/dist/src/reply-dispatcher.js +422 -0
  32. package/dist/src/runtime.js +1254 -0
  33. package/dist/src/storage.js +525 -0
  34. package/dist/src/streaming.js +65 -0
  35. package/dist/src/terminal-send.js +36 -0
  36. package/dist/src/tools-schema.js +208 -0
  37. package/dist/src/tools.js +920 -0
  38. package/dist/src/ws-alignment.js +178 -0
  39. package/dist/src/ws-client.js +588 -0
  40. package/dist/src/ws-log.js +19 -0
  41. package/index.ts +24 -0
  42. package/openclaw.plugin.json +169 -0
  43. package/package.json +80 -0
  44. package/prompts/default-group-bio.md +19 -0
  45. package/prompts/default-owner-behavior.md +27 -0
  46. package/prompts/platform.md +13 -0
  47. package/setup-entry.ts +4 -0
  48. package/skills/clawchat/SKILL.md +91 -0
  49. package/src/api-client.test.ts +827 -0
  50. package/src/api-client.ts +414 -0
  51. package/src/api-types.ts +146 -0
  52. package/src/channel.outbound.test.ts +433 -0
  53. package/src/channel.setup.ts +145 -0
  54. package/src/channel.test.ts +262 -0
  55. package/src/channel.ts +81 -0
  56. package/src/clawchat-memory.test.ts +480 -0
  57. package/src/clawchat-memory.ts +533 -0
  58. package/src/clawchat-metadata.test.ts +477 -0
  59. package/src/clawchat-metadata.ts +429 -0
  60. package/src/client.test.ts +169 -0
  61. package/src/client.ts +56 -0
  62. package/src/commands.test.ts +39 -0
  63. package/src/commands.ts +41 -0
  64. package/src/config.test.ts +344 -0
  65. package/src/config.ts +404 -0
  66. package/src/group-message-coalescer.test.ts +237 -0
  67. package/src/group-message-coalescer.ts +171 -0
  68. package/src/inbound.test.ts +508 -0
  69. package/src/inbound.ts +278 -0
  70. package/src/llm-context-debug.test.ts +55 -0
  71. package/src/llm-context-debug.ts +139 -0
  72. package/src/login.runtime.test.ts +737 -0
  73. package/src/login.runtime.ts +277 -0
  74. package/src/manifest.test.ts +352 -0
  75. package/src/media-runtime.test.ts +207 -0
  76. package/src/media-runtime.ts +152 -0
  77. package/src/message-mapper.test.ts +201 -0
  78. package/src/message-mapper.ts +174 -0
  79. package/src/mock-transport.test.ts +35 -0
  80. package/src/mock-transport.ts +38 -0
  81. package/src/outbound.test.ts +1269 -0
  82. package/src/outbound.ts +803 -0
  83. package/src/plugin-entry.test.ts +38 -0
  84. package/src/plugin-prompts.test.ts +94 -0
  85. package/src/plugin-prompts.ts +107 -0
  86. package/src/profile-prompt.test.ts +274 -0
  87. package/src/profile-prompt.ts +351 -0
  88. package/src/profile-sync.test.ts +539 -0
  89. package/src/profile-sync.ts +191 -0
  90. package/src/prompt-injection.test.ts +39 -0
  91. package/src/prompt-injection.ts +45 -0
  92. package/src/protocol-types.test.ts +69 -0
  93. package/src/protocol-types.ts +296 -0
  94. package/src/protocol-types.typecheck.ts +89 -0
  95. package/src/protocol.test.ts +39 -0
  96. package/src/protocol.ts +42 -0
  97. package/src/reply-dispatcher.test.ts +1324 -0
  98. package/src/reply-dispatcher.ts +555 -0
  99. package/src/runtime.test.ts +4719 -0
  100. package/src/runtime.ts +1493 -0
  101. package/src/scripts.test.ts +85 -0
  102. package/src/storage.test.ts +560 -0
  103. package/src/storage.ts +807 -0
  104. package/src/terminal-send.test.ts +81 -0
  105. package/src/terminal-send.ts +56 -0
  106. package/src/tools-schema.ts +337 -0
  107. package/src/tools.test.ts +933 -0
  108. package/src/tools.ts +1185 -0
  109. package/src/ws-alignment.test.ts +103 -0
  110. package/src/ws-alignment.ts +275 -0
  111. package/src/ws-client.test.ts +1217 -0
  112. package/src/ws-client.ts +662 -0
  113. package/src/ws-log.test.ts +32 -0
  114. package/src/ws-log.ts +31 -0
@@ -0,0 +1,277 @@
1
+ import { createInterface, type Interface as ReadlineInterface } from "node:readline/promises";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
3
+ import { createOpenclawClawlingApiClient } from "./api-client.ts";
4
+ import { ClawlingApiError, type AgentConnectResult } from "./api-types.ts";
5
+ import {
6
+ CHANNEL_ID,
7
+ mergeOpenclawClawchatRuntimePluginActivation,
8
+ mergeOpenclawClawchatToolAllow,
9
+ resolveOpenclawClawlingAccount,
10
+ } from "./config.ts";
11
+ import { getClawChatStore, type ClawChatStore } from "./storage.ts";
12
+
13
+ /**
14
+ * Platform tag sent to `/v1/agents/connect`. Identifies the host of this
15
+ * agent runtime — openclaw's bundled clawchat channel.
16
+ */
17
+ export const AGENTS_CONNECT_PLATFORM = "openclaw" as const;
18
+ /**
19
+ * Agent type tag sent to `/v1/agents/connect`. The clawchat channel is
20
+ * always a bot; humans don't log in through this flow.
21
+ */
22
+ export const AGENTS_CONNECT_TYPE = "clawbot" as const;
23
+
24
+ export type OpenclawClawchatMutateConfigFile = <T = void>(params: {
25
+ afterWrite: { mode: "auto" } | { mode: "none" | "restart"; reason: string };
26
+ mutate: (
27
+ draft: OpenClawConfig,
28
+ context: { snapshot: unknown; previousHash: string | null },
29
+ ) => Promise<T | void> | T | void;
30
+ }) => Promise<unknown>;
31
+
32
+ export interface LoginParams {
33
+ cfg: OpenClawConfig;
34
+ accountId?: string | null;
35
+ runtime: { log: (message: string) => void };
36
+ /**
37
+ * Override for the invite-code prompt — used by tests. Defaults to
38
+ * stdin via `node:readline/promises`.
39
+ */
40
+ readInviteCode?: () => Promise<string>;
41
+ /** Override for the HTTP client — used by tests. */
42
+ apiClientFactory?: typeof createOpenclawClawlingApiClient;
43
+ /** Official runtime config mutator. Production callers must provide this. */
44
+ mutateConfigFile?: OpenclawClawchatMutateConfigFile;
45
+ /** Test-only config persistence override. */
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;
51
+ }
52
+
53
+ /**
54
+ * Prompt the operator for an invite code.
55
+ *
56
+ * The prompt text is emitted via `runtime.log` so it flows through the
57
+ * same openclaw logging pipeline every other channel plugin uses (no
58
+ * clack frame, no raw-mode takeover, no TTY detection). Input is read
59
+ * from stdin with `node:readline` — Enter-to-submit is plain language in
60
+ * the prompt so any upstream LLM / orchestrator reading the log stream
61
+ * knows a newline is expected, and the behavior is identical under a
62
+ * TTY, piped stdin, or a test harness.
63
+ */
64
+ async function promptInviteCodeFromStdin(runtime: {
65
+ log: (message: string) => void;
66
+ }): Promise<string> {
67
+ runtime.log("Please enter your ClawChat invite code (press Enter to submit):");
68
+ let rl: ReadlineInterface | undefined;
69
+ try {
70
+ rl = createInterface({ input: process.stdin, output: process.stdout });
71
+ const answer = await rl.question("> ");
72
+ return answer.trim();
73
+ } finally {
74
+ rl?.close();
75
+ }
76
+ }
77
+
78
+ function buildLoginConfig(cfg: OpenClawConfig, result: AgentConnectResult): OpenClawConfig {
79
+ const channels = (cfg.channels ?? {}) as Record<string, unknown>;
80
+ const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
81
+ const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
82
+ ? existing.groupMode
83
+ : "all";
84
+ const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
85
+ ? existing.groupCommandMode
86
+ : "owner";
87
+ const nextSection: Record<string, unknown> = {
88
+ ...existing,
89
+ enabled: true,
90
+ groupMode,
91
+ groupCommandMode,
92
+ token: result.access_token,
93
+ ...(result.agent.id ? { agentId: result.agent.id } : {}),
94
+ userId: result.agent.user_id,
95
+ ownerUserId: result.agent.owner_id,
96
+ };
97
+ if (result.refresh_token) {
98
+ nextSection.refreshToken = result.refresh_token;
99
+ } else {
100
+ delete nextSection.refreshToken;
101
+ }
102
+ return mergeOpenclawClawchatRuntimePluginActivation(
103
+ mergeOpenclawClawchatToolAllow({
104
+ ...cfg,
105
+ channels: { ...channels, [CHANNEL_ID]: nextSection },
106
+ }),
107
+ );
108
+ }
109
+
110
+ async function persistLoginConfig(
111
+ params: LoginParams,
112
+ result: AgentConnectResult,
113
+ ): Promise<void> {
114
+ if (params.mutateConfigFile) {
115
+ params.runtime.log(
116
+ `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`,
117
+ );
118
+ await params.mutateConfigFile({
119
+ afterWrite: {
120
+ mode: "restart",
121
+ reason: "clawchat-plugin-openclaw credentials changed",
122
+ },
123
+ mutate(draft) {
124
+ Object.assign(draft, buildLoginConfig(draft, result));
125
+ },
126
+ });
127
+ params.runtime.log(
128
+ `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
129
+ );
130
+ return;
131
+ }
132
+
133
+ if (params.persistConfig) {
134
+ params.runtime.log(
135
+ `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
136
+ );
137
+ await params.persistConfig(buildLoginConfig(params.cfg, result));
138
+ params.runtime.log(
139
+ `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
140
+ );
141
+ return;
142
+ }
143
+
144
+ throw new Error("clawchat-plugin-openclaw: mutateConfigFile is required to persist login credentials");
145
+ }
146
+
147
+ function requireConnectString(value: unknown, fieldName: string): string {
148
+ if (typeof value !== "string") {
149
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
150
+ }
151
+ const trimmed = value.trim();
152
+ if (!trimmed) {
153
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
154
+ }
155
+ return trimmed;
156
+ }
157
+
158
+ function readOptionalConnectString(value: unknown, fieldName: string): string | undefined {
159
+ if (value == null) {
160
+ return undefined;
161
+ }
162
+ return requireConnectString(value, fieldName);
163
+ }
164
+
165
+ /**
166
+ * Run the invite-code credential exchange used by `/clawchat-activate`,
167
+ * `openclaw channels add --channel clawchat-plugin-openclaw --token <invite-code>`,
168
+ * and `openclaw channels login --channel clawchat-plugin-openclaw`:
169
+ * 1. Read the existing channel section; require `baseUrl` to be set so we
170
+ * know which server to hit.
171
+ * 2. Prompt the user for an invite code on stdin.
172
+ * 3. POST it to `${baseUrl}/v1/agents/connect`.
173
+ * 4. Write the returned `websocket_url` / `token` / `user_id` back into
174
+ * the config so subsequent Gateway runs pick them up.
175
+ *
176
+ * Errors surface with clear messages (missing baseUrl, empty invite,
177
+ * server-side rejection) so the caller can relay them to the operator.
178
+ */
179
+ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<void> {
180
+ const { cfg, runtime } = params;
181
+
182
+ // `resolveOpenclawClawlingAccount` falls back to the built-in
183
+ // `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
184
+ // overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
185
+ const account = resolveOpenclawClawlingAccount(cfg);
186
+
187
+ const inviteCode = (
188
+ await (params.readInviteCode ?? (() => promptInviteCodeFromStdin(runtime)))()
189
+ ).trim();
190
+ if (!inviteCode) {
191
+ throw new Error("Login aborted: invite code is required.");
192
+ }
193
+
194
+ const apiClient = (params.apiClientFactory ?? createOpenclawClawlingApiClient)({
195
+ baseUrl: account.baseUrl,
196
+ // Pre-login we may not have a token yet. Send the current one (or empty)
197
+ // — the server should accept an unauthenticated invite-code exchange.
198
+ token: account.token || "",
199
+ });
200
+
201
+ runtime.log("Verifying invite code …");
202
+ let result;
203
+ try {
204
+ result = await apiClient.agentsConnect({
205
+ code: inviteCode,
206
+ platform: AGENTS_CONNECT_PLATFORM,
207
+ type: AGENTS_CONNECT_TYPE,
208
+ });
209
+ } catch (err) {
210
+ if (err instanceof ClawlingApiError) {
211
+ throw new Error(`agents/connect failed (${err.kind}): ${err.message}`);
212
+ }
213
+ throw err;
214
+ }
215
+
216
+ const accessToken = requireConnectString(result?.access_token, "access_token");
217
+ const agentUserId = requireConnectString(result?.agent?.user_id, "agent.user_id");
218
+ const ownerUserId = requireConnectString(result?.agent?.owner_id, "agent.owner_id");
219
+ const agentId = readOptionalConnectString(result?.agent?.id, "agent.id");
220
+
221
+ let conversationId: string | null = null;
222
+ if (result?.conversation != null) {
223
+ conversationId = requireConnectString(result.conversation.id, "conversation.id");
224
+ }
225
+
226
+ const normalizedResult: AgentConnectResult = {
227
+ ...result,
228
+ access_token: accessToken,
229
+ refresh_token: typeof result?.refresh_token === "string" ? result.refresh_token.trim() : "",
230
+ agent: {
231
+ ...result.agent,
232
+ ...(agentId ? { id: agentId } : {}),
233
+ owner_id: ownerUserId,
234
+ user_id: agentUserId,
235
+ },
236
+ ...(conversationId
237
+ ? {
238
+ conversation: {
239
+ ...result.conversation,
240
+ id: conversationId,
241
+ },
242
+ }
243
+ : {}),
244
+ };
245
+
246
+ runtime.log(
247
+ `Updating config: channels.${CHANNEL_ID}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${
248
+ normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""
249
+ } plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`,
250
+ );
251
+ await persistLoginConfig(params, normalizedResult);
252
+ try {
253
+ const store =
254
+ params.store ??
255
+ getClawChatStore({
256
+ ...(params.dbPath ? { dbPath: params.dbPath } : {}),
257
+ log: { error: runtime.log },
258
+ });
259
+ store.upsertActivation({
260
+ platform: "openclaw",
261
+ accountId: account.accountId,
262
+ userId: normalizedResult.agent.user_id,
263
+ ownerUserId: normalizedResult.agent.owner_id,
264
+ accessToken: normalizedResult.access_token,
265
+ refreshToken: normalizedResult.refresh_token || null,
266
+ conversationId: normalizedResult.conversation?.id ?? null,
267
+ loginMethod: "login",
268
+ });
269
+ } catch {
270
+ runtime.log("clawchat-plugin-openclaw sqlite activation persistence failed; login continues.");
271
+ }
272
+ runtime.log(`Config file updated.`);
273
+
274
+ runtime.log(
275
+ `clawchat-plugin-openclaw login succeeded (user_id=${normalizedResult.agent.user_id}, owner_user_id=${normalizedResult.agent.owner_id}, nickname=${normalizedResult.agent.nickname || "-"}).`,
276
+ );
277
+ }
@@ -0,0 +1,352 @@
1
+ import fs from "node:fs";
2
+ import { describe, expect, it } from "vitest";
3
+ import pluginManifest from "../openclaw.plugin.json" with { type: "json" };
4
+ import packageJson from "../package.json" with { type: "json" };
5
+ import {
6
+ CLAWCHAT_AGENT_ID_ENV,
7
+ CLAWCHAT_OWNER_USER_ID_ENV,
8
+ openclawClawlingConfigSchema,
9
+ } from "./config.ts";
10
+
11
+ interface PackageJsonWithOpenclaw {
12
+ name: string;
13
+ files: string[];
14
+ scripts: Record<string, string>;
15
+ devDependencies: Record<string, string>;
16
+ peerDependencies: Record<string, string>;
17
+ openclaw: {
18
+ extensions: string[];
19
+ runtimeExtensions?: string[];
20
+ setupEntry?: string;
21
+ runtimeSetupEntry?: string;
22
+ plugin?: {
23
+ id: string;
24
+ label: string;
25
+ };
26
+ channel?: {
27
+ id: string;
28
+ label: string;
29
+ selectionLabel?: string;
30
+ docsPath?: string;
31
+ docsLabel?: string;
32
+ blurb: string;
33
+ order?: number;
34
+ aliases?: string[];
35
+ cliAddOptions?: Array<{ flags: string; description: string }>;
36
+ };
37
+ install: { npmSpec: string; minHostVersion: string };
38
+ };
39
+ }
40
+
41
+ describe("clawchat-plugin-openclaw manifest", () => {
42
+ it("keeps plugin id / channel id / package name aligned", () => {
43
+ expect(pluginManifest.id).toBe("clawchat-plugin-openclaw");
44
+ expect(pluginManifest.channels).toContain("clawchat-plugin-openclaw");
45
+ expect(pluginManifest.skills).toEqual(["./skills"]);
46
+ expect(pluginManifest.channelConfigs?.["clawchat-plugin-openclaw"]?.label).toBe(
47
+ "Clawling Chat",
48
+ );
49
+ expect(pluginManifest.channelConfigs?.["clawchat-plugin-openclaw"]?.schema?.properties).toHaveProperty(
50
+ "token",
51
+ );
52
+ expect(packageJson.name).toBe("@clawling/clawchat-plugin-openclaw");
53
+ const pkg = packageJson as PackageJsonWithOpenclaw;
54
+ expect(pkg.openclaw.extensions).toContain("./index.ts");
55
+ expect(pkg.openclaw.install.npmSpec).toBe("@clawling/clawchat-plugin-openclaw");
56
+ });
57
+
58
+ it("requires an OpenClaw host with runtime config mutation support", () => {
59
+ const pkg = packageJson as PackageJsonWithOpenclaw;
60
+ expect(pkg.peerDependencies.openclaw).toBe(">=2026.5.4");
61
+ expect(pkg.devDependencies.openclaw).toBe("2026.5.4");
62
+ expect(pkg.openclaw.install.minHostVersion).toBe(">=2026.5.4");
63
+ });
64
+
65
+ it("publishes compiled runtime entrypoints for npm plugin installs", () => {
66
+ const pkg = packageJson as PackageJsonWithOpenclaw;
67
+ expect(pkg.openclaw.extensions).toEqual(["./index.ts"]);
68
+ expect(pkg.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
69
+ expect(pkg.openclaw.setupEntry).toBe("./setup-entry.ts");
70
+ expect(pkg.openclaw.runtimeSetupEntry).toBe("./dist/setup-entry.js");
71
+ expect(pkg.files).toContain("dist");
72
+ expect(pkg.files).toContain("setup-entry.ts");
73
+ expect(pkg.files).toContain("skills");
74
+ expect(pkg.files).toContain("INSTALL.md");
75
+ expect(pkg.scripts.build).toBe("tsc -p tsconfig.build.json");
76
+ expect(pkg.scripts.prepack).toBe("npm run build");
77
+ expect(fs.existsSync(new URL("../tsconfig.build.json", import.meta.url))).toBe(true);
78
+ expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(true);
79
+ });
80
+
81
+ it("publishes channel catalog metadata for OpenClaw CLI discovery", () => {
82
+ const pkg = packageJson as PackageJsonWithOpenclaw;
83
+ expect(pkg.openclaw.plugin).toEqual({
84
+ id: "clawchat-plugin-openclaw",
85
+ label: "Clawling Chat",
86
+ });
87
+ expect(pkg.openclaw.channel).toEqual({
88
+ id: "clawchat-plugin-openclaw",
89
+ label: "Clawling Chat",
90
+ selectionLabel: "Clawling Chat",
91
+ docsPath: "/channels/clawchat-plugin-openclaw",
92
+ docsLabel: "clawchat-plugin-openclaw",
93
+ blurb: "ClawChat Protocol v2 over WebSocket.",
94
+ order: 110,
95
+ cliAddOptions: [
96
+ {
97
+ flags: "--token <invite-code>",
98
+ description: "ClawChat invite code",
99
+ },
100
+ ],
101
+ });
102
+ });
103
+
104
+ it("declares supported channel/command activation hints for plugin loading", () => {
105
+ expect(pluginManifest.activation).toEqual({
106
+ onStartup: true,
107
+ onChannels: ["clawchat-plugin-openclaw"],
108
+ onCommands: ["clawchat-activate"],
109
+ });
110
+ expect(pluginManifest.commandAliases).toEqual([
111
+ { name: "clawchat-activate", kind: "runtime-slash" },
112
+ ]);
113
+ });
114
+
115
+ it("declares env-driven ClawChat channel credentials for host setup/status surfaces", () => {
116
+ expect(pluginManifest.channelEnvVars).toEqual({
117
+ "clawchat-plugin-openclaw": [
118
+ "CLAWCHAT_TOKEN",
119
+ CLAWCHAT_AGENT_ID_ENV,
120
+ "CLAWCHAT_USER_ID",
121
+ CLAWCHAT_OWNER_USER_ID_ENV,
122
+ "CLAWCHAT_REFRESH_TOKEN",
123
+ "CLAWCHAT_BASE_URL",
124
+ "CLAWCHAT_WEBSOCKET_URL",
125
+ ],
126
+ });
127
+ });
128
+
129
+ it("keeps host manifest channel schemas aligned with runtime config schema", () => {
130
+ expect(pluginManifest.configSchema).toEqual(openclawClawlingConfigSchema);
131
+ expect(pluginManifest.channelConfigs?.["clawchat-plugin-openclaw"]?.schema).toEqual(
132
+ openclawClawlingConfigSchema,
133
+ );
134
+ });
135
+
136
+ it("does not publish stream tuning in manifest config schemas", () => {
137
+ expect(pluginManifest.configSchema.properties).not.toHaveProperty("stream");
138
+ expect(
139
+ pluginManifest.channelConfigs?.["clawchat-plugin-openclaw"]?.schema?.properties,
140
+ ).not.toHaveProperty("stream");
141
+ });
142
+
143
+ it("keeps setup entry on a lightweight setup surface", () => {
144
+ const pkg = packageJson as PackageJsonWithOpenclaw;
145
+ expect(pkg.files).not.toContain("setup-api.ts");
146
+ expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
147
+ const setupEntry = fs.readFileSync(new URL("../setup-entry.ts", import.meta.url), "utf8");
148
+ expect(setupEntry).toMatch(/defineSetupPluginEntry/);
149
+ expect(setupEntry).toMatch(/openclawClawlingSetupPlugin/);
150
+ expect(setupEntry).not.toMatch(/\.\/src\/channel\.ts/);
151
+ expect(setupEntry).not.toMatch(/\.\/src\/runtime(?:\.ts)?/);
152
+ expect(setupEntry).not.toMatch(/\.\/src\/outbound(?:\.ts)?/);
153
+ });
154
+
155
+ it("uses the OpenClaw channel entry helper for registration-mode splitting", () => {
156
+ const entry = fs.readFileSync(new URL("../index.ts", import.meta.url), "utf8");
157
+ expect(entry).toMatch(/defineChannelPluginEntry/);
158
+ expect(entry).toMatch(/registerFull/);
159
+ expect(entry).toMatch(/registerOpenclawClawlingCommands/);
160
+ expect(entry).toMatch(/registerOpenclawClawlingTools/);
161
+ expect(entry).not.toMatch(/register\(api: OpenClawPluginApi\)/);
162
+ });
163
+
164
+ it("documents runtime activation as the reliable first-time activation path", () => {
165
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
166
+ const docs = fs.readFileSync(new URL("../docs/clawchat-plugin-openclaw.md", import.meta.url), "utf8");
167
+ expect(readme).not.toMatch(/clawchat_activate/i);
168
+ expect(docs).not.toMatch(/clawchat_activate/i);
169
+ expect(readme).toMatch(/\/clawchat-activate A1B2C3/i);
170
+ expect(docs).toMatch(/\/clawchat-activate A1B2C3/i);
171
+ expect(readme).toMatch(/OpenClaw 2026\.5\.5/i);
172
+ expect(docs).toMatch(/OpenClaw 2026\.5\.5/i);
173
+ expect(readme).toMatch(/Unknown channel: clawchat-plugin-openclaw/i);
174
+ expect(docs).toMatch(/Unknown channel: clawchat-plugin-openclaw/i);
175
+ });
176
+
177
+ it("publishes the repository-provided ClawChat skill", () => {
178
+ const pkg = packageJson as PackageJsonWithOpenclaw;
179
+ expect(pluginManifest.skills).toEqual(["./skills"]);
180
+ expect(pkg.files).toContain("skills");
181
+
182
+ const skillUrl = new URL("../skills/clawchat/SKILL.md", import.meta.url);
183
+ expect(fs.existsSync(skillUrl)).toBe(true);
184
+ const skill = fs.readFileSync(skillUrl, "utf8");
185
+ expect(skill).toMatch(
186
+ /^---\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,
187
+ );
188
+ expect(skill).toMatch(
189
+ /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 lookup instead of direct HTTP calls, shell scripts, or handwritten clients\./,
190
+ );
191
+ expect(skill).toMatch(/clawchat_get_account_profile/);
192
+ expect(skill).toMatch(/clawchat_search_users/);
193
+ expect(skill).toMatch(/clawchat_upload_avatar_image/);
194
+ expect(skill).toMatch(/clawchat_upload_media_file/);
195
+ expect(skill).toMatch(/clawchat_get_conversation/);
196
+ expect(skill).toMatch(/For conversations\/groups, use only `clawchat_get_conversation`/);
197
+ expect(skill).not.toMatch(
198
+ /conversations?\/groups?.*(?:create|update|leave|dissolve|add members|remove members|administer)/i,
199
+ );
200
+ expect(skill).not.toMatch(/clawchat_create_group_conversation/);
201
+ expect(skill).not.toMatch(/clawchat_update_conversation/);
202
+ expect(skill).not.toMatch(/clawchat_leave_conversation/);
203
+ expect(skill).not.toMatch(/clawchat_dissolve_conversation/);
204
+ expect(skill).not.toMatch(/clawchat_add_conversation_member/);
205
+ expect(skill).not.toMatch(/clawchat_remove_conversation_member/);
206
+ expect(skill).not.toMatch(/clawchat_list_conversation_users/);
207
+ expect(skill).toMatch(/## Profile And Identity Sync/);
208
+ expect(skill).toMatch(/When updating the OpenClaw agent identity file/);
209
+ expect(skill).toMatch(/display name \/ nickname \| `clawchat_update_account_profile` with `nickname`/);
210
+ expect(skill).toMatch(/bio \/ self-introduction \| `clawchat_update_account_profile` with `bio`/);
211
+ expect(skill).toMatch(/local avatar image \| `clawchat_upload_avatar_image`, then `clawchat_update_account_profile` with `avatar_url`/);
212
+ expect(skill).toMatch(/Do not invent invite codes, tokens, moment ids, comment ids, user ids, emoji reactions, image URLs, or file paths/);
213
+ expect(skill).not.toMatch(/hermes/i);
214
+ expect(skill).not.toMatch(/target hermes/i);
215
+ expect(skill).not.toMatch(/choosing among registered clawchat_\*/);
216
+ expect(skill).not.toMatch(/\b(?:whe|regis|plu)\s*$/m);
217
+ });
218
+
219
+ it("declares ownership of registered ClawChat agent tools", () => {
220
+ expect(pluginManifest.contracts?.tools).toEqual([
221
+ "clawchat_get_account_profile",
222
+ "clawchat_get_user_profile",
223
+ "clawchat_list_account_friends",
224
+ "clawchat_search_users",
225
+ "clawchat_mention_message",
226
+ "clawchat_get_conversation",
227
+ "clawchat_list_moments",
228
+ "clawchat_create_moment",
229
+ "clawchat_delete_moment",
230
+ "clawchat_toggle_moment_reaction",
231
+ "clawchat_create_moment_comment",
232
+ "clawchat_reply_moment_comment",
233
+ "clawchat_delete_moment_comment",
234
+ "clawchat_update_account_profile",
235
+ "clawchat_upload_avatar_image",
236
+ "clawchat_upload_media_file",
237
+ "clawchat_memory_search",
238
+ "clawchat_memory_read",
239
+ "clawchat_memory_write",
240
+ "clawchat_memory_edit",
241
+ "clawchat_metadata_sync",
242
+ "clawchat_metadata_update",
243
+ ]);
244
+ });
245
+
246
+ it("declares exactly the six file-backed memory and metadata tools without old-prefix aliases", () => {
247
+ const memoryTools = (pluginManifest.contracts?.tools ?? []).filter((name) =>
248
+ /memory|metadata/.test(name),
249
+ );
250
+ expect(memoryTools).toEqual([
251
+ "clawchat_memory_search",
252
+ "clawchat_memory_read",
253
+ "clawchat_memory_write",
254
+ "clawchat_memory_edit",
255
+ "clawchat_metadata_sync",
256
+ "clawchat_metadata_update",
257
+ ]);
258
+ expect(pluginManifest.contracts?.tools ?? []).not.toContain("cc_memory_read");
259
+ expect(pluginManifest.contracts?.tools ?? []).not.toContain("cc_metadata_sync");
260
+ });
261
+
262
+ it("keeps the optional OpenClaw source checkout local-only", () => {
263
+ expect(fs.existsSync(new URL("../.gitmodules", import.meta.url))).toBe(false);
264
+
265
+ const gitignore = fs.readFileSync(new URL("../.gitignore", import.meta.url), "utf8");
266
+ expect(gitignore).toMatch(/^tmp\/openclaw\/$/m);
267
+
268
+ const pkg = packageJson as PackageJsonWithOpenclaw;
269
+ expect(pkg.scripts["dev:openclaw-source"]).toBe(
270
+ "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
271
+ );
272
+
273
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
274
+ expect(readme).toMatch(/npm run dev:openclaw-source/);
275
+ expect(readme).toMatch(
276
+ /git clone --depth=1 https:\/\/github\.com\/openclaw\/openclaw\.git tmp\/openclaw/,
277
+ );
278
+ expect(readme).toMatch(/local-only/i);
279
+ });
280
+
281
+ it("keeps default Vitest discovery scoped to plugin sources", () => {
282
+ const configUrl = new URL("../vitest.config.ts", import.meta.url);
283
+ expect(fs.existsSync(configUrl)).toBe(true);
284
+ const config = fs.readFileSync(configUrl, "utf8");
285
+ expect(config).toMatch(/include:\s*\["src\/\*\*\/\*\.test\.ts"\]/);
286
+ expect(config).toMatch(/"tmp\/\*\*"/);
287
+ expect(config).toMatch(/"\.e2e\/\*\*"/);
288
+ });
289
+
290
+ it("documents slash command as the chat activation path", () => {
291
+ const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
292
+ const docs = fs.readFileSync(new URL("../docs/clawchat-plugin-openclaw.md", import.meta.url), "utf8");
293
+ expect(readme).toMatch(/Current activation paths/i);
294
+ expect(docs).toMatch(/Current activation paths/i);
295
+ expect(readme).not.toMatch(/clawchat_activate/i);
296
+ expect(docs).not.toMatch(/clawchat_activate/i);
297
+ expect(readme).toMatch(/\/clawchat-activate A1B2C3/i);
298
+ expect(docs).toMatch(/\/clawchat-activate A1B2C3/i);
299
+ expect(readme).toMatch(/openclaw channels add --channel clawchat-plugin-openclaw --token "\$CLAWCHAT_INVITE_CODE"/i);
300
+ expect(docs).toMatch(/openclaw channels add --channel clawchat-plugin-openclaw --token "\$CLAWCHAT_INVITE_CODE"/i);
301
+ expect(readme).toMatch(/openclaw channels login --channel clawchat-plugin-openclaw/i);
302
+ expect(docs).toMatch(/openclaw channels login --channel clawchat-plugin-openclaw/i);
303
+ expect(readme).toMatch(/refresh credentials/i);
304
+ expect(docs).toMatch(/refresh credentials/i);
305
+ expect(readme).toMatch(/OpenClaw 2026\.5\.5/i);
306
+ expect(docs).toMatch(/OpenClaw 2026\.5\.5/i);
307
+ expect(readme).toMatch(/Unknown channel: clawchat-plugin-openclaw/i);
308
+ expect(docs).toMatch(/Unknown channel: clawchat-plugin-openclaw/i);
309
+ expect(readme).toMatch(/openclaw channels status --probe/i);
310
+ expect(docs).toMatch(/openclaw channels status --probe/i);
311
+ expect(readme).toMatch(/openclaw gateway restart/i);
312
+ expect(docs).toMatch(/openclaw gateway restart/i);
313
+ expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
314
+ expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
315
+ expect(readme).toMatch(/runtime slash command/i);
316
+ expect(docs).toMatch(/runtime slash command/i);
317
+ });
318
+
319
+ it("documents the numbered install restart step", () => {
320
+ const install = fs.readFileSync(new URL("../INSTALL.md", import.meta.url), "utf8");
321
+ const restart = install.indexOf("## 3. Restart");
322
+ const activate = install.indexOf("## 4. Activate");
323
+ const installSection = install.slice(restart, activate);
324
+
325
+ expect(installSection).toMatch(/openclaw gateway restart/);
326
+ expect(installSection).toMatch(/First restart completed/);
327
+ expect(installSection).not.toMatch(/kill -TERM 1/);
328
+ expect(installSection).not.toMatch(/docker restart <container>/);
329
+ });
330
+
331
+ it("documents numbered CLI activation, hot reload verification, and restart fallback", () => {
332
+ const install = fs.readFileSync(new URL("../INSTALL.md", import.meta.url), "utf8");
333
+ const activate = install.indexOf("## 4. Activate");
334
+ const verify = install.indexOf("## 5. Verify");
335
+ const activateSection = install.slice(activate, verify);
336
+
337
+ expect(activateSection).toMatch(/openclaw channels add --channel clawchat-plugin-openclaw --token "\$CLAWCHAT_INVITE_CODE"/);
338
+ expect(activateSection).toMatch(/Activation completed/);
339
+ expect(activateSection).not.toMatch(/openclaw channels status --probe/i);
340
+ expect(activateSection).not.toMatch(/openclaw gateway restart/i);
341
+ expect(install).not.toMatch(/## 5\. Restart Again/i);
342
+ expect(install).not.toMatch(/Second restart completed/);
343
+
344
+ const verifySection = install.slice(verify);
345
+ expect(verifySection).toMatch(/sleep 5/);
346
+ expect(verifySection).toMatch(/openclaw channels status --probe/i);
347
+ expect(verifySection).toMatch(/Verification completed/);
348
+ expect(verifySection).toMatch(/enabled, configured, running, and\s+connected/i);
349
+ expect(verifySection).toMatch(/restart OpenClaw/i);
350
+ expect(verifySection).toMatch(/installation flow is complete/i);
351
+ });
352
+ });