automify 0.1.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +401 -0
  4. package/SECURITY.md +17 -0
  5. package/examples/anthropic-provider.js +18 -0
  6. package/examples/browser-basic.js +30 -0
  7. package/examples/browser-with-safety.js +38 -0
  8. package/examples/claude-model-adapter.js +141 -0
  9. package/examples/cli-basic.js +20 -0
  10. package/examples/cli-docker.js +42 -0
  11. package/examples/custom-computer.js +18 -0
  12. package/examples/custom-model-adapter.js +48 -0
  13. package/examples/desktop-docker.js +37 -0
  14. package/examples/desktop-local.js +28 -0
  15. package/examples/evaluate-image.js +26 -0
  16. package/examples/files-and-shared-folder.js +42 -0
  17. package/package.json +74 -0
  18. package/scripts/generate-argument-reference.js +17 -0
  19. package/scripts/install-browser.js +12 -0
  20. package/scripts/install-desktop.js +281 -0
  21. package/src/index.d.ts +1049 -0
  22. package/src/index.js +83 -0
  23. package/src/lib/adapter-locks.js +93 -0
  24. package/src/lib/adapter-toolkit.js +239 -0
  25. package/src/lib/anthropic-model-adapter.js +451 -0
  26. package/src/lib/argument-reference.js +98 -0
  27. package/src/lib/automify.js +938 -0
  28. package/src/lib/browser-automify.js +89 -0
  29. package/src/lib/cli-automify.js +520 -0
  30. package/src/lib/computer-automify.js +103 -0
  31. package/src/lib/docker-cli-automify.js +517 -0
  32. package/src/lib/docker-desktop-computer.js +725 -0
  33. package/src/lib/errors.js +24 -0
  34. package/src/lib/file-data.js +140 -0
  35. package/src/lib/init.js +217 -0
  36. package/src/lib/local-desktop-computer.js +963 -0
  37. package/src/lib/model-adapter.js +32 -0
  38. package/src/lib/openai-responses-client.js +162 -0
  39. package/src/lib/output.js +57 -0
  40. package/src/lib/playwright-computer.js +363 -0
  41. package/src/lib/presets.js +141 -0
  42. package/src/lib/result.js +95 -0
  43. package/src/lib/runtime.js +471 -0
  44. package/src/lib/virtual-shared-folder.js +109 -0
  45. package/src/lib/zod-output.js +26 -0
  46. package/src/zod.d.ts +12 -0
  47. package/src/zod.js +5 -0
@@ -0,0 +1,517 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+
5
+ import { CliAutomify } from "./cli-automify.js";
6
+ import { AutomifyError } from "./errors.js";
7
+ import { applyDockerCliPreset } from "./presets.js";
8
+ import {
9
+ AUTOMIFY_OPTION_KEYS,
10
+ assertKnownOptions,
11
+ debugLog,
12
+ mergeOptionKeys,
13
+ normalizeLogFile,
14
+ writeDebugLogFile
15
+ } from "./runtime.js";
16
+ import { prepareVirtualSharedFolder } from "./virtual-shared-folder.js";
17
+
18
+ const execFileAsync = promisify(execFile);
19
+
20
+ const DEFAULT_IMAGE = "debian:bookworm-slim";
21
+ const DEFAULT_CWD = "/workspace";
22
+ const DEFAULT_TIMEOUT_MS = 30_000;
23
+ const VIRTUAL_CLI_OPTION_KEYS = mergeOptionKeys(AUTOMIFY_OPTION_KEYS, [
24
+ "preset",
25
+ "command",
26
+ "commands",
27
+ "cwd",
28
+ "env",
29
+ "shell",
30
+ "timeoutMs",
31
+ "runner",
32
+ "confirmCommand",
33
+ "approval",
34
+ "allowedCommands",
35
+ "blockedCommands",
36
+ "instructions",
37
+ "logFile",
38
+ "session",
39
+ "container",
40
+ "dockerCommand",
41
+ "image",
42
+ "containerName",
43
+ "existingContainer",
44
+ "keepContainer",
45
+ "workdir",
46
+ "workspacePath",
47
+ "containerCwd",
48
+ "startupCommand",
49
+ "packages",
50
+ "additionalAptPackages",
51
+ "installDependencies",
52
+ "autoRemove",
53
+ "sandbox",
54
+ "readOnly",
55
+ "network",
56
+ "cpus",
57
+ "memory",
58
+ "memorySwap",
59
+ "cpuShares",
60
+ "cpusetCpus",
61
+ "pidsLimit",
62
+ "shmSize",
63
+ "tmpfsTmp",
64
+ "tmpfsRun",
65
+ "volumes",
66
+ "containerEnv",
67
+ "shared",
68
+ "sharedFolder",
69
+ "sharedFiles",
70
+ "files",
71
+ "dockerTimeoutMs",
72
+ "commandMaxBuffer",
73
+ "execFile"
74
+ ]);
75
+ const CONTAINER_OPTION_KEYS = new Set([
76
+ "docker",
77
+ "dockerCommand",
78
+ "image",
79
+ "name",
80
+ "existing",
81
+ "keep",
82
+ "autoRemove",
83
+ "sandbox",
84
+ "readOnly",
85
+ "network",
86
+ "cpus",
87
+ "memory",
88
+ "memorySwap",
89
+ "cpuShares",
90
+ "cpusetCpus",
91
+ "pidsLimit",
92
+ "shmSize",
93
+ "tmpfsTmp",
94
+ "volumes",
95
+ "env",
96
+ "timeoutMs",
97
+ "cwd",
98
+ "workdir",
99
+ "packages",
100
+ "additionalAptPackages",
101
+ "installDependencies",
102
+ "startupCommand"
103
+ ]);
104
+
105
+ export function createDockerCliAutomify(options = {}) {
106
+ options = normalizeVirtualCliOptions(options);
107
+ return new DockerCliAutomify(options);
108
+ }
109
+
110
+ export class DockerCliAutomify extends CliAutomify {
111
+ constructor(options = {}) {
112
+ options = normalizeVirtualCliOptions(options);
113
+ const session = options.session ?? new DockerCliSession(options);
114
+ super({
115
+ ...cliOptionsFromVirtualOptions(options),
116
+ cwd: options.cwd ?? session.cwd,
117
+ runner: options.runner ?? ((command, runOptions) => session.run(command, runOptions))
118
+ });
119
+ this.session = session;
120
+ }
121
+
122
+ get sharedFolder() {
123
+ return this.session.sharedFolder?.data;
124
+ }
125
+
126
+ async do(instruction, runOptions = {}, maybeOptions) {
127
+ await this.session.prepareSharedFolder();
128
+ if (runOptions && typeof runOptions === "object" && !Array.isArray(runOptions)) {
129
+ const data = runOptions.data;
130
+ const canAttachSharedFolder =
131
+ data && typeof data === "object" && !Array.isArray(data) && data.sharedFolder == null && this.sharedFolder;
132
+
133
+ if (canAttachSharedFolder) {
134
+ return super.do(
135
+ instruction,
136
+ {
137
+ ...runOptions,
138
+ data: {
139
+ ...data,
140
+ sharedFolder: this.sharedFolder
141
+ }
142
+ },
143
+ maybeOptions
144
+ );
145
+ }
146
+ }
147
+
148
+ return super.do(instruction, runOptions, maybeOptions);
149
+ }
150
+
151
+ async close() {
152
+ await this.session.close();
153
+ }
154
+ }
155
+
156
+ export class DockerCliSession {
157
+ constructor(options = {}) {
158
+ options = normalizeVirtualCliOptions(options);
159
+ this.options = options;
160
+ this.docker = options.dockerCommand ?? "docker";
161
+ this.execFile = options.execFile ?? execFileAsync;
162
+ this.image = options.image ?? DEFAULT_IMAGE;
163
+ this.name = options.containerName ?? `automify-cli-${randomUUID()}`;
164
+ this.cwd = normalizeContainerPath(options.cwd ?? DEFAULT_CWD);
165
+ this.started = false;
166
+ this.created = false;
167
+ this.sharedFolder = null;
168
+ }
169
+
170
+ async prepareSharedFolder() {
171
+ if (this.sharedFolder) return this.sharedFolder;
172
+ this.sharedFolder = await prepareVirtualSharedFolder(this.options, {
173
+ prefix: "automify-docker-cli-",
174
+ containerPath: this.cwd
175
+ });
176
+ return this.sharedFolder;
177
+ }
178
+
179
+ async start() {
180
+ if (this.started) return;
181
+ await this.prepareSharedFolder();
182
+ if (this.options.existingContainer) {
183
+ debugVirtualCli(this.options, "use_existing_container", { containerName: this.name });
184
+ this.started = true;
185
+ return;
186
+ }
187
+
188
+ const args = [
189
+ "run",
190
+ "-d",
191
+ "--name",
192
+ this.name,
193
+ "--network",
194
+ dockerNetwork(this.options.network),
195
+ "--pids-limit",
196
+ String(positiveInteger(this.options.pidsLimit) ?? 512),
197
+ "--shm-size",
198
+ String(this.options.shmSize ?? "512m"),
199
+ "--workdir",
200
+ this.cwd
201
+ ];
202
+ appendDockerResourceArgs(args, this.options);
203
+
204
+ if (this.options.autoRemove === true) {
205
+ args.splice(2, 0, "--rm");
206
+ }
207
+ const installsDependencies = dockerCliInstallsDependencies(this.options);
208
+ if (this.options.sandbox !== false && !installsDependencies) {
209
+ args.push("--cap-drop", "ALL", "--security-opt", "no-new-privileges");
210
+ }
211
+ if (this.options.readOnly === true && !installsDependencies) {
212
+ args.push("--read-only", "--tmpfs", String(this.options.tmpfsTmp ?? "/tmp:exec,nosuid,nodev,size=512m"));
213
+ }
214
+ for (const volume of this.options.volumes ?? []) {
215
+ args.push("-v", String(volume));
216
+ }
217
+ if (this.sharedFolder) {
218
+ args.push("-v", this.sharedFolder.volume);
219
+ }
220
+ for (const env of this.options.containerEnv ?? []) {
221
+ args.push("-e", String(env));
222
+ }
223
+
224
+ args.push(this.image, "sh", "-lc", dockerCliStartupCommand(this.options));
225
+ await this.runDocker(args, "start Docker CLI container");
226
+ this.created = true;
227
+ this.started = true;
228
+ debugVirtualCli(this.options, "container_ready", { containerName: this.name, image: this.image });
229
+ }
230
+
231
+ async run(command, options = {}) {
232
+ await this.start();
233
+ const cwd = normalizeContainerPath(options.cwd ?? this.cwd);
234
+ const timeoutMs =
235
+ positiveInteger(options.timeoutMs) ?? positiveInteger(this.options.timeoutMs) ?? DEFAULT_TIMEOUT_MS;
236
+ const envArgs = [];
237
+ for (const [key, value] of Object.entries(options.env ?? {})) {
238
+ envArgs.push("-e", `${key}=${value}`);
239
+ }
240
+
241
+ debugVirtualCli(this.options, "command", { command: { command, cwd, timeoutMs } });
242
+ try {
243
+ const { stdout, stderr } = await this.execFile(
244
+ this.docker,
245
+ ["exec", "--workdir", cwd, ...envArgs, this.name, "sh", "-lc", command],
246
+ {
247
+ timeout: timeoutMs,
248
+ maxBuffer: this.options.commandMaxBuffer ?? 10 * 1024 * 1024
249
+ }
250
+ );
251
+ const result = {
252
+ command,
253
+ cwd,
254
+ exitCode: 0,
255
+ stdout: String(stdout ?? ""),
256
+ stderr: String(stderr ?? ""),
257
+ timedOut: false
258
+ };
259
+ debugVirtualCli(this.options, "command_result", summarizeCommandResult(result));
260
+ return result;
261
+ } catch (error) {
262
+ const result = {
263
+ command,
264
+ cwd,
265
+ exitCode: typeof error.code === "number" ? error.code : null,
266
+ signal: error.signal,
267
+ stdout: String(error.stdout ?? ""),
268
+ stderr: String(error.stderr || error.message || ""),
269
+ timedOut: error.killed === true || error.signal === "SIGTERM"
270
+ };
271
+ debugVirtualCli(this.options, "command_result", summarizeCommandResult(result));
272
+ return result;
273
+ }
274
+ }
275
+
276
+ async runDocker(args, label) {
277
+ debugVirtualCli(this.options, "docker", { label, args });
278
+ try {
279
+ return await this.execFile(this.docker, args, {
280
+ timeout: this.options.dockerTimeoutMs ?? 30_000
281
+ });
282
+ } catch (error) {
283
+ throw new AutomifyError(`Unable to ${label}. Ensure Docker is running and image ${this.image} exists.`, {
284
+ cause: error
285
+ });
286
+ }
287
+ }
288
+
289
+ async close() {
290
+ if (this.created && !this.options.existingContainer && !this.options.keepContainer) {
291
+ debugVirtualCli(this.options, "container_close", { containerName: this.name });
292
+ await this.execFile(this.docker, ["rm", "-f", this.name]).catch(() => {});
293
+ }
294
+ await this.sharedFolder?.close();
295
+ this.started = false;
296
+ this.created = false;
297
+ }
298
+ }
299
+
300
+ export const createVirtualCliAutomify = createDockerCliAutomify;
301
+ export const VirtualCliAutomify = DockerCliAutomify;
302
+ export const DockerVirtualCliSession = DockerCliSession;
303
+
304
+ function normalizeVirtualCliOptions(options = {}) {
305
+ assertKnownOptions("Docker CLI adapter", options, VIRTUAL_CLI_OPTION_KEYS);
306
+ assertKnownOptions("Docker CLI container", options.container, CONTAINER_OPTION_KEYS);
307
+ options = applyDockerCliPreset(options);
308
+ const container = options.container ?? {};
309
+ const cwd =
310
+ options.cwd ??
311
+ options.workdir ??
312
+ options.workspacePath ??
313
+ options.containerCwd ??
314
+ container.cwd ??
315
+ container.workdir;
316
+
317
+ return {
318
+ ...options,
319
+ debug: options.debug ?? false,
320
+ logFile: normalizeLogFile(options.logFile, "Docker CLI logFile"),
321
+ dockerCommand: options.dockerCommand ?? container.dockerCommand ?? container.docker,
322
+ image: options.image ?? container.image,
323
+ containerName: options.containerName ?? container.name,
324
+ existingContainer: options.existingContainer ?? container.existing,
325
+ keepContainer: options.keepContainer ?? container.keep,
326
+ autoRemove: options.autoRemove ?? container.autoRemove,
327
+ sandbox: options.sandbox ?? container.sandbox,
328
+ readOnly: options.readOnly ?? container.readOnly,
329
+ network: options.network ?? container.network,
330
+ cpus: options.cpus ?? container.cpus,
331
+ memory: options.memory ?? container.memory,
332
+ memorySwap: options.memorySwap ?? container.memorySwap,
333
+ cpuShares: options.cpuShares ?? container.cpuShares,
334
+ cpusetCpus: options.cpusetCpus ?? container.cpusetCpus,
335
+ pidsLimit: options.pidsLimit ?? container.pidsLimit,
336
+ shmSize: options.shmSize ?? container.shmSize,
337
+ tmpfsTmp: options.tmpfsTmp ?? container.tmpfsTmp,
338
+ volumes: options.volumes ?? container.volumes,
339
+ containerEnv: options.containerEnv ?? container.env,
340
+ dockerTimeoutMs: options.dockerTimeoutMs ?? container.timeoutMs,
341
+ packages: options.packages ?? container.packages,
342
+ additionalAptPackages: options.additionalAptPackages ?? container.additionalAptPackages,
343
+ installDependencies: options.installDependencies ?? container.installDependencies,
344
+ startupCommand: options.startupCommand ?? container.startupCommand,
345
+ cwd,
346
+ sharedFolder: options.sharedFolder ?? options.shared,
347
+ files: options.files ?? options.sharedFiles
348
+ };
349
+ }
350
+
351
+ function dockerCliStartupCommand(options) {
352
+ const startupCommand = options.startupCommand ?? "sleep infinity";
353
+ const installCommand = dockerCliInstallCommand(options);
354
+ return installCommand ? `${installCommand} && ${startupCommand}` : startupCommand;
355
+ }
356
+
357
+ function dockerCliInstallsDependencies(options) {
358
+ return Boolean(dockerCliInstallCommand(options));
359
+ }
360
+
361
+ function dockerCliInstallCommand(options) {
362
+ if (options.installDependencies === false) return "";
363
+
364
+ const packages = uniquePackages([...(options.packages ?? []), ...(options.additionalAptPackages ?? [])]);
365
+ if (packages.length === 0) return "";
366
+
367
+ return [
368
+ "apt-get update",
369
+ `DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ${packages.map(shellQuote).join(" ")}`,
370
+ "rm -rf /var/lib/apt/lists/*"
371
+ ].join(" && ");
372
+ }
373
+
374
+ function uniquePackages(packages) {
375
+ return [...new Set(packages.map((pkg) => String(pkg).trim()).filter(Boolean))];
376
+ }
377
+
378
+ function shellQuote(value) {
379
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
380
+ }
381
+
382
+ function cliOptionsFromVirtualOptions(options) {
383
+ const {
384
+ openaiApiKey,
385
+ client,
386
+ model,
387
+ baseURL,
388
+ fetchImpl,
389
+ maxSteps,
390
+ limits,
391
+ request,
392
+ requestOptions,
393
+ command,
394
+ commands,
395
+ cwd,
396
+ env,
397
+ shell,
398
+ timeoutMs,
399
+ runner,
400
+ confirmCommand,
401
+ approval,
402
+ allowedCommands,
403
+ blockedCommands,
404
+ instructions,
405
+ hooks,
406
+ onStep,
407
+ onRequest,
408
+ onResponse,
409
+ onComplete,
410
+ debug,
411
+ logFile,
412
+ silent,
413
+ reasoning,
414
+ safetyIdentifier,
415
+ preset
416
+ } = options;
417
+
418
+ return {
419
+ openaiApiKey,
420
+ client,
421
+ model,
422
+ baseURL,
423
+ fetchImpl,
424
+ maxSteps,
425
+ limits,
426
+ request,
427
+ requestOptions,
428
+ command,
429
+ commands,
430
+ cwd,
431
+ env,
432
+ shell,
433
+ timeoutMs,
434
+ runner,
435
+ confirmCommand,
436
+ approval,
437
+ allowedCommands,
438
+ blockedCommands,
439
+ instructions,
440
+ hooks,
441
+ onStep,
442
+ onRequest,
443
+ onResponse,
444
+ onComplete,
445
+ debug,
446
+ logFile,
447
+ silent,
448
+ reasoning,
449
+ safetyIdentifier,
450
+ preset
451
+ };
452
+ }
453
+
454
+ function debugVirtualCli(options, message, details) {
455
+ writeDebugLogFile(options.logFile, "automify:docker-cli", message, details, { silent: options.silent });
456
+ debugLog(options.debug, "automify:docker-cli", message, details, { silent: options.silent });
457
+ }
458
+
459
+ function dockerNetwork(value) {
460
+ if (value === false || value === "none") return "none";
461
+ if (typeof value === "string" && value.trim()) return value.trim();
462
+ return "bridge";
463
+ }
464
+
465
+ function normalizeContainerPath(value) {
466
+ const path = String(value || DEFAULT_CWD).trim();
467
+ return path.startsWith("/") ? path : `/${path}`;
468
+ }
469
+
470
+ function summarizeCommandResult(result) {
471
+ return {
472
+ command: {
473
+ command: result.command,
474
+ cwd: result.cwd
475
+ },
476
+ exitCode: result.exitCode,
477
+ signal: result.signal,
478
+ timedOut: result.timedOut,
479
+ stdout: result.stdout,
480
+ stderr: result.stderr,
481
+ stdoutLength: typeof result.stdout === "string" ? result.stdout.length : undefined,
482
+ stderrLength: typeof result.stderr === "string" ? result.stderr.length : undefined
483
+ };
484
+ }
485
+
486
+ function positiveInteger(value) {
487
+ const number = Number(value);
488
+ if (!Number.isFinite(number) || number <= 0) return null;
489
+ return Math.floor(number);
490
+ }
491
+
492
+ function appendDockerResourceArgs(args, options) {
493
+ const cpus = positiveNumber(options.cpus);
494
+ if (cpus != null) {
495
+ args.push("--cpus", String(cpus));
496
+ }
497
+ appendNonEmptyArg(args, "--memory", options.memory);
498
+ appendNonEmptyArg(args, "--memory-swap", options.memorySwap);
499
+
500
+ const cpuShares = positiveInteger(options.cpuShares);
501
+ if (cpuShares != null) {
502
+ args.push("--cpu-shares", String(cpuShares));
503
+ }
504
+ appendNonEmptyArg(args, "--cpuset-cpus", options.cpusetCpus);
505
+ }
506
+
507
+ function appendNonEmptyArg(args, flag, value) {
508
+ if (value == null || value === false) return;
509
+ const normalized = String(value).trim();
510
+ if (normalized) args.push(flag, normalized);
511
+ }
512
+
513
+ function positiveNumber(value) {
514
+ const number = Number(value);
515
+ if (!Number.isFinite(number) || number <= 0) return null;
516
+ return number;
517
+ }