playclaw 1.0.0 → 1.0.2
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/index.js +230 -136
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
* What it does:
|
|
12
12
|
* 1. Reads your OpenClaw gateway token (auto-detects or you provide it)
|
|
13
13
|
* 2. Connects to your local OpenClaw gateway (localhost:18789)
|
|
14
|
-
* 3.
|
|
14
|
+
* 3. Opens a private bridge channel to PlayClaw's cloud
|
|
15
15
|
* 4. Relays messages between the PlayClaw playground and your agent
|
|
16
|
-
*
|
|
17
|
-
* No separate bridge server needed — everything runs through Supabase.
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
18
|
"use strict";
|
|
@@ -26,7 +24,7 @@ const path = require("path");
|
|
|
26
24
|
const os = require("os");
|
|
27
25
|
const http = require("http");
|
|
28
26
|
|
|
29
|
-
// ─── Parse args
|
|
27
|
+
// ─── Parse args ────────────────────────────────────────────────────────────────
|
|
30
28
|
|
|
31
29
|
const args = process.argv.slice(2);
|
|
32
30
|
|
|
@@ -37,12 +35,11 @@ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
|
37
35
|
|
|
38
36
|
const playclawToken = args[0];
|
|
39
37
|
if (!playclawToken.startsWith("PC-")) {
|
|
40
|
-
console.error("
|
|
41
|
-
console.error("
|
|
38
|
+
console.error("\n ✗ Invalid PlayClaw token — it should start with PC-");
|
|
39
|
+
console.error(" Get your token at: https://playclaw.info/connect\n");
|
|
42
40
|
process.exit(1);
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
// Optional flags
|
|
46
43
|
const flagIndex = (flag) => args.findIndex((a) => a === flag);
|
|
47
44
|
const flagValue = (flag) => {
|
|
48
45
|
const i = flagIndex(flag);
|
|
@@ -52,13 +49,11 @@ const flagValue = (flag) => {
|
|
|
52
49
|
const gatewayPortArg = parseInt(flagValue("--port") || flagValue("-p")) || null;
|
|
53
50
|
const gatewayTokenArg = flagValue("--gateway-token") || flagValue("-g");
|
|
54
51
|
|
|
55
|
-
// ─── Config
|
|
52
|
+
// ─── Config ────────────────────────────────────────────────────────────────────
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
// These are overridable via env vars for local development / self-hosting.
|
|
59
|
-
const SUPABASE_URL =
|
|
54
|
+
const CLOUD_URL =
|
|
60
55
|
process.env.PLAYCLAW_SUPABASE_URL || "https://bbplxuprwmbkbslvwjfv.supabase.co";
|
|
61
|
-
const
|
|
56
|
+
const CLOUD_KEY =
|
|
62
57
|
process.env.PLAYCLAW_SUPABASE_ANON_KEY ||
|
|
63
58
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJicGx4dXByd21ia2JzbHZ3amZ2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIzMjY2OTIsImV4cCI6MjA4NzkwMjY5Mn0.Y_R6ozJmyCiu_oCKJYJ7Bt17xbztRlZEk7eAI2D81fM";
|
|
64
59
|
|
|
@@ -67,25 +62,20 @@ const OPENCLAW_PORT =
|
|
|
67
62
|
parseInt(process.env.OPENCLAW_PORT || "") ||
|
|
68
63
|
18789;
|
|
69
64
|
|
|
70
|
-
// ─── Auto-detect OpenClaw gateway token
|
|
65
|
+
// ─── Auto-detect OpenClaw gateway token ────────────────────────────────────────
|
|
71
66
|
|
|
72
67
|
function detectGatewayToken() {
|
|
73
|
-
// 1. CLI flag
|
|
74
68
|
if (gatewayTokenArg) return gatewayTokenArg;
|
|
75
|
-
|
|
76
|
-
// 2. Environment variable
|
|
77
69
|
if (process.env.OPENCLAW_GATEWAY_TOKEN) return process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
78
70
|
|
|
79
|
-
// 3. Auto-detect from ~/.openclaw/openclaw.json
|
|
80
71
|
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
81
72
|
try {
|
|
82
73
|
if (!fs.existsSync(configPath)) return null;
|
|
83
|
-
// openclaw.json supports JSON5 (comments + trailing commas) — strip them
|
|
84
74
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
85
75
|
const cleaned = raw
|
|
86
|
-
.replace(/\/\/[^\n]*/g, "")
|
|
87
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
88
|
-
.replace(/,\s*([}\]])/g, "$1");
|
|
76
|
+
.replace(/\/\/[^\n]*/g, "")
|
|
77
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
78
|
+
.replace(/,\s*([}\]])/g, "$1");
|
|
89
79
|
const config = JSON.parse(cleaned);
|
|
90
80
|
return config?.gateway?.auth?.token || null;
|
|
91
81
|
} catch {
|
|
@@ -96,23 +86,18 @@ function detectGatewayToken() {
|
|
|
96
86
|
const gatewayToken = detectGatewayToken();
|
|
97
87
|
|
|
98
88
|
if (!gatewayToken) {
|
|
99
|
-
console.error("");
|
|
100
|
-
console.error("
|
|
101
|
-
console.error(
|
|
102
|
-
console.error("
|
|
103
|
-
console.error(`
|
|
104
|
-
console.error("");
|
|
105
|
-
console.error("
|
|
106
|
-
console.error(
|
|
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("");
|
|
89
|
+
console.error("\n ✗ Could not find your OpenClaw gateway token.\n");
|
|
90
|
+
console.error(" Option A — provide it directly:");
|
|
91
|
+
console.error(` npx playclaw ${playclawToken} --gateway-token <your-token>\n`);
|
|
92
|
+
console.error(" Option B — set an env var:");
|
|
93
|
+
console.error(` OPENCLAW_GATEWAY_TOKEN=<token> npx playclaw ${playclawToken}\n`);
|
|
94
|
+
console.error(" Option C — auto-detect from config:");
|
|
95
|
+
console.error(" openclaw config get gateway.auth.token");
|
|
96
|
+
console.error(" (~/.openclaw/openclaw.json → gateway.auth.token)\n");
|
|
112
97
|
process.exit(1);
|
|
113
98
|
}
|
|
114
99
|
|
|
115
|
-
// ───
|
|
100
|
+
// ─── Colors ────────────────────────────────────────────────────────────────────
|
|
116
101
|
|
|
117
102
|
const c = {
|
|
118
103
|
reset: "\x1b[0m",
|
|
@@ -123,117 +108,201 @@ const c = {
|
|
|
123
108
|
red: "\x1b[31m",
|
|
124
109
|
cyan: "\x1b[36m",
|
|
125
110
|
magenta: "\x1b[35m",
|
|
111
|
+
gray: "\x1b[90m",
|
|
126
112
|
};
|
|
127
113
|
|
|
114
|
+
// ─── Spinner ───────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
class Spinner {
|
|
117
|
+
constructor() {
|
|
118
|
+
this.frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];
|
|
119
|
+
this.i = 0;
|
|
120
|
+
this._iv = null;
|
|
121
|
+
this._active = false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
start(text) {
|
|
125
|
+
this._active = true;
|
|
126
|
+
this.i = 0;
|
|
127
|
+
process.stdout.write("\n");
|
|
128
|
+
this._iv = setInterval(() => {
|
|
129
|
+
if (!this._active) return;
|
|
130
|
+
const f = this.frames[this.i++ % this.frames.length];
|
|
131
|
+
process.stdout.write(`\r ${c.cyan}${f}${c.reset} ${c.dim}${text}${c.reset} `);
|
|
132
|
+
}, 80);
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
success(text) {
|
|
137
|
+
this._active = false;
|
|
138
|
+
clearInterval(this._iv);
|
|
139
|
+
process.stdout.write(`\r ${c.green}✓${c.reset} ${text}${" ".repeat(10)}\n`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fail(text) {
|
|
143
|
+
this._active = false;
|
|
144
|
+
clearInterval(this._iv);
|
|
145
|
+
process.stdout.write(`\r ${c.red}✗${c.reset} ${c.red}${text}${c.reset}${" ".repeat(10)}\n\n`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const spinner = new Spinner();
|
|
150
|
+
|
|
151
|
+
// ─── Logging ───────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
128
153
|
function log(icon, msg, color = c.reset) {
|
|
129
154
|
const time = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
130
|
-
console.log(
|
|
155
|
+
console.log(` ${c.gray}${time}${c.reset} ${icon} ${color}${msg}${c.reset}`);
|
|
131
156
|
}
|
|
132
157
|
|
|
133
|
-
// ─── State
|
|
158
|
+
// ─── State ─────────────────────────────────────────────────────────────────────
|
|
134
159
|
|
|
135
|
-
let openclawWs
|
|
136
|
-
let bridgeChannel
|
|
137
|
-
let openclawReady
|
|
138
|
-
let pendingMessages = [];
|
|
160
|
+
let openclawWs = null;
|
|
161
|
+
let bridgeChannel = null;
|
|
162
|
+
let openclawReady = false;
|
|
163
|
+
let pendingMessages = [];
|
|
139
164
|
let reconnectTimer = null;
|
|
165
|
+
let gatewayConnected = false;
|
|
166
|
+
let bridgeConnected = false;
|
|
140
167
|
|
|
141
|
-
// ───
|
|
168
|
+
// ─── Cloud bridge client ───────────────────────────────────────────────────────
|
|
142
169
|
|
|
143
|
-
const
|
|
170
|
+
const cloud = createClient(CLOUD_URL, CLOUD_KEY, {
|
|
144
171
|
realtime: { params: { eventsPerSecond: 10 } },
|
|
145
172
|
});
|
|
146
173
|
|
|
147
|
-
// ─── OpenClaw Gateway connection
|
|
174
|
+
// ─── OpenClaw Gateway connection ───────────────────────────────────────────────
|
|
148
175
|
|
|
149
176
|
function connectToOpenClaw() {
|
|
150
177
|
openclawReady = false;
|
|
151
178
|
|
|
179
|
+
if (!gatewayConnected) {
|
|
180
|
+
spinner.start("Connecting to agent gateway...");
|
|
181
|
+
}
|
|
182
|
+
|
|
152
183
|
openclawWs = new WebSocket(`ws://localhost:${OPENCLAW_PORT}`, {
|
|
153
184
|
headers: { Authorization: `Bearer ${gatewayToken}` },
|
|
154
185
|
});
|
|
155
186
|
|
|
187
|
+
// OpenClaw uses a challenge-response handshake:
|
|
188
|
+
// 1. Server emits connect.challenge with a nonce
|
|
189
|
+
// 2. Client replies with connect req (correct params schema)
|
|
156
190
|
openclawWs.on("open", () => {
|
|
157
|
-
|
|
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
|
-
);
|
|
191
|
+
if (process.env.DEBUG) log("·", "[DEBUG] WS open — waiting for challenge", c.dim);
|
|
171
192
|
});
|
|
172
193
|
|
|
173
194
|
openclawWs.on("message", (raw) => {
|
|
174
195
|
let msg;
|
|
175
196
|
try { msg = JSON.parse(raw.toString()); } catch { return; }
|
|
176
197
|
|
|
177
|
-
//
|
|
198
|
+
// Step 1 — challenge received, send connect request
|
|
199
|
+
if (msg.type === "event" && msg.event === "connect.challenge") {
|
|
200
|
+
if (process.env.DEBUG) log("·", `[DEBUG] Challenge: nonce=${msg.payload?.nonce}`, c.dim);
|
|
201
|
+
|
|
202
|
+
openclawWs.send(JSON.stringify({
|
|
203
|
+
type: "req",
|
|
204
|
+
id: `playclaw-${Date.now()}`,
|
|
205
|
+
method: "connect",
|
|
206
|
+
params: {
|
|
207
|
+
minProtocol: 3,
|
|
208
|
+
maxProtocol: 3,
|
|
209
|
+
client: {
|
|
210
|
+
id: "cli",
|
|
211
|
+
version: "1.0.2",
|
|
212
|
+
platform: process.platform,
|
|
213
|
+
mode: "operator",
|
|
214
|
+
},
|
|
215
|
+
role: "operator",
|
|
216
|
+
scopes: ["operator.read", "operator.write"],
|
|
217
|
+
caps: [],
|
|
218
|
+
commands: [],
|
|
219
|
+
permissions: {},
|
|
220
|
+
auth: { token: gatewayToken },
|
|
221
|
+
locale: "en-US",
|
|
222
|
+
userAgent: "playclaw/1.0.2",
|
|
223
|
+
},
|
|
224
|
+
}));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Step 2 — handshake accepted
|
|
178
229
|
if (msg.type === "res" && msg.ok && !openclawReady) {
|
|
179
230
|
openclawReady = true;
|
|
180
|
-
log("✅", `OpenClaw gateway connected (port ${OPENCLAW_PORT})`, c.green);
|
|
181
231
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
232
|
+
if (!gatewayConnected) {
|
|
233
|
+
gatewayConnected = true;
|
|
234
|
+
spinner.success(`Agent gateway connected ${c.gray}· port ${OPENCLAW_PORT}${c.reset}`);
|
|
235
|
+
if (!bridgeChannel) connectToBridge();
|
|
236
|
+
} else {
|
|
237
|
+
log("✓", "Gateway reconnected", c.green);
|
|
238
|
+
while (pendingMessages.length > 0) {
|
|
239
|
+
const q = pendingMessages.shift();
|
|
240
|
+
sendToOpenClaw(q.content, q.sessionId);
|
|
241
|
+
}
|
|
186
242
|
}
|
|
187
|
-
|
|
188
|
-
// Connect to PlayClaw via Supabase Realtime (only once)
|
|
189
|
-
if (!bridgeChannel) connectToBridge();
|
|
190
243
|
return;
|
|
191
244
|
}
|
|
192
245
|
|
|
193
|
-
//
|
|
246
|
+
// Handshake rejected
|
|
194
247
|
if (msg.type === "res" && !msg.ok) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
248
|
+
const reason = msg.error?.message || "invalid credentials";
|
|
249
|
+
if (!gatewayConnected) {
|
|
250
|
+
spinner.fail(`Gateway auth failed — ${reason}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
} else {
|
|
253
|
+
log("✗", `Gateway auth error — ${reason}`, c.red);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
198
256
|
}
|
|
199
257
|
|
|
200
|
-
// Agent
|
|
258
|
+
// Agent events
|
|
201
259
|
if (msg.type === "event") {
|
|
202
260
|
handleOpenClawEvent(msg);
|
|
203
261
|
}
|
|
204
262
|
});
|
|
205
263
|
|
|
206
264
|
openclawWs.on("error", (err) => {
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
265
|
+
if (!gatewayConnected) {
|
|
266
|
+
if (err.code === "ECONNREFUSED") {
|
|
267
|
+
spinner.fail(`Gateway not found on port ${OPENCLAW_PORT} — is OpenClaw running?`);
|
|
268
|
+
} else {
|
|
269
|
+
spinner.fail(`Gateway error: ${err.message}`);
|
|
270
|
+
}
|
|
271
|
+
process.exit(1);
|
|
210
272
|
} else {
|
|
211
|
-
|
|
273
|
+
if (err.code === "ECONNREFUSED") {
|
|
274
|
+
log("⚠", `Gateway offline (port ${OPENCLAW_PORT}) — retrying in 5s`, c.yellow);
|
|
275
|
+
} else {
|
|
276
|
+
log("⚠", `Gateway error: ${err.message}`, c.yellow);
|
|
277
|
+
}
|
|
212
278
|
}
|
|
213
279
|
});
|
|
214
280
|
|
|
215
281
|
openclawWs.on("close", () => {
|
|
216
282
|
openclawReady = false;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
283
|
+
if (gatewayConnected) {
|
|
284
|
+
log("⟳", "Gateway disconnected — reconnecting in 5s...", c.gray);
|
|
285
|
+
clearTimeout(reconnectTimer);
|
|
286
|
+
reconnectTimer = setTimeout(connectToOpenClaw, 5000);
|
|
287
|
+
}
|
|
220
288
|
});
|
|
221
289
|
}
|
|
222
290
|
|
|
223
|
-
// ─── Handle events from OpenClaw
|
|
291
|
+
// ─── Handle events from OpenClaw ───────────────────────────────────────────────
|
|
224
292
|
|
|
225
293
|
function handleOpenClawEvent(msg) {
|
|
226
294
|
const eventName = msg.event || "";
|
|
227
295
|
|
|
228
|
-
|
|
229
|
-
|
|
296
|
+
if (process.env.DEBUG) {
|
|
297
|
+
log("·", `[DEBUG] Gateway event: ${eventName}`, c.dim);
|
|
298
|
+
if (process.env.DEBUG === "2") {
|
|
299
|
+
console.log(" payload:", JSON.stringify(msg.payload, null, 2));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
230
303
|
const responseEvents = [
|
|
231
|
-
"agent.response",
|
|
232
|
-
"
|
|
233
|
-
"turn.complete",
|
|
234
|
-
"reply.sent",
|
|
235
|
-
"assistant.message",
|
|
236
|
-
"output.complete",
|
|
304
|
+
"agent.response", "message.sent", "turn.complete",
|
|
305
|
+
"reply.sent", "assistant.message", "output.complete",
|
|
237
306
|
];
|
|
238
307
|
|
|
239
308
|
if (responseEvents.some((e) => eventName.includes(e))) {
|
|
@@ -246,18 +315,14 @@ function handleOpenClawEvent(msg) {
|
|
|
246
315
|
JSON.stringify(msg.payload);
|
|
247
316
|
|
|
248
317
|
if (content) {
|
|
249
|
-
|
|
318
|
+
const preview = String(content);
|
|
319
|
+
log("←", `Agent: "${preview.length > 80 ? preview.substring(0, 80) + "…" : preview}"`, c.cyan);
|
|
250
320
|
forwardResponseToPlayClaw(String(content));
|
|
251
321
|
}
|
|
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
322
|
}
|
|
258
323
|
}
|
|
259
324
|
|
|
260
|
-
// ─── Send message to OpenClaw
|
|
325
|
+
// ─── Send message to OpenClaw ──────────────────────────────────────────────────
|
|
261
326
|
|
|
262
327
|
function sendToOpenClaw(content, sessionId) {
|
|
263
328
|
if (!openclawReady) {
|
|
@@ -265,8 +330,6 @@ function sendToOpenClaw(content, sessionId) {
|
|
|
265
330
|
return;
|
|
266
331
|
}
|
|
267
332
|
|
|
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
333
|
const body = JSON.stringify({
|
|
271
334
|
message: content,
|
|
272
335
|
agentId: "hooks",
|
|
@@ -291,65 +354,74 @@ function sendToOpenClaw(content, sessionId) {
|
|
|
291
354
|
},
|
|
292
355
|
(res) => {
|
|
293
356
|
if (res.statusCode === 202) {
|
|
294
|
-
log("
|
|
357
|
+
log("→", `Forwarded to agent ${c.gray}(session: ${sessionId || "default"})${c.reset}`, c.reset);
|
|
295
358
|
} else if (res.statusCode === 401) {
|
|
296
|
-
log("
|
|
359
|
+
log("✗", "Gateway rejected — check your token", c.red);
|
|
297
360
|
} else {
|
|
298
|
-
log("
|
|
361
|
+
log("⚠", `Unexpected gateway response: ${res.statusCode}`, c.yellow);
|
|
299
362
|
}
|
|
300
363
|
}
|
|
301
364
|
);
|
|
302
365
|
|
|
303
366
|
req.on("error", (err) => {
|
|
304
|
-
log("
|
|
305
|
-
forwardErrorToPlayClaw("
|
|
367
|
+
log("✗", `Could not reach gateway: ${err.message}`, c.red);
|
|
368
|
+
forwardErrorToPlayClaw("Agent gateway unreachable. Is OpenClaw running?");
|
|
306
369
|
});
|
|
307
370
|
|
|
308
371
|
req.write(body);
|
|
309
372
|
req.end();
|
|
310
373
|
}
|
|
311
374
|
|
|
312
|
-
// ─── PlayClaw Bridge
|
|
375
|
+
// ─── PlayClaw Bridge ───────────────────────────────────────────────────────────
|
|
313
376
|
|
|
314
377
|
function connectToBridge() {
|
|
315
|
-
|
|
316
|
-
|
|
378
|
+
if (!bridgeConnected) {
|
|
379
|
+
spinner.start("Opening PlayClaw bridge...");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
bridgeChannel = cloud.channel(`bridge:${playclawToken}`, {
|
|
317
383
|
config: {
|
|
318
|
-
broadcast: { ack: false },
|
|
319
|
-
presence: { key: "cli" },
|
|
384
|
+
broadcast: { ack: false },
|
|
385
|
+
presence: { key: "cli" },
|
|
320
386
|
},
|
|
321
387
|
});
|
|
322
388
|
|
|
323
389
|
bridgeChannel
|
|
324
|
-
// Listen for messages from the playground
|
|
325
390
|
.on("broadcast", { event: "playground:message" }, ({ payload }) => {
|
|
326
|
-
|
|
391
|
+
const preview = payload.content || "";
|
|
392
|
+
log("↓", `Playground: "${preview.length > 60 ? preview.substring(0, 60) + "…" : preview}"`, c.magenta);
|
|
327
393
|
sendToOpenClaw(payload.content, payload.session_id);
|
|
328
394
|
})
|
|
329
395
|
.subscribe(async (status) => {
|
|
330
396
|
if (status === "SUBSCRIBED") {
|
|
331
|
-
// Signal that the CLI is online (playground reads this via presence)
|
|
332
397
|
await bridgeChannel.track({
|
|
333
398
|
online_at: new Date().toISOString(),
|
|
334
399
|
token: playclawToken,
|
|
335
400
|
});
|
|
336
401
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
402
|
+
if (!bridgeConnected) {
|
|
403
|
+
bridgeConnected = true;
|
|
404
|
+
spinner.success("PlayClaw bridge established");
|
|
405
|
+
printReady();
|
|
406
|
+
} else {
|
|
407
|
+
log("✓", "Bridge reconnected", c.green);
|
|
408
|
+
}
|
|
341
409
|
|
|
342
410
|
} else if (status === "CHANNEL_ERROR") {
|
|
343
|
-
|
|
411
|
+
if (!bridgeConnected) {
|
|
412
|
+
spinner.fail("Bridge failed — check your PC token at playclaw.info/connect");
|
|
413
|
+
} else {
|
|
414
|
+
log("✗", "Bridge error — check your PC token", c.red);
|
|
415
|
+
}
|
|
344
416
|
|
|
345
417
|
} else if (status === "TIMED_OUT") {
|
|
346
|
-
log("
|
|
347
|
-
|
|
418
|
+
log("⟳", "Bridge timed out — reconnecting in 5s...", c.gray);
|
|
419
|
+
cloud.removeChannel(bridgeChannel);
|
|
348
420
|
bridgeChannel = null;
|
|
349
421
|
setTimeout(connectToBridge, 5000);
|
|
350
422
|
|
|
351
423
|
} else if (status === "CLOSED") {
|
|
352
|
-
log("
|
|
424
|
+
log("⟳", "Bridge closed — reconnecting in 5s...", c.gray);
|
|
353
425
|
bridgeChannel = null;
|
|
354
426
|
setTimeout(connectToBridge, 5000);
|
|
355
427
|
}
|
|
@@ -358,7 +430,7 @@ function connectToBridge() {
|
|
|
358
430
|
|
|
359
431
|
function forwardResponseToPlayClaw(content, sessionId) {
|
|
360
432
|
if (!bridgeChannel) {
|
|
361
|
-
log("
|
|
433
|
+
log("⚠", "Bridge not ready — response dropped", c.yellow);
|
|
362
434
|
return;
|
|
363
435
|
}
|
|
364
436
|
bridgeChannel.send({
|
|
@@ -377,52 +449,74 @@ function forwardErrorToPlayClaw(message) {
|
|
|
377
449
|
});
|
|
378
450
|
}
|
|
379
451
|
|
|
380
|
-
// ─── Startup banner
|
|
452
|
+
// ─── Startup banner ────────────────────────────────────────────────────────────
|
|
381
453
|
|
|
382
454
|
function printBanner() {
|
|
455
|
+
const g = c.gray, r = c.reset, b = c.bold, m = c.magenta, d = c.dim, cy = c.cyan;
|
|
456
|
+
|
|
457
|
+
// OSC 8 hyperlink — clickable in Windows Terminal, iTerm2, GNOME Terminal, etc.
|
|
458
|
+
const twitterLink = `\x1b]8;;https://x.com/uxKero\x1b\\${b}@KeroClow${r}\x1b]8;;\x1b\\`;
|
|
459
|
+
|
|
383
460
|
console.log("");
|
|
384
|
-
console.log(
|
|
385
|
-
console.log(
|
|
461
|
+
console.log(` 🦞 ${b}${m}P L A Y C L A W${r}`);
|
|
462
|
+
console.log(` ${d}Agent Testing Playground · playclaw.info${r}`);
|
|
463
|
+
console.log(` ${d}by ${r}${twitterLink}${g} · ${d}x.com/uxKero${r}`);
|
|
464
|
+
console.log(` ${g}${"─".repeat(43)}${r}`);
|
|
386
465
|
console.log("");
|
|
387
|
-
console.log(
|
|
388
|
-
console.log(
|
|
389
|
-
|
|
466
|
+
console.log(` ${g}Token ${r} ${b}${playclawToken}${r}`);
|
|
467
|
+
console.log(` ${g}Gateway${r} localhost:${OPENCLAW_PORT}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─── Ready message ─────────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
function printReady() {
|
|
473
|
+
const line = ` ${c.gray}${"─".repeat(43)}${c.reset}`;
|
|
474
|
+
console.log("");
|
|
475
|
+
console.log(line);
|
|
476
|
+
console.log(` 🎮 ${c.bold}Your agent is live and ready!${c.reset}`);
|
|
477
|
+
console.log(` ${c.gray} playclaw.info/playground${c.reset}`);
|
|
478
|
+
console.log(line);
|
|
390
479
|
console.log("");
|
|
391
480
|
}
|
|
392
481
|
|
|
482
|
+
// ─── Help ──────────────────────────────────────────────────────────────────────
|
|
483
|
+
|
|
393
484
|
function printHelp() {
|
|
394
485
|
console.log("");
|
|
395
|
-
console.log("
|
|
486
|
+
console.log(" 🦞 PlayClaw — Agent Testing Playground");
|
|
487
|
+
console.log(" playclaw.info · by @KeroClow");
|
|
488
|
+
console.log("");
|
|
489
|
+
console.log(" Usage:");
|
|
490
|
+
console.log(" npx playclaw <PC-TOKEN> [options]");
|
|
396
491
|
console.log("");
|
|
397
492
|
console.log(" Arguments:");
|
|
398
|
-
console.log(" PC-TOKEN
|
|
493
|
+
console.log(" PC-TOKEN Your PlayClaw token (from playclaw.info/connect)");
|
|
399
494
|
console.log("");
|
|
400
495
|
console.log(" Options:");
|
|
401
496
|
console.log(" --gateway-token, -g <token> OpenClaw gateway bearer token");
|
|
402
|
-
console.log(" --port, -p <port>
|
|
497
|
+
console.log(" --port, -p <port> Gateway port (default: 18789)");
|
|
403
498
|
console.log(" --help, -h Show this help");
|
|
404
499
|
console.log("");
|
|
405
500
|
console.log(" Examples:");
|
|
406
501
|
console.log(" npx playclaw PC-XXXX-XXXX-XXXX");
|
|
407
|
-
console.log(" npx playclaw PC-XXXX-XXXX-XXXX --gateway-token my-
|
|
502
|
+
console.log(" npx playclaw PC-XXXX-XXXX-XXXX --gateway-token my-token");
|
|
408
503
|
console.log(" OPENCLAW_GATEWAY_TOKEN=mytoken npx playclaw PC-XXXX-XXXX-XXXX");
|
|
409
504
|
console.log("");
|
|
410
505
|
console.log(" Auto-detection:");
|
|
411
|
-
console.log("
|
|
412
|
-
console.log("
|
|
413
|
-
console.log(" to find yours.");
|
|
506
|
+
console.log(" Gateway token is auto-read from ~/.openclaw/openclaw.json");
|
|
507
|
+
console.log(" Run `openclaw config get gateway.auth.token` to find yours.");
|
|
414
508
|
console.log("");
|
|
415
509
|
}
|
|
416
510
|
|
|
417
|
-
// ─── Graceful shutdown
|
|
511
|
+
// ─── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
418
512
|
|
|
419
513
|
async function shutdown() {
|
|
420
514
|
console.log("");
|
|
421
|
-
log("
|
|
515
|
+
log("·", "Shutting down...", c.gray);
|
|
422
516
|
|
|
423
517
|
if (bridgeChannel) {
|
|
424
518
|
await bridgeChannel.untrack().catch(() => {});
|
|
425
|
-
await
|
|
519
|
+
await cloud.removeChannel(bridgeChannel).catch(() => {});
|
|
426
520
|
}
|
|
427
521
|
|
|
428
522
|
openclawWs?.close();
|
|
@@ -432,7 +526,7 @@ async function shutdown() {
|
|
|
432
526
|
process.on("SIGINT", shutdown);
|
|
433
527
|
process.on("SIGTERM", shutdown);
|
|
434
528
|
|
|
435
|
-
// ─── Main
|
|
529
|
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
|
436
530
|
|
|
437
531
|
printBanner();
|
|
438
532
|
connectToOpenClaw();
|