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
package/src/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Automify,
|
|
3
|
+
createAutomify,
|
|
4
|
+
AutomifyError,
|
|
5
|
+
SafetyCheckError,
|
|
6
|
+
MaxStepsExceededError
|
|
7
|
+
} from "./lib/automify.js";
|
|
8
|
+
|
|
9
|
+
export { OpenAIResponsesClient } from "./lib/openai-responses-client.js";
|
|
10
|
+
export { AnthropicModelAdapter, createAnthropicModelAdapter } from "./lib/anthropic-model-adapter.js";
|
|
11
|
+
export {
|
|
12
|
+
createBrowserComputer,
|
|
13
|
+
createPlaywrightComputer,
|
|
14
|
+
executePlaywrightAction
|
|
15
|
+
} from "./lib/playwright-computer.js";
|
|
16
|
+
export {
|
|
17
|
+
captureLocalDesktopScreenshot,
|
|
18
|
+
createLocalDesktopComputer,
|
|
19
|
+
executeLocalDesktopAction
|
|
20
|
+
} from "./lib/local-desktop-computer.js";
|
|
21
|
+
export {
|
|
22
|
+
DockerDesktopSession,
|
|
23
|
+
DockerVirtualDesktopSession,
|
|
24
|
+
createDockerDesktopComputer,
|
|
25
|
+
createVirtualDesktopComputer,
|
|
26
|
+
defaultDockerDesktopImage,
|
|
27
|
+
defaultVirtualDesktopImage,
|
|
28
|
+
dockerDesktopDockerfile,
|
|
29
|
+
virtualDesktopDockerfile
|
|
30
|
+
} from "./lib/docker-desktop-computer.js";
|
|
31
|
+
export {
|
|
32
|
+
BrowserAutomify,
|
|
33
|
+
createBrowserAutomify,
|
|
34
|
+
withBrowserAutomify
|
|
35
|
+
} from "./lib/browser-automify.js";
|
|
36
|
+
export { initAutomify } from "./lib/init.js";
|
|
37
|
+
export { createModelAdapter } from "./lib/model-adapter.js";
|
|
38
|
+
export {
|
|
39
|
+
computerCall,
|
|
40
|
+
defaultAdapterScenarios,
|
|
41
|
+
functionCall,
|
|
42
|
+
getComputerTool,
|
|
43
|
+
getFunctionOutputs,
|
|
44
|
+
getInputText,
|
|
45
|
+
getLastComputerScreenshot,
|
|
46
|
+
getOutputText,
|
|
47
|
+
getTool,
|
|
48
|
+
message,
|
|
49
|
+
parseOutputJson,
|
|
50
|
+
parseDataUrl,
|
|
51
|
+
response,
|
|
52
|
+
runCommandCall,
|
|
53
|
+
testModelAdapter,
|
|
54
|
+
toDataUrl
|
|
55
|
+
} from "./lib/adapter-toolkit.js";
|
|
56
|
+
export { jsonOutput } from "./lib/output.js";
|
|
57
|
+
export {
|
|
58
|
+
DockerComputerAutomify,
|
|
59
|
+
LocalComputerAutomify,
|
|
60
|
+
createComputerAutomify,
|
|
61
|
+
createDockerComputerAutomify,
|
|
62
|
+
createLocalComputerAutomify
|
|
63
|
+
} from "./lib/computer-automify.js";
|
|
64
|
+
export {
|
|
65
|
+
CliAutomify,
|
|
66
|
+
createCliAutomify,
|
|
67
|
+
runShellCommand
|
|
68
|
+
} from "./lib/cli-automify.js";
|
|
69
|
+
export {
|
|
70
|
+
DockerCliAutomify,
|
|
71
|
+
DockerCliSession,
|
|
72
|
+
createDockerCliAutomify,
|
|
73
|
+
DockerVirtualCliSession,
|
|
74
|
+
VirtualCliAutomify,
|
|
75
|
+
createVirtualCliAutomify
|
|
76
|
+
} from "./lib/docker-cli-automify.js";
|
|
77
|
+
export {
|
|
78
|
+
fileToEvaluate,
|
|
79
|
+
fileToData,
|
|
80
|
+
filesToEvaluate,
|
|
81
|
+
filesToData
|
|
82
|
+
} from "./lib/file-data.js";
|
|
83
|
+
export { argumentReference } from "./lib/argument-reference.js";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { hostname, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { AutomifyError } from "./errors.js";
|
|
7
|
+
|
|
8
|
+
const LOCK_ROOT = join(tmpdir(), "automify-locks");
|
|
9
|
+
|
|
10
|
+
export async function acquireAdapterLock(resource, options = {}) {
|
|
11
|
+
if (!resource) return null;
|
|
12
|
+
|
|
13
|
+
const label = options.label ?? resource;
|
|
14
|
+
const token = randomUUID();
|
|
15
|
+
const lockDir = adapterLockPath(resource);
|
|
16
|
+
const owner = {
|
|
17
|
+
pid: process.pid,
|
|
18
|
+
resource,
|
|
19
|
+
label,
|
|
20
|
+
token,
|
|
21
|
+
hostname: safeHostname(),
|
|
22
|
+
createdAt: new Date().toISOString()
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
await mkdir(LOCK_ROOT, { recursive: true });
|
|
26
|
+
|
|
27
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
28
|
+
try {
|
|
29
|
+
await mkdir(lockDir);
|
|
30
|
+
await writeFile(join(lockDir, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`);
|
|
31
|
+
return async () => {
|
|
32
|
+
await releaseAdapterLock(lockDir, token);
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error?.code !== "EEXIST") throw error;
|
|
36
|
+
if (!(await removeStaleLock(lockDir))) {
|
|
37
|
+
throw new AutomifyError(adapterLockErrorMessage(label, lockDir, await readLockOwner(lockDir)));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw new AutomifyError(adapterLockErrorMessage(label, lockDir, await readLockOwner(lockDir)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function adapterLockPath(resource) {
|
|
46
|
+
const digest = createHash("sha256").update(String(resource)).digest("hex").slice(0, 24);
|
|
47
|
+
return join(LOCK_ROOT, digest);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function releaseAdapterLock(lockDir, token) {
|
|
51
|
+
const owner = await readLockOwner(lockDir);
|
|
52
|
+
if (owner?.token !== token) return;
|
|
53
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function removeStaleLock(lockDir) {
|
|
57
|
+
const owner = await readLockOwner(lockDir);
|
|
58
|
+
if (!owner || isPidAlive(owner.pid)) return false;
|
|
59
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readLockOwner(lockDir) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(await readFile(join(lockDir, "owner.json"), "utf8"));
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPidAlive(pid) {
|
|
72
|
+
const normalized = Number(pid);
|
|
73
|
+
if (!Number.isInteger(normalized) || normalized <= 0) return false;
|
|
74
|
+
try {
|
|
75
|
+
process.kill(normalized, 0);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return error?.code === "EPERM";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function adapterLockErrorMessage(label, lockDir, owner) {
|
|
83
|
+
const details = owner?.pid ? `pid ${owner.pid}` : "an unknown process";
|
|
84
|
+
return `${label} is already in use by ${details}. Close the existing adapter before creating another one, or remove stale lock ${lockDir}.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function safeHostname() {
|
|
88
|
+
try {
|
|
89
|
+
return hostname();
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { AutomifyError } from "./errors.js";
|
|
2
|
+
import { createModelAdapter } from "./model-adapter.js";
|
|
3
|
+
|
|
4
|
+
export function response({ id = `resp_${Date.now()}`, output = [], ...rest } = {}) {
|
|
5
|
+
return { id, output, ...rest };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function message(text, options = {}) {
|
|
9
|
+
return {
|
|
10
|
+
type: "message",
|
|
11
|
+
content: [
|
|
12
|
+
{
|
|
13
|
+
type: "output_text",
|
|
14
|
+
text
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function computerCall(action, options = {}) {
|
|
22
|
+
const callId = options.callId ?? options.call_id ?? `call_${Date.now()}`;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
type: "computer_call",
|
|
26
|
+
call_id: callId,
|
|
27
|
+
action,
|
|
28
|
+
pending_safety_checks: options.pendingSafetyChecks ?? options.pending_safety_checks ?? [],
|
|
29
|
+
status: options.status ?? "completed",
|
|
30
|
+
...withoutKeys(options, ["callId", "call_id", "pendingSafetyChecks", "pending_safety_checks", "status"])
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runCommandCall(command, options = {}) {
|
|
35
|
+
const callId = options.callId ?? options.call_id ?? `call_${Date.now()}`;
|
|
36
|
+
|
|
37
|
+
return functionCall("run_command", { command, cwd: options.cwd, timeoutMs: options.timeoutMs }, {
|
|
38
|
+
...withoutKeys(options, ["callId", "call_id", "cwd", "timeoutMs"]),
|
|
39
|
+
callId
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function functionCall(name, args = {}, options = {}) {
|
|
44
|
+
return {
|
|
45
|
+
type: "function_call",
|
|
46
|
+
name,
|
|
47
|
+
call_id: options.callId ?? options.call_id ?? `call_${Date.now()}`,
|
|
48
|
+
arguments: typeof args === "string" ? args : JSON.stringify(removeUndefined(args)),
|
|
49
|
+
...withoutKeys(options, ["callId", "call_id"])
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getInputText(payload) {
|
|
54
|
+
const chunks = [];
|
|
55
|
+
|
|
56
|
+
for (const item of payload.input ?? []) {
|
|
57
|
+
for (const content of item.content ?? []) {
|
|
58
|
+
if (content?.type === "input_text" && typeof content.text === "string") {
|
|
59
|
+
chunks.push(content.text);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return chunks.join("\n\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getTool(payload, typeOrName) {
|
|
68
|
+
return payload.tools?.find((tool) => tool.type === typeOrName || tool.name === typeOrName) ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getComputerTool(payload) {
|
|
72
|
+
return getTool(payload, "computer");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getLastComputerScreenshot(payload) {
|
|
76
|
+
for (const item of [...(payload.input ?? [])].reverse()) {
|
|
77
|
+
if (item?.type === "computer_call_output" && item.output?.image_url) {
|
|
78
|
+
return parseDataUrl(item.output.image_url);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const content of [...(item.content ?? [])].reverse()) {
|
|
82
|
+
if ((content?.type === "input_image" || content?.type === "computer_screenshot") && content.image_url) {
|
|
83
|
+
return parseDataUrl(content.image_url);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getFunctionOutputs(payload) {
|
|
92
|
+
return (payload.input ?? [])
|
|
93
|
+
.filter((item) => item?.type === "function_call_output")
|
|
94
|
+
.map((item) => ({
|
|
95
|
+
callId: item.call_id,
|
|
96
|
+
output: parseMaybeJson(item.output)
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getOutputText(response) {
|
|
101
|
+
const chunks = [];
|
|
102
|
+
|
|
103
|
+
for (const item of response?.output ?? []) {
|
|
104
|
+
if (item?.type !== "message") continue;
|
|
105
|
+
|
|
106
|
+
for (const content of item.content ?? []) {
|
|
107
|
+
if (content?.type === "output_text" && typeof content.text === "string") {
|
|
108
|
+
chunks.push(content.text);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return chunks.join("\n\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseOutputJson(response) {
|
|
117
|
+
const text = getOutputText(response);
|
|
118
|
+
if (!text) {
|
|
119
|
+
throw new AutomifyError("Expected the model response to contain output text.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(text);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new AutomifyError("Expected the model response text to be valid JSON.", { cause: error });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function parseDataUrl(value) {
|
|
130
|
+
if (typeof value !== "string") {
|
|
131
|
+
throw new AutomifyError("Expected a data URL or base64 string.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const match = value.match(/^data:([^;,]+)?(;base64)?,(.*)$/);
|
|
135
|
+
if (!match) {
|
|
136
|
+
return {
|
|
137
|
+
mediaType: "image/png",
|
|
138
|
+
base64: value,
|
|
139
|
+
buffer: Buffer.from(value, "base64")
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const mediaType = match[1] || "application/octet-stream";
|
|
144
|
+
const isBase64 = Boolean(match[2]);
|
|
145
|
+
const data = match[3] ?? "";
|
|
146
|
+
const buffer = isBase64 ? Buffer.from(data, "base64") : Buffer.from(decodeURIComponent(data));
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
mediaType,
|
|
150
|
+
base64: isBase64 ? data : buffer.toString("base64"),
|
|
151
|
+
buffer
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function toDataUrl(input, mediaType = "image/png") {
|
|
156
|
+
if (typeof input === "string" && input.startsWith("data:")) return input;
|
|
157
|
+
if (typeof input === "string") return `data:${mediaType};base64,${input}`;
|
|
158
|
+
if (input instanceof ArrayBuffer) return `data:${mediaType};base64,${Buffer.from(input).toString("base64")}`;
|
|
159
|
+
if (ArrayBuffer.isView(input)) {
|
|
160
|
+
return `data:${mediaType};base64,${Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString("base64")}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new AutomifyError("toDataUrl input must be a data URL, base64 string, Buffer, Uint8Array, or ArrayBuffer.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function testModelAdapter(adapter, scenarios = defaultAdapterScenarios()) {
|
|
167
|
+
const modelAdapter = createModelAdapter(adapter);
|
|
168
|
+
|
|
169
|
+
for (const scenario of scenarios) {
|
|
170
|
+
const result = await modelAdapter.createResponse(scenario.payload, scenario.context);
|
|
171
|
+
scenario.assert?.(result);
|
|
172
|
+
assertResponseShape(result, scenario.name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function defaultAdapterScenarios() {
|
|
177
|
+
return [
|
|
178
|
+
{
|
|
179
|
+
name: "text",
|
|
180
|
+
context: { surface: "cli", phase: "initial" },
|
|
181
|
+
payload: {
|
|
182
|
+
model: "adapter-test",
|
|
183
|
+
input: [{ role: "user", content: [{ type: "input_text", text: "Say hello" }] }],
|
|
184
|
+
tools: []
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "computer",
|
|
189
|
+
context: { surface: "computer", phase: "initial" },
|
|
190
|
+
payload: {
|
|
191
|
+
model: "adapter-test",
|
|
192
|
+
input: [{ role: "user", content: [{ type: "input_text", text: "Click the button" }] }],
|
|
193
|
+
tools: [{ type: "computer" }]
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "cli",
|
|
198
|
+
context: { surface: "cli", phase: "initial" },
|
|
199
|
+
payload: {
|
|
200
|
+
model: "adapter-test",
|
|
201
|
+
input: [{ role: "user", content: [{ type: "input_text", text: "Run tests" }] }],
|
|
202
|
+
tools: [{ type: "function", name: "run_command" }]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertResponseShape(result, scenarioName) {
|
|
209
|
+
if (!result || typeof result !== "object") {
|
|
210
|
+
throw new AutomifyError(`Adapter scenario '${scenarioName}' did not return an object.`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof result.id !== "string" || result.id.length === 0) {
|
|
214
|
+
throw new AutomifyError(`Adapter scenario '${scenarioName}' must return a string id.`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!Array.isArray(result.output)) {
|
|
218
|
+
throw new AutomifyError(`Adapter scenario '${scenarioName}' must return an output array.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function withoutKeys(object, keys) {
|
|
223
|
+
const blocked = new Set(keys);
|
|
224
|
+
return Object.fromEntries(Object.entries(object).filter(([key]) => !blocked.has(key)));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function removeUndefined(object) {
|
|
228
|
+
return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseMaybeJson(value) {
|
|
232
|
+
if (typeof value !== "string") return value;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(value);
|
|
236
|
+
} catch {
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
}
|