automify 0.1.4 → 0.1.7

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
@@ -10,11 +10,11 @@
10
10
 
11
11
  Computer use surfaces:
12
12
 
13
- | Surface | Factory | What it does |
14
- | -------------- | --------------------------- | ---------------------------------------------------------- |
15
- | Browser | `automify.browser()` | Playwright browser automation with screenshots and actions |
16
- | Desktop | `automify.localComputer()` | Native desktop computer use on the current machine |
17
- | Docker desktop | `automify.dockerComputer()` | Containerized Linux desktop automation with screenshots |
13
+ | Surface | Factory | Controlled environment |
14
+ | -------------- | --------------------------- | ----------------------------------------------------------- |
15
+ | Browser | `automify.browser()` | Playwright browser with screenshots and actions |
16
+ | Desktop | `automify.localComputer()` | Native desktop on the current macOS, Windows, or Linux host |
17
+ | Docker desktop | `automify.dockerComputer()` | Linux desktop inside a Docker container |
18
18
 
19
19
  Command use surfaces:
20
20
 
@@ -198,12 +198,30 @@ try {
198
198
 
199
199
  ### Desktop Computer Use
200
200
 
201
- Local desktop computer use is optional. Install it with the command below; it may take a while because it compiles native desktop dependencies. When you use the local desktop adapter, your OS may ask for permission to control the desktop.
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.
202
202
 
203
- ```bash
204
- npx automify-install-desktop
203
+ Before running `npx automify-install-desktop`, install the native build tools for your OS:
204
+
205
+ ```sh
206
+ # Windows: Visual Studio C++ Build Tools plus CMake on PATH.
207
+ winget install Kitware.CMake
208
+
209
+ # macOS: Xcode Command Line Tools plus CMake on PATH.
210
+ xcode-select --install
211
+ brew install cmake
212
+
213
+ # Debian/Ubuntu Linux.
214
+ sudo apt-get install -y build-essential cmake libxtst-dev libpng++-dev
215
+
216
+ # Fedora Linux.
217
+ sudo dnf install -y gcc-c++ make cmake libXtst-devel libpng-devel
218
+
219
+ # Arch Linux.
220
+ sudo pacman -S --needed base-devel cmake libxtst libpng
205
221
  ```
206
222
 
223
+ 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.
224
+
207
225
  ```js
208
226
  import { initAutomify } from "automify";
209
227
 
@@ -215,6 +233,7 @@ const automify = initAutomify({
215
233
  }
216
234
  });
217
235
 
236
+ // Reminder: local desktop support requires `npx automify-install-desktop` once for this project.
218
237
  const desktop = await automify.localComputer();
219
238
 
