automify 0.2.0 → 0.3.1
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 +239 -36
- package/examples/browser-with-safety.js +7 -10
- package/examples/cli-qemu.js +28 -0
- package/examples/desktop-qemu.js +41 -0
- package/package.json +5 -2
- package/scripts/generate-argument-reference.js +3 -1
- package/scripts/qemu-image.js +154 -0
- package/src/index.d.ts +368 -10
- package/src/index.js +18 -38
- package/src/lib/adapter-toolkit.js +8 -4
- package/src/lib/anthropic-model-adapter.js +24 -13
- package/src/lib/argument-reference.js +60 -8
- package/src/lib/automify.js +96 -0
- package/src/lib/cli-automify.js +41 -2
- package/src/lib/computer-automify.js +45 -26
- package/src/lib/docker-cli-automify.js +2 -6
- package/src/lib/docker-desktop-computer.js +7 -13
- package/src/lib/file-data.js +6 -6
- package/src/lib/init.js +14 -3
- package/src/lib/local-desktop-computer.js +2 -1
- package/src/lib/openai-responses-client.js +10 -3
- package/src/lib/presets.js +50 -2
- package/src/lib/qemu-cli-automify.js +568 -0
- package/src/lib/qemu-desktop-computer.js +681 -0
- package/src/lib/qemu-runtime.js +654 -0
- package/src/lib/runtime.js +23 -2
- package/src/lib/screen-recording.js +184 -0
- package/src/lib/task.js +564 -0
- package/src/lib/virtual-shared-folder.js +3 -1
package/src/lib/runtime.js
CHANGED
|
@@ -38,6 +38,8 @@ export const AUTOMIFY_OPTION_KEYS = new Set([
|
|
|
38
38
|
"finalScreenshot",
|
|
39
39
|
"actionScreenshots",
|
|
40
40
|
"screenshots",
|
|
41
|
+
"recording",
|
|
42
|
+
"screenRecording",
|
|
41
43
|
"trace",
|
|
42
44
|
"silent",
|
|
43
45
|
"debug",
|
|
@@ -229,6 +231,8 @@ export const DO_OPTION_KEYS = new Set([
|
|
|
229
231
|
"finalScreenshot",
|
|
230
232
|
"actionScreenshots",
|
|
231
233
|
"screenshots",
|
|
234
|
+
"recording",
|
|
235
|
+
"screenRecording",
|
|
232
236
|
"screenshot",
|
|
233
237
|
"trace",
|
|
234
238
|
"silent",
|
|
@@ -313,7 +317,8 @@ export function pickKnownOptions(options, allowedKeys) {
|
|
|
313
317
|
|
|
314
318
|
export function normalizeAutomifyOptions(options = {}) {
|
|
315
319
|
assertKnownOptions("Automify", options, AUTOMIFY_OPTION_KEYS);
|
|
316
|
-
const { viewport, limits, request, safety, hooks, screenshots, screenshot, ...rest } =
|
|
320
|
+
const { viewport, limits, request, safety, hooks, screenshots, screenshot, recording, screenRecording, ...rest } =
|
|
321
|
+
options;
|
|
317
322
|
const viewportOptions = viewport ?? {};
|
|
318
323
|
const limitOptions = limits ?? {};
|
|
319
324
|
const safetyOptions = safety ?? {};
|
|
@@ -336,6 +341,8 @@ export function normalizeAutomifyOptions(options = {}) {
|
|
|
336
341
|
initialScreenshot: rest.initialScreenshot ?? screenshotPaths.initial,
|
|
337
342
|
finalScreenshot: rest.finalScreenshot ?? screenshotPaths.final,
|
|
338
343
|
actionScreenshots: rest.actionScreenshots ?? screenshotPaths.actions ?? screenshotPaths.actionScreenshots,
|
|
344
|
+
screenRecording:
|
|
345
|
+
rest.screenRecording ?? screenRecording ?? rest.recording ?? recording ?? screenshotPaths.recording,
|
|
339
346
|
screenshotDetail: rest.screenshotDetail ?? screenshotOptions.detail,
|
|
340
347
|
screenshotMaxWidth: rest.screenshotMaxWidth ?? screenshotOptions.maxWidth ?? screenshotOptions.screenshotMaxWidth,
|
|
341
348
|
screenshotMaxHeight:
|
|
@@ -347,7 +354,20 @@ export function normalizeAutomifyOptions(options = {}) {
|
|
|
347
354
|
}
|
|
348
355
|
|
|
349
356
|
function normalizeDoOptionAliases(options) {
|
|
350
|
-
const {
|
|
357
|
+
const {
|
|
358
|
+
evaluate,
|
|
359
|
+
limits,
|
|
360
|
+
request,
|
|
361
|
+
safety,
|
|
362
|
+
hooks,
|
|
363
|
+
screenshots,
|
|
364
|
+
screenshot,
|
|
365
|
+
recording,
|
|
366
|
+
screenRecording,
|
|
367
|
+
command,
|
|
368
|
+
commands,
|
|
369
|
+
...rest
|
|
370
|
+
} = options;
|
|
351
371
|
|
|
352
372
|
const commandOptions = commands ?? command;
|
|
353
373
|
assertKnownOptions("do() command", commandOptions, COMMAND_OPTION_KEYS);
|
|
@@ -364,6 +384,7 @@ function normalizeDoOptionAliases(options) {
|
|
|
364
384
|
initialScreenshot: rest.initialScreenshot ?? screenshots?.initial,
|
|
365
385
|
finalScreenshot: rest.finalScreenshot ?? screenshots?.final,
|
|
366
386
|
actionScreenshots: rest.actionScreenshots ?? screenshots?.actions ?? screenshots?.actionScreenshots,
|
|
387
|
+
screenRecording: rest.screenRecording ?? screenRecording ?? rest.recording ?? recording ?? screenshots?.recording,
|
|
367
388
|
screenshotDetail: rest.screenshotDetail ?? screenshot?.detail,
|
|
368
389
|
screenshotMaxWidth: rest.screenshotMaxWidth ?? screenshot?.maxWidth ?? screenshot?.screenshotMaxWidth,
|
|
369
390
|
screenshotMaxHeight: rest.screenshotMaxHeight ?? screenshot?.maxHeight ?? screenshot?.screenshotMaxHeight,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { mkdtemp, mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { AutomifyError } from "./errors.js";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFileCallback);
|
|
10
|
+
const DEFAULT_RECORDING_FPS = 4;
|
|
11
|
+
const DEFAULT_RECORDING_TIMEOUT_MS = 120_000;
|
|
12
|
+
|
|
13
|
+
export async function startScreenRecording(input, context = {}) {
|
|
14
|
+
const options = normalizeScreenRecordingOptions(input, context);
|
|
15
|
+
if (!options) return null;
|
|
16
|
+
|
|
17
|
+
await mkdir(dirname(options.path), { recursive: true });
|
|
18
|
+
const framesDir =
|
|
19
|
+
options.framesDir ??
|
|
20
|
+
(await mkdtemp(join(tmpdir(), `automify-recording-${Date.now()}-${Math.random().toString(16).slice(2)}-`)));
|
|
21
|
+
await mkdir(framesDir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
let frameCount = 0;
|
|
24
|
+
let stopped = false;
|
|
25
|
+
let captureTimer = null;
|
|
26
|
+
let inFlight = Promise.resolve();
|
|
27
|
+
|
|
28
|
+
const capture = async (force = false) => {
|
|
29
|
+
if (stopped && !force) return;
|
|
30
|
+
const frameNumber = frameCount + 1;
|
|
31
|
+
const screenshot = await options.captureFrame({
|
|
32
|
+
...context,
|
|
33
|
+
recording: true,
|
|
34
|
+
frame: frameNumber
|
|
35
|
+
});
|
|
36
|
+
await writeFile(join(framesDir, `frame-${padFrame(frameNumber)}.png`), screenshotToBuffer(screenshot));
|
|
37
|
+
frameCount = frameNumber;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const schedule = () => {
|
|
41
|
+
captureTimer = setTimeout(() => {
|
|
42
|
+
inFlight = inFlight.then(capture).catch((error) => {
|
|
43
|
+
stopped = true;
|
|
44
|
+
throw error;
|
|
45
|
+
});
|
|
46
|
+
if (!stopped) schedule();
|
|
47
|
+
}, options.intervalMs);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
inFlight = capture();
|
|
51
|
+
schedule();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
path: options.path,
|
|
55
|
+
framesDir,
|
|
56
|
+
fps: options.fps,
|
|
57
|
+
startedAt: new Date().toISOString(),
|
|
58
|
+
async stop(stopContext = {}) {
|
|
59
|
+
if (stopped && stopContext.force !== true) return null;
|
|
60
|
+
stopped = true;
|
|
61
|
+
clearTimeout(captureTimer);
|
|
62
|
+
await inFlight;
|
|
63
|
+
if (frameCount === 0) {
|
|
64
|
+
await capture(true);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await encodeScreenRecording({
|
|
68
|
+
...options,
|
|
69
|
+
framesDir
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const size = await stat(options.path).catch(() => null);
|
|
73
|
+
if (options.keepFrames !== true) {
|
|
74
|
+
await rm(framesDir, { recursive: true, force: true }).catch(() => {});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
path: options.path,
|
|
79
|
+
bytes: size?.size,
|
|
80
|
+
frames: frameCount,
|
|
81
|
+
fps: options.fps,
|
|
82
|
+
startedAt: this.startedAt,
|
|
83
|
+
stoppedAt: new Date().toISOString()
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeScreenRecordingOptions(input, context = {}) {
|
|
90
|
+
if (input == null || input === false) return null;
|
|
91
|
+
|
|
92
|
+
const raw =
|
|
93
|
+
input === true || typeof input === "string"
|
|
94
|
+
? {
|
|
95
|
+
path: typeof input === "string" ? input : undefined
|
|
96
|
+
}
|
|
97
|
+
: input;
|
|
98
|
+
|
|
99
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
100
|
+
throw new AutomifyError("screenRecording must be true, a file path, or an options object.");
|
|
101
|
+
}
|
|
102
|
+
if (raw.enabled === false) return null;
|
|
103
|
+
|
|
104
|
+
const fps = positiveNumber(raw.fps) ?? DEFAULT_RECORDING_FPS;
|
|
105
|
+
const intervalMs = positiveNumber(raw.intervalMs ?? raw.captureIntervalMs) ?? Math.max(1, Math.round(1000 / fps));
|
|
106
|
+
const captureFrame = raw.captureFrame ?? context.captureFrame;
|
|
107
|
+
if (typeof captureFrame !== "function") {
|
|
108
|
+
throw new AutomifyError("screenRecording requires a captureFrame function.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...raw,
|
|
113
|
+
path: normalizeRecordingPath(raw.path),
|
|
114
|
+
framesDir: normalizeOptionalPath(raw.framesDir),
|
|
115
|
+
fps,
|
|
116
|
+
intervalMs,
|
|
117
|
+
captureFrame,
|
|
118
|
+
keepFrames: raw.keepFrames === true,
|
|
119
|
+
execFile: raw.execFile ?? execFileAsync,
|
|
120
|
+
ffmpegCommand: raw.ffmpegCommand ?? raw.command ?? "ffmpeg",
|
|
121
|
+
encodingTimeoutMs: positiveNumber(raw.encodingTimeoutMs ?? raw.timeoutMs) ?? DEFAULT_RECORDING_TIMEOUT_MS
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function encodeScreenRecording(options) {
|
|
126
|
+
const outputPattern = join(options.framesDir, "frame-%06d.png");
|
|
127
|
+
const args = [
|
|
128
|
+
"-y",
|
|
129
|
+
"-framerate",
|
|
130
|
+
String(options.fps),
|
|
131
|
+
"-i",
|
|
132
|
+
outputPattern,
|
|
133
|
+
"-vf",
|
|
134
|
+
`fps=${options.fps},format=yuv420p`,
|
|
135
|
+
options.path
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await options.execFile(options.ffmpegCommand, args, {
|
|
140
|
+
timeout: options.encodingTimeoutMs
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new AutomifyError(
|
|
144
|
+
`Unable to encode screen recording with ${options.ffmpegCommand}. Install ffmpeg or pass screenRecording.execFile/ffmpegCommand.`,
|
|
145
|
+
{ cause: error }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeRecordingPath(path) {
|
|
151
|
+
if (path == null || path === true) {
|
|
152
|
+
return join(tmpdir(), `automify-recording-${Date.now()}-${Math.random().toString(16).slice(2)}.mp4`);
|
|
153
|
+
}
|
|
154
|
+
if (typeof path !== "string" || path.trim() === "") {
|
|
155
|
+
throw new AutomifyError("screenRecording.path must be a non-empty file path.");
|
|
156
|
+
}
|
|
157
|
+
return path;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeOptionalPath(path) {
|
|
161
|
+
if (path == null) return undefined;
|
|
162
|
+
if (typeof path !== "string" || path.trim() === "") {
|
|
163
|
+
throw new AutomifyError("screenRecording.framesDir must be a non-empty directory path.");
|
|
164
|
+
}
|
|
165
|
+
return path;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function positiveNumber(value) {
|
|
169
|
+
const number = Number(value);
|
|
170
|
+
return Number.isFinite(number) && number > 0 ? number : null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function padFrame(value) {
|
|
174
|
+
return String(value).padStart(6, "0");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function screenshotToBuffer(value) {
|
|
178
|
+
if (typeof value === "string") {
|
|
179
|
+
const dataUrlMatch = /^data:[^;]+;base64,(.*)$/s.exec(value);
|
|
180
|
+
return Buffer.from(dataUrlMatch ? dataUrlMatch[1] : value, dataUrlMatch ? "base64" : "utf8");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Buffer.from(value);
|
|
184
|
+
}
|