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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/SECURITY.md +17 -0
- package/examples/anthropic-provider.js +18 -0
- package/examples/browser-basic.js +30 -0
- package/examples/browser-with-safety.js +38 -0
- package/examples/claude-model-adapter.js +141 -0
- package/examples/cli-basic.js +20 -0
- package/examples/cli-docker.js +42 -0
- package/examples/custom-computer.js +18 -0
- package/examples/custom-model-adapter.js +48 -0
- package/examples/desktop-docker.js +37 -0
- package/examples/desktop-local.js +28 -0
- package/examples/evaluate-image.js +26 -0
- package/examples/files-and-shared-folder.js +42 -0
- package/package.json +74 -0
- package/scripts/generate-argument-reference.js +17 -0
- package/scripts/install-browser.js +12 -0
- package/scripts/install-desktop.js +281 -0
- package/src/index.d.ts +1049 -0
- package/src/index.js +83 -0
- package/src/lib/adapter-locks.js +93 -0
- package/src/lib/adapter-toolkit.js +239 -0
- package/src/lib/anthropic-model-adapter.js +451 -0
- package/src/lib/argument-reference.js +98 -0
- package/src/lib/automify.js +938 -0
- package/src/lib/browser-automify.js +89 -0
- package/src/lib/cli-automify.js +520 -0
- package/src/lib/computer-automify.js +103 -0
- package/src/lib/docker-cli-automify.js +517 -0
- package/src/lib/docker-desktop-computer.js +725 -0
- package/src/lib/errors.js +24 -0
- package/src/lib/file-data.js +140 -0
- package/src/lib/init.js +217 -0
- package/src/lib/local-desktop-computer.js +963 -0
- package/src/lib/model-adapter.js +32 -0
- package/src/lib/openai-responses-client.js +162 -0
- package/src/lib/output.js +57 -0
- package/src/lib/playwright-computer.js +363 -0
- package/src/lib/presets.js +141 -0
- package/src/lib/result.js +95 -0
- package/src/lib/runtime.js +471 -0
- package/src/lib/virtual-shared-folder.js +109 -0
- package/src/lib/zod-output.js +26 -0
- package/src/zod.d.ts +12 -0
- package/src/zod.js +5 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AutomifyError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
const REPO_COMMAND = {
|
|
4
|
+
cwd: process.cwd(),
|
|
5
|
+
allow: ["git", "node", "npm", "pnpm", "yarn", "bun"],
|
|
6
|
+
block: [/^rm\s+-rf\b/]
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function applyBrowserPreset(options = {}) {
|
|
10
|
+
switch (options.preset) {
|
|
11
|
+
case undefined:
|
|
12
|
+
case null:
|
|
13
|
+
return options;
|
|
14
|
+
case "browser-review":
|
|
15
|
+
return mergePreset(
|
|
16
|
+
{
|
|
17
|
+
limits: { steps: 50 },
|
|
18
|
+
screenshot: { detail: "high" }
|
|
19
|
+
},
|
|
20
|
+
options
|
|
21
|
+
);
|
|
22
|
+
default:
|
|
23
|
+
throw unknownPreset("browser", options.preset, ["browser-review"]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function applyCliPreset(options = {}) {
|
|
28
|
+
switch (options.preset) {
|
|
29
|
+
case undefined:
|
|
30
|
+
case null:
|
|
31
|
+
return options;
|
|
32
|
+
case "repo":
|
|
33
|
+
return mergePreset({ command: REPO_COMMAND }, options);
|
|
34
|
+
case "locked-down-cli":
|
|
35
|
+
return mergePreset(
|
|
36
|
+
{
|
|
37
|
+
command: {
|
|
38
|
+
approval: "always",
|
|
39
|
+
allow: [],
|
|
40
|
+
block: [/^rm\b/, /^sudo\b/, /^curl\b/, /^wget\b/]
|
|
41
|
+
},
|
|
42
|
+
limits: { steps: 20 }
|
|
43
|
+
},
|
|
44
|
+
options
|
|
45
|
+
);
|
|
46
|
+
default:
|
|
47
|
+
throw unknownPreset("cli", options.preset, ["repo", "locked-down-cli"]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function applyDockerCliPreset(options = {}) {
|
|
52
|
+
switch (options.preset) {
|
|
53
|
+
case undefined:
|
|
54
|
+
case null:
|
|
55
|
+
return options;
|
|
56
|
+
case "repo":
|
|
57
|
+
return mergePreset(
|
|
58
|
+
{
|
|
59
|
+
command: REPO_COMMAND,
|
|
60
|
+
shared: {
|
|
61
|
+
hostPath: process.cwd(),
|
|
62
|
+
containerPath: "/workspace"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
options
|
|
66
|
+
);
|
|
67
|
+
case "locked-down-cli":
|
|
68
|
+
return mergePreset(
|
|
69
|
+
{
|
|
70
|
+
command: {
|
|
71
|
+
approval: "always",
|
|
72
|
+
allow: [],
|
|
73
|
+
block: [/^rm\b/, /^sudo\b/, /^curl\b/, /^wget\b/]
|
|
74
|
+
},
|
|
75
|
+
container: {
|
|
76
|
+
network: "none",
|
|
77
|
+
sandbox: true,
|
|
78
|
+
readOnly: true
|
|
79
|
+
},
|
|
80
|
+
limits: { steps: 20 }
|
|
81
|
+
},
|
|
82
|
+
options
|
|
83
|
+
);
|
|
84
|
+
default:
|
|
85
|
+
throw unknownPreset("Docker CLI", options.preset, ["repo", "locked-down-cli"]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const applyVirtualCliPreset = applyDockerCliPreset;
|
|
90
|
+
|
|
91
|
+
export function applyDockerDesktopPreset(options = {}) {
|
|
92
|
+
switch (options.preset) {
|
|
93
|
+
case undefined:
|
|
94
|
+
case null:
|
|
95
|
+
return options;
|
|
96
|
+
case "desktop-review":
|
|
97
|
+
return mergePreset(
|
|
98
|
+
{
|
|
99
|
+
viewport: { width: 1440, height: 900 },
|
|
100
|
+
waitMs: 750,
|
|
101
|
+
screenshotSettleMs: 500
|
|
102
|
+
},
|
|
103
|
+
options
|
|
104
|
+
);
|
|
105
|
+
default:
|
|
106
|
+
throw unknownPreset("Docker desktop", options.preset, ["desktop-review"]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const applyVirtualDesktopPreset = applyDockerDesktopPreset;
|
|
111
|
+
|
|
112
|
+
function mergePreset(defaults, options) {
|
|
113
|
+
return {
|
|
114
|
+
...defaults,
|
|
115
|
+
...options,
|
|
116
|
+
command: mergeObject(defaults.command, options.command ?? options.commands),
|
|
117
|
+
commands: options.commands,
|
|
118
|
+
container: mergeObject(defaults.container, options.container),
|
|
119
|
+
desktop: mergeObject(defaults.desktop, options.desktop),
|
|
120
|
+
limits: mergeObject(defaults.limits, options.limits),
|
|
121
|
+
safety: mergeObject(defaults.safety, options.safety),
|
|
122
|
+
screenshot: mergeObject(defaults.screenshot, options.screenshot),
|
|
123
|
+
screenshots: mergeObject(defaults.screenshots, options.screenshots),
|
|
124
|
+
viewport: mergeObject(defaults.viewport, options.viewport),
|
|
125
|
+
shared: options.shared ?? options.sharedFolder ?? defaults.shared
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function mergeObject(defaults, overrides) {
|
|
130
|
+
if (defaults == null && overrides == null) return undefined;
|
|
131
|
+
return {
|
|
132
|
+
...(defaults ?? {}),
|
|
133
|
+
...(overrides ?? {})
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function unknownPreset(surface, preset, allowed) {
|
|
138
|
+
return new AutomifyError(
|
|
139
|
+
`Unknown ${surface} preset ${JSON.stringify(preset)}. Available presets: ${allowed.map((name) => JSON.stringify(name)).join(", ")}.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getOutputText, parseOutputJson } from "./adapter-toolkit.js";
|
|
2
|
+
import { AutomifyError } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
export function buildRunResult(response, steps, output) {
|
|
5
|
+
const text = getOutputText(response);
|
|
6
|
+
const result = {
|
|
7
|
+
response,
|
|
8
|
+
steps,
|
|
9
|
+
ok: true,
|
|
10
|
+
status: "succeeded",
|
|
11
|
+
completed: true,
|
|
12
|
+
stopReason: "done",
|
|
13
|
+
text
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (shouldParseStructuredOutput(output, text)) {
|
|
17
|
+
const parsed = parseOutputJson(response);
|
|
18
|
+
result.parsed = typeof output.parseResult === "function" ? output.parseResult(parsed) : parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildTextConfig(output) {
|
|
25
|
+
if (!output) return undefined;
|
|
26
|
+
|
|
27
|
+
if (output.type === "text") {
|
|
28
|
+
return { format: { type: "text" } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (output.type === "json_object") {
|
|
32
|
+
return { format: { type: "json_object" } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (output.type === "json_schema") {
|
|
36
|
+
if (typeof output.name !== "string" || output.name.trim() === "") {
|
|
37
|
+
throw new AutomifyError("Structured output requires a non-empty output.name.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!output.schema || typeof output.schema !== "object") {
|
|
41
|
+
throw new AutomifyError("Structured output requires an output.schema object.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
format: removeUndefined({
|
|
46
|
+
type: "json_schema",
|
|
47
|
+
name: output.name,
|
|
48
|
+
description: output.description,
|
|
49
|
+
schema: output.schema,
|
|
50
|
+
strict: output.strict
|
|
51
|
+
})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw new AutomifyError(`Unsupported output.type: ${output.type}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildOutputInstruction(textConfig) {
|
|
59
|
+
const format = textConfig?.format;
|
|
60
|
+
if (!format || format.type === "text") return "";
|
|
61
|
+
|
|
62
|
+
if (format.type === "json_object") {
|
|
63
|
+
return [
|
|
64
|
+
"Return only a valid JSON object.",
|
|
65
|
+
"Do not wrap it in Markdown.",
|
|
66
|
+
"Do not include prose before or after the JSON."
|
|
67
|
+
].join(" ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (format.type === "json_schema") {
|
|
71
|
+
return [
|
|
72
|
+
"Return only valid JSON matching this schema.",
|
|
73
|
+
"Do not wrap it in Markdown.",
|
|
74
|
+
"Do not include prose before or after the JSON.",
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
name: format.name,
|
|
77
|
+
description: format.description,
|
|
78
|
+
schema: format.schema,
|
|
79
|
+
strict: format.strict
|
|
80
|
+
})
|
|
81
|
+
].join(" ");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldParseStructuredOutput(output, text) {
|
|
88
|
+
return Boolean(
|
|
89
|
+
text && output && (output.type === "json_schema" || output.type === "json_object") && output.parse !== false
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeUndefined(object) {
|
|
94
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
|
|
95
|
+
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { AutomifyError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
export const AUTOMIFY_OPTION_KEYS = new Set([
|
|
7
|
+
"openaiApiKey",
|
|
8
|
+
"client",
|
|
9
|
+
"computer",
|
|
10
|
+
"model",
|
|
11
|
+
"baseURL",
|
|
12
|
+
"fetchImpl",
|
|
13
|
+
"maxSteps",
|
|
14
|
+
"limits",
|
|
15
|
+
"request",
|
|
16
|
+
"requestOptions",
|
|
17
|
+
"viewport",
|
|
18
|
+
"displayWidth",
|
|
19
|
+
"displayHeight",
|
|
20
|
+
"environment",
|
|
21
|
+
"reasoning",
|
|
22
|
+
"safety",
|
|
23
|
+
"safetyIdentifier",
|
|
24
|
+
"allowedDomains",
|
|
25
|
+
"hooks",
|
|
26
|
+
"onStep",
|
|
27
|
+
"onRequest",
|
|
28
|
+
"onResponse",
|
|
29
|
+
"onComplete",
|
|
30
|
+
"screenshot",
|
|
31
|
+
"redactScreenshot",
|
|
32
|
+
"screenshotDetail",
|
|
33
|
+
"screenshotMaxWidth",
|
|
34
|
+
"screenshotMaxHeight",
|
|
35
|
+
"screenshotResize",
|
|
36
|
+
"sendInitialScreenshot",
|
|
37
|
+
"initialScreenshot",
|
|
38
|
+
"finalScreenshot",
|
|
39
|
+
"actionScreenshots",
|
|
40
|
+
"screenshots",
|
|
41
|
+
"trace",
|
|
42
|
+
"silent",
|
|
43
|
+
"debug",
|
|
44
|
+
"logFile"
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export async function callHook(hook, ...args) {
|
|
48
|
+
if (typeof hook === "function") {
|
|
49
|
+
await hook(...args);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function debugLog(debug, scope, message, details, options = {}) {
|
|
54
|
+
if (options.silent || !debug) return;
|
|
55
|
+
const label = `[${scope}] ${message}`;
|
|
56
|
+
if (typeof debug === "function") {
|
|
57
|
+
debug(label, details);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (options.full) {
|
|
61
|
+
console.error(formatFullLog(label, details));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.error(formatDefaultLog(label, details));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function logLogger(debug, options = {}) {
|
|
68
|
+
if (options.silent || !debug) return null;
|
|
69
|
+
if (typeof debug === "function") return debug;
|
|
70
|
+
return console.error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function writeDebugLogFile(logFile, scope, message, details, options = {}) {
|
|
74
|
+
if (options.silent || !logFile) return;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(dirname(logFile), { recursive: true });
|
|
78
|
+
appendFileSync(
|
|
79
|
+
logFile,
|
|
80
|
+
`${JSON.stringify({
|
|
81
|
+
at: new Date().toISOString(),
|
|
82
|
+
scope,
|
|
83
|
+
message,
|
|
84
|
+
label: `[${scope}] ${message}`,
|
|
85
|
+
details
|
|
86
|
+
})}\n`
|
|
87
|
+
);
|
|
88
|
+
} catch {
|
|
89
|
+
// Logging must not change automation behavior.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeLogFile(value, scope = "logFile") {
|
|
94
|
+
if (value == null || value === false) return undefined;
|
|
95
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
96
|
+
throw new AutomifyError(`${scope} must be a non-empty file path.`);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatDefaultLog(label, details) {
|
|
102
|
+
const summary = summarizeLogDetails(details);
|
|
103
|
+
return summary ? `${label} ${summary}` : label;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatFullLog(label, details) {
|
|
107
|
+
if (details === undefined) return label;
|
|
108
|
+
return `${label} ${JSON.stringify(details, null, 2)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function summarizeLogDetails(details) {
|
|
112
|
+
if (!details || typeof details !== "object") return "";
|
|
113
|
+
const parts = [];
|
|
114
|
+
const add = (key, value) => {
|
|
115
|
+
if (value == null || value === "") return;
|
|
116
|
+
parts.push(`${key}=${value}`);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (details.label) parts.push(details.label);
|
|
120
|
+
if (details.index != null) add("step", details.index);
|
|
121
|
+
add("phase", details.phase);
|
|
122
|
+
add("step", details.step);
|
|
123
|
+
add("action", describeAction(details.action));
|
|
124
|
+
if (details.executableAction && JSON.stringify(details.executableAction) !== JSON.stringify(details.action)) {
|
|
125
|
+
add("executed", describeAction(details.executableAction));
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(details.actions) && details.actions.length > 1) {
|
|
128
|
+
add(
|
|
129
|
+
"actions",
|
|
130
|
+
details.actions
|
|
131
|
+
.map((action) => describeAction(action))
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(",")
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
add("call", details.call?.call_id ?? details.callId);
|
|
137
|
+
if (details.safetyChecks?.length || details.call?.pending_safety_checks?.length) {
|
|
138
|
+
add("safetyChecks", details.safetyChecks?.length ?? details.call.pending_safety_checks.length);
|
|
139
|
+
}
|
|
140
|
+
if (details.currentUrl) add("url", JSON.stringify(details.currentUrl));
|
|
141
|
+
|
|
142
|
+
if (details.payload) {
|
|
143
|
+
add("phase", details.meta?.phase);
|
|
144
|
+
add("step", details.meta?.step);
|
|
145
|
+
add("model", details.payload.model);
|
|
146
|
+
add("previous", shortenId(details.payload.previous_response_id));
|
|
147
|
+
add("tools", details.payload.toolCount);
|
|
148
|
+
add("inputs", details.payload.inputCount);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (details.response) {
|
|
152
|
+
add("phase", details.meta?.phase);
|
|
153
|
+
add("step", details.meta?.step);
|
|
154
|
+
add("response", shortenId(details.response.id));
|
|
155
|
+
if (details.response.outputTypes?.length) add("outputs", details.response.outputTypes.join(","));
|
|
156
|
+
if (details.response.actions?.length) add("actions", details.response.actions.join(","));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (details.command?.command) add("command", JSON.stringify(details.command.command));
|
|
160
|
+
if (details.command?.cwd) add("cwd", JSON.stringify(details.command.cwd));
|
|
161
|
+
add("cwd", details.cwd ? JSON.stringify(details.cwd) : undefined);
|
|
162
|
+
add("timeoutMs", details.command?.timeoutMs ?? details.timeoutMs);
|
|
163
|
+
add("exitCode", details.exitCode);
|
|
164
|
+
add("signal", details.signal);
|
|
165
|
+
if (details.timedOut != null) add("timedOut", details.timedOut);
|
|
166
|
+
add("stdoutLength", details.stdoutLength);
|
|
167
|
+
add("stderrLength", details.stderrLength);
|
|
168
|
+
if (typeof details.stdout === "string" && details.stdout.length > 0) add("stdout", previewText(details.stdout));
|
|
169
|
+
if (typeof details.stderr === "string" && details.stderr.length > 0) add("stderr", previewText(details.stderr));
|
|
170
|
+
if (details.status) add("status", details.status);
|
|
171
|
+
if (details.ok != null) add("ok", details.ok);
|
|
172
|
+
if (details.completed != null) add("completed", details.completed);
|
|
173
|
+
if (details.stopReason) add("stop", details.stopReason);
|
|
174
|
+
if (Array.isArray(details.steps)) add("steps", details.steps.length);
|
|
175
|
+
if (details.containerName) add("container", details.containerName);
|
|
176
|
+
if (details.image) add("image", details.image);
|
|
177
|
+
if (details.width && details.height) add("size", `${details.width}x${details.height}`);
|
|
178
|
+
if (details.display) add("display", details.display);
|
|
179
|
+
if (details.installDependencies != null) add("installDeps", details.installDependencies);
|
|
180
|
+
if (Array.isArray(details.args)) add("args", JSON.stringify(details.args));
|
|
181
|
+
if (details.path) add("path", JSON.stringify(details.path));
|
|
182
|
+
add("bytes", details.bytes);
|
|
183
|
+
add("writtenBytes", details.writtenBytes);
|
|
184
|
+
if (details.originalWidth && details.originalHeight)
|
|
185
|
+
add("original", `${details.originalWidth}x${details.originalHeight}`);
|
|
186
|
+
if (details.resized != null) add("resized", details.resized);
|
|
187
|
+
if (details.reused != null) add("reused", details.reused);
|
|
188
|
+
add("detail", details.detail);
|
|
189
|
+
add("durationMs", details.durationMs);
|
|
190
|
+
|
|
191
|
+
return parts.join(" ");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function mergeRequestOptions(requestOptions, payload) {
|
|
195
|
+
if (!requestOptions) return payload;
|
|
196
|
+
return {
|
|
197
|
+
...requestOptions,
|
|
198
|
+
...payload
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const DO_OPTION_KEYS = new Set([
|
|
203
|
+
"data",
|
|
204
|
+
"evaluate",
|
|
205
|
+
"filesToEvaluate",
|
|
206
|
+
"model",
|
|
207
|
+
"maxSteps",
|
|
208
|
+
"limits",
|
|
209
|
+
"request",
|
|
210
|
+
"requestOptions",
|
|
211
|
+
"output",
|
|
212
|
+
"displayWidth",
|
|
213
|
+
"displayHeight",
|
|
214
|
+
"environment",
|
|
215
|
+
"reasoning",
|
|
216
|
+
"safetyIdentifier",
|
|
217
|
+
"allowedDomains",
|
|
218
|
+
"safety",
|
|
219
|
+
"onStep",
|
|
220
|
+
"onComplete",
|
|
221
|
+
"hooks",
|
|
222
|
+
"redactScreenshot",
|
|
223
|
+
"screenshotDetail",
|
|
224
|
+
"screenshotMaxWidth",
|
|
225
|
+
"screenshotMaxHeight",
|
|
226
|
+
"screenshotResize",
|
|
227
|
+
"sendInitialScreenshot",
|
|
228
|
+
"initialScreenshot",
|
|
229
|
+
"finalScreenshot",
|
|
230
|
+
"actionScreenshots",
|
|
231
|
+
"screenshots",
|
|
232
|
+
"screenshot",
|
|
233
|
+
"trace",
|
|
234
|
+
"silent",
|
|
235
|
+
"onSafetyCheck",
|
|
236
|
+
"cwd",
|
|
237
|
+
"env",
|
|
238
|
+
"shell",
|
|
239
|
+
"timeoutMs",
|
|
240
|
+
"approval",
|
|
241
|
+
"allowedCommands",
|
|
242
|
+
"blockedCommands",
|
|
243
|
+
"instructions",
|
|
244
|
+
"confirmCommand",
|
|
245
|
+
"command",
|
|
246
|
+
"commands"
|
|
247
|
+
]);
|
|
248
|
+
export const COMMAND_OPTION_KEYS = new Set([
|
|
249
|
+
"cwd",
|
|
250
|
+
"env",
|
|
251
|
+
"shell",
|
|
252
|
+
"timeoutMs",
|
|
253
|
+
"timeout",
|
|
254
|
+
"approval",
|
|
255
|
+
"allow",
|
|
256
|
+
"allowed",
|
|
257
|
+
"allowedCommands",
|
|
258
|
+
"block",
|
|
259
|
+
"blocked",
|
|
260
|
+
"blockedCommands",
|
|
261
|
+
"confirm",
|
|
262
|
+
"confirmCommand"
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
export function normalizeDoArguments(dataOrOptions, maybeOptions) {
|
|
266
|
+
if (maybeOptions !== undefined) {
|
|
267
|
+
throw new AutomifyError("do() now accepts a single run object: do(instruction, { data, output, ...options }).");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (dataOrOptions === undefined) {
|
|
271
|
+
return { data: {}, options: {} };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!dataOrOptions || typeof dataOrOptions !== "object" || Array.isArray(dataOrOptions)) {
|
|
275
|
+
throw new AutomifyError("do() run options must be an object, for example { data: {...}, output }.");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const unknownKeys = Object.keys(dataOrOptions).filter((key) => !DO_OPTION_KEYS.has(key));
|
|
279
|
+
if (unknownKeys.length > 0) {
|
|
280
|
+
throw new AutomifyError(
|
|
281
|
+
`${unknownOptionMessage("do()", unknownKeys[0], DO_OPTION_KEYS)} Put input values under data: { ${unknownKeys[0]}: ... }.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const { data = {}, ...rawOptions } = dataOrOptions;
|
|
286
|
+
return {
|
|
287
|
+
data: data ?? {},
|
|
288
|
+
options: normalizeDoOptionAliases(rawOptions)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function assertKnownOptions(scope, options, allowedKeys) {
|
|
293
|
+
if (options == null) return;
|
|
294
|
+
if (typeof options !== "object" || Array.isArray(options)) {
|
|
295
|
+
throw new AutomifyError(`${scope} options must be an object.`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const allowed = allowedKeys instanceof Set ? allowedKeys : new Set(allowedKeys);
|
|
299
|
+
const unknownKey = Object.keys(options).find((key) => !allowed.has(key));
|
|
300
|
+
if (unknownKey) {
|
|
301
|
+
throw new AutomifyError(unknownOptionMessage(scope, unknownKey, allowed));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function mergeOptionKeys(...sets) {
|
|
306
|
+
return new Set(sets.flatMap((set) => [...set]));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function pickKnownOptions(options, allowedKeys) {
|
|
310
|
+
const allowed = allowedKeys instanceof Set ? allowedKeys : new Set(allowedKeys);
|
|
311
|
+
return Object.fromEntries(Object.entries(options ?? {}).filter(([key]) => allowed.has(key)));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function normalizeAutomifyOptions(options = {}) {
|
|
315
|
+
assertKnownOptions("Automify", options, AUTOMIFY_OPTION_KEYS);
|
|
316
|
+
const { viewport, limits, request, safety, hooks, screenshots, screenshot, ...rest } = options;
|
|
317
|
+
const viewportOptions = viewport ?? {};
|
|
318
|
+
const limitOptions = limits ?? {};
|
|
319
|
+
const safetyOptions = safety ?? {};
|
|
320
|
+
const hookOptions = hooks ?? {};
|
|
321
|
+
const screenshotPaths = screenshots ?? {};
|
|
322
|
+
const screenshotOptions = screenshot ?? {};
|
|
323
|
+
|
|
324
|
+
return cleanUndefined({
|
|
325
|
+
...rest,
|
|
326
|
+
debug: rest.debug ?? false,
|
|
327
|
+
logFile: normalizeLogFile(rest.logFile, "Automify logFile"),
|
|
328
|
+
maxSteps: rest.maxSteps ?? limitOptions.steps ?? limitOptions.maxSteps,
|
|
329
|
+
requestOptions: rest.requestOptions ?? request,
|
|
330
|
+
displayWidth: rest.displayWidth ?? viewportOptions.width,
|
|
331
|
+
displayHeight: rest.displayHeight ?? viewportOptions.height,
|
|
332
|
+
safetyIdentifier: rest.safetyIdentifier ?? safetyOptions.identifier ?? safetyOptions.safetyIdentifier,
|
|
333
|
+
allowedDomains: rest.allowedDomains ?? safetyOptions.domains ?? safetyOptions.allowedDomains,
|
|
334
|
+
onStep: rest.onStep ?? hookOptions.step ?? hookOptions.onStep,
|
|
335
|
+
onComplete: rest.onComplete ?? hookOptions.complete ?? hookOptions.onComplete,
|
|
336
|
+
initialScreenshot: rest.initialScreenshot ?? screenshotPaths.initial,
|
|
337
|
+
finalScreenshot: rest.finalScreenshot ?? screenshotPaths.final,
|
|
338
|
+
actionScreenshots: rest.actionScreenshots ?? screenshotPaths.actions ?? screenshotPaths.actionScreenshots,
|
|
339
|
+
screenshotDetail: rest.screenshotDetail ?? screenshotOptions.detail,
|
|
340
|
+
screenshotMaxWidth: rest.screenshotMaxWidth ?? screenshotOptions.maxWidth ?? screenshotOptions.screenshotMaxWidth,
|
|
341
|
+
screenshotMaxHeight:
|
|
342
|
+
rest.screenshotMaxHeight ?? screenshotOptions.maxHeight ?? screenshotOptions.screenshotMaxHeight,
|
|
343
|
+
screenshotResize: rest.screenshotResize ?? screenshotOptions.resize ?? screenshotOptions.screenshotResize,
|
|
344
|
+
sendInitialScreenshot: rest.sendInitialScreenshot ?? screenshotOptions.sendInitialScreenshot,
|
|
345
|
+
redactScreenshot: rest.redactScreenshot ?? screenshotOptions.redact ?? screenshotOptions.redactScreenshot
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeDoOptionAliases(options) {
|
|
350
|
+
const { evaluate, limits, request, safety, hooks, screenshots, screenshot, command, commands, ...rest } = options;
|
|
351
|
+
|
|
352
|
+
const commandOptions = commands ?? command;
|
|
353
|
+
assertKnownOptions("do() command", commandOptions, COMMAND_OPTION_KEYS);
|
|
354
|
+
return cleanUndefined({
|
|
355
|
+
...rest,
|
|
356
|
+
filesToEvaluate: rest.filesToEvaluate ?? evaluate,
|
|
357
|
+
maxSteps: rest.maxSteps ?? limits?.steps ?? limits?.maxSteps,
|
|
358
|
+
requestOptions: rest.requestOptions ?? request,
|
|
359
|
+
safetyIdentifier: rest.safetyIdentifier ?? safety?.identifier ?? safety?.safetyIdentifier,
|
|
360
|
+
allowedDomains: rest.allowedDomains ?? safety?.domains ?? safety?.allowedDomains,
|
|
361
|
+
onSafetyCheck: rest.onSafetyCheck ?? safety?.onCheck ?? safety?.onSafetyCheck,
|
|
362
|
+
onStep: rest.onStep ?? hooks?.step ?? hooks?.onStep,
|
|
363
|
+
onComplete: rest.onComplete ?? hooks?.complete ?? hooks?.onComplete,
|
|
364
|
+
initialScreenshot: rest.initialScreenshot ?? screenshots?.initial,
|
|
365
|
+
finalScreenshot: rest.finalScreenshot ?? screenshots?.final,
|
|
366
|
+
actionScreenshots: rest.actionScreenshots ?? screenshots?.actions ?? screenshots?.actionScreenshots,
|
|
367
|
+
screenshotDetail: rest.screenshotDetail ?? screenshot?.detail,
|
|
368
|
+
screenshotMaxWidth: rest.screenshotMaxWidth ?? screenshot?.maxWidth ?? screenshot?.screenshotMaxWidth,
|
|
369
|
+
screenshotMaxHeight: rest.screenshotMaxHeight ?? screenshot?.maxHeight ?? screenshot?.screenshotMaxHeight,
|
|
370
|
+
screenshotResize: rest.screenshotResize ?? screenshot?.resize ?? screenshot?.screenshotResize,
|
|
371
|
+
sendInitialScreenshot: rest.sendInitialScreenshot ?? screenshot?.sendInitialScreenshot,
|
|
372
|
+
redactScreenshot: rest.redactScreenshot ?? screenshot?.redact ?? screenshot?.redactScreenshot,
|
|
373
|
+
cwd: rest.cwd ?? commandOptions?.cwd,
|
|
374
|
+
env: rest.env ?? commandOptions?.env,
|
|
375
|
+
shell: rest.shell ?? commandOptions?.shell,
|
|
376
|
+
timeoutMs: rest.timeoutMs ?? commandOptions?.timeoutMs ?? commandOptions?.timeout,
|
|
377
|
+
approval: rest.approval ?? commandOptions?.approval,
|
|
378
|
+
allowedCommands:
|
|
379
|
+
rest.allowedCommands ?? commandOptions?.allow ?? commandOptions?.allowed ?? commandOptions?.allowedCommands,
|
|
380
|
+
blockedCommands:
|
|
381
|
+
rest.blockedCommands ?? commandOptions?.block ?? commandOptions?.blocked ?? commandOptions?.blockedCommands,
|
|
382
|
+
confirmCommand: rest.confirmCommand ?? commandOptions?.confirm ?? commandOptions?.confirmCommand
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function cleanUndefined(value) {
|
|
387
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function unknownOptionMessage(scope, unknownKey, allowedKeys) {
|
|
391
|
+
const suggestion = closestOption(unknownKey, [...allowedKeys]);
|
|
392
|
+
return `Unknown ${scope} option ${JSON.stringify(unknownKey)}.${suggestion ? ` Did you mean ${JSON.stringify(suggestion)}?` : ""}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function closestOption(input, candidates) {
|
|
396
|
+
let best = null;
|
|
397
|
+
let bestDistance = Infinity;
|
|
398
|
+
for (const candidate of candidates) {
|
|
399
|
+
const distance = editDistance(input.toLowerCase(), candidate.toLowerCase());
|
|
400
|
+
if (distance < bestDistance) {
|
|
401
|
+
best = candidate;
|
|
402
|
+
bestDistance = distance;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return bestDistance <= Math.max(2, Math.floor(input.length / 3)) ? best : null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function editDistance(a, b) {
|
|
410
|
+
const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
411
|
+
const current = Array.from({ length: b.length + 1 }, () => 0);
|
|
412
|
+
|
|
413
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
414
|
+
current[0] = i;
|
|
415
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
416
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
417
|
+
}
|
|
418
|
+
previous.splice(0, previous.length, ...current);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return previous[b.length];
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function summarizePayload(payload) {
|
|
425
|
+
return {
|
|
426
|
+
model: payload.model,
|
|
427
|
+
previous_response_id: payload.previous_response_id,
|
|
428
|
+
toolCount: payload.tools?.length ?? 0,
|
|
429
|
+
inputCount: Array.isArray(payload.input) ? payload.input.length : undefined
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function summarizeResponse(response) {
|
|
434
|
+
return {
|
|
435
|
+
id: response?.id,
|
|
436
|
+
outputTypes: response?.output?.map((item) => item.type) ?? [],
|
|
437
|
+
actions:
|
|
438
|
+
response?.output?.flatMap((item) => {
|
|
439
|
+
if (item?.type !== "computer_call") return [];
|
|
440
|
+
const actions =
|
|
441
|
+
Array.isArray(item.actions) && item.actions.length > 0 ? item.actions : [item.action].filter(Boolean);
|
|
442
|
+
return actions.map((action) => describeAction(action)).filter(Boolean);
|
|
443
|
+
}) ?? []
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function describeAction(action) {
|
|
448
|
+
if (!action?.type) return "";
|
|
449
|
+
const parts = [action.type];
|
|
450
|
+
if (action.x != null || action.y != null) parts.push(`@${action.x ?? "?"},${action.y ?? "?"}`);
|
|
451
|
+
if (action.button) parts.push(`button:${action.button}`);
|
|
452
|
+
const keys = action.keys ?? [action.key].filter(Boolean);
|
|
453
|
+
if (keys?.length) parts.push(`keys:${keys.join("+")}`);
|
|
454
|
+
if (action.text != null) parts.push(`text:${JSON.stringify(String(action.text).slice(0, 80))}`);
|
|
455
|
+
if (action.ms != null || action.duration_ms != null) parts.push(`ms:${action.ms ?? action.duration_ms}`);
|
|
456
|
+
if (action.scroll_x != null || action.scroll_y != null)
|
|
457
|
+
parts.push(`scroll:${action.scroll_x ?? 0},${action.scroll_y ?? 0}`);
|
|
458
|
+
if (action.delta_x != null || action.delta_y != null)
|
|
459
|
+
parts.push(`delta:${action.delta_x ?? 0},${action.delta_y ?? 0}`);
|
|
460
|
+
return parts.join(":");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function shortenId(value) {
|
|
464
|
+
if (typeof value !== "string" || value.length <= 18) return value;
|
|
465
|
+
return `${value.slice(0, 10)}...${value.slice(-6)}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function previewText(value) {
|
|
469
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
470
|
+
return JSON.stringify(compact.length > 120 ? `${compact.slice(0, 117)}...` : compact);
|
|
471
|
+
}
|