@rethinkingstudio/clawpilot 2.0.0-beta.0 → 2.0.0

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.
@@ -1,328 +0,0 @@
1
- import { WebSocket } from "ws";
2
- import { OpenClawGatewayClient } from "./gateway-client.js";
3
- import { handleLocalCommand } from "../commands/local-handlers.js";
4
- import { handleProviderCommand } from "../commands/provider-handlers.js";
5
- import { getServicePlatform } from "../platform/service-manager.js";
6
- import { uploadAssistantAttachments } from "../media/assistant-attachments.js";
7
- import { homedir } from "os";
8
- import { extname, join } from "path";
9
- import { mkdir, writeFile } from "fs/promises";
10
- import { randomUUID } from "crypto";
11
-
12
- // ---------------------------------------------------------------------------
13
- // Constants
14
- // ---------------------------------------------------------------------------
15
-
16
- const OUTBOUND_DIR = join(homedir(), ".openclaw", "media", "outbound");
17
-
18
- function isSuspiciousShortReply(text: string | null | undefined): boolean {
19
- const normalized = (text ?? "").replace(/\s+/g, " ").trim().toLowerCase();
20
- if (!normalized) return true;
21
- return normalized.length <= 4 || ["no", "ok", "nope", "n/a", "no_reply"].includes(normalized);
22
- }
23
-
24
- function extensionFromMimeType(mimeType: string): string {
25
- const map: Record<string, string> = {
26
- "image/jpeg": ".jpg",
27
- "image/png": ".png",
28
- "image/gif": ".gif",
29
- "image/webp": ".webp",
30
- "video/mp4": ".mp4",
31
- "video/quicktime": ".mov",
32
- "audio/mpeg": ".mp3",
33
- "audio/mp4": ".m4a",
34
- "audio/wav": ".wav",
35
- "audio/x-wav": ".wav",
36
- "audio/aac": ".aac",
37
- "application/pdf": ".pdf",
38
- "text/plain": ".txt",
39
- "text/markdown": ".md",
40
- "application/json": ".json",
41
- };
42
- return map[mimeType] ?? ".bin";
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Messages: relay client ↔ relay server
47
- // ---------------------------------------------------------------------------
48
-
49
- /** Messages the relay client sends to the relay server. */
50
- type ToServer =
51
- | { type: "relay_hello"; platform: "macos" | "linux" | "windows" | "unsupported" }
52
- | { type: "gateway_connected" }
53
- | { type: "gateway_disconnected"; reason: string }
54
- | { type: "event"; event: string; payload: unknown }
55
- | { type: "res"; id: string; ok: boolean; payload?: unknown; error?: { message?: string } };
56
-
57
- /** Messages the relay server sends to the relay client. */
58
- interface FromServer {
59
- type: "cmd";
60
- id?: string;
61
- method: string;
62
- params: unknown;
63
- }
64
-
65
- // ---------------------------------------------------------------------------
66
- // Options
67
- // ---------------------------------------------------------------------------
68
-
69
- export interface RelayManagerOptions {
70
- relayServerUrl: string;
71
- gatewayId: string;
72
- relaySecret: string;
73
- gatewayUrl: string;
74
- gatewayToken?: string;
75
- gatewayPassword?: string;
76
- onConnected?: () => void;
77
- onDisconnected?: () => void;
78
- }
79
-
80
- // ---------------------------------------------------------------------------
81
- // Main entry point
82
- // ---------------------------------------------------------------------------
83
-
84
- /**
85
- * Connects to the cloud relay server and the local OpenClaw Gateway,
86
- * then bridges messages between them indefinitely.
87
- *
88
- * The gateway client runs for as long as this relay connection is alive.
89
- * Returns a Promise that resolves `true` (retry) when the relay server
90
- * connection closes.
91
- */
92
- export async function runRelayManager(opts: RelayManagerOptions): Promise<boolean> {
93
- const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
94
-
95
- return new Promise<boolean>((resolve) => {
96
- let relayWs: WebSocket;
97
- try {
98
- relayWs = new WebSocket(wsUrl);
99
- } catch (err) {
100
- console.error("Failed to create relay WebSocket:", err);
101
- resolve(true);
102
- return;
103
- }
104
-
105
- let gatewayClient: OpenClawGatewayClient | null = null;
106
-
107
- function send(msg: ToServer): void {
108
- if (relayWs.readyState === WebSocket.OPEN) {
109
- relayWs.send(JSON.stringify(msg));
110
- }
111
- }
112
-
113
- relayWs.on("open", () => {
114
- console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
115
- send({ type: "relay_hello", platform: getServicePlatform() });
116
- opts.onConnected?.();
117
-
118
- // Start the persistent gateway connection as soon as we're connected
119
- // to the relay server. Its lifetime is tied to this relay session.
120
- gatewayClient = new OpenClawGatewayClient({
121
- url: opts.gatewayUrl,
122
- token: opts.gatewayToken,
123
- password: opts.gatewayPassword,
124
-
125
- onConnected: () => {
126
- console.log("Gateway connected.");
127
- send({ type: "gateway_connected" });
128
- },
129
-
130
- onDisconnected: (reason) => {
131
- console.log(`Gateway disconnected: ${reason}`);
132
- send({ type: "gateway_disconnected", reason });
133
- },
134
-
135
- onEvent: (event, payload) => {
136
- // On chat final, fetch history to get actual content + extract media attachments.
137
- // OpenClaw 2026.3.2+ no longer includes message content in chat final payload.
138
- if (event === "chat") {
139
- const p = payload as { state?: string; sessionKey?: string; runId?: string; message?: unknown };
140
- if (p?.state === "final" && p?.sessionKey) {
141
- const sessionKey = p.sessionKey;
142
- const runId = p.runId;
143
- type ContentBlock = { type: string; text?: string; source?: { type?: string; media_type?: string; data?: string; path?: string } };
144
- type HistoryResponse = { messages?: Array<{ role: string; content?: ContentBlock[] }> };
145
- const fetchHistory = () =>
146
- gatewayClient!.request<HistoryResponse>("chat.history", { sessionKey, limit: 10 });
147
- const extractText = (h: HistoryResponse | undefined) => {
148
- const msgs = h?.messages ?? [];
149
- const last = [...msgs].reverse().find((m) => m.role === "assistant");
150
- return last?.content?.find((b) => b.type === "text")?.text;
151
- };
152
- fetchHistory()
153
- .then(async (history) => {
154
- let text = extractText(history);
155
- // Retry once after 600ms if OpenClaw hasn't committed the message yet
156
- if (!text) {
157
- await new Promise((resolve) => setTimeout(resolve, 600));
158
- const retryHistory = await fetchHistory();
159
- text = extractText(retryHistory);
160
- history = retryHistory;
161
- }
162
- if (text) {
163
- (p as Record<string, unknown>).message = { content: [{ type: "text", text }] };
164
- }
165
- if (isSuspiciousShortReply(text)) {
166
- console.warn(
167
- `[relay] suspicious chat final history text runId=${runId} sessionKey=${sessionKey} text=${JSON.stringify(text ?? "")}`
168
- );
169
- }
170
-
171
- // Upload any media blocks found in the history
172
- try {
173
- const attachments = await uploadAssistantAttachments(
174
- history ?? {},
175
- opts.relayServerUrl,
176
- opts.gatewayId,
177
- opts.relaySecret
178
- );
179
- if (attachments.length > 0) {
180
- (p as Record<string, unknown>).attachments = attachments;
181
- console.log(`[relay] chat final: injected ${attachments.length} attachment(s) runId=${runId}`);
182
- }
183
- } catch (mediaErr) {
184
- console.error(`[relay] media upload error (non-fatal): ${mediaErr}`);
185
- }
186
-
187
- console.log(`[relay] chat final (history fetched): runId=${runId} textLength=${text?.length ?? 0}`);
188
- send({ type: "event", event, payload });
189
- })
190
- .catch((err) => {
191
- console.error(`[relay] chat.history fetch failed: ${err}`);
192
- console.warn(
193
- `[relay] suspicious chat final history fetch failure runId=${runId} sessionKey=${sessionKey}`
194
- );
195
- send({ type: "event", event, payload });
196
- });
197
- return; // will send after history fetch + media upload
198
- }
199
- }
200
- send({ type: "event", event, payload });
201
- },
202
- });
203
-
204
- gatewayClient.start();
205
- });
206
-
207
- relayWs.on("message", async (raw) => {
208
- let msg: FromServer;
209
- try {
210
- msg = JSON.parse(raw.toString()) as FromServer;
211
- } catch {
212
- return;
213
- }
214
-
215
- if (msg.type !== "cmd" || !msg.method) return;
216
-
217
- const requestId = msg.id;
218
- console.log(`[relay] cmd received method=${msg.method} id=${requestId ?? "(no-id)"}`);
219
-
220
- // Handle clawpilot.provider.* commands locally (async)
221
- const providerPromise = handleProviderCommand(msg.method, msg.params);
222
- if (providerPromise !== null) {
223
- const result = await providerPromise;
224
- if (requestId) {
225
- send({
226
- type: "res",
227
- id: requestId,
228
- ok: result.ok,
229
- ...(result.ok
230
- ? { payload: result.payload }
231
- : { error: { message: result.error } }),
232
- });
233
- }
234
- return;
235
- }
236
-
237
- // Handle clawpilot.* commands locally without forwarding to the gateway
238
- const localResult = handleLocalCommand(msg.method, msg.params);
239
- if (localResult !== null) {
240
- if (requestId) {
241
- if (localResult.ok) {
242
- send({ type: "res", id: requestId, ok: true, payload: localResult.payload });
243
- } else {
244
- send({ type: "res", id: requestId, ok: false, error: { message: localResult.error } });
245
- }
246
- }
247
- return;
248
- }
249
-
250
- // Handle chat.send with attachments - save to disk and add path reference
251
- if (msg.method === "chat.send") {
252
- const params = msg.params as any;
253
- // Always set deliver:false so OpenClaw responds via WebSocket (not external channel)
254
- params.deliver = false;
255
- if (params.attachments && params.attachments.length > 0) {
256
- const fileReferences: string[] = [];
257
-
258
- // Ensure outbound directory exists
259
- await mkdir(OUTBOUND_DIR, { recursive: true });
260
-
261
- // Save each attachment to disk and create path reference
262
- for (const att of params.attachments) {
263
- try {
264
- // Decode base64 to buffer
265
- const buffer = Buffer.from(att.content, "base64");
266
- const ext = extname(att.fileName ?? "") || extensionFromMimeType(att.mimeType);
267
- const stagedFileName = `${randomUUID()}${ext}`;
268
- const stagedPath = join(OUTBOUND_DIR, stagedFileName);
269
-
270
- // Write to disk
271
- await writeFile(stagedPath, buffer);
272
- console.log(`[relay] Saved attachment to: ${stagedPath}`);
273
-
274
- // Create path reference (same format as ClawX)
275
- fileReferences.push(
276
- `[media attached: ${stagedPath} (${att.mimeType}) | ${stagedPath}]`
277
- );
278
- } catch (err) {
279
- console.error(`[relay] Failed to save attachment: ${err}`);
280
- }
281
- }
282
-
283
- // Append file references to message
284
- if (fileReferences.length > 0) {
285
- const refs = fileReferences.join("\n");
286
- params.message = params.message ? `${params.message}\n\n${refs}` : refs;
287
- console.log(`[relay] Added file references to message`);
288
- }
289
- }
290
- }
291
-
292
- gatewayClient
293
- ?.request(msg.method, msg.params)
294
- .then((result) => {
295
- console.log(`[relay] cmd ok method=${msg.method} id=${requestId ?? "(no-id)"}`);
296
- if (requestId) {
297
- send({ type: "res", id: requestId, ok: true, payload: result });
298
- }
299
- })
300
- .catch((err: unknown) => {
301
- console.error(`[relay] cmd failed method=${msg.method} id=${requestId ?? "(no-id)"}: ${String(err)}`);
302
- if (requestId) {
303
- send({ type: "res", id: requestId, ok: false, error: { message: String(err) } });
304
- }
305
- });
306
- });
307
-
308
- relayWs.on("close", (code, reason) => {
309
- console.log(`Relay connection closed: ${code} ${reason.toString()}`);
310
- opts.onDisconnected?.();
311
- gatewayClient?.stop();
312
- gatewayClient = null;
313
- // Code 4000 = server kicked us because another relay client took over.
314
- // Stop retrying so the two instances don't bounce each other forever.
315
- resolve(code !== 4000);
316
- });
317
-
318
- relayWs.on("error", (err) => {
319
- console.error("Relay WebSocket error:", err.message);
320
- // close event will follow
321
- });
322
- });
323
- }
324
-
325
- function buildRelayUrl(serverUrl: string, gatewayId: string, relaySecret: string): string {
326
- const base = serverUrl.replace(/\/+$/, "").replace(/^http/, "ws");
327
- return `${base}/relay/${gatewayId}?secret=${encodeURIComponent(relaySecret)}`;
328
- }
package/test-chat.mjs DELETED
@@ -1,64 +0,0 @@
1
- import { WebSocket } from "ws";
2
- import { randomUUID } from "crypto";
3
- import { readFileSync } from "fs";
4
-
5
- const token = readFileSync("/tmp/test_token.txt", "utf8").trim();
6
- const url = `ws://localhost:3000/gw/295c6252c69746124af48ebbe7001f11?token=${token}`;
7
-
8
- const ws = new WebSocket(url);
9
-
10
- ws.on("open", () => {
11
- console.log("[ios] connected to relay server");
12
-
13
- ws.send(JSON.stringify({ method: "sessions.reset", params: { key: "main" } }));
14
- console.log("[ios] → sessions.reset { key: 'main' }");
15
-
16
- setTimeout(() => {
17
- ws.send(JSON.stringify({
18
- method: "chat.send",
19
- params: {
20
- sessionKey: "main",
21
- message: "hello, please reply with just one short sentence",
22
- idempotencyKey: randomUUID(),
23
- },
24
- }));
25
- console.log("[ios] → chat.send");
26
- }, 800);
27
- });
28
-
29
- let lastText = "";
30
- ws.on("message", (raw) => {
31
- const msg = JSON.parse(raw.toString());
32
- if (msg.type === "connected") { console.log("[ios] ← gateway online"); return; }
33
- if (msg.type === "disconnected") { console.log("[gateway] ← disconnected:", msg.reason); return; }
34
- if (msg.type === "event" && msg.event === "chat") {
35
- const p = msg.payload;
36
- if (p.state === "delta") {
37
- const text = p.message?.content?.[0]?.text ?? "";
38
- if (text !== lastText) {
39
- process.stdout.write("\r[delta] " + text.slice(-80).padEnd(80));
40
- lastText = text;
41
- }
42
- } else if (p.state === "final") {
43
- const text = p.message?.content?.[0]?.text ?? "(no text)";
44
- console.log("\n\n[FINAL RESPONSE]\n" + text);
45
- ws.close();
46
- process.exit(0);
47
- } else if (p.state === "error") {
48
- console.log("\n[ERROR]", p.errorMessage);
49
- ws.close();
50
- process.exit(1);
51
- }
52
- return;
53
- }
54
- if (msg.event !== "health" && msg.event !== "presence") {
55
- console.log("[event]", JSON.stringify(msg).slice(0, 120));
56
- }
57
- });
58
-
59
- ws.on("error", (e) => console.error("[ws error]", e.message));
60
- setTimeout(() => {
61
- console.log("\n[timeout after 60s]");
62
- ws.close();
63
- process.exit(1);
64
- }, 60000);
package/test-direct.mjs DELETED
@@ -1,171 +0,0 @@
1
- /**
2
- * Direct test: connect to OpenClaw Gateway and send chat.send
3
- * Uses same logic as gateway-client.ts
4
- */
5
- import { WebSocket } from "ws";
6
- import { randomUUID, generateKeyPairSync, createPrivateKey, sign, createPublicKey, createHash } from "node:crypto";
7
- import { readFileSync, existsSync } from "node:fs";
8
- import { join } from "node:path";
9
- import { homedir } from "node:os";
10
-
11
- const IDENTITY_PATH = join(homedir(), ".clawai", "device-identity.json");
12
- const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
13
-
14
- function base64UrlEncode(buf) {
15
- return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
16
- }
17
-
18
- function rawPublicKeyBytes(publicKeyPem) {
19
- const key = createPublicKey(publicKeyPem);
20
- const spki = key.export({ type: "spki", format: "der" });
21
- if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
22
- return spki.subarray(ED25519_SPKI_PREFIX.length);
23
- }
24
- return spki;
25
- }
26
-
27
- const stored = JSON.parse(readFileSync(IDENTITY_PATH, "utf8"));
28
- const identity = { deviceId: stored.deviceId, publicKeyPem: stored.publicKeyPem, privateKeyPem: stored.privateKeyPem };
29
-
30
- const openclawCfg = JSON.parse(readFileSync(join(homedir(), ".openclaw", "openclaw.json"), "utf8"));
31
- const authToken = openclawCfg?.gateway?.auth?.token;
32
- const port = openclawCfg?.gateway?.port ?? 18789;
33
-
34
- console.log("Device ID:", identity.deviceId);
35
- console.log("Auth token:", authToken ? authToken.slice(0, 8) + "..." : "(none)");
36
- console.log("Connecting to ws://localhost:" + port);
37
-
38
- const ws = new WebSocket("ws://localhost:" + port, { maxPayload: 25 * 1024 * 1024 });
39
- const pending = new Map();
40
- let connectNonce = null;
41
- let connectSent = false;
42
- let connected = false;
43
-
44
- function sendFrame(obj) {
45
- ws.send(JSON.stringify(obj));
46
- }
47
-
48
- function request(method, params) {
49
- const id = randomUUID();
50
- return new Promise((resolve, reject) => {
51
- pending.set(id, { resolve, reject });
52
- sendFrame({ type: "req", id, method, params });
53
- console.log(`→ req [${id.slice(0, 8)}] ${method}`);
54
- });
55
- }
56
-
57
- function sendConnect(nonce) {
58
- if (connectSent) return;
59
- connectSent = true;
60
- const role = "operator";
61
- const scopes = ["operator.admin"];
62
- const clientId = "gateway-client";
63
- const clientMode = "backend";
64
- const signedAtMs = Date.now();
65
-
66
- const version = nonce ? "v2" : "v1";
67
- const payload = [version, identity.deviceId, clientId, clientMode, role, scopes.join(","), String(signedAtMs), authToken ?? "", ...(nonce ? [nonce] : [])].join("|");
68
- const key = createPrivateKey(identity.privateKeyPem);
69
- const signature = base64UrlEncode(sign(null, Buffer.from(payload, "utf8"), key));
70
- const device = {
71
- id: identity.deviceId,
72
- publicKey: base64UrlEncode(rawPublicKeyBytes(identity.publicKeyPem)),
73
- signature,
74
- signedAt: signedAtMs,
75
- nonce,
76
- };
77
-
78
- const params = {
79
- minProtocol: 3, maxProtocol: 3,
80
- role, scopes, caps: [], commands: [],
81
- client: { id: clientId, displayName: "ClawAI Direct Test", version: "1.0.0", platform: process.platform, mode: clientMode },
82
- device,
83
- auth: authToken ? { token: authToken } : undefined,
84
- };
85
-
86
- request("connect", params)
87
- .then((res) => {
88
- console.log("✓ connect OK:", JSON.stringify(res).slice(0, 100));
89
- connected = true;
90
- runTest();
91
- })
92
- .catch((err) => {
93
- console.error("✗ connect FAILED:", err.message);
94
- process.exit(1);
95
- });
96
- }
97
-
98
- async function runTest() {
99
- console.log("\n--- Running test ---");
100
- try {
101
- const r1 = await request("sessions.reset", { key: "main" });
102
- console.log("✓ sessions.reset:", JSON.stringify(r1));
103
-
104
- const r2 = await request("chat.send", {
105
- sessionKey: "main",
106
- message: "hello, please reply with just one short sentence",
107
- idempotencyKey: randomUUID(),
108
- });
109
- console.log("✓ chat.send:", JSON.stringify(r2));
110
- console.log("\nWaiting for chat events...");
111
- } catch (err) {
112
- console.error("✗ command failed:", err.message);
113
- process.exit(1);
114
- }
115
- }
116
-
117
- ws.on("open", () => {
118
- console.log("WebSocket open, waiting for challenge...");
119
- setTimeout(() => { if (!connectSent) { console.log("(no challenge, sending connect now)"); sendConnect(null); } }, 1000);
120
- });
121
-
122
- ws.on("message", (raw) => {
123
- const msg = JSON.parse(raw.toString());
124
- if (msg.type === "event") {
125
- if (msg.event === "connect.challenge") {
126
- const nonce = msg.payload?.nonce;
127
- console.log("← challenge nonce:", nonce);
128
- sendConnect(nonce ?? null);
129
- return;
130
- }
131
- if (msg.event === "chat") {
132
- const p = msg.payload;
133
- if (p.state === "delta") {
134
- process.stdout.write("\r[delta] " + (p.message?.content?.[0]?.text ?? "").slice(-80).padEnd(80));
135
- } else if (p.state === "final") {
136
- console.log("\n\n[FINAL] " + (p.message?.content?.[0]?.text ?? "(no text)"));
137
- ws.close();
138
- process.exit(0);
139
- } else if (p.state === "error") {
140
- console.log("\n[ERROR]", p.errorMessage);
141
- ws.close();
142
- process.exit(1);
143
- }
144
- } else if (msg.event !== "health" && msg.event !== "presence" && msg.event !== "tick") {
145
- console.log("\n← event:", msg.event, JSON.stringify(msg.payload ?? {}).slice(0, 80));
146
- }
147
- return;
148
- }
149
- if (msg.type === "res") {
150
- const p = pending.get(msg.id);
151
- if (p) {
152
- pending.delete(msg.id);
153
- console.log(`← res [${msg.id.slice(0, 8)}] ok=${msg.ok}` + (msg.error ? " error=" + msg.error.message : ""));
154
- if (msg.ok) p.resolve(msg.payload);
155
- else p.reject(new Error(msg.error?.message ?? "gateway error"));
156
- }
157
- }
158
- });
159
-
160
- ws.on("close", (code, reason) => {
161
- console.log("\n← close", code, reason.toString() || "(no reason)");
162
- process.exit(code === 1000 ? 0 : 1);
163
- });
164
-
165
- ws.on("error", (err) => console.error("WS error:", err.message));
166
-
167
- setTimeout(() => {
168
- console.log("\n[timeout 60s]");
169
- ws.close();
170
- process.exit(1);
171
- }, 60000);
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "dist",
7
- "rootDir": "src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "declaration": true,
12
- "sourceMap": true
13
- },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist"]
16
- }