220
239
  try {
@@ -226,7 +245,7 @@ try {
226
245
  }
227
246
  ```
228
247
 
229
- For isolated Linux desktop computer use, use Docker:
248
+ For isolated Linux desktop computer use, use Docker. `dockerComputer()` can run from a macOS, Windows, or Linux host with Docker, but the desktop it controls inside the container is Linux. Docker desktop does not use `automify-install-desktop`; it needs Docker and an initial app command:
230
249
 
231
250
  ```js
232
251
  import { initAutomify } from "automify";
@@ -299,6 +318,57 @@ const run = await browser.do("Create the lead from data and return the saved rec
299
318
  - `shared` and `sharedFiles` expose files inside Docker CLI or Docker desktop runs.
300
319
  - `jsonOutput()` requests structured JSON and makes parsed output available as `run.parsed`.
301
320
 
321
+ For arrays of objects, the most ergonomic shape is usually an object with a named array property:
322
+
323
+ ```js
324
+ const run = await browser.do("Extract the products.", {
325
+ output: jsonOutput("product_list", {
326
+ products: {
327
+ type: "array",
328
+ items: {
329
+ type: "object",
330
+ properties: {
331
+ sku: { type: "string" },
332
+ title: { type: "string" },
333
+ price: { type: "number" }
334
+ },
335
+ required: ["sku", "title", "price"],
336
+ additionalProperties: false
337
+ }
338
+ }
339
+ })
340
+ });
341
+
342
+ console.log(run.parsed.products);
343
+ ```
344
+
345
+ If you need `run.parsed` itself to be an array, pass the lower-level `json_schema` output format directly:
346
+
347
+ ```js
348
+ const run = await browser.do("Extract the products.", {
349
+ output: {
350
+ type: "json_schema",
351
+ name: "products",
352
+ strict: true,
353
+ schema: {
354
+ type: "array",
355
+ items: {
356
+ type: "object",
357
+ properties: {
358
+ sku: { type: "string" },
359
+ title: { type: "string" },
360
+ price: { type: "number" }
361
+ },
362
+ required: ["sku", "title", "price"],
363
+ additionalProperties: false
364
+ }
365
+ }
366
+ }
367
+ });
368
+
369
+ console.log(run.parsed[0].sku);
370
+ ```
371
+
302
372
  ### Optional Zod Output
303
373
 
304
374
  If your app already uses Zod 4, you can use the optional Zod adapter instead of writing compact shapes or JSON Schema by hand. Install `zod` in your app and import from the dedicated `automify/zod` subpath:
@@ -320,6 +390,26 @@ const run = await browser.do("Create the lead and return it.", {
320
390
  console.log(run.parsed.id);
321
391
  ```
322
392
 
393
+ Zod works well for array outputs too:
394
+
395
+ ```js
396
+ const ProductList = z.object({
397
+ products: z.array(
398
+ z.object({
399
+ sku: z.string(),
400
+ title: z.string(),
401
+ price: z.number()
402
+ })
403
+ )
404
+ });
405
+
406
+ const run = await browser.do("Extract the products.", {
407
+ output: zodOutput("product_list", ProductList)
408
+ });
409
+
410
+ console.log(run.parsed.products);
411
+ ```
412
+
323
413
  `zodOutput()` is not part of the main `automify` import on purpose. Zod is an optional peer dependency, so projects that only use `jsonOutput()` do not need to install it.
324
414
 
325
415
  At runtime, `zodOutput()` does two things:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automify",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
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": {
@@ -44,7 +44,7 @@ mkdirSync(buildRoot, { recursive: true });
44
44
  cloneOrPull("https://github.com/nut-tree/libnut-core.git", libnutSource, refs.libnutCore);
45
45
  cloneOrPull("https://github.com/nut-tree/nut.js.git", nutSource, refs.nut);
46
46
  if (process.platform === "darwin") {
47
- cloneOrPull("https://github.com/nut-tree/node-mac-permissions.git", macPermissionsSource, refs.macPermissions);
47
+ cloneOrPull("https://github.com/nut-tree/node-mac-permissions.git", macPermissionsSource, refs.macPermissions);
48
48
  }
49
49
 
50
50
  patchNutWorkspace();
@@ -66,11 +66,13 @@ patchPlatformLibnutDependency();
66
66
 
67
67
  runPnpm(["install"], { cwd: nutSource });
68
68
  runPnpm(["--filter", "@nut-tree/shared", "run", "compile"], { cwd: nutSource });
69
+ patchNutJimpCompatibility();
69
70
  runPnpm(["--filter", "@nut-tree/provider-interfaces", "run", "compile"], { cwd: nutSource });
70
71
  runPnpm(["--filter", "@nut-tree/default-clipboard-provider", "run", "compile"], { cwd: nutSource });
71
72
  runPnpm(["--filter", "@nut-tree/libnut", "run", "compile"], { cwd: nutSource });
72
73
  writeLibnutImportBridge();
73
74
  runPnpm(["--filter", "@nut-tree/nut-js", "run", "compile"], { cwd: nutSource });
75
+ patchNutJimpCompatibility();
74
76
 
75
77
  run("npm", ["install", "--no-save", ...runtimeDependencies], { cwd: root });
76
78
 
@@ -139,7 +141,7 @@ function checkBuildPrerequisites() {
139
141
  }
140
142
 
141
143
  function commandExists(command) {
142
- const result = spawnSync(command, ["--version"], {
144
+ const result = spawnSync(resolveCommand(command), ["--version"], {
143
145
  cwd: root,
144
146
  stdio: "ignore"
145
147
  });
@@ -250,6 +252,77 @@ export { libnut };
250
252
  );
251
253
  }
252
254
 
255
+ function patchNutJimpCompatibility() {
256
+ const sharedImageToJimpPath = join(
257
+ nutSource,
258
+ "core",
259
+ "shared",
260
+ "dist",
261
+ "lib",
262
+ "functions",
263
+ "imageToJimp.function.js"
264
+ );
265
+ if (existsSync(sharedImageToJimpPath)) {
266
+ writeText(
267
+ sharedImageToJimpPath,
268
+ `"use strict";
269
+ Object.defineProperty(exports, "__esModule", { value: true });
270
+ exports.imageToJimp = void 0;
271
+ const jimp_1 = require("jimp");
272
+ const colormode_enum_1 = require("../enums/colormode.enum");
273
+ function imageToJimp(image) {
274
+ const jimpImage = new jimp_1.Jimp({
275
+ data: image.data,
276
+ width: image.width,
277
+ height: image.height
278
+ });
279
+ if (image.colorMode === colormode_enum_1.ColorMode.BGR) {
280
+ jimpImage.scan(0, 0, jimpImage.bitmap.width, jimpImage.bitmap.height, function (_, __, idx) {
281
+ const red = this.bitmap.data[idx];
282
+ this.bitmap.data[idx] = this.bitmap.data[idx + 2];
283
+ this.bitmap.data[idx + 2] = red;
284
+ });
285
+ }
286
+ return jimpImage;
287
+ }
288
+ exports.imageToJimp = imageToJimp;
289
+ `
290
+ );
291
+ }
292
+
293
+ const jimpImageWriterPath = join(
294
+ nutSource,
295
+ "core",
296
+ "nut.js",
297
+ "dist",
298
+ "lib",
299
+ "provider",
300
+ "io",
301
+ "jimp-image-writer.class.js"
302
+ );
303
+ if (existsSync(jimpImageWriterPath)) {
304
+ writeText(
305
+ jimpImageWriterPath,
306
+ `"use strict";
307
+ Object.defineProperty(exports, "__esModule", { value: true });
308
+ const shared_1 = require("@nut-tree/shared");
309
+ class default_1 {
310
+ store(parameters) {
311
+ return new Promise((resolve, reject) => {
312
+ const jimpImage = (0, shared_1.imageToJimp)(parameters.image);
313
+ jimpImage
314
+ .write(parameters.path)
315
+ .then((_) => resolve())
316
+ .catch((err) => reject(err));
317
+ });
318
+ }
319
+ }
320
+ exports.default = default_1;
321
+ `
322
+ );
323
+ }
324
+ }
325
+
253
326
  function installWorkspacePackage(source, target) {
254
327
  rmSync(target, { recursive: true, force: true });
255
328
  mkdirSync(dirname(target), { recursive: true });
@@ -260,7 +333,7 @@ function installWorkspacePackage(source, target) {
260
333
  }
261
334
 
262
335
  function run(command, args, options = {}) {
263
- const executable = process.platform === "win32" && ["npm", "npx"].includes(command) ? `${command}.cmd` : command;
336
+ const executable = resolveCommand(command);
264
337
  const result = spawnSync(executable, args, {
265
338
  cwd: options.cwd ?? root,
266
339
  env: options.env ?? process.env,
@@ -294,6 +367,10 @@ function run(command, args, options = {}) {
294
367
  return commandResult;
295
368
  }
296
369
 
370
+ function resolveCommand(command) {
371
+ return process.platform === "win32" && ["npm", "npx"].includes(command) ? `${command}.cmd` : command;
372
+ }
373
+
297
374
  function runPnpm(args, options = {}) {
298
375
  run("npx", ["--yes", "pnpm@8.15.2", ...args], options);
299
376
  }
@@ -83,6 +83,16 @@ const LOCAL_CALIBRATION_KEYS = new Set([
83
83
  "required"
84
84
  ]);
85
85
  const LOCAL_VIRTUAL_DISPLAY_KEYS = new Set(["display", "width", "height", "depth", "command", "args", "startupMs"]);
86
+ const LOCAL_DESKTOP_ENVIRONMENTS = new Set(["mac", "windows", "ubuntu", "linux"]);
87
+ const LOCAL_DESKTOP_ENVIRONMENT_ALIASES = new Map([
88
+ ["macos", "mac"],
89
+ ["darwin", "mac"],
90
+ ["win32", "windows"],
91
+ ["win", "windows"],
92
+ ["debian", "linux"],
93
+ ["fedora", "linux"],
94
+ ["arch", "linux"]
95
+ ]);
86
96
 
87
97
  const KEY_ALIASES = new Map([
88
98
  ["alt", "LeftAlt"],
@@ -327,6 +337,7 @@ function normalizeLocalDesktopOptions(options = {}) {
327
337
  const keyboard = options.keyboard ?? {};
328
338
  const calibration = options.calibration ?? {};
329
339
  const virtualDisplay = typeof options.virtualDisplay === "object" ? options.virtualDisplay : {};
340
+ const environment = normalizeLocalDesktopEnvironment(options.environment);
330
341
  return {
331
342
  ...options,
332
343
  debug: options.debug ?? false,
@@ -334,6 +345,7 @@ function normalizeLocalDesktopOptions(options = {}) {
334
345
  display: typeof options.display === "object" ? undefined : options.display,
335
346
  displayWidth: options.displayWidth ?? viewport.width ?? display.width,
336
347
  displayHeight: options.displayHeight ?? viewport.height ?? display.height,
348
+ environment,
337
349
  pixelScale: options.pixelScale ?? display.pixelScale ?? calibration.pixelScale,
338
350
  mouseScaleX: options.mouseScaleX ?? mouse.scaleX ?? calibration.mouseScaleX,
339
351
  mouseScaleY: options.mouseScaleY ?? mouse.scaleY ?? calibration.mouseScaleY,
@@ -366,7 +378,15 @@ async function captureLocalDesktopScreenshotToFile(nut, options = {}) {
366
378
  const filename = `automify-nut-${Date.now()}-${Math.random().toString(16).slice(2)}`;
367
379
  const directory = options.screenshotPath ? undefined : tmpdir();
368
380
  const requestedPath = options.screenshotPath;
369
- const capturedPath = await nut.screen.capture(requestedPath ?? filename, nut.FileType?.PNG, directory);
381
+ let capturedPath;
382
+ try {
383
+ capturedPath = await nut.screen.capture(requestedPath ?? filename, nut.FileType?.PNG, directory);
384
+ } catch (error) {
385
+ throw new AutomifyError(
386
+ `local desktop screenshot capture failed. ${localDesktopRuntimeHelp(options.environment)}${errorMessageSuffix(error)}`,
387
+ { cause: error }
388
+ );
389
+ }
370
390
 
371
391
  if (isByteLike(capturedPath)) return capturedPath;
372
392
  if (isImageObject(capturedPath)) return capturedPath;
@@ -432,12 +452,57 @@ async function importNut() {
432
452
  return await import("@nut-tree/nut-js");
433
453
  } catch (error) {
434
454
  throw new AutomifyError(
435
- "createLocalDesktopComputer requires the local desktop adapter dependency built from source. Install it with: npx automify-install-desktop",
455
+ `createLocalDesktopComputer requires the local desktop adapter dependency built from source. ${localDesktopInstallHelp()} After the OS prerequisites are available, run: npx automify-install-desktop`,
436
456
  { cause: error }
437
457
  );
438
458
  }
439
459
  }
440
460
 
461
+ function normalizeLocalDesktopEnvironment(environment) {
462
+ if (environment == null) return undefined;
463
+ if (typeof environment !== "string" || environment.trim() === "") {
464
+ throw new AutomifyError(
465
+ 'local desktop environment must be "mac", "windows", or "linux". "ubuntu" is also accepted for compatibility.'
466
+ );
467
+ }
468
+
469
+ const normalized = environment.trim().toLowerCase();
470
+ const canonical = LOCAL_DESKTOP_ENVIRONMENT_ALIASES.get(normalized) ?? normalized;
471
+ if (LOCAL_DESKTOP_ENVIRONMENTS.has(canonical)) return canonical;
472
+
473
+ throw new AutomifyError(
474
+ `Unsupported local desktop environment ${JSON.stringify(environment)}. Use "mac" for macOS, "windows" for Windows, or "linux" for Linux. "ubuntu" is also accepted for compatibility. Use instructions for OS-specific guidance, not a custom environment value.`
475
+ );
476
+ }
477
+
478
+ function localDesktopInstallHelp(platform = process.platform) {
479
+ if (platform === "darwin") {
480
+ return "On macOS, install Xcode Command Line Tools with `xcode-select --install` and make sure `cmake --version` works, for example after `brew install cmake`.";
481
+ }
482
+ if (platform === "win32") {
483
+ return "On Windows, install Visual Studio C++ Build Tools and make sure `cmake --version` works, for example after `winget install Kitware.CMake`.";
484
+ }
485
+ if (platform === "linux") {
486
+ return "On Linux, install the native build tools for your distro: Debian/Ubuntu need `build-essential cmake libxtst-dev libpng++-dev`; Fedora needs `gcc-c++ make cmake libXtst-devel libpng-devel`; Arch needs `base-devel cmake libxtst libpng`. Headless hosts also need `xvfb` unless DISPLAY is managed externally.";
487
+ }
488
+ return "Install the native build tools and CMake for this OS.";
489
+ }
490
+
491
+ function localDesktopRuntimeHelp(environment = defaultDesktopEnvironment()) {
492
+ if (environment === "mac") {
493
+ return "On macOS, grant the terminal or Node.js process Screen Recording and Accessibility permissions, then restart the process.";
494
+ }
495
+ if (environment === "windows") {
496
+ return "On Windows, make sure the desktop session is unlocked and the process is allowed to control the desktop.";
497
+ }
498
+ return "On Linux, make sure DISPLAY points to a running X server; on headless hosts install xvfb or pass virtualDisplay options.";
499
+ }
500
+
501
+ function errorMessageSuffix(error) {
502
+ const message = error?.message;
503
+ return message ? ` Original error: ${message}` : "";
504
+ }
505
+
441
506
  async function maybeCall(fn, thisArg) {
442
507
  if (typeof fn !== "function") return undefined;
443
508
  return fn.call(thisArg);