@revyl/verify 0.4.0 → 0.5.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,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 (unreleased)
4
+
5
+ - 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.
6
+ - 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.
7
+ - iOS and Android: `goHome()` goes to the home screen.
8
+ - 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.
9
+ - 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.
10
+ - 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.
11
+ - Docs: the platform table, `api.md`, the web and mobile guides with a script for the new calls, `agents.md` and `llms.txt`.
12
+
13
+ ## Go 0.4.0 (unreleased)
14
+
15
+ Published as the `go/v0.4.0` tag.
16
+
17
+ - 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.
18
+ - 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.
19
+ - iOS and Android: `GoHome(ctx)`.
20
+ - 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.
21
+ - `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.
22
+
23
+ ## Python 0.3.0 (unreleased)
24
+
25
+ - 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.
26
+ - 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.
27
+ - iOS and Android: `go_home()`. `AsyncSession` has every new method.
28
+ - 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.
29
+ - `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.
30
+
31
+ ## 0.4.1 (2026-11-12)
32
+
33
+ - **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.
34
+ - 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.
35
+ - `locate()` keeps an off-screen match outside `screenSize()` instead of moving it to the edge, so you can tell it is off screen, and an action at it is refused.
36
+ - `instruction()` and `validation()` on your own model are unchanged: the model still sees screenshots, and Verify still maps its answers to the device.
37
+ - 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.
38
+
39
+ ## Go 0.3.1 (2026-11-12)
40
+
41
+ Published as the `go/v0.3.1` tag.
42
+
43
+ - **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`, `ScreenSize` 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.
44
+ - 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.
45
+ - README: the same coordinate rule as TypeScript and Python.
46
+
47
+ ## Python 0.2.1 (2026-11-12)
48
+
49
+ - **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.
50
+ - 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.
51
+ - README: the same coordinate rule as TypeScript and Go.
52
+
3
53
  ## 0.4.0 (2026-11-11)
4
54
 
5
55
  - Early access: run `instruction()` and `validation()` on your own model. `open('ios', { model: 'anthropic/claude-sonnet-5-5' })` runs every step on Claude with the key from `ANTHROPIC_API_KEY`; a step's `{ model }` overrides it. Built-in providers: Anthropic, OpenAI, Google, OpenRouter, Groq, Cerebras, xAI and Ollama, each reading its usual key variable; `anthropic/` and `openai/` also honour `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` for proxies and gateways. `{ id, baseURL, apiKey, headers }` reaches any OpenAI-compatible endpoint, with the key taken only from `apiKey`. Your model names elements and Revyl's grounding model finds them. `StepResult.model` reports turns and tokens. Revyl's agent remains the default.
package/README.md CHANGED
@@ -1,6 +1,6 @@
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.0/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.5.0/docs/images/verify-hero.webp)
4
4
 
5
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.
6
6
 
@@ -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.0/packages/verify/docs/getting-started.md) covers sign-in, your own app, and [CI](https://github.com/RevylAI/verify/blob/v0.4.0/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.5.0/packages/verify/docs/getting-started.md) covers sign-in, your own app, and [CI](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/ci.md).
45
45
 
46
- For finished projects, see [`examples/`](https://github.com/RevylAI/verify/tree/v0.4.0/examples/): a [web store](https://github.com/RevylAI/verify/tree/v0.4.0/examples/web-store/), [mobile Settings](https://github.com/RevylAI/verify/tree/v0.4.0/examples/mobile-settings/), a [Mac workspace](https://github.com/RevylAI/verify/tree/v0.4.0/examples/mac-workspace/), a small [agent platform](https://github.com/RevylAI/verify/tree/v0.4.0/examples/agent-platform/) that runs web and iOS side by side, and [your own model](https://github.com/RevylAI/verify/tree/v0.4.0/examples/byo-model/) (with [Python](https://github.com/RevylAI/verify/blob/v0.4.0/python/examples/byo_model.py) and [Go](https://github.com/RevylAI/verify/blob/v0.4.0/go/examples/byomodel/main.go) versions).
46
+ For finished projects, see [`examples/`](https://github.com/RevylAI/verify/tree/v0.5.0/examples/): a [web store](https://github.com/RevylAI/verify/tree/v0.5.0/examples/web-store/), [mobile Settings](https://github.com/RevylAI/verify/tree/v0.5.0/examples/mobile-settings/), a [Mac workspace](https://github.com/RevylAI/verify/tree/v0.5.0/examples/mac-workspace/), a small [agent platform](https://github.com/RevylAI/verify/tree/v0.5.0/examples/agent-platform/) that runs web and iOS side by side, and [your own model](https://github.com/RevylAI/verify/tree/v0.5.0/examples/byo-model/) (with [Python](https://github.com/RevylAI/verify/blob/v0.5.0/python/examples/byo_model.py) and [Go](https://github.com/RevylAI/verify/blob/v0.5.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
- await session.tap({ x: 196, y: 640 }); // or device coordinates
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.0/python/README.md#exact-actions) and [Go](https://github.com/RevylAI/verify/blob/v0.4.0/go/README.md#exact-actions) READMEs show them with each language's names. The [web](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/web.md#exact-actions) and [mobile](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/mobile.md#exact-actions) guides have runnable scripts, and [api.md](https://github.com/RevylAI/verify/blob/v0.4.0/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. The [Python](https://github.com/RevylAI/verify/blob/v0.5.0/python/README.md#exact-actions) and [Go](https://github.com/RevylAI/verify/blob/v0.5.0/go/README.md#exact-actions) READMEs show them with each language's names. The [web](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/web.md#exact-actions) and [mobile](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/mobile.md#exact-actions) guides have runnable scripts, and [api.md](https://github.com/RevylAI/verify/blob/v0.5.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.0/python/README.md#your-own-model) and [Go](https://github.com/RevylAI/verify/blob/v0.4.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.4.0/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.5.0/python/README.md#your-own-model) and [Go](https://github.com/RevylAI/verify/blob/v0.5.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.5.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.0/go) | `go get github.com/RevylAI/verify/go@v0.3.0` | Published as the `go/v0.3.0` 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.0/python/README.md). |
85
+ | [Go module](https://github.com/RevylAI/verify/tree/go/v0.4.0/go) | `go get github.com/RevylAI/verify/go@v0.4.0` | Published as the `go/v0.4.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.5.0/python/README.md). |
85
87
 
