The @opta/daemon-client TypeScript package provides a typed client for the daemon HTTP API and WebSocket event stream, handling authentication, serialization, and reconnection automatically.
Daemon Client SDK
The daemon client is used by Code Desktop and can be used by any TypeScript application that needs to interact with the Opta daemon. It wraps the daemon v3 REST API and WebSocket endpoint with fully typed request and response objects.
Creating a Client
Creating a daemon client
import { DaemonClient } from "@opta/daemon-client";
const client = new DaemonClient({
host: "127.0.0.1",
port: 9999,
token: "<bearer-token>",
});
// Verify connection
const health = await client.health();
console.log(health.status); // "ok"
The token is read from the daemon's state.json file at ~/.config/opta/daemon/state.json. In native desktop runtime, Code Desktop persists connection secrets in OS keyring-backed storage; in browser/dev runtime, localStorage is used as a compatibility fallback.
Session Lifecycle
The daemon client supports the full session lifecycle: create, submit turns, poll for events, and close.
Create a Session
Create a session
const session = await client.createSession({
mode: "chat", // "chat" or "do"
model: "qwen3-72b", // model to use for inference
});
console.log(session.id); // "sess-abc123"
console.log(session.status); // "active"
Submit a Turn
Submit a turn
const turn = await client.submitTurn(session.id, {
content: "Explain how unified memory works on Apple Silicon",
});
console.log(turn.id); // "turn-xyz789"
After submitting a turn, the model begins inference. You can track progress through WebSocket events or by polling the session endpoint.
Poll Events
Poll for events
const events = await client.getEvents(session.id, {
afterSeq: 0, // get all events from the beginning
});
for (const event of events) {
console.log(event.event, event.seq);
// "turn.token" 1
// "turn.token" 2
// ...
// "turn.done" 42
}
Polling vs. WebSocket
Polling is simpler but less efficient. For real-time streaming, use the WebSocket connection described below. Polling is best for one-off event retrieval or when WebSocket is unavailable.
WebSocket Streaming
WebSocket streaming
const ws = client.connectWebSocket();
ws.on("event", (envelope) => {
switch (envelope.event) {
case "turn.token":
// Streaming token: envelope.data.token
process.stdout.write(envelope.data.token);
break;
case "turn.tool_call":
// Tool invocation: envelope.data.toolName, envelope.data.args
console.log("Tool:", envelope.data.toolName);
break;
case "turn.tool_result":
// Tool result: envelope.data.toolName, envelope.data.result
console.log("Result:", envelope.data.result);
break;
case "turn.done":
// Turn completed: envelope.stats
console.log("\nStats:", envelope.stats);
break;
case "turn.error":
// Error during turn
console.error("Error:", envelope.data.message);
break;
case "session.cancelled":
// Session was cancelled
console.log("Session cancelled");
break;
}
});
ws.on("disconnect", () => {
console.log("WebSocket disconnected");
});
Event Handling
The daemon emits several event types through the WebSocket connection:
Event
Description
Stop Event
turn.token
Streaming token from model output
No
turn.thinking
Model reasoning/thinking output
No
turn.tool_call
Model invoked a tool
No
turn.tool_result
Tool execution result
No
turn.done
Turn completed successfully
Yes
turn.error
Error during inference or tool execution
Yes
session.cancelled
Session was cancelled by user
Yes
Stop events (turn.done, turn.error, session.cancelled) indicate that no more events will be emitted for the current turn. Your UI should transition from a streaming state to a completed state when it receives a stop event.
Reconnection with afterSeq
Each event has a sequence number (seq). When reconnecting after a disconnection, pass the last received sequence number as afterSeq to avoid re-delivery of events you have already processed.
The afterSeq cursor is essential for reliable reconnection. Without it, the client would receive duplicate events for everything that happened before the disconnection.