playclaw 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.
Files changed (3) hide show
  1. package/README.md +43 -0
  2. package/index.js +438 -0
  3. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # playclaw
2
+
3
+ Connect your [OpenClaw](https://openclaw.dev) agent to [PlayClaw](https://playclaw.info) — the professional AI agent testing playground.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx playclaw PC-XXXX-XXXX-XXXX
9
+ ```
10
+
11
+ Replace `PC-XXXX-XXXX-XXXX` with your PlayClaw connection token from [playclaw.info/connect](https://playclaw.info/connect).
12
+
13
+ ## What it does
14
+
15
+ 1. Reads your OpenClaw gateway token (auto-detects from `~/.openclaw/openclaw.json`)
16
+ 2. Connects to your local OpenClaw gateway on `localhost:18789`
17
+ 3. Subscribes to a private Supabase Realtime channel using your PC token
18
+ 4. Relays messages between the PlayClaw playground and your agent in real time
19
+
20
+ ## Options
21
+
22
+ ```
23
+ npx playclaw <PC-TOKEN> [options]
24
+
25
+ Options:
26
+ --gateway-token, -g <token> OpenClaw gateway bearer token
27
+ --port, -p <port> OpenClaw gateway port (default: 18789)
28
+ --help, -h Show help
29
+ ```
30
+
31
+ ## Environment variables
32
+
33
+ ```bash
34
+ OPENCLAW_GATEWAY_TOKEN=<token> npx playclaw PC-XXXX-XXXX-XXXX
35
+ ```
36
+
37
+ ## Privacy
38
+
39
+ Only message payloads travel through the private Supabase Realtime channel. Your system prompts, API keys, and agent logic stay on your server.
40
+
41
+ ## License
42
+
43
+ MIT — [playclaw.info](https://playclaw.info)
package/index.js ADDED
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * playclaw — CLI bridge for PlayClaw
5
+ *
6
+ * Usage:
7
+ * npx playclaw PC-XXXX-XXXX-XXXX
8
+ * npx playclaw PC-XXXX-XXXX-XXXX --gateway-token <token>
9
+ * npx playclaw PC-XXXX-XXXX-XXXX --port 18789
10
+ *
11
+ * What it does:
12
+ * 1. Reads your OpenClaw gateway token (auto-detects or you provide it)
13
+ * 2. Connects to your local OpenClaw gateway (localhost:18789)
14
+ * 3. Subscribes to PlayClaw's Supabase Realtime channel (bridge:<token>)
15
+ * 4. Relays messages between the PlayClaw playground and your agent
16
+ *
17
+ * No separate bridge server needed — everything runs through Supabase.
18
+ */
19
+
20
+ "use strict";
21
+
22
+ const { createClient } = require("@supabase/supabase-js");
23
+ const WebSocket = require("ws");
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+ const os = require("os");
27
+ const http = require("http");
28
+
29
+ // ─── Parse args ───────────────────────────────────────────────────────────────
30
+
31
+ const args = process.argv.slice(2);
32
+
33
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
34
+ printHelp();
35
+ process.exit(0);
36
+ }
37
+
38
+ const playclawToken = args[0];
39
+ if (!playclawToken.startsWith("PC-")) {
40
+ console.error("❌ Invalid PlayClaw token. It should start with PC-");
41
+ console.error(" Get your token from: https://playclaw.info/connect");
42
+ process.exit(1);
43
+ }
44
+
45
+ // Optional flags
46
+ const flagIndex = (flag) => args.findIndex((a) => a === flag);
47
+ const flagValue = (flag) => {
48
+ const i = flagIndex(flag);
49
+ return i !== -1 ? args[i + 1] : null;
50
+ };
51
+
52
+ const gatewayPortArg = parseInt(flagValue("--port") || flagValue("-p")) || null;
53
+ const gatewayTokenArg = flagValue("--gateway-token") || flagValue("-g");
54
+
55
+ // ─── Config ───────────────────────────────────────────────────────────────────
56
+
57
+ // PlayClaw's Supabase public credentials — safe to embed (anon key is public)
58
+ // These are overridable via env vars for local development / self-hosting.
59
+ const SUPABASE_URL =
60
+ process.env.PLAYCLAW_SUPABASE_URL || "https://bbplxuprwmbkbslvwjfv.supabase.co";
61
+ const SUPABASE_ANON_KEY =
62
+ process.env.PLAYCLAW_SUPABASE_ANON_KEY ||
63
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJicGx4dXByd21ia2JzbHZ3amZ2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIzMjY2OTIsImV4cCI6MjA4NzkwMjY5Mn0.Y_R6ozJmyCiu_oCKJYJ7Bt17xbztRlZEk7eAI2D81fM";
64
+
65
+ const OPENCLAW_PORT =
66
+ gatewayPortArg ||
67
+ parseInt(process.env.OPENCLAW_PORT || "") ||
68
+ 18789;
69
+
70
+ // ─── Auto-detect OpenClaw gateway token ─────────────────────────────────────
71
+
72
+ function detectGatewayToken() {
73
+ // 1. CLI flag
74
+ if (gatewayTokenArg) return gatewayTokenArg;
75
+
76
+ // 2. Environment variable
77
+ if (process.env.OPENCLAW_GATEWAY_TOKEN) return process.env.OPENCLAW_GATEWAY_TOKEN;
78
+
79
+ // 3. Auto-detect from ~/.openclaw/openclaw.json
80
+ const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
81
+ try {
82
+ if (!fs.existsSync(configPath)) return null;
83
+ // openclaw.json supports JSON5 (comments + trailing commas) — strip them
84
+ const raw = fs.readFileSync(configPath, "utf-8");
85
+ const cleaned = raw
86
+ .replace(/\/\/[^\n]*/g, "") // strip // comments
87
+ .replace(/\/\*[\s\S]*?\*\//g, "") // strip /* */ comments
88
+ .replace(/,\s*([}\]])/g, "$1"); // strip trailing commas
89
+ const config = JSON.parse(cleaned);
90
+ return config?.gateway?.auth?.token || null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ const gatewayToken = detectGatewayToken();
97
+
98
+ if (!gatewayToken) {
99
+ console.error("");
100
+ console.error("❌ Could not find your OpenClaw gateway token.");
101
+ console.error("");
102
+ console.error(" Option A — provide it directly:");
103
+ console.error(` npx playclaw ${playclawToken} --gateway-token <your-token>`);
104
+ console.error("");
105
+ console.error(" Option B — set an env var:");
106
+ console.error(` OPENCLAW_GATEWAY_TOKEN=<token> npx playclaw ${playclawToken}`);
107
+ console.error("");
108
+ console.error(" Option C — find it in your OpenClaw config:");
109
+ console.error(" openclaw config get gateway.auth.token");
110
+ console.error(" or look in: ~/.openclaw/openclaw.json → gateway.auth.token");
111
+ console.error("");
112
+ process.exit(1);
113
+ }
114
+
115
+ // ─── Logging ─────────────────────────────────────────────────────────────────
116
+
117
+ const c = {
118
+ reset: "\x1b[0m",
119
+ bold: "\x1b[1m",
120
+ dim: "\x1b[2m",
121
+ green: "\x1b[32m",
122
+ yellow: "\x1b[33m",
123
+ red: "\x1b[31m",
124
+ cyan: "\x1b[36m",
125
+ magenta: "\x1b[35m",
126
+ };
127
+
128
+ function log(icon, msg, color = c.reset) {
129
+ const time = new Date().toLocaleTimeString("en-US", { hour12: false });
130
+ console.log(`${c.dim}${time}${c.reset} ${icon} ${color}${msg}${c.reset}`);
131
+ }
132
+
133
+ // ─── State ────────────────────────────────────────────────────────────────────
134
+
135
+ let openclawWs = null;
136
+ let bridgeChannel = null;
137
+ let openclawReady = false;
138
+ let pendingMessages = []; // queued while OpenClaw reconnects
139
+ let reconnectTimer = null;
140
+
141
+ // ─── Supabase client ─────────────────────────────────────────────────────────
142
+
143
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
144
+ realtime: { params: { eventsPerSecond: 10 } },
145
+ });
146
+
147
+ // ─── OpenClaw Gateway connection ─────────────────────────────────────────────
148
+
149
+ function connectToOpenClaw() {
150
+ openclawReady = false;
151
+
152
+ openclawWs = new WebSocket(`ws://localhost:${OPENCLAW_PORT}`, {
153
+ headers: { Authorization: `Bearer ${gatewayToken}` },
154
+ });
155
+
156
+ openclawWs.on("open", () => {
157
+ // Send OpenClaw handshake — identifies us as an "operator" client
158
+ openclawWs.send(
159
+ JSON.stringify({
160
+ type: "req",
161
+ id: `playclaw-${Date.now()}`,
162
+ method: "connect",
163
+ params: {
164
+ role: "operator",
165
+ token: gatewayToken,
166
+ client: "playclaw",
167
+ version: "1.0.0",
168
+ },
169
+ })
170
+ );
171
+ });
172
+
173
+ openclawWs.on("message", (raw) => {
174
+ let msg;
175
+ try { msg = JSON.parse(raw.toString()); } catch { return; }
176
+
177
+ // Handshake accepted — we're in
178
+ if (msg.type === "res" && msg.ok && !openclawReady) {
179
+ openclawReady = true;
180
+ log("✅", `OpenClaw gateway connected (port ${OPENCLAW_PORT})`, c.green);
181
+
182
+ // Flush messages that arrived while we were reconnecting
183
+ while (pendingMessages.length > 0) {
184
+ const queued = pendingMessages.shift();
185
+ sendToOpenClaw(queued.content, queued.sessionId);
186
+ }
187
+
188
+ // Connect to PlayClaw via Supabase Realtime (only once)
189
+ if (!bridgeChannel) connectToBridge();
190
+ return;
191
+ }
192
+
193
+ // Auth failure — stop immediately
194
+ if (msg.type === "res" && !msg.ok) {
195
+ log("❌", `OpenClaw auth failed: ${msg.error?.message || "Unknown error"}`, c.red);
196
+ log(" ", "Check your gateway token and try again.", c.yellow);
197
+ process.exit(1);
198
+ }
199
+
200
+ // Agent response events
201
+ if (msg.type === "event") {
202
+ handleOpenClawEvent(msg);
203
+ }
204
+ });
205
+
206
+ openclawWs.on("error", (err) => {
207
+ if (err.code === "ECONNREFUSED") {
208
+ log("⚠️ ", `OpenClaw not found on port ${OPENCLAW_PORT}. Is it running?`, c.yellow);
209
+ log(" ", "Start it with: openclaw gateway run", c.dim);
210
+ } else {
211
+ log("❌", `OpenClaw error: ${err.message}`, c.red);
212
+ }
213
+ });
214
+
215
+ openclawWs.on("close", () => {
216
+ openclawReady = false;
217
+ log("⚠️ ", "OpenClaw disconnected. Reconnecting in 5s...", c.yellow);
218
+ clearTimeout(reconnectTimer);
219
+ reconnectTimer = setTimeout(connectToOpenClaw, 5000);
220
+ });
221
+ }
222
+
223
+ // ─── Handle events from OpenClaw ─────────────────────────────────────────────
224
+
225
+ function handleOpenClawEvent(msg) {
226
+ const eventName = msg.event || "";
227
+
228
+ // We listen for several possible response event names.
229
+ // OpenClaw's exact event depends on version — DEBUG=1 logs all events.
230
+ const responseEvents = [
231
+ "agent.response",
232
+ "message.sent",
233
+ "turn.complete",
234
+ "reply.sent",
235
+ "assistant.message",
236
+ "output.complete",
237
+ ];
238
+
239
+ if (responseEvents.some((e) => eventName.includes(e))) {
240
+ const content =
241
+ msg.payload?.content ||
242
+ msg.payload?.text ||
243
+ msg.payload?.message ||
244
+ msg.payload?.reply ||
245
+ msg.payload?.output ||
246
+ JSON.stringify(msg.payload);
247
+
248
+ if (content) {
249
+ log("📨", `Agent responded: "${String(content).substring(0, 80)}..."`, c.cyan);
250
+ forwardResponseToPlayClaw(String(content));
251
+ }
252
+ } else if (process.env.DEBUG) {
253
+ log("🔍", `[DEBUG] OpenClaw event: ${eventName}`, c.dim);
254
+ if (process.env.DEBUG === "2") {
255
+ console.log(" payload:", JSON.stringify(msg.payload, null, 2));
256
+ }
257
+ }
258
+ }
259
+
260
+ // ─── Send message to OpenClaw ─────────────────────────────────────────────────
261
+
262
+ function sendToOpenClaw(content, sessionId) {
263
+ if (!openclawReady) {
264
+ pendingMessages.push({ content, sessionId });
265
+ return;
266
+ }
267
+
268
+ // POST to /hooks/agent — OpenClaw's async webhook endpoint (returns 202)
269
+ // The agent processes it and emits an event back on the WebSocket.
270
+ const body = JSON.stringify({
271
+ message: content,
272
+ agentId: "hooks",
273
+ wakeMode: "now",
274
+ channel: "last",
275
+ sessionKey: sessionId || "playclaw",
276
+ timeoutSeconds: 60,
277
+ });
278
+
279
+ const req = http.request(
280
+ {
281
+ hostname: "localhost",
282
+ port: OPENCLAW_PORT,
283
+ path: "/hooks/agent",
284
+ method: "POST",
285
+ headers: {
286
+ "Content-Type": "application/json",
287
+ "Authorization": `Bearer ${gatewayToken}`,
288
+ "x-openclaw-token": gatewayToken,
289
+ "Content-Length": Buffer.byteLength(body),
290
+ },
291
+ },
292
+ (res) => {
293
+ if (res.statusCode === 202) {
294
+ log("📤", `Message sent to agent (session: ${sessionId || "default"})`, c.magenta);
295
+ } else if (res.statusCode === 401) {
296
+ log("❌", "OpenClaw auth failed — check your gateway token", c.red);
297
+ } else {
298
+ log("⚠️ ", `Unexpected status from OpenClaw: ${res.statusCode}`, c.yellow);
299
+ }
300
+ }
301
+ );
302
+
303
+ req.on("error", (err) => {
304
+ log("❌", `Failed to reach OpenClaw: ${err.message}`, c.red);
305
+ forwardErrorToPlayClaw("Could not reach OpenClaw gateway. Is it running?");
306
+ });
307
+
308
+ req.write(body);
309
+ req.end();
310
+ }
311
+
312
+ // ─── PlayClaw Bridge via Supabase Realtime ────────────────────────────────────
313
+
314
+ function connectToBridge() {
315
+ // Channel name = PC token → acts as a shared secret between CLI and playground
316
+ bridgeChannel = supabase.channel(`bridge:${playclawToken}`, {
317
+ config: {
318
+ broadcast: { ack: false }, // fire-and-forget is fine for low-latency relay
319
+ presence: { key: "cli" }, // track CLI presence so playground knows we're live
320
+ },
321
+ });
322
+
323
+ bridgeChannel
324
+ // Listen for messages from the playground
325
+ .on("broadcast", { event: "playground:message" }, ({ payload }) => {
326
+ log("💬", `From playground: "${payload.content?.substring(0, 60)}"`, c.cyan);
327
+ sendToOpenClaw(payload.content, payload.session_id);
328
+ })
329
+ .subscribe(async (status) => {
330
+ if (status === "SUBSCRIBED") {
331
+ // Signal that the CLI is online (playground reads this via presence)
332
+ await bridgeChannel.track({
333
+ online_at: new Date().toISOString(),
334
+ token: playclawToken,
335
+ });
336
+
337
+ log("✅", "PlayClaw Bridge connected (Supabase Realtime)", c.green);
338
+ log("🎮", "Agent is ready to be tested!", c.bold);
339
+ log(" ", "Open https://playclaw.info/playground to start.", c.dim);
340
+ console.log("");
341
+
342
+ } else if (status === "CHANNEL_ERROR") {
343
+ log("❌", "Bridge connection failed. Check your PC token.", c.red);
344
+
345
+ } else if (status === "TIMED_OUT") {
346
+ log("⚠️ ", "Bridge timed out. Reconnecting in 5s...", c.yellow);
347
+ supabase.removeChannel(bridgeChannel);
348
+ bridgeChannel = null;
349
+ setTimeout(connectToBridge, 5000);
350
+
351
+ } else if (status === "CLOSED") {
352
+ log("⚠️ ", "Bridge disconnected. Reconnecting in 5s...", c.yellow);
353
+ bridgeChannel = null;
354
+ setTimeout(connectToBridge, 5000);
355
+ }
356
+ });
357
+ }
358
+
359
+ function forwardResponseToPlayClaw(content, sessionId) {
360
+ if (!bridgeChannel) {
361
+ log("⚠️ ", "Bridge not ready — response dropped", c.yellow);
362
+ return;
363
+ }
364
+ bridgeChannel.send({
365
+ type: "broadcast",
366
+ event: "agent:response",
367
+ payload: { content, session_id: sessionId || null },
368
+ });
369
+ }
370
+
371
+ function forwardErrorToPlayClaw(message) {
372
+ if (!bridgeChannel) return;
373
+ bridgeChannel.send({
374
+ type: "broadcast",
375
+ event: "agent:error",
376
+ payload: { message },
377
+ });
378
+ }
379
+
380
+ // ─── Startup banner ───────────────────────────────────────────────────────────
381
+
382
+ function printBanner() {
383
+ console.log("");
384
+ console.log(`${c.bold}${c.magenta} 🦞 PlayClaw Bridge${c.reset}`);
385
+ console.log(`${c.dim} https://playclaw.info${c.reset}`);
386
+ console.log("");
387
+ console.log(`${c.dim} PC Token:${c.reset} ${playclawToken}`);
388
+ console.log(`${c.dim} Gateway:${c.reset} localhost:${OPENCLAW_PORT}`);
389
+ console.log(`${c.dim} Relay:${c.reset} Supabase Realtime`);
390
+ console.log("");
391
+ }
392
+
393
+ function printHelp() {
394
+ console.log("");
395
+ console.log(" Usage: npx playclaw <PC-TOKEN> [options]");
396
+ console.log("");
397
+ console.log(" Arguments:");
398
+ console.log(" PC-TOKEN Your PlayClaw connection token (from playclaw.info/connect)");
399
+ console.log("");
400
+ console.log(" Options:");
401
+ console.log(" --gateway-token, -g <token> OpenClaw gateway bearer token");
402
+ console.log(" --port, -p <port> OpenClaw gateway port (default: 18789)");
403
+ console.log(" --help, -h Show this help");
404
+ console.log("");
405
+ console.log(" Examples:");
406
+ console.log(" npx playclaw PC-XXXX-XXXX-XXXX");
407
+ console.log(" npx playclaw PC-XXXX-XXXX-XXXX --gateway-token my-secret-token");
408
+ console.log(" OPENCLAW_GATEWAY_TOKEN=mytoken npx playclaw PC-XXXX-XXXX-XXXX");
409
+ console.log("");
410
+ console.log(" Auto-detection:");
411
+ console.log(" The gateway token is auto-read from ~/.openclaw/openclaw.json");
412
+ console.log(" if not provided. Run `openclaw config get gateway.auth.token`");
413
+ console.log(" to find yours.");
414
+ console.log("");
415
+ }
416
+
417
+ // ─── Graceful shutdown ────────────────────────────────────────────────────────
418
+
419
+ async function shutdown() {
420
+ console.log("");
421
+ log("👋", "Shutting down PlayClaw bridge...", c.dim);
422
+
423
+ if (bridgeChannel) {
424
+ await bridgeChannel.untrack().catch(() => {});
425
+ await supabase.removeChannel(bridgeChannel).catch(() => {});
426
+ }
427
+
428
+ openclawWs?.close();
429
+ process.exit(0);
430
+ }
431
+
432
+ process.on("SIGINT", shutdown);
433
+ process.on("SIGTERM", shutdown);
434
+
435
+ // ─── Main ─────────────────────────────────────────────────────────────────────
436
+
437
+ printBanner();
438
+ connectToOpenClaw();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "playclaw",
3
+ "version": "1.0.0",
4
+ "description": "Connect your OpenClaw agent to PlayClaw — the professional testing playground",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "playclaw": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "dependencies": {
13
+ "@supabase/supabase-js": "^2.49.1",
14
+ "ws": "^8.18.0"
15
+ },
16
+ "keywords": [
17
+ "openclaw",
18
+ "playclaw",
19
+ "ai-agent",
20
+ "bridge",
21
+ "testing",
22
+ "cli"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "license": "MIT",
28
+ "author": "PlayClaw <hello@playclaw.info>",
29
+ "homepage": "https://playclaw.info",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/WonuSama/PLAYCLAW.git",
33
+ "directory": "packages/playclaw"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/WonuSama/PLAYCLAW/issues"
37
+ },
38
+ "files": [
39
+ "index.js",
40
+ "README.md"
41
+ ]
42
+ }