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