@revyl/verify 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,60 @@
1
1
  # Changelog
2
2
 
3
- ## 0.4.1 (unreleased)
3
+ ## 0.6.0 (unreleased)
4
+
5
+ - Early access: a Mac computer's screen. `tap({ x, y })` clicks a point in screenshot pixels and resolves with it, `screenSize()` returns the screenshot's size, `typeFocused(text, { clear: false })` types where the cursor is, and `pressKey(key, { modifiers })` takes the Web keys and modifiers, where `'meta'` is Command and `'alt'` is Option, as in `pressKey('a', { modifiers: ['meta'] })` to select all.
6
+ - `typeFocused` on a computer requires `{ clear: false }` and throws `INVALID_ARGUMENT` without it, naming the fix. It takes up to 16,384 characters, with line breaks and tabs; other control characters are refused before anything is sent. Text over 500 characters goes in pieces, in order, and a failure part way says how many characters were certainly typed and whether the failed piece may have been.
7
+ - A point past the screen's edge throws `INVALID_ARGUMENT` naming the valid range, and nothing is clicked; negative and fractional points are refused before anything is sent. There is no extra request per click.
8
+ - Screen errors: someone controlling the screen in the Revyl viewer (`screen_control_held`) throws `SESSION_BUSY` and nothing happened; an action Revyl can't confirm (`screen_input_uncertain`, `screen_unavailable`, `screen_input_unavailable`) throws `ACTION_UNCERTAIN`; a 403 throws `AUTH_REJECTED` saying the key's owner needs the Member role or above and CI runner keys can't control the screen yet; a 503 `authentication_unavailable` throws `API_ERROR` and is safe to retry. These are read from the answer's `detail`, so `screen_unavailable` is uncertain as a 409 or a 503. Verify never repeats a screen action, except `screen_starting` (409, nothing happened): it waits as `Retry-After` asks and tries again for up to 30 seconds, then throws `SESSION_BUSY` naming when to try again.
9
+ - `tap({ target })` on a Mac computer: Revyl finds the described element on a fresh screenshot, clicks it, and `tap` resolves with the point it clicked. It takes several seconds and can click a look-alike, or something else when the target isn't on the screen, so the docs say to check the point or a screenshot afterwards. `screen_target_not_found` throws `ACTION_FAILED` and `screen_target_unavailable` throws `API_ERROR`, both with nothing clicked; a backend without described clicks (422 `invalid_request`) throws `NOT_AVAILABLE_ON_PLATFORM` saying to pass a point.
10
+ - `locate()` and the other exact actions aren't available on computers. They throw `NOT_AVAILABLE_ON_PLATFORM` before anything is sent, naming `tap({ x, y })`, `tap({ target })`, `typeFocused`, `pressKey` and `exec()`.
11
+ - Every other call sends the same requests as 0.5.0.
12
+ - Error messages on a computer say what happened and what to do next. `screen_control_lost` on a screen action throws `SESSION_BUSY` saying nothing was sent and to try again. `owner_unavailable`, a 5xx or no answer on a screen action throws `ACTION_UNCERTAIN` saying to take a screenshot before repeating it. `invalid_request` on `tap({ x, y })` says nothing was clicked and to pick the point from a new screenshot, and `screen_key_unsupported` says nothing was pressed. Partial typing counts characters as Unicode code points, the same in TypeScript, Python and Go, and a failure in the first piece says nothing was certainly typed. Every other computer refusal adds a next step, and the `screen_starting` message says "1 second" rather than "1 seconds".
13
+ - Docs: a Screen section in `computers.md`, the platform table, `api.md`, `errors.md`, `agents.md` and `llms.txt`. The Web docs say `'meta'` is not the shortcut key there; use `'ctrl'`.
14
+
15
+ ## Go 0.5.0 (unreleased)
16
+
17
+ Published as the `go/v0.5.0` tag.
18
+
19
+ - Early access: a Mac computer's screen, as in TypeScript 0.6.0. `Tap(ctx, verify.Point{X, Y})`, `Tap(ctx, verify.Element("description"))` returning the point it clicked, `ScreenSize(ctx)`, `TypeFocused(ctx, text, TypeOptions{Clear})` with `Clear` pointing to `false`, and `PressKey(ctx, key, KeyOptions{Modifiers})`, where `ModifierMeta` is Command. Leaving `Clear` unset returns `ErrInvalidArgument` naming the fix.
20
+ - The same limits, errors and refusals as TypeScript 0.6.0: `ErrSessionBusy` for `screen_control_held`, `ErrActionUncertain` for an action Revyl can't confirm, `ErrAuthRejected` for a 403, `ErrActionFailed` for `screen_target_not_found`, a wait of up to 30 seconds on `screen_starting`, and `ErrNotAvailableOnPlatform` before any request for `Locate` and the other exact actions. Every other call sends the same requests as v0.4.0.
21
+ - The same error messages as TypeScript 0.6.0, each with a next step, with partial typing counted in Unicode code points.
22
+
23
+ ## Python 0.4.0 (unreleased)
24
+
25
+ - Early access: a Mac computer's screen, as in TypeScript 0.6.0. `tap(Point(x, y))`, `tap("description")` returning the point it clicked, `screen_size()`, `type_focused(text, clear=False)` and `press_key(key, modifiers=[...])`, where `"meta"` is Command. Leaving `clear` out raises `INVALID_ARGUMENT` naming the fix. `AsyncSession` has the same methods.
26
+ - The same limits, errors and refusals as TypeScript 0.6.0, and every other call sends the same requests as 0.3.0.
27
+ - The same error messages as TypeScript 0.6.0, each with a next step, with partial typing counted in Unicode code points.
28
+
29
+ ## 0.5.0 (2026-11-12)
30
+
31
+ - Early access: more exact actions on Web, iOS and Android. `doubleTap(at)` double-taps (double-clicks on Web), `longPress(at, { duration })` presses and holds for `duration` milliseconds (1 to 5000, 1500 by default), and `drag(from, to)` presses at one location, moves to the other and releases. They take a description or screenshot pixels like `tap`, and `doubleTap` and `longPress` resolve with the point.
32
+ - Web: `typeFocused(text, { clear })` types into the field that already has focus, without clicking, and replaces its text unless `clear` is `false`, the same default as `type()`. `pressKey` also takes `'tab'`, `'escape'`, `'delete'`, `'space'`, `'arrowup'`, `'arrowdown'`, `'arrowleft'`, `'arrowright'`, `'home'`, `'end'`, `'pageup'` and `'pagedown'`, and `{ modifiers }`: up to four of `'ctrl'`, `'shift'`, `'alt'` and `'meta'`, with which a lowercase letter or digit works too, as in `pressKey('a', { modifiers: ['ctrl'] })`. The `Key` and `KeyModifier` types are exported.
33
+ - iOS and Android: `goHome()` goes to the home screen.
34
+ - A call the platform lacks throws `NOT_AVAILABLE_ON_PLATFORM` before anything is sent, and the message names what to use: `typeFocused` on iOS and Android (use `type(location, text)`), keys other than `'enter'` and `'backspace'` or any modifiers on iOS and Android, and `goHome` on Web. Computers refuse all of them, like the other exact actions.
35
+ - Your own model can choose `double_tap`, `long_press` and `drag` on every platform, `type_focused` and the wider `press_key` on Web, and `go_home` on iOS and Android, with both groundings. The model sees these extra tools in its tool list; the tools it had before send the same requests as 0.4.1.
36
+ - Every existing call sends the same requests as 0.4.1. Your own model choosing `open_url` with a deep link on Web gets the `openUrl()` wording.
37
+ - Docs: the platform table, `api.md`, the web and mobile guides with a script for the new calls, `agents.md` and `llms.txt`.
38
+
39
+ ## Go 0.4.0 (2026-11-12)
40
+
41
+ Published as the `go/v0.4.0` tag.
42
+
43
+ - Early access: more exact actions on Web, iOS and Android, as in TypeScript 0.5.0. `DoubleTap(ctx, at)` and `LongPress(ctx, at, LongPressOptions{Duration})` return the point; `Duration` is whole milliseconds from 1 millisecond to 5 seconds, and zero leaves Revyl's 1.5 seconds. `Drag(ctx, from, to)` presses, moves and releases.
44
+ - Web: `TypeFocused(ctx, text, TypeOptions{Clear})` types into the field that already has focus and replaces its text unless `Clear` points to `false`. `PressKey(ctx, key, KeyOptions{Modifiers})` adds `KeyTab`, `KeyEscape`, `KeyDelete`, `KeySpace`, `KeyArrowUp`, `KeyArrowDown`, `KeyArrowLeft`, `KeyArrowRight`, `KeyHome`, `KeyEnd`, `KeyPageUp` and `KeyPageDown`, and modifiers (`ModifierCtrl`, `ModifierShift`, `ModifierAlt`, `ModifierMeta`), with which `Key("a")` and other lowercase letters and digits work too. Existing `PressKey(ctx, key)` calls compile unchanged.
45
+ - iOS and Android: `GoHome(ctx)`.
46
+ - A call the platform lacks returns `ErrNotAvailableOnPlatform` before anything is sent, naming the alternative. Your own model can choose the new actions with both groundings, so it sees more tools; the tools it had send the same requests as v0.3.1, as does every existing call.
47
+ - `ErrInstructionFailed` names the step and the reason, as TypeScript does: `Instruction failed: "<text>". <reason> See <link>`. The uncertain-action message says "The step deadline passed". Your own model choosing `open_url` with a deep link on Web gets the `OpenURL` wording.
48
+
49
+ ## Python 0.3.0 (2026-11-12)
50
+
51
+ - Early access: more exact actions on Web, iOS and Android, as in TypeScript 0.5.0. `double_tap(at)` and `long_press(at, duration=None)` return the point; `duration` is seconds, 0.001 to 5 in whole milliseconds, 1.5 by default. `drag(start, end)` presses, moves and releases.
52
+ - Web: `type_focused(text, clear=True)` types into the field that already has focus and replaces its text unless `clear` is `False`. `press_key(key, modifiers=())` adds `"tab"`, `"escape"`, `"delete"`, `"space"`, the arrows, `"home"`, `"end"`, `"pageup"` and `"pagedown"`, and modifiers (`"ctrl"`, `"shift"`, `"alt"`, `"meta"`), with which a lowercase letter or digit works too. `Key` and `KeyModifier` are exported.
53
+ - iOS and Android: `go_home()`. `AsyncSession` has every new method.
54
+ - A call the platform lacks raises `NOT_AVAILABLE_ON_PLATFORM` before anything is sent, naming the alternative. Your own model can choose the new actions with both groundings, so it sees more tools; the tools it had send the same requests as 0.2.1, as does every existing call.
55
+ - `INSTRUCTION_FAILED` names the step and the reason, as TypeScript does: `Instruction failed: "<text>". <reason> See <link>`, and the progress line for a failed step on your own model ends with the reason. Your own model choosing `open_url` with a deep link on Web gets the `open_url()` wording.
56
+
57
+ ## 0.4.1 (2026-11-12)
4
58
 
