@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +39 -17
  2. package/dist/index.js +3 -1
  3. package/dist/src/api-client.js +71 -12
  4. package/dist/src/api-types.test-d.js +10 -0
  5. package/dist/src/channel.js +5 -5
  6. package/dist/src/channel.setup.js +4 -17
  7. package/dist/src/clawchat-memory.js +290 -0
  8. package/dist/src/clawchat-metadata.js +235 -0
  9. package/dist/src/client.js +31 -93
  10. package/dist/src/commands.js +3 -3
  11. package/dist/src/config.js +58 -3
  12. package/dist/src/group-message-coalescer.js +107 -0
  13. package/dist/src/inbound.js +24 -28
  14. package/dist/src/login.runtime.js +82 -19
  15. package/dist/src/media-runtime.js +2 -3
  16. package/dist/src/message-mapper.js +1 -1
  17. package/dist/src/mock-transport.js +31 -0
  18. package/dist/src/outbound.js +281 -56
  19. package/dist/src/plugin-prompts.js +76 -0
  20. package/dist/src/profile-prompt.js +150 -0
  21. package/dist/src/profile-sync.js +169 -0
  22. package/dist/src/prompt-injection.js +25 -0
  23. package/dist/src/protocol-types.js +63 -0
  24. package/dist/src/protocol-types.typecheck.js +1 -0
  25. package/dist/src/protocol.js +2 -2
  26. package/dist/src/reply-dispatcher.js +143 -40
  27. package/dist/src/runtime.js +813 -109
  28. package/dist/src/storage.js +636 -0
  29. package/dist/src/tools-schema.js +70 -10
  30. package/dist/src/tools.js +600 -112
  31. package/dist/src/ws-alignment.js +8 -0
  32. package/dist/src/ws-client.js +588 -0
  33. package/index.ts +6 -1
  34. package/openclaw.plugin.json +44 -4
  35. package/package.json +4 -3
  36. package/prompts/platform.md +7 -0
  37. package/skills/clawchat/SKILL.md +90 -0
  38. package/src/api-client.test.ts +360 -15
  39. package/src/api-client.ts +127 -25
  40. package/src/api-types.test-d.ts +12 -0
  41. package/src/api-types.ts +71 -4
  42. package/src/buffered-stream.test.ts +1 -1
  43. package/src/buffered-stream.ts +1 -1
  44. package/src/channel.outbound.test.ts +270 -60
  45. package/src/channel.setup.ts +9 -18
  46. package/src/channel.test.ts +33 -25
  47. package/src/channel.ts +5 -7
  48. package/src/clawchat-memory.test.ts +372 -0
  49. package/src/clawchat-memory.ts +363 -0
  50. package/src/clawchat-metadata.test.ts +350 -0
  51. package/src/clawchat-metadata.ts +352 -0
  52. package/src/client.test.ts +57 -48
  53. package/src/client.ts +37 -129
  54. package/src/commands.test.ts +2 -2
  55. package/src/commands.ts +3 -3
  56. package/src/config.test.ts +169 -4
  57. package/src/config.ts +86 -6
  58. package/src/group-message-coalescer.test.ts +223 -0
  59. package/src/group-message-coalescer.ts +154 -0
  60. package/src/inbound.test.ts +106 -19
  61. package/src/inbound.ts +31 -35
  62. package/src/login.runtime.test.ts +294 -11
  63. package/src/login.runtime.ts +90 -21
  64. package/src/manifest.test.ts +86 -14
  65. package/src/media-runtime.test.ts +31 -2
  66. package/src/media-runtime.ts +7 -10
  67. package/src/message-mapper.test.ts +2 -2
  68. package/src/message-mapper.ts +2 -2
  69. package/src/mock-transport.test.ts +35 -0
  70. package/src/mock-transport.ts +38 -0
  71. package/src/outbound.test.ts +811 -95
  72. package/src/outbound.ts +332 -65
  73. package/src/plugin-entry.test.ts +3 -1
  74. package/src/plugin-prompts.test.ts +78 -0
  75. package/src/plugin-prompts.ts +92 -0
  76. package/src/profile-prompt.test.ts +435 -0
  77. package/src/profile-prompt.ts +208 -0
  78. package/src/profile-sync.test.ts +611 -0
  79. package/src/profile-sync.ts +268 -0
  80. package/src/prompt-injection.test.ts +39 -0
  81. package/src/prompt-injection.ts +45 -0
  82. package/src/protocol-types.test.ts +69 -0
  83. package/src/protocol-types.ts +296 -0
  84. package/src/protocol-types.typecheck.ts +89 -0
  85. package/src/protocol.ts +2 -2
  86. package/src/reply-dispatcher.test.ts +720 -135
  87. package/src/reply-dispatcher.ts +174 -42
  88. package/src/runtime.test.ts +3884 -337
  89. package/src/runtime.ts +956 -128
  90. package/src/storage.test.ts +692 -0
  91. package/src/storage.ts +989 -0
  92. package/src/streaming.test.ts +1 -1
  93. package/src/streaming.ts +1 -1
  94. package/src/tools-schema.ts +115 -13
  95. package/src/tools.test.ts +501 -10
  96. package/src/tools.ts +739 -133
  97. package/src/ws-alignment.ts +9 -0
  98. package/src/ws-client.test.ts +1218 -0
  99. package/src/ws-client.ts +662 -0
