@newbase-clawchat/openclaw-clawchat 2026.4.30 → 2026.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Pro
10
10
  - Outbound text replies in `static` or `stream` mode, with a consolidated final `message.reply`
11
11
  - Typing indicators and filtered forwarding for thinking / tool-call content
12
12
  - Media fragments (image / file / audio / video) in either direction
13
- - Invite-code onboarding via `clawchat_activate` or channel login, plus always-registered `clawchat_*` account/media tools
13
+ - Invite-code onboarding via `clawchat_activate` or `channels add --token`, plus always-registered `clawchat_*` account/media tools
14
14
 
15
15
  ## Install
16
16
 
@@ -19,7 +19,7 @@ OpenClaw channel plugin that connects an agent to ClawChat over the ClawChat Pro
19
19
  npm i @newbase-clawchat/openclaw-clawchat
20
20
  ```
21
21
 
22
- Requires `openclaw >= 2026.4.29` as a peer host.
22
+ Requires `openclaw >= 2026.5.4` as a peer host.
23
23
 
24
24
  For the OpenClaw plugin install/update flow, see [`INSTALL.md`](./INSTALL.md).
25
25
 
@@ -39,11 +39,12 @@ activate ClawChat with invite code A1B2C3
39
39
 
40
40
  If the plugin is already loaded by the Gateway, the activation skill calls the
41
41
  `clawchat_activate` tool and OpenClaw hot-reloads the updated `channels.*`
42
- credentials. If the tool is not available yet, use channel login as the CLI
43
- fallback:
42
+ credentials. If the tool is not available yet, use `channels add` as the
43
+ first-time CLI activation path:
44
44
 
45
45
  ```bash
46
- openclaw channels login --channel openclaw-clawchat
46
+ CLAWCHAT_INVITE_CODE="A1B2C3"
47
+ openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
47
48
  openclaw channels status --probe
48
49
  ```
49
50
 
@@ -55,15 +56,17 @@ openclaw gateway restart
55
56
  ```
56
57
 
57
58
  If you run the gateway manually instead of as a service and it is not already
58
- running, start it after login:
59
+ running, start it after activation:
59
60
 
60
61
  ```bash
61
62
  openclaw gateway run
62
63
  ```
63
64
 
64
- The invite code is not a token; token fields are written only after login. The
65
+ The `--token` value above is the ClawChat invite code for OpenClaw's generic
66
+ `channels add` CLI surface; persisted token fields are written only after the
67
+ invite code exchange succeeds. The
65
68
  plugin registers `clawchat_activate` and the six account/media tools at plugin
66
- load time so they stay visible before activation. Before login, account/media
69
+ load time so they stay visible before activation. Before activation, account/media
67
70
  tools return a config error instead of disappearing; after activation/login, the
68
71
  channel is enabled and the same tools read the hot-reloaded token/userId.
69
72
 