5
59
  - **Breaking for iOS exact actions (early access):** coordinates are now screenshot pixels on every platform. On iOS, `tap`, `type`, `swipe`'s `from`, `locate`, `screenSize` and the points `tap` and `type` return used the screen's points (440 × 956 on an iPhone whose screenshots are 1320 × 2868); they now use the screenshot's pixels, and Verify converts them to points for you. A script that passed iOS points must pass screenshot pixels instead: multiply by the screenshot's width over 0.4.0's `screenSize()` width (3 on that iPhone). Android and Web coordinates are unchanged, because their screenshots already match the screen.
6
60
  - A point outside the screenshot is refused with `INVALID_ARGUMENT` before any action is sent, and the message names the valid range. The first call in a session that takes or returns a point reads the screen size once (on iOS, plus one screenshot), and later calls reuse it. Before refusing a point, Verify reads the size again, so a rotated device or a resized Web viewport doesn't get a valid point refused. On Android and Web, if the size can't be read, the action is sent as in 0.4.0; on iOS the call fails, because the scale is needed. Calls by description send the same requests as 0.4.0 on Android and Web.
@@ -8,7 +62,7 @@
8
62
  - `instruction()` and `validation()` on your own model are unchanged: the model still sees screenshots, and Verify still maps its answers to the device.
9
63
  - Docs: one coordinate rule in the same words for TypeScript, Python and Go, in the README, `api.md`, the web and mobile guides, `agents.md` and `llms.txt`. Web coordinates are screenshot pixels, which are CSS pixels at the default device scale factor of 1.
10
64
 
11
- ## Go 0.3.1 (unreleased)
65
+ ## Go 0.3.1 (2026-11-12)
12
66
 
13
67
  Published as the `go/v0.3.1` tag.
14
68
 
