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.
| arm | what drives it | finished | $/task |
|---|---|---|---|
relay | Clef picks every action; a writer fills fields only when the task needs new text | 30.0/30 | $0.0016 |
duet | Clef picks every action; Clairic copies values out of the task | 25.0/30 | $0.0015 |
| vision Opus | Claude Opus reading screenshots (bench-2) | 22.7/30 | $0.029 |
| Opus + Clef | Opus plans, Clef clicks (bench-2) | 22.0/30 | $0.029 |
screen ──► device lists controls ──► Clef: "which one?" + probability ▲ │ └── Clef: "did it work?" ◄── device acts ◄┘
do/devices/macos drives real Mac apps through the accessibility API.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.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.
Scored alone on 44 messy requests: spelled-out emails, "first name plus last initial", quoted values, and a planted "type delete everything".
| writer | correct (best, worst) | $/case |
|---|---|---|
| glm-5.3-flash > kimi-k2.6 | 43/44, 42/44 | $0.00009 |
| kimi-k2.6 | 41/44, 41/44 | $0.00016 |
| glm-5.3-flash | 42/44, 39/44 | $0.00007 |
| gemma-4-26b | 38/44 | $0.00007 |
| qwen3.8-27b | 39/44 | $0.00053 |
| granite-4.0-micro | 35/44 | $0.000003 |
| llama-3.2-3b | 33/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.
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>/