use-computer 0.0.20__tar.gz → 0.0.22__tar.gz
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.
- {use_computer-0.0.20 → use_computer-0.0.22}/PKG-INFO +1 -1
- use_computer-0.0.22/examples/go/hello_windows.go +35 -0
- use_computer-0.0.22/examples/javascript/hello_windows.ts +14 -0
- use_computer-0.0.22/go/README.md +46 -0
- use_computer-0.0.22/go/client.go +192 -0
- use_computer-0.0.22/go/go.mod +3 -0
- use_computer-0.0.22/go/sandbox.go +156 -0
- use_computer-0.0.22/javascript/.gitignore +3 -0
- use_computer-0.0.22/javascript/README.md +48 -0
- use_computer-0.0.22/javascript/package-lock.json +51 -0
- use_computer-0.0.22/javascript/package.json +27 -0
- use_computer-0.0.22/javascript/src/client.ts +82 -0
- use_computer-0.0.22/javascript/src/http.ts +96 -0
- use_computer-0.0.22/javascript/src/index.ts +11 -0
- use_computer-0.0.22/javascript/src/sandbox.ts +208 -0
- use_computer-0.0.22/javascript/tsconfig.json +16 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/pyproject.toml +1 -1
- {use_computer-0.0.20 → use_computer-0.0.22}/.env.example +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/.gitignore +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/.pre-commit-config.yaml +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/README.md +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/examples/_1_hello_macos.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/examples/_2_hello_ios.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/examples/_3_recording.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/examples/_4_file_transfer.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/examples/_5_keepalive.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/examples/_6_hello_tvos.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/examples/_7_hello_windows.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/__init__.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/accessibility.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/ax_transpile.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/client.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/display.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/errors.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/ios/__init__.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/ios/apps.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/ios/environment.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/ios/input.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/macos/__init__.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/macos/keyboard.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/macos/mouse.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/models.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/parsers.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/py.typed +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/recording.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/retry.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/sandbox.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/screenshot.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/simulators.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/__init__.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/pre_command.sh +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/task.toml +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_ios.sh +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_macos.sh +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_macos_check.sh +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/windows/__init__.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/use_computer/windows/shell.py +0 -0
- {use_computer-0.0.20 → use_computer-0.0.22}/uv.lock +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Windows (Beta). Run:
|
|
2
|
+
// export USE_COMPUTER_API_KEY=...
|
|
3
|
+
// go run hello_windows.go
|
|
4
|
+
package main
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"context"
|
|
8
|
+
"fmt"
|
|
9
|
+
"os"
|
|
10
|
+
|
|
11
|
+
uc "github.com/josancamon19/use-computer-sdk/go"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func main() {
|
|
15
|
+
ctx := context.Background()
|
|
16
|
+
c := uc.New()
|
|
17
|
+
|
|
18
|
+
win, err := c.Create(ctx, uc.CreateOptions{Type: uc.Windows, Version: "windows-11"})
|
|
19
|
+
if err != nil {
|
|
20
|
+
panic(err)
|
|
21
|
+
}
|
|
22
|
+
defer win.Close(ctx)
|
|
23
|
+
|
|
24
|
+
name, _ := win.RunShell(ctx, "$env:COMPUTERNAME", "powershell")
|
|
25
|
+
fmt.Println("computer name:", name.Stdout)
|
|
26
|
+
win.RunShell(ctx, "Start-Process notepad", "powershell")
|
|
27
|
+
win.TypeText(ctx, "hello from use.computer")
|
|
28
|
+
|
|
29
|
+
png, err := win.Screenshot(ctx)
|
|
30
|
+
if err != nil {
|
|
31
|
+
panic(err)
|
|
32
|
+
}
|
|
33
|
+
os.WriteFile("hello_windows.png", png, 0o644)
|
|
34
|
+
fmt.Printf("saved hello_windows.png (%d bytes)\n", len(png))
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Windows (Beta) — run: USE_COMPUTER_API_KEY=... npx tsx hello_windows.ts
|
|
2
|
+
import { Computer } from "use-computer";
|
|
3
|
+
|
|
4
|
+
const computer = new Computer();
|
|
5
|
+
await using win = await computer.create({ type: "windows", version: "windows-11" });
|
|
6
|
+
|
|
7
|
+
console.log("computer name:", (await win.run("$env:COMPUTERNAME")).stdout.trim());
|
|
8
|
+
await win.run("Start-Process notepad");
|
|
9
|
+
await win.keyboard.type("hello from use.computer");
|
|
10
|
+
|
|
11
|
+
const png = await win.screenshot.takeFullScreen();
|
|
12
|
+
const { writeFile } = await import("node:fs/promises");
|
|
13
|
+
await writeFile("hello_windows.png", png);
|
|
14
|
+
console.log(`saved hello_windows.png (${png.length} bytes)`);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# use-computer — Go SDK
|
|
2
|
+
|
|
3
|
+
Go client for [use.computer](https://use.computer) — rent dedicated VMs (macOS, iOS/visionOS/tvOS simulators, **Windows**, **Ubuntu**) built for computer-use agents. Parity with the [Python](../python) and [TypeScript](../javascript) SDKs.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
go get github.com/josancamon19/use-computer-sdk/go@latest
|
|
7
|
+
export USE_COMPUTER_API_KEY=mk_live_...
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```go
|
|
11
|
+
package main
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"context"
|
|
15
|
+
"fmt"
|
|
16
|
+
|
|
17
|
+
uc "github.com/josancamon19/use-computer-sdk/go"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
func main() {
|
|
21
|
+
ctx := context.Background()
|
|
22
|
+
c := uc.New() // base URL + key from env, or uc.WithBaseURL/WithAPIKey
|
|
23
|
+
|
|
24
|
+
// Windows (Beta) — no SSH; runs in-guest via PowerShell/cmd
|
|
25
|
+
win, err := c.Create(ctx, uc.CreateOptions{Type: uc.Windows, Version: "windows-11"})
|
|
26
|
+
if err != nil {
|
|
27
|
+
panic(err)
|
|
28
|
+
}
|
|
29
|
+
defer win.Close(ctx)
|
|
30
|
+
|
|
31
|
+
r, _ := win.RunShell(ctx, "$env:COMPUTERNAME", "powershell")
|
|
32
|
+
fmt.Println(r.Stdout)
|
|
33
|
+
win.Click(ctx, 400, 300)
|
|
34
|
+
win.TypeText(ctx, "hello from use.computer")
|
|
35
|
+
png, _ := win.Screenshot(ctx)
|
|
36
|
+
fmt.Printf("screenshot %d bytes\n", len(png))
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Surface
|
|
41
|
+
|
|
42
|
+
- `New(opts...)` → `*Computer`; `WithBaseURL`, `WithAPIKey`, `WithHTTPClient`.
|
|
43
|
+
- `Create(ctx, CreateOptions{Type, Version, Host, Family, DeviceType, Runtime})` → `*Sandbox`.
|
|
44
|
+
- `Sandbox`: `Exec` (SSH on macOS / in-guest on Ubuntu / PowerShell on Windows), `RunShell(cmd, "powershell"|"cmd")`, `Screenshot`, `Click`, `Move`, `Scroll`, `TypeText`, `PressKey`, `Hotkey`, `UITree`, `Upload`, `Download`, `StartRecording`/`StopRecording`, `Keepalive`, `Close`.
|
|
45
|
+
|
|
46
|
+
Every call takes a `context.Context`. Docs: [docs.use.computer](https://docs.use.computer). Windows is **Beta** (admin-only).
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Package usecomputer is the Go SDK for use.computer — rent dedicated VMs
|
|
2
|
+
// (macOS, iOS/visionOS/tvOS simulators, Windows, Ubuntu) for computer-use agents.
|
|
3
|
+
// Parity with the Python and TypeScript SDKs.
|
|
4
|
+
package usecomputer
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"bytes"
|
|
8
|
+
"context"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"fmt"
|
|
11
|
+
"io"
|
|
12
|
+
"net/http"
|
|
13
|
+
"net/url"
|
|
14
|
+
"os"
|
|
15
|
+
"strings"
|
|
16
|
+
"time"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// SandboxType is the OS family of a sandbox.
|
|
20
|
+
type SandboxType string
|
|
21
|
+
|
|
22
|
+
const (
|
|
23
|
+
MacOS SandboxType = "macos"
|
|
24
|
+
IOS SandboxType = "ios"
|
|
25
|
+
Windows SandboxType = "windows"
|
|
26
|
+
Ubuntu SandboxType = "ubuntu"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// Computer is a client for a use.computer gateway.
|
|
30
|
+
type Computer struct {
|
|
31
|
+
baseURL string
|
|
32
|
+
apiKey string
|
|
33
|
+
http *http.Client
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Option configures a Computer.
|
|
37
|
+
type Option func(*Computer)
|
|
38
|
+
|
|
39
|
+
// WithBaseURL sets the gateway base URL (default $USE_COMPUTER_BASE_URL or https://api.use.computer).
|
|
40
|
+
func WithBaseURL(u string) Option { return func(c *Computer) { c.baseURL = strings.TrimRight(u, "/") } }
|
|
41
|
+
|
|
42
|
+
// WithAPIKey sets the API key (default $USE_COMPUTER_API_KEY).
|
|
43
|
+
func WithAPIKey(k string) Option { return func(c *Computer) { c.apiKey = k } }
|
|
44
|
+
|
|
45
|
+
// WithHTTPClient overrides the underlying *http.Client.
|
|
46
|
+
func WithHTTPClient(h *http.Client) Option { return func(c *Computer) { c.http = h } }
|
|
47
|
+
|
|
48
|
+
// New creates a Computer client.
|
|
49
|
+
func New(opts ...Option) *Computer {
|
|
50
|
+
c := &Computer{
|
|
51
|
+
baseURL: envOr("USE_COMPUTER_BASE_URL", "https://api.use.computer"),
|
|
52
|
+
apiKey: os.Getenv("USE_COMPUTER_API_KEY"),
|
|
53
|
+
http: &http.Client{Timeout: 200 * time.Second},
|
|
54
|
+
}
|
|
55
|
+
for _, o := range opts {
|
|
56
|
+
o(c)
|
|
57
|
+
}
|
|
58
|
+
c.baseURL = strings.TrimRight(c.baseURL, "/")
|
|
59
|
+
return c
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// CreateOptions selects the sandbox to create.
|
|
63
|
+
type CreateOptions struct {
|
|
64
|
+
Type SandboxType // default MacOS
|
|
65
|
+
Host string // pin to a host (e.g. "mm001")
|
|
66
|
+
Version string // windows image version: "windows-11" (default), "windows-10"
|
|
67
|
+
Family string // ios simulator family
|
|
68
|
+
DeviceType string // ios exact device type
|
|
69
|
+
Runtime string // ios exact runtime
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type createResponse struct {
|
|
73
|
+
SandboxID string `json:"sandbox_id"`
|
|
74
|
+
Type string `json:"type"`
|
|
75
|
+
VNCURL string `json:"vnc_url"`
|
|
76
|
+
SSHURL string `json:"ssh_url"`
|
|
77
|
+
VMIP string `json:"vm_ip"`
|
|
78
|
+
Host string `json:"host"`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create provisions a new sandbox and returns it once ready.
|
|
82
|
+
func (c *Computer) Create(ctx context.Context, opts CreateOptions) (*Sandbox, error) {
|
|
83
|
+
t := opts.Type
|
|
84
|
+
if t == "" {
|
|
85
|
+
t = MacOS
|
|
86
|
+
}
|
|
87
|
+
body := map[string]any{"type": string(t)}
|
|
88
|
+
if opts.Host != "" {
|
|
89
|
+
body["host"] = opts.Host
|
|
90
|
+
}
|
|
91
|
+
if t == Windows && opts.Version != "" {
|
|
92
|
+
body["version"] = opts.Version
|
|
93
|
+
}
|
|
94
|
+
if t == IOS {
|
|
95
|
+
if opts.Family != "" {
|
|
96
|
+
body["family"] = opts.Family
|
|
97
|
+
}
|
|
98
|
+
if opts.DeviceType != "" {
|
|
99
|
+
body["device_type"] = opts.DeviceType
|
|
100
|
+
}
|
|
101
|
+
if opts.Runtime != "" {
|
|
102
|
+
body["runtime"] = opts.Runtime
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
var resp createResponse
|
|
106
|
+
if err := c.doJSON(ctx, http.MethodPost, "/v1/sandboxes", body, &resp); err != nil {
|
|
107
|
+
return nil, err
|
|
108
|
+
}
|
|
109
|
+
return &Sandbox{
|
|
110
|
+
ID: resp.SandboxID, Type: t, VNCURL: resp.VNCURL, SSHURL: resp.SSHURL, VMIP: resp.VMIP,
|
|
111
|
+
c: c, prefix: "/v1/sandboxes/" + resp.SandboxID,
|
|
112
|
+
}, nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Platforms discovers available platforms, images, runtimes, and device types.
|
|
116
|
+
func (c *Computer) Platforms(ctx context.Context) (map[string]any, error) {
|
|
117
|
+
var out map[string]any
|
|
118
|
+
return out, c.doJSON(ctx, http.MethodGet, "/v1/platforms", nil, &out)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- internal HTTP helpers ---
|
|
122
|
+
|
|
123
|
+
func (c *Computer) newReq(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
|
|
124
|
+
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
|
|
125
|
+
if err != nil {
|
|
126
|
+
return nil, err
|
|
127
|
+
}
|
|
128
|
+
if c.apiKey != "" {
|
|
129
|
+
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
130
|
+
}
|
|
131
|
+
return req, nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func (c *Computer) doJSON(ctx context.Context, method, path string, body, out any) error {
|
|
135
|
+
var rdr io.Reader
|
|
136
|
+
if body != nil {
|
|
137
|
+
b, err := json.Marshal(body)
|
|
138
|
+
if err != nil {
|
|
139
|
+
return err
|
|
140
|
+
}
|
|
141
|
+
rdr = bytes.NewReader(b)
|
|
142
|
+
} else if method == http.MethodPost {
|
|
143
|
+
rdr = strings.NewReader("{}")
|
|
144
|
+
}
|
|
145
|
+
req, err := c.newReq(ctx, method, path, rdr)
|
|
146
|
+
if err != nil {
|
|
147
|
+
return err
|
|
148
|
+
}
|
|
149
|
+
if rdr != nil {
|
|
150
|
+
req.Header.Set("Content-Type", "application/json")
|
|
151
|
+
}
|
|
152
|
+
resp, err := c.http.Do(req)
|
|
153
|
+
if err != nil {
|
|
154
|
+
return err
|
|
155
|
+
}
|
|
156
|
+
defer resp.Body.Close()
|
|
157
|
+
if resp.StatusCode >= 400 {
|
|
158
|
+
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
159
|
+
return fmt.Errorf("%s %s: %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(b)))
|
|
160
|
+
}
|
|
161
|
+
if out == nil {
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
return json.NewDecoder(resp.Body).Decode(out)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func (c *Computer) getBytes(ctx context.Context, path string, q url.Values) ([]byte, error) {
|
|
168
|
+
if q != nil {
|
|
169
|
+
path += "?" + q.Encode()
|
|
170
|
+
}
|
|
171
|
+
req, err := c.newReq(ctx, http.MethodGet, path, nil)
|
|
172
|
+
if err != nil {
|
|
173
|
+
return nil, err
|
|
174
|
+
}
|
|
175
|
+
resp, err := c.http.Do(req)
|
|
176
|
+
if err != nil {
|
|
177
|
+
return nil, err
|
|
178
|
+
}
|
|
179
|
+
defer resp.Body.Close()
|
|
180
|
+
if resp.StatusCode >= 400 {
|
|
181
|
+
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
182
|
+
return nil, fmt.Errorf("GET %s: %d: %s", path, resp.StatusCode, strings.TrimSpace(string(b)))
|
|
183
|
+
}
|
|
184
|
+
return io.ReadAll(resp.Body)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func envOr(k, def string) string {
|
|
188
|
+
if v := os.Getenv(k); v != "" {
|
|
189
|
+
return v
|
|
190
|
+
}
|
|
191
|
+
return def
|
|
192
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package usecomputer
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/url"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// Sandbox is a running VM. Action methods map 1:1 to the gateway HTTP surface.
|
|
12
|
+
type Sandbox struct {
|
|
13
|
+
ID string
|
|
14
|
+
Type SandboxType
|
|
15
|
+
VNCURL string
|
|
16
|
+
SSHURL string // macOS only
|
|
17
|
+
VMIP string // macOS only
|
|
18
|
+
|
|
19
|
+
c *Computer
|
|
20
|
+
prefix string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ExecResult is the outcome of a shell command.
|
|
24
|
+
type ExecResult struct {
|
|
25
|
+
Stdout string `json:"stdout"`
|
|
26
|
+
Stderr string `json:"stderr"`
|
|
27
|
+
ExitCode int `json:"-"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type execWire struct {
|
|
31
|
+
Stdout string `json:"stdout"`
|
|
32
|
+
Stderr string `json:"stderr"`
|
|
33
|
+
ExitCode *int `json:"exit_code"`
|
|
34
|
+
ReturnCode *int `json:"return_code"`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func (e execWire) result() ExecResult {
|
|
38
|
+
code := 0
|
|
39
|
+
if e.ExitCode != nil {
|
|
40
|
+
code = *e.ExitCode
|
|
41
|
+
} else if e.ReturnCode != nil {
|
|
42
|
+
code = *e.ReturnCode
|
|
43
|
+
}
|
|
44
|
+
return ExecResult{Stdout: e.Stdout, Stderr: e.Stderr, ExitCode: code}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Exec runs a command: SSH on macOS, in-guest on Ubuntu, PowerShell on Windows.
|
|
48
|
+
// On Windows pass shell ("powershell" or "cmd") via RunShell for cmd.
|
|
49
|
+
func (s *Sandbox) Exec(ctx context.Context, command string) (ExecResult, error) {
|
|
50
|
+
var w execWire
|
|
51
|
+
err := s.c.doJSON(ctx, http.MethodPost, s.prefix+"/exec", map[string]any{"command": command}, &w)
|
|
52
|
+
return w.result(), err
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// RunShell runs a command on Windows in the given shell ("powershell" | "cmd").
|
|
56
|
+
func (s *Sandbox) RunShell(ctx context.Context, command, shell string) (ExecResult, error) {
|
|
57
|
+
var w execWire
|
|
58
|
+
err := s.c.doJSON(ctx, http.MethodPost, s.prefix+"/exec", map[string]any{"command": command, "shell": shell}, &w)
|
|
59
|
+
return w.result(), err
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Screenshot returns a full-screen PNG/JPEG.
|
|
63
|
+
func (s *Sandbox) Screenshot(ctx context.Context) ([]byte, error) {
|
|
64
|
+
return s.c.getBytes(ctx, s.prefix+"/screenshot", nil)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Click left-clicks at (x, y).
|
|
68
|
+
func (s *Sandbox) Click(ctx context.Context, x, y int) error {
|
|
69
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/act",
|
|
70
|
+
map[string]any{"action": map[string]any{"type": "click", "x": x, "y": y, "button": "left"}}, nil)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Move moves the cursor to (x, y).
|
|
74
|
+
func (s *Sandbox) Move(ctx context.Context, x, y int) error {
|
|
75
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/mouse/move", map[string]any{"x": x, "y": y}, nil)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Scroll scrolls by (dx, dy) at (x, y).
|
|
79
|
+
func (s *Sandbox) Scroll(ctx context.Context, x, y, dx, dy int) error {
|
|
80
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/act",
|
|
81
|
+
map[string]any{"action": map[string]any{"type": "scroll", "x": x, "y": y, "scroll_x": dx, "scroll_y": dy}}, nil)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// TypeText types a string.
|
|
85
|
+
func (s *Sandbox) TypeText(ctx context.Context, text string) error {
|
|
86
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/keyboard/type", map[string]any{"text": text}, nil)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// PressKey presses a single key.
|
|
90
|
+
func (s *Sandbox) PressKey(ctx context.Context, key string) error {
|
|
91
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/keyboard/press", map[string]any{"key": key}, nil)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Hotkey presses a combo, e.g. "ctrl+c" (macOS "cmd+c", Windows "win+d").
|
|
95
|
+
func (s *Sandbox) Hotkey(ctx context.Context, combo string) error {
|
|
96
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/keyboard/hotkey", map[string]any{"keys": combo}, nil)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// UITree returns the native UI tree (macOS axe schema / Windows UIAutomation).
|
|
100
|
+
func (s *Sandbox) UITree(ctx context.Context) (map[string]any, error) {
|
|
101
|
+
var out map[string]any
|
|
102
|
+
return out, s.c.doJSON(ctx, http.MethodGet, s.prefix+"/display/windows", nil, &out)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Upload writes bytes to remotePath in the VM.
|
|
106
|
+
func (s *Sandbox) Upload(ctx context.Context, data []byte, remotePath string) error {
|
|
107
|
+
req, err := s.c.newReq(ctx, http.MethodPut, s.prefix+"/files?"+url.Values{"path": {remotePath}}.Encode(), bytes.NewReader(data))
|
|
108
|
+
if err != nil {
|
|
109
|
+
return err
|
|
110
|
+
}
|
|
111
|
+
req.Header.Set("Content-Type", "application/octet-stream")
|
|
112
|
+
resp, err := s.c.http.Do(req)
|
|
113
|
+
if err != nil {
|
|
114
|
+
return err
|
|
115
|
+
}
|
|
116
|
+
defer resp.Body.Close()
|
|
117
|
+
if resp.StatusCode >= 400 {
|
|
118
|
+
return fmt.Errorf("upload %s: %d", remotePath, resp.StatusCode)
|
|
119
|
+
}
|
|
120
|
+
return nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Download reads remotePath from the VM.
|
|
124
|
+
func (s *Sandbox) Download(ctx context.Context, remotePath string) ([]byte, error) {
|
|
125
|
+
return s.c.getBytes(ctx, s.prefix+"/files", url.Values{"path": {remotePath}})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// StartRecording starts a screen recording, returning its id.
|
|
129
|
+
func (s *Sandbox) StartRecording(ctx context.Context) (string, error) {
|
|
130
|
+
var out struct {
|
|
131
|
+
RecordingID string `json:"recording_id"`
|
|
132
|
+
ID string `json:"id"`
|
|
133
|
+
}
|
|
134
|
+
if err := s.c.doJSON(ctx, http.MethodPost, s.prefix+"/recording/start", nil, &out); err != nil {
|
|
135
|
+
return "", err
|
|
136
|
+
}
|
|
137
|
+
if out.RecordingID != "" {
|
|
138
|
+
return out.RecordingID, nil
|
|
139
|
+
}
|
|
140
|
+
return out.ID, nil
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// StopRecording stops a recording by id.
|
|
144
|
+
func (s *Sandbox) StopRecording(ctx context.Context, recordingID string) error {
|
|
145
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/recording/stop", map[string]any{"recording_id": recordingID}, nil)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Keepalive resets the idle timer.
|
|
149
|
+
func (s *Sandbox) Keepalive(ctx context.Context) error {
|
|
150
|
+
return s.c.doJSON(ctx, http.MethodPost, s.prefix+"/keepalive", nil, nil)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Close destroys the sandbox.
|
|
154
|
+
func (s *Sandbox) Close(ctx context.Context) error {
|
|
155
|
+
return s.c.doJSON(ctx, http.MethodDelete, s.prefix, nil, nil)
|
|
156
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# use-computer — TypeScript SDK
|
|
2
|
+
|
|
3
|
+
TypeScript/JavaScript client for [use.computer](https://use.computer) — rent dedicated VMs (macOS, iOS/visionOS/tvOS simulators, **Windows**, **Ubuntu**) built for computer-use agents. Parity with the [Python SDK](../python); async-native.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install use-computer
|
|
7
|
+
export USE_COMPUTER_API_KEY=mk_live_...
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { Computer } from "use-computer";
|
|
12
|
+
|
|
13
|
+
const computer = new Computer(); // baseUrl + apiKey from env, or pass {baseUrl, apiKey}
|
|
14
|
+
|
|
15
|
+
// macOS (default)
|
|
16
|
+
const mac = await computer.create({ type: "macos" });
|
|
17
|
+
await mac.execSsh("open -a TextEdit");
|
|
18
|
+
await mac.keyboard.type("hello");
|
|
19
|
+
const png = await mac.screenshot.takeFullScreen();
|
|
20
|
+
await mac.close();
|
|
21
|
+
|
|
22
|
+
// Windows (Beta) — no SSH; exec runs in-guest via PowerShell/cmd
|
|
23
|
+
await using win = await computer.create({ type: "windows", version: "windows-11" });
|
|
24
|
+
console.log((await win.run("$env:COMPUTERNAME")).stdout);
|
|
25
|
+
await win.mouse.click(400, 300);
|
|
26
|
+
await win.keyboard.type("hello from use.computer");
|
|
27
|
+
const tree = await win.uiTree(); // native Windows UIAutomation tree
|
|
28
|
+
|
|
29
|
+
// iOS simulator
|
|
30
|
+
const ios = await computer.create({ type: "ios", family: "iphone" });
|
|
31
|
+
await ios.tap(120, 300);
|
|
32
|
+
|
|
33
|
+
// Ubuntu
|
|
34
|
+
const ubuntu = await computer.create({ type: "ubuntu" });
|
|
35
|
+
console.log((await ubuntu.exec("uname -a")).stdout);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Surface
|
|
39
|
+
|
|
40
|
+
- `Computer.create({ type, version?, host?, family?, deviceType?, runtime? })` → typed sandbox.
|
|
41
|
+
- All sandboxes: `screenshot.takeFullScreen()/takeCompressed()`, `recording.start()/stop()/listAll()/download()`, `uiTree()`, `displayInfo()`, `upload(bytes, path)`, `download(path)`, `keepalive()`, `close()`.
|
|
42
|
+
- macOS / Windows / Ubuntu: `mouse.move/click/doubleClick/scroll/drag/position`, `keyboard.type/press/hotkey`.
|
|
43
|
+
- macOS: `execSsh(cmd)`. Windows: `run(cmd, "powershell"|"cmd")` (no SSH). Ubuntu: `exec(cmd)`.
|
|
44
|
+
- iOS: `tap`, `pressButton`, `pressRemote`, `launch` (no axe-schema `uiTree`).
|
|
45
|
+
|
|
46
|
+
`await using` (TS 5.2+) auto-closes the sandbox at scope exit.
|
|
47
|
+
|
|
48
|
+
Docs: [docs.use.computer](https://docs.use.computer). Windows is **Beta** (admin-only).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "use-computer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "use-computer",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/node": "^20.0.0",
|
|
13
|
+
"typescript": "^5.6.0"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"node_modules/@types/node": {
|
|
20
|
+
"version": "20.19.41",
|
|
21
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
|
22
|
+
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
|
23
|
+
"dev": true,
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"undici-types": "~6.21.0"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"node_modules/typescript": {
|
|
30
|
+
"version": "5.9.3",
|
|
31
|
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
32
|
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
33
|
+
"dev": true,
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"bin": {
|
|
36
|
+
"tsc": "bin/tsc",
|
|
37
|
+
"tsserver": "bin/tsserver"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=14.17"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"node_modules/undici-types": {
|
|
44
|
+
"version": "6.21.0",
|
|
45
|
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
46
|
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
|
47
|
+
"dev": true,
|
|
48
|
+
"license": "MIT"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "use-computer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for use.computer — rent dedicated VMs (macOS, iOS/visionOS/tvOS simulators, Windows, Ubuntu) built for computer-use agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "README.md"],
|
|
17
|
+
"engines": { "node": ">=18" },
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["use.computer", "computer-use", "agents", "macos", "windows", "ios", "ubuntu", "sandbox", "vnc"],
|
|
24
|
+
"repository": { "type": "git", "url": "https://github.com/josancamon19/use-computer-sdk.git", "directory": "javascript" },
|
|
25
|
+
"homepage": "https://docs.use.computer",
|
|
26
|
+
"devDependencies": { "typescript": "^5.6.0", "@types/node": "^20.0.0" }
|
|
27
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { HttpClient } from "./http.js";
|
|
2
|
+
import {
|
|
3
|
+
IOSSandbox,
|
|
4
|
+
MacOSSandbox,
|
|
5
|
+
type SandboxType,
|
|
6
|
+
UbuntuSandbox,
|
|
7
|
+
WindowsSandbox,
|
|
8
|
+
type AnySandbox,
|
|
9
|
+
} from "./sandbox.js";
|
|
10
|
+
|
|
11
|
+
export interface ComputerOptions {
|
|
12
|
+
/** Gateway base URL. Default: $USE_COMPUTER_BASE_URL or https://api.use.computer */
|
|
13
|
+
baseUrl?: string;
|
|
14
|
+
/** API key. Default: $USE_COMPUTER_API_KEY */
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateOptions {
|
|
19
|
+
type?: SandboxType;
|
|
20
|
+
/** Pin to a specific host machine (e.g. "mm001"). */
|
|
21
|
+
host?: string;
|
|
22
|
+
/** iOS: simulator family ("iphone" | "ipad" | "tv" | "watch" | "vision"). */
|
|
23
|
+
family?: string;
|
|
24
|
+
/** iOS: exact CoreSimulator device type / runtime identifiers. */
|
|
25
|
+
deviceType?: string;
|
|
26
|
+
runtime?: string;
|
|
27
|
+
/** Windows: image version — "windows-11" (default), "windows-10". */
|
|
28
|
+
version?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CreateResponse {
|
|
32
|
+
sandbox_id: string;
|
|
33
|
+
type?: string;
|
|
34
|
+
vnc_url?: string;
|
|
35
|
+
ssh_url?: string;
|
|
36
|
+
vm_ip?: string;
|
|
37
|
+
host?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class Computer {
|
|
41
|
+
private readonly http: HttpClient;
|
|
42
|
+
|
|
43
|
+
constructor(opts: ComputerOptions = {}) {
|
|
44
|
+
const baseUrl = opts.baseUrl ?? process.env.USE_COMPUTER_BASE_URL ?? "https://api.use.computer";
|
|
45
|
+
const apiKey = opts.apiKey ?? process.env.USE_COMPUTER_API_KEY ?? "";
|
|
46
|
+
this.http = new HttpClient(baseUrl, apiKey);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Discover available platforms, images, runtimes, and device types. */
|
|
50
|
+
platforms(): Promise<unknown> {
|
|
51
|
+
return this.http.getJSON("/v1/platforms");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async create(opts: { type?: "macos"; host?: string }): Promise<MacOSSandbox>;
|
|
55
|
+
async create(opts: { type: "ios"; family?: string; deviceType?: string; runtime?: string }): Promise<IOSSandbox>;
|
|
56
|
+
async create(opts: { type: "windows"; version?: string; host?: string }): Promise<WindowsSandbox>;
|
|
57
|
+
async create(opts: { type: "ubuntu"; host?: string }): Promise<UbuntuSandbox>;
|
|
58
|
+
async create(opts: CreateOptions = {}): Promise<AnySandbox> {
|
|
59
|
+
const type = (opts.type ?? "macos") as SandboxType;
|
|
60
|
+
const body: Record<string, unknown> = { type };
|
|
61
|
+
if (opts.host) body.host = opts.host;
|
|
62
|
+
if (type === "windows" && opts.version) body.version = opts.version;
|
|
63
|
+
if (type === "ios") {
|
|
64
|
+
if (opts.family) body.family = opts.family;
|
|
65
|
+
if (opts.deviceType) body.device_type = opts.deviceType;
|
|
66
|
+
if (opts.runtime) body.runtime = opts.runtime;
|
|
67
|
+
}
|
|
68
|
+
const d = await this.http.postJSON<CreateResponse>("/v1/sandboxes", body);
|
|
69
|
+
const id = d.sandbox_id;
|
|
70
|
+
const vnc = d.vnc_url ?? "";
|
|
71
|
+
switch (type) {
|
|
72
|
+
case "windows":
|
|
73
|
+
return new WindowsSandbox(id, this.http, vnc);
|
|
74
|
+
case "ubuntu":
|
|
75
|
+
return new UbuntuSandbox(id, this.http, vnc);
|
|
76
|
+
case "ios":
|
|
77
|
+
return new IOSSandbox(id, this.http, vnc);
|
|
78
|
+
default:
|
|
79
|
+
return new MacOSSandbox(id, this.http, vnc, d.ssh_url ?? "", d.vm_ip ?? "");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Thin fetch wrapper: base URL + bearer auth, JSON/binary helpers, typed errors.
|
|
2
|
+
|
|
3
|
+
export class UseComputerError extends Error {}
|
|
4
|
+
|
|
5
|
+
export class PlatformNotSupportedError extends UseComputerError {
|
|
6
|
+
constructor(
|
|
7
|
+
public action: string,
|
|
8
|
+
public sandboxType: string,
|
|
9
|
+
public hint: string,
|
|
10
|
+
) {
|
|
11
|
+
super(`${action} not supported on ${sandboxType} sandbox: ${hint}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class HttpClient {
|
|
16
|
+
readonly baseUrl: string;
|
|
17
|
+
private readonly apiKey: string;
|
|
18
|
+
|
|
19
|
+
constructor(baseUrl: string, apiKey: string) {
|
|
20
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
21
|
+
this.apiKey = apiKey;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private headers(extra?: Record<string, string>): Record<string, string> {
|
|
25
|
+
const h: Record<string, string> = { ...extra };
|
|
26
|
+
if (this.apiKey) h["Authorization"] = `Bearer ${this.apiKey}`;
|
|
27
|
+
return h;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async check(resp: Response, path: string): Promise<Response> {
|
|
31
|
+
if (resp.ok) return resp;
|
|
32
|
+
const body = await resp.text().catch(() => "");
|
|
33
|
+
throw new UseComputerError(`${resp.status} ${path}: ${body.slice(0, 500)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getJSON<T = unknown>(path: string, params?: Record<string, string>): Promise<T> {
|
|
37
|
+
const url = new URL(this.baseUrl + path);
|
|
38
|
+
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
39
|
+
const resp = await this.check(await fetch(url, { headers: this.headers() }), path);
|
|
40
|
+
return (await resp.json()) as T;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getBytes(path: string, params?: Record<string, string>): Promise<Uint8Array> {
|
|
44
|
+
const url = new URL(this.baseUrl + path);
|
|
45
|
+
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
46
|
+
const resp = await this.check(await fetch(url, { headers: this.headers() }), path);
|
|
47
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async postJSON<T = unknown>(path: string, body?: unknown, timeoutMs = 180_000): Promise<T> {
|
|
51
|
+
const resp = await this.check(
|
|
52
|
+
await fetch(this.baseUrl + path, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
55
|
+
body: body === undefined ? "{}" : JSON.stringify(body),
|
|
56
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
57
|
+
}),
|
|
58
|
+
path,
|
|
59
|
+
);
|
|
60
|
+
const ct = resp.headers.get("content-type") ?? "";
|
|
61
|
+
if (ct.startsWith("application/json")) return (await resp.json()) as T;
|
|
62
|
+
return undefined as T;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** POST returning either an image (screenshot) or JSON — used by /act. */
|
|
66
|
+
async postActionRaw(path: string, body: unknown): Promise<{ image?: Uint8Array; data?: unknown }> {
|
|
67
|
+
const resp = await this.check(
|
|
68
|
+
await fetch(this.baseUrl + path, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: this.headers({ "Content-Type": "application/json" }),
|
|
71
|
+
body: JSON.stringify(body),
|
|
72
|
+
}),
|
|
73
|
+
path,
|
|
74
|
+
);
|
|
75
|
+
const ct = resp.headers.get("content-type") ?? "";
|
|
76
|
+
if (ct.startsWith("image/")) return { image: new Uint8Array(await resp.arrayBuffer()) };
|
|
77
|
+
return { data: await resp.json() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async putBytes(path: string, data: Uint8Array, params?: Record<string, string>): Promise<void> {
|
|
81
|
+
const url = new URL(this.baseUrl + path);
|
|
82
|
+
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
83
|
+
await this.check(
|
|
84
|
+
await fetch(url, {
|
|
85
|
+
method: "PUT",
|
|
86
|
+
headers: this.headers({ "Content-Type": "application/octet-stream" }),
|
|
87
|
+
body: data as unknown as BodyInit,
|
|
88
|
+
}),
|
|
89
|
+
path,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async delete(path: string): Promise<void> {
|
|
94
|
+
await this.check(await fetch(this.baseUrl + path, { method: "DELETE", headers: this.headers() }), path);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { Computer } from "./client.js";
|
|
2
|
+
export type { ComputerOptions, CreateOptions } from "./client.js";
|
|
3
|
+
export {
|
|
4
|
+
Sandbox,
|
|
5
|
+
MacOSSandbox,
|
|
6
|
+
WindowsSandbox,
|
|
7
|
+
UbuntuSandbox,
|
|
8
|
+
IOSSandbox,
|
|
9
|
+
} from "./sandbox.js";
|
|
10
|
+
export type { SandboxType, ExecResult, RecordingInfo, AnySandbox, WindowsShellKind } from "./sandbox.js";
|
|
11
|
+
export { UseComputerError, PlatformNotSupportedError } from "./http.js";
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { HttpClient, PlatformNotSupportedError } from "./http.js";
|
|
2
|
+
|
|
3
|
+
export type SandboxType = "macos" | "ios" | "windows" | "ubuntu";
|
|
4
|
+
|
|
5
|
+
export interface ExecResult {
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RecordingInfo {
|
|
12
|
+
recordingId: string;
|
|
13
|
+
fileSize?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function execFromJSON(d: Record<string, unknown>): ExecResult {
|
|
17
|
+
return {
|
|
18
|
+
stdout: String(d.stdout ?? ""),
|
|
19
|
+
stderr: String(d.stderr ?? ""),
|
|
20
|
+
exitCode: Number(d.exit_code ?? d.return_code ?? d.returncode ?? 0),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- action sub-objects (each bound to a sandbox's HTTP prefix) ---
|
|
25
|
+
|
|
26
|
+
class Screenshot {
|
|
27
|
+
constructor(private http: HttpClient, private prefix: string) {}
|
|
28
|
+
takeFullScreen(): Promise<Uint8Array> {
|
|
29
|
+
return this.http.getBytes(`${this.prefix}/screenshot`);
|
|
30
|
+
}
|
|
31
|
+
takeCompressed(): Promise<Uint8Array> {
|
|
32
|
+
return this.http.getBytes(`${this.prefix}/screenshot/compressed`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class Mouse {
|
|
37
|
+
constructor(private http: HttpClient, private prefix: string) {}
|
|
38
|
+
move(x: number, y: number): Promise<void> {
|
|
39
|
+
return this.http.postJSON(`${this.prefix}/mouse/move`, { x, y });
|
|
40
|
+
}
|
|
41
|
+
async click(x: number, y: number, button = "left"): Promise<void> {
|
|
42
|
+
await this.http.postActionRaw(`${this.prefix}/act`, { action: { type: "click", x, y, button } });
|
|
43
|
+
}
|
|
44
|
+
async doubleClick(x: number, y: number): Promise<void> {
|
|
45
|
+
await this.http.postActionRaw(`${this.prefix}/act`, { action: { type: "double_click", x, y } });
|
|
46
|
+
}
|
|
47
|
+
async scroll(x: number, y: number, dx: number, dy: number): Promise<void> {
|
|
48
|
+
await this.http.postActionRaw(`${this.prefix}/act`, { action: { type: "scroll", x, y, scroll_x: dx, scroll_y: dy } });
|
|
49
|
+
}
|
|
50
|
+
async drag(path: Array<[number, number]>): Promise<void> {
|
|
51
|
+
await this.http.postActionRaw(`${this.prefix}/act`, { action: { type: "drag", path } });
|
|
52
|
+
}
|
|
53
|
+
position(): Promise<{ x: number; y: number }> {
|
|
54
|
+
return this.http.getJSON(`${this.prefix}/mouse/position`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class Keyboard {
|
|
59
|
+
constructor(private http: HttpClient, private prefix: string) {}
|
|
60
|
+
type(text: string): Promise<void> {
|
|
61
|
+
return this.http.postJSON(`${this.prefix}/keyboard/type`, { text });
|
|
62
|
+
}
|
|
63
|
+
press(key: string): Promise<void> {
|
|
64
|
+
return this.http.postJSON(`${this.prefix}/keyboard/press`, { key });
|
|
65
|
+
}
|
|
66
|
+
/** e.g. "ctrl+c" (macOS: "cmd+c", windows: "ctrl+c", "win+d"). */
|
|
67
|
+
hotkey(combo: string): Promise<void> {
|
|
68
|
+
return this.http.postJSON(`${this.prefix}/keyboard/hotkey`, { keys: combo });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class Recording {
|
|
73
|
+
constructor(private http: HttpClient, private prefix: string) {}
|
|
74
|
+
async start(): Promise<string> {
|
|
75
|
+
const d = await this.http.postJSON<Record<string, unknown>>(`${this.prefix}/recording/start`);
|
|
76
|
+
return String(d.recording_id ?? d.id ?? "");
|
|
77
|
+
}
|
|
78
|
+
async stop(recordingId: string): Promise<RecordingInfo> {
|
|
79
|
+
const d = await this.http.postJSON<Record<string, unknown>>(`${this.prefix}/recording/stop`, { recording_id: recordingId });
|
|
80
|
+
return { recordingId: String(d.recording_id ?? recordingId), fileSize: Number(d.file_size ?? 0) };
|
|
81
|
+
}
|
|
82
|
+
async listAll(): Promise<RecordingInfo[]> {
|
|
83
|
+
const d = await this.http.getJSON<unknown>(`${this.prefix}/recordings`);
|
|
84
|
+
const items = Array.isArray(d) ? d : ((d as Record<string, unknown>).recordings as unknown[]) ?? [];
|
|
85
|
+
return items.map((r) => {
|
|
86
|
+
const o = r as Record<string, unknown>;
|
|
87
|
+
return { recordingId: String(o.recording_id ?? o.id ?? ""), fileSize: Number(o.file_size ?? 0) };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
download(recordingId: string): Promise<Uint8Array> {
|
|
91
|
+
return this.http.getBytes(`${this.prefix}/recordings/${recordingId}/download`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- sandboxes ---
|
|
96
|
+
|
|
97
|
+
export class Sandbox {
|
|
98
|
+
readonly screenshot: Screenshot;
|
|
99
|
+
readonly recording: Recording;
|
|
100
|
+
protected readonly http: HttpClient;
|
|
101
|
+
protected readonly prefix: string;
|
|
102
|
+
|
|
103
|
+
constructor(
|
|
104
|
+
readonly sandboxId: string,
|
|
105
|
+
readonly type: SandboxType,
|
|
106
|
+
http: HttpClient,
|
|
107
|
+
readonly vncUrl = "",
|
|
108
|
+
) {
|
|
109
|
+
this.http = http;
|
|
110
|
+
this.prefix = `/v1/sandboxes/${sandboxId}`;
|
|
111
|
+
this.screenshot = new Screenshot(http, this.prefix);
|
|
112
|
+
this.recording = new Recording(http, this.prefix);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Native UI tree (macOS axe schema / Windows UIAutomation). */
|
|
116
|
+
uiTree(): Promise<unknown> {
|
|
117
|
+
return this.http.getJSON(`${this.prefix}/display/windows`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
displayInfo(): Promise<{ width: number; height: number }> {
|
|
121
|
+
return this.http.getJSON(`${this.prefix}/display/info`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async upload(data: Uint8Array, remotePath: string): Promise<void> {
|
|
125
|
+
await this.http.putBytes(`${this.prefix}/files`, data, { path: remotePath });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
download(remotePath: string): Promise<Uint8Array> {
|
|
129
|
+
return this.http.getBytes(`${this.prefix}/files`, { path: remotePath });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async keepalive(): Promise<void> {
|
|
133
|
+
await this.http.postJSON(`${this.prefix}/keepalive`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async close(): Promise<void> {
|
|
137
|
+
await this.http.delete(this.prefix);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class MacOSSandbox extends Sandbox {
|
|
142
|
+
readonly mouse: Mouse;
|
|
143
|
+
readonly keyboard: Keyboard;
|
|
144
|
+
constructor(sandboxId: string, http: HttpClient, vncUrl = "", readonly sshUrl = "", readonly vmIp = "") {
|
|
145
|
+
super(sandboxId, "macos", http, vncUrl);
|
|
146
|
+
this.mouse = new Mouse(http, this.prefix);
|
|
147
|
+
this.keyboard = new Keyboard(http, this.prefix);
|
|
148
|
+
}
|
|
149
|
+
async execSsh(command: string, timeoutMs = 120_000): Promise<ExecResult> {
|
|
150
|
+
const d = await this.http.postJSON<Record<string, unknown>>(`${this.prefix}/exec`, { command }, timeoutMs);
|
|
151
|
+
return execFromJSON(d);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type WindowsShellKind = "powershell" | "cmd";
|
|
156
|
+
|
|
157
|
+
export class WindowsSandbox extends Sandbox {
|
|
158
|
+
readonly mouse: Mouse;
|
|
159
|
+
readonly keyboard: Keyboard;
|
|
160
|
+
constructor(sandboxId: string, http: HttpClient, vncUrl = "") {
|
|
161
|
+
super(sandboxId, "windows", http, vncUrl);
|
|
162
|
+
this.mouse = new Mouse(http, this.prefix);
|
|
163
|
+
this.keyboard = new Keyboard(http, this.prefix);
|
|
164
|
+
}
|
|
165
|
+
/** Run a command in-guest (no SSH). PowerShell by default. */
|
|
166
|
+
async run(command: string, shell: WindowsShellKind = "powershell", timeoutMs = 120_000): Promise<ExecResult> {
|
|
167
|
+
const d = await this.http.postJSON<Record<string, unknown>>(`${this.prefix}/exec`, { command, shell }, timeoutMs);
|
|
168
|
+
return execFromJSON(d);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export class UbuntuSandbox extends Sandbox {
|
|
173
|
+
readonly mouse: Mouse;
|
|
174
|
+
readonly keyboard: Keyboard;
|
|
175
|
+
constructor(sandboxId: string, http: HttpClient, vncUrl = "") {
|
|
176
|
+
super(sandboxId, "ubuntu", http, vncUrl);
|
|
177
|
+
this.mouse = new Mouse(http, this.prefix);
|
|
178
|
+
this.keyboard = new Keyboard(http, this.prefix);
|
|
179
|
+
}
|
|
180
|
+
async exec(command: string, timeoutMs = 120_000): Promise<ExecResult> {
|
|
181
|
+
const d = await this.http.postJSON<Record<string, unknown>>(`${this.prefix}/exec`, { command }, timeoutMs);
|
|
182
|
+
return execFromJSON(d);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class IOSSandbox extends Sandbox {
|
|
187
|
+
constructor(sandboxId: string, http: HttpClient, vncUrl = "") {
|
|
188
|
+
super(sandboxId, "ios", http, vncUrl);
|
|
189
|
+
}
|
|
190
|
+
tap(x: number, y: number): Promise<void> {
|
|
191
|
+
return this.http.postJSON(`${this.prefix}/tap`, { x, y });
|
|
192
|
+
}
|
|
193
|
+
pressButton(button: string): Promise<void> {
|
|
194
|
+
return this.http.postJSON(`${this.prefix}/button`, { button });
|
|
195
|
+
}
|
|
196
|
+
pressRemote(button: string): Promise<void> {
|
|
197
|
+
return this.http.postJSON(`${this.prefix}/remote`, { button });
|
|
198
|
+
}
|
|
199
|
+
launch(bundleId: string): Promise<void> {
|
|
200
|
+
return this.http.postJSON(`${this.prefix}/launch`, { bundle_id: bundleId });
|
|
201
|
+
}
|
|
202
|
+
// axe-schema accessibility is macOS-only.
|
|
203
|
+
uiTree(): Promise<unknown> {
|
|
204
|
+
return Promise.reject(new PlatformNotSupportedError("ui_tree", "ios", "use screenshots + tap coordinates"));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type AnySandbox = MacOSSandbox | WindowsSandbox | UbuntuSandbox | IOSSandbox;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"]
|
|
16
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_ios_nograder.sh
RENAMED
|
File without changes
|
|
File without changes
|
{use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_macos_check.sh
RENAMED
|
File without changes
|
{use_computer-0.0.20 → use_computer-0.0.22}/use_computer/tasks/templates/test_macos_nograder.sh
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|