automify 0.1.9 → 0.1.11
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/README.md +60 -14
- package/examples/browser-basic.js +4 -1
- package/examples/desktop-local.js +1 -1
- package/package.json +2 -2
- package/scripts/install-browser.js +1 -0
- package/scripts/install-desktop-if-needed.js +42 -0
- package/scripts/install-desktop.js +85 -16
- package/src/index.d.ts +7 -1
- package/src/lib/argument-reference.js +2 -2
- package/src/lib/desktop-runtime.js +142 -0
- package/src/lib/local-desktop-computer.js +34 -2
package/README.md
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
`Automify` is a Node.js library for AI computer use and command use across web apps, terminals, native desktops, Docker CLI sandboxes, and Docker-backed Linux desktops.
|
|
10
10
|
|
|
11
|
+
Created by [Aldo Vincenti](https://aldovincenti.com).
|
|
12
|
+
|
|
11
13
|
Computer use surfaces:
|
|
12
14
|
|
|
13
|
-
| Surface | Factory | Controlled environment
|
|
14
|
-
| -------------- | --------------------------- |
|
|
15
|
-
| Browser | `automify.browser()` | Playwright browser with screenshots and actions
|
|
16
|
-
| Desktop | `automify.localComputer()` | Native desktop on
|
|
17
|
-
| Docker desktop | `automify.dockerComputer()` | Linux desktop inside a running Docker container
|
|
15
|
+
| Surface | Factory | Controlled environment |
|
|
16
|
+
| -------------- | --------------------------- | --------------------------------------------------------- |
|
|
17
|
+
| Browser | `automify.browser()` | Playwright browser with screenshots and actions |
|
|
18
|
+
| Desktop | `automify.localComputer()` | Native desktop on macOS, Windows, or Linux X11/Xorg hosts |
|
|
19
|
+
| Docker desktop | `automify.dockerComputer()` | Linux desktop inside a running Docker container |
|
|
18
20
|
|
|
19
21
|
Command use surfaces:
|
|
20
22
|
|
|
@@ -40,6 +42,9 @@ Full docs live at [aldovincenti.github.io/automify](https://aldovincenti.github.
|
|
|
40
42
|
|
|
41
43
|
```bash
|
|
42
44
|
npm install automify
|
|
45
|
+
|
|
46
|
+
# Ubuntu 26.04 only, if Playwright blocks Chromium install
|
|
47
|
+
PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 npm install automify
|
|
43
48
|
```
|
|
44
49
|
|
|
45
50
|
Chromium is installed by the package `postinstall` script. Skip it with:
|
|
@@ -66,6 +71,37 @@ npm install zod
|
|
|
66
71
|
|
|
67
72
|
Automify does not require Zod for `jsonOutput()` or any browser, CLI, or desktop runtime.
|
|
68
73
|
|
|
74
|
+
### Optional Docker Setup
|
|
75
|
+
|
|
76
|
+
Docker is required only for `automify.dockerCli()` and `automify.dockerComputer()`.
|
|
77
|
+
|
|
78
|
+
On macOS and Windows, install Docker Desktop from the official Docker website:
|
|
79
|
+
|
|
80
|
+
- macOS: <https://docs.docker.com/desktop/setup/install/mac-install/>
|
|
81
|
+
- Windows: <https://docs.docker.com/desktop/setup/install/windows-install/>
|
|
82
|
+
|
|
83
|
+
On Ubuntu, install Docker from the Ubuntu repositories:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
sudo apt-get update
|
|
87
|
+
sudo apt-get install -y docker.io
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use `docker.io`, not the `docker` package. In Ubuntu packages, `docker.io` provides the Docker Engine/runtime and CLI.
|
|
91
|
+
|
|
92
|
+
Start Docker on Ubuntu and enable it after reboot:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
sudo systemctl enable --now docker
|
|
96
|
+
sudo docker run hello-world
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To run Docker commands without `sudo`, add your user to the `docker` group, then log out and back in:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
sudo usermod -aG docker $USER
|
|
103
|
+
```
|
|
104
|
+
|
|
69
105
|
## Quick Start
|
|
70
106
|
|
|
71
107
|
```js
|
|
@@ -99,7 +135,10 @@ try {
|
|
|
99
135
|
})
|
|
100
136
|
});
|
|
101
137
|
|
|
102
|
-
console.log(run.
|
|
138
|
+
console.log(run.parsed.id, run.parsed.firstName, run.parsed.lastName);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("Automation failed:", error);
|
|
141
|
+
process.exitCode = 1;
|
|
103
142
|
} finally {
|
|
104
143
|
await browser.close();
|
|
105
144
|
}
|
|
@@ -120,12 +159,12 @@ const browser = await automify.browser({
|
|
|
120
159
|
});
|
|
121
160
|
|
|
122
161
|
try {
|
|
123
|
-
const run = await browser.do("
|
|
162
|
+
const run = await browser.do("Summarize what you see on the page.", {
|
|
124
163
|
// Optional: structured result shape.
|
|
125
|
-
output: jsonOutput("
|
|
164
|
+
output: jsonOutput("page_summary", { title: "string", summary: "string" })
|
|
126
165
|
});
|
|
127
166
|
|
|
128
|
-
console.log(run.parsed.
|
|
167
|
+
console.log(run.parsed.title, run.parsed.summary);
|
|
129
168
|
} finally {
|
|
130
169
|
await browser.close();
|
|
131
170
|
}
|
|
@@ -147,7 +186,7 @@ const cli = automify.cli({
|
|
|
147
186
|
await cli.do("Run the tests and summarize failures");
|
|
148
187
|
```
|
|
149
188
|
|
|
150
|
-
Use Docker CLI when command execution should happen inside an isolated container. Docker must be installed and running before you create the adapter:
|
|
189
|
+
Use Docker CLI when command execution should happen inside an isolated container. Docker must be installed and running before you create the adapter. See [Optional Docker Setup](#optional-docker-setup) if you still need to install Docker:
|
|
151
190
|
|
|
152
191
|
```js
|
|
153
192
|
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
@@ -198,7 +237,9 @@ try {
|
|
|
198
237
|
|
|
199
238
|
### Desktop Computer Use
|
|
200
239
|
|
|
201
|
-
Local desktop computer use controls the native desktop on the machine running your Node.js process. It supports macOS, Windows, and Linux through the local desktop adapter. It needs native desktop dependencies that are not installed by default, and your OS may ask for permission to control the desktop.
|
|
240
|
+
Local desktop computer use controls the native desktop on the machine running your Node.js process. It supports macOS, Windows, and Linux through the local desktop adapter. On Linux, local desktop support requires X11/Xorg or Xvfb; Wayland sessions are not supported. It needs native desktop dependencies that are not installed by default, and your OS may ask for permission to control the desktop.
|
|
241
|
+
|
|
242
|
+
**Linux Wayland is not supported for local desktop control.** If `echo $XDG_SESSION_TYPE` prints `wayland`, `localComputer()` can fail during screenshot capture with native X11 errors such as `BadMatch` / `X_GetImage`. Use an Xorg session, run under Xvfb with `forceVirtualDisplay`, or use `dockerComputer()` for an isolated Linux desktop.
|
|
202
243
|
|
|
203
244
|
Before running `npx automify-install-desktop`, install the native build tools for your OS:
|
|
204
245
|
|
|
@@ -208,6 +249,9 @@ winget install --id Microsoft.VisualStudio.2022.BuildTools --exact --override "-
|
|
|
208
249
|
winget install --id Kitware.CMake --exact --source winget
|
|
209
250
|
|
|
210
251
|
# macOS: Xcode Command Line Tools plus CMake on PATH.
|
|
252
|
+
# If Homebrew is not installed, install it first:
|
|
253
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
254
|
+
|
|
211
255
|
xcode-select --install
|
|
212
256
|
brew install cmake
|
|
213
257
|
|
|
@@ -221,7 +265,9 @@ sudo dnf install -y gcc-c++ make cmake libXtst-devel libpng-devel
|
|
|
221
265
|
sudo pacman -S --needed base-devel cmake libxtst libpng
|
|
222
266
|
```
|
|
223
267
|
|
|
224
|
-
On Linux, install the full package list before running `npx automify-install-desktop`; the installer checks for command-line build tools but does not verify every native library package. On headless Linux hosts, also install `xvfb` unless you manage `DISPLAY` yourself. On macOS and Windows, `cmake --version` must work in the terminal where you run `npx automify-install-desktop`. On Windows, the VS Code CMake Tools extension is not enough by itself, and Visual Studio 2026 is not currently recognized by the native build chain used by nut.js.
|
|
268
|
+
On Linux, install the full package list before running `npx automify-install-desktop`; the installer checks for command-line build tools but does not verify every native library package. Linux local desktop capture is X11-based: use Xorg/X11, not Wayland. On headless Linux hosts, also install `xvfb` unless you manage `DISPLAY` yourself. On macOS, install Homebrew first if `brew` is not available, then install CMake with `brew install cmake`. On macOS and Windows, `cmake --version` must work in the terminal where you run `npx automify-install-desktop`. On Windows, the VS Code CMake Tools extension is not enough by itself, and Visual Studio 2026 is not currently recognized by the native build chain used by nut.js.
|
|
269
|
+
|
|
270
|
+
`npx automify-install-desktop` stores the compiled desktop runtime outside `node_modules` in a long-term cache, so normal `npm update` runs do not remove it. If the command is run again and the cached runtime already matches the current platform, CPU architecture, Node ABI, and pinned nut.js/libnut revisions, Automify prints a skip message and exits without rebuilding. Use `npx automify-install-desktop --force` (or `npx automify-install-desktop force`) to rebuild a compatible cache anyway. If a later `npm install` or `npm update` detects that a previously installed desktop runtime no longer matches the current environment, Automify rebuilds it automatically during `postinstall`. Default cache roots are `%LOCALAPPDATA%\automify\desktop-runtime` on Windows, `~/Library/Caches/automify/desktop-runtime` on macOS, and `${XDG_CACHE_HOME:-~/.cache}/automify/desktop-runtime` on Linux. Override with `AUTOMIFY_DESKTOP_RUNTIME_DIR`; disable auto-rebuild with `AUTOMIFY_SKIP_DESKTOP_AUTO_REBUILD=1`.
|
|
225
271
|
|
|
226
272
|
```js
|
|
227
273
|
import { initAutomify } from "automify";
|
|
@@ -239,14 +285,14 @@ const desktop = await automify.localComputer();
|
|
|
239
285
|
|
|
240
286
|
try {
|
|
241
287
|
await desktop.do(
|
|
242
|
-
"Open the Calendar app installed on this computer, find the next event
|
|
288
|
+
"Open the Calendar app installed on this computer, find the next event, and summarize it. Do not create or edit events."
|
|
243
289
|
);
|
|
244
290
|
} finally {
|
|
245
291
|
await desktop.close();
|
|
246
292
|
}
|
|
247
293
|
```
|
|
248
294
|
|
|
249
|
-
For isolated Linux desktop computer use, use Docker. `dockerComputer()` can run from a macOS, Windows, or Linux host with Docker installed and running, but the desktop it controls inside the container is Linux. Docker desktop does not use `automify-install-desktop`; it needs a running Docker daemon and an initial app command:
|
|
295
|
+
For isolated Linux desktop computer use, use Docker. `dockerComputer()` can run from a macOS, Windows, or Linux host with Docker installed and running, but the desktop it controls inside the container is Linux. This is the recommended path when the host Linux session uses Wayland, because `localComputer()` does not support Wayland. Docker desktop does not use `automify-install-desktop`; it needs a running Docker daemon and an initial app command. See [Optional Docker Setup](#optional-docker-setup) if Docker is not installed yet:
|
|
250
296
|
|
|
251
297
|
```js
|
|
252
298
|
import { initAutomify } from "automify";
|
|
@@ -17,7 +17,7 @@ const desktop = automify.computer({
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
const instruction =
|
|
20
|
-
"Open the Calendar app installed on this computer, find the next event
|
|
20
|
+
"Open the Calendar app installed on this computer, find the next event, and summarize it. Do not create or edit events.";
|
|
21
21
|
|
|
22
22
|
await desktop.do(instruction, {
|
|
23
23
|
screenshots: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "automify",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "AI computer use for browser, CLI, and desktop in Node.js.",
|
|
5
5
|
"homepage": "https://aldovincenti.github.io/automify",
|
|
6
6
|
"bugs": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"SECURITY.md"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"postinstall": "node scripts/install-browser.js",
|
|
39
|
+
"postinstall": "node scripts/install-browser.js && node scripts/install-desktop-if-needed.js",
|
|
40
40
|
"install:desktop": "node scripts/install-desktop.js",
|
|
41
41
|
"docs:arguments": "node scripts/generate-argument-reference.js",
|
|
42
42
|
"test": "node --test test/*.test.js",
|
|
@@ -9,6 +9,7 @@ if (process.env.AUTOMIFY_SKIP_BROWSER_INSTALL === "1" || process.env.PLAYWRIGHT_
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const playwrightCli = join(dirname(require.resolve("playwright")), "cli.js");
|
|
12
|
+
console.log("Automify: installing Playwright browser...");
|
|
12
13
|
const result = spawnSync(process.execPath, [playwrightCli, "install", "chromium"], {
|
|
13
14
|
cwd: process.cwd(),
|
|
14
15
|
stdio: "inherit"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
desktopRuntimeDir,
|
|
9
|
+
desktopRuntimeIsInstalled,
|
|
10
|
+
desktopRuntimeKey,
|
|
11
|
+
findDesktopRuntimeManifests
|
|
12
|
+
} from "../src/lib/desktop-runtime.js";
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const root = resolve(__dirname, "..");
|
|
16
|
+
|
|
17
|
+
if (process.env.AUTOMIFY_SKIP_DESKTOP_INSTALL === "1" || process.env.AUTOMIFY_SKIP_DESKTOP_AUTO_REBUILD === "1") {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (desktopRuntimeIsInstalled()) {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const existingManifests = findDesktopRuntimeManifests();
|
|
26
|
+
const legacyNodeModulesRuntime = existsSync(join(root, "node_modules", "@nut-tree", "nut-js", "package.json"));
|
|
27
|
+
|
|
28
|
+
if (existingManifests.length === 0 && !legacyNodeModulesRuntime) {
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log("Automify desktop runtime was previously installed but is not compatible with this environment.");
|
|
33
|
+
console.log(`Rebuilding desktop runtime cache: ${desktopRuntimeKey()}`);
|
|
34
|
+
console.log(`Runtime directory: ${desktopRuntimeDir()}`);
|
|
35
|
+
|
|
36
|
+
const result = spawnSync(process.execPath, [join(__dirname, "install-desktop.js")], {
|
|
37
|
+
cwd: root,
|
|
38
|
+
env: process.env,
|
|
39
|
+
stdio: "inherit"
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
process.exit(result.status ?? 1);
|
|
@@ -4,6 +4,17 @@ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } fr
|
|
|
4
4
|
import { dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
DESKTOP_RUNTIME_MANIFEST,
|
|
9
|
+
desktopRuntimeDir,
|
|
10
|
+
desktopRuntimeIsInstalled,
|
|
11
|
+
desktopRuntimeKey,
|
|
12
|
+
desktopRuntimeManifest,
|
|
13
|
+
desktopRuntimeNodeModules,
|
|
14
|
+
desktopRuntimeRefs,
|
|
15
|
+
resetDesktopRuntimeInstallState
|
|
16
|
+
} from "../src/lib/desktop-runtime.js";
|
|
17
|
+
|
|
7
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
19
|
const root = resolve(__dirname, "..");
|
|
9
20
|
const buildRoot = process.env.AUTOMIFY_DESKTOP_BUILD_DIR
|
|
@@ -12,21 +23,32 @@ const buildRoot = process.env.AUTOMIFY_DESKTOP_BUILD_DIR
|
|
|
12
23
|
const nutSource = join(buildRoot, "nut.js");
|
|
13
24
|
const libnutSource = join(buildRoot, "libnut-core");
|
|
14
25
|
const macPermissionsSource = join(buildRoot, "node-mac-permissions");
|
|
15
|
-
const refs =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
const nodeModules = join(root, "node_modules");
|
|
26
|
+
const refs = desktopRuntimeRefs();
|
|
27
|
+
const runtimeDir = desktopRuntimeDir();
|
|
28
|
+
const runtimeNodeModules = desktopRuntimeNodeModules();
|
|
29
|
+
const nodeModules = runtimeNodeModules;
|
|
21
30
|
const nutScope = join(nodeModules, "@nut-tree");
|
|
22
31
|
const platformPackageName = `@nut-tree/libnut-${process.platform}`;
|
|
23
32
|
const platformPackageDir = join(nutScope, `libnut-${process.platform}`);
|
|
24
33
|
const macPermissionsPackageDir = join(nutScope, "node-mac-permissions");
|
|
34
|
+
const forceInstall = process.argv.slice(2).some((arg) => arg === "--force" || arg === "force");
|
|
25
35
|
|
|
26
36
|
const runtimeDependencies = ["jimp@1.6.1", "node-abort-controller@3.1.1", "clipboardy@2.3.0", "bindings@1.5.0"];
|
|
27
37
|
|
|
38
|
+
if (!forceInstall && desktopRuntimeIsInstalled()) {
|
|
39
|
+
console.log("Automify desktop runtime is already installed and compatible; skipping rebuild.");
|
|
40
|
+
console.log(`Runtime directory: ${runtimeDir}`);
|
|
41
|
+
console.log("Use `npx automify-install-desktop --force` to rebuild it anyway.");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
console.log("Building official nut.js from source.");
|
|
46
|
+
if (forceInstall) {
|
|
47
|
+
console.log("Force enabled: rebuilding the desktop runtime even if the cache is compatible.");
|
|
48
|
+
}
|
|
29
49
|
console.log(`Build directory: ${buildRoot}`);
|
|
50
|
+
console.log(`Runtime directory: ${runtimeDir}`);
|
|
51
|
+
console.log(`Runtime key: ${desktopRuntimeKey()}`);
|
|
30
52
|
console.log(`nut.js ref: ${refs.nut}`);
|
|
31
53
|
console.log(`libnut-core ref: ${refs.libnutCore}`);
|
|
32
54
|
if (process.platform === "darwin") {
|
|
@@ -36,6 +58,9 @@ if (process.platform === "darwin") {
|
|
|
36
58
|
checkBuildPrerequisites();
|
|
37
59
|
|
|
38
60
|
mkdirSync(buildRoot, { recursive: true });
|
|
61
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
62
|
+
resetDesktopRuntimeInstallState();
|
|
63
|
+
writeRuntimePackageJson();
|
|
39
64
|
cloneOrPull("https://github.com/nut-tree/libnut-core.git", libnutSource, refs.libnutCore);
|
|
40
65
|
cloneOrPull("https://github.com/nut-tree/nut.js.git", nutSource, refs.nut);
|
|
41
66
|
if (process.platform === "darwin") {
|
|
@@ -69,7 +94,7 @@ writeLibnutImportBridge();
|
|
|
69
94
|
runPnpm(["--filter", "@nut-tree/nut-js", "run", "compile"], { cwd: nutSource });
|
|
70
95
|
patchNutJimpCompatibility();
|
|
71
96
|
|
|
72
|
-
run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd:
|
|
97
|
+
run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd: runtimeDir });
|
|
73
98
|
|
|
74
99
|
installWorkspacePackage(join(nutSource, "core", "shared"), join(nutScope, "shared"));
|
|
75
100
|
installWorkspacePackage(join(nutSource, "core", "provider-interfaces"), join(nutScope, "provider-interfaces"));
|
|
@@ -81,9 +106,20 @@ if (process.platform === "darwin") {
|
|
|
81
106
|
installWorkspacePackage(macPermissionsSource, macPermissionsPackageDir);
|
|
82
107
|
}
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
writeRuntimeManifest();
|
|
110
|
+
run(
|
|
111
|
+
"node",
|
|
112
|
+
[
|
|
113
|
+
"-e",
|
|
114
|
+
`const { createRequire } = require("node:module");
|
|
115
|
+
const { join } = require("node:path");
|
|
116
|
+
const runtimeDir = process.argv[1];
|
|
117
|
+
createRequire(join(runtimeDir, "automify-desktop-runtime.cjs"))("@nut-tree/nut-js");
|
|
118
|
+
console.log("nut.js source build import ok");`,
|
|
119
|
+
runtimeDir
|
|
120
|
+
],
|
|
121
|
+
{ cwd: root }
|
|
122
|
+
);
|
|
87
123
|
|
|
88
124
|
function cloneOrPull(repo, target, ref) {
|
|
89
125
|
if (existsSync(join(target, ".git"))) {
|
|
@@ -160,22 +196,26 @@ function resolveCommand(command) {
|
|
|
160
196
|
function commandCandidates(command) {
|
|
161
197
|
const candidates = [];
|
|
162
198
|
|
|
199
|
+
const npmCli = npmCliCandidate(command);
|
|
200
|
+
if (npmCli) candidates.push(npmCli);
|
|
201
|
+
|
|
163
202
|
if (process.platform === "win32" && ["npm", "npx"].includes(command)) {
|
|
164
203
|
candidates.push({ command: `${command}.cmd`, args: [] });
|
|
165
204
|
}
|
|
166
205
|
|
|
167
|
-
const npmCli = npmCliCandidate(command);
|
|
168
|
-
if (npmCli) candidates.push(npmCli);
|
|
169
|
-
|
|
170
206
|
candidates.push({ command, args: [] });
|
|
171
207
|
return candidates;
|
|
172
208
|
}
|
|
173
209
|
|
|
174
210
|
function npmCliCandidate(command) {
|
|
175
|
-
if (!["npm", "npx"].includes(command)
|
|
176
|
-
|
|
211
|
+
if (!["npm", "npx"].includes(command)) return null;
|
|
212
|
+
|
|
213
|
+
const npmExecPath =
|
|
214
|
+
process.env.npm_execpath ?? join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
215
|
+
if (!existsSync(npmExecPath)) return null;
|
|
216
|
+
if (command === "npm") return { command: process.execPath, args: [npmExecPath] };
|
|
177
217
|
|
|
178
|
-
const npxExecPath = join(dirname(
|
|
218
|
+
const npxExecPath = join(dirname(npmExecPath), "npx-cli.js");
|
|
179
219
|
if (!existsSync(npxExecPath)) return null;
|
|
180
220
|
return { command: process.execPath, args: [npxExecPath] };
|
|
181
221
|
}
|
|
@@ -404,6 +444,35 @@ function installWorkspacePackage(source, target) {
|
|
|
404
444
|
});
|
|
405
445
|
}
|
|
406
446
|
|
|
447
|
+
function writeRuntimePackageJson() {
|
|
448
|
+
writeText(
|
|
449
|
+
join(runtimeDir, "package.json"),
|
|
450
|
+
`${JSON.stringify(
|
|
451
|
+
{
|
|
452
|
+
private: true,
|
|
453
|
+
name: "automify-desktop-runtime",
|
|
454
|
+
description: "Persistent native runtime cache for Automify local desktop support."
|
|
455
|
+
},
|
|
456
|
+
null,
|
|
457
|
+
2
|
|
458
|
+
)}\n`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function writeRuntimeManifest() {
|
|
463
|
+
writeText(
|
|
464
|
+
join(runtimeDir, DESKTOP_RUNTIME_MANIFEST),
|
|
465
|
+
`${JSON.stringify(
|
|
466
|
+
{
|
|
467
|
+
...desktopRuntimeManifest(),
|
|
468
|
+
createdAt: new Date().toISOString()
|
|
469
|
+
},
|
|
470
|
+
null,
|
|
471
|
+
2
|
|
472
|
+
)}\n`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
407
476
|
function run(command, args, options = {}) {
|
|
408
477
|
const result = runResolvedCommand(command, args, {
|
|
409
478
|
cwd: options.cwd ?? root,
|
package/src/index.d.ts
CHANGED
|
@@ -211,7 +211,8 @@ export interface LocalDesktopComputerOptions {
|
|
|
211
211
|
};
|
|
212
212
|
/**
|
|
213
213
|
* Linux only. Defaults to true when DISPLAY is missing. Starts Xvfb so the
|
|
214
|
-
* local nut.js desktop adapter can run on headless servers.
|
|
214
|
+
* local nut.js desktop adapter can run on headless servers. Linux local
|
|
215
|
+
* desktop capture is X11-based; Wayland sessions are not supported.
|
|
215
216
|
*/
|
|
216
217
|
virtualDisplay?:
|
|
217
218
|
| boolean
|
|
@@ -224,6 +225,11 @@ export interface LocalDesktopComputerOptions {
|
|
|
224
225
|
args?: string[];
|
|
225
226
|
startupMs?: number;
|
|
226
227
|
};
|
|
228
|
+
/**
|
|
229
|
+
* Linux only. Forces Xvfb even when DISPLAY is already set. Ignored on macOS
|
|
230
|
+
* and Windows. Use this when the host Linux session is Wayland or otherwise
|
|
231
|
+
* unsuitable for X11 screenshot capture.
|
|
232
|
+
*/
|
|
227
233
|
forceVirtualDisplay?: boolean;
|
|
228
234
|
display?:
|
|
229
235
|
| string
|
|
@@ -72,13 +72,13 @@ export const argumentReference = [
|
|
|
72
72
|
surface: "automify.localComputer()",
|
|
73
73
|
preferred: ["viewport", "mouse", "keyboard", "calibration", "virtualDisplay", "logFile"],
|
|
74
74
|
notes:
|
|
75
|
-
"Creates a local desktop runner and takes an exclusive cross-process lock until close(). Use logFile to capture automation and local desktop events."
|
|
75
|
+
"Creates a local desktop runner and takes an exclusive cross-process lock until close(). Linux local desktop requires X11/Xorg or Xvfb; Wayland is not supported. Use logFile to capture automation and local desktop events."
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
surface: "createLocalDesktopComputer()",
|
|
79
79
|
preferred: ["viewport", "mouse", "keyboard", "calibration", "virtualDisplay", "logFile"],
|
|
80
80
|
notes:
|
|
81
|
-
"Grouped mouse, keyboard, and calibration options are preferred over the older flat names. Use logFile to capture local desktop events. Local desktop control takes an exclusive cross-process lock until close()."
|
|
81
|
+
"Grouped mouse, keyboard, and calibration options are preferred over the older flat names. Linux local desktop requires X11/Xorg or Xvfb; Wayland is not supported. Use logFile to capture local desktop events. Local desktop control takes an exclusive cross-process lock until close()."
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
surface: "createDockerDesktopComputer()",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const DESKTOP_RUNTIME_PACKAGE = "@nut-tree/nut-js";
|
|
6
|
+
export const DESKTOP_RUNTIME_MANIFEST = "automify-desktop-runtime.json";
|
|
7
|
+
|
|
8
|
+
export function desktopRuntimeRefs(env = process.env) {
|
|
9
|
+
return {
|
|
10
|
+
nut: env.AUTOMIFY_DESKTOP_NUT_REF ?? "e413fa1f19a19c4631812e4e1eaf47aa732b5cbe",
|
|
11
|
+
libnutCore: env.AUTOMIFY_DESKTOP_LIBNUT_CORE_REF ?? "6bbe5825f1123bcd740117ca932c8b1c6cffb48c",
|
|
12
|
+
macPermissions: env.AUTOMIFY_DESKTOP_MAC_PERMISSIONS_REF ?? "6b6ddee993ddce5071b637e42f6ee1434150d0bb"
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function desktopRuntimeCompatibility(env = process.env) {
|
|
17
|
+
const refs = desktopRuntimeRefs(env);
|
|
18
|
+
return {
|
|
19
|
+
platform: process.platform,
|
|
20
|
+
arch: process.arch,
|
|
21
|
+
nodeAbi: process.versions.modules,
|
|
22
|
+
nutRef: refs.nut,
|
|
23
|
+
libnutCoreRef: refs.libnutCore,
|
|
24
|
+
macPermissionsRef: process.platform === "darwin" ? refs.macPermissions : undefined
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function desktopRuntimeKey(compatibility = desktopRuntimeCompatibility()) {
|
|
29
|
+
return [
|
|
30
|
+
compatibility.platform,
|
|
31
|
+
compatibility.arch,
|
|
32
|
+
`node-${compatibility.nodeAbi}`,
|
|
33
|
+
`nut-${shortRef(compatibility.nutRef)}`,
|
|
34
|
+
`libnut-${shortRef(compatibility.libnutCoreRef)}`,
|
|
35
|
+
compatibility.macPermissionsRef ? `macperms-${shortRef(compatibility.macPermissionsRef)}` : null
|
|
36
|
+
]
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join("-");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function defaultDesktopRuntimeRoot(env = process.env) {
|
|
42
|
+
if (env.AUTOMIFY_DESKTOP_RUNTIME_DIR) {
|
|
43
|
+
return resolve(env.AUTOMIFY_DESKTOP_RUNTIME_DIR);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.platform === "win32") {
|
|
47
|
+
return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "automify", "desktop-runtime");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (process.platform === "darwin") {
|
|
51
|
+
return join(homedir(), "Library", "Caches", "automify", "desktop-runtime");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return join(env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "automify", "desktop-runtime");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function desktopRuntimeDir(env = process.env) {
|
|
58
|
+
return join(defaultDesktopRuntimeRoot(env), desktopRuntimeKey(desktopRuntimeCompatibility(env)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function desktopRuntimeNodeModules(env = process.env) {
|
|
62
|
+
return join(desktopRuntimeDir(env), "node_modules");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function desktopRuntimePackageJsonPath(env = process.env) {
|
|
66
|
+
return join(desktopRuntimeNodeModules(env), "@nut-tree", "nut-js", "package.json");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function desktopRuntimeManifestPath(env = process.env) {
|
|
70
|
+
return join(desktopRuntimeDir(env), DESKTOP_RUNTIME_MANIFEST);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function desktopRuntimeManifest(env = process.env) {
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
package: DESKTOP_RUNTIME_PACKAGE,
|
|
77
|
+
runtimeDir: desktopRuntimeDir(env),
|
|
78
|
+
compatibility: desktopRuntimeCompatibility(env)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resetDesktopRuntimeInstallState(env = process.env) {
|
|
83
|
+
const runtimeDir = desktopRuntimeDir(env);
|
|
84
|
+
rmSync(join(desktopRuntimeNodeModules(env), "@nut-tree"), { recursive: true, force: true });
|
|
85
|
+
rmSync(join(runtimeDir, "package-lock.json"), { recursive: true, force: true });
|
|
86
|
+
rmSync(join(runtimeDir, "npm-shrinkwrap.json"), { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function readDesktopRuntimeManifest(env = process.env) {
|
|
90
|
+
const path = desktopRuntimeManifestPath(env);
|
|
91
|
+
if (!existsSync(path)) return null;
|
|
92
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function findDesktopRuntimeManifests(env = process.env) {
|
|
96
|
+
const root = defaultDesktopRuntimeRoot(env);
|
|
97
|
+
if (!existsSync(root)) return [];
|
|
98
|
+
|
|
99
|
+
return readdirSync(root, { withFileTypes: true })
|
|
100
|
+
.filter((entry) => entry.isDirectory())
|
|
101
|
+
.map((entry) => {
|
|
102
|
+
const runtimeDir = join(root, entry.name);
|
|
103
|
+
const manifestPath = join(runtimeDir, DESKTOP_RUNTIME_MANIFEST);
|
|
104
|
+
if (!existsSync(manifestPath)) return null;
|
|
105
|
+
try {
|
|
106
|
+
return {
|
|
107
|
+
runtimeDir,
|
|
108
|
+
manifestPath,
|
|
109
|
+
manifest: JSON.parse(readFileSync(manifestPath, "utf8"))
|
|
110
|
+
};
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function desktopRuntimeIsInstalled(env = process.env) {
|
|
119
|
+
const manifest = readDesktopRuntimeManifest(env);
|
|
120
|
+
return desktopRuntimeManifestMatches(manifest, env) && existsSync(desktopRuntimePackageJsonPath(env));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function desktopRuntimeManifestMatches(manifest, env = process.env) {
|
|
124
|
+
if (!manifest || manifest.version !== 1 || manifest.package !== DESKTOP_RUNTIME_PACKAGE) return false;
|
|
125
|
+
|
|
126
|
+
const expected = desktopRuntimeCompatibility(env);
|
|
127
|
+
const actual = manifest.compatibility ?? {};
|
|
128
|
+
return (
|
|
129
|
+
actual.platform === expected.platform &&
|
|
130
|
+
actual.arch === expected.arch &&
|
|
131
|
+
actual.nodeAbi === expected.nodeAbi &&
|
|
132
|
+
actual.nutRef === expected.nutRef &&
|
|
133
|
+
actual.libnutCoreRef === expected.libnutCoreRef &&
|
|
134
|
+
actual.macPermissionsRef === expected.macPermissionsRef
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function shortRef(ref) {
|
|
139
|
+
return String(ref ?? "unknown")
|
|
140
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_")
|
|
141
|
+
.slice(0, 12);
|
|
142
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
1
3
|
import { readFile, unlink } from "node:fs/promises";
|
|
2
4
|
import { execFile, spawn } from "node:child_process";
|
|
3
5
|
import { tmpdir } from "node:os";
|
|
@@ -8,6 +10,12 @@ import { promisify } from "node:util";
|
|
|
8
10
|
import { AutomifyError } from "./errors.js";
|
|
9
11
|
import { acquireAdapterLock } from "./adapter-locks.js";
|
|
10
12
|
import { assertKnownOptions, normalizeLogFile, writeDebugLogFile } from "./runtime.js";
|
|
13
|
+
import {
|
|
14
|
+
DESKTOP_RUNTIME_PACKAGE,
|
|
15
|
+
desktopRuntimeDir,
|
|
16
|
+
desktopRuntimeManifestMatches,
|
|
17
|
+
desktopRuntimeManifestPath
|
|
18
|
+
} from "./desktop-runtime.js";
|
|
11
19
|
|
|
12
20
|
const execFileAsync = promisify(execFile);
|
|
13
21
|
const LOCAL_DESKTOP_OPTION_KEYS = new Set([
|
|
@@ -128,6 +136,7 @@ const DEFAULT_DESKTOP_INSTRUCTIONS = [
|
|
|
128
136
|
"You are controlling a native desktop through screenshots and mouse/keyboard actions.",
|
|
129
137
|
"Orient from the screenshot first: identify the active app, visible window, focused field, current page, and the specific target required by the task before acting.",
|
|
130
138
|
"Use deterministic entry points. For a website or web app, use the browser address bar only when a browser is clearly focused; otherwise open the browser through a visible Dock/app icon or OS search/launcher, then use the address bar. For app content, use visible in-app search, filters, or navigation controls.",
|
|
139
|
+
"Before using a terminal to run a shell command, confirm the terminal is idle and showing a prompt. If the visible terminal is occupied by a running foreground process, such as the Node.js script that started this session, do not type commands into it; use the OS launcher/Dock/search or a separate idle terminal session instead.",
|
|
131
140
|
"Do not open or use Command+Tab, Alt+Tab, Mission Control, or other cyclic app/window switchers unless the task explicitly asks to switch to the previous app. Cyclic switching is unreliable because the open-app order is unknown.",
|
|
132
141
|
"Do not click as a probe. Click only when the screenshot shows a specific visible target and the purpose of that click is clear from the task or current UI. Prefer named controls, fields, menu items, visible app icons, and address/search fields over unlabeled areas.",
|
|
133
142
|
"If the target is not visible, choose a deterministic recovery path: direct URL, OS search/launcher, in-app search, visible navigation, or a screenshot/wait when loading is visible. Do not repeat nearly identical clicks after no visible change.",
|
|
@@ -448,16 +457,39 @@ async function saveNutImageObject(nut, image, options = {}) {
|
|
|
448
457
|
}
|
|
449
458
|
|
|
450
459
|
async function importNut() {
|
|
460
|
+
let runtimeError;
|
|
451
461
|
try {
|
|
452
|
-
|
|
462
|
+
const runtimeNut = importNutFromDesktopRuntime();
|
|
463
|
+
if (runtimeNut) return runtimeNut;
|
|
453
464
|
} catch (error) {
|
|
465
|
+
runtimeError = error;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
return await import(DESKTOP_RUNTIME_PACKAGE);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
const runtimeSuffix = runtimeError ? ` Cached desktop runtime import failed: ${runtimeError.message}` : "";
|
|
454
472
|
throw new AutomifyError(
|
|
455
|
-
`createLocalDesktopComputer requires the local desktop adapter dependency built from source. ${localDesktopInstallHelp()} After the OS prerequisites are available, run: npx automify-install-desktop`,
|
|
473
|
+
`createLocalDesktopComputer requires the local desktop adapter dependency built from source. ${localDesktopInstallHelp()} After the OS prerequisites are available, run: npx automify-install-desktop.${runtimeSuffix}`,
|
|
456
474
|
{ cause: error }
|
|
457
475
|
);
|
|
458
476
|
}
|
|
459
477
|
}
|
|
460
478
|
|
|
479
|
+
function importNutFromDesktopRuntime() {
|
|
480
|
+
const manifestPath = desktopRuntimeManifestPath();
|
|
481
|
+
let manifest;
|
|
482
|
+
try {
|
|
483
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
if (!desktopRuntimeManifestMatches(manifest)) return null;
|
|
488
|
+
|
|
489
|
+
const runtimeRequire = createRequire(join(desktopRuntimeDir(), "automify-desktop-runtime.cjs"));
|
|
490
|
+
return runtimeRequire(DESKTOP_RUNTIME_PACKAGE);
|
|
491
|
+
}
|
|
492
|
+
|
|
461
493
|
function normalizeLocalDesktopEnvironment(environment) {
|
|
462
494
|
if (environment == null) return undefined;
|
|
463
495
|
if (typeof environment !== "string" || environment.trim() === "") {
|