do

customer0

Don't let a model guess pixels. List what can be clicked, and let a small model choose.

do is a computer-use library. A device lists the controls on screen in code. Clef, Cloudflare's small decision model, picks one with a probability. The device acts, and Clef checks whether it worked. A larger model is called only when a task needs text that is not in it word for word.

The only real device today is macOS, through the accessibility API. The benchmark drives a test app in a browser.

A real 38 s recording. Clef + Clairic types "jordan at cloudflare dot com" as written and fails. The relay arm sees the task needs new text, a writer fills the field once, and Clef still makes every click.

Results

30/30tasks finished, all 3 repeats (relay arm, bench-6)
$0.0016per task, Clef plus writer
~1/18the cost of Opus, which finished 22.7/30
0unrequested destructive actions in any run
armwhat drives itfinished$/taskmedian
relayClef picks every action; a writer fills fields only when the task needs new text30.0/30$0.00162.8 s
duetClef picks every action; Clairic copies values out of the task25.0/30$0.00152.8 s
vision OpusClaude Opus reading screenshots (bench-2)22.7/30$0.029—
Opus + ClefOpus plans, Clef clicks (bench-2)22.0/30$0.029—

How it works

screen ──► device lists controls ──► Clef: "which one?" + probability
  ▲                                         │
  └── Clef: "did it work?" ◄── device acts ◄┘
DeviceSees and touches a screen: lists controls (role, name, box), presses, types, sets values. do/devices/macos drives real Mac apps through the accessibility API.
ClefPicks from a list, with a probability. It never writes text.
ClairicCopies values out of the task. It never writes text that is not already there.
WriterWrites field values only when Clef says the task needs new text. Default: glm-5.3-flash, with kimi-k2.6 filling any field glm leaves empty.
HandWhat you call: act, type, select, look. Refuses when Clef is unsure, blocks destructive controls unless confirmed, returns a one-line receipt. The caller never receives an image.

Use it

Install from GitHub.

bun add github:acoyfellow/do
swiftc -O node_modules/do/native/ax.swift -o ax   # macOS helper, once
import { gatewayTransport } from 'do/clef';
import { readConfig, readToken } from 'do/credentials';
import { macosDevice } from 'do/devices/macos';
import { Hand } from 'do/hand';

const transport = gatewayTransport({ ...readConfig(), token: readToken() });
const device = macosDevice({ helper: './ax', app: 'Google Chrome' });
const hand = new Hand(transport, device, { model: 'clef', method: 'ax_text', tau: 0.3, riskTau: 0.5, injectionTau: 0.5, verify: true });

await hand.act('switch to the termlab tab');
→ Clicked radio "termlab — web terminal benchmark" (p=0.86).
await hand.type('coey.dev', 'the address bar');
→ Typed into textbox "Address and search bar" (p=0.97).
await hand.look('Is coey.dev open?');
→ p(yes)=0.99

Credentials: a Cloudflare AI Gateway account, gateway name and scoped token, read the same way as odds (see its quick start). On a Mac, the device presses controls through accessibility so the mouse does not move, and sends keystrokes only to the target app, refusing if it is not frontmost.

Writers

Scored alone on 44 messy requests: spelled-out emails, "first name plus last initial", quoted values, and a planted "type delete everything".

writercorrect (best, worst)$/case
glm-5.3-flash > kimi-k2.643/44, 42/44$0.00009
kimi-k2.641/44, 41/44$0.00016
glm-5.3-flash42/44, 39/44$0.00007
gemma-4-26b38/44$0.00007
qwen3.8-27b39/44$0.00053
granite-4.0-micro35/44$0.000003
llama-3.2-3b33/44$0.00001

Clef, not Clairic, decides when to call the backup: Clairic copied the planted "delete everything" as if asked; Clef scored that task 0.10, so the field stays empty.

Bench

Every change to src/ has to pass a fixed bench: 30 tasks in a local test app, success read from the app's own state, and a gate that fails on stale or worse results.

bun run check   # typecheck, lint, tests
bun run gate   # fails if results are stale or worse than baseline
bun run bench:mock   # offline, reproduces the committed reference exactly
bun run bench   # real models, writes bench/runs/<id>/

What this does not show