1 Quickstart
uncaney edited this page 2026-08-14 14:00:34 +02:00

Quickstart

Everything below works against a running launcher, v5.7.1 or later. The API server is always on: it binds api.sock at launcher startup, whether you started the GUI or headless mode.

Socket path (<launcher_dir>/api.sock):

Platform Path
macOS ~/Library/Application Support/EkaiiLauncher/api.sock
Linux ~/.local/share/EkaiiLauncher/api.sock
Portable <portable_dir>/EkaiiLauncher/api.sock
Windows Named pipe \\.\pipe\pandora-launcher-api (server works; the bundled --api client is Unix-only and exits with code 2 on Windows, but any client that speaks the protocol over the pipe works)

1. The bundled CLI (--api)

The launcher binary doubles as a client: it connects to the running launcher's socket, sends one command, prints the raw JSON response line and exits. It never takes the lockfile and never opens a window.

ekaii-launcher --api launcher.version
ekaii-launcher --api launcher.status
ekaii-launcher --api instances.list | jq '.data[].name'
ekaii-launcher --api instances.start --api-params '{"name": "main"}'

Exit codes: 0 = response with "ok": true (or clean end of an event stream), 1 = response with "ok": false, 2 = transport error (launcher not running, bad --api-params JSON, connection dropped).

2. Raw socket, NDJSON

The protocol is one JSON object per line, \n terminated. Requests carry id, cmd and optional params; the response echoes your id:

SOCK="$HOME/Library/Application Support/EkaiiLauncher/api.sock"   # macOS
# SOCK="$HOME/.local/share/EkaiiLauncher/api.sock"                # Linux

echo '{"id":1,"cmd":"launcher.version"}' | nc -U "$SOCK"

# Pipelining works; requests on one connection are processed sequentially
printf '%s\n%s\n' \
  '{"id":1,"cmd":"instances.list"}' \
  '{"id":2,"cmd":"accounts.list"}' | nc -U "$SOCK"

Success: {"id":1,"ok":true,"data":{...}}. Failure: {"id":1,"ok":false,"error":{"code":"not_found","message":"...","retryable":false}}. Branch on error.code, never on message text.

3. Python

A minimal client with id correlation (event lines, which carry an event key instead of id/ok, are treated as out-of-band):

import json, os, socket

SOCK = os.path.expanduser("~/Library/Application Support/EkaiiLauncher/api.sock")  # macOS
# Linux: os.path.expanduser("~/.local/share/EkaiiLauncher/api.sock")

class Pandapi:
    def __init__(self, path=SOCK):
        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.sock.connect(path)
        self.buf = b""
        self.next_id = 0

    def _readline(self):
        while b"\n" not in self.buf:
            chunk = self.sock.recv(65536)
            if not chunk:
                raise ConnectionError("launcher closed the connection")
            self.buf += chunk
        line, self.buf = self.buf.split(b"\n", 1)
        return line

    def call(self, cmd, params=None):
        self.next_id += 1
        req = {"id": self.next_id, "cmd": cmd, "params": params or {}}
        self.sock.sendall(json.dumps(req).encode() + b"\n")
        while True:
            msg = json.loads(self._readline())
            if "event" in msg:
                continue  # out-of-band event line (only after events.subscribe)
            if msg.get("id") == self.next_id:
                if not msg["ok"]:
                    e = msg["error"]
                    raise RuntimeError(f"{e['code']}: {e['message']} (retryable={e['retryable']})")
                return msg["data"]

api = Pandapi()
print(api.call("launcher.version"))

# Idempotent provisioning, then a launch that blocks until the game runs
api.call("instances.ensure", {"name": "api-demo", "version": "26.2", "loader": "fabric",
                              "memory": {"enabled": True, "min": 512, "max": 4096}})
op = api.call("instances.start", {"name": "api-demo"})["op"]
print(api.call("ops.wait", {"op": op, "timeout_secs": 600}))

4. Discover the surface: api.describe

The API is self-describing. One call returns every command (grouped, with one-line descriptions), every event type, and a machine-readable security block (scopes, the read-only set, the local-only set, denied fields, env vars, error codes):

ekaii-launcher --api api.describe | jq '.data.commands | keys'
ekaii-launcher --api api.describe | jq '.data.events'
ekaii-launcher --api api.describe | jq '.data.security'

This is the intended bootstrap for SDK generation and for LLM agents (see Automation-and-AI). Over the network it needs only the read scope.

5. A first real workflow

# Create an instance (asynchronous; it appears in instances.list within a second or two)
ekaii-launcher --api instances.create --api-params '{"name":"api-demo","version":"26.2","loader":"fabric"}'

# Install a mod from Modrinth (returns an op)
ekaii-launcher --api content.install --api-params \
  '{"name":"api-demo","files":[{"modrinth":{"project_id":"AANobbMI"}}]}'

# Block until that op finishes
ekaii-launcher --api ops.wait --api-params '{"op": 1}'

# Launch and block until the game process is running (wait: "running" | "exited")
ekaii-launcher --api instances.start --api-params '{"name":"api-demo","wait":"running","timeout_secs":600}'

# Plain-language status, then kill
ekaii-launcher --api status.summary
ekaii-launcher --api instances.kill --api-params '{"name":"api-demo"}'

Long-running commands return {"op": N} immediately; poll ops.status, block with ops.wait, or subscribe to op.* events. Fire-and-forget mutations return {"requested": true}, meaning enqueued, not applied: read back to verify. See Concepts.

6. Events

events.subscribe switches the connection (or the CLI) to streaming mode:

ekaii-launcher --api events.subscribe
{"id":0,"ok":true,"data":{"subscribed":true,"current_seq":41,"session_id":"18f3c0a1b2"}}
{"seq":42,"event":"instance.modified","data":{"id":"0:1","name":"api-demo","status":"launching",...}}
{"seq":43,"event":"instance.modified","data":{"id":"0:1","name":"api-demo","status":"running",...}}

Every event carries a monotonic seq; reconnect with {"since_seq": <last seen>} to replay missed events from the 4096-event ring, and filter server-side with types / instance. See Concepts.

7. Headless mode

--headless runs the backend and the control API without opening any window, turning the launcher into a scriptable daemon:

ekaii-launcher --headless &

# There is no game-output window headless, so opt into console capture
# (applies to instances launched after the call) to get game.output and game.* events:
ekaii-launcher --api game.capture --api-params '{"enabled": true}'

ekaii-launcher --api instances.start --api-params '{"name":"api-demo","wait":"running"}'

# Stop the daemon (local-socket-only command)
ekaii-launcher --api launcher.quit

Headless mode still takes the single-instance lock and binds the socket exactly as a normal launch would.

Next