Solari

VMs

@solarisdk/desktop: classes DesktopClient and Desktop. A VM is a GUI desktop you drive with mouse, keyboard, and screenshots, and stream live. See the TypeScript SDK hub for install and configuration.

npm install @solarisdk/desktop

Contents

DesktopClient

The VM entry point. Talks the gateway HTTP API.

Properties

Constructors

new DesktopClient()

new DesktopClient(options: DesktopClientOptions): DesktopClient

Creates a client. Does not open any connection.

Parameters:

  • options.apiKey string: required.
  • options.baseUrl string: required, e.g. https://api.getsolari.com.
  • options.fetch? typeof fetch: defaults to the global.
  • options.callTimeoutMs? number: per-RPC timeout for handles this client creates.

Returns: DesktopClient

Throws: SolariError if apiKey or baseUrl is missing.

Example:

import { DesktopClient } from "@solarisdk/desktop";

const client = new DesktopClient({
  apiKey: process.env.SOLARI_API_KEY!,
  baseUrl: "https://api.getsolari.com",
});

Methods

create()

create(opts?: CreateDesktopOptions): Promise<Desktop>

Creates a VM and returns a live handle. The call sends an idempotency key, so retrying is safe.

Parameters:

Returns: Promise<Desktop>

Throws: AuthError, PlanError, ConcurrencyLimitError, NoCapacityError, or GatewayError.

Example:

const vm = await client.create({
  template: "office",
  resolution: "1280x720",
  cpu: 2,
  memMb: 4096,
});

connect()

connect(sessionId: string): Promise<Desktop>

Re-attaches by id, resuming the session first if it is paused. The canonical way back to a VM when all you have is its id.

Parameters:

  • sessionId string: the session id.

Returns: Promise<Desktop>

Throws: GatewayError if the id is unknown.

Example:

const vm = await client.connect(sessionId);  // resumes if paused
await vm.connect();                           // open the control channel
await vm.keyboard.type("back again");

get()

get(sessionId: string): Promise<GetDesktopResponse>

Fetches a session’s current status. Returns the raw record, not a handle. The gateway does not re-issue control URLs here.

Parameters:

  • sessionId string: the session id.

Returns: Promise<GetDesktopResponse>: status, expiresAt, optional recordingUrl.

Example:

const st = await client.get(sessionId);
console.log(st.status);  // "ready" | "paused" | ...

pause()

pause(sessionId: string): Promise<DesktopLifecycleResponse>

Saves full RAM + disk state so it resumes exactly where it left off. Billing for compute stops and it stops counting against your concurrency limit. Prefer Desktop.pause() when you hold a live handle.

Parameters:

  • sessionId string: the session id.

Returns: Promise<DesktopLifecycleResponse>: sessionId, status.

Example:

await client.pause(sessionId);

resume()

resume(sessionId: string): Promise<Desktop>

Restores full RAM + disk state and returns a new handle. Always resumes. Use connect() if the session may already be running.

Parameters:

  • sessionId string: the session id.

Returns: Promise<Desktop>

Example:

const vm = await client.resume(sessionId);

attach()

attach(session: CreateDesktopResponse): Desktop

Rebuilds a handle from a saved create response. Synchronous, no network call. Use it to carry a session across processes.

Parameters:

  • session CreateDesktopResponse: sessionId, controlUrl, streamUrl, expiresAt.

Returns: Desktop

Example:

const saved = JSON.parse(await readFile("session.json", "utf8"));
const vm = client.attach(saved);

destroy()

destroy(sessionId: string): Promise<DeleteDesktopResponse>

Destroys a session. Idempotent.

Parameters:

  • sessionId string: the session id.

Returns: Promise<DeleteDesktopResponse>

Example:

await client.destroy(sessionId);

volumes

Identical to SandboxClient.volumes: create(), list(), get(), delete().

Example:

const vol = await client.volumes.create({ name: "assets" });
const vm = await client.create({
  template: "office",
  volumes: [{ volumeId: vol.volumeId, path: "/data" }],
});

Desktop

A live GUI session. Construct via DesktopClient.create().

