@rethinkingstudio/clawpilot 1.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.
- package/README.md +79 -0
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +105 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/pair.d.ts +6 -0
- package/dist/commands/pair.js +60 -0
- package/dist/commands/pair.js.map +1 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +27 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/config/config.d.ts +22 -0
- package/dist/config/config.js +53 -0
- package/dist/config/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/relay/gateway-client.d.ts +34 -0
- package/dist/relay/gateway-client.js +268 -0
- package/dist/relay/gateway-client.js.map +1 -0
- package/dist/relay/reconnect.d.ts +14 -0
- package/dist/relay/reconnect.js +27 -0
- package/dist/relay/reconnect.js.map +1 -0
- package/dist/relay/relay-manager.d.ts +19 -0
- package/dist/relay/relay-manager.js +101 -0
- package/dist/relay/relay-manager.js.map +1 -0
- package/dist/relay/session-proxy.d.ts +18 -0
- package/dist/relay/session-proxy.js +75 -0
- package/dist/relay/session-proxy.js.map +1 -0
- package/package.json +26 -0
- package/run-relay.sh +24 -0
- package/src/commands/install.ts +110 -0
- package/src/commands/pair.ts +84 -0
- package/src/commands/run.ts +33 -0
- package/src/commands/status.ts +58 -0
- package/src/config/config.ts +67 -0
- package/src/index.ts +70 -0
- package/src/relay/gateway-client.ts +333 -0
- package/src/relay/reconnect.ts +37 -0
- package/src/relay/relay-manager.ts +148 -0
- package/test-chat.mjs +64 -0
- package/test-direct.mjs +171 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import { randomUUID, generateKeyPairSync, createPrivateKey, sign, createPublicKey, createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Device identity (Ed25519, persisted across restarts)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const IDENTITY_PATH = join(homedir(), ".clawai", "device-identity.json");
|
|
10
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
11
|
+
function base64UrlEncode(buf) {
|
|
12
|
+
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
13
|
+
}
|
|
14
|
+
function rawPublicKeyBytes(publicKeyPem) {
|
|
15
|
+
const key = createPublicKey(publicKeyPem);
|
|
16
|
+
const spki = key.export({ type: "spki", format: "der" });
|
|
17
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
18
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
19
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
20
|
+
}
|
|
21
|
+
return spki;
|
|
22
|
+
}
|
|
23
|
+
function loadOrCreateDeviceIdentity() {
|
|
24
|
+
if (existsSync(IDENTITY_PATH)) {
|
|
25
|
+
try {
|
|
26
|
+
const stored = JSON.parse(readFileSync(IDENTITY_PATH, "utf8"));
|
|
27
|
+
if (stored.deviceId && stored.publicKeyPem && stored.privateKeyPem) {
|
|
28
|
+
return { deviceId: stored.deviceId, publicKeyPem: stored.publicKeyPem, privateKeyPem: stored.privateKeyPem };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* fall through */ }
|
|
32
|
+
}
|
|
33
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
34
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
35
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
36
|
+
const deviceId = createHash("sha256").update(rawPublicKeyBytes(publicKeyPem)).digest("hex");
|
|
37
|
+
const identity = { deviceId, publicKeyPem, privateKeyPem };
|
|
38
|
+
mkdirSync(join(homedir(), ".clawai"), { recursive: true });
|
|
39
|
+
writeFileSync(IDENTITY_PATH, JSON.stringify({ version: 1, ...identity, createdAtMs: Date.now() }, null, 2) + "\n", { mode: 0o600 });
|
|
40
|
+
return identity;
|
|
41
|
+
}
|
|
42
|
+
function buildSignedDevice(identity, opts) {
|
|
43
|
+
const version = opts.nonce ? "v2" : "v1";
|
|
44
|
+
const payload = [
|
|
45
|
+
version,
|
|
46
|
+
identity.deviceId,
|
|
47
|
+
opts.clientId,
|
|
48
|
+
opts.clientMode,
|
|
49
|
+
opts.role,
|
|
50
|
+
opts.scopes.join(","),
|
|
51
|
+
String(opts.signedAtMs),
|
|
52
|
+
opts.token ?? "",
|
|
53
|
+
...(version === "v2" ? [opts.nonce ?? ""] : []),
|
|
54
|
+
].join("|");
|
|
55
|
+
const key = createPrivateKey(identity.privateKeyPem);
|
|
56
|
+
const signature = base64UrlEncode(sign(null, Buffer.from(payload, "utf8"), key));
|
|
57
|
+
return {
|
|
58
|
+
id: identity.deviceId,
|
|
59
|
+
publicKey: base64UrlEncode(rawPublicKeyBytes(identity.publicKeyPem)),
|
|
60
|
+
signature,
|
|
61
|
+
signedAt: opts.signedAtMs,
|
|
62
|
+
nonce: opts.nonce,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const PROTOCOL_VERSION = 3;
|
|
66
|
+
export class OpenClawGatewayClient {
|
|
67
|
+
opts;
|
|
68
|
+
ws = null;
|
|
69
|
+
pending = new Map();
|
|
70
|
+
backoffMs = 1000;
|
|
71
|
+
stopped = false;
|
|
72
|
+
connectNonce = null;
|
|
73
|
+
connectSent = false;
|
|
74
|
+
storedDeviceToken = null;
|
|
75
|
+
connectTimer = null;
|
|
76
|
+
tickTimer = null;
|
|
77
|
+
lastTick = 0;
|
|
78
|
+
tickIntervalMs = 30_000;
|
|
79
|
+
identity;
|
|
80
|
+
constructor(opts) {
|
|
81
|
+
this.opts = opts;
|
|
82
|
+
this.identity = loadOrCreateDeviceIdentity();
|
|
83
|
+
}
|
|
84
|
+
start() {
|
|
85
|
+
if (this.stopped)
|
|
86
|
+
return;
|
|
87
|
+
this.ws = new WebSocket(this.opts.url, { maxPayload: 25 * 1024 * 1024 });
|
|
88
|
+
this.ws.on("open", () => {
|
|
89
|
+
this.connectNonce = null;
|
|
90
|
+
this.connectSent = false;
|
|
91
|
+
// Fallback: send connect after 1 s if challenge hasn't arrived
|
|
92
|
+
this.connectTimer = setTimeout(() => this.sendConnect(), 1000);
|
|
93
|
+
});
|
|
94
|
+
this.ws.on("message", (data) => {
|
|
95
|
+
const raw = typeof data === "string" ? data : data.toString();
|
|
96
|
+
this.handleMessage(raw);
|
|
97
|
+
});
|
|
98
|
+
this.ws.on("close", (code, reason) => {
|
|
99
|
+
const reasonText = reason.toString() || `code ${code}`;
|
|
100
|
+
this.teardown();
|
|
101
|
+
this.opts.onDisconnected(reasonText);
|
|
102
|
+
this.scheduleReconnect();
|
|
103
|
+
});
|
|
104
|
+
this.ws.on("error", (err) => {
|
|
105
|
+
console.error(`[gateway-client] ws error: ${String(err)}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
stop() {
|
|
109
|
+
this.stopped = true;
|
|
110
|
+
this.teardown();
|
|
111
|
+
this.ws?.close();
|
|
112
|
+
this.ws = null;
|
|
113
|
+
this.flushPending(new Error("gateway client stopped"));
|
|
114
|
+
}
|
|
115
|
+
send(method, params) {
|
|
116
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
117
|
+
throw new Error("gateway not connected");
|
|
118
|
+
}
|
|
119
|
+
const frame = { type: "req", id: randomUUID(), method, params };
|
|
120
|
+
this.ws.send(JSON.stringify(frame));
|
|
121
|
+
}
|
|
122
|
+
async request(method, params) {
|
|
123
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
124
|
+
throw new Error("gateway not connected");
|
|
125
|
+
}
|
|
126
|
+
const id = randomUUID();
|
|
127
|
+
const frame = { type: "req", id, method, params };
|
|
128
|
+
const p = new Promise((resolve, reject) => {
|
|
129
|
+
this.pending.set(id, { resolve: (v) => resolve(v), reject });
|
|
130
|
+
});
|
|
131
|
+
this.ws.send(JSON.stringify(frame));
|
|
132
|
+
return p;
|
|
133
|
+
}
|
|
134
|
+
// -------------------------------------------------------------------------
|
|
135
|
+
sendConnect() {
|
|
136
|
+
if (this.connectSent)
|
|
137
|
+
return;
|
|
138
|
+
this.connectSent = true;
|
|
139
|
+
if (this.connectTimer) {
|
|
140
|
+
clearTimeout(this.connectTimer);
|
|
141
|
+
this.connectTimer = null;
|
|
142
|
+
}
|
|
143
|
+
const role = "operator";
|
|
144
|
+
const scopes = ["operator.admin"];
|
|
145
|
+
const clientId = "gateway-client";
|
|
146
|
+
const clientMode = "backend";
|
|
147
|
+
const signedAtMs = Date.now();
|
|
148
|
+
const nonce = this.connectNonce ?? undefined;
|
|
149
|
+
const authToken = this.storedDeviceToken ?? this.opts.token;
|
|
150
|
+
const device = buildSignedDevice(this.identity, {
|
|
151
|
+
clientId, clientMode, role, scopes, signedAtMs,
|
|
152
|
+
token: authToken ?? undefined,
|
|
153
|
+
nonce,
|
|
154
|
+
});
|
|
155
|
+
const params = {
|
|
156
|
+
minProtocol: PROTOCOL_VERSION,
|
|
157
|
+
maxProtocol: PROTOCOL_VERSION,
|
|
158
|
+
role,
|
|
159
|
+
scopes,
|
|
160
|
+
caps: ["tool-events"],
|
|
161
|
+
commands: ["chat.push"],
|
|
162
|
+
client: {
|
|
163
|
+
id: clientId,
|
|
164
|
+
displayName: "ClawAI Relay",
|
|
165
|
+
version: "1.0.0",
|
|
166
|
+
platform: process.platform,
|
|
167
|
+
mode: clientMode,
|
|
168
|
+
},
|
|
169
|
+
device,
|
|
170
|
+
auth: authToken || this.opts.password
|
|
171
|
+
? { token: authToken, password: this.opts.password }
|
|
172
|
+
: undefined,
|
|
173
|
+
};
|
|
174
|
+
this.request("connect", params)
|
|
175
|
+
.then((helloOk) => {
|
|
176
|
+
const deviceToken = helloOk?.auth?.deviceToken;
|
|
177
|
+
if (typeof deviceToken === "string") {
|
|
178
|
+
this.storedDeviceToken = deviceToken;
|
|
179
|
+
}
|
|
180
|
+
if (typeof helloOk?.policy?.tickIntervalMs === "number") {
|
|
181
|
+
this.tickIntervalMs = helloOk.policy.tickIntervalMs;
|
|
182
|
+
}
|
|
183
|
+
this.backoffMs = 1000;
|
|
184
|
+
this.lastTick = Date.now();
|
|
185
|
+
this.startTickWatch();
|
|
186
|
+
this.opts.onConnected();
|
|
187
|
+
})
|
|
188
|
+
.catch((err) => {
|
|
189
|
+
console.error(`[gateway-client] connect failed: ${String(err)}`);
|
|
190
|
+
this.ws?.close(1008, "connect failed");
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
handleMessage(raw) {
|
|
194
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (typeof parsed?.type !== "string")
|
|
203
|
+
return;
|
|
204
|
+
if (parsed.type === "event") {
|
|
205
|
+
const evt = parsed;
|
|
206
|
+
if (evt.event === "connect.challenge") {
|
|
207
|
+
const nonce = evt.payload?.nonce;
|
|
208
|
+
if (typeof nonce === "string") {
|
|
209
|
+
this.connectNonce = nonce;
|
|
210
|
+
this.sendConnect();
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (evt.event === "tick") {
|
|
215
|
+
this.lastTick = Date.now();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.opts.onEvent(evt.event, evt.payload ?? null);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (parsed.type === "res") {
|
|
222
|
+
const res = parsed;
|
|
223
|
+
const pending = this.pending.get(res.id);
|
|
224
|
+
if (!pending)
|
|
225
|
+
return;
|
|
226
|
+
this.pending.delete(res.id);
|
|
227
|
+
if (res.ok)
|
|
228
|
+
pending.resolve(res.payload);
|
|
229
|
+
else
|
|
230
|
+
pending.reject(new Error(res.error?.message ?? "gateway error"));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
startTickWatch() {
|
|
234
|
+
if (this.tickTimer)
|
|
235
|
+
clearInterval(this.tickTimer);
|
|
236
|
+
const interval = Math.max(this.tickIntervalMs, 1000);
|
|
237
|
+
this.tickTimer = setInterval(() => {
|
|
238
|
+
if (this.stopped || !this.lastTick)
|
|
239
|
+
return;
|
|
240
|
+
if (Date.now() - this.lastTick > this.tickIntervalMs * 2) {
|
|
241
|
+
this.ws?.close(4000, "tick timeout");
|
|
242
|
+
}
|
|
243
|
+
}, interval);
|
|
244
|
+
}
|
|
245
|
+
scheduleReconnect() {
|
|
246
|
+
if (this.stopped)
|
|
247
|
+
return;
|
|
248
|
+
const delay = this.backoffMs;
|
|
249
|
+
this.backoffMs = Math.min(this.backoffMs * 2, 30_000);
|
|
250
|
+
setTimeout(() => this.start(), delay).unref();
|
|
251
|
+
}
|
|
252
|
+
teardown() {
|
|
253
|
+
if (this.connectTimer) {
|
|
254
|
+
clearTimeout(this.connectTimer);
|
|
255
|
+
this.connectTimer = null;
|
|
256
|
+
}
|
|
257
|
+
if (this.tickTimer) {
|
|
258
|
+
clearInterval(this.tickTimer);
|
|
259
|
+
this.tickTimer = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
flushPending(err) {
|
|
263
|
+
for (const p of this.pending.values())
|
|
264
|
+
p.reject(err);
|
|
265
|
+
this.pending.clear();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=gateway-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-client.js","sourceRoot":"","sources":["../../src/relay/gateway-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ReconnectOptions {
|
|
2
|
+
initialDelayMs?: number;
|
|
3
|
+
maxDelayMs?: number;
|
|
4
|
+
onRetry?: (attempt: number, delayMs: number) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Implements exponential backoff: 500ms → 1s → 2s → ... → 30s cap.
|
|
8
|
+
* Calls `connect` repeatedly until `connect` returns false (permanent failure)
|
|
9
|
+
* or the returned WebSocket connection stays alive.
|
|
10
|
+
*
|
|
11
|
+
* The `connect` callback should return `true` if it attempted a connection
|
|
12
|
+
* and `false` if it should stop retrying.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withReconnect(connect: () => Promise<boolean>, opts?: ReconnectOptions): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Implements exponential backoff: 500ms → 1s → 2s → ... → 30s cap.
|
|
3
|
+
* Calls `connect` repeatedly until `connect` returns false (permanent failure)
|
|
4
|
+
* or the returned WebSocket connection stays alive.
|
|
5
|
+
*
|
|
6
|
+
* The `connect` callback should return `true` if it attempted a connection
|
|
7
|
+
* and `false` if it should stop retrying.
|
|
8
|
+
*/
|
|
9
|
+
export async function withReconnect(connect, opts = {}) {
|
|
10
|
+
const initialDelay = opts.initialDelayMs ?? 500;
|
|
11
|
+
const maxDelay = opts.maxDelayMs ?? 30_000;
|
|
12
|
+
let attempt = 0;
|
|
13
|
+
let delay = initialDelay;
|
|
14
|
+
while (true) {
|
|
15
|
+
const shouldRetry = await connect();
|
|
16
|
+
if (!shouldRetry)
|
|
17
|
+
break;
|
|
18
|
+
attempt++;
|
|
19
|
+
opts.onRetry?.(attempt, delay);
|
|
20
|
+
await sleep(delay);
|
|
21
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=reconnect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconnect.js","sourceRoot":"","sources":["../../src/relay/reconnect.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAA+B,EAC/B,OAAyB,EAAE;IAE3B,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,IAAI,GAAG,CAAC;IAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC;IAC3C,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,KAAK,GAAG,YAAY,CAAC;IAEzB,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,IAAI,CAAC,WAAW;YAAE,MAAM;QAExB,OAAO,EAAE,CAAC;QACV,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC/B,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC;QACnB,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface RelayManagerOptions {
|
|
2
|
+
relayServerUrl: string;
|
|
3
|
+
gatewayId: string;
|
|
4
|
+
relaySecret: string;
|
|
5
|
+
gatewayUrl: string;
|
|
6
|
+
gatewayToken?: string;
|
|
7
|
+
gatewayPassword?: string;
|
|
8
|
+
onConnected?: () => void;
|
|
9
|
+
onDisconnected?: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Connects to the cloud relay server and the local OpenClaw Gateway,
|
|
13
|
+
* then bridges messages between them indefinitely.
|
|
14
|
+
*
|
|
15
|
+
* The gateway client runs for as long as this relay connection is alive.
|
|
16
|
+
* Returns a Promise that resolves `true` (retry) when the relay server
|
|
17
|
+
* connection closes.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runRelayManager(opts: RelayManagerOptions): Promise<boolean>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import { OpenClawGatewayClient } from "./gateway-client.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Main entry point
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Connects to the cloud relay server and the local OpenClaw Gateway,
|
|
8
|
+
* then bridges messages between them indefinitely.
|
|
9
|
+
*
|
|
10
|
+
* The gateway client runs for as long as this relay connection is alive.
|
|
11
|
+
* Returns a Promise that resolves `true` (retry) when the relay server
|
|
12
|
+
* connection closes.
|
|
13
|
+
*/
|
|
14
|
+
export async function runRelayManager(opts) {
|
|
15
|
+
const wsUrl = buildRelayUrl(opts.relayServerUrl, opts.gatewayId, opts.relaySecret);
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
let relayWs;
|
|
18
|
+
try {
|
|
19
|
+
relayWs = new WebSocket(wsUrl);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
console.error("Failed to create relay WebSocket:", err);
|
|
23
|
+
resolve(true);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let gatewayClient = null;
|
|
27
|
+
function send(msg) {
|
|
28
|
+
if (relayWs.readyState === WebSocket.OPEN) {
|
|
29
|
+
relayWs.send(JSON.stringify(msg));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
relayWs.on("open", () => {
|
|
33
|
+
console.log(`Connected to relay server (gatewayId=${opts.gatewayId})`);
|
|
34
|
+
opts.onConnected?.();
|
|
35
|
+
// Start the persistent gateway connection as soon as we're connected
|
|
36
|
+
// to the relay server. Its lifetime is tied to this relay session.
|
|
37
|
+
gatewayClient = new OpenClawGatewayClient({
|
|
38
|
+
url: opts.gatewayUrl,
|
|
39
|
+
token: opts.gatewayToken,
|
|
40
|
+
password: opts.gatewayPassword,
|
|
41
|
+
onConnected: () => {
|
|
42
|
+
console.log("Gateway connected.");
|
|
43
|
+
send({ type: "gateway_connected" });
|
|
44
|
+
},
|
|
45
|
+
onDisconnected: (reason) => {
|
|
46
|
+
console.log(`Gateway disconnected: ${reason}`);
|
|
47
|
+
send({ type: "gateway_disconnected", reason });
|
|
48
|
+
},
|
|
49
|
+
onEvent: (event, payload) => {
|
|
50
|
+
send({ type: "event", event, payload });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
gatewayClient.start();
|
|
54
|
+
});
|
|
55
|
+
relayWs.on("message", (raw) => {
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(raw.toString());
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (msg.type !== "cmd" || !msg.method)
|
|
64
|
+
return;
|
|
65
|
+
const requestId = msg.id;
|
|
66
|
+
console.log(`[relay] cmd received method=${msg.method} id=${requestId ?? "(no-id)"}`);
|
|
67
|
+
gatewayClient
|
|
68
|
+
?.request(msg.method, msg.params)
|
|
69
|
+
.then((result) => {
|
|
70
|
+
console.log(`[relay] cmd ok method=${msg.method} id=${requestId ?? "(no-id)"}`);
|
|
71
|
+
if (requestId) {
|
|
72
|
+
send({ type: "res", id: requestId, ok: true, payload: result });
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.catch((err) => {
|
|
76
|
+
console.error(`[relay] cmd failed method=${msg.method} id=${requestId ?? "(no-id)"}: ${String(err)}`);
|
|
77
|
+
if (requestId) {
|
|
78
|
+
send({ type: "res", id: requestId, ok: false, error: { message: String(err) } });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
relayWs.on("close", (code, reason) => {
|
|
83
|
+
console.log(`Relay connection closed: ${code} ${reason.toString()}`);
|
|
84
|
+
opts.onDisconnected?.();
|
|
85
|
+
gatewayClient?.stop();
|
|
86
|
+
gatewayClient = null;
|
|
87
|
+
// Code 4000 = server kicked us because another relay client took over.
|
|
88
|
+
// Stop retrying so the two instances don't bounce each other forever.
|
|
89
|
+
resolve(code !== 4000);
|
|
90
|
+
});
|
|
91
|
+
relayWs.on("error", (err) => {
|
|
92
|
+
console.error("Relay WebSocket error:", err.message);
|
|
93
|
+
// close event will follow
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function buildRelayUrl(serverUrl, gatewayId, relaySecret) {
|
|
98
|
+
const base = serverUrl.replace(/\/+$/, "").replace(/^http/, "ws");
|
|
99
|
+
return `${base}/relay/${gatewayId}?secret=${encodeURIComponent(relaySecret)}`;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=relay-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-manager.js","sourceRoot":"","sources":["../../src/relay/relay-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
/**
|
|
3
|
+
* Proxies WebSocket frames between the relay server (via relayWs) and the
|
|
4
|
+
* local OpenClaw Gateway (via a new direct WebSocket connection).
|
|
5
|
+
*
|
|
6
|
+
* All frames are forwarded as raw bytes — no parsing of OpenClaw protocol.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SessionProxy {
|
|
9
|
+
private readonly sessionId;
|
|
10
|
+
private readonly relayWs;
|
|
11
|
+
private readonly gatewayUrl;
|
|
12
|
+
private gwWs;
|
|
13
|
+
private closed;
|
|
14
|
+
constructor(sessionId: string, relayWs: WebSocket, gatewayUrl: string);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
forwardToGateway(base64Data: string): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
/**
|
|
3
|
+
* Proxies WebSocket frames between the relay server (via relayWs) and the
|
|
4
|
+
* local OpenClaw Gateway (via a new direct WebSocket connection).
|
|
5
|
+
*
|
|
6
|
+
* All frames are forwarded as raw bytes — no parsing of OpenClaw protocol.
|
|
7
|
+
*/
|
|
8
|
+
export class SessionProxy {
|
|
9
|
+
sessionId;
|
|
10
|
+
relayWs;
|
|
11
|
+
gatewayUrl;
|
|
12
|
+
gwWs = null;
|
|
13
|
+
closed = false;
|
|
14
|
+
constructor(sessionId, relayWs, gatewayUrl) {
|
|
15
|
+
this.sessionId = sessionId;
|
|
16
|
+
this.relayWs = relayWs;
|
|
17
|
+
this.gatewayUrl = gatewayUrl;
|
|
18
|
+
}
|
|
19
|
+
async start() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const gw = new WebSocket(this.gatewayUrl);
|
|
22
|
+
this.gwWs = gw;
|
|
23
|
+
const timeout = setTimeout(() => {
|
|
24
|
+
gw.terminate();
|
|
25
|
+
reject(new Error(`Timeout connecting to gateway at ${this.gatewayUrl}`));
|
|
26
|
+
}, 10_000);
|
|
27
|
+
gw.on("open", () => {
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
gw.on("error", (err) => {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
if (!this.closed)
|
|
34
|
+
reject(err);
|
|
35
|
+
});
|
|
36
|
+
// Gateway → relay server
|
|
37
|
+
gw.on("message", (raw) => {
|
|
38
|
+
if (this.relayWs.readyState !== WebSocket.OPEN)
|
|
39
|
+
return;
|
|
40
|
+
const data = raw instanceof Buffer ? raw : Buffer.from(raw);
|
|
41
|
+
const msg = {
|
|
42
|
+
ctrl: "DATA",
|
|
43
|
+
sessionId: this.sessionId,
|
|
44
|
+
data: data.toString("base64"),
|
|
45
|
+
};
|
|
46
|
+
this.relayWs.send(JSON.stringify(msg));
|
|
47
|
+
});
|
|
48
|
+
gw.on("close", () => {
|
|
49
|
+
if (!this.closed) {
|
|
50
|
+
this.closed = true;
|
|
51
|
+
// Notify relay server that session is done
|
|
52
|
+
if (this.relayWs.readyState === WebSocket.OPEN) {
|
|
53
|
+
const msg = {
|
|
54
|
+
ctrl: "SESSION_CLOSE",
|
|
55
|
+
sessionId: this.sessionId,
|
|
56
|
+
};
|
|
57
|
+
this.relayWs.send(JSON.stringify(msg));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Called when DATA arrives from the relay server for this session
|
|
64
|
+
forwardToGateway(base64Data) {
|
|
65
|
+
if (!this.gwWs || this.gwWs.readyState !== WebSocket.OPEN)
|
|
66
|
+
return;
|
|
67
|
+
const buf = Buffer.from(base64Data, "base64");
|
|
68
|
+
this.gwWs.send(buf);
|
|
69
|
+
}
|
|
70
|
+
close() {
|
|
71
|
+
this.closed = true;
|
|
72
|
+
this.gwWs?.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=session-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-proxy.js","sourceRoot":"","sources":["../../src/relay/session-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAa/B;;;;;GAKG;AACH,MAAM,OAAO,YAAY;IAKJ;IACA;IACA;IANX,IAAI,GAAqB,IAAI,CAAC;IAC9B,MAAM,GAAG,KAAK,CAAC;IAEvB,YACmB,SAAiB,EACjB,OAAkB,EAClB,UAAkB;QAFlB,cAAS,GAAT,SAAS,CAAQ;QACjB,YAAO,GAAP,OAAO,CAAW;QAClB,eAAU,GAAV,UAAU,CAAQ;IAClC,CAAC;IAEJ,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC1C,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;YAEf,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,EAAE,CAAC,SAAS,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;YAC3E,CAAC,EAAE,MAAM,CAAC,CAAC;YAEX,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;gBACjB,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACrB,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,IAAI,CAAC,IAAI,CAAC,MAAM;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;YAChC,CAAC,CAAC,CAAC;YAEH,yBAAyB;YACzB,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE;gBACvB,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI;oBAAE,OAAO;gBACvD,MAAM,IAAI,GAAG,GAAG,YAAY,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAkB,CAAC,CAAC;gBAC3E,MAAM,GAAG,GAAa;oBACpB,IAAI,EAAE,MAAM;oBACZ,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;iBAC9B,CAAC;gBACF,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;YACzC,CAAC,CAAC,CAAC;YAEH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBAClB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;oBACjB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;oBACnB,2CAA2C;oBAC3C,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;wBAC/C,MAAM,GAAG,GAAqB;4BAC5B,IAAI,EAAE,eAAe;4BACrB,SAAS,EAAE,IAAI,CAAC,SAAS;yBAC1B,CAAC;wBACF,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;oBACzC,CAAC;gBACH,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,kEAAkE;IAClE,gBAAgB,CAAC,UAAkB;QACjC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI;YAAE,OAAO;QAClE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;IACrB,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rethinkingstudio/clawpilot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ClawAI relay client for Mac mini",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clawpilot": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"commander": "^12.1.0",
|
|
16
|
+
"qrcode-terminal": "^0.12.0",
|
|
17
|
+
"ws": "^8.18.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"@types/qrcode-terminal": "^0.12.1",
|
|
22
|
+
"@types/ws": "^8.5.12",
|
|
23
|
+
"tsx": "^4.19.2",
|
|
24
|
+
"typescript": "^5.7.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/run-relay.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# run-relay.sh — convenience script to start both backend and relay client
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
6
|
+
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
7
|
+
|
|
8
|
+
echo "Starting clawai-backend..."
|
|
9
|
+
cd "$ROOT_DIR/clawai-backend"
|
|
10
|
+
node dist/index.js &
|
|
11
|
+
BACKEND_PID=$!
|
|
12
|
+
echo "Backend started (PID=$BACKEND_PID)"
|
|
13
|
+
|
|
14
|
+
sleep 1
|
|
15
|
+
|
|
16
|
+
echo "Starting clawai-relay-client..."
|
|
17
|
+
cd "$ROOT_DIR/clawai-relay-client"
|
|
18
|
+
node dist/index.js run &
|
|
19
|
+
CLIENT_PID=$!
|
|
20
|
+
echo "Relay client started (PID=$CLIENT_PID)"
|
|
21
|
+
|
|
22
|
+
trap "echo 'Stopping...'; kill $BACKEND_PID $CLIENT_PID 2>/dev/null; exit 0" INT TERM
|
|
23
|
+
|
|
24
|
+
wait
|