@rethinkingstudio/clawpilot 1.1.17 → 2.0.0-beta.1

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 (43) hide show
  1. package/dist/commands/install.js +17 -18
  2. package/dist/commands/install.js.map +1 -1
  3. package/dist/commands/openclaw-cli.js +23 -3
  4. package/dist/commands/openclaw-cli.js.map +1 -1
  5. package/dist/commands/pair.js +8 -1
  6. package/dist/commands/pair.js.map +1 -1
  7. package/dist/commands/status.js +3 -0
  8. package/dist/commands/status.js.map +1 -1
  9. package/dist/i18n/index.js +6 -0
  10. package/dist/i18n/index.js.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/platform/service-manager.d.ts +6 -0
  14. package/dist/platform/service-manager.js +384 -42
  15. package/dist/platform/service-manager.js.map +1 -1
  16. package/package.json +8 -1
  17. package/src/commands/assistant-send.ts +0 -91
  18. package/src/commands/install.ts +0 -128
  19. package/src/commands/local-handlers.ts +0 -533
  20. package/src/commands/openclaw-cli.ts +0 -329
  21. package/src/commands/pair.ts +0 -120
  22. package/src/commands/provider-config.ts +0 -275
  23. package/src/commands/provider-handlers.ts +0 -184
  24. package/src/commands/provider-registry.ts +0 -138
  25. package/src/commands/run.ts +0 -34
  26. package/src/commands/send.ts +0 -42
  27. package/src/commands/session-key.ts +0 -77
  28. package/src/commands/set-token.ts +0 -45
  29. package/src/commands/status.ts +0 -154
  30. package/src/commands/upload.ts +0 -141
  31. package/src/config/config.ts +0 -137
  32. package/src/generated/build-config.ts +0 -1
  33. package/src/i18n/index.ts +0 -179
  34. package/src/index.ts +0 -166
  35. package/src/media/assistant-attachments.ts +0 -205
  36. package/src/media/oss-uploader.ts +0 -306
  37. package/src/platform/service-manager.ts +0 -570
  38. package/src/relay/gateway-client.ts +0 -359
  39. package/src/relay/reconnect.ts +0 -37
  40. package/src/relay/relay-manager.ts +0 -328
  41. package/test-chat.mjs +0 -64
  42. package/test-direct.mjs +0 -171
  43. package/tsconfig.json +0 -16
