@newbase-clawchat/openclaw-clawchat 2026.4.29 → 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 +37 -11
- package/dist/index.js +27 -0
- package/dist/src/api-client.js +156 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +200 -0
- package/dist/src/client.js +176 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +226 -0
- package/dist/src/inbound.js +133 -0
- package/dist/src/login.runtime.js +132 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +82 -0
- package/dist/src/outbound.js +181 -0
- package/dist/src/protocol.js +38 -0
- package/dist/src/reply-dispatcher.js +440 -0
- package/dist/src/runtime.js +288 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/tools-schema.js +38 -0
- package/dist/src/tools.js +287 -0
- package/openclaw.plugin.json +21 -0
- package/package.json +27 -5
- package/skills/clawchat-activate/SKILL.md +18 -9
- package/src/buffered-stream.test.ts +10 -0
- package/src/buffered-stream.ts +6 -6
- package/src/channel.outbound.test.ts +3 -3
- package/src/channel.test.ts +7 -1
- package/src/channel.ts +27 -8
- package/src/client.test.ts +8 -1
- package/src/client.ts +11 -10
- package/src/commands.test.ts +6 -0
- package/src/commands.ts +5 -1
- package/src/config.test.ts +47 -0
- package/src/config.ts +28 -5
- package/src/inbound.test.ts +4 -1
- package/src/inbound.ts +11 -10
- package/src/login.runtime.test.ts +36 -0
- package/src/login.runtime.ts +57 -27
- package/src/manifest.test.ts +156 -30
- package/src/outbound.test.ts +6 -5
- package/src/outbound.ts +8 -7
- package/src/plugin-entry.test.ts +7 -1
- package/src/reply-dispatcher.test.ts +418 -3
- package/src/reply-dispatcher.ts +137 -12
- package/src/runtime.ts +1 -0
- package/src/streaming.test.ts +12 -9
- package/src/streaming.ts +6 -6
- package/src/tools.test.ts +81 -18
- package/src/tools.ts +65 -74
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,13 +89,15 @@ 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;
|
|
91
96
|
groupMode?: GroupMode;
|
|
92
97
|
forwardThinking?: boolean;
|
|
93
98
|
forwardToolCalls?: boolean;
|
|
99
|
+
/** Emit approval/action rich fragments instead of plain fallback text. */
|
|
100
|
+
richInteractions?: boolean;
|
|
94
101
|
stream?: OpenclawClawlingStreamConfig;
|
|
95
102
|
reconnect?: OpenclawClawlingReconnectConfig;
|
|
96
103
|
heartbeat?: OpenclawClawlingHeartbeatConfig;
|
|
@@ -111,6 +118,7 @@ export const openclawClawlingConfigSchema = {
|
|
|
111
118
|
groupMode: { type: "string", enum: ["mention", "all"] },
|
|
112
119
|
forwardThinking: { type: "boolean" },
|
|
113
120
|
forwardToolCalls: { type: "boolean" },
|
|
121
|
+
richInteractions: { type: "boolean" },
|
|
114
122
|
stream: {
|
|
115
123
|
type: "object",
|
|
116
124
|
additionalProperties: false,
|
|
@@ -213,6 +221,7 @@ export type ResolvedOpenclawClawlingAccount = {
|
|
|
213
221
|
groupMode: GroupMode;
|
|
214
222
|
forwardThinking: boolean;
|
|
215
223
|
forwardToolCalls: boolean;
|
|
224
|
+
richInteractions: boolean;
|
|
216
225
|
allowFrom: string[];
|
|
217
226
|
stream: Required<OpenclawClawlingStreamConfig>;
|
|
218
227
|
reconnect: Required<OpenclawClawlingReconnectConfig>;
|
|
@@ -230,6 +239,10 @@ function readOptionalString(value: unknown): string {
|
|
|
230
239
|
return typeof value === "string" ? value.trim() : "";
|
|
231
240
|
}
|
|
232
241
|
|
|
242
|
+
function readEnvString(env: Record<string, string | undefined>, key: string): string {
|
|
243
|
+
return readOptionalString(env[key]);
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
function readReplyMode(value: unknown): ReplyMode {
|
|
234
247
|
return value === "stream" ? "stream" : "static";
|
|
235
248
|
}
|
|
@@ -285,13 +298,20 @@ function readAck(raw: unknown): Required<OpenclawClawlingAckConfig> {
|
|
|
285
298
|
|
|
286
299
|
export function resolveOpenclawClawlingAccount(
|
|
287
300
|
cfg: OpenClawConfig,
|
|
301
|
+
env: Record<string, string | undefined> = process.env,
|
|
288
302
|
): ResolvedOpenclawClawlingAccount {
|
|
289
303
|
const channel = readChannelSection(cfg);
|
|
290
304
|
// Apply built-in defaults so login/gateway work without prior setup.
|
|
291
|
-
const websocketUrl =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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);
|
|
295
315
|
const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
296
316
|
const replyMode = readReplyMode(channel.replyMode);
|
|
297
317
|
const groupMode = readGroupMode(channel.groupMode);
|
|
@@ -299,6 +319,8 @@ export function resolveOpenclawClawlingAccount(
|
|
|
299
319
|
typeof channel.forwardThinking === "boolean" ? channel.forwardThinking : true;
|
|
300
320
|
const forwardToolCalls =
|
|
301
321
|
typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
|
|
322
|
+
const richInteractions =
|
|
323
|
+
typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
|
|
302
324
|
|
|
303
325
|
return {
|
|
304
326
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
@@ -313,6 +335,7 @@ export function resolveOpenclawClawlingAccount(
|
|
|
313
335
|
groupMode,
|
|
314
336
|
forwardThinking,
|
|
315
337
|
forwardToolCalls,
|
|
338
|
+
richInteractions,
|
|
316
339
|
allowFrom: [],
|
|
317
340
|
stream: readStream(channel.stream),
|
|
318
341
|
reconnect: readReconnect(channel.reconnect),
|
package/src/inbound.test.ts
CHANGED
|
@@ -41,6 +41,7 @@ function buildSendEnvelope(
|
|
|
41
41
|
mentions: string[];
|
|
42
42
|
reply: unknown;
|
|
43
43
|
messageId: string;
|
|
44
|
+
chatId: string;
|
|
44
45
|
}> = {},
|
|
45
46
|
): Envelope<DownlinkMessageSendPayload> {
|
|
46
47
|
return {
|
|
@@ -48,6 +49,7 @@ function buildSendEnvelope(
|
|
|
48
49
|
event: overrides.event ?? "message.send",
|
|
49
50
|
trace_id: "trace-1",
|
|
50
51
|
emitted_at: 1776162600000,
|
|
52
|
+
chat_id: overrides.chatId,
|
|
51
53
|
to: { id: "agent-1", type: overrides.senderType ?? "direct" },
|
|
52
54
|
sender: {
|
|
53
55
|
sender_id: "user-1",
|
|
@@ -174,7 +176,7 @@ describe("openclaw-clawchat inbound", () => {
|
|
|
174
176
|
},
|
|
175
177
|
};
|
|
176
178
|
await dispatchOpenclawClawlingInbound({
|
|
177
|
-
envelope: buildSendEnvelope({ event: "message.reply", reply: replyRef }),
|
|
179
|
+
envelope: buildSendEnvelope({ event: "message.reply", reply: replyRef, chatId: "chat-1" }),
|
|
178
180
|
cfg: {},
|
|
179
181
|
runtime: {} as never,
|
|
180
182
|
account: baseAccount(),
|
|
@@ -183,6 +185,7 @@ describe("openclaw-clawchat inbound", () => {
|
|
|
183
185
|
const { replyCtx } = ingest.mock.calls[0]![0];
|
|
184
186
|
expect(replyCtx).toEqual({
|
|
185
187
|
replyToMessageId: "m-orig",
|
|
188
|
+
replyPreviewChatId: "chat-1",
|
|
186
189
|
replyPreviewSenderId: "user-2",
|
|
187
190
|
replyPreviewNickName: "User Two",
|
|
188
191
|
replyPreviewText: "original text",
|
package/src/inbound.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface IngestTurnParams {
|
|
|
29
29
|
mediaItems: MediaItem[];
|
|
30
30
|
replyCtx?: {
|
|
31
31
|
replyToMessageId: string;
|
|
32
|
+
replyPreviewChatId: string;
|
|
32
33
|
replyPreviewSenderId: string;
|
|
33
34
|
replyPreviewNickName: string;
|
|
34
35
|
replyPreviewText: string;
|
|
@@ -191,9 +192,19 @@ export async function dispatchOpenclawClawlingInbound(
|
|
|
191
192
|
return;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
log?.info?.(
|
|
196
|
+
`[${account.accountId}] openclaw-clawchat inbound event=${envelope.event === EVENT.MESSAGE_REPLY ? "reply" : "send"} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// New protocol: `chat_id` is the routing primary; `to` is deprecated.
|
|
200
|
+
// Fall back to sender.id if neither is present (defensive).
|
|
201
|
+
const chatId =
|
|
202
|
+
(envelope as Envelope<DownlinkMessageSendPayload> & { chat_id?: string }).chat_id ??
|
|
203
|
+
sender.id;
|
|
194
204
|
const replyCtx = message.context.reply
|
|
195
205
|
? {
|
|
196
206
|
replyToMessageId: message.context.reply.reply_to_msg_id,
|
|
207
|
+
replyPreviewChatId: chatId,
|
|
197
208
|
replyPreviewSenderId:
|
|
198
209
|
message.context.reply.reply_preview.id ??
|
|
199
210
|
message.context.reply.reply_preview.sender_id ??
|
|
@@ -206,16 +217,6 @@ export async function dispatchOpenclawClawlingInbound(
|
|
|
206
217
|
}
|
|
207
218
|
: undefined;
|
|
208
219
|
|
|
209
|
-
log?.info?.(
|
|
210
|
-
`[${account.accountId}] openclaw-clawchat inbound event=${envelope.event === EVENT.MESSAGE_REPLY ? "reply" : "send"} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`,
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// New protocol: `chat_id` is the routing primary; `to` is deprecated.
|
|
214
|
-
// Fall back to sender.id if neither is present (defensive).
|
|
215
|
-
const chatId =
|
|
216
|
-
(envelope as Envelope<DownlinkMessageSendPayload> & { chat_id?: string }).chat_id ??
|
|
217
|
-
sender.id;
|
|
218
|
-
|
|
219
220
|
await params.ingest({
|
|
220
221
|
channel: "openclaw-clawchat",
|
|
221
222
|
accountId: account.accountId,
|
|
@@ -112,6 +112,42 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
112
112
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
it("uses the runtime config mutator with auto reload intent for config writes", async () => {
|
|
116
|
+
const cfg = buildCfg({
|
|
117
|
+
baseUrl: "https://api.example.com",
|
|
118
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
119
|
+
});
|
|
120
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
121
|
+
agent: { user_id: "agent-123", nickname: "Bot" },
|
|
122
|
+
access_token: "access-tok",
|
|
123
|
+
refresh_token: "refresh-tok",
|
|
124
|
+
});
|
|
125
|
+
let mutatedCfg: OpenClawConfig | undefined;
|
|
126
|
+
const mutateConfigFile = vi.fn(async (params) => {
|
|
127
|
+
expect(params.afterWrite).toEqual({ mode: "auto" });
|
|
128
|
+
const draft = structuredClone(cfg) as OpenClawConfig;
|
|
129
|
+
await params.mutate(draft, { snapshot: {} as never, previousHash: "before" });
|
|
130
|
+
mutatedCfg = draft;
|
|
131
|
+
return { nextConfig: draft } as never;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await runOpenclawClawlingLogin({
|
|
135
|
+
cfg,
|
|
136
|
+
runtime: { log: vi.fn() },
|
|
137
|
+
readInviteCode: async () => "INV-ABC",
|
|
138
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
139
|
+
mutateConfigFile,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(mutateConfigFile).toHaveBeenCalledTimes(1);
|
|
143
|
+
const section = (mutatedCfg!.channels as Record<string, Record<string, unknown>>)[
|
|
144
|
+
CHANNEL_ID
|
|
145
|
+
]!;
|
|
146
|
+
expect(section.token).toBe("access-tok");
|
|
147
|
+
expect(section.refreshToken).toBe("refresh-tok");
|
|
148
|
+
expect(section.userId).toBe("agent-123");
|
|
149
|
+
});
|
|
150
|
+
|
|
115
151
|
it("preserves other configured channels when persisting ClawChat credentials", async () => {
|
|
116
152
|
const cfg = {
|
|
117
153
|
channels: {
|
package/src/login.runtime.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createInterface, type Interface as ReadlineInterface } from "node:readline/promises";
|
|
2
|
-
import { writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
|
|
3
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
3
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
5
|
-
import { ClawlingApiError } from "./api-types.ts";
|
|
4
|
+
import { ClawlingApiError, type AgentConnectResult } from "./api-types.ts";
|
|
6
5
|
import {
|
|
7
6
|
CHANNEL_ID,
|
|
8
7
|
mergeOpenclawClawchatToolAllow,
|
|
@@ -20,6 +19,14 @@ export const AGENTS_CONNECT_PLATFORM = "openclaw" as const;
|
|
|
20
19
|
*/
|
|
21
20
|
export const AGENTS_CONNECT_TYPE = "clawbot" as const;
|
|
22
21
|
|
|
22
|
+
export type OpenclawClawchatMutateConfigFile = <T = void>(params: {
|
|
23
|
+
afterWrite: { mode: "auto" } | { mode: "none" | "restart"; reason: string };
|
|
24
|
+
mutate: (
|
|
25
|
+
draft: OpenClawConfig,
|
|
26
|
+
context: { snapshot: unknown; previousHash: string | null },
|
|
27
|
+
) => Promise<T | void> | T | void;
|
|
28
|
+
}) => Promise<unknown>;
|
|
29
|
+
|
|
23
30
|
export interface LoginParams {
|
|
24
31
|
cfg: OpenClawConfig;
|
|
25
32
|
accountId?: string | null;
|
|
@@ -31,7 +38,9 @@ export interface LoginParams {
|
|
|
31
38
|
readInviteCode?: () => Promise<string>;
|
|
32
39
|
/** Override for the HTTP client — used by tests. */
|
|
33
40
|
apiClientFactory?: typeof createOpenclawClawlingApiClient;
|
|
34
|
-
/**
|
|
41
|
+
/** Official runtime config mutator. Production callers must provide this. */
|
|
42
|
+
mutateConfigFile?: OpenclawClawchatMutateConfigFile;
|
|
43
|
+
/** Test-only config persistence override. */
|
|
35
44
|
persistConfig?: (cfg: OpenClawConfig) => Promise<void> | void;
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -60,14 +69,56 @@ async function promptInviteCodeFromStdin(runtime: {
|
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
function buildLoginConfig(cfg: OpenClawConfig, result: AgentConnectResult): OpenClawConfig {
|
|
73
|
+
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
74
|
+
const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
75
|
+
const nextSection: Record<string, unknown> = {
|
|
76
|
+
...existing,
|
|
77
|
+
enabled: true,
|
|
78
|
+
token: result.access_token,
|
|
79
|
+
userId: result.agent.user_id,
|
|
80
|
+
};
|
|
81
|
+
if (result.refresh_token) {
|
|
82
|
+
nextSection.refreshToken = result.refresh_token;
|
|
83
|
+
}
|
|
84
|
+
return mergeOpenclawClawchatToolAllow({
|
|
85
|
+
...cfg,
|
|
86
|
+
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function persistLoginConfig(
|
|
91
|
+
params: LoginParams,
|
|
92
|
+
result: AgentConnectResult,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
if (params.mutateConfigFile) {
|
|
95
|
+
await params.mutateConfigFile({
|
|
96
|
+
afterWrite: { mode: "auto" },
|
|
97
|
+
mutate(draft) {
|
|
98
|
+
Object.assign(draft, buildLoginConfig(draft, result));
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (params.persistConfig) {
|
|
105
|
+
await params.persistConfig(buildLoginConfig(params.cfg, result));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
|
|
110
|
+
}
|
|
111
|
+
|
|
63
112
|
/**
|
|
64
|
-
* 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`:
|
|
65
116
|
* 1. Read the existing channel section; require `baseUrl` to be set so we
|
|
66
117
|
* know which server to hit.
|
|
67
118
|
* 2. Prompt the user for an invite code on stdin.
|
|
68
119
|
* 3. POST it to `${baseUrl}/v1/agents/connect`.
|
|
69
120
|
* 4. Write the returned `websocket_url` / `token` / `user_id` back into
|
|
70
|
-
* the config so subsequent
|
|
121
|
+
* the config so subsequent Gateway runs pick them up.
|
|
71
122
|
*
|
|
72
123
|
* Errors surface with clear messages (missing baseUrl, empty invite,
|
|
73
124
|
* server-side rejection) so the caller can relay them to the operator.
|
|
@@ -115,34 +166,13 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
115
166
|
);
|
|
116
167
|
}
|
|
117
168
|
|
|
118
|
-
// Merge credentials into cfg.channels.openclaw-clawchat and persist
|
|
119
|
-
// immediately so a subsequent `openclaw gateway run` picks them up
|
|
120
|
-
// without any manual edit. `baseUrl` / `websocketUrl` stay untouched —
|
|
121
|
-
// the built-in defaults (or operator overrides) remain authoritative
|
|
122
|
-
// because `/v1/agents/connect` doesn't return them.
|
|
123
|
-
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
124
|
-
const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
125
|
-
const nextSection: Record<string, unknown> = {
|
|
126
|
-
...existing,
|
|
127
|
-
enabled: true,
|
|
128
|
-
token: result.access_token,
|
|
129
|
-
userId: result.agent.user_id,
|
|
130
|
-
};
|
|
131
|
-
if (result.refresh_token) {
|
|
132
|
-
nextSection.refreshToken = result.refresh_token;
|
|
133
|
-
}
|
|
134
|
-
const nextCfg: OpenClawConfig = mergeOpenclawClawchatToolAllow({
|
|
135
|
-
...cfg,
|
|
136
|
-
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
137
|
-
});
|
|
138
|
-
|
|
139
169
|
const tokenPreview = redactToken(result.access_token);
|
|
140
170
|
runtime.log(
|
|
141
171
|
`Updating config: channels.${CHANNEL_ID}.token=${tokenPreview} userId=${result.agent.user_id}${
|
|
142
172
|
result.refresh_token ? " refreshToken=***" : ""
|
|
143
173
|
} …`,
|
|
144
174
|
);
|
|
145
|
-
await (params
|
|
175
|
+
await persistLoginConfig(params, result);
|
|
146
176
|
runtime.log(`Config file updated.`);
|
|
147
177
|
|
|
148
178
|
runtime.log(
|
package/src/manifest.test.ts
CHANGED
|
@@ -6,11 +6,24 @@ import packageJson from "../package.json" with { type: "json" };
|
|
|
6
6
|
interface PackageJsonWithOpenclaw {
|
|
7
7
|
name: string;
|
|
8
8
|
files: string[];
|
|
9
|
+
scripts: Record<string, string>;
|
|
10
|
+
devDependencies: Record<string, string>;
|
|
11
|
+
peerDependencies: Record<string, string>;
|
|
9
12
|
openclaw: {
|
|
10
13
|
extensions: string[];
|
|
14
|
+
runtimeExtensions?: string[];
|
|
11
15
|
setupEntry?: string;
|
|
12
|
-
channel?: {
|
|
13
|
-
|
|
16
|
+
channel?: {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
selectionLabel?: string;
|
|
20
|
+
docsPath?: string;
|
|
21
|
+
docsLabel?: string;
|
|
22
|
+
blurb: string;
|
|
23
|
+
order?: number;
|
|
24
|
+
aliases?: string[];
|
|
25
|
+
};
|
|
26
|
+
install: { npmSpec: string; minHostVersion: string };
|
|
14
27
|
};
|
|
15
28
|
}
|
|
16
29
|
|
|
@@ -31,32 +44,73 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
31
44
|
expect(pkg.openclaw.install.npmSpec).toBe("@newbase-clawchat/openclaw-clawchat");
|
|
32
45
|
});
|
|
33
46
|
|
|
47
|
+
it("requires an OpenClaw host with runtime config mutation support", () => {
|
|
48
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
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
|
+
});
|
|
53
|
+
|
|
54
|
+
it("publishes compiled runtime entrypoints for npm plugin installs", () => {
|
|
55
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
56
|
+
expect(pkg.openclaw.extensions).toEqual(["./index.ts"]);
|
|
57
|
+
expect(pkg.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
|
|
58
|
+
expect(pkg.files).toContain("dist");
|
|
59
|
+
expect(pkg.scripts.build).toBe("tsc -p tsconfig.build.json");
|
|
60
|
+
expect(pkg.scripts.prepack).toBe("npm run build");
|
|
61
|
+
expect(fs.existsSync(new URL("../tsconfig.build.json", import.meta.url))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("publishes channel catalog metadata for OpenClaw CLI discovery", () => {
|
|
65
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
66
|
+
expect(pkg.openclaw.channel).toEqual({
|
|
67
|
+
id: "openclaw-clawchat",
|
|
68
|
+
label: "Clawling Chat",
|
|
69
|
+
selectionLabel: "Clawling Chat",
|
|
70
|
+
docsPath: "/channels/openclaw-clawchat",
|
|
71
|
+
docsLabel: "openclaw-clawchat",
|
|
72
|
+
blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
73
|
+
order: 110,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
34
77
|
it("declares supported channel/command activation hints for plugin loading", () => {
|
|
35
78
|
expect(pluginManifest.activation).toEqual({
|
|
79
|
+
onStartup: true,
|
|
36
80
|
onChannels: ["openclaw-clawchat"],
|
|
37
81
|
onCommands: ["clawchat-login"],
|
|
38
82
|
});
|
|
39
|
-
expect(pluginManifest.activation).not.toHaveProperty("onStartup");
|
|
40
83
|
expect(pluginManifest.commandAliases).toEqual([
|
|
41
84
|
{ name: "clawchat-login", kind: "runtime-slash" },
|
|
42
85
|
]);
|
|
43
86
|
});
|
|
44
87
|
|
|
45
|
-
it("
|
|
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
|
+
|
|
100
|
+
it("does not publish setup migration or setup-runtime entry metadata", () => {
|
|
46
101
|
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
47
102
|
expect(pkg.files).not.toContain("setup-api.ts");
|
|
48
103
|
expect(pkg.files).not.toContain("setup-entry.ts");
|
|
49
104
|
expect(pkg.openclaw.setupEntry).toBeUndefined();
|
|
50
|
-
expect(pkg.openclaw.channel).toBeUndefined();
|
|
51
105
|
expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
|
|
52
106
|
expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
|
|
53
107
|
});
|
|
54
108
|
|
|
55
|
-
it("
|
|
109
|
+
it("documents channels add --token as the first-time CLI activation path", () => {
|
|
56
110
|
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
57
111
|
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
58
|
-
expect(readme).
|
|
59
|
-
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);
|
|
60
114
|
});
|
|
61
115
|
|
|
62
116
|
it("publishes a ClawChat account tools skill for non-activation workflows", () => {
|
|
@@ -73,52 +127,124 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
73
127
|
expect(skill).not.toMatch(/clawchat_activate/);
|
|
74
128
|
});
|
|
75
129
|
|
|
76
|
-
it("
|
|
130
|
+
it("declares ownership of registered ClawChat agent tools", () => {
|
|
131
|
+
expect(pluginManifest.contracts?.tools).toEqual([
|
|
132
|
+
"clawchat_activate",
|
|
133
|
+
"clawchat_get_account_profile",
|
|
134
|
+
"clawchat_get_user_profile",
|
|
135
|
+
"clawchat_list_account_friends",
|
|
136
|
+
"clawchat_update_account_profile",
|
|
137
|
+
"clawchat_upload_avatar_image",
|
|
138
|
+
"clawchat_upload_media_file",
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("keeps the optional OpenClaw source checkout local-only", () => {
|
|
143
|
+
expect(fs.existsSync(new URL("../.gitmodules", import.meta.url))).toBe(false);
|
|
144
|
+
|
|
145
|
+
const gitignore = fs.readFileSync(new URL("../.gitignore", import.meta.url), "utf8");
|
|
146
|
+
expect(gitignore).toMatch(/^tmp\/openclaw\/$/m);
|
|
147
|
+
|
|
148
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
149
|
+
expect(pkg.scripts["dev:openclaw-source"]).toBe(
|
|
150
|
+
"test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
154
|
+
expect(readme).toMatch(/npm run dev:openclaw-source/);
|
|
155
|
+
expect(readme).toMatch(
|
|
156
|
+
/git clone --depth=1 https:\/\/github\.com\/openclaw\/openclaw\.git tmp\/openclaw/,
|
|
157
|
+
);
|
|
158
|
+
expect(readme).toMatch(/local-only/i);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("keeps default Vitest discovery scoped to plugin sources", () => {
|
|
162
|
+
const configUrl = new URL("../vitest.config.ts", import.meta.url);
|
|
163
|
+
expect(fs.existsSync(configUrl)).toBe(true);
|
|
164
|
+
const config = fs.readFileSync(configUrl, "utf8");
|
|
165
|
+
expect(config).toMatch(/include:\s*\["src\/\*\*\/\*\.test\.ts"\]/);
|
|
166
|
+
expect(config).toMatch(/"tmp\/\*\*"/);
|
|
167
|
+
expect(config).toMatch(/"\.e2e\/\*\*"/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("keeps the activation skill on clawchat_activate with channels-add fallback", () => {
|
|
77
171
|
const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
|
|
78
172
|
expect(skill).toMatch(/name:\s*clawchat-activate/);
|
|
79
|
-
expect(skill).
|
|
80
|
-
expect(skill).not.toMatch(/command-dispatch:\s*tool/);
|
|
81
|
-
expect(skill).not.toMatch(/command-tool:/);
|
|
82
|
-
expect(skill).not.toMatch(/command-dispatch:/);
|
|
83
|
-
expect(skill).not.toMatch(/command-command:/);
|
|
84
|
-
expect(skill).not.toMatch(/command-arg-mode:/);
|
|
85
|
-
expect(skill).not.toMatch(/user-invocable:/);
|
|
173
|
+
expect(skill).toMatch(/clawchat_activate/);
|
|
86
174
|
expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
|
|
87
175
|
expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
|
|
88
176
|
expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
|
|
89
177
|
expect(skill).not.toMatch(/\/clawchat-activate A1B2C3/);
|
|
90
178
|
expect(skill).not.toMatch(/\/clawchat-login A1B2C3/);
|
|
91
|
-
expect(skill).toMatch(/openclaw channels
|
|
92
|
-
expect(skill).toMatch(/
|
|
93
|
-
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);
|
|
94
182
|
expect(skill).toMatch(/channel login/i);
|
|
183
|
+
expect(skill).toMatch(/openclaw channels status --probe/);
|
|
95
184
|
expect(skill).toMatch(/openclaw gateway restart/);
|
|
96
185
|
expect(skill).not.toMatch(/ask the user to send/i);
|
|
97
186
|
expect(skill).not.toMatch(/give the exact/i);
|
|
98
|
-
expect(skill).toMatch(/
|
|
99
|
-
expect(skill).toMatch(/execute[^\n]+openclaw gateway restart/i);
|
|
187
|
+
expect(skill).toMatch(/restart[^\n]+only when/i);
|
|
100
188
|
});
|
|
101
189
|
|
|
102
|
-
it("documents
|
|
190
|
+
it("documents clawchat_activate as the natural-language activation path with channels-add CLI fallback", () => {
|
|
103
191
|
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
104
192
|
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
105
|
-
expect(readme).toMatch(/
|
|
106
|
-
expect(docs).toMatch(/
|
|
193
|
+
expect(readme).toMatch(/clawchat_activate/i);
|
|
194
|
+
expect(docs).toMatch(/clawchat_activate/i);
|
|
195
|
+
expect(readme).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
|
|
196
|
+
expect(docs).toMatch(/openclaw channels add --channel openclaw-clawchat --token/i);
|
|
197
|
+
expect(readme).toMatch(/openclaw channels status --probe/i);
|
|
198
|
+
expect(docs).toMatch(/openclaw channels status --probe/i);
|
|
107
199
|
expect(readme).toMatch(/openclaw gateway restart/i);
|
|
108
200
|
expect(docs).toMatch(/openclaw gateway restart/i);
|
|
109
201
|
expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
|
|
110
202
|
expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
|
|
111
203
|
expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
112
204
|
expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
113
|
-
expect(readme).not.toMatch(/clawchat_activate/);
|
|
114
|
-
expect(docs).not.toMatch(/clawchat_activate/);
|
|
115
205
|
expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
116
206
|
expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
117
207
|
expect(readme).not.toMatch(/direct users to/i);
|
|
118
208
|
expect(docs).not.toMatch(/direct the\s+user/i);
|
|
119
|
-
expect(
|
|
120
|
-
expect(docs).
|
|
121
|
-
|
|
122
|
-
|
|
209
|
+
expect(readme).toMatch(/activation skill calls/i);
|
|
210
|
+
expect(docs).toMatch(/Natural-language activation requests should call `clawchat_activate`/i);
|
|
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);
|
|
123
249
|
});
|
|
124
250
|
});
|
package/src/outbound.test.ts
CHANGED
|
@@ -65,9 +65,9 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
65
65
|
});
|
|
66
66
|
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
|
67
67
|
chat_id: "user-1",
|
|
68
|
-
chat_type: "direct",
|
|
69
68
|
body: { fragments: [{ kind: "text", text: "hello" }] },
|
|
70
69
|
});
|
|
70
|
+
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
71
71
|
expect(client.replyMessage).not.toHaveBeenCalled();
|
|
72
72
|
expect(result?.messageId).toBe("server-m1");
|
|
73
73
|
expect(result?.acceptedAt).toBe(1234);
|
|
@@ -78,26 +78,27 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
78
78
|
await sendOpenclawClawlingText({
|
|
79
79
|
client,
|
|
80
80
|
account: baseAccount(),
|
|
81
|
-
to: { chatId: "
|
|
81
|
+
to: { chatId: "chat-1", chatType: "direct" },
|
|
82
82
|
text: "reply",
|
|
83
83
|
replyCtx: {
|
|
84
84
|
replyToMessageId: "m-orig",
|
|
85
|
+
replyPreviewChatId: "chat-1",
|
|
85
86
|
replyPreviewSenderId: "user-2",
|
|
86
87
|
replyPreviewNickName: "Sender",
|
|
87
88
|
replyPreviewText: "original",
|
|
88
89
|
},
|
|
89
90
|
});
|
|
90
91
|
expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
|
91
|
-
chat_id: "
|
|
92
|
-
chat_type: "direct",
|
|
92
|
+
chat_id: "chat-1",
|
|
93
93
|
replyTo: {
|
|
94
94
|
msgId: "m-orig",
|
|
95
|
-
senderId: "
|
|
95
|
+
senderId: "chat-1",
|
|
96
96
|
nickName: "Sender",
|
|
97
97
|
fragments: [{ kind: "text", text: "original" }],
|
|
98
98
|
},
|
|
99
99
|
body: { fragments: [{ kind: "text", text: "reply" }] },
|
|
100
100
|
});
|
|
101
|
+
expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
101
102
|
expect(client.sendMessage).not.toHaveBeenCalled();
|
|
102
103
|
});
|
|
103
104
|
|