@phone-use/sdk 0.2.0 → 0.3.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/dist/index.d.mts +73 -2
- package/dist/index.mjs +124 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/backends/cloud-sandbox.ts +202 -0
- package/src/index.ts +9 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phone-use/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Typed mobile-device SDK
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Typed mobile-device SDK \u2014 Device, backends, errors, actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { BaseDeviceBackend } from '../backend.ts';
|
|
3
|
+
import {
|
|
4
|
+
ALL_CAPABILITIES,
|
|
5
|
+
type AlertAction,
|
|
6
|
+
type BackendAlertResult,
|
|
7
|
+
type Capability,
|
|
8
|
+
type OpenAppResult,
|
|
9
|
+
type PressTarget,
|
|
10
|
+
type ScrollDirection,
|
|
11
|
+
type Snapshot,
|
|
12
|
+
} from '../device.ts';
|
|
13
|
+
import { PhoneUseError } from '../errors.ts';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// CloudSandboxBackend: drives a phone-use cloud sandbox (a simulator on a
|
|
17
|
+
// remote worker) over the worker's sandbox-scoped RPC endpoint. This is the
|
|
18
|
+
// client half of packages/cloud's worker protocol; it lives in the SDK so the
|
|
19
|
+
// CLI/MCP/agent can attach to a sandbox without depending on @phone-use/cloud
|
|
20
|
+
// (which also contains the server side and is not published).
|
|
21
|
+
//
|
|
22
|
+
// Wire protocol (shared with packages/cloud — keep in sync):
|
|
23
|
+
// POST <endpoint>/rpc {method, args} -> {ok:true,result} | {ok:false,error}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type SandboxRpcMethod =
|
|
27
|
+
| 'snapshot'
|
|
28
|
+
| 'screenshot'
|
|
29
|
+
| 'press'
|
|
30
|
+
| 'longPress'
|
|
31
|
+
| 'fill'
|
|
32
|
+
| 'typeText'
|
|
33
|
+
| 'pressKey'
|
|
34
|
+
| 'scroll'
|
|
35
|
+
| 'pan'
|
|
36
|
+
| 'installApp'
|
|
37
|
+
| 'waitForText'
|
|
38
|
+
| 'systemAlert'
|
|
39
|
+
| 'home'
|
|
40
|
+
| 'back'
|
|
41
|
+
| 'openApp'
|
|
42
|
+
| 'listApps'
|
|
43
|
+
| 'closeSession';
|
|
44
|
+
|
|
45
|
+
export type SandboxRpcRequest = { method: SandboxRpcMethod; args: unknown[] };
|
|
46
|
+
export type SandboxRpcSuccess = { ok: true; result: unknown };
|
|
47
|
+
export type SandboxRpcFailure = {
|
|
48
|
+
ok: false;
|
|
49
|
+
error: { message: string; code?: string | undefined; retryable?: boolean | undefined };
|
|
50
|
+
};
|
|
51
|
+
export type SandboxRpcResponse = SandboxRpcSuccess | SandboxRpcFailure;
|
|
52
|
+
|
|
53
|
+
export type CloudSandboxBackendOptions = {
|
|
54
|
+
endpoint: string;
|
|
55
|
+
token: string;
|
|
56
|
+
capabilities?: Capability[] | undefined;
|
|
57
|
+
fetch?: typeof globalThis.fetch | undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class CloudSandboxBackend extends BaseDeviceBackend {
|
|
61
|
+
private readonly endpoint: string;
|
|
62
|
+
private readonly token: string;
|
|
63
|
+
private readonly fetchImpl: typeof globalThis.fetch;
|
|
64
|
+
|
|
65
|
+
constructor(opts: CloudSandboxBackendOptions) {
|
|
66
|
+
super('phone-use-cloud', opts.capabilities ?? ALL_CAPABILITIES);
|
|
67
|
+
this.endpoint = opts.endpoint.replace(/\/+$/, '');
|
|
68
|
+
this.token = opts.token;
|
|
69
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async rpc<T>(method: SandboxRpcMethod, ...args: unknown[]): Promise<T> {
|
|
73
|
+
const response = await this.fetchImpl(`${this.endpoint}/rpc`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
authorization: `Bearer ${this.token}`,
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({ method, args } satisfies SandboxRpcRequest),
|
|
80
|
+
});
|
|
81
|
+
const body = (await response.json().catch(() => ({}))) as
|
|
82
|
+
| SandboxRpcResponse
|
|
83
|
+
| { error?: string; message?: string };
|
|
84
|
+
if (!response.ok || !('ok' in body)) {
|
|
85
|
+
const message =
|
|
86
|
+
('error' in body && typeof body.error === 'string' ? body.error : undefined) ??
|
|
87
|
+
('message' in body ? body.message : undefined) ??
|
|
88
|
+
`HTTP ${response.status}`;
|
|
89
|
+
throw new PhoneUseError(message, { code: 'UNKNOWN', retryable: false });
|
|
90
|
+
}
|
|
91
|
+
if (!body.ok) {
|
|
92
|
+
throw new PhoneUseError(body.error.message, {
|
|
93
|
+
code: normalizeErrorCode(body.error.code),
|
|
94
|
+
retryable: body.error.retryable ?? false,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return body.result as T;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override snapshot(opts?: { interactiveOnly?: boolean; depth?: number }): Promise<Snapshot> {
|
|
101
|
+
return this.rpc('snapshot', opts);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override async screenshot(opts: { path: string; overlayRefs?: boolean }): Promise<{ path: string }> {
|
|
105
|
+
const result = await this.rpc<{ base64: string }>('screenshot', {
|
|
106
|
+
overlayRefs: opts.overlayRefs,
|
|
107
|
+
});
|
|
108
|
+
await writeFile(opts.path, Buffer.from(result.base64, 'base64'));
|
|
109
|
+
return { path: opts.path };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override press(target: PressTarget): Promise<void> {
|
|
113
|
+
return this.rpc('press', target);
|
|
114
|
+
}
|
|
115
|
+
override longPress(ref: string, durationMs?: number): Promise<void> {
|
|
116
|
+
return this.rpc('longPress', ref, durationMs);
|
|
117
|
+
}
|
|
118
|
+
override fill(ref: string, text: string): Promise<void> {
|
|
119
|
+
return this.rpc('fill', ref, text);
|
|
120
|
+
}
|
|
121
|
+
override typeText(text: string): Promise<void> {
|
|
122
|
+
return this.rpc('typeText', text);
|
|
123
|
+
}
|
|
124
|
+
override pressKey(key: 'return'): Promise<void> {
|
|
125
|
+
return this.rpc('pressKey', key);
|
|
126
|
+
}
|
|
127
|
+
override scroll(direction: ScrollDirection): Promise<void> {
|
|
128
|
+
return this.rpc('scroll', direction);
|
|
129
|
+
}
|
|
130
|
+
override pan(x: number, y: number, dx: number, dy: number, durationMs?: number): Promise<void> {
|
|
131
|
+
return this.rpc('pan', x, y, dx, dy, durationMs);
|
|
132
|
+
}
|
|
133
|
+
override waitForText(text: string, timeoutMs?: number): Promise<void> {
|
|
134
|
+
return this.rpc('waitForText', text, timeoutMs);
|
|
135
|
+
}
|
|
136
|
+
override systemAlert(action: AlertAction): Promise<BackendAlertResult> {
|
|
137
|
+
return this.rpc('systemAlert', action);
|
|
138
|
+
}
|
|
139
|
+
override home(): Promise<void> {
|
|
140
|
+
return this.rpc('home');
|
|
141
|
+
}
|
|
142
|
+
override back(): Promise<void> {
|
|
143
|
+
return this.rpc('back');
|
|
144
|
+
}
|
|
145
|
+
override openApp(opts: { app?: string; url?: string; relaunch?: boolean }): Promise<OpenAppResult> {
|
|
146
|
+
return this.rpc('openApp', opts);
|
|
147
|
+
}
|
|
148
|
+
override listApps(): Promise<string[]> {
|
|
149
|
+
return this.rpc('listApps');
|
|
150
|
+
}
|
|
151
|
+
override closeSession(): Promise<void> {
|
|
152
|
+
return this.rpc('closeSession');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Upload a zipped .app bundle (base64) and install it on the sandbox device. */
|
|
156
|
+
installApp(base64Zip: string): Promise<{ installed?: string }> {
|
|
157
|
+
return this.rpc('installApp', base64Zip);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a CloudSandboxBackend from explicit options or the environment:
|
|
163
|
+
* PHONE_USE_SANDBOX_URL + PHONE_USE_SANDBOX_TOKEN (printed by
|
|
164
|
+
* `phone-use sandbox env <id>`).
|
|
165
|
+
*/
|
|
166
|
+
export function createCloudSandboxBackend(config?: Partial<CloudSandboxBackendOptions>): CloudSandboxBackend {
|
|
167
|
+
const endpoint = config?.endpoint ?? process.env.PHONE_USE_SANDBOX_URL;
|
|
168
|
+
const token = config?.token ?? process.env.PHONE_USE_SANDBOX_TOKEN;
|
|
169
|
+
if (!endpoint || !token) {
|
|
170
|
+
throw new PhoneUseError(
|
|
171
|
+
'cloud sandbox backend needs PHONE_USE_SANDBOX_URL and PHONE_USE_SANDBOX_TOKEN (see `phone-use sandbox env <id>`)',
|
|
172
|
+
{ code: 'BACKEND_NOT_FOUND', retryable: false },
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return new CloudSandboxBackend({ ...config, endpoint, token });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeErrorCode(
|
|
179
|
+
code: string | undefined,
|
|
180
|
+
):
|
|
181
|
+
| 'DEVICE_NOT_FOUND'
|
|
182
|
+
| 'DEVICE_IN_USE'
|
|
183
|
+
| 'SESSION_NOT_FOUND'
|
|
184
|
+
| 'TIMEOUT'
|
|
185
|
+
| 'ACTION_FAILED'
|
|
186
|
+
| 'UNSUPPORTED_CAPABILITY'
|
|
187
|
+
| 'BACKEND_NOT_FOUND'
|
|
188
|
+
| 'ABORTED'
|
|
189
|
+
| 'UNKNOWN' {
|
|
190
|
+
const codes = new Set([
|
|
191
|
+
'DEVICE_NOT_FOUND',
|
|
192
|
+
'DEVICE_IN_USE',
|
|
193
|
+
'SESSION_NOT_FOUND',
|
|
194
|
+
'TIMEOUT',
|
|
195
|
+
'ACTION_FAILED',
|
|
196
|
+
'UNSUPPORTED_CAPABILITY',
|
|
197
|
+
'BACKEND_NOT_FOUND',
|
|
198
|
+
'ABORTED',
|
|
199
|
+
'UNKNOWN',
|
|
200
|
+
]);
|
|
201
|
+
return codes.has(code ?? '') ? (code as ReturnType<typeof normalizeErrorCode>) : 'UNKNOWN';
|
|
202
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* deliberately not re-exported here.
|
|
8
8
|
*/
|
|
9
9
|
/** The published package version (kept in sync with package.json by the release flow). */
|
|
10
|
-
export const VERSION = '0.
|
|
10
|
+
export const VERSION = '0.3.0';
|
|
11
11
|
|
|
12
12
|
export {
|
|
13
13
|
type Action,
|
|
@@ -31,6 +31,14 @@ export {
|
|
|
31
31
|
registerBackend,
|
|
32
32
|
} from './backend.ts';
|
|
33
33
|
export { createAgentDeviceBackend } from './backends/agent-device.ts';
|
|
34
|
+
export {
|
|
35
|
+
CloudSandboxBackend,
|
|
36
|
+
type CloudSandboxBackendOptions,
|
|
37
|
+
createCloudSandboxBackend,
|
|
38
|
+
type SandboxRpcMethod,
|
|
39
|
+
type SandboxRpcRequest,
|
|
40
|
+
type SandboxRpcResponse,
|
|
41
|
+
} from './backends/cloud-sandbox.ts';
|
|
34
42
|
export { createDeviceRunnerBackend, DeviceRunnerBackend } from './backends/device-runner.ts';
|
|
35
43
|
export { type IosConnectOptions, type IosLaunchOptions, ios } from './backends/ios.ts';
|
|
36
44
|
export type { AndroidDeviceConfig, CommonDeviceConfig, DeviceConfig, IosDeviceConfig } from './config.ts';
|