@@ -1,6 +1,7 @@
1
- import { MockTransport, type ClawlingChatClient } from "@newbase-clawchat/sdk";
2
1
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
3
2
  import { describe, expect, it, vi } from "vitest";
3
+ import { MockTransport } from "./mock-transport.ts";
4
+ import type { ClawlingChatClient } from "./ws-client.ts";
4
5
  import {
5
6
  createOpenclawClawlingClient,
6
7
  emitStreamCreated,
@@ -21,6 +22,7 @@ function baseAccount(): ResolvedOpenclawClawlingAccount {
21
22
  baseUrl: "",
22
23
  token: "t",
23
24
  userId: "agent-1",
25
+ ownerUserId: "owner-1",
24
26
  replyMode: "static",
25
27
  forwardThinking: true,
26
28
  forwardToolCalls: false,
@@ -38,11 +40,7 @@ function baseAccount(): ResolvedOpenclawClawlingAccount {
38
40
  }
39
41
 
40
42
  async function authenticate(transport: MockTransport) {
41
- // Yield to allow MockTransport.connect's internal Promise.resolve() to fire
42
- // onOpen (which transitions state: connecting -> challenging) before we send
43
- // the challenge envelope.
44
43
  await Promise.resolve();
45
- // Challenge from server
46
44
  transport.emitInbound(
47
45
  JSON.stringify({
48
46
  version: "2",
@@ -52,12 +50,14 @@ async function authenticate(transport: MockTransport) {
52
50
  payload: { nonce: "nonce-1" },
53
51
  }),
54
52
  );
55
- // hello-ok
53
+ const connectFrame = transport.sent
54
+ .map((raw) => JSON.parse(raw))
55
+ .find((env) => env.event === "connect");
56
56
  transport.emitInbound(
57
57
  JSON.stringify({
58
58
  version: "2",
59
59
  event: "hello-ok",
60
- trace_id: "t-hello",
60
+ trace_id: connectFrame.trace_id,
61
61
  emitted_at: Date.now(),
62
62
  payload: {},
63
63
  }),
@@ -116,51 +116,60 @@ describe("openclaw-clawchat client", () => {
116
116
  client.close();
117
117
  });
118
118
 
119
- it("wraps the production default transport so connect frames cannot include signature fields", () => {
120
- const client = createOpenclawClawlingClient(baseAccount());
121
- const transport = (client as unknown as {
122
- opts?: {
123
- transport?: {
124
- send: (wire: string) => void;
125
- __openclawInnerTransport?: { send: (wire: string) => void };
126
- };
127
- };
128
- }).opts?.transport;
129
- const inner = transport?.__openclawInnerTransport;
130
- expect(inner).toBeTruthy();
131
-
132
- const sent: unknown[] = [];
133
- inner!.send = (wire: string) => {
134
- sent.push(JSON.parse(wire));
135
- };
136
-
137
- transport!.send(
119
+ it("reports connect frames after the transport records them", async () => {
120
+ const transport = new MockTransport();
121
+ const onConnectFrameSent = vi.fn((env: { trace_id?: unknown }) => {
122
+ const sentConnect = transport.sent
123
+ .map((raw) => JSON.parse(raw))
124
+ .find((frame) => frame.event === "connect");
125
+ expect(sentConnect?.trace_id).toBe(env.trace_id);
126
+ });
127
+ const client = createOpenclawClawlingClient(baseAccount(), {
128
+ transport,
129
+ wsLifecycle: { onConnectFrameSent },
130
+ });
131
+
132
+ const connected = client.connect();
133
+ await authenticate(transport);
134
+ await connected;
135
+
136
+ expect(onConnectFrameSent).toHaveBeenCalledTimes(1);
137
+ client.close();
138
+ });
139
+
140
+ it("reports connect frames only after they are sent", async () => {
141
+ const transport = new MockTransport();
142
+ const onConnectFrameSent = vi.fn();
143
+ const originalSend = transport.send.bind(transport);
144
+ transport.send = vi.fn((wire: string) => {
145
+ const env = JSON.parse(wire);
146
+ if (env.event === "connect") throw new Error("send failed");
147
+ originalSend(wire);
148
+ });
149
+ const client = createOpenclawClawlingClient(baseAccount(), {
150
+ transport,
151
+ wsLifecycle: { onConnectFrameSent },
152
+ });
153
+
154
+ const connected = client.connect();
155
+ await Promise.resolve();
156
+ transport.emitInbound(
138
157
  JSON.stringify({
139
158
  version: "2",
140
- event: "connect",
141
- trace_id: "trace-connect",
159
+ event: "connect.challenge",
160
+ trace_id: "challenge-1",
142
161
  emitted_at: Date.now(),
143
- payload: {
144
- token: "old-token",
145
- nonce: "nonce-1",
146
- signature: "legacy-signature",
147
- client_id: "legacy-client",
148
- client_version: "legacy-version",
149
- },
162
+ payload: { nonce: "nonce-1" },
150
163
  }),
151
164
  );
152
165
 
153
- expect(sent).toHaveLength(1);
154
- const connectFrame = sent[0] as { payload: Record<string, unknown> };
155
- expect(connectFrame.payload).toEqual({
156
- token: "t",
157
- nonce: "nonce-1",
158
- device_id: "agent-1",
159
- capabilities: {
160
- multi_device: true,
161
- device_replay: true,
162
- },
163
- });
166
+ await expect(connected).rejects.toThrow(/send failed/);
167
+ expect(onConnectFrameSent).not.toHaveBeenCalled();
168
+ });
169
+
170
+ it("creates a local production transport when no override is supplied", () => {
171
+ const client = createOpenclawClawlingClient(baseAccount());
172
+ expect(client.transportState).toBe("closed");
164
173
  });
165
174
 
166
175
  it("emitStreamCreated emits a minimal message.created envelope with just message_id", async () => {
@@ -189,7 +198,7 @@ describe("openclaw-clawchat client", () => {
189
198
  client.close();
190
199
  });
191
200
 
192
- it("uses SDK emitRaw with chat_id routing for stream lifecycle frames", () => {
201
+ it("uses local emitRaw with chat_id routing for stream lifecycle frames", () => {
193
202
  const transportSend = vi.fn();
194
203
  const emitRaw = vi.fn();
195
204
  const client = {
@@ -213,7 +222,7 @@ describe("openclaw-clawchat client", () => {
213
222
  expect(transportSend).not.toHaveBeenCalled();
214
223
  });
215
224
 
216
- it("falls back to SDK emitRaw with chat_id routing when raw transport is unavailable", () => {
225
+ it("uses local emitRaw with chat_id routing when raw transport is unavailable", () => {
217
226
  const emitRaw = vi.fn();
218
227
  const client = { emitRaw } as unknown as ClawlingChatClient;
219
228
 
package/src/client.ts CHANGED
@@ -1,16 +1,12 @@
1
- import {
2
- createWSClient,
3
- type ClawlingChatClient,
4
- type CreateWSClientOptions,
5
- type Fragment,
6
- type Transport,
7
- } from "@newbase-clawchat/sdk";
1
+ import type { ChatType, Envelope, Fragment, Transport } from "./protocol-types.ts";
2
+ import { createClawChatClient, type ClawlingChatClient } from "./ws-client.ts";
8
3
  import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
9
4
 
5
+ export type { ChatType } from "./protocol-types.ts";
6
+
10
7
  export interface CreateClientOverrides {
11
8
  /** Transport override — only intended for tests (e.g. MockTransport). */
12
9
  transport?: Transport;
13
- logger?: CreateWSClientOptions["logger"];
14
10
  wsLifecycle?: {
15
11
  onConnectFrameSent?: (env: {
16
12
  trace_id?: unknown;
@@ -19,99 +15,22 @@ export interface CreateClientOverrides {
19
15
  };
20
16
  }
21
17
 
22
- export interface MsghubConnectTransport extends Transport {
23
- __openclawInnerTransport: Transport;
24
- }
25
-
26
- function createMsghubConnectTransport(
27
- account: ResolvedOpenclawClawlingAccount,
28
- transport: Transport,
29
- lifecycle?: CreateClientOverrides["wsLifecycle"],
30
- ): MsghubConnectTransport {
31
- const maybeWrapped = transport as Partial<MsghubConnectTransport>;
32
- if (maybeWrapped.__openclawInnerTransport) return transport as MsghubConnectTransport;
33
-
34
- const wrapped: MsghubConnectTransport = {
35
- __openclawInnerTransport: transport,
36
- get state() {
37
- return transport.state;
38
- },
39
- connect(url, handlers) {
40
- return transport.connect(url, handlers);
41
- },
42
- send(data) {
43
- let parsed: unknown;
44
- try {
45
- parsed = JSON.parse(data);
46
- } catch {
47
- transport.send(data);
48
- return;
49
- }
50
-
51
- if (
52
- !parsed ||
53
- typeof parsed !== "object" ||
54
- (parsed as { event?: unknown }).event !== "connect"
55
- ) {
56
- transport.send(data);
57
- return;
58
- }
59
-
60
- const env = parsed as {
61
- payload?: { nonce?: unknown };
62
- [key: string]: unknown;
63
- };
64
- const nonce = typeof env.payload?.nonce === "string" ? env.payload.nonce : "";
65
- const payload = {
66
- token: account.token,
67
- nonce,
68
- ...(account.userId ? { device_id: account.userId } : {}),
69
- capabilities: {
70
- multi_device: true,
71
- device_replay: true,
72
- },
73
- };
74
- const connectEnv = { ...env, payload };
75
- transport.send(JSON.stringify(connectEnv));
76
- lifecycle?.onConnectFrameSent?.(connectEnv);
77
- },
78
- close(code, reason) {
79
- transport.close(code, reason);
80
- },
81
- };
82
- return wrapped;
83
- }
84
-
85
- function installMsghubConnectTransport(
86
- account: ResolvedOpenclawClawlingAccount,
87
- client: ClawlingChatClient,
88
- lifecycle?: CreateClientOverrides["wsLifecycle"],
89
- ): void {
90
- const inner = client as unknown as { opts?: { transport?: Transport } };
91
- if (!inner.opts?.transport) return;
92
- inner.opts.transport = createMsghubConnectTransport(account, inner.opts.transport, lifecycle);
93
- }
94
-
95
18
  export function createOpenclawClawlingClient(
96
19
  account: ResolvedOpenclawClawlingAccount,
97
20
  overrides: CreateClientOverrides = {},
98
21
  ): ClawlingChatClient {
99
- // Only forward a finite `maxRetries` to the SDK — the SDK's own default
100
- // is already unbounded, so omitting the field keeps that behavior. This
101
- // avoids forcing the SDK to special-case `Infinity`.
102
- const maxRetries = account.reconnect.maxRetries;
103
- const reconnect: CreateWSClientOptions["reconnect"] = {
104
- enabled: true,
105
- initialDelay: account.reconnect.initialDelay,
106
- maxDelay: account.reconnect.maxDelay,
107
- jitterRatio: account.reconnect.jitterRatio,
108
- ...(Number.isFinite(maxRetries) ? { maxRetries } : {}),
109
- };
110
-
111
- const options: CreateWSClientOptions = {
22
+ const client = createClawChatClient({
112
23
  url: account.websocketUrl,
113
24
  token: account.token,
114
- reconnect,
25
+ deviceId: account.userId,
26
+ ...(overrides.transport ? { transport: overrides.transport } : {}),
27
+ reconnect: {
28
+ enabled: true,
29
+ initialDelay: account.reconnect.initialDelay,
30
+ maxDelay: account.reconnect.maxDelay,
31
+ jitterRatio: account.reconnect.jitterRatio,
32
+ maxRetries: account.reconnect.maxRetries,
33
+ },
115
34
  heartbeat: {
116
35
  enabled: true,
117
36
  interval: account.heartbeat.interval,
@@ -121,19 +40,21 @@ export function createOpenclawClawlingClient(
121
40
  timeout: account.ack.timeout,
122
41
  autoResendOnTimeout: account.ack.autoResendOnTimeout,
123
42
  },
124
- // Buffer outbound sends during the tiny reconnect window so an inbound
125
- // message isn't silently dropped while the socket is flapping.
126
- queueWhileReconnecting: true,
127
- ...(overrides.transport ? { transport: overrides.transport } : {}),
128
- ...(overrides.logger ? { logger: overrides.logger } : {}),
129
- };
130
- const client = createWSClient(options);
131
- installMsghubConnectTransport(account, client, overrides.wsLifecycle);
43
+ });
44
+ if (overrides.wsLifecycle?.onConnectFrameSent) {
45
+ const sendRawEnvelope = client.sendRawEnvelope.bind(client);
46
+ client.sendRawEnvelope = (env: Envelope) => {
47
+ sendRawEnvelope(env);
48
+ if (env.event === "connect") {
49
+ overrides.wsLifecycle?.onConnectFrameSent?.(
50
+ env as { trace_id?: unknown; payload?: { device_id?: unknown } },
51
+ );
52
+ }
53
+ };
54
+ }
132
55
  return client;
133
56
  }
134
57
 
135
- export type ChatType = "direct" | "group";
136
-
137
58
  export interface StreamSender {
138
59
  id: string;
139
60
  type: "direct";
@@ -157,8 +78,8 @@ function normalizeRouting(params: {
157
78
  }
158
79
 
159
80
  /**
160
- * Emit a raw v2 envelope directly over the transport so we can carry top-level
161
- * `chat_id` routing without SDK-injected `to` metadata.
81
+ * Emit a raw v2 envelope through the local client so stream helpers carry
82
+ * top-level `chat_id` routing without legacy `to` metadata.
162
83
  */
163
84
  function emitEnvelope(
164
85
  client: ClawlingChatClient,
@@ -167,33 +88,22 @@ function emitEnvelope(
167
88
  routing: EnvelopeRouting,
168
89
  options: { forceRawTransport?: boolean } = {},
169
90
  ): void {
170
- const inner = client as unknown as {
171
- opts?: {
172
- transport: { send: (data: string) => void };
173
- traceIdFactory: () => string;
174
- };
175
- emitRaw?: (event: string, payload: object, routing?: { chat_id?: string; chat_type?: ChatType }) => void;
176
- };
177
- if (!options.forceRawTransport && inner.emitRaw) {
178
- inner.emitRaw(event, payload, { chat_id: routing.chatId });
91
+ if (!options.forceRawTransport) {
92
+ client.emitRaw(event, payload, { chat_id: routing.chatId });
179
93
  return;
180
94
  }
181
- if (!inner.opts?.transport) {
182
- if (!options.forceRawTransport && inner.emitRaw) {
183
- inner.emitRaw(event, payload, { chat_id: routing.chatId });
184
- return;
185
- }
186
- throw new Error("openclaw-clawchat streaming emit requires SDK raw transport");
95
+ if (typeof client.nextTraceId !== "function" || typeof client.sendRawEnvelope !== "function") {
96
+ throw new Error("openclaw-clawchat streaming emit requires local raw transport");
187
97
  }
188
- const env = {
98
+ const env: Envelope = {
189
99
  version: "2" as const,
190
100
  event,
191
- trace_id: inner.opts.traceIdFactory(),
101
+ trace_id: client.nextTraceId(),
192
102
  emitted_at: Date.now(),
193
103
  chat_id: routing.chatId,
194
104
  payload,
195
105
  };
196
- inner.opts.transport.send(JSON.stringify(env));
106
+ client.sendRawEnvelope(env);
197
107
  }
198
108
 
199
109
  /**
@@ -308,10 +218,8 @@ export function emitStreamDone(
308
218
  * the same `payload.message_id` as the preceding `message.created` /
309
219
  * `message.add` / `message.done` frames.
310
220
  *
311
- * The SDK's high-level `client.replyMessage()` disallows `payload.message_id`
312
- * on outbound replies (the server normally assigns one via ack); for the
313
- * streaming-finalize use case the backend expects the correlated id, so we
314
- * bypass the SDK validator and write directly to the transport.
221
+ * Final stream replies include the correlated `payload.message_id`, so they
222
+ * use the local raw-envelope API instead of any higher-level ackable send.
315
223
  */
316
224
  export function emitFinalStreamReply(
317
225
  client: ClawlingChatClient,
@@ -8,7 +8,7 @@ const loginRuntime = vi.hoisted(() => ({
8
8
  vi.mock("./login.runtime.ts", () => loginRuntime);
9
9
 
10
10
  describe("registerOpenclawClawlingCommands", () => {
11
- it("registers a distinct slash login command that passes the invite code to login", async () => {
11
+ it("registers a distinct slash activate command that passes the invite code to login", async () => {
12
12
  loginRuntime.runOpenclawClawlingLogin.mockResolvedValue(undefined);
13
13
  const commands: Array<{ name: string; acceptsArgs?: boolean; handler: (ctx: unknown) => Promise<{ text: string }> }> = [];
14
14
  const api = {
@@ -22,7 +22,7 @@ describe("registerOpenclawClawlingCommands", () => {
22
22
 
23
23
  registerOpenclawClawlingCommands(api);
24
24
 
25
- expect(commands.map((command) => command.name)).toEqual(["clawchat-login"]);
25
+ expect(commands.map((command) => command.name)).toEqual(["clawchat-activate"]);
26
26
  expect(commands[0]?.acceptsArgs).toBe(true);
27
27
 
28
28
  const result = await commands[0]!.handler({
package/src/commands.ts CHANGED
@@ -12,14 +12,14 @@ function errorMessage(err: unknown): string {
12
12
 
13
13
  export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "registerCommand" | "logger" | "runtime">): void {
14
14
  api.registerCommand({
15
- name: "clawchat-login",
16
- description: "Activate ClawChat with an invite code, e.g. /clawchat-login A1B2C3.",
15
+ name: "clawchat-activate",
16
+ description: "Activate ClawChat with an invite code, e.g. /clawchat-activate A1B2C3.",
17
17
  acceptsArgs: true,
18
18
  requireAuth: true,
19
19
  async handler(ctx) {
20
20
  const code = extractInviteCode(ctx.args ?? ctx.commandBody);
21
21
  if (!code) {
22
- return { text: "ClawChat invite code is required. Usage: /clawchat-login A1B2C3" };
22
+ return { text: "ClawChat invite code is required. Usage: /clawchat-activate A1B2C3" };
23
23
  }
24
24
  try {
25
25
  const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");