@@ -1,359 +0,0 @@
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
- // ---------------------------------------------------------------------------
8
- // Device identity (Ed25519, persisted across restarts)
9
- // ---------------------------------------------------------------------------
10
-
11
- const IDENTITY_PATH = join(homedir(), ".clawai", "device-identity.json");
12
- const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
13
-
14
- interface DeviceIdentity {
15
- deviceId: string;
16
- publicKeyPem: string;
17
- privateKeyPem: string;
18
- }
19
-
20
- function base64UrlEncode(buf: Buffer): string {
21
- return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
22
- }
23
-
24
- function rawPublicKeyBytes(publicKeyPem: string): Buffer {
25
- const key = createPublicKey(publicKeyPem);
26
- const spki = key.export({ type: "spki", format: "der" }) as Buffer;
27
- if (
28
- spki.length === ED25519_SPKI_PREFIX.length + 32 &&
29
- spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
30
- ) {
31
- return spki.subarray(ED25519_SPKI_PREFIX.length);
32
- }
33
- return spki;
34
- }
35
-
36
- function loadOrCreateDeviceIdentity(): DeviceIdentity {
37
- if (existsSync(IDENTITY_PATH)) {
38
- try {
39
- const stored = JSON.parse(readFileSync(IDENTITY_PATH, "utf8")) as DeviceIdentity & { version?: number };
40
- if (stored.deviceId && stored.publicKeyPem && stored.privateKeyPem) {
41
- return { deviceId: stored.deviceId, publicKeyPem: stored.publicKeyPem, privateKeyPem: stored.privateKeyPem };
42
- }
43
- } catch { /* fall through */ }
44
- }
45
-
46
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
47
- const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
48
- const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
49
- const deviceId = createHash("sha256").update(rawPublicKeyBytes(publicKeyPem)).digest("hex");
50
- const identity: DeviceIdentity = { deviceId, publicKeyPem, privateKeyPem };
51
-
52
- mkdirSync(join(homedir(), ".clawai"), { recursive: true });
53
- writeFileSync(IDENTITY_PATH, JSON.stringify({ version: 1, ...identity, createdAtMs: Date.now() }, null, 2) + "\n", { mode: 0o600 });
54
- return identity;
55
- }
56
-
57
- function buildSignedDevice(identity: DeviceIdentity, opts: {
58
- clientId: string;
59
- clientMode: string;
60
- role: string;
61
- scopes: string[];
62
- signedAtMs: number;
63
- token?: string;
64
- nonce?: string;
65
- }): { id: string; publicKey: string; signature: string; signedAt: number; nonce?: string } {
66
- const version = opts.nonce ? "v2" : "v1";
67
- const payload = [
68
- version,
69
- identity.deviceId,
70
- opts.clientId,
71
- opts.clientMode,
72
- opts.role,
73
- opts.scopes.join(","),
74
- String(opts.signedAtMs),
75
- opts.token ?? "",
76
- ...(version === "v2" ? [opts.nonce ?? ""] : []),
77
- ].join("|");
78
-
79
- const key = createPrivateKey(identity.privateKeyPem);
80
- const signature = base64UrlEncode(sign(null, Buffer.from(payload, "utf8"), key));
81
-
82
- return {
83
- id: identity.deviceId,
84
- publicKey: base64UrlEncode(rawPublicKeyBytes(identity.publicKeyPem)),
85
- signature,
86
- signedAt: opts.signedAtMs,
87
- nonce: opts.nonce,
88
- };
89
- }
90
-
91
- // ---------------------------------------------------------------------------
92
- // Wire frame types (OpenClaw protocol v3)
93
- // ---------------------------------------------------------------------------
94
-
95
- interface ReqFrame {
96
- type: "req";
97
- id: string;
98
- method: string;
99
- params?: unknown;
100
- }
101
-
102
- interface ResFrame {
103
- type: "res";
104
- id: string;
105
- ok: boolean;
106
- payload?: unknown;
107
- error?: { message?: string };
108
- }
109
-
110
- interface EvtFrame {
111
- type: "event";
112
- event: string;
113
- payload?: unknown;
114
- seq?: number;
115
- }
116
-
117
- const PROTOCOL_VERSION = 3;
118
-
119
- // ---------------------------------------------------------------------------
120
- // Public API
121
- // ---------------------------------------------------------------------------
122
-
123
- export interface GatewayClientOptions {
124
- url: string;
125
- token?: string;
126
- password?: string;
127
- onConnected: () => void;
128
- onEvent: (eventName: string, payload: unknown) => void;
129
- onDisconnected: (reason: string) => void;
130
- }
131
-
132
- export class OpenClawGatewayClient {
133
- private ws: WebSocket | null = null;
134
- private pending = new Map<string, { resolve: (v: unknown) => void; reject: (e: unknown) => void }>();
135
- private backoffMs = 1000;
136
- private stopped = false;
137
- private connectNonce: string | null = null;
138
- private connectSent = false;
139
- private storedDeviceToken: string | null = null;
140
- private connectTimer: ReturnType<typeof setTimeout> | null = null;
141
- private tickTimer: ReturnType<typeof setInterval> | null = null;
142
- private lastTick = 0;
143
- private tickIntervalMs = 30_000;
144
- private readonly identity: DeviceIdentity;
145
-
146
- constructor(private readonly opts: GatewayClientOptions) {
147
- this.identity = loadOrCreateDeviceIdentity();
148
- }
149
-
150
- start(): void {
151
- if (this.stopped) return;
152
- this.ws = new WebSocket(this.opts.url, { maxPayload: 25 * 1024 * 1024 });
153
-
154
- this.ws.on("open", () => {
155
- this.connectNonce = null;
156
- this.connectSent = false;
157
- // Fallback: send connect after 1 s if challenge hasn't arrived
158
- this.connectTimer = setTimeout(() => this.sendConnect(), 1000);
159
- });
160
-
161
- this.ws.on("message", (data) => {
162
- const raw = typeof data === "string" ? data : data.toString();
163
- this.handleMessage(raw);
164
- });
165
-
166
- this.ws.on("close", (code, reason) => {
167
- const reasonText = reason.toString() || `code ${code}`;
168
- this.teardown();
169
- this.opts.onDisconnected(reasonText);
170
- this.scheduleReconnect();
171
- });
172
-
173
- this.ws.on("error", (err) => {
174
- console.error(`[gateway-client] ws error: ${String(err)}`);
175
- });
176
- }
177
-
178
- stop(): void {
179
- this.stopped = true;
180
- this.teardown();
181
- this.ws?.close();
182
- this.ws = null;
183
- this.flushPending(new Error("gateway client stopped"));
184
- }
185
-
186
- send(method: string, params?: unknown): void {
187
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
188
- throw new Error("gateway not connected");
189
- }
190
- const frame: ReqFrame = { type: "req", id: randomUUID(), method, params };
191
- this.ws.send(JSON.stringify(frame));
192
- }
193
-
194
- async request<T = unknown>(method: string, params?: unknown): Promise<T> {
195
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
196
- throw new Error("gateway not connected");
197
- }
198
- // Debug: log chat.send with attachments
199
- if (method === "chat.send") {
200
- const p = params as any;
201
- if (p.attachments && p.attachments.length > 0) {
202
- console.log(`[gateway-client] Sending chat.send to gateway, message="${p.message}", attachments count=${p.attachments.length}`);
203
- }
204
- }
205
- const id = randomUUID();
206
- const frame: ReqFrame = { type: "req", id, method, params };
207
- const p = new Promise<T>((resolve, reject) => {
208
- this.pending.set(id, { resolve: (v) => resolve(v as T), reject });
209
- });
210
- this.ws.send(JSON.stringify(frame));
211
- return p;
212
- }
213
-
214
- // -------------------------------------------------------------------------
215
-
216
- private sendConnect(): void {
217
- if (this.connectSent) return;
218
- this.connectSent = true;
219
- if (this.connectTimer) { clearTimeout(this.connectTimer); this.connectTimer = null; }
220
-
221
- const role = "operator";
222
- const scopes = ["operator.admin", "operator.read", "operator.write", "operator.approvals", "operator.pairing"];
223
- const clientId = "openclaw-macos";
224
- const clientMode = "ui";
225
- const signedAtMs = Date.now();
226
- const nonce = this.connectNonce ?? undefined;
227
- const authToken = this.storedDeviceToken ?? this.opts.token;
228
-
229
- const device = buildSignedDevice(this.identity, {
230
- clientId, clientMode, role, scopes, signedAtMs,
231
- token: authToken ?? undefined,
232
- nonce,
233
- });
234
-
235
- const params = {
236
- minProtocol: PROTOCOL_VERSION,
237
- maxProtocol: PROTOCOL_VERSION,
238
- role,
239
- scopes,
240
- caps: ["tool-events"],
241
- client: {
242
- id: clientId,
243
- displayName: "PocketClaw",
244
- version: "1.0.0",
245
- platform: process.platform,
246
- mode: clientMode,
247
- },
248
- device,
249
- auth: authToken || this.opts.password
250
- ? { token: authToken, password: this.opts.password }
251
- : undefined,
252
- };
253
-
254
- this.request<{ policy?: { tickIntervalMs?: number }; auth?: { deviceToken?: string } }>("connect", params)
255
- .then((helloOk) => {
256
- const deviceToken = helloOk?.auth?.deviceToken;
257
- if (typeof deviceToken === "string") {
258
- this.storedDeviceToken = deviceToken;
259
- }
260
- if (typeof helloOk?.policy?.tickIntervalMs === "number") {
261
- this.tickIntervalMs = helloOk.policy.tickIntervalMs;
262
- }
263
- this.backoffMs = 1000;
264
- this.lastTick = Date.now();
265
- this.startTickWatch();
266
- this.opts.onConnected();
267
- })
268
- .catch((err: unknown) => {
269
- console.error(`[gateway-client] connect failed: ${String(err)}`);
270
- // Clear stale device token so the next reconnect uses the base token from config
271
- this.storedDeviceToken = null;
272
- this.ws?.close(1008, "connect failed");
273
- });
274
- }
275
-
276
- private handleMessage(raw: string): void {
277
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
278
- let parsed: any;
279
- try {
280
- parsed = JSON.parse(raw);
281
- } catch {
282
- return;
283
- }
284
- if (typeof parsed?.type !== "string") return;
285
-
286
- if (parsed.type === "event") {
287
- const evt = parsed as EvtFrame;
288
-
289
- if (evt.event === "connect.challenge") {
290
- const nonce = (evt.payload as { nonce?: unknown } | undefined)?.nonce;
291
- if (typeof nonce === "string") {
292
- this.connectNonce = nonce;
293
- this.sendConnect();
294
- }
295
- return;
296
- }
297
-
298
- if (evt.event === "tick") { this.lastTick = Date.now(); return; }
299
-
300
- this.opts.onEvent(evt.event!, evt.payload ?? null);
301
- return;
302
- }
303
-
304
- if (parsed.type === "res") {
305
- const res = parsed as ResFrame;
306
- const pending = this.pending.get(res.id);
307
- if (!pending) return;
308
- this.pending.delete(res.id);
309
- if (res.ok) pending.resolve(res.payload);
310
- else pending.reject(new Error(res.error?.message ?? "gateway error"));
311
- return;
312
- }
313
-
314
- // Handle incoming req frames from OpenClaw (e.g. chat.push in OpenClaw 2026.3.2+)
315
- if (parsed.type === "req") {
316
- const id: string | undefined = parsed.id;
317
- const method: string | undefined = parsed.method;
318
- const params: unknown = parsed.params;
319
- console.log(`[gateway-client] incoming req: method=${method} params=${JSON.stringify(params).slice(0, 300)}`);
320
- // Ack immediately so OpenClaw doesn't retry
321
- if (id && this.ws?.readyState === WebSocket.OPEN) {
322
- this.ws.send(JSON.stringify({ type: "res", id, ok: true }));
323
- }
324
- // Forward chat.push as a "chat" event so the relay server can broadcast it to iOS
325
- if (method === "chat.push" && params != null) {
326
- this.opts.onEvent("chat", params);
327
- }
328
- return;
329
- }
330
- }
331
-
332
- private startTickWatch(): void {
333
- if (this.tickTimer) clearInterval(this.tickTimer);
334
- const interval = Math.max(this.tickIntervalMs, 1000);
335
- this.tickTimer = setInterval(() => {
336
- if (this.stopped || !this.lastTick) return;
337
- if (Date.now() - this.lastTick > this.tickIntervalMs * 2) {
338
- this.ws?.close(4000, "tick timeout");
339
- }
340
- }, interval);
341
- }
342
-
343
- private scheduleReconnect(): void {
344
- if (this.stopped) return;
345
- const delay = this.backoffMs;
346
- this.backoffMs = Math.min(this.backoffMs * 2, 30_000);
347
- setTimeout(() => this.start(), delay).unref();
348
- }
349
-
350
- private teardown(): void {
351
- if (this.connectTimer) { clearTimeout(this.connectTimer); this.connectTimer = null; }
352
- if (this.tickTimer) { clearInterval(this.tickTimer); this.tickTimer = null; }
353
- }
354
-
355
- private flushPending(err: Error): void {
356
- for (const p of this.pending.values()) p.reject(err);
357
- this.pending.clear();
358
- }
359
- }
@@ -1,37 +0,0 @@
1
- export interface ReconnectOptions {
2
- initialDelayMs?: number;
3
- maxDelayMs?: number;
4
- onRetry?: (attempt: number, delayMs: number) => void;
5
- }
6
-
7
- /**
8
- * Implements exponential backoff: 500ms → 1s → 2s → ... → 30s cap.
9
- * Calls `connect` repeatedly until `connect` returns false (permanent failure)
10
- * or the returned WebSocket connection stays alive.
11
- *
12
- * The `connect` callback should return `true` if it attempted a connection
13
- * and `false` if it should stop retrying.
14
- */
15
- export async function withReconnect(
16
- connect: () => Promise<boolean>,
17
- opts: ReconnectOptions = {}
18
- ): Promise<void> {
19
- const initialDelay = opts.initialDelayMs ?? 500;
20
- const maxDelay = opts.maxDelayMs ?? 30_000;
21
- let attempt = 0;
22
- let delay = initialDelay;
23
-
24
- while (true) {
25
- const shouldRetry = await connect();
26
- if (!shouldRetry) break;
27
-
28
- attempt++;
29
- opts.onRetry?.(attempt, delay);
30
- await sleep(delay);
31
- delay = Math.min(delay * 2, maxDelay);
32
- }
33
- }
34
-
35
- function sleep(ms: number): Promise<void> {
36
- return new Promise((resolve) => setTimeout(resolve, ms));
37
- }