Skill: niczy/claw-voice (version 0.0.2)

# Claw Voice You are connected to a live user session via voice. The user is speaking to you through a voice interface. Use the bundled `client.py` script to communicate with them in real time. The script is located at `$skill_dir/client.py`. ## Sending a message to the user To say something to the user, run: ```bash uv run python $skill_dir/client.py send "Hello! How can I help you today?" ``` The server echoes back a confirmation as JSON: ```json {"type": "echo", "content": "Hello! How can I help you today?"} ``` ## Receiving the next message from the user To wait for the user to say something: ```bash uv run python $skill_dir/client.py recv ``` This blocks until the user speaks, then prints their message as JSON and exits: ```json {"type": "message", "content": "What's the weather like?"} ``` Use `--timeout` to control how long to wait (default 30s): ```bash uv run python $skill_dir/client.py recv --timeout 60 ``` If the timeout expires with no message, it prints an error and exits with code 1. ## Listening for multiple messages To receive a stream of messages over a period of time: ```bash uv run python $skill_dir/client.py listen --timeout 60 ``` This prints each incoming message as a JSON line until the timeout expires: ``` {"type": "message", "content": "Tell me a joke"} {"type": "message", "content": "Actually, make it about cats"} ``` ## Running as an agent bridge The `agent` command creates a loop: it listens for user messages, forwards each one to `openclaw agent --agent main --message '<message>'`, captures the stdout, and sends it back to the user over the WebSocket. ```bash uv run python $skill_dir/client.py agent ``` This runs indefinitely by default. Use `--timeout` to limit the session: ```bash uv run python $skill_dir/client.py agent --timeout 300 ``` The flow for each message: 1. User speaks -> server sends `{"type": "message", "content": "..."}` to the client 2. Client runs `openclaw agent --agent main --message '...'` and captures stdout 3. Client sends the agent's response back: `{"type": "message", "content": "<stdout>"}` Logs are printed to stderr-style output as `[user]` and `[agent]` prefixed lines. ## Connection options All commands accept `--url` to override the default WebSocket address: ```bash uv run python $skill_dir/client.py --url ws://$host:$port/connect send "Hi" ``` Default URL: `ws://localhost:3111/connect` ## Message types reference ### Messages you send (agent -> user) | Type | Fields | Description | |-----------|---------------------|---------------------------------| | `message` | `content` (string) | Text to speak/display to the user | ### Messages you receive (user -> agent) | Type | Fields | Description | |-----------|---------------------|-------------------------------------| | `message` | `content` (string) | What the user said (transcribed text) | | `echo` | `content` (string) | Server confirmation of your sent message — ignore these | | `pong` | | Connection health check response — ignore these | ## Behavior guidelines - Respond promptly to every `message` you receive from the user. - Keep responses conversational and concise — the user is speaking, not reading. - Send one message at a time. Do not batch multiple sends. - Ignore `echo` messages — they are confirmations, not user input. - Use `recv` for turn-based conversation. Use `listen` when you expect the user to say multiple things.

path: client.py
content: """WebSocket client for communicating with the Claw Voice server."""

import argparse
import asyncio
import json
import shlex
import sys

import aiohttp


DEFAULT_URL = "ws://localhost:3111/connect"


async def run_agent(message: str) -> str:
    """Run openclaw agent and return its stdout."""
    cmd = f"openclaw agent --agent main --message {shlex.quote(message)}"
    proc = await asyncio.create_subprocess_shell(
        cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await proc.communicate()
    if proc.returncode != 0:
        return f"[agent error] {stderr.decode().strip()}"
    return stdout.decode().strip()


async def cmd_send(url: str, content: str) -> None:
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect(url) as ws:
            await ws.send_json({"type": "message", "content": content})
            resp = await ws.receive_json()
            print(json.dumps(resp))


async def cmd_recv(url: str, timeout: float) -> None:
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect(url) as ws:
            try:
                msg = await asyncio.wait_for(ws.receive_json(), timeout=timeout)
                print(json.dumps(msg))
            except asyncio.TimeoutError:
                print(json.dumps({"type": "error", "content": "timeout waiting for message"}))
                sys.exit(1)


async def cmd_listen(url: str, timeout: float) -> None:
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect(url) as ws:
            try:
                async with asyncio.timeout(timeout):
                    async for msg in ws:
                        if msg.type == aiohttp.WSMsgType.TEXT:
                            print(json.dumps(json.loads(msg.data)), flush=True)
                        elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
                            break
            except TimeoutError:
                pass


async def cmd_agent(url: str, timeout: float) -> None:
    """Listen for user messages, forward each to openclaw agent, send the response back."""
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect(url) as ws:
            try:
                async with asyncio.timeout(timeout):
                    async for msg in ws:
                        if msg.type == aiohttp.WSMsgType.TEXT:
                            data = json.loads(msg.data)
                            if data.get("type") != "message":
                                continue
                            user_text = data.get("content", "")
                            print(f"[user] {user_text}", flush=True)
                            reply = await run_agent(user_text)
                            print(f"[agent] {reply}", flush=True)
                            await ws.send_json({"type": "message", "content": reply})
                        elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
                            break
            except TimeoutError:
                pass


def main() -> None:
    parser = argparse.ArgumentParser(description="Claw Voice WebSocket client")
    parser.add_argument("--url", default=DEFAULT_URL, help=f"WebSocket URL (default: {DEFAULT_URL})")
    sub = parser.add_subparsers(dest="command", required=True)

    p_send = sub.add_parser("send", help="Send a message and print the server response")
    p_send.add_argument("content", help="Message text to send")

    p_recv = sub.add_parser("recv", help="Wait for the next incoming message and print it")
    p_recv.add_argument("--timeout", type=float, default=30, help="Seconds to wait (default: 30)")

    p_listen = sub.add_parser("listen", help="Print incoming messages as JSON lines until timeout")
    p_listen.add_argument("--timeout", type=float, default=60, help="Seconds to listen (default: 60)")

    p_agent = sub.add_parser("agent", help="Listen for messages, forward to openclaw agent, send replies back")
    p_agent.add_argument("--timeout", type=float, default=0, help="Seconds to run (default: 0 = forever)")

    args = parser.parse_args()

    if args.command == "send":
        asyncio.run(cmd_send(args.url, args.content))
    elif args.command == "recv":
        asyncio.run(cmd_recv(args.url, args.timeout))
    elif args.command == "listen":
        asyncio.run(cmd_listen(args.url, args.timeout))
    elif args.command == "agent":
        timeout = args.timeout if args.timeout > 0 else float("inf")
        asyncio.run(cmd_agent(args.url, timeout))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector:  ()