@@ -32,9 +32,9 @@ const configAdapter = createTopLevelChannelConfigAdapter({
32
32
  * one-shot setup metadata because current hosts do not discover channels from
33
33
  * `plugins.load.paths`.
34
34
  *
35
- * Setup takes exactly ONE input: `code` (an invite code). URL + token +
36
- * userId come from the login flow which is triggered automatically in
37
- * `afterAccountConfigWritten`.
35
+ * Setup takes an invite code from `code` or from OpenClaw's generic
36
+ * `channels add --token` input. URL + token + userId come from the login flow
37
+ * which is triggered automatically in `afterAccountConfigWritten`.
38
38
  *
39
39
  * `applyAccountConfig` itself only marks the section `enabled: true`;
40
40
  * credentials are written by `runOpenclawClawlingLogin` via the runtime config
@@ -43,7 +43,12 @@ const configAdapter = createTopLevelChannelConfigAdapter({
43
43
  const setupAdapter = {
44
44
  resolveAccountId: () => DEFAULT_ACCOUNT_ID,
45
45
  validateInput: ({ input }) => {
46
- if (!input.code?.trim()) {
46
+ const inviteCode = typeof input.code === "string" && input.code.trim()
47
+ ? input.code.trim()
48
+ : typeof input.token === "string"
49
+ ? input.token.trim()
50
+ : "";
51
+ if (!inviteCode) {
47
52
  return "ClawChat invite code is required.";
48
53
  }
49
54
  return null;
@@ -62,7 +67,11 @@ const setupAdapter = {
62
67
  });
63
68
  },
64
69
  afterAccountConfigWritten: async ({ cfg, input, runtime, }) => {
65
- const code = input.code?.trim();
70
+ const code = typeof input.code === "string" && input.code.trim()
71
+ ? input.code.trim()
72
+ : typeof input.token === "string"
73
+ ? input.token.trim()
74
+ : "";
66
75
  if (!code)
67
76
  return;
68
77
  // Lazy-import the login runtime to keep @clack/prompts / readline /
@@ -1,5 +1,10 @@
1
1
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
2
2
  export const CHANNEL_ID = "openclaw-clawchat";
3
+ export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN";
4
+ export const CLAWCHAT_USER_ID_ENV = "CLAWCHAT_USER_ID";
5
+ export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN";
6
+ export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL";
7
+ export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL";
3
8
  /**
4
9
  * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
5
10
  * login` works out of the box without requiring a prior `openclaw channel
@@ -134,6 +139,9 @@ function readChannelSection(cfg) {
134
139
  function readOptionalString(value) {
135
140
  return typeof value === "string" ? value.trim() : "";
136
141
  }
142
+ function readEnvString(env, key) {
143
+ return readOptionalString(env[key]);
144
+ }
137
145
  function readReplyMode(value) {
138
146
  return value === "stream" ? "stream" : "static";
139
147
  }
@@ -175,13 +183,17 @@ function readAck(raw) {
175
183
  : DEFAULT_ACK.autoResendOnTimeout,
176
184
  };
177
185
  }
178
- export function resolveOpenclawClawlingAccount(cfg) {
186
+ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
179
187
  const channel = readChannelSection(cfg);
180
188
  // Apply built-in defaults so login/gateway work without prior setup.
181
- const websocketUrl = readOptionalString(channel.websocketUrl) || DEFAULT_WEBSOCKET_URL;
182
- const baseUrl = readOptionalString(channel.baseUrl) || DEFAULT_BASE_URL;
183
- const token = readOptionalString(channel.token);
184
- const userId = readOptionalString(channel.userId);
189
+ const websocketUrl = readOptionalString(channel.websocketUrl) ||
190
+ readEnvString(env, CLAWCHAT_WEBSOCKET_URL_ENV) ||
191
+ DEFAULT_WEBSOCKET_URL;
192
+ const baseUrl = readOptionalString(channel.baseUrl) ||
193
+ readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
194
+ DEFAULT_BASE_URL;
195
+ const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
196
+ const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
185
197
  const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
186
198
  const replyMode = readReplyMode(channel.replyMode);
187
199
  const groupMode = readGroupMode(channel.groupMode);
@@ -69,7 +69,9 @@ async function persistLoginConfig(params, result) {
69
69
  throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
70
70
  }
71
71
  /**
72
- * Run the `openclaw channels login --channel openclaw-clawchat` flow:
72
+ * Run the invite-code credential exchange used by `clawchat_activate`,
73
+ * `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
74
+ * and `openclaw channels login --channel openclaw-clawchat`:
73
75
  * 1. Read the existing channel section; require `baseUrl` to be set so we
74
76
  * know which server to hit.
75
77
  * 2. Prompt the user for an invite code on stdin.
package/dist/src/tools.js CHANGED
@@ -118,8 +118,8 @@ export function registerOpenclawClawlingTools(api) {
118
118
  function buildClient() {
119
119
  const acct = resolveCurrent();
120
120
  // `baseUrl` always resolves via the built-in default in config.ts, so we
121
- // only need to gate on `token` here (which is populated by `openclaw
122
- // channel login`).
121
+ // only need to gate on `token` here (which is populated by ClawChat
122
+ // activation/login).
123
123
  if (!acct.token) {
124
124
  return { ok: false, error: configError("openclaw-clawchat: token is required") };
125
125
  }
@@ -7,6 +7,15 @@
7
7
  "onChannels": ["openclaw-clawchat"],
8
8
  "onCommands": ["clawchat-login"]
9
9
  },
10
+ "channelEnvVars": {
11
+ "openclaw-clawchat": [
12
+ "CLAWCHAT_TOKEN",
13
+ "CLAWCHAT_USER_ID",
14
+ "CLAWCHAT_REFRESH_TOKEN",
15
+ "CLAWCHAT_BASE_URL",
16
+ "CLAWCHAT_WEBSOCKET_URL"
17
+ ]
18
+ },
10
19
  "commandAliases": [
11
20
  { "name": "clawchat-login", "kind": "runtime-slash" }
12
21
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newbase-clawchat/openclaw-clawchat",
3
- "version": "2026.4.30",
3
+ "version": "2026.5.4",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "files": [
6
6
  "dist",
@@ -15,6 +15,8 @@
15
15
  "build": "tsc -p tsconfig.build.json",
16
16
  "test": "vitest",
17
17
  "test:e2e:install-clawchat-plugin": "bash .e2e/run-install-clawchat-plugin-e2e.sh",
18
+ "test:e2e:install-clawchat-plugin:agent": "bash .e2e/run-install-clawchat-plugin-agent-e2e.sh",
19
+ "test:e2e:install-clawchat-plugin:agent:smoke": "node --test .e2e/run-install-clawchat-plugin-agent-e2e.test.mjs",
18
20
  "test:e2e:install-clawchat-plugin:smoke": "node --test .e2e/run-install-clawchat-plugin-e2e.test.mjs",
19
21
  "dev:openclaw-source": "test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
20
22
  "prepack": "npm run build",
@@ -28,12 +30,12 @@
28
30
  },
29
31
  "devDependencies": {
30
32
  "@types/node": "^25.5.0",
31
- "openclaw": "2026.4.29",
33
+ "openclaw": "2026.5.4",
32
34
  "typescript": "^5.4.0",
33
35
  "vitest": "^4.1.5"
34
36
  },
35
37
  "peerDependencies": {
36
- "openclaw": "^2026.4.29"
38
+ "openclaw": ">=2026.5.4"
37
39
  },
38
40
  "peerDependenciesMeta": {
39
41
  "openclaw": {
@@ -62,7 +64,7 @@
62
64
  "install": {
63
65
  "npmSpec": "@newbase-clawchat/openclaw-clawchat",
64
66
  "defaultChoice": "npm",
65
- "minHostVersion": ">=2026.4.29"
67
+ "minHostVersion": ">=2026.5.4"
66
68
  }
67
69
  }
68
70
  }
@@ -8,20 +8,20 @@ description: |
8
8
 
9
9
  Activation should use the `clawchat_activate` tool when that tool is available. The tool runs the same invite-code login runtime as OpenClaw channel login and persists credentials into `channels.openclaw-clawchat`.
10
10
 
11
- If the tool is not available because the Gateway has not loaded this plugin yet, fall back to OpenClaw channel login. Do not edit config files manually and do not try to execute slash commands from this skill.
11
+ If the tool is not available because the Gateway has not loaded this plugin yet, fall back to OpenClaw channel add for first-time CLI activation. Do not edit config files manually and do not try to execute slash commands from this skill.
12
12
 
13
13
  ## Workflow
14
14
 
15
15
  1. Check whether the user is trying to activate or log in ClawChat.
16
16
  2. If `clawchat_activate` is available and the invite code is known, call it with the invite code.
17
17
  3. If the invite code is not known, ask the user for it before calling `clawchat_activate`.
18
- 4. If `clawchat_activate` is not available, execute the channel login command:
18
+ 4. If `clawchat_activate` is not available, execute the channel add command with the invite code:
19
19
 
20
20
  ```bash
21
- openclaw channels login --channel openclaw-clawchat
21
+ openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
22
22
  ```
23
- 5. Do not append the invite code to the command; `openclaw channels login` has no code argument.
24
- 6. When the command prompts for an invite code, provide the user's invite code if one was already present, otherwise ask the user for it.
23
+ 5. `channels add --token` is the first-time CLI activation path; the token argument is the ClawChat invite code on this setup path.
24
+ 6. Use `openclaw channels login --channel openclaw-clawchat` only to refresh credentials later after the channel already exists.
25
25
  7. After activation succeeds, rely on OpenClaw's `channels.*` config hot reload when the plugin is already loaded. Verify channel status if available:
26
26
 
27
27
  ```bash
@@ -32,7 +32,7 @@ openclaw channels status --probe
32
32
  ```bash
33
33
  openclaw gateway restart
34
34
  ```
35
- 9. Tell the user activation completes after login succeeds and either config hot reload/probe succeeds or Gateway restart succeeds.
35
+ 9. Tell the user activation completes after channel add/login succeeds and either config hot reload/probe succeeds or Gateway restart succeeds.
36
36
 
37
37
  ## Trigger Examples
38
38
 
@@ -42,6 +42,6 @@ openclaw gateway restart
42
42
  - `绑定 ClawChat,邀请码 A1B2C3`
43
43
  - `激活 ClawChat`
44
44
 
45
- Do not ask the user to enter a bare ClawChat command. If activation is requested and `clawchat_activate` is available, call that tool yourself. If the tool is unavailable, execute `openclaw channels login --channel openclaw-clawchat` yourself, then probe channel status and restart the Gateway only when needed.
45
+ Do not ask the user to enter a bare ClawChat command. If activation is requested and `clawchat_activate` is available, call that tool yourself. If the tool is unavailable, execute `openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"` yourself, then probe channel status and restart the Gateway only when needed.
46
46
 
47
- When the user asks to activate ClawChat without including a code, ask for the invite code before calling `clawchat_activate`; if falling back to channel login, provide the code when the command needs it.
47
+ When the user asks to activate ClawChat without including a code, ask for the invite code before calling `clawchat_activate` or falling back to channel add.
@@ -28,9 +28,12 @@ describe("openclaw-clawchat plugin", () => {
28
28
  expect(
29
29
  validate!({ cfg: {}, accountId: "default", input: { code: " " } }),
30
30
  ).toMatch(/invite code is required/i);
31
+ expect(
32
+ validate!({ cfg: {}, accountId: "default", input: { token: " " } }),
33
+ ).toMatch(/invite code is required/i);
31
34
  });
32
35
 
33
- it("setup.validateInput passes when code is present", () => {
36
+ it("setup.validateInput passes when code or token is present", () => {
34
37
  const validate = openclawClawlingPlugin.setup?.validateInput as (args: {
35
38
  cfg: unknown;
36
39
  accountId: string;
@@ -39,6 +42,9 @@ describe("openclaw-clawchat plugin", () => {
39
42
  expect(
40
43
  validate({ cfg: {}, accountId: "default", input: { code: "INV-XXXX" } }),
41
44
  ).toBeNull();
45
+ expect(
46
+ validate({ cfg: {}, accountId: "default", input: { token: "INV-XXXX" } }),
47
+ ).toBeNull();
42
48
  });
43
49
 
44
50
  it("setup.applyAccountConfig marks the channel enabled without touching credentials", () => {
package/src/channel.ts CHANGED
@@ -50,9 +50,9 @@ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlin
50
50
  * one-shot setup metadata because current hosts do not discover channels from
51
51
  * `plugins.load.paths`.
52
52
  *
53
- * Setup takes exactly ONE input: `code` (an invite code). URL + token +
54
- * userId come from the login flow which is triggered automatically in
55
- * `afterAccountConfigWritten`.
53
+ * Setup takes an invite code from `code` or from OpenClaw's generic
54
+ * `channels add --token` input. URL + token + userId come from the login flow
55
+ * which is triggered automatically in `afterAccountConfigWritten`.
56
56
  *
57
57
  * `applyAccountConfig` itself only marks the section `enabled: true`;
58
58
  * credentials are written by `runOpenclawClawlingLogin` via the runtime config
@@ -61,7 +61,13 @@ const configAdapter = createTopLevelChannelConfigAdapter<ResolvedOpenclawClawlin
61
61
  const setupAdapter = {
62
62
  resolveAccountId: () => DEFAULT_ACCOUNT_ID,
63
63
  validateInput: ({ input }: { cfg: unknown; accountId: string; input: ChannelSetupInput }) => {
64
- if (!input.code?.trim()) {
64
+ const inviteCode =
65
+ typeof input.code === "string" && input.code.trim()
66
+ ? input.code.trim()
67
+ : typeof input.token === "string"
68
+ ? input.token.trim()
69
+ : "";
70
+ if (!inviteCode) {
65
71
  return "ClawChat invite code is required.";
66
72
  }
67
73
  return null;
@@ -96,7 +102,12 @@ const setupAdapter = {
96
102
  runtime: { log: (message: string) => void };
97
103
  previousCfg: OpenClawConfig;
98
104
  }) => {
99
- const code = input.code?.trim();
105
+ const code =
106
+ typeof input.code === "string" && input.code.trim()
107
+ ? input.code.trim()
108
+ : typeof input.token === "string"
109
+ ? input.token.trim()
110
+ : "";
100
111
  if (!code) return;
101
112
  // Lazy-import the login runtime to keep @clack/prompts / readline /
102
113
  // config-runtime off the plugin's cold-start path. `readInviteCode`
@@ -56,6 +56,50 @@ describe("openclaw-clawchat config", () => {
56
56
  expect(account.stream.maxBufferChars).toBe(3000);
57
57
  });
58
58
 
59
+ it("resolves credentials from ClawChat environment variables when config omits them", () => {
60
+ const account = resolveOpenclawClawlingAccount(
61
+ {},
62
+ {
63
+ CLAWCHAT_TOKEN: "env-token",
64
+ CLAWCHAT_USER_ID: "env-user",
65
+ CLAWCHAT_BASE_URL: "https://api.env.example",
66
+ CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
67
+ },
68
+ );
69
+
70
+ expect(account.configured).toBe(true);
71
+ expect(account.token).toBe("env-token");
72
+ expect(account.userId).toBe("env-user");
73
+ expect(account.baseUrl).toBe("https://api.env.example");
74
+ expect(account.websocketUrl).toBe("wss://ws.env.example/ws");
75
+ });
76
+
77
+ it("prefers explicit channel config over ClawChat environment variables", () => {
78
+ const account = resolveOpenclawClawlingAccount(
79
+ {
80
+ channels: {
81
+ "openclaw-clawchat": {
82
+ token: "config-token",
83
+ userId: "config-user",
84
+ baseUrl: "https://api.config.example",
85
+ websocketUrl: "wss://ws.config.example/ws",
86
+ },
87
+ },
88
+ },
89
+ {
90
+ CLAWCHAT_TOKEN: "env-token",
91
+ CLAWCHAT_USER_ID: "env-user",
92
+ CLAWCHAT_BASE_URL: "https://api.env.example",
93
+ CLAWCHAT_WEBSOCKET_URL: "wss://ws.env.example/ws",
94
+ },
95
+ );
96
+
97
+ expect(account.token).toBe("config-token");
98
+ expect(account.userId).toBe("config-user");
99
+ expect(account.baseUrl).toBe("https://api.config.example");
100
+ expect(account.websocketUrl).toBe("wss://ws.config.example/ws");
101
+ });
102
+
59
103
  it("falls back to static replyMode for unknown values", () => {
60
104
  const cfg = {
61
105
  channels: {
package/src/config.ts CHANGED
@@ -2,6 +2,11 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
2
2
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
3
3
 
4
4
  export const CHANNEL_ID = "openclaw-clawchat" as const;
5
+ export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN" as const;
6
+ export const CLAWCHAT_USER_ID_ENV = "CLAWCHAT_USER_ID" as const;
7
+ export const CLAWCHAT_REFRESH_TOKEN_ENV = "CLAWCHAT_REFRESH_TOKEN" as const;
8
+ export const CLAWCHAT_BASE_URL_ENV = "CLAWCHAT_BASE_URL" as const;
9
+ export const CLAWCHAT_WEBSOCKET_URL_ENV = "CLAWCHAT_WEBSOCKET_URL" as const;
5
10
 
6
11
  /**
7
12
  * Built-in defaults for the Clawling Chat endpoints so `openclaw channel
@@ -84,7 +89,7 @@ export type OpenclawClawlingConfig = {
84
89
  websocketUrl?: string;
85
90
  baseUrl?: string;
86
91
  token?: string;
87
- /** Refresh token persisted by `openclaw channels login --channel openclaw-clawchat` (paired with `token`). */
92
+ /** Refresh token persisted by ClawChat activation/login (paired with `token`). */
88
93
  refreshToken?: string;
89
94
  userId?: string;
90
95
  replyMode?: ReplyMode;
@@ -234,6 +239,10 @@ function readOptionalString(value: unknown): string {
234
239
  return typeof value === "string" ? value.trim() : "";
235
240
  }
236
241
 
242
+ function readEnvString(env: Record<string, string | undefined>, key: string): string {
243
+ return readOptionalString(env[key]);
244
+ }
245
+
237
246
  function readReplyMode(value: unknown): ReplyMode {
238
247
  return value === "stream" ? "stream" : "static";
239
248
  }
@@ -289,13 +298,20 @@ function readAck(raw: unknown): Required<OpenclawClawlingAckConfig> {
289
298
 
290
299
  export function resolveOpenclawClawlingAccount(
291
300
  cfg: OpenClawConfig,
301
+ env: Record<string, string | undefined> = process.env,
292
302
  ): ResolvedOpenclawClawlingAccount {
293
303
  const channel = readChannelSection(cfg);
294
304
  // Apply built-in defaults so login/gateway work without prior setup.
295
- const websocketUrl = readOptionalString(channel.websocketUrl) || DEFAULT_WEBSOCKET_URL;
296
- const baseUrl = readOptionalString(channel.baseUrl) || DEFAULT_BASE_URL;
297
- const token = readOptionalString(channel.token);
298
- const userId = readOptionalString(channel.userId);
305
+ const websocketUrl =
306
+ readOptionalString(channel.websocketUrl) ||
307
+ readEnvString(env, CLAWCHAT_WEBSOCKET_URL_ENV) ||
308
+ DEFAULT_WEBSOCKET_URL;
309
+ const baseUrl =
310
+ readOptionalString(channel.baseUrl) ||
311
+ readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
312
+ DEFAULT_BASE_URL;
313
+ const token = readOptionalString(channel.token) || readEnvString(env, CLAWCHAT_TOKEN_ENV);
314
+ const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
299
315
  const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
300
316
  const replyMode = readReplyMode(channel.replyMode);
301
317
  const groupMode = readGroupMode(channel.groupMode);
@@ -110,7 +110,9 @@ async function persistLoginConfig(
110
110
  }
111
111
 
112
112
  /**
113
- * Run the `openclaw channels login --channel openclaw-clawchat` flow:
113
+ * Run the invite-code credential exchange used by `clawchat_activate`,
114
+ * `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
115
+ * and `openclaw channels login --channel openclaw-clawchat`:
114
116
  * 1. Read the existing channel section; require `baseUrl` to be set so we
115
117
  * know which server to hit.
116
118
  * 2. Prompt the user for an invite code on stdin.
@@ -46,9 +46,9 @@ describe("openclaw-clawchat manifest", () => {
46
46
 
47
47
  it("requires an OpenClaw host with runtime config mutation support", () => {
48
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");
49
+ expect(pkg.peerDependencies.openclaw).toBe(">=2026.5.4");
50
+ expect(pkg.devDependencies.openclaw).toBe("2026.5.4");
51
+ expect(pkg.openclaw.install.minHostVersion).toBe(">=2026.5.4");
52
52
  });
53
53
 
54
54
  it("publishes compiled runtime entrypoints for npm plugin installs", () => {
@@ -85,6 +85,18 @@ describe("openclaw-clawchat manifest", () => {
85
85
  ]);
86
86
  });
87
87
 
88
+ it("declares env-driven ClawChat channel credentials for host setup/status surfaces", () => {
89
+ expect(pluginManifest.channelEnvVars).toEqual({
90
+ "openclaw-clawchat": [
91
+ "CLAWCHAT_TOKEN",
92
+ "CLAWCHAT_USER_ID",
93
+ "CLAWCHAT_REFRESH_TOKEN",
94
+ "CLAWCHAT_BASE_URL",
95
+ "CLAWCHAT_WEBSOCKET_URL",
96
+ ],
97
+ });
98
+ });
99
+
88
100
  it("does not publish setup migration or setup-runtime entry metadata", () => {
89
101
  const pkg = packageJson as PackageJsonWithOpenclaw;
90
102
  expect(pkg.files).not.toContain("setup-api.ts");
@@ -94,11 +106,11 @@ describe("openclaw-clawchat manifest", () => {
94
106
  expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
95
107
  });
96
108
 
97
- it("does not document channels add as an activation path", () => {
109
+ it("documents channels add --token as the first-time CLI activation path", () => {
98
110
  const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
99
111
  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);
112
+ expect(readme).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
113
+ expect(docs).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
102
114
  });
103
115
 
104
116
  it("publishes a ClawChat account tools skill for non-activation workflows", () => {
@@ -155,7 +167,7 @@ describe("openclaw-clawchat manifest", () => {
155
167
  expect(config).toMatch(/"\.e2e\/\*\*"/);
156
168
  });
157
169
 
158
- it("keeps the activation skill on clawchat_activate with channel-login fallback", () => {
170
+ it("keeps the activation skill on clawchat_activate with channels-add fallback", () => {
159
171
  const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
160
172
  expect(skill).toMatch(/name:\s*clawchat-activate/);
161
173
  expect(skill).toMatch(/clawchat_activate/);
@@ -164,9 +176,9 @@ describe("openclaw-clawchat manifest", () => {
164
176
  expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
165
177
  expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
166
178
  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);
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);
170
182
  expect(skill).toMatch(/channel login/i);
171
183
  expect(skill).toMatch(/openclaw channels status --probe/);
172
184
  expect(skill).toMatch(/openclaw gateway restart/);
@@ -175,13 +187,13 @@ describe("openclaw-clawchat manifest", () => {
175
187
  expect(skill).toMatch(/restart[^\n]+only when/i);
176
188
  });
177
189
 
178
- it("documents clawchat_activate as the natural-language activation path with CLI fallback", () => {
190
+ it("documents clawchat_activate as the natural-language activation path with channels-add CLI fallback", () => {
179
191
  const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
180
192
  const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
181
193
  expect(readme).toMatch(/clawchat_activate/i);
182
194
  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);
195
+ expect(readme).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
196
+ expect(docs).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
185
197
  expect(readme).toMatch(/openclaw channels status --probe/i);
186
198
  expect(docs).toMatch(/openclaw channels status --probe/i);
187
199
  expect(readme).toMatch(/openclaw gateway restart/i);
@@ -197,4 +209,42 @@ describe("openclaw-clawchat manifest", () => {
197
209
  expect(readme).toMatch(/activation skill calls/i);
198
210
  expect(docs).toMatch(/Natural-language activation requests should call `clawchat_activate`/i);
199
211
  });
212
+
213
+ it("documents gateway restart as the required next step after plugin install or update", () => {
214
+ 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);
218
+
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
+ 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);
227
+ });
228
+
229
+ it("documents activation as a direct channels-add command after restarting the Openclaw Gateway", () => {
230
+ 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");
233
+ const activateSection = install.slice(activate, verify);
234
+
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
+ 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);
241
+ expect(activateSection).not.toMatch(/openclaw channels status --probe/i);
242
+ expect(activateSection).not.toMatch(/verify the channel/i);
243
+
244
+ const verifySection = install.slice(verify);
245
+ expect(verifySection).toMatch(/openclaw channels status --probe/i);
246
+ 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);
249
+ });
200
250
  });
@@ -3,9 +3,14 @@ import pluginEntry from "../index.ts";
3
3
 
4
4
  describe("openclaw-clawchat plugin entry", () => {
5
5
  it("registers the channel/tools and native activation command without bootstrap migration", () => {
6
+ const mutateConfigFile = vi.fn();
6
7
  const api = {
7
8
  config: {},
8
- runtime: {},
9
+ runtime: {
10
+ config: {
11
+ mutateConfigFile,
12
+ },
13
+ },
9
14
  logger: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
10
15
  registerChannel: vi.fn(),
11
16
  registerCommand: vi.fn(),
@@ -23,5 +28,6 @@ describe("openclaw-clawchat plugin entry", () => {
23
28
  name: "clawchat-login",
24
29
  }),
25
30
  );
31
+ expect(mutateConfigFile).not.toHaveBeenCalled();
26
32
  });
27
33
  });
package/src/tools.ts CHANGED
@@ -155,8 +155,8 @@ export function registerOpenclawClawlingTools(api: OpenClawPluginApi): void {
155
155
  function buildClient(): ClientResult {
156
156
  const acct = resolveCurrent();
157
157
  // `baseUrl` always resolves via the built-in default in config.ts, so we
158
- // only need to gate on `token` here (which is populated by `openclaw
159
- // channel login`).
158
+ // only need to gate on `token` here (which is populated by ClawChat
159
+ // activation/login).
160
160
  if (!acct.token) {
161
161
  return { ok: false, error: configError("openclaw-clawchat: token is required") };
162
162
  }