@@ -16,7 +70,7 @@ Published as the `go/v0.3.1` tag.
16
70
  - A point outside the screenshot returns `ErrInvalidArgument` before any action is sent, naming the valid range, after Verify reads the size again in case the screen rotated. The screen size is read once per session. On Android and Web a failed size read skips the check and sends the action as before; on iOS it returns the error.
17
71
  - README: the same coordinate rule as TypeScript and Python.
18
72
 
19
- ## Python 0.2.1 (unreleased)
73
+ ## Python 0.2.1 (2026-11-12)
20
74
 
21
75
  - **Breaking for iOS exact actions (early access):** coordinates are now screenshot pixels on every platform, as in TypeScript 0.4.1. On iOS, `tap`, `type`, `swipe`'s `from_`, `locate`, `screen_size` and the points `tap` and `type` return used the screen's points; they now use the screenshot's pixels, and Verify converts them to points for you. Android and Web coordinates are unchanged. `AsyncSession` behaves the same.
22
76
  - A point outside the screenshot raises `INVALID_ARGUMENT` before any action is sent, naming the valid range, after Verify reads the size again in case the screen rotated. The screen size is read once per session. On Android and Web a failed size read skips the check and sends the action as before; on iOS it raises.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Verify
2
2
 
3
- ![Terminal output from one Verify script beside a Mac computer's Terminal window showing the macOS version, and the screens the script reached in a cloud browser, an Android emulator, and an iOS simulator](https://raw.githubusercontent.com/RevylAI/verify/v0.4.1/docs/images/verify-hero.webp)
3
+ ![Terminal output from one Verify script beside a Mac computer's Terminal window showing the macOS version, and the screens the script reached in a cloud browser, an Android emulator, and an iOS simulator](https://raw.githubusercontent.com/RevylAI/verify/v0.6.0/docs/images/verify-hero.webp)
4
4
 
5
- Verify runs end-to-end checks in a cloud browser, iOS simulator, or Android emulator on Revyl. You write what the agent should do and what must be true afterward, in plain sentences, and call it from a script, a coding agent, CI, or the test runner you already use. When you know exactly what to press, you can tap, type and swipe yourself instead. Verify also opens Mac computers for commands, files, and screenshots.
5
+ Verify runs end-to-end checks in a cloud browser, iOS simulator, or Android emulator on Revyl. You write what the agent should do and what must be true afterward, in plain sentences, and call it from a script, a coding agent, CI, or the test runner you already use. When you know exactly what to press, you can tap, type and swipe yourself instead. Verify also opens Mac computers for commands, files, screenshots, and clicks and keys on the screen.
6
6
 
7
7
  ```ts
8
8
  import { open } from '@revyl/verify';
@@ -41,24 +41,26 @@ Or set it up yourself:
41
41
  npx @revyl/verify init
42
42
  ```
43
43
 
44
- `init` adds Verify to your project, asks what to open, writes `verify.config.ts` and an example script, signs you in through the browser on the first interactive run, and prints the command that runs the example. With no app id, the iOS and Android examples open the built-in Settings app, so they run on a new account before you upload anything. To skip the config, run `npm install @revyl/verify` and save the example above as `verify/check.mts`, then run `node verify/check.mts`. [Getting started](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/getting-started.md) covers sign-in, your own app, and [CI](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/ci.md).
44
+ `init` adds Verify to your project, asks what to open, writes `verify.config.ts` and an example script, signs you in through the browser on the first interactive run, and prints the command that runs the example. With no app id, the iOS and Android examples open the built-in Settings app, so they run on a new account before you upload anything. To skip the config, run `npm install @revyl/verify` and save the example above as `verify/check.mts`, then run `node verify/check.mts`. [Getting started](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/getting-started.md) covers sign-in, your own app, and [CI](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/ci.md).
45
45
 
46
- For finished projects, see [`examples/`](https://github.com/RevylAI/verify/tree/v0.4.1/examples/): a [web store](https://github.com/RevylAI/verify/tree/v0.4.1/examples/web-store/), [mobile Settings](https://github.com/RevylAI/verify/tree/v0.4.1/examples/mobile-settings/), a [Mac workspace](https://github.com/RevylAI/verify/tree/v0.4.1/examples/mac-workspace/), a small [agent platform](https://github.com/RevylAI/verify/tree/v0.4.1/examples/agent-platform/) that runs web and iOS side by side, and [your own model](https://github.com/RevylAI/verify/tree/v0.4.1/examples/byo-model/) (with [Python](https://github.com/RevylAI/verify/blob/v0.4.1/python/examples/byo_model.py) and [Go](https://github.com/RevylAI/verify/blob/v0.4.1/go/examples/byomodel/main.go) versions).
46
+ For finished projects, see [`examples/`](https://github.com/RevylAI/verify/tree/v0.6.0/examples/): a [web store](https://github.com/RevylAI/verify/tree/v0.6.0/examples/web-store/), [mobile Settings](https://github.com/RevylAI/verify/tree/v0.6.0/examples/mobile-settings/), a [Mac workspace](https://github.com/RevylAI/verify/tree/v0.6.0/examples/mac-workspace/), a small [agent platform](https://github.com/RevylAI/verify/tree/v0.6.0/examples/agent-platform/) that runs web and iOS side by side, and [your own model](https://github.com/RevylAI/verify/tree/v0.6.0/examples/byo-model/) (with [Python](https://github.com/RevylAI/verify/blob/v0.6.0/python/examples/byo_model.py) and [Go](https://github.com/RevylAI/verify/blob/v0.6.0/go/examples/byomodel/main.go) versions).
47
47
 
48
48
  ## Exact actions
49
49
 
50
- Tap, type, swipe and press keys yourself, by a description Revyl locates on screen or by coordinates, in the same script as your steps:
50
+ Tap, double-tap, long-press, drag, type, swipe and press keys yourself, by a description Revyl locates on screen or by coordinates, in the same script as your steps:
51
51
 
52
52
  ```ts
53
53
  await session.type({ target: 'Email field' }, 'demo@example.com'); // Revyl finds the element you describe
54
54
  await session.pressKey('enter');
55
55
  await session.swipe('up'); // scrolls the content down
56
56
  await session.tap({ x: 196, y: 640 }); // or a point in screenshot pixels
57
+ await session.longPress({ target: 'First message row' }); // press and hold, 1.5 s by default
58
+ await session.drag({ target: 'Third item handle' }, { target: 'First item' }); // press, move, release
57
59
  await session.instruction('Accept the terms if they are shown'); // the agent where it's fuzzy
58
60
  await session.validation('The welcome screen is shown');
59
61
  ```
60
62
 
61
- `back`, `openUrl`, `launchApp`, `locate` and `screenSize` round them out; [which call works where](#which-call-works-where) shows the platform limits. Early access. In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.3.0); the [Python](https://github.com/RevylAI/verify/blob/v0.4.1/python/README.md#exact-actions) and [Go](https://github.com/RevylAI/verify/blob/v0.4.1/go/README.md#exact-actions) READMEs show them with each language's names. The [web](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/web.md#exact-actions) and [mobile](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/mobile.md#exact-actions) guides have runnable scripts, and [api.md](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/api.md#exact-actions) lists every call.
63
+ `doubleTap`, `typeFocused`, `back`, `goHome`, `openUrl`, `launchApp`, `locate` and `screenSize` round them out; [which call works where](#which-call-works-where) shows the platform limits. Early access. Since TypeScript (`@revyl/verify`) 0.4.0, Python (`revyl-verify`) 0.2.0 and Go v0.3.0. `doubleTap`, `longPress`, `drag`, `typeFocused`, `goHome`, and Web keys and modifiers in `pressKey`, are new in TypeScript 0.5.0, Python 0.3.0 and Go v0.4.0. On a Mac computer's screen, `tap` (by point or by description), `screenSize`, `typeFocused` and `pressKey` are new in TypeScript 0.6.0, Python 0.4.0 and Go v0.5.0. The [Python](https://github.com/RevylAI/verify/blob/v0.6.0/python/README.md#exact-actions) and [Go](https://github.com/RevylAI/verify/blob/v0.6.0/go/README.md#exact-actions) READMEs show them with each language's names. The [web](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/web.md#exact-actions) and [mobile](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/mobile.md#exact-actions) guides have runnable scripts, and [api.md](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/api.md#exact-actions) lists every call.
62
64
 
63
65
  ## Your own model
64
66
 
@@ -72,7 +74,7 @@ await session.instruction('Open Privacy', { model: 'openai/gpt-5.5' });
72
74
  await open('ios', { model: { id: 'qwen-3.8-27b', baseURL: 'https://api.cerebras.ai/v1', apiKey: process.env.CEREBRAS_API_KEY } });
73
75
  ```
74
76
 
75
- In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.3.0); the [Python](https://github.com/RevylAI/verify/blob/v0.4.1/python/README.md#your-own-model) and [Go](https://github.com/RevylAI/verify/blob/v0.4.1/go/README.md#your-own-model) READMEs show them with each language's names. Built in: `anthropic`, `openai`, `google`, `openrouter`, `groq`, `cerebras`, `xai` and `ollama`; anything OpenAI-compatible works through `baseURL`. Early access; [Use your own model](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/own-model.md) covers the providers, picking a model, what Revyl still sees, limits and troubleshooting.
77
+ Since TypeScript (`@revyl/verify`) 0.4.0, Python (`revyl-verify`) 0.2.0 and Go v0.3.0; the [Python](https://github.com/RevylAI/verify/blob/v0.6.0/python/README.md#your-own-model) and [Go](https://github.com/RevylAI/verify/blob/v0.6.0/go/README.md#your-own-model) READMEs show them with each language's names. Built in: `anthropic`, `openai`, `google`, `openrouter`, `groq`, `cerebras`, `xai` and `ollama`; anything OpenAI-compatible works through `baseURL`. Early access; [Use your own model](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/own-model.md) covers the providers, picking a model, what Revyl still sees, limits and troubleshooting.
76
78
 
77
79
  ## Packages
78
80
 
@@ -80,8 +82,8 @@ In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.
80
82
  | --- | --- | --- |
81
83
  | [`@revyl/verify`](https://www.npmjs.com/package/@revyl/verify) | `npm install @revyl/verify` | TypeScript SDK and the `verify` CLI (`init`, `login`, `logout`, `whoami`). Published on npm, Node.js 22.18 or later. |
82
84
  | [`revyl-verify`](https://www.npmjs.com/package/revyl-verify) (npm) | `npm install revyl-verify` | Alias of `@revyl/verify` at the same version. |
83
- | [Go module](https://github.com/RevylAI/verify/tree/go/v0.3.1/go) | `go get github.com/RevylAI/verify/go@v0.3.1` | Published as the `go/v0.3.1` tag, Go 1.26.9 or later. Calls return errors instead of throwing. |
84
- | [`revyl-verify`](https://pypi.org/project/revyl-verify/) (PyPI) | `pip install revyl-verify` or `uv add revyl-verify` | Python SDK, published on PyPI. Python 3.10 or later, sync and asyncio APIs, no CLI; see the [Python guide](https://github.com/RevylAI/verify/blob/v0.4.1/python/README.md). |
85
+ | [Go module](https://github.com/RevylAI/verify/tree/go/v0.5.0/go) | `go get github.com/RevylAI/verify/go@v0.5.0` | Published as the `go/v0.5.0` tag, Go 1.26.9 or later. Calls return errors instead of throwing. |
86
+ | [`revyl-verify`](https://pypi.org/project/revyl-verify/) (PyPI) | `pip install revyl-verify` or `uv add revyl-verify` | Python SDK, published on PyPI. Python 3.10 or later, sync and asyncio APIs, no CLI; see the [Python guide](https://github.com/RevylAI/verify/blob/v0.6.0/python/README.md). |
85
87
 
86
88
  ## Platforms
87
89
 
@@ -90,35 +92,40 @@ In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.
90
92
  | Web | Yes | Yes | — | Yes |
91
93
  | iOS simulator | Yes | Yes | — | Yes |
92
94
  | Android emulator | Yes | Yes | — | Yes |
93
- | Mac computer | — | — | Yes | Yes |
95
+ | Mac computer | — | Screen: `tap`, `typeFocused`, `pressKey` | Yes | Yes |
94
96
 
95
97
  ### Which call works where
96
98
 
97
99
  | Call | Web | iOS | Android | Mac computer |
98
100
  | --- | --- | --- | --- | --- |
99
- | `tap`, `type`, `swipe`, `pressKey`, `locate`, `screenSize` | Yes | Yes | Yes | No, use `exec()` and files |
101
+ | `tap({ x, y })`, `tap({ target })`, `screenSize` | Yes | Yes | Yes | Yes |
102
+ | `locate` | Yes | Yes | Yes | No, use `tap({ target })`, which returns the point it clicked, or read the point off `screenshot()` |
103
+ | `doubleTap`, `longPress`, `type`, `swipe`, `drag` | Yes | Yes | Yes | No |
104
+ | `pressKey(key, { modifiers })` | Yes: `'enter'`, `'backspace'`, `'tab'`, `'escape'`, arrows and more, with modifiers | `'enter'` and `'backspace'` only | `'enter'` and `'backspace'` only | Yes: the Web keys and modifiers, where `'meta'` is Command |
105
+ | `typeFocused(text, { clear })` | Yes, types into the focused field | No, use `type(location, text)` | No, use `type(location, text)` | Yes, with `{ clear: false }` |
100
106
  | `back()` | Yes, browser history | No, tap the on-screen back control | Yes, the system back button | No |
107
+ | `goHome()` | No, use `openUrl` or `navigate` | Yes, the home screen | Yes, the home screen | No |
101
108
  | `openUrl(url)` | Yes, loads an http(s) URL in the tab | Yes, a URL or deep link | Yes, a URL or deep link | No |
102
109
  | `launchApp(bundleId)` | No, use `openUrl` or `navigate` | Yes | Yes | No |
103
110
  | `navigate(url)` | Yes | No, use `openUrl` | No, use `openUrl` | No |
104
- | `instruction`, `validation`, your own model | Yes | Yes | Yes | No, use `exec()` |
111
+ | `instruction`, `validation`, your own model | Yes | Yes | Yes | No, use `exec()` and the screen calls above |
105
112
  | `screenshot()` | Yes | Yes | Yes | Yes |
106
113
  | `exec`, files, processes | No, open a Mac computer | No, open a Mac computer | No, open a Mac computer | Yes |
107
114
 
108
- **Coordinates.** Prefer a description, such as `tap({ target: 'Sign in button' })`: Revyl finds the element on the current screen, and you never handle coordinates. Otherwise a point is `{ x, y }` in screenshot pixels on every platform: whole numbers from the top-left corner of the image `screenshot()` returns. So a position you read off a screenshot goes to `tap()`, `type()` and `swipe()` unchanged. `screenSize()` returns the screenshot's width and height, and `locate()` and the points `tap()` and `type()` return use the same pixels. A point outside the screenshot is refused with `INVALID_ARGUMENT` before any action is sent, and the message names the valid range. On iOS, Verify converts the pixels to the screen's points for you. On Web they equal CSS pixels at the default device scale factor of 1, and you still pass screenshot pixels at any other. This holds from TypeScript 0.4.1, Python 0.2.1 and Go v0.3.1; earlier versions took iOS points.
115
+ **Coordinates.** Prefer a description, such as `tap({ target: 'Sign in button' })`: Revyl finds the element on the current screen, and you never handle coordinates. Otherwise a point is `{ x, y }` in screenshot pixels on every platform: whole numbers from the top-left corner of the image `screenshot()` returns. So a position you read off a screenshot goes to `tap()`, `type()`, `swipe()` and every other exact action unchanged. `screenSize()` returns the screenshot's width and height, and `locate()` and the points `tap()`, `doubleTap()`, `longPress()` and `type()` return use the same pixels. A point outside the screenshot is refused with `INVALID_ARGUMENT` before any action is sent, and the message names the valid range. On iOS, Verify converts the pixels to the screen's points for you. On Web they equal CSS pixels at the default device scale factor of 1, and you still pass screenshot pixels at any other. This holds from TypeScript 0.4.1, Python 0.2.1 and Go v0.3.1; earlier versions took iOS points. On a Mac computer a point is in screenshot pixels too, and `tap({ target })` returns the point it clicked in the same pixels. Verify refuses a negative or fractional point before sending it, and Revyl refuses one past the screen's edge without clicking; both throw `INVALID_ARGUMENT` naming the valid range.
109
116
 
110
117
  A call a platform doesn't support throws `NOT_AVAILABLE_ON_PLATFORM`, and its message names the alternative.
111
118
 
112
- Web is available to every organization. Mac computers are early access and have a server-bounded lifetime; see [computers](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/computers.md).
119
+ Web is available to every organization. Mac computers are early access and have a server-bounded lifetime; see [computers](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/computers.md).
113
120
 
114
121
  ## Documentation
115
122
 
116
- The [documentation](https://github.com/RevylAI/verify/tree/v0.4.1/packages/verify/docs/) covers the [API](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/api.md), [configuration](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/configuration.md), [web](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/web.md), [mobile](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/mobile.md), [computers](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/computers.md), [CI](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/ci.md), [your own model](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/own-model.md), [errors and troubleshooting](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/errors.md), [coding agents](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/agents.md), and [live end-to-end results](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/end-to-end.md). The npm package ships these pages in `node_modules/@revyl/verify/docs/`, with an [llms.txt](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/llms.txt) index, so a coding agent can read the docs for the version you installed. The Revyl docs site has an overview at [docs.revyl.com/verify](https://docs.revyl.com/verify).
123
+ The [documentation](https://github.com/RevylAI/verify/tree/v0.6.0/packages/verify/docs/) covers the [API](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/api.md), [configuration](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/configuration.md), [web](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/web.md), [mobile](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/mobile.md), [computers](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/computers.md), [CI](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/ci.md), [your own model](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/own-model.md), [errors and troubleshooting](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/errors.md), [coding agents](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/agents.md), and [live end-to-end results](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/end-to-end.md). The npm package ships these pages in `node_modules/@revyl/verify/docs/`, with an [llms.txt](https://github.com/RevylAI/verify/blob/v0.6.0/packages/verify/docs/llms.txt) index, so a coding agent can read the docs for the version you installed. The Revyl docs site has an overview at [docs.revyl.com/verify](https://docs.revyl.com/verify).
117
124
 
118
125
  ## Status
119
126
 
120
- Verify is at 0.x, so APIs can still change between minor releases; the [changelog](https://github.com/RevylAI/verify/blob/v0.4.1/CHANGELOG.md) records each release. Mac computers are early access. See [Contributing](https://github.com/RevylAI/verify/blob/v0.4.1/CONTRIBUTING.md) and [Releasing](https://github.com/RevylAI/verify/blob/v0.4.1/RELEASING.md) to work on Verify.
127
+ Verify is at 0.x, so APIs can still change between minor releases; the [changelog](https://github.com/RevylAI/verify/blob/v0.6.0/CHANGELOG.md) records each release. Mac computers are early access. See [Contributing](https://github.com/RevylAI/verify/blob/v0.6.0/CONTRIBUTING.md) and [Releasing](https://github.com/RevylAI/verify/blob/v0.6.0/RELEASING.md) to work on Verify.
121
128
 
122
129
  ## License
123
130
 
124
- [Apache 2.0](https://github.com/RevylAI/verify/blob/v0.4.1/LICENSE).
131
+ [Apache 2.0](https://github.com/RevylAI/verify/blob/v0.6.0/LICENSE).
@@ -10,27 +10,69 @@ export interface Target {
10
10
  }
11
11
  export type Location = Point | Target;
12
12
  export type SwipeDirection = 'up' | 'down' | 'left' | 'right';
13
+ /** Keys pressKey() takes on Web. iOS and Android take only 'enter' and 'backspace'. */
14
+ export declare const WEB_KEYS: {
15
+ readonly enter: "Enter";
16
+ readonly backspace: "Backspace";
17
+ readonly tab: "Tab";
18
+ readonly escape: "Escape";
19
+ readonly delete: "Delete";
20
+ readonly space: "Space";
21
+ readonly arrowup: "ArrowUp";
22
+ readonly arrowdown: "ArrowDown";
23
+ readonly arrowleft: "ArrowLeft";
24
+ readonly arrowright: "ArrowRight";
25
+ readonly home: "Home";
26
+ readonly end: "End";
27
+ readonly pageup: "PageUp";
28
+ readonly pagedown: "PageDown";
29
+ };
30
+ export type Key = keyof typeof WEB_KEYS;
31
+ export type KeyModifier = 'ctrl' | 'shift' | 'alt' | 'meta';
13
32
  export type DeviceAction = {
14
33
  kind: 'tap';
15
34
  at: Location;
35
+ } | {
36
+ kind: 'double_tap';
37
+ at: Location;
38
+ } | {
39
+ kind: 'long_press';
40
+ at: Location;
41
+ durationMs?: number;
16
42
  } | {
17
43
  kind: 'type';
18
44
  at: Location;
19
45
  text: string;
20
46
  clear: boolean;
47
+ } | {
48
+ kind: 'type_focused';
49
+ text: string;
50
+ clear: boolean;
21
51
  } | {
22
52
  kind: 'swipe';
23
53
  direction: SwipeDirection;
24
54
  from?: Location;
55
+ } | {
56
+ kind: 'drag';
57
+ from: Location;
58
+ to: Location;
25
59
  } | {
26
60
  kind: 'key';
27
- key: 'enter' | 'backspace';
61
+ key: string;
62
+ modifiers?: KeyModifier[];
28
63
  } | {
29
64
  kind: 'back';
65
+ } | {
66
+ kind: 'go_home';
30
67
  } | {
31
68
  kind: 'open_url';
32
69
  url: string;
33
70
  };
71
+ /** Checks a key and its modifiers for the platform, before any request. */
72
+ export declare function checkKey(platform: string, key: unknown, modifiers?: unknown): {
73
+ key: string;
74
+ modifiers: KeyModifier[];
75
+ };
34
76
  /** What the agent loop needs from a session. DeviceSession provides it. */
35
77
  export interface AgentControls {
36
78
  readonly platform: 'ios' | 'android' | 'web';
@@ -1,6 +1,40 @@
1
1
  import { setTimeout as delay } from 'node:timers/promises';
2
2
  import { VerifyError, errorMessage, isVerifyError } from '../errors.js';
3
3
  import { fitPng } from './image.js';
4
+ /** Keys pressKey() takes on Web. iOS and Android take only 'enter' and 'backspace'. */
5
+ export const WEB_KEYS = {
6
+ enter: 'Enter',
7
+ backspace: 'Backspace',
8
+ tab: 'Tab',
9
+ escape: 'Escape',
10
+ delete: 'Delete',
11
+ space: 'Space',
12
+ arrowup: 'ArrowUp',
13
+ arrowdown: 'ArrowDown',
14
+ arrowleft: 'ArrowLeft',
15
+ arrowright: 'ArrowRight',
16
+ home: 'Home',
17
+ end: 'End',
18
+ pageup: 'PageUp',
19
+ pagedown: 'PageDown',
20
+ };
21
+ /** Checks a key and its modifiers for the platform, before any request. */
22
+ export function checkKey(platform, key, modifiers = []) {
23
+ if (!Array.isArray(modifiers) || modifiers.length > 4 || !modifiers.every((modifier) => ['ctrl', 'shift', 'alt', 'meta'].includes(modifier))) {
24
+ throw new VerifyError('INVALID_ARGUMENT', "pressKey() modifiers are up to four of 'ctrl', 'shift', 'alt' and 'meta'.");
25
+ }
26
+ if (platform !== 'web') {
27
+ if ((key === 'enter' || key === 'backspace') && !modifiers.length)
28
+ return { key, modifiers: [] };
29
+ if (modifiers.length || (typeof key === 'string' && key in WEB_KEYS)) {
30
+ throw new VerifyError('NOT_AVAILABLE_ON_PLATFORM', "On iOS and Android, pressKey() takes only 'enter' or 'backspace', with no modifiers; tap the on-screen control instead.");
31
+ }
32
+ throw new VerifyError('INVALID_ARGUMENT', "pressKey() takes 'enter' or 'backspace'.");
33
+ }
34
+ if (typeof key === 'string' && (key in WEB_KEYS || (/^[a-z0-9]$/.test(key) && modifiers.length)))
35
+ return { key, modifiers: [...modifiers] };
36
+ throw new VerifyError('INVALID_ARGUMENT', `pressKey() takes one of ${Object.keys(WEB_KEYS).map((name) => `'${name}'`).join(', ')}, or a lowercase letter or digit with modifiers, such as pressKey('a', { modifiers: ['ctrl'] }).`);
37
+ }
4
38
  /** Runs instructions and validations on a model you choose, on your machine. Revyl only receives device actions. */
5
39
  export function localAgent(options) {
6
40
  if (!options || typeof options.model?.complete !== 'function' || typeof options.model.name !== 'string') {
@@ -93,45 +127,67 @@ export async function runLocalStep(type, task, agent, controls, signal, log) {
93
127
  }
94
128
  function toAction(call, platform, grounding, image, size) {
95
129
  const args = call.arguments;
96
- const location = (required) => {
130
+ const location = (required, prefix = '') => {
131
+ const [target, x, y] = [args[`${prefix}target`], args[`${prefix}x`], args[`${prefix}y`]];
97
132
  if (grounding === 'revyl') {
98
- if (typeof args.target === 'string' && args.target.trim())
99
- return { target: args.target.trim().slice(0, 500) };
133
+ if (typeof target === 'string' && target.trim())
134
+ return { target: target.trim().slice(0, 500) };
100
135
  if (required)
101
- throw new VerifyError('INVALID_ARGUMENT', 'target is required: describe the element by its visible text or appearance.');
136
+ throw new VerifyError('INVALID_ARGUMENT', `${prefix}target is required: describe the element by its visible text or appearance.`);
102
137
  return undefined;
103
138
  }
104
- if (typeof args.x !== 'number' || typeof args.y !== 'number') {
139
+ if (typeof x !== 'number' || typeof y !== 'number') {
105
140
  if (required)
106
- throw new VerifyError('INVALID_ARGUMENT', 'x and y are required.');
141
+ throw new VerifyError('INVALID_ARGUMENT', `${prefix}x and ${prefix}y are required.`);
107
142
  return undefined;
108
143
  }
109
- if (args.x < 0 || args.y < 0 || args.x >= image.width || args.y >= image.height) {
110
- throw new VerifyError('INVALID_ARGUMENT', `(${args.x}, ${args.y}) is outside the ${image.width}x${image.height} screenshot.`);
144
+ if (x < 0 || y < 0 || x >= image.width || y >= image.height) {
145
+ throw new VerifyError('INVALID_ARGUMENT', `(${x}, ${y}) is outside the ${image.width}x${image.height} screenshot.`);
111
146
  }
112
- return { x: Math.round((args.x * size.width) / image.width), y: Math.round((args.y * size.height) / image.height) };
147
+ return { x: Math.round((x * size.width) / image.width), y: Math.round((y * size.height) / image.height) };
113
148
  };
114
149
  switch (call.name) {
115
150
  case 'tap':
116
151
  return { kind: 'tap', at: location(true) };
152
+ case 'double_tap':
153
+ return { kind: 'double_tap', at: location(true) };
154
+ case 'long_press':
155
+ return { kind: 'long_press', at: location(true) };
117
156
  case 'type':
118
157
  if (typeof args.text !== 'string')
119
158
  throw new VerifyError('INVALID_ARGUMENT', 'text is required.');
120
159
  return { kind: 'type', at: location(true), text: args.text, clear: args.clear !== false };
160
+ case 'type_focused':
161
+ if (platform !== 'web')
162
+ throw new VerifyError('NOT_AVAILABLE_ON_PLATFORM', 'type_focused is available only in a web browser; use type, which taps the field first.');
163
+ if (typeof args.text !== 'string')
164
+ throw new VerifyError('INVALID_ARGUMENT', 'text is required.');
165
+ return { kind: 'type_focused', text: args.text, clear: args.clear !== false };
166
+ case 'drag':
167
+ return { kind: 'drag', from: location(true, 'from_'), to: location(true, 'to_') };
121
168
  case 'swipe': {
122
169
  if (!['up', 'down', 'left', 'right'].includes(String(args.direction)))
123
170
  throw new VerifyError('INVALID_ARGUMENT', 'direction must be up, down, left or right.');
124
171
  const from = location(false);
125
172
  return { kind: 'swipe', direction: args.direction, ...(from ? { from } : {}) };
126
173
  }
127
- case 'press_key':
128
- if (args.key !== 'enter' && args.key !== 'backspace')
129
- throw new VerifyError('INVALID_ARGUMENT', 'key must be enter or backspace.');
130
- return { kind: 'key', key: args.key };
174
+ case 'press_key': {
175
+ if (platform !== 'web') {
176
+ if (args.key !== 'enter' && args.key !== 'backspace')
177
+ throw new VerifyError('INVALID_ARGUMENT', 'key must be enter or backspace.');
178
+ return { kind: 'key', key: args.key };
179
+ }
180
+ const { key, modifiers } = checkKey(platform, args.key, args.modifiers ?? []);
181
+ return { kind: 'key', key, ...(modifiers.length ? { modifiers } : {}) };
182
+ }
131
183
  case 'back':
132
184
  if (platform === 'ios')
133
185
  throw new VerifyError('NOT_AVAILABLE_ON_PLATFORM', 'iOS has no back button; tap the on-screen back control.');
134
186
  return { kind: 'back' };
187
+ case 'go_home':
188
+ if (platform === 'web')
189
+ throw new VerifyError('NOT_AVAILABLE_ON_PLATFORM', 'A web browser has no home screen; use open_url to load a page.');
190
+ return { kind: 'go_home' };
135
191
  case 'open_url':
136
192
  if (typeof args.url !== 'string' || !args.url)
137
193
  throw new VerifyError('INVALID_ARGUMENT', 'url is required.');
@@ -145,14 +201,24 @@ function describe(action) {
145
201
  switch (action.kind) {
146
202
  case 'tap':
147
203
  return `tap${where(action.at)}`;
204
+ case 'double_tap':
205
+ return `double tap${where(action.at)}`;
206
+ case 'long_press':
207
+ return `long press${where(action.at)}`;
148
208
  case 'type':
149
209
  return `type ${JSON.stringify(action.text.length > 40 ? `${action.text.slice(0, 40)}…` : action.text)} into${where(action.at)}`;
210
+ case 'type_focused':
211
+ return `type ${JSON.stringify(action.text.length > 40 ? `${action.text.slice(0, 40)}…` : action.text)} into the focused field`;
150
212
  case 'swipe':
151
213
  return `swipe ${action.direction}${action.from ? ` from${where(action.from)}` : ''}`;
214
+ case 'drag':
215
+ return `drag from${where(action.from)} to${where(action.to)}`;
152
216
  case 'key':
153
- return `press ${action.key}`;
217
+ return `press ${[...(action.modifiers ?? []), action.key].join('+')}`;
154
218
  case 'back':
155
219
  return 'back';
220
+ case 'go_home':
221
+ return 'home';
156
222
  case 'open_url':
157
223
  return `open ${action.url}`;
158
224
  }
@@ -188,25 +254,57 @@ function instructionTools(platform, grounding) {
188
254
  const at = grounding === 'revyl'
189
255
  ? { properties: { target: { type: 'string', description: 'The element, by its visible text or look.' } }, required: ['target'] }
190
256
  : { properties: { x: { type: 'integer', description: 'Screenshot pixel x.' }, y: { type: 'integer', description: 'Screenshot pixel y.' } }, required: ['x', 'y'] };
191
- const tools = [
257
+ const prefixed = (prefix) => ({
258
+ properties: Object.fromEntries(Object.entries(at.properties).map(([name, schema]) => [`${prefix}${name}`, schema])),
259
+ required: at.required.map((name) => `${prefix}${name}`),
260
+ });
261
+ const [from, to] = [prefixed('from_'), prefixed('to_')];
262
+ const web = platform === 'web';
263
+ return [
192
264
  { name: 'tap', description: 'Tap an element.', parameters: { type: 'object', ...at } },
265
+ { name: 'double_tap', description: 'Double-tap an element.', parameters: { type: 'object', ...at } },
266
+ { name: 'long_press', description: 'Press and hold an element, for example to open a context menu.', parameters: { type: 'object', ...at } },
193
267
  {
194
268
  name: 'type',
195
269
  description: 'Tap a text field and type into it. Clears the field first unless clear is false.',
196
270
  parameters: { type: 'object', properties: { ...at.properties, text: { type: 'string' }, clear: { type: 'boolean' } }, required: [...at.required, 'text'] },
197
271
  },
272
+ ...(web
273
+ ? [{
274
+ name: 'type_focused',
275
+ description: 'Type into the field that already has focus, without tapping. Clears the field first unless clear is false.',
276
+ parameters: { type: 'object', properties: { text: { type: 'string' }, clear: { type: 'boolean' } }, required: ['text'] },
277
+ }]
278
+ : []),
198
279
  {
199
280
  name: 'swipe',
200
281
  description: "Swipe from a point, or from the screen's center. up moves the finger up, which scrolls content down.",
201
282
  parameters: { type: 'object', properties: { direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] }, ...at.properties }, required: ['direction'] },
202
283
  },
203
- { name: 'press_key', description: 'Press a key on the focused field.', parameters: { type: 'object', properties: { key: { type: 'string', enum: ['enter', 'backspace'] } }, required: ['key'] } },
204
- { name: 'open_url', description: platform === 'web' ? 'Load a URL in the current tab.' : 'Open a URL or deep link on the device.', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
284
+ {
285
+ name: 'drag',
286
+ description: 'Press an element, move it to another place and release, for example to reorder a list or move a slider.',
287
+ parameters: { type: 'object', properties: { ...from.properties, ...to.properties }, required: [...from.required, ...to.required] },
288
+ },
289
+ web
290
+ ? {
291
+ name: 'press_key',
292
+ description: 'Press a key on the focused element, optionally with modifiers held, such as escape, tab, arrowdown, or a with ctrl.',
293
+ parameters: {
294
+ type: 'object',
295
+ properties: {
296
+ key: { type: 'string', description: `One of ${Object.keys(WEB_KEYS).join(', ')}, or a lowercase letter or digit when modifiers are given.` },
297
+ modifiers: { type: 'array', items: { type: 'string', enum: ['ctrl', 'shift', 'alt', 'meta'] } },
298
+ },
299
+ required: ['key'],
300
+ },
301
+ }
302
+ : { name: 'press_key', description: 'Press a key on the focused field.', parameters: { type: 'object', properties: { key: { type: 'string', enum: ['enter', 'backspace'] } }, required: ['key'] } },
303
+ ...(platform !== 'ios' ? [{ name: 'back', description: web ? 'Go back in browser history.' : 'Press the Android back button.', parameters: { type: 'object', properties: {} } }] : []),
304
+ ...(!web ? [{ name: 'go_home', description: 'Go to the home screen.', parameters: { type: 'object', properties: {} } }] : []),
305
+ { name: 'open_url', description: web ? 'Load a URL in the current tab.' : 'Open a URL or deep link on the device.', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
205
306
  { name: 'done', description: 'Finish the task.', parameters: { type: 'object', properties: { success: { type: 'boolean' }, reason: { type: 'string' } }, required: ['success', 'reason'] } },
206
307
  ];
207
- if (platform !== 'ios')
208
- tools.splice(4, 0, { name: 'back', description: platform === 'web' ? 'Go back in browser history.' : 'Press the Android back button.', parameters: { type: 'object', properties: {} } });
209
- return tools;
210
308
  }
211
309
  function bounded(value, min, max, name) {
212
310
  if (!Number.isInteger(value) || value < min || value > max)
@@ -1,6 +1,7 @@
1
1
  import { RevylClient } from './api/client.js';
2
2
  import type { ComputerPlatform } from './api/types.js';
3
3
  import { type HostedTargetConfig, type ResolvedComputerTarget } from './config.js';
4
+ import { type Key, type KeyModifier, type Location, type Point } from './agent/loop.js';
4
5
  import { type ComputerProcess, type ComputerRecording, type ExecOptions, type ExecResult, type FileStat, type OpenedSession, type OperationOptions, type Session, type SessionOptions, type StepResult } from './session.js';
5
6
  export declare class ComputerSession implements Session {
6
7
  private readonly client;
@@ -27,15 +28,32 @@ export declare class ComputerSession implements Session {
27
28
  open(target: HostedTargetConfig): Promise<OpenedSession>;
28
29
  report(): Promise<Record<string, unknown>>;
29
30
  navigate(): Promise<never>;
30
- tap(): Promise<never>;
31
+ /** Clicks a point in screenshot pixels, or an element Revyl finds from a description, with the left button. Returns the point clicked. */
32
+ tap(at: Location): Promise<Point>;
33
+ private tapTarget;
31
34
  type(): Promise<never>;
35
+ /** Types into whatever has keyboard focus. Computers can't clear a field, so pass { clear: false }. */
36
+ typeFocused(text: string, options?: {
37
+ clear?: boolean;
38
+ }): Promise<void>;
39
+ doubleTap(): Promise<never>;
40
+ longPress(): Promise<never>;
32
41
  swipe(): Promise<never>;
33
- pressKey(): Promise<never>;
42
+ drag(): Promise<never>;
43
+ /** Presses a key with modifiers held. 'meta' is the Command key on a Mac computer, and 'alt' is Option. */
44
+ pressKey(key: Key | string, options?: {
45
+ modifiers?: KeyModifier[];
46
+ }): Promise<void>;
34
47
  back(): Promise<never>;
48
+ goHome(): Promise<never>;
35
49
  openUrl(): Promise<never>;
36
50
  launchApp(): Promise<never>;
37
51
  locate(): Promise<never>;
38
- screenSize(): Promise<never>;
52
+ /** The screenshot's size in pixels, the space tap() uses. */
53
+ screenSize(): Promise<{
54
+ width: number;
55
+ height: number;
56
+ }>;
39
57
  get expiresAt(): Date | undefined;
40
58
  recording(): Promise<ComputerRecording | null>;
41
59
  instruction(): Promise<never>;
@@ -53,6 +71,7 @@ export declare class ComputerSession implements Session {
53
71
  [Symbol.asyncDispose](): Promise<void>;
54
72
  private operation;
55
73
  private request;
74
+ private act;
56
75
  private parseReport;
57
76
  private assertOpen;
58
77
  private observationSignal;