@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 +11 -8
- package/dist/src/channel.js +14 -5
- package/dist/src/config.js +17 -5
- package/dist/src/login.runtime.js +3 -1
- package/dist/src/tools.js +2 -2
- package/openclaw.plugin.json +9 -0
- package/package.json +6 -4
- package/skills/clawchat-activate/SKILL.md +8 -8
- package/src/channel.test.ts +7 -1
- package/src/channel.ts +16 -5
- package/src/config.test.ts +44 -0
- package/src/config.ts +21 -5
- package/src/login.runtime.ts +3 -1
- package/src/manifest.test.ts +63 -13
- package/src/plugin-entry.test.ts +7 -1
- package/src/tools.ts +2 -2
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
|
|
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
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
59
|
+
running, start it after activation:
|
|
59
60
|
|
|
60
61
|
```bash
|
|
61
62
|
openclaw gateway run
|
|
62
63
|
```
|
|
63
64
|
|
|
64
|
-
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
|
|
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
|
|
package/dist/src/channel.js
CHANGED
|
@@ -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
|
|
36
|
-
* userId come from the login flow
|
|
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
|
-
|
|
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
|
|
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 /
|
package/dist/src/config.js
CHANGED
|
@@ -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) ||
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
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
|
|
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
|
|
122
|
-
//
|
|
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
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
|
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
|
|
33
|
+
"openclaw": "2026.5.4",
|
|
32
34
|
"typescript": "^5.4.0",
|
|
33
35
|
"vitest": "^4.1.5"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
|
-
"openclaw": "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
21
|
+
openclaw channels add --channel openclaw-clawchat --token "$CLAWCHAT_INVITE_CODE"
|
|
22
22
|
```
|
|
23
|
-
5.
|
|
24
|
-
6.
|
|
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
|
|
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
|
|
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.
|
package/src/channel.test.ts
CHANGED
|
@@ -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
|
|
54
|
-
* userId come from the login flow
|
|
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
|
-
|
|
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 =
|
|
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`
|
package/src/config.test.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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);
|
package/src/login.runtime.ts
CHANGED
|
@@ -110,7 +110,9 @@ async function persistLoginConfig(
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* Run the
|
|
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.
|
package/src/manifest.test.ts
CHANGED
|
@@ -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("
|
|
50
|
-
expect(pkg.devDependencies.openclaw).toBe("2026.4
|
|
51
|
-
expect(pkg.openclaw.install.minHostVersion).toBe(">=2026.4
|
|
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("
|
|
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).
|
|
101
|
-
expect(docs).
|
|
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
|
|
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
|
|
168
|
-
expect(skill).toMatch(/
|
|
169
|
-
expect(skill).toMatch(/
|
|
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
|
|
184
|
-
expect(docs).toMatch(/openclaw channels
|
|
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
|
});
|
package/src/plugin-entry.test.ts
CHANGED
|
@@ -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
|
|
159
|
-
//
|
|
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
|
}
|