86
88
  ## Platforms
87
89
 
@@ -96,8 +98,11 @@ In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.
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`, `doubleTap`, `longPress`, `type`, `swipe`, `drag`, `locate`, `screenSize` | Yes | Yes | Yes | No, use `exec()` and files |
102
+ | `pressKey(key, { modifiers })` | Yes: `'enter'`, `'backspace'`, `'tab'`, `'escape'`, arrows and more, with modifiers | `'enter'` and `'backspace'` only | `'enter'` and `'backspace'` only | No |
103
+ | `typeFocused(text)` | Yes, types into the focused field | No, use `type(location, text)` | No, use `type(location, text)` | No |
100
104
  | `back()` | Yes, browser history | No, tap the on-screen back control | Yes, the system back button | No |
105
+ | `goHome()` | No, use `openUrl` or `navigate` | Yes, the home screen | Yes, the home screen | No |
101
106
  | `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
107
  | `launchApp(bundleId)` | No, use `openUrl` or `navigate` | Yes | Yes | No |
103
108
  | `navigate(url)` | Yes | No, use `openUrl` | No, use `openUrl` | No |
@@ -105,18 +110,20 @@ In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.
105
110
  | `screenshot()` | Yes | Yes | Yes | Yes |
106
111
  | `exec`, files, processes | No, open a Mac computer | No, open a Mac computer | No, open a Mac computer | Yes |
107
112
 
108
- Coordinates are in iOS points, Android pixels and Web CSS pixels, from the top-left corner. A call a platform doesn't support throws `NOT_AVAILABLE_ON_PLATFORM`, and its message names the alternative.
113
+ **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.
109
114
 
110
- 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.0/packages/verify/docs/computers.md).
115
+ A call a platform doesn't support throws `NOT_AVAILABLE_ON_PLATFORM`, and its message names the alternative.
116
+
117
+ 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.5.0/packages/verify/docs/computers.md).
111
118
 
112
119
  ## Documentation
113
120
 
114
- The [documentation](https://github.com/RevylAI/verify/tree/v0.4.0/packages/verify/docs/) covers the [API](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/api.md), [configuration](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/configuration.md), [web](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/web.md), [mobile](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/mobile.md), [computers](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/computers.md), [CI](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/ci.md), [your own model](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/own-model.md), [errors and troubleshooting](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/errors.md), [coding agents](https://github.com/RevylAI/verify/blob/v0.4.0/packages/verify/docs/agents.md), and [live end-to-end results](https://github.com/RevylAI/verify/blob/v0.4.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.4.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).
121
+ The [documentation](https://github.com/RevylAI/verify/tree/v0.5.0/packages/verify/docs/) covers the [API](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/api.md), [configuration](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/configuration.md), [web](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/web.md), [mobile](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/mobile.md), [computers](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/computers.md), [CI](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/ci.md), [your own model](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/own-model.md), [errors and troubleshooting](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/errors.md), [coding agents](https://github.com/RevylAI/verify/blob/v0.5.0/packages/verify/docs/agents.md), and [live end-to-end results](https://github.com/RevylAI/verify/blob/v0.5.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.5.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).
115
122
 
116
123
  ## Status
117
124
 
118
- Verify is at 0.x, so APIs can still change between minor releases; the [changelog](https://github.com/RevylAI/verify/blob/v0.4.0/CHANGELOG.md) records each release. Mac computers are early access. See [Contributing](https://github.com/RevylAI/verify/blob/v0.4.0/CONTRIBUTING.md) and [Releasing](https://github.com/RevylAI/verify/blob/v0.4.0/RELEASING.md) to work on Verify.
125
+ Verify is at 0.x, so APIs can still change between minor releases; the [changelog](https://github.com/RevylAI/verify/blob/v0.5.0/CHANGELOG.md) records each release. Mac computers are early access. See [Contributing](https://github.com/RevylAI/verify/blob/v0.5.0/CONTRIBUTING.md) and [Releasing](https://github.com/RevylAI/verify/blob/v0.5.0/RELEASING.md) to work on Verify.
119
126
 
120
127
  ## License
121
128
 
122
- [Apache 2.0](https://github.com/RevylAI/verify/blob/v0.4.0/LICENSE).
129
+ [Apache 2.0](https://github.com/RevylAI/verify/blob/v0.5.0/LICENSE).
@@ -1,5 +1,5 @@
1
1
  import type { ModelAdapter } from './model.js';
2
- /** A point in device units: iOS points, Android and Web pixels, from the top-left corner. */
2
+ /** A point in screenshot pixels, from the top-left corner. */
3
3
  export interface Point {
4
4
  x: number;
5
5
  y: number;
@@ -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)
@@ -29,9 +29,14 @@ export declare class ComputerSession implements Session {
29
29
  navigate(): Promise<never>;
30
30
  tap(): Promise<never>;
31
31
  type(): Promise<never>;
32
+ typeFocused(): Promise<never>;
33
+ doubleTap(): Promise<never>;
34
+ longPress(): Promise<never>;
32
35
  swipe(): Promise<never>;
36
+ drag(): Promise<never>;
33
37
  pressKey(): Promise<never>;
34
38
  back(): Promise<never>;
39
+ goHome(): Promise<never>;
35
40
  openUrl(): Promise<never>;
36
41
  launchApp(): Promise<never>;
37
42
  locate(): Promise<never>;
@@ -129,15 +129,30 @@ export class ComputerSession {
129
129
  type() {
130
130
  return this.unavailable('type');
131
131
  }
132
+ typeFocused() {
133
+ return this.unavailable('typeFocused');
134
+ }
135
+ doubleTap() {
136
+ return this.unavailable('doubleTap');
137
+ }
138
+ longPress() {
139
+ return this.unavailable('longPress');
140
+ }
132
141
  swipe() {
133
142
  return this.unavailable('swipe');
134
143
  }
144
+ drag() {
145
+ return this.unavailable('drag');
146
+ }
135
147
  pressKey() {
136
148
  return this.unavailable('pressKey');
137
149
  }
138
150
  back() {
139
151
  return this.unavailable('back');
140
152
  }
153
+ goHome() {
154
+ return this.unavailable('goHome');
155
+ }
141
156
  openUrl() {
142
157
  return this.unavailable('openUrl');
143
158
  }
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export type { Platform, ComputerPlatform, MobilePlatform } from './api/types.js'
7
7
  export { VerifyError, isVerifyError } from './errors.js';
8
8
  export type { RefusalReason, VerifyErrorCode } from './errors.js';
9
9
  export { localAgent } from './agent/loop.js';
10
- export type { Grounding, LocalAgent, LocalAgentOptions, Location, ModelRun, ModelTurn, Point, SwipeDirection, Target } from './agent/loop.js';
10
+ export type { Grounding, Key, KeyModifier, LocalAgent, LocalAgentOptions, Location, ModelRun, ModelTurn, Point, SwipeDirection, Target } from './agent/loop.js';
11
11
  export type { CustomModel, ModelOption } from './agent/providers.js';
12
12
  export { anthropic, openaiCompatible } from './agent/model.js';
13
13
  export type { AnthropicOptions, JsonSchema, ModelAdapter, ModelReply, ModelRequest, ModelTool, ModelToolCall, ModelUsage, OpenAICompatibleOptions } from './agent/model.js';