// Phase-1 drive client: pair with a ZeroClaw gateway, open /ws/chat as an // agent, send one message, and print the streamed turn events. This is the // Clawmates-side seam that (in prod) the control plane uses to run a claw's // turn and journal its events. Node >=22 (global WebSocket + fetch). // // node tools/runtime-spike/drive.mjs "" // e.g. node tools/runtime-spike/drive.mjs http://100.108.129.81:42617 scout "Say hi in 5 words" const [base, agent, message] = process.argv.slice(2); if (!base || !agent || !message) { console.error('usage: drive.mjs ""'); process.exit(2); } // 1) Pair: read the one-time code from env (printed in the daemon's startup log). const code = process.env.PAIR_CODE; if (!code) { console.error("set PAIR_CODE="); process.exit(2); } const pairRes = await fetch(`${base}/pair`, { method: "POST", headers: { "X-Pairing-Code": code, "Content-Type": "application/json" }, body: "{}", }); if (!pairRes.ok) { console.error(`pair failed: ${pairRes.status} ${await pairRes.text()}`); process.exit(1); } const pair = await pairRes.json(); const token = pair.token ?? pair.bearer ?? pair.access_token; console.log(`✓ paired, token acquired (${String(token).slice(0, 6)}…)`); // 2) Open the agent chat socket and run one turn. // Node's global WebSocket can't set headers, so pass the token as ?token= // (gateway accepts header > subprotocol > query). const wsUrl = base.replace(/^http/, "ws") + `/ws/chat?agent=${encodeURIComponent(agent)}&name=${encodeURIComponent("clawmates-spike")}` + `&token=${encodeURIComponent(token)}`; const ws = new WebSocket(wsUrl); const t0 = Date.now(); ws.addEventListener("open", () => { console.log("✓ ws open → sending message"); ws.send(JSON.stringify({ type: "message", content: message })); }); ws.addEventListener("message", (ev) => { let m; try { m = JSON.parse(ev.data); } catch { return console.log("raw:", ev.data); } switch (m.type) { case "session_start": console.log(` session_start id=${m.session_id} resumed=${m.resumed}`); break; case "chunk": process.stdout.write(m.content ?? ""); break; case "tool_call": console.log(`\n [tool_call] ${m.name} ${JSON.stringify(m.args)}`); break; case "tool_result": console.log(` [tool_result] ${m.name}: ${String(m.output).slice(0, 120)}`); break; case "approval_request": console.log(`\n [APPROVAL REQUIRED] ${JSON.stringify(m)} ← §15 gate hook`); break; case "done": console.log(`\n✓ done in ${Date.now() - t0}ms`); ws.close(); process.exit(0); case "error": console.error("\n✗ error:", JSON.stringify(m)); ws.close(); process.exit(1); default: console.log(" evt:", JSON.stringify(m)); } }); ws.addEventListener("error", (e) => { console.error("ws error:", e.message ?? e); process.exit(1); }); setTimeout(() => { console.error("timeout (90s)"); process.exit(1); }, 90_000);