Properties

  • id string: the session id.
  • sessionId string: alias of id.
  • streamUrl string: wss:// URL serving RFB (VNC) bytes for the live view.
  • controlUrl string: wss:// JSON-RPC control channel.
  • expiresAt string: ISO 8601 expiry.
  • connected boolean: whether the control channel is open.
Desktop extends Sandbox
Commands, code, files, git, snapshots, pause/resume, and the rest of the shared surface are documented once on the Sandboxes page. This page covers only the GUI additions. See inherited members for the index.

Methods

Open the control channel first
Every GUI method below — screenshot(), mouse.*, keyboard.*, display.*, clipboard.*, open(), and the rest — runs over the control channel and throws ConnectionError until it is open. Call connect() once after create(), connect(), resume(), or attach() before driving the VM.

health()

health(): Promise<HealthResult>

Readiness probe for the display, VNC server, and guest agent.

Returns: Promise<HealthResult>: ready, display, vnc.

Example:

const h = await vm.health();
if (!h.ready) throw new Error("desktop not ready");

screenshot()

screenshot(opts?: ScreenshotOptions): Promise<Uint8Array>

Captures the current screen as image bytes.

Parameters:

  • opts.format? "png" | "jpeg": default "png".
  • opts.quality? number: JPEG quality 1 to 100; ignored for PNG.

Returns: Promise<Uint8Array>: decoded image bytes.

Example:

import { writeFile } from "node:fs/promises";

await writeFile("shot.png", await vm.screenshot());
const jpeg = await vm.screenshot({ format: "jpeg", quality: 80 });

mouse.*

mouse.move(x: number, y: number, opts?: { humanize?: boolean }): Promise<void>
mouse.click(x: number, y: number, opts?: { button?: MouseButton; humanize?: boolean }): Promise<void>
mouse.doubleClick(x: number, y: number, opts?: ClickOptions): Promise<void>
mouse.down(x: number, y: number, button?: MouseButton): Promise<void>
mouse.up(x: number, y: number, button?: MouseButton): Promise<void>
mouse.scroll(x: number, y: number, opts?: { button?: MouseButton; humanize?: boolean }): Promise<void>
mouse.drag(from: { x: number; y: number }, to: { x: number; y: number }, button?: MouseButton): Promise<void>

Drive the pointer at absolute screen coordinates. The origin is the top-left of the display.

Parameters:

  • x, y number: absolute coordinates.
  • button? MouseButton: "left" (default), "middle", "right".
  • humanize? boolean: use a humanized trajectory instead of teleporting. Honored by move, click, and scroll.
  • from, to { x: number; y: number }: drag endpoints.

Returns: Promise<void>

Example:

await vm.mouse.move(400, 300, { humanize: true });
await vm.mouse.click(400, 300);
await vm.mouse.doubleClick(120, 80);
await vm.mouse.scroll(400, 300);
await vm.mouse.drag({ x: 100, y: 100 }, { x: 500, y: 400 });
doubleClick and drag ignore humanize
doubleClick accepts a full ClickOptions for type parity but only forwards button; drag takes no options at all. Both always teleport.

keyboard.*

keyboard.type(text: string): Promise<void>
keyboard.press(keys: string | string[]): Promise<void>
keyboard.hotkey(...keys: string[]): Promise<void>
keyboard.down(keys: string | string[]): Promise<void>
keyboard.up(keys: string | string[]): Promise<void>

Type literal text, or press key chords. hotkey() is press() with variadic arguments.

Parameters:

  • text string: literal text to type.
  • keys string | string[]: key names, e.g. "Return" or ["ctrl", "c"].

Returns: Promise<void>

Example:

await vm.keyboard.type("hello world");
await vm.keyboard.press("Return");
await vm.keyboard.hotkey("ctrl", "c");

await vm.keyboard.down("shift");   // hold
await vm.keyboard.press("Left");
await vm.keyboard.up("shift");

display.*

display.set(w: number, h: number): Promise<void>
display.size(): Promise<{ w: number; h: number }>
display.cursor(): Promise<{ x: number; y: number }>

Read or change the display resolution and read the cursor position.

Parameters:

  • w, h number: new resolution in pixels.

Returns: Promise<void> / Promise<{ w, h }> / Promise<{ x, y }>

Example:

await vm.display.set(1920, 1080);
const { w, h } = await vm.display.size();
const { x, y } = await vm.display.cursor();

clipboard.*

clipboard.get(): Promise<string>
clipboard.set(text: string): Promise<void>

Read or write the guest clipboard. get() returns "" when the clipboard is empty.

Parameters:

  • text string: content to place on the clipboard.

Returns: Promise<string> / Promise<void>

Example:

await vm.clipboard.set("pasted from the host");
await vm.keyboard.hotkey("ctrl", "v");
console.log(await vm.clipboard.get());

open()

open(name: string, args?: string[]): Promise<number>

Launches a GUI application by name and returns its pid.

Parameters:

  • name string: executable name, e.g. "firefox".
  • args? string[]: arguments.

Returns: Promise<number>, the pid.

Example:

const pid = await vm.open("firefox", ["https://example.com"]);

stream.*

stream.start(): Promise<{ streamUrl: string; token?: string }>
stream.stop(): Promise<void>

Returns the embeddable live-view URL. The stream URL is minted at create time, so start() is a getter and stop() is a no-op. The RFB socket belongs to the caller.

Returns: Promise<{ streamUrl: string; token?: string }> / Promise<void>

Example:

const { streamUrl } = await vm.stream.start();
// same value as vm.streamUrl, hand it to a noVNC client
Rendering the stream in a browser
@solarisdk/desktop also exports mountDesktop(el, { streamUrl }), which wires noVNC to a DOM element for you. It needs the optional @novnc/novnc peer dependency and only runs in a browser.

record.*

record.start(opts?: { fps?: number; format?: string; path?: string }): Promise<RecordStartResult>
record.stop(): Promise<RecordStopResult>

Records the session to an mp4 inside the guest. Retrieve it afterwards with downloadUrl().

Parameters:

  • opts.fps? number: capture frame rate.
  • opts.format? string, opts.path? string: output format and in-guest path.

Returns: Promise<RecordStartResult> (path, fps) / Promise<RecordStopResult> (path, sizeBytes).

Example:

await vm.record.start({ fps: 15 });
await vm.keyboard.type("recorded work");
const { path, sizeBytes } = await vm.record.stop();

const { url } = await vm.downloadUrl(path);

process.*

process.list(): Promise<ProcessInfo[]>
process.start(cmd: string, opts?: { args?: string[]; cwd?: string }): Promise<number>
process.kill(pid: number): Promise<void>
process.signal(pid: number, signal?: number): Promise<void>

Inspect and control guest processes by pid. For output capture use commands.run() instead.

Parameters:

  • cmd string: program to start.
  • opts.args? string[], opts.cwd? string.
  • pid number: target process.
  • signal? number: signal number.

Returns: Promise<ProcessInfo[]> / Promise<number> (the pid) / Promise<void>

Example:

const pid = await vm.process.start("xterm");
for (const p of await vm.process.list()) console.log(p.pid, p.name);
await vm.process.kill(pid);

ports.*

ports.list(): Promise<PortInfo[]>

Lists TCP sockets listening inside the guest.

Returns: Promise<PortInfo[]>: each with port, addr, optional pid.

Example:

for (const p of await vm.ports.list()) console.log(p.port, p.addr, p.pid);

pkg.install()

pkg.install(manager: "apt" | "pip" | "npm", packages: string[]): Promise<PkgInstallResult>

Installs packages in the guest.

Parameters:

  • manager PackageManager: "apt", "pip", or "npm".
  • packages string[]: package names.

Returns: Promise<PkgInstallResult>: exitCode, stdout, stderr. A failed install resolves with a non-zero exitCode; it does not throw.

Example:

const r = await vm.pkg.install("apt", ["imagemagick"]);
if (r.exitCode !== 0) console.error(r.stderr);

exec()

exec(cmd: string, opts?: ExecOptions): Promise<ExecResult>

Runs a command to completion (a v1 convenience). Prefer commands.run(), which adds streaming callbacks and a warm HTTP fast path.

Parameters:

  • cmd string: program to run.
  • opts.args? string[], opts.cwd? string, opts.timeoutMs? number.
  • opts.stream? boolean: reserved; use execStream(), which sets it for you.

Returns: Promise<ExecResult>: exitCode, stdout, stderr.

Example:

const r = await vm.exec("xdotool", { args: ["getactivewindow"] });

execStream()

execStream(cmd: string, onChunk: ExecStreamHandler, opts?: ExecOptions): Promise<ExecResult>

Runs a command, delivering output chunks as they arrive (a v1 convenience). Prefer commands.run() with onStdout.

Parameters:

  • cmd string: program to run.
  • onChunk ExecStreamHandler: called with { stream, text, bytes }.
  • opts? ExecOptions: without stream.

Returns: Promise<ExecResult>

Example:

await vm.execStream("apt-get", (c) => process.stdout.write(c.text), {
  args: ["update"],
});

fs.*

fs.read(path) / fs.readText(path) / fs.write(path, data, mode?)
fs.list(path) / fs.stat(path) / fs.remove(path, recursive?) / fs.mkdir(path)

A v1 alias for a subset of files.*, which is the canonical surface and has more methods.

Example:

await vm.fs.write("/tmp/note.txt", "hi");   // same as vm.files.write(...)

Inherited members

Desktop extends the same base as Sandbox. These all work on a VM:

MemberPurpose
connect(), reconnect(), close()Control-channel lifecycle.
commands.run(), commands.start(), pty.create()Run commands and open PTYs.
runCode(), createCodeContext()Stateful kernel execution with rich outputs.
files.*, downloadUrl(), uploadUrl()Filesystem access and signed transfer URLs.
git.*Clone, status, commit, push, pull, checkout, log.
env(), volumes.mount()Session env vars and volume mounts.
metrics(), snapshot(), revert()Usage, checkpoints, in-place restore.
pause(), resume(), setTimeout(), kill()Session lifecycle.
previewUrl()Public URL for an in-guest port.

Types

CreateDesktopOptions

  • template? string: e.g. "office", "ubuntu-desktop". Default "default".
  • resolution? string: initial display size, e.g. "1280x720".
  • cpu? number: vCPUs, 1 to 16. Default 2; grown on assign via vCPU hot-add.
  • memMb? number: RAM in MiB, 2048 to 65536. Default 2048; grown via virtio-mem hotplug.
  • record? boolean: record server-side. The 201 body carries a presigned recordingUrl, but create() returns a Desktop handle, which does not surface it. Read it back from get().
  • timeoutMs? number: rolling idle window; resets on every use. Overrides ttlSeconds and the 30-minute default.
  • lifecycle? SandboxLifecycle: { onTimeout: "pause" | "kill", autoResume?: boolean }.
  • metadata? Record<string, string>: opaque labels.
  • volumes? VolumeAttachment[]: each { volumeId, path }.
  • ttlSeconds? number: legacy TTL; prefer timeoutMs.

CreateDesktopResponse

  • sessionId string: signed, opaque session capability.
  • streamUrl string: wss:// RFB stream.
  • controlUrl string: wss:// JSON-RPC control channel.
  • expiresAt string: ISO 8601 expiry.
  • recordingUrl? string: present when record: true.

GetDesktopResponse

  • sessionId string.
  • status DesktopStatus: starting, ready, paused, releasing, gone.
  • expiresAt string, recordingUrl? string.

ScreenshotOptions

  • format? "png" | "jpeg": default "png".
  • quality? number: JPEG only, 1 to 100.

HealthResult

  • ready boolean, display boolean, vnc boolean.

ProcessInfo / PortInfo

  • ProcessInfo: pid number, name string, cmd? string.
  • PortInfo: port number, addr string, pid? number.

RecordStartResult / RecordStopResult

  • RecordStartResult: path string (in-guest mp4), fps number.
  • RecordStopResult: path string, sizeBytes number.

Errors are shared across products. See Errors.