DaemonClient provides a Node.js client for communicating with the Rubber Duck daemon over a Unix domain socket using NDJSON protocol.
Constructor
The constructor is private. Use the static connect() method to create instances.
Static Methods
connect()
Connect to the daemon and return a DaemonClient instance.
static connect(
options?: number | { socketPath?: string; timeoutMs?: number }
): Promise<DaemonClient>
Connection options. If a number, treated as timeoutMs. If an object:Custom Unix socket path. Defaults to ~/Library/Application Support/RubberDuck/daemon.sock
Connection timeout in milliseconds
Resolves with a connected client instance, or rejects on timeout/error
Example:
import { DaemonClient } from '@rubberduck/cli';
// Connect with default timeout (5s)
const client = await DaemonClient.connect();
// Connect with custom timeout
const client = await DaemonClient.connect(10000);
// Connect with custom socket path
const client = await DaemonClient.connect({
socketPath: '/tmp/custom.sock',
timeoutMs: 3000
});
Instance Methods
request()
Send a typed request to the daemon and await the response.
request<M extends DaemonRequest["method"]>(
method: M,
params: DaemonRequestMap[M],
timeoutMs = 30_000
): Promise<DaemonResponse>
Request method: attach, follow, unfollow, say, sessions, abort, doctor, get_state, ping, extension_ui_response, voice_connect, voice_tool_call, voice_state
params
DaemonRequestMap[M]
required
Type-safe parameters corresponding to the method. See Types for details.
Request timeout in milliseconds
Response with { id, ok, data?, error? }
Example:
// Ping the daemon
const pong = await client.request('ping', {});
// Attach to a workspace
const result = await client.request('attach', {
path: '/Users/dev/my-project'
});
// Send a message
const response = await client.request('say', {
message: 'List all files',
sessionId: 'abc123'
});
// Get session list
const sessions = await client.request('sessions', {
workspaceId: 'xyz'
});
onEvent()
Register a handler for daemon events (pushed asynchronously).
onEvent(handler: (event: DaemonEvent) => void): void
handler
(event: DaemonEvent) => void
required
Callback invoked when the daemon pushes an event. Receives events like { event, sessionId, data }
Example:
client.onEvent((event) => {
console.log(`[${event.sessionId}] ${event.event}`, event.data);
if (event.data.type === 'message_update') {
// Handle streaming message delta
}
});
removeEventHandler()
Unregister a previously registered event handler.
removeEventHandler(handler: (event: DaemonEvent) => void): void
handler
(event: DaemonEvent) => void
required
The exact handler function reference to remove
Example:
const handler = (event) => console.log(event);
client.onEvent(handler);
// Later...
client.removeEventHandler(handler);
isConnected()
Check if the client is currently connected to the daemon.
Example:
if (client.isConnected()) {
await client.request('ping', {});
}
close()
Close the connection and reject all pending requests.
Example:
try {
await client.request('say', { message: 'Hello' });
} finally {
client.close();
}
Event Handling
Events are pushed from the daemon asynchronously and do not correspond to specific requests. Common event types:
agent_start, agent_end — Agent lifecycle
message_start, message_update, message_end — Streaming assistant messages
tool_execution_start, tool_execution_update, tool_execution_end — Tool calls
extension_ui_request — UI prompts (e.g., confirm, input)
See PiEvent types for the full event schema.
Error Handling
connect() rejects if the daemon is not running or connection times out
request() rejects if the request times out or the socket disconnects
request() resolves with { ok: false, error } for application-level errors
Example:
try {
const client = await DaemonClient.connect();
const response = await client.request('say', { message: 'test' });
if (!response.ok) {
console.error('Request failed:', response.error);
}
} catch (err) {
console.error('Connection or timeout error:', err);
}
Full Example
import { DaemonClient } from '@rubberduck/cli';
async function main() {
const client = await DaemonClient.connect();
// Subscribe to events
client.onEvent((event) => {
if (event.data.type === 'message_update') {
const delta = event.data.assistantMessageEvent;
if (delta.type === 'text_delta' && delta.delta) {
process.stdout.write(delta.delta);
}
}
});
// Attach to workspace
await client.request('attach', {
path: process.cwd()
});
// Follow the session
const followResponse = await client.request('follow', {});
const sessionId = followResponse.data?.sessionId as string;
// Send a message
await client.request('say', {
sessionId,
message: 'What files are in this directory?'
});
// Wait for agent to finish...
await new Promise(resolve => setTimeout(resolve, 5000));
client.close();
}
main().catch(console.error);