JackyJacky

Jacky includes a local WebSocket server that allows external applications, scripts, or home-automation systems (like Home Assistant) to trigger actions on Jacky. You can command Jacky to speak, show animations, trigger system notifications, set timers, run configured routines, switch skins, walk around the screen, query status, or pipe text through the full LLM agent pipeline.


Getting Started & Configuration

For security reasons, the WebSocket server is disabled by default. To configure and enable it:

Step 1: Enable the Permission

  1. Right-click on Jacky to open the context menu and select Settings.
  2. Go to the Permissions tab.
  3. Scroll to Control local WebSocket server (allow_websocket) and check the box to enable it.
  4. Click Save at the bottom of the window.

Step 2: Enable the Server & Get Your Token

  1. Open Settings again and navigate to the Integrations tab.
  2. Under the Local WebSocket API group:
    • Check the Enable local WebSocket server box.
    • Configure a Port (defaults to 9876). If the port is in use by another Jacky instance, it will automatically try next ports up to port + 5.
    • Select which Allowed actions the WebSocket clients are permitted to execute.
    • Find your automatically generated Access token (or click Regenerate to create a new one). Click Copy to copy it to your clipboard.
  3. Click Save.

Security Model

Because WebSocket connections can be initiated from other programs running on your computer, Jacky implements a robust security model to protect your system:

  • Localhost Binding only: The server binds strictly to 127.0.0.1 (localhost). It is not reachable from other devices on your local network unless you explicitly proxy it.
  • Origin Checking: To prevent malicious websites you visit from making cross-origin WebSocket calls to your local Jacky instance, the server rejects any connection containing a browser Origin header. Only native clients, command-line tools, and backend scripts are permitted.
  • Token Authentication: Every client must authenticate using the shared secret token via HMAC comparison. Unauthenticated connections are closed immediately.
  • Action Allowlist: You choose which commands are active. High-risk actions like ask (which can call tools, search the web, or open applications) and run_routine are opt-in and disabled by default.
  • Message Size Limits: Inbound messages are capped at 8 KB to prevent memory exhaustion.
  • Rate Limiting: Clients are rate-limited using a token-bucket algorithm (burst capacity of 10 actions, refilling over a rolling 10-second window). Excess messages receive a rate_limited error response.

Connection & Authentication

The WebSocket server accepts connections at: ws://127.0.0.1:<configured_port>/ (e.g., ws://127.0.0.1:9876/)

You can authenticate in one of two ways:

1. Connection-Level URL Parameter (Query Param)

The simplest way is to pass the token directly in the connection URL:

ws://127.0.0.1:9876/?token=YOUR_ACCESS_TOKEN

This is also useful for firing a single, one-shot command when establishing the connection (see One-Shot Query Commands).

2. JSON Handshake Message

Alternatively, open a standard connection to ws://127.0.0.1:9876/ and send an authentication handshake message as your very first message:

{
  "action": "auth",
  "token": "YOUR_ACCESS_TOKEN"
}

If successful, the server replies:

{
  "ok": true,
  "action": "auth"
}

If authentication fails, the server responds with an error and terminates the socket:

{
  "ok": false,
  "error": "unauthorized"
}

Protocol & Action Reference

Once connected and authenticated, you send commands as JSON-formatted messages.

Every command follows this basic schema:

{
  "action": "ACTION_NAME",
  "...": "..." // action-specific fields
}

All commands reply with a JSON status message. Commands fall into two categories: Immediate and Deferred.

Immediate Actions

These run immediately on the main thread and reply instantly to acknowledge receipt.

1. say

Instructs Jacky to display a speech bubble containing verbatim text.

  • Payload:
    {
      "action": "say",
      "text": "Hello, world!"
    }
  • Success Response: {"ok": true, "action": "say"}

2. notify

Sends an OS-level system tray notification.

  • Payload:
    {
      "action": "notify",
      "title": "Alert",
      "text": "Task finished!"
    }
  • Success Response: {"ok": true, "action": "notify"}

3. emote

Triggers a visual animation/state change for Jacky. The pet automatically reverts to its normal state after 3 seconds. See Skins & Animations for the full list of available states.

  • Payload:
    {
      "action": "emote",
      "state": "dance"
    }
  • Success Response: {"ok": true, "action": "emote"}

4. timer

Creates a timer or alarm inside Jacky's timer manager.

  • Payload (Timer):
    {
      "action": "timer",
      "kind": "timer",
      "seconds": 600,
      "label": "Coffee is ready"
    }
  • Payload (Alarm):
    {
      "action": "timer",
      "kind": "alarm",
      "time": "14:30",
      "repeat": "none",
      "label": "Meeting"
    }
  • Success Response: {"ok": true, "action": "timer"}

5. run_routine

Triggers an automation routine by its unique ID.

  • Payload:
    {
      "action": "run_routine",
      "id": "btc_price",
      "variables": {
        "currency": "usd"
      }
    }
  • Success Response: {"ok": true, "action": "run_routine"}

