@revyl/verify 0.3.1 → 0.4.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 +66 -0
- package/README.md +63 -113
- package/dist/agent/image.d.ts +7 -0
- package/dist/agent/image.js +29 -0
- package/dist/agent/loop.d.ts +97 -0
- package/dist/agent/loop.js +215 -0
- package/dist/agent/model.d.ts +91 -0
- package/dist/agent/model.js +255 -0
- package/dist/agent/providers.d.ts +23 -0
- package/dist/agent/providers.js +58 -0
- package/dist/api/client.d.ts +1 -0
- package/dist/api/client.js +2 -2
- package/dist/auth.js +2 -2
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +34 -9
- package/dist/cli/install.js +14 -9
- package/dist/cli/login.js +7 -7
- package/dist/cli/main.js +89 -29
- package/dist/computer-session.d.ts +9 -0
- package/dist/computer-session.js +27 -0
- package/dist/errors.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/open.d.ts +6 -0
- package/dist/open.js +6 -1
- package/dist/session.d.ts +71 -2
- package/dist/session.js +230 -12
- package/docs/README.md +5 -4
- package/docs/agents.md +5 -5
- package/docs/api.md +89 -13
- package/docs/ci.md +4 -0
- package/docs/computers.md +9 -11
- package/docs/configuration.md +11 -7
- package/docs/errors.md +15 -5
- package/docs/getting-started.md +41 -18
- package/docs/llms.txt +6 -5
- package/docs/mobile.md +46 -0
- package/docs/own-model.md +216 -0
- package/docs/web.md +56 -1
- package/package.json +4 -4
package/dist/api/client.js
CHANGED
|
@@ -91,7 +91,7 @@ export class RevylClient {
|
|
|
91
91
|
timeoutMs: options.timeoutMs,
|
|
92
92
|
signal: options.signal,
|
|
93
93
|
retry: options.retry ?? false,
|
|
94
|
-
headers: { 'X-Revyl-Agent': 'Verify' },
|
|
94
|
+
headers: { 'X-Revyl-Agent': 'Verify', ...options.headers },
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
async sendOnce(method, url, options) {
|
|
@@ -198,7 +198,7 @@ export function edgeBlockedError(response) {
|
|
|
198
198
|
}
|
|
199
199
|
const REFUSAL_REASONS = new Set([
|
|
200
200
|
'web_runtime_unavailable', 'web_runtime_not_configured', 'computer_limit', 'runtime_unavailable', 'computer_runtime_unavailable',
|
|
201
|
-
'computer_at_capacity', 'computer_not_running', 'computer_cannot_host_platform', 'computer_not_found',
|
|
201
|
+
'computer_at_capacity', 'computer_platform_at_capacity', 'computer_not_running', 'computer_cannot_host_platform', 'computer_not_found',
|
|
202
202
|
'web_build_required', 'web_build_not_supported', 'ios_build_required', 'ios_build_not_supported',
|
|
203
203
|
'android_build_required', 'android_build_not_supported', 'web_session_conflict', 'ios_session_conflict',
|
|
204
204
|
'android_session_conflict', 'not_available_on_platform', 'request_timeout', 'protocol_error',
|
package/dist/auth.js
CHANGED
|
@@ -28,8 +28,8 @@ export async function resolveCredentials(options = {}) {
|
|
|
28
28
|
export function missingCredentialsMessage() {
|
|
29
29
|
return [
|
|
30
30
|
'No Revyl credentials found.',
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
"Sign in through your browser with 'npx @revyl/verify login' in a terminal,",
|
|
32
|
+
'or set REVYL_API_KEY to an API key from https://app.revyl.ai (Settings → API keys).',
|
|
33
33
|
].join(' ');
|
|
34
34
|
}
|
|
35
35
|
async function readStoredCredentials(path) {
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface InitResult {
|
|
|
8
8
|
created: string[];
|
|
9
9
|
gitignoreUpdated: boolean;
|
|
10
10
|
}
|
|
11
|
+
export declare function webAppIdMissingMessage(url: string | undefined): string;
|
|
11
12
|
export declare function validateInitAnswers(answers: InitAnswers): InitAnswers;
|
|
12
13
|
export declare function initProject(cwd: string, answers: InitAnswers, options?: {
|
|
13
14
|
force?: boolean;
|
package/dist/cli/init.js
CHANGED
|
@@ -5,7 +5,15 @@ import { VerifyError } from '../errors.js';
|
|
|
5
5
|
import { PACKAGE_NAME } from '../version.js';
|
|
6
6
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
7
|
const CONFIG_FILE = 'verify.config.ts';
|
|
8
|
-
const EXAMPLE_SCRIPT = join('verify', 'example.
|
|
8
|
+
const EXAMPLE_SCRIPT = join('verify', 'example.mts');
|
|
9
|
+
const SETTINGS_BUNDLE_ID = { ios: 'com.apple.Preferences', android: 'com.android.settings' };
|
|
10
|
+
export function webAppIdMissingMessage(url) {
|
|
11
|
+
return [
|
|
12
|
+
'A Web target needs the id of a Web app in Revyl, so init wrote no files.',
|
|
13
|
+
'Create one at https://app.revyl.ai: click Create app, keep Web selected, and enter your site\'s public HTTPS URL. Then copy the app id from the app\'s page.',
|
|
14
|
+
`Then run: npx ${PACKAGE_NAME} init --platform web --app-id <app-id> --url ${url ?? 'https://example.com'}`,
|
|
15
|
+
].join('\n');
|
|
16
|
+
}
|
|
9
17
|
export function validateInitAnswers(answers) {
|
|
10
18
|
if (!['web', 'ios', 'android', 'mac'].includes(answers.platform)) {
|
|
11
19
|
throw new VerifyError('INVALID_ARGUMENT', "--platform must be 'web', 'ios', 'android', or 'mac'. Linux init is unavailable; use open({ platform: 'linux' }) or a Linux config target.");
|
|
@@ -23,13 +31,16 @@ export function validateInitAnswers(answers) {
|
|
|
23
31
|
throw new VerifyError('INVALID_ARGUMENT', '--url must be a public HTTPS URL without credentials, query, or fragment.');
|
|
24
32
|
}
|
|
25
33
|
}
|
|
34
|
+
if (answers.platform === 'web' && answers.appId === undefined) {
|
|
35
|
+
throw new VerifyError('INVALID_ARGUMENT', webAppIdMissingMessage(answers.url));
|
|
36
|
+
}
|
|
26
37
|
return answers;
|
|
27
38
|
}
|
|
28
39
|
export async function initProject(cwd, answers, options = {}) {
|
|
29
40
|
validateInitAnswers(answers);
|
|
30
41
|
const files = [
|
|
31
42
|
[CONFIG_FILE, configTemplate(answers)],
|
|
32
|
-
[EXAMPLE_SCRIPT, scriptTemplate(answers
|
|
43
|
+
[EXAMPLE_SCRIPT, scriptTemplate(answers)],
|
|
33
44
|
];
|
|
34
45
|
const existing = files.map(([path]) => path).filter((path) => existsSync(join(cwd, path)));
|
|
35
46
|
if (existing.length > 0 && !options.force) {
|
|
@@ -50,25 +61,33 @@ async function ignoreOutput(cwd) {
|
|
|
50
61
|
return true;
|
|
51
62
|
}
|
|
52
63
|
function configTemplate({ platform, appId, url }) {
|
|
53
|
-
const app = appId ? `'${appId}'` : `process.env.REVYL_APP_ID ?? 'REPLACE_WITH_YOUR_REVYL_APP_ID'`;
|
|
54
64
|
const target = platform === 'mac'
|
|
55
65
|
? ` mac: { platform: 'mac' },`
|
|
56
66
|
: platform === 'web'
|
|
57
67
|
? ` web: {
|
|
58
68
|
platform: 'web',
|
|
59
69
|
app: {
|
|
60
|
-
// The Revyl
|
|
61
|
-
appId: ${
|
|
70
|
+
// The Revyl Web app to open.
|
|
71
|
+
appId: '${appId}',
|
|
62
72
|
// A public HTTPS URL, such as a preview deployment. Remove it to open the app's newest Revyl build.
|
|
63
73
|
url: '${url ?? 'https://example.com'}',
|
|
64
74
|
},
|
|
65
75
|
viewport: { width: 1280, height: 800 },
|
|
66
76
|
},`
|
|
67
|
-
:
|
|
77
|
+
: appId
|
|
78
|
+
? ` ${platform}: {
|
|
68
79
|
platform: '${platform}',
|
|
69
80
|
app: {
|
|
70
81
|
// The Revyl app to install: each session installs its newest build. Use buildId to pin one build.
|
|
71
|
-
appId: ${
|
|
82
|
+
appId: '${appId}',
|
|
83
|
+
},
|
|
84
|
+
},`
|
|
85
|
+
: ` ${platform}: {
|
|
86
|
+
platform: '${platform}',
|
|
87
|
+
app: {
|
|
88
|
+
// The device's built-in Settings app, so the example runs before you upload anything.
|
|
89
|
+
// To test your own app, replace this line with appId: '<your Revyl app id>'.
|
|
90
|
+
bundleId: '${SETTINGS_BUNDLE_ID[platform]}',
|
|
72
91
|
},
|
|
73
92
|
},`;
|
|
74
93
|
return `import { defineConfig } from '${PACKAGE_NAME}';
|
|
@@ -80,7 +99,7 @@ ${target}
|
|
|
80
99
|
});
|
|
81
100
|
`;
|
|
82
101
|
}
|
|
83
|
-
function scriptTemplate(platform) {
|
|
102
|
+
function scriptTemplate({ platform, appId }) {
|
|
84
103
|
if (platform === 'mac') {
|
|
85
104
|
return `// Mac computers are in early access; ask support@revyl.ai to enable your organization.
|
|
86
105
|
import assert from 'node:assert/strict';
|
|
@@ -101,7 +120,13 @@ try {
|
|
|
101
120
|
const steps = platform === 'web'
|
|
102
121
|
? `await session.instruction('Follow the link on the page');
|
|
103
122
|
await session.validation('A page about domains reserved for documentation is shown');`
|
|
104
|
-
:
|
|
123
|
+
: !appId && platform === 'ios'
|
|
124
|
+
? `await session.instruction('Open General, then About');
|
|
125
|
+
await session.validation('The screen shows the operating system version');`
|
|
126
|
+
: !appId && platform === 'android'
|
|
127
|
+
? `await session.instruction('Open the screen that shows information about this device, such as About or About phone, then scroll or navigate until the operating system version is visible');
|
|
128
|
+
await session.validation('The screen shows the version of the operating system');`
|
|
129
|
+
: `await session.instruction('Wait for the app to finish opening and show its first screen');
|
|
105
130
|
await session.validation('The app has opened and shows its first screen without an error');`;
|
|
106
131
|
return `import { open } from '${PACKAGE_NAME}';
|
|
107
132
|
|
package/dist/cli/install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { VerifyError } from '../errors.js';
|
|
6
6
|
import { PACKAGE_NAME } from '../version.js';
|
|
@@ -32,24 +32,29 @@ export async function installVerify(cwd) {
|
|
|
32
32
|
const manager = detectPackageManager(cwd);
|
|
33
33
|
if (!existsSync(join(cwd, 'package.json')))
|
|
34
34
|
await writeFile(join(cwd, 'package.json'), '{\n "private": true,\n "type": "module"\n}\n', { flag: 'wx' });
|
|
35
|
+
const shell = process.platform === 'win32';
|
|
35
36
|
const args = manager === 'npm'
|
|
36
37
|
? ['install', '--save-dev', '--ignore-scripts']
|
|
37
|
-
: manager === '
|
|
38
|
-
? ['add', '
|
|
39
|
-
:
|
|
38
|
+
: manager === 'pnpm'
|
|
39
|
+
? ['add', '-D', '--ignore-scripts']
|
|
40
|
+
: manager === 'yarn'
|
|
41
|
+
? yarnAddArgs(cwd, shell)
|
|
42
|
+
: ['add', '--dev', '--ignore-scripts'];
|
|
40
43
|
args.push(PACKAGE_NAME);
|
|
41
44
|
process.stdout.write(`Installing ${PACKAGE_NAME} with ${manager}...\n`);
|
|
42
45
|
await new Promise((resolve, reject) => {
|
|
43
|
-
const
|
|
44
|
-
const value = process.env[key];
|
|
45
|
-
return value === undefined ? [] : [[key, value]];
|
|
46
|
-
}));
|
|
47
|
-
const child = spawn(manager, args, { cwd, env, stdio: 'inherit', shell: process.platform === 'win32', timeout: 180_000, killSignal: 'SIGKILL' });
|
|
46
|
+
const child = spawn(manager, args, { cwd, stdio: 'inherit', shell, timeout: 180_000, killSignal: 'SIGKILL' });
|
|
48
47
|
const fail = () => reject(new VerifyError('CONFIG_INVALID', `Could not install ${PACKAGE_NAME} with ${manager}. Install it manually, then retry init --no-install.`));
|
|
49
48
|
child.once('error', fail);
|
|
50
49
|
child.once('exit', (code) => code === 0 ? resolve() : fail());
|
|
51
50
|
});
|
|
52
51
|
}
|
|
52
|
+
function yarnAddArgs(cwd, shell) {
|
|
53
|
+
const major = Number.parseInt(spawnSync('yarn', ['--version'], { cwd, shell, encoding: 'utf8', timeout: 60_000 }).stdout?.trim() ?? '', 10);
|
|
54
|
+
if (major === 1)
|
|
55
|
+
return ['add', '--dev', '--ignore-scripts'];
|
|
56
|
+
return major >= 3 ? ['add', '--dev', '--mode=skip-build'] : ['add', '--dev'];
|
|
57
|
+
}
|
|
53
58
|
function detectPackageManager(cwd) {
|
|
54
59
|
const lockfiles = [['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun'], ['package-lock.json', 'npm'], ['npm-shrinkwrap.json', 'npm']];
|
|
55
60
|
for (let directory = cwd;; directory = dirname(directory)) {
|
package/dist/cli/login.js
CHANGED
|
@@ -53,7 +53,7 @@ export async function signIn(options = {}) {
|
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
55
|
if (credential.state === 'denied')
|
|
56
|
-
throw new VerifyError('AUTH_REJECTED', 'The Revyl login request was denied. Run your script again to sign in, or
|
|
56
|
+
throw new VerifyError('AUTH_REJECTED', 'The Revyl login request was denied. Run your script again to sign in, or run npx @revyl/verify login.');
|
|
57
57
|
if (credential.state !== 'approved')
|
|
58
58
|
throw invalidResponse();
|
|
59
59
|
const token = requiredString(credential, 'api_key_token');
|
|
@@ -80,11 +80,11 @@ export async function signIn(options = {}) {
|
|
|
80
80
|
await writePrivateState('credentials.json', stored);
|
|
81
81
|
return { token, source: 'revyl-cli-login' };
|
|
82
82
|
}
|
|
83
|
-
throw new VerifyError('AUTH_MISSING', 'The Revyl login request expired. Run your script again to sign in, or
|
|
83
|
+
throw new VerifyError('AUTH_MISSING', 'The Revyl login request expired. Run your script again to sign in, or run npx @revyl/verify login.');
|
|
84
84
|
}
|
|
85
85
|
catch (error) {
|
|
86
86
|
if (signal.aborted)
|
|
87
|
-
throw new VerifyError('AUTH_MISSING', 'Revyl sign-in was cancelled or timed out. Run your script again to sign in, or
|
|
87
|
+
throw new VerifyError('AUTH_MISSING', 'Revyl sign-in was cancelled or timed out. Run your script again to sign in, or run npx @revyl/verify login.');
|
|
88
88
|
throw error;
|
|
89
89
|
}
|
|
90
90
|
finally {
|
|
@@ -98,12 +98,12 @@ async function post(backendUrl, path, body, signal) {
|
|
|
98
98
|
response = await fetch(new URL(path, backendUrl), { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(body), redirect: 'error', signal: AbortSignal.any([signal, AbortSignal.timeout(15_000)]) });
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
|
-
throw new VerifyError('AUTH_MISSING', 'Could not reach Revyl to sign in. Run your script again,
|
|
101
|
+
throw new VerifyError('AUTH_MISSING', 'Could not reach Revyl to sign in. Run your script again, run npx @revyl/verify login, or set REVYL_API_KEY.');
|
|
102
102
|
}
|
|
103
103
|
if (response.status === 404)
|
|
104
|
-
throw new VerifyError('AUTH_MISSING', 'The Revyl login request is unavailable or expired. Run your script again, or
|
|
104
|
+
throw new VerifyError('AUTH_MISSING', 'The Revyl login request is unavailable or expired. Run your script again, or run npx @revyl/verify login.');
|
|
105
105
|
if (!response.ok)
|
|
106
|
-
throw new VerifyError('AUTH_REJECTED', `Revyl sign-in failed (HTTP ${response.status}). Run your script again,
|
|
106
|
+
throw new VerifyError('AUTH_REJECTED', `Revyl sign-in failed (HTTP ${response.status}). Run your script again, run npx @revyl/verify login, or set REVYL_API_KEY.`);
|
|
107
107
|
let value;
|
|
108
108
|
try {
|
|
109
109
|
value = await response.json();
|
|
@@ -130,7 +130,7 @@ function optionalString(value, key) {
|
|
|
130
130
|
return text || undefined;
|
|
131
131
|
}
|
|
132
132
|
function invalidResponse() {
|
|
133
|
-
return new VerifyError('AUTH_REJECTED', 'Revyl returned an invalid sign-in response. Run your script again,
|
|
133
|
+
return new VerifyError('AUTH_REJECTED', 'Revyl returned an invalid sign-in response. Run your script again, run npx @revyl/verify login, or set REVYL_API_KEY.');
|
|
134
134
|
}
|
|
135
135
|
function tokenExpiry(token) {
|
|
136
136
|
try {
|
package/dist/cli/main.js
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
2
3
|
import { createInterface } from 'node:readline/promises';
|
|
3
4
|
import { parseArgs } from 'node:util';
|
|
4
5
|
import { RevylClient } from '../api/client.js';
|
|
5
|
-
import { missingCredentialsMessage, resolveCredentials } from '../auth.js';
|
|
6
|
+
import { missingCredentialsMessage, resolveCredentials, revylCredentialsPath } from '../auth.js';
|
|
6
7
|
import { VerifyError, errorMessage, isVerifyError } from '../errors.js';
|
|
7
8
|
import { PACKAGE_NAME, VERSION } from '../version.js';
|
|
8
|
-
import { initProject, validateInitAnswers } from './init.js';
|
|
9
|
+
import { initProject, validateInitAnswers, webAppIdMissingMessage } from './init.js';
|
|
9
10
|
import { installVerify, packageHasVerify } from './install.js';
|
|
10
11
|
import { signIn } from './login.js';
|
|
12
|
+
const INIT_USAGE = `npx ${PACKAGE_NAME} init [--platform ios|android|web|mac] [--app-id <id>] [--url <https-url>] [--force] [--yes] [--no-install]`;
|
|
13
|
+
const DOCS = `https://github.com/RevylAI/verify/tree/v${VERSION}/packages/verify/docs`;
|
|
11
14
|
const HELP = `verify ${VERSION}: open Revyl cloud browsers, devices, and computers from your code
|
|
12
15
|
|
|
13
16
|
Usage
|
|
14
|
-
|
|
15
|
-
Install Verify, write verify.config.ts and an example script,
|
|
17
|
+
${INIT_USAGE}
|
|
18
|
+
Install Verify, write verify.config.ts and an example script, and sign in
|
|
19
|
+
npx ${PACKAGE_NAME} login
|
|
20
|
+
Sign in to Revyl through your browser
|
|
21
|
+
npx ${PACKAGE_NAME} logout
|
|
22
|
+
Remove the saved Revyl login from this computer
|
|
16
23
|
npx ${PACKAGE_NAME} whoami
|
|
17
24
|
Check which Revyl account your credentials belong to
|
|
18
25
|
|
|
19
26
|
Authentication
|
|
20
|
-
|
|
27
|
+
In a terminal, init, login, and your first open() sign you in through your browser and save the
|
|
28
|
+
login to ~/.revyl/credentials.json, which the Revyl CLI shares. In CI, set REVYL_API_KEY instead.
|
|
21
29
|
|
|
22
|
-
Docs:
|
|
30
|
+
Docs: ${DOCS}
|
|
31
|
+
`;
|
|
32
|
+
const INIT_HELP = `Usage: ${INIT_USAGE}
|
|
33
|
+
|
|
34
|
+
Writes verify.config.ts and verify/example.mts, installs Verify when the project lacks it, and signs
|
|
35
|
+
you in through your browser when no credentials are saved.
|
|
36
|
+
|
|
37
|
+
--platform ios, android, web, or mac. Without it, init asks. iOS and Android without --app-id
|
|
38
|
+
open the device's built-in Settings app, so the example runs before you upload anything.
|
|
39
|
+
--app-id The Revyl app to open. Required for web; create a Web app at https://app.revyl.ai first.
|
|
40
|
+
--url The public HTTPS URL a web target opens (default https://example.com).
|
|
41
|
+
--force Overwrite existing files.
|
|
42
|
+
--yes Install Verify without asking.
|
|
43
|
+
--no-install Skip installing Verify.
|
|
44
|
+
|
|
45
|
+
Docs: ${DOCS}/getting-started.md
|
|
23
46
|
`;
|
|
24
47
|
const USAGE_CODES = new Set(['INVALID_ARGUMENT', 'AUTH_MISSING', 'AUTH_REJECTED']);
|
|
25
48
|
export async function main(argv) {
|
|
@@ -37,8 +60,18 @@ export async function main(argv) {
|
|
|
37
60
|
return 0;
|
|
38
61
|
case 'init':
|
|
39
62
|
return initCommand(rest);
|
|
40
|
-
case '
|
|
41
|
-
|
|
63
|
+
case 'login':
|
|
64
|
+
case 'logout':
|
|
65
|
+
case 'whoami': {
|
|
66
|
+
const { values } = parseArgs({ args: rest, options: { help: { type: 'boolean', short: 'h' } } });
|
|
67
|
+
if (values.help) {
|
|
68
|
+
process.stdout.write(HELP);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (command === 'login')
|
|
72
|
+
return signInAndCheck(Boolean(process.stdin.isTTY) && !process.env.CI);
|
|
73
|
+
return command === 'logout' ? logoutCommand() : whoamiCommand();
|
|
74
|
+
}
|
|
42
75
|
default:
|
|
43
76
|
process.stderr.write(`Unknown command: ${command}\n\n${HELP}`);
|
|
44
77
|
return 2;
|
|
@@ -54,8 +87,13 @@ async function initCommand(args) {
|
|
|
54
87
|
force: { type: 'boolean' },
|
|
55
88
|
yes: { type: 'boolean' },
|
|
56
89
|
'no-install': { type: 'boolean' },
|
|
90
|
+
help: { type: 'boolean', short: 'h' },
|
|
57
91
|
},
|
|
58
92
|
});
|
|
93
|
+
if (values.help) {
|
|
94
|
+
process.stdout.write(INIT_HELP);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
59
97
|
const interactive = Boolean(process.stdin.isTTY) && !process.env.CI;
|
|
60
98
|
let answers;
|
|
61
99
|
if (values.platform) {
|
|
@@ -86,44 +124,46 @@ async function initCommand(args) {
|
|
|
86
124
|
answers ??= await askInitQuestions(values['app-id'], values.url);
|
|
87
125
|
const result = await initProject(process.cwd(), answers, { force: values.force ?? false });
|
|
88
126
|
process.stdout.write(`Created ${result.created.join(' and ')}${result.gitignoreUpdated ? ', and added .verify/ to .gitignore' : ''}.\n`);
|
|
127
|
+
await signInAndCheck(interactive);
|
|
128
|
+
const steps = answers.platform === 'mac'
|
|
129
|
+
? ['Mac computers are early access. Ask support@revyl.ai about access.', 'Once your organization has Mac access, run: node verify/example.mts']
|
|
130
|
+
: answers.appId
|
|
131
|
+
? ['Run: node verify/example.mts']
|
|
132
|
+
: ['Run: node verify/example.mts', 'To test your own app, put its Revyl app id in verify.config.ts in place of the Settings bundle id.'];
|
|
133
|
+
process.stdout.write([
|
|
134
|
+
'Next:',
|
|
135
|
+
...steps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
136
|
+
'',
|
|
137
|
+
].join('\n'));
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
async function signInAndCheck(interactive) {
|
|
89
141
|
const credentials = await resolveCredentials();
|
|
90
142
|
if (!credentials) {
|
|
91
|
-
if (interactive)
|
|
92
|
-
await signIn();
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
143
|
+
if (!interactive)
|
|
95
144
|
throw new VerifyError('AUTH_MISSING', `${missingCredentialsMessage()} In non-interactive environments, set the REVYL_API_KEY secret.`);
|
|
96
|
-
|
|
145
|
+
await signIn();
|
|
97
146
|
}
|
|
98
147
|
try {
|
|
99
|
-
await whoamiCommand();
|
|
148
|
+
return await whoamiCommand();
|
|
100
149
|
}
|
|
101
150
|
catch (error) {
|
|
102
151
|
if (!interactive || !isVerifyError(error) || error.code !== 'AUTH_REJECTED')
|
|
103
152
|
throw error;
|
|
104
153
|
await signIn({ force: true });
|
|
105
|
-
|
|
154
|
+
return whoamiCommand();
|
|
106
155
|
}
|
|
107
|
-
const steps = [
|
|
108
|
-
'Configure the generated verify.config.ts if needed',
|
|
109
|
-
...(answers.platform === 'mac' ? ['Mac computers are early access. Ask support@revyl.ai about access.'] : []),
|
|
110
|
-
...(answers.platform === 'mac' || answers.appId ? [] : ['Put your Revyl app id in verify.config.ts (or set REVYL_APP_ID)']),
|
|
111
|
-
'node verify/example.ts',
|
|
112
|
-
];
|
|
113
|
-
process.stdout.write([
|
|
114
|
-
'Next:',
|
|
115
|
-
...steps.map((step, index) => ` ${index + 1}. ${step}`),
|
|
116
|
-
'',
|
|
117
|
-
].join('\n'));
|
|
118
|
-
return 0;
|
|
119
156
|
}
|
|
120
157
|
async function askInitQuestions(appId, url) {
|
|
121
158
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
122
159
|
try {
|
|
123
|
-
const platform = ((await readline.question('What do you want to open? [
|
|
160
|
+
const platform = ((await readline.question('What do you want to open? [ios/android/web/mac] (ios): ')).trim().toLowerCase() || 'ios');
|
|
124
161
|
if (platform === 'mac')
|
|
125
162
|
return validateInitAnswers({ platform, ...(appId ? { appId } : {}), ...(url ? { url } : {}) });
|
|
126
|
-
const
|
|
163
|
+
const question = platform === 'web' ? 'Revyl Web app id (Enter if you have none yet): ' : 'Revyl app id (Enter to use the built-in Settings app): ';
|
|
164
|
+
const id = appId ?? ((await readline.question(question)).trim() || undefined);
|
|
165
|
+
if (platform === 'web' && !id)
|
|
166
|
+
throw new VerifyError('INVALID_ARGUMENT', webAppIdMissingMessage(url));
|
|
127
167
|
const webUrl = platform === 'web' ? (url ?? ((await readline.question('HTTPS URL to open (Enter for https://example.com): ')).trim() || undefined)) : undefined;
|
|
128
168
|
return validateInitAnswers({ platform, ...(id ? { appId: id } : {}), ...(webUrl ? { url: webUrl } : {}) });
|
|
129
169
|
}
|
|
@@ -141,7 +181,27 @@ async function whoamiCommand() {
|
|
|
141
181
|
process.stdout.write(`Signed in to Revyl as ${name}${org ? ` in ${org}` : ''} (credentials from ${credentials.source}).\n`);
|
|
142
182
|
return 0;
|
|
143
183
|
}
|
|
184
|
+
async function logoutCommand() {
|
|
185
|
+
try {
|
|
186
|
+
await rm(revylCredentialsPath());
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (error.code !== 'ENOENT')
|
|
190
|
+
throw new VerifyError('AUTH_MISSING', `Could not remove ~/.revyl/credentials.json: ${errorMessage(error)}`);
|
|
191
|
+
process.stdout.write('No saved Revyl login on this computer.\n');
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
process.stdout.write('Signed out: removed ~/.revyl/credentials.json, which the Revyl CLI shares. The API key it held still works until you delete it in Revyl (Settings → API keys).\n');
|
|
195
|
+
if (process.env.REVYL_API_KEY?.trim())
|
|
196
|
+
process.stdout.write('REVYL_API_KEY is still set in this environment, so Verify keeps using it.\n');
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
144
199
|
export async function runCli(argv) {
|
|
200
|
+
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
|
|
201
|
+
if (major < 22 || (major === 22 && minor < 18)) {
|
|
202
|
+
process.stderr.write(`Verify needs Node.js 22.18 or later; this is Node.js ${process.version}.\n`);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
145
205
|
try {
|
|
146
206
|
return await main(argv);
|
|
147
207
|
}
|
|
@@ -27,6 +27,15 @@ export declare class ComputerSession implements Session {
|
|
|
27
27
|
open(target: HostedTargetConfig): Promise<OpenedSession>;
|
|
28
28
|
report(): Promise<Record<string, unknown>>;
|
|
29
29
|
navigate(): Promise<never>;
|
|
30
|
+
tap(): Promise<never>;
|
|
31
|
+
type(): Promise<never>;
|
|
32
|
+
swipe(): Promise<never>;
|
|
33
|
+
pressKey(): Promise<never>;
|
|
34
|
+
back(): Promise<never>;
|
|
35
|
+
openUrl(): Promise<never>;
|
|
36
|
+
launchApp(): Promise<never>;
|
|
37
|
+
locate(): Promise<never>;
|
|
38
|
+
screenSize(): Promise<never>;
|
|
30
39
|
get expiresAt(): Date | undefined;
|
|
31
40
|
recording(): Promise<ComputerRecording | null>;
|
|
32
41
|
instruction(): Promise<never>;
|
package/dist/computer-session.js
CHANGED
|
@@ -123,6 +123,33 @@ export class ComputerSession {
|
|
|
123
123
|
navigate() {
|
|
124
124
|
return this.unavailable('navigate');
|
|
125
125
|
}
|
|
126
|
+
tap() {
|
|
127
|
+
return this.unavailable('tap');
|
|
128
|
+
}
|
|
129
|
+
type() {
|
|
130
|
+
return this.unavailable('type');
|
|
131
|
+
}
|
|
132
|
+
swipe() {
|
|
133
|
+
return this.unavailable('swipe');
|
|
134
|
+
}
|
|
135
|
+
pressKey() {
|
|
136
|
+
return this.unavailable('pressKey');
|
|
137
|
+
}
|
|
138
|
+
back() {
|
|
139
|
+
return this.unavailable('back');
|
|
140
|
+
}
|
|
141
|
+
openUrl() {
|
|
142
|
+
return this.unavailable('openUrl');
|
|
143
|
+
}
|
|
144
|
+
launchApp() {
|
|
145
|
+
return this.unavailable('launchApp');
|
|
146
|
+
}
|
|
147
|
+
locate() {
|
|
148
|
+
return this.unavailable('locate');
|
|
149
|
+
}
|
|
150
|
+
screenSize() {
|
|
151
|
+
return this.unavailable('screenSize');
|
|
152
|
+
}
|
|
126
153
|
get expiresAt() {
|
|
127
154
|
return this.expiresAtMs === undefined ? undefined : new Date(this.expiresAtMs);
|
|
128
155
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type VerifyErrorCode = 'INVALID_ARGUMENT' | 'CONFIG_NOT_FOUND' | 'CONFIG_INVALID' | 'AUTH_MISSING' | 'COMPUTER_ID_MISSING' | 'AUTH_REJECTED' | 'EDGE_BLOCKED' | 'API_ERROR' | 'SESSION_START_FAILED' | 'SESSION_ENDED' | 'SESSION_BUSY' | 'COMPUTER_LIMIT' | 'NOT_AVAILABLE_ON_PLATFORM' | 'ACTION_FAILED' | 'ACTION_UNCERTAIN' | 'INSTRUCTION_FAILED' | 'VALIDATION_FAILED' | 'STEP_FAILED' | 'COMMAND_FAILED' | 'NOT_FOUND' | 'ACCESS_DENIED' | 'TIMEOUT';
|
|
2
|
-
export type RefusalReason = 'web_runtime_unavailable' | 'web_runtime_not_configured' | 'computer_limit' | 'runtime_unavailable' | 'computer_runtime_unavailable' | 'computer_at_capacity' | 'computer_not_running' | 'computer_cannot_host_platform' | 'computer_not_found' | 'web_build_required' | 'web_build_not_supported' | 'ios_build_required' | 'ios_build_not_supported' | 'android_build_required' | 'android_build_not_supported' | 'web_session_conflict' | 'ios_session_conflict' | 'android_session_conflict' | 'not_available_on_platform' | 'request_timeout' | 'protocol_error';
|
|
1
|
+
export type VerifyErrorCode = 'INVALID_ARGUMENT' | 'CONFIG_NOT_FOUND' | 'CONFIG_INVALID' | 'AUTH_MISSING' | 'COMPUTER_ID_MISSING' | 'AUTH_REJECTED' | 'EDGE_BLOCKED' | 'API_ERROR' | 'SESSION_START_FAILED' | 'SESSION_ENDED' | 'SESSION_BUSY' | 'COMPUTER_LIMIT' | 'NOT_AVAILABLE_ON_PLATFORM' | 'ACTION_FAILED' | 'ACTION_UNCERTAIN' | 'INSTRUCTION_FAILED' | 'VALIDATION_FAILED' | 'STEP_FAILED' | 'COMMAND_FAILED' | 'NOT_FOUND' | 'ACCESS_DENIED' | 'MODEL_ERROR' | 'MODEL_KEY_MISSING' | 'TIMEOUT';
|
|
2
|
+
export type RefusalReason = 'web_runtime_unavailable' | 'web_runtime_not_configured' | 'computer_limit' | 'runtime_unavailable' | 'computer_runtime_unavailable' | 'computer_at_capacity' | 'computer_platform_at_capacity' | 'computer_not_running' | 'computer_cannot_host_platform' | 'computer_not_found' | 'web_build_required' | 'web_build_not_supported' | 'ios_build_required' | 'ios_build_not_supported' | 'android_build_required' | 'android_build_not_supported' | 'web_session_conflict' | 'ios_session_conflict' | 'android_session_conflict' | 'not_available_on_platform' | 'request_timeout' | 'protocol_error';
|
|
3
3
|
export declare class VerifyError extends Error {
|
|
4
4
|
readonly code: VerifyErrorCode;
|
|
5
5
|
readonly reason?: RefusalReason;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,8 @@ export type { HostedTargetConfig, MobileAppConfig, MobileTargetConfig, ComputerT
|
|
|
6
6
|
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
|
+
export { localAgent } from './agent/loop.js';
|
|
10
|
+
export type { Grounding, LocalAgent, LocalAgentOptions, Location, ModelRun, ModelTurn, Point, SwipeDirection, Target } from './agent/loop.js';
|
|
11
|
+
export type { CustomModel, ModelOption } from './agent/providers.js';
|
|
12
|
+
export { anthropic, openaiCompatible } from './agent/model.js';
|
|
13
|
+
export type { AnthropicOptions, JsonSchema, ModelAdapter, ModelReply, ModelRequest, ModelTool, ModelToolCall, ModelUsage, OpenAICompatibleOptions } from './agent/model.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { attach, currentComputer, open } from './open.js';
|
|
2
2
|
export { defineConfig } from './config.js';
|
|
3
3
|
export { VerifyError, isVerifyError } from './errors.js';
|
|
4
|
+
export { localAgent } from './agent/loop.js';
|
|
5
|
+
export { anthropic, openaiCompatible } from './agent/model.js';
|
package/dist/open.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type TargetConfig } from './config.js';
|
|
2
2
|
import { type OpenedSession } from './session.js';
|
|
3
3
|
import { ComputerSession } from './computer-session.js';
|
|
4
|
+
import type { LocalAgent } from './agent/loop.js';
|
|
5
|
+
import { type ModelOption } from './agent/providers.js';
|
|
4
6
|
export interface OpenOptions {
|
|
5
7
|
/** The config file to read target names from. Defaults to the nearest verify.config.ts at or above the current folder. */
|
|
6
8
|
config?: string;
|
|
@@ -8,6 +10,10 @@ export interface OpenOptions {
|
|
|
8
10
|
log?: false | ((line: string) => void);
|
|
9
11
|
backendUrl?: string;
|
|
10
12
|
appUrl?: string;
|
|
13
|
+
/** Runs every instruction and validation on your model, such as 'anthropic/claude-sonnet-5-5', instead of Revyl's agent. Browsers and devices only. */
|
|
14
|
+
model?: ModelOption;
|
|
15
|
+
/** Runs every instruction and validation on a custom agent from localAgent(). Pass model or agent, not both. */
|
|
16
|
+
agent?: LocalAgent;
|
|
11
17
|
}
|
|
12
18
|
export interface AttachOptions extends Pick<OpenOptions, 'log' | 'backendUrl' | 'appUrl'> {
|
|
13
19
|
stopOnClose?: boolean;
|
package/dist/open.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DEFAULT_STEP_TIMEOUT_MS, loadConfig, validateTarget } from './config.js
|
|
|
5
5
|
import { VerifyError } from './errors.js';
|
|
6
6
|
import { DeviceSession } from './session.js';
|
|
7
7
|
import { ComputerSession } from './computer-session.js';
|
|
8
|
+
import { chooseAgent } from './agent/providers.js';
|
|
8
9
|
const signIns = new Map();
|
|
9
10
|
async function credentialsForUse(options) {
|
|
10
11
|
const credentials = await resolveCredentials();
|
|
@@ -53,10 +54,14 @@ export async function open(target, options = {}) {
|
|
|
53
54
|
else {
|
|
54
55
|
resolved = validateTarget(target);
|
|
55
56
|
}
|
|
57
|
+
const agent = chooseAgent(options, 'open()');
|
|
58
|
+
if (agent && (resolved.platform === 'mac' || resolved.platform === 'linux')) {
|
|
59
|
+
throw new VerifyError('NOT_AVAILABLE_ON_PLATFORM', 'Your own model runs steps on browsers and devices, not on computers.');
|
|
60
|
+
}
|
|
56
61
|
const credentials = await credentialsForUse(options);
|
|
57
62
|
const log = options.log === false ? undefined : (options.log ?? ((line) => void process.stderr.write(`[verify] ${line}\n`)));
|
|
58
63
|
const client = new RevylClient({ token: credentials.token, backendUrl: options.backendUrl });
|
|
59
64
|
if (resolved.platform === 'mac' || resolved.platform === 'linux')
|
|
60
65
|
return ComputerSession.start(client, resolved, { stepTimeout, log, appUrl: options.appUrl });
|
|
61
|
-
return DeviceSession.start(client, resolved, { stepTimeout, log, appUrl: options.appUrl });
|
|
66
|
+
return DeviceSession.start(client, resolved, { stepTimeout, log, appUrl: options.appUrl, ...(agent ? { agent } : {}) });
|
|
62
67
|
}
|