@phone-use/sdk 0.2.0 → 0.3.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phone-use/sdk",
3
- "version": "0.2.0",
4
- "description": "Typed mobile-device SDK Device, backends, errors, actions",
3
+ "version": "0.3.1",
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,226 @@
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 apps/api'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 apps/api — keep in sync):
23
+ // POST <endpoint>/rpc {method, args} -> {ok:true,result} | {ok:false,error}
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Every device verb a cloud sandbox accepts over the RPC wire. The worker's
27
+ * runtime set is contract-tested against this union. */
28
+ export type SandboxRpcMethod =
29
+ | 'snapshot'
30
+ | 'screenshot'
31
+ | 'press'
32
+ | 'longPress'
33
+ | 'fill'
34
+ | 'typeText'
35
+ | 'pressKey'
36
+ | 'scroll'
37
+ | 'pan'
38
+ | 'installApp'
39
+ | 'waitForText'
40
+ | 'systemAlert'
41
+ | 'home'
42
+ | 'back'
43
+ | 'openApp'
44
+ | 'listApps'
45
+ | 'closeSession';
46
+
47
+ /** Body of `POST <endpoint>/rpc`: one verb and its positional arguments. */
48
+ export type SandboxRpcRequest = { method: SandboxRpcMethod; args: unknown[] };
49
+ /** RPC completed; `result` is the verb-specific payload. */
50
+ export type SandboxRpcSuccess = { ok: true; result: unknown };
51
+ /** RPC failed on-device. `code` is a stable taxonomy bucket (`device_gone`,
52
+ * `timeout`, `session_conflict`, `target_not_found`, `internal`) and
53
+ * `retryable` tells agents whether retrying may succeed. */
54
+ export type SandboxRpcFailure = {
55
+ ok: false;
56
+ error: { message: string; code?: string | undefined; retryable?: boolean | undefined };
57
+ };
58
+ /** Discriminated RPC outcome — branch on `ok`. */
59
+ export type SandboxRpcResponse = SandboxRpcSuccess | SandboxRpcFailure;
60
+
61
+ /** Connection settings for one provisioned cloud phone: the per-sandbox
62
+ * `endpoint`/`token` pair returned by `phone-use create` (or `/v1/sandboxes`). */
63
+ export type CloudSandboxBackendOptions = {
64
+ endpoint: string;
65
+ token: string;
66
+ capabilities?: Capability[] | undefined;
67
+ fetch?: typeof globalThis.fetch | undefined;
68
+ };
69
+
70
+ export class CloudSandboxBackend extends BaseDeviceBackend {
71
+ private readonly endpoint: string;
72
+ private readonly token: string;
73
+ private readonly fetchImpl: typeof globalThis.fetch;
74
+
75
+ constructor(opts: CloudSandboxBackendOptions) {
76
+ super('phone-use-cloud', opts.capabilities ?? ALL_CAPABILITIES);
77
+ this.endpoint = opts.endpoint.replace(/\/+$/, '');
78
+ this.token = opts.token;
79
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
80
+ }
81
+
82
+ private async rpc<T>(method: SandboxRpcMethod, ...args: unknown[]): Promise<T> {
83
+ const response = await this.fetchImpl(`${this.endpoint}/rpc`, {
84
+ method: 'POST',
85
+ headers: {
86
+ authorization: `Bearer ${this.token}`,
87
+ 'content-type': 'application/json',
88
+ },
89
+ body: JSON.stringify({ method, args } satisfies SandboxRpcRequest),
90
+ });
91
+ const body = (await response.json().catch(() => ({}))) as
92
+ | SandboxRpcResponse
93
+ | { error?: string; message?: string };
94
+ if (!response.ok || !('ok' in body)) {
95
+ const message =
96
+ ('error' in body && typeof body.error === 'string' ? body.error : undefined) ??
97
+ ('message' in body ? body.message : undefined) ??
98
+ `HTTP ${response.status}`;
99
+ throw new PhoneUseError(message, { code: 'UNKNOWN', retryable: false });
100
+ }
101
+ if (!body.ok) {
102
+ throw new PhoneUseError(body.error.message, {
103
+ code: normalizeErrorCode(body.error.code),
104
+ retryable: body.error.retryable ?? false,
105
+ });
106
+ }
107
+ return body.result as T;
108
+ }
109
+
110
+ override snapshot(opts?: { interactiveOnly?: boolean; depth?: number }): Promise<Snapshot> {
111
+ return this.rpc('snapshot', opts);
112
+ }
113
+
114
+ override async screenshot(opts: { path: string; overlayRefs?: boolean }): Promise<{ path: string }> {
115
+ const result = await this.rpc<{ base64: string }>('screenshot', {
116
+ overlayRefs: opts.overlayRefs,
117
+ });
118
+ await writeFile(opts.path, Buffer.from(result.base64, 'base64'));
119
+ return { path: opts.path };
120
+ }
121
+
122
+ override press(target: PressTarget): Promise<void> {
123
+ return this.rpc('press', target);
124
+ }
125
+ override longPress(ref: string, durationMs?: number): Promise<void> {
126
+ return this.rpc('longPress', ref, durationMs);
127
+ }
128
+ override fill(ref: string, text: string): Promise<void> {
129
+ return this.rpc('fill', ref, text);
130
+ }
131
+ override typeText(text: string): Promise<void> {
132
+ return this.rpc('typeText', text);
133
+ }
134
+ override pressKey(key: 'return'): Promise<void> {
135
+ return this.rpc('pressKey', key);
136
+ }
137
+ override scroll(direction: ScrollDirection): Promise<void> {
138
+ return this.rpc('scroll', direction);
139
+ }
140
+ override pan(x: number, y: number, dx: number, dy: number, durationMs?: number): Promise<void> {
141
+ return this.rpc('pan', x, y, dx, dy, durationMs);
142
+ }
143
+ override waitForText(text: string, timeoutMs?: number): Promise<void> {
144
+ return this.rpc('waitForText', text, timeoutMs);
145
+ }
146
+ override systemAlert(action: AlertAction): Promise<BackendAlertResult> {
147
+ return this.rpc('systemAlert', action);
148
+ }
149
+ override home(): Promise<void> {
150
+ return this.rpc('home');
151
+ }
152
+ override back(): Promise<void> {
153
+ return this.rpc('back');
154
+ }
155
+ override openApp(opts: { app?: string; url?: string; relaunch?: boolean }): Promise<OpenAppResult> {
156
+ return this.rpc('openApp', opts);
157
+ }
158
+ override listApps(): Promise<string[]> {
159
+ return this.rpc('listApps');
160
+ }
161
+ override closeSession(): Promise<void> {
162
+ return this.rpc('closeSession');
163
+ }
164
+
165
+ /** Upload a zipped .app bundle (base64) and install it on the sandbox device. */
166
+ installApp(base64Zip: string): Promise<{ installed?: string }> {
167
+ return this.rpc('installApp', base64Zip);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Build a CloudSandboxBackend from explicit options or the environment:
173
+ * PHONE_USE_SANDBOX_URL + PHONE_USE_SANDBOX_TOKEN (printed by
174
+ * `phone-use sandbox env <id>`).
175
+ */
176
+ export function createCloudSandboxBackend(config?: Partial<CloudSandboxBackendOptions>): CloudSandboxBackend {
177
+ const endpoint = config?.endpoint ?? process.env.PHONE_USE_SANDBOX_URL;
178
+ const token = config?.token ?? process.env.PHONE_USE_SANDBOX_TOKEN;
179
+ if (!endpoint || !token) {
180
+ throw new PhoneUseError(
181
+ 'cloud sandbox backend needs PHONE_USE_SANDBOX_URL and PHONE_USE_SANDBOX_TOKEN (see `phone-use sandbox env <id>`)',
182
+ { code: 'BACKEND_NOT_FOUND', retryable: false },
183
+ );
184
+ }
185
+ return new CloudSandboxBackend({ ...config, endpoint, token });
186
+ }
187
+
188
+ function normalizeErrorCode(
189
+ code: string | undefined,
190
+ ):
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
+ const codes = new Set([
201
+ 'DEVICE_NOT_FOUND',
202
+ 'DEVICE_IN_USE',
203
+ 'SESSION_NOT_FOUND',
204
+ 'TIMEOUT',
205
+ 'ACTION_FAILED',
206
+ 'UNSUPPORTED_CAPABILITY',
207
+ 'BACKEND_NOT_FOUND',
208
+ 'ABORTED',
209
+ 'UNKNOWN',
210
+ ]);
211
+ if (codes.has(code ?? '')) return code as ReturnType<typeof normalizeErrorCode>;
212
+ // The cloud worker's lowercase taxonomy (device_gone/timeout/…) maps into
213
+ // the typed PhoneUseError codes instead of collapsing to UNKNOWN.
214
+ const cloud: Record<string, ReturnType<typeof normalizeErrorCode>> = {
215
+ device_gone: 'DEVICE_NOT_FOUND',
216
+ sandbox_not_found: 'DEVICE_NOT_FOUND',
217
+ timeout: 'TIMEOUT',
218
+ session_conflict: 'DEVICE_IN_USE',
219
+ target_not_found: 'ACTION_FAILED',
220
+ unauthorized: 'ABORTED',
221
+ payload_too_large: 'ACTION_FAILED',
222
+ bad_request: 'ACTION_FAILED',
223
+ internal: 'UNKNOWN',
224
+ };
225
+ return cloud[code ?? ''] ?? 'UNKNOWN';
226
+ }
@@ -201,5 +201,7 @@ export class DeviceRunnerBackend extends BaseDeviceBackend {
201
201
  }
202
202
  }
203
203
 
204
+ /** Backend for a phone-runner reached by URL (defaults come from
205
+ * `PHONE_USE_RUNNER_URL` / `PHONE_USE_RUNNER_TOKEN` when `config` is omitted). */
204
206
  export const createDeviceRunnerBackend = (config?: DeviceRunnerConfig): DeviceRunnerBackend =>
205
207
  new DeviceRunnerBackend(config);
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.2.0';
10
+ export const VERSION = '0.3.1';
11
11
 
12
12
  export {
13
13
  type Action,
@@ -31,7 +31,21 @@ export {
31
31
  registerBackend,
32
32
  } from './backend.ts';
33
33
  export { createAgentDeviceBackend } from './backends/agent-device.ts';
34
- export { createDeviceRunnerBackend, DeviceRunnerBackend } from './backends/device-runner.ts';
34
+ export {
35
+ CloudSandboxBackend,
36
+ type CloudSandboxBackendOptions,
37
+ createCloudSandboxBackend,
38
+ type SandboxRpcFailure,
39
+ type SandboxRpcMethod,
40
+ type SandboxRpcRequest,
41
+ type SandboxRpcResponse,
42
+ type SandboxRpcSuccess,
43
+ } from './backends/cloud-sandbox.ts';
44
+ export {
45
+ createDeviceRunnerBackend,
46
+ DeviceRunnerBackend,
47
+ type DeviceRunnerConfig,
48
+ } from './backends/device-runner.ts';
35
49
  export { type IosConnectOptions, type IosLaunchOptions, ios } from './backends/ios.ts';
36
50
  export type { AndroidDeviceConfig, CommonDeviceConfig, DeviceConfig, IosDeviceConfig } from './config.ts';
37
51
  export type {