@trycua/cua
v0.4.1
Published
The cua SDK for Node and the browser: sandboxes, cua-spacesd, media streaming, Spaces and the cua daemon, on a Rust core
Readme
@trycua/cua
The cua SDK for Node (and, via @trycua/cua/browser, the browser): a thin,
generated binding over the Rust cua-sdk crate. Sandboxes (Fleet, local,
direct), cua-spacesd (typed calls plus callJson for every
cua.env.v1 RPC), media sessions with FrameSink/AudioSink callbacks,
Fleet pools/claims/images, and local runtimes, embedded or through a
cua daemon.
Native payloads ship as optional per-platform packages
(@trycua/cua-darwin-arm64, -linux-x64-gnu, ...), assembled by
libs/cua/scripts/build-npm-packages.mjs. src/native is generated by
libs/cua/scripts/generate-uniffi-bindings.mjs and checked with --check.
cd libs/cua && cargo build --release -p cua-sdk
scripts/build-test-fixtures.sh
cd typescript && npm ci && npm run stage:uniffi && npm testSandboxes: any image, local or cloud
import { embedded, http, CloudOptions, HttpHeader, SandboxCreateOptions } from "@trycua/cua"
const sb = await embedded().sandboxes().create(SandboxCreateOptions.create({
on: "local", // or "cloud": same options
image: "python:3.12-slim",
command: ["python", "-m", "my_mcp", "--port", "8765"],
env: new Map([["FOO", "bar"]]),
services: new Map([["mcp", 8765]]),
waitFor: [http("mcp", "/health")], // or tcp("mcp")
cloud: CloudOptions.create({ warm: true }), // optional, ignored locally
}))
const svc = sb.service("mcp")
const r = await svc.request("POST", "/mcp", body, undefined,
[HttpHeader.create({ name: "accept", value: "application/json, text/event-stream" })])
const url = await svc.url() // usable from this machine
const share = await sb.publicUrl("mcp", 3600, undefined) // shareable, expires
const fwd = await sb.forward(8765) // loopback URL: fwd.url()
const info = sb.info() // id, phase, location, services, expiresAtUnix, providerDetailspublicUrl is a signed URL in the cloud and a loopback URL with its own token
locally, served by the cua daemon (started on demand; CUA_BIN names the
CLI).
Image.linux() / Image.windows() / Image.macos() return the canonical
refs (ghcr.io/trycua/linux:24.04, ...). MCP servers in a sandbox use the
official SDK (npm install @modelcontextprotocol/client), no cua-spacesd:
import { connectMcp, registrySecret, sidecar } from "@trycua/cua"
const client = await connectMcp(await sb.mcpConfig("mcp", undefined))
const result = await client.callTool({ name: "add", arguments: { a: 2, b: 3 } })
// Sidecars share the sandbox's network namespace; a private image takes credentials.
SandboxCreateOptions.create({
on: "local",
image: "ghcr.io/acme/app:1",
registrySecret: registrySecret.fromEnv(),
sidecars: [sidecar("redis:7-alpine", { ports: [6379] })],
services: new Map([["db", 6379]]),
runtime: "runc", // local sidecars need runc
})The sandbox reaches a sidecar at its name (redis:6379 here) and a sidecar
reaches the sandbox at main, locally and in the cloud (gVisor and KubeVirt).
Cloud env and registry secrets work too; cloud image layers (a remote build)
are not available yet and fail with a clear error. Docs: Sandboxes.
Spaces (@trycua/cua/spaces)
The Rust cua-spaces runtime (embedded, or in cua daemon), typed:
import { embedded } from "@trycua/cua"
import { approve, startThread, SpaceSendFileOptions } from "@trycua/cua/spaces"
const spaces = embedded().spaces() // or connect().spaces()
const info = await spaces.add("http://10.0.0.5:3211", token, "dev")
const space = await spaces.space(info.id)
await space.bash("uname -a", undefined)
await space.sendFile("./report.pdf", SpaceSendFileOptions.create({}))
await space.teleport("firefox", undefined, approve((m) => ask(m))) // consent callback
const bot = await startThread(spaces, {
agent: "claude-code", prompt: "summarise the open PRs",
placement: { type: "shared", space, acknowledgeNoIsolation: true },
})Spaces (create / add / list / resolve / delete_ / remove / space /
callToolJson), Space (bash, write, upload, download,
sendFile, listTools, callTool, windows, openStream, streamSession,
joinPresence, teleportManifest, teleport, start/stop/hotspotStatus, agent*),
plus Thread, SpacesError (toSpacesError maps CuaError) and the
transcript adapter. @trycua/cua/spaces/host drives the desktop app's
control server (PiP, viewer). @trycua/cua/spaces/pip is picture-in-picture
of the desktop or one window in a browser or webview: PictureInPicture
(Document Picture-in-Picture, falling back to video PiP) and, for a shell's
own always-on-top window, pipWindowLabel and encodePipRoute /
parsePipRoute. @trycua/cua/teleport has the one drop zone,
<cua-drop-zone> ("Drop a file or window", Send file…, Teleport an app…).
Three more need no native library: @trycua/cua/spaces/presence
(PresenceRoster, the other participants' cursors as data, and
waitForPresence), @trycua/cua/spaces/routines (RoutineStore: a Bot's
recurring prompts, their clock and a RoutineRunner) and
@trycua/cua/spaces/groups (GroupChatStore: two to six Bots in one thread
over a GroupMessenger). The saved routine list is the same JSON the Rust
(cua_spaces::routines) and Swift SDKs write.
Webviews and browsers cannot load the native binding; they use
@trycua/cua/spaces/transport: McpSession over McpHttpTransport (the
daemon's loopback /mcp, bearer = its loopback token) or TauriTransport
(invoke). Same Spaces contract tools either way.
Tests: node --test test/spaces-unit.test.mjs test/pip.test.mjs test/routines.test.mjs (no native library needed) and
node --test test/spaces.test.mjs (needs cua-test-fixtures and, for the
daemon case, the cua CLI in ../target/debug).