6. skin

Switches Jacky to an already-installed skin by name. The name is matched using exact, case-insensitive, then fuzzy matching against installed skins.

  • Payload:
    {
      "action": "skin",
      "name": "Jacky"
    }
  • Success Response: {"ok": true, "action": "skin"}

7. move_to

Walks Jacky to absolute screen coordinates (pixels). The target is clamped to the visible screen area and avoids exclusion zones.

  • Payload:
    {
      "action": "move_to",
      "x": 800,
      "y": 600,
      "run": false
    }
    The run field is optional — set it to true to make Jacky run instead of walk.
  • Success Response: {"ok": true, "action": "move_to"}

8. move

Walks Jacky a relative distance in pixels from its current position. Valid directions: left, right, up, down, top (alias of up), bottom (alias of down).

  • Payload:
    {
      "action": "move",
      "direction": "left",
      "distance": 100,
      "run": false
    }
    The run field is optional — set it to true to make Jacky run instead of walk.
  • Success Response: {"ok": true, "action": "move"}

9. status

Returns Jacky's current status as a JSON string, including app version, pet state, and LLM/TTS availability.

  • Payload:
    {
      "action": "status"
    }
  • Success Response:
    {
      "ok": true,
      "action": "status",
      "reply": "{\"version\":\"0.4.0\",\"pet_state\":\"IDLE\",\"llm_enabled\":true,\"tts_enabled\":false,\"fallbackReason\":\"\"}"
    }

Deferred Actions (LLM-Backed)

These trigger asynchronous LLM generation. The server sends a status reply only after the generation completes, containing Jacky's spoken response.

10. react_to

Forces Jacky to generate a natural, in-character reaction to an external event description (runs LLM context, but does not call tools).

  • Payload:
    {
      "action": "react_to",
      "text": "The deploy failed on staging server"
    }
  • Success Response:
    {
      "ok": true,
      "action": "react_to",
      "reply": "Oh no! Staging is broken again? Who pushed to main?!"
    }

11. ask

Pipes input directly through Jacky's full agent pipeline (equivalent to typing in the question box or speaking to the pet). It handles keyword routines, intent classification, and tool execution (like opening applications, running shell commands, screen navigation, or searching the web).

  • Payload:
    {
      "action": "ask",
      "text": "Check the weather in Tokyo and tell me if I need an umbrella."
    }
  • Success Response:
    {
      "ok": true,
      "action": "ask",
      "reply": "It's currently raining in Tokyo with a temperature of 18°C. Yes, definitely take an umbrella!"
    }
Warning

Because the ask action can execute tools on your computer, it inherits all granular permission checks configured in Settings.


One-Shot Query Commands

For simple shell scripts or curl invocations, you can authenticate and execute an action in a single step using URL query parameters. When a connection is opened with query parameters, the server executes the action immediately and remains open.

Example URL format:

ws://127.0.0.1:9876/?token=YOUR_TOKEN&say=Hello%20world

The supported parameter names are: say, ask, react_to, notify, and emote.

  • For notify, you can optionally add a &title=MyTitle parameter.
  • For emote, the parameter value represents the animation state (e.g., &emote=happy).

Error Handling Reference

If a request fails, the server responds with {"ok": false, "error": "ERROR_CODE", ...}. Here are the common error codes:

Error CodeDescription
unauthorizedThe provided token was incorrect or missing. The connection will be closed.
message_too_largeThe incoming message exceeded the 8 KB cap.
invalid_jsonThe payload was not well-formed JSON.
invalid_payloadThe JSON shape was invalid (e.g. missing required fields or incorrect types).
missing_actionThe action key was missing or blank.
unknown_actionThe action is not recognized by Jacky.
action_not_allowedThe action was valid, but is not enabled in the Allowed actions settings on the Integrations tab.
rate_limitedToo many commands were sent too quickly. Throttled.
internal_errorAn unexpected exception occurred inside Jacky while processing the command.

Integration Examples

Python (using websockets library)

import asyncio
import json
import websockets
 
async def trigger_jacky():
    uri = "ws://127.0.0.1:9876/"
    async with websockets.connect(uri) as websocket:
        # Step 1: Handshake Authentication
        await websocket.send(json.dumps({
            "action": "auth",
            "token": "YOUR_ACCESS_TOKEN"
        }))
        
        # Read auth response
        response = await websocket.recv()
        print("Auth Response:", response)
        
        # Step 2: Send smart query
        await websocket.send(json.dumps({
            "action": "ask",
            "text": "run_routine dice_roll"
        }))
        
        # Wait for deferred response
        result = await websocket.recv()
        print("Result:", json.loads(result))
 
asyncio.run(trigger_jacky())

Bash / Command Line (using wscat)

You can test from the terminal using wscat (installed via npm: npm install -g wscat):

# Connect and send a verbatim bubble
wscat -c "ws://127.0.0.1:9876/?token=YOUR_ACCESS_TOKEN&say=Hello%20from%20terminal"