automify 0.1.9 → 0.1.10

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 CHANGED
@@ -120,12 +120,12 @@ const browser = await automify.browser({
120
120
  });
121
121
 
122
122
  try {
123
- const run = await browser.do("Extract the support email.", {
123
+ const run = await browser.do("Summarize what you see on the page.", {
124
124
  // Optional: structured result shape.
125
- output: jsonOutput("support_contact", { email: "string" })
125
+ output: jsonOutput("page_summary", { title: "string", summary: "string" })
126
126
  });
127
127
 
128
- console.log(run.parsed.email);
128
+ console.log(run.parsed.title, run.parsed.summary);
129
129
  } finally {
130
130
  await browser.close();
131
131
  }
@@ -208,6 +208,9 @@ winget install --id Microsoft.VisualStudio.2022.BuildTools --exact --override "-
208
208
  winget install --id Kitware.CMake --exact --source winget
209
209
 
210
210
  # macOS: Xcode Command Line Tools plus CMake on PATH.
211
+ # If Homebrew is not installed, install it first:
212
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
213
+
211
214
  xcode-select --install
212
215
  brew install cmake
213
216
 
@@ -221,7 +224,9 @@ sudo dnf install -y gcc-c++ make cmake libXtst-devel libpng-devel
221
224
  sudo pacman -S --needed base-devel cmake libxtst libpng
222
225
  ```
223
226
 
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.
227
+ 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, 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.
228
+
229
+ `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 a later `npm install` or `npm update` detects that a previously installed desktop runtime no longer matches the current platform, CPU architecture, Node ABI, or pinned nut.js/libnut revisions, 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
230
 
226
231
  ```js
227
232
  import { initAutomify } from "automify";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automify",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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,15 @@ 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
+ desktopRuntimeKey,
11
+ desktopRuntimeManifest,
12
+ desktopRuntimeNodeModules,
13
+ desktopRuntimeRefs
14
+ } from "../src/lib/desktop-runtime.js";
15
+
7
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
17
  const root = resolve(__dirname, "..");
9
18
  const buildRoot = process.env.AUTOMIFY_DESKTOP_BUILD_DIR
@@ -12,12 +21,10 @@ const buildRoot = process.env.AUTOMIFY_DESKTOP_BUILD_DIR
12
21
  const nutSource = join(buildRoot, "nut.js");
13
22
  const libnutSource = join(buildRoot, "libnut-core");
14
23
  const macPermissionsSource = join(buildRoot, "node-mac-permissions");
15
- const refs = {
16
- nut: process.env.AUTOMIFY_DESKTOP_NUT_REF ?? "e413fa1f19a19c4631812e4e1eaf47aa732b5cbe",
17
- libnutCore: process.env.AUTOMIFY_DESKTOP_LIBNUT_CORE_REF ?? "6bbe5825f1123bcd740117ca932c8b1c6cffb48c",
18
- macPermissions: process.env.AUTOMIFY_DESKTOP_MAC_PERMISSIONS_REF ?? "6b6ddee993ddce5071b637e42f6ee1434150d0bb"
19
- };
20
- const nodeModules = join(root, "node_modules");
24
+ const refs = desktopRuntimeRefs();
25
+ const runtimeDir = desktopRuntimeDir();
26
+ const runtimeNodeModules = desktopRuntimeNodeModules();
27
+ const nodeModules = runtimeNodeModules;
21
28
  const nutScope = join(nodeModules, "@nut-tree");
22
29
  const platformPackageName = `@nut-tree/libnut-${process.platform}`;
23
30
  const platformPackageDir = join(nutScope, `libnut-${process.platform}`);
@@ -27,6 +34,8 @@ const runtimeDependencies = ["jimp@1.6.1", "node-abort-controller@3.1.1", "clipb
27
34
 
28
35
  console.log("Building official nut.js from source.");
29
36
  console.log(`Build directory: ${buildRoot}`);
37
+ console.log(`Runtime directory: ${runtimeDir}`);
38
+ console.log(`Runtime key: ${desktopRuntimeKey()}`);
30
39
  console.log(`nut.js ref: ${refs.nut}`);
31
40
  console.log(`libnut-core ref: ${refs.libnutCore}`);
32
41
  if (process.platform === "darwin") {
@@ -36,6 +45,8 @@ if (process.platform === "darwin") {
36
45
  checkBuildPrerequisites();
37
46
 
38
47
  mkdirSync(buildRoot, { recursive: true });
48
+ mkdirSync(runtimeDir, { recursive: true });
49
+ writeRuntimePackageJson();
39
50
  cloneOrPull("https://github.com/nut-tree/libnut-core.git", libnutSource, refs.libnutCore);
40
51
  cloneOrPull("https://github.com/nut-tree/nut.js.git", nutSource, refs.nut);
41
52
  if (process.platform === "darwin") {
@@ -69,7 +80,7 @@ writeLibnutImportBridge();
69
80
  runPnpm(["--filter", "@nut-tree/nut-js", "run", "compile"], { cwd: nutSource });
70
81
  patchNutJimpCompatibility();
71
82
 
72
- run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd: root });
83
+ run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd: runtimeDir });
73
84
 
74
85
  installWorkspacePackage(join(nutSource, "core", "shared"), join(nutScope, "shared"));
75
86
  installWorkspacePackage(join(nutSource, "core", "provider-interfaces"), join(nutScope, "provider-interfaces"));
@@ -81,9 +92,20 @@ if (process.platform === "darwin") {
81
92
  installWorkspacePackage(macPermissionsSource, macPermissionsPackageDir);
82
93
  }
83
94
 
84
- run("node", ["-e", "import('@nut-tree/nut-js').then(() => console.log('nut.js source build import ok'))"], {
85
- cwd: root
86
- });
95
+ writeRuntimeManifest();
96
+ run(
97
+ "node",
98
+ [
99
+ "-e",
100
+ `const { createRequire } = require("node:module");
101
+ const { join } = require("node:path");
102
+ const runtimeDir = process.argv[1];
103
+ createRequire(join(runtimeDir, "automify-desktop-runtime.cjs"))("@nut-tree/nut-js");
104
+ console.log("nut.js source build import ok");`,
105
+ runtimeDir
106
+ ],
107
+ { cwd: root }
108
+ );
87
109
 
88
110
  function cloneOrPull(repo, target, ref) {
89
111
  if (existsSync(join(target, ".git"))) {
@@ -160,22 +182,26 @@ function resolveCommand(command) {
160
182
  function commandCandidates(command) {
161
183
  const candidates = [];
162
184
 
185
+ const npmCli = npmCliCandidate(command);
186
+ if (npmCli) candidates.push(npmCli);
187
+
163
188
  if (process.platform === "win32" && ["npm", "npx"].includes(command)) {
164
189
  candidates.push({ command: `${command}.cmd`, args: [] });
165
190
  }
166
191
 
167
- const npmCli = npmCliCandidate(command);
168
- if (npmCli) candidates.push(npmCli);
169
-
170
192
  candidates.push({ command, args: [] });
171
193
  return candidates;
172
194
  }
173
195
 
174
196
  function npmCliCandidate(command) {
175
- if (!["npm", "npx"].includes(command) || !process.env.npm_execpath) return null;
176
- if (command === "npm") return { command: process.execPath, args: [process.env.npm_execpath] };
197
+ if (!["npm", "npx"].includes(command)) return null;
198
+
199
+ const npmExecPath =
200
+ process.env.npm_execpath ?? join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
201
+ if (!existsSync(npmExecPath)) return null;
202
+ if (command === "npm") return { command: process.execPath, args: [npmExecPath] };
177
203
 
178
- const npxExecPath = join(dirname(process.env.npm_execpath), "npx-cli.js");
204
+ const npxExecPath = join(dirname(npmExecPath), "npx-cli.js");
179
205
  if (!existsSync(npxExecPath)) return null;
180
206
  return { command: process.execPath, args: [npxExecPath] };
181
207
  }
@@ -404,6 +430,35 @@ function installWorkspacePackage(source, target) {
404
430
  });
405
431
  }
406
432
 
433
+ function writeRuntimePackageJson() {
434
+ writeText(
435
+ join(runtimeDir, "package.json"),
436
+ `${JSON.stringify(
437
+ {
438
+ private: true,
439
+ name: "automify-desktop-runtime",
440
+ description: "Persistent native runtime cache for Automify local desktop support."
441
+ },
442
+ null,
443
+ 2
444
+ )}\n`
445
+ );
446
+ }
447
+
448
+ function writeRuntimeManifest() {
449
+ writeText(
450
+ join(runtimeDir, DESKTOP_RUNTIME_MANIFEST),
451
+ `${JSON.stringify(
452
+ {
453
+ ...desktopRuntimeManifest(),
454
+ createdAt: new Date().toISOString()
455
+ },
456
+ null,
457
+ 2
458
+ )}\n`
459
+ );
460
+ }
461
+
407
462
  function run(command, args, options = {}) {
408
463
  const result = runResolvedCommand(command, args, {
409
464
  cwd: options.cwd ?? root,
@@ -0,0 +1,135 @@
1
+ import { existsSync, readdirSync, readFileSync } 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 readDesktopRuntimeManifest(env = process.env) {
83
+ const path = desktopRuntimeManifestPath(env);
84
+ if (!existsSync(path)) return null;
85
+ return JSON.parse(readFileSync(path, "utf8"));
86
+ }
87
+
88
+ export function findDesktopRuntimeManifests(env = process.env) {
89
+ const root = defaultDesktopRuntimeRoot(env);
90
+ if (!existsSync(root)) return [];
91
+
92
+ return readdirSync(root, { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory())
94
+ .map((entry) => {
95
+ const runtimeDir = join(root, entry.name);
96
+ const manifestPath = join(runtimeDir, DESKTOP_RUNTIME_MANIFEST);
97
+ if (!existsSync(manifestPath)) return null;
98
+ try {
99
+ return {
100
+ runtimeDir,
101
+ manifestPath,
102
+ manifest: JSON.parse(readFileSync(manifestPath, "utf8"))
103
+ };
104
+ } catch {
105
+ return null;
106
+ }
107
+ })
108
+ .filter(Boolean);
109
+ }
110
+
111
+ export function desktopRuntimeIsInstalled(env = process.env) {
112
+ const manifest = readDesktopRuntimeManifest(env);
113
+ return desktopRuntimeManifestMatches(manifest, env) && existsSync(desktopRuntimePackageJsonPath(env));
114
+ }
115
+
116
+ export function desktopRuntimeManifestMatches(manifest, env = process.env) {
117
+ if (!manifest || manifest.version !== 1 || manifest.package !== DESKTOP_RUNTIME_PACKAGE) return false;
118
+
119
+ const expected = desktopRuntimeCompatibility(env);
120
+ const actual = manifest.compatibility ?? {};
121
+ return (
122
+ actual.platform === expected.platform &&
123
+ actual.arch === expected.arch &&
124
+ actual.nodeAbi === expected.nodeAbi &&
125
+ actual.nutRef === expected.nutRef &&
126
+ actual.libnutCoreRef === expected.libnutCoreRef &&
127
+ actual.macPermissionsRef === expected.macPermissionsRef
128
+ );
129
+ }
130
+
131
+ function shortRef(ref) {
132
+ return String(ref ?? "unknown")
133
+ .replace(/[^a-zA-Z0-9._-]/g, "_")
134
+ .slice(0, 12);
135
+ }
@@ -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([
@@ -448,16 +456,39 @@ async function saveNutImageObject(nut, image, options = {}) {
448
456
  }
449
457
 
450
458
  async function importNut() {
459
+ let runtimeError;
451
460
  try {
452
- return await import("@nut-tree/nut-js");
461
+ const runtimeNut = importNutFromDesktopRuntime();
462
+ if (runtimeNut) return runtimeNut;
453
463
  } catch (error) {
464
+ runtimeError = error;
465
+ }
466
+
467
+ try {
468
+ return await import(DESKTOP_RUNTIME_PACKAGE);
469
+ } catch (error) {
470
+ const runtimeSuffix = runtimeError ? ` Cached desktop runtime import failed: ${runtimeError.message}` : "";
454
471
  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`,
472
+ `createLocalDesktopComputer requires the local desktop adapter dependency built from source. ${localDesktopInstallHelp()} After the OS prerequisites are available, run: npx automify-install-desktop.${runtimeSuffix}`,
456
473
  { cause: error }
457
474
  );
458
475
  }
459
476
  }
460
477
 
478
+ function importNutFromDesktopRuntime() {
479
+ const manifestPath = desktopRuntimeManifestPath();
480
+ let manifest;
481
+ try {
482
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
483
+ } catch {
484
+ return null;
485
+ }
486
+ if (!desktopRuntimeManifestMatches(manifest)) return null;
487
+
488
+ const runtimeRequire = createRequire(join(desktopRuntimeDir(), "automify-desktop-runtime.cjs"));
489
+ return runtimeRequire(DESKTOP_RUNTIME_PACKAGE);
490
+ }
491
+
461
492
  function normalizeLocalDesktopEnvironment(environment) {
462
493
  if (environment == null) return undefined;
463
494
  if (typeof environment !== "string" || environment.trim() === "") {