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,89 @@
|
|
|
1
|
+
import { Automify } from "./automify.js";
|
|
2
|
+
import { createBrowserComputer } from "./playwright-computer.js";
|
|
3
|
+
import { applyBrowserPreset } from "./presets.js";
|
|
4
|
+
import { AUTOMIFY_OPTION_KEYS, assertKnownOptions, mergeOptionKeys, pickKnownOptions } from "./runtime.js";
|
|
5
|
+
|
|
6
|
+
const BROWSER_AUTOMIFY_OPTION_KEYS = mergeOptionKeys(AUTOMIFY_OPTION_KEYS, [
|
|
7
|
+
"computer",
|
|
8
|
+
"playwright",
|
|
9
|
+
"browser",
|
|
10
|
+
"browserName",
|
|
11
|
+
"browserOptions",
|
|
12
|
+
"headless",
|
|
13
|
+
"startUrl",
|
|
14
|
+
"url",
|
|
15
|
+
"launch",
|
|
16
|
+
"launchOptions",
|
|
17
|
+
"context",
|
|
18
|
+
"contextOptions",
|
|
19
|
+
"navigation",
|
|
20
|
+
"gotoOptions",
|
|
21
|
+
"actionDelayMs",
|
|
22
|
+
"waitMs",
|
|
23
|
+
"onUnknownAction"
|
|
24
|
+
]);
|
|
25
|
+
const BROWSER_OPTIONS_KEYS = new Set(["name", "launch", "context", "navigation"]);
|
|
26
|
+
|
|
27
|
+
export async function createBrowserAutomify(options = {}) {
|
|
28
|
+
assertKnownOptions("browser adapter", options, BROWSER_AUTOMIFY_OPTION_KEYS);
|
|
29
|
+
assertKnownOptions("browserOptions", options.browserOptions, BROWSER_OPTIONS_KEYS);
|
|
30
|
+
options = applyBrowserPreset(options);
|
|
31
|
+
const browserOptions = browserOptionsFrom(options);
|
|
32
|
+
const computer = options.computer ?? (await createBrowserComputer(browserOptions));
|
|
33
|
+
|
|
34
|
+
return new BrowserAutomify({
|
|
35
|
+
...options,
|
|
36
|
+
computer
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function withBrowserAutomify(options, run) {
|
|
41
|
+
const automify = await createBrowserAutomify(options);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return await run(automify);
|
|
45
|
+
} finally {
|
|
46
|
+
await automify.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class BrowserAutomify extends Automify {
|
|
51
|
+
constructor(options) {
|
|
52
|
+
super(pickKnownOptions(options, AUTOMIFY_OPTION_KEYS));
|
|
53
|
+
this.browser = this.computer.browser;
|
|
54
|
+
this.context = this.computer.context;
|
|
55
|
+
this.page = this.computer.page;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async goto(url, options) {
|
|
59
|
+
await this.computer.goto(url, options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async close() {
|
|
63
|
+
if (typeof this.computer.close === "function") {
|
|
64
|
+
await this.computer.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function browserOptionsFrom(options) {
|
|
70
|
+
const viewport = options.viewport ?? {};
|
|
71
|
+
const browserOptions = options.browserOptions ?? {};
|
|
72
|
+
return {
|
|
73
|
+
playwright: options.playwright,
|
|
74
|
+
browserName: options.browserName ?? options.browser ?? browserOptions.name,
|
|
75
|
+
headless: options.headless,
|
|
76
|
+
url: options.url ?? options.startUrl,
|
|
77
|
+
displayWidth: options.displayWidth ?? viewport.width,
|
|
78
|
+
displayHeight: options.displayHeight ?? viewport.height,
|
|
79
|
+
environment: options.environment,
|
|
80
|
+
launchOptions: options.launchOptions ?? options.launch ?? browserOptions.launch,
|
|
81
|
+
contextOptions: options.contextOptions ?? options.context ?? browserOptions.context,
|
|
82
|
+
gotoOptions: options.gotoOptions ?? options.navigation ?? browserOptions.navigation,
|
|
83
|
+
waitMs: options.waitMs ?? options.actionDelayMs,
|
|
84
|
+
silent: options.silent,
|
|
85
|
+
debug: options.debug ?? false,
|
|
86
|
+
logFile: options.logFile,
|
|
87
|
+
onUnknownAction: options.onUnknownAction
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { AutomifyError, MaxStepsExceededError } from "./errors.js";
|
|
3
|
+
import { OpenAIResponsesClient } from "./openai-responses-client.js";
|
|
4
|
+
import { filesToEvaluate } from "./file-data.js";
|
|
5
|
+
import { applyCliPreset } from "./presets.js";
|
|
6
|
+
import { buildRunResult, buildTextConfig } from "./result.js";
|
|
7
|
+
import {
|
|
8
|
+
AUTOMIFY_OPTION_KEYS,
|
|
9
|
+
COMMAND_OPTION_KEYS,
|
|
10
|
+
assertKnownOptions,
|
|
11
|
+
callHook,
|
|
12
|
+
debugLog,
|
|
13
|
+
mergeOptionKeys,
|
|
14
|
+
mergeRequestOptions,
|
|
15
|
+
normalizeLogFile,
|
|
16
|
+
normalizeDoArguments,
|
|
17
|
+
summarizePayload,
|
|
18
|
+
summarizeResponse,
|
|
19
|
+
writeDebugLogFile
|
|
20
|
+
} from "./runtime.js";
|
|
21
|
+
|
|
22
|
+
const DEFAULT_MAX_STEPS = 1000;
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
24
|
+
const CLI_OPTION_KEYS = mergeOptionKeys(AUTOMIFY_OPTION_KEYS, [
|
|
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
|
+
"preset"
|
|
39
|
+
]);
|
|
40
|
+
const RUN_COMMAND_TOOL = {
|
|
41
|
+
type: "function",
|
|
42
|
+
name: "run_command",
|
|
43
|
+
description:
|
|
44
|
+
"Run a shell command in the configured working directory and return stdout, stderr, and the exit code. If the instructions include a command policy, the full command string must satisfy that policy before you call this tool.",
|
|
45
|
+
strict: true,
|
|
46
|
+
parameters: {
|
|
47
|
+
type: "object",
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
properties: {
|
|
50
|
+
command: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description:
|
|
53
|
+
"The full shell command to run. If a command policy is present, this entire string is checked as one command."
|
|
54
|
+
},
|
|
55
|
+
cwd: {
|
|
56
|
+
type: ["string", "null"],
|
|
57
|
+
description: "Optional working directory. Use null to use the configured cwd."
|
|
58
|
+
},
|
|
59
|
+
timeoutMs: {
|
|
60
|
+
type: ["number", "null"],
|
|
61
|
+
description: "Optional command timeout in milliseconds. Use null to use the configured timeout."
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
required: ["command", "cwd", "timeoutMs"]
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function createCliAutomify(options = {}) {
|
|
69
|
+
return new CliAutomify(options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeCliOptions(options = {}) {
|
|
73
|
+
assertKnownOptions("CLI adapter", options, CLI_OPTION_KEYS);
|
|
74
|
+
assertKnownOptions("CLI command", options.command, COMMAND_OPTION_KEYS);
|
|
75
|
+
assertKnownOptions("CLI command", options.commands, COMMAND_OPTION_KEYS);
|
|
76
|
+
options = applyCliPreset(options);
|
|
77
|
+
const command = options.commands ?? options.command ?? {};
|
|
78
|
+
const confirmCommand = options.confirmCommand ?? command.confirm ?? command.confirmCommand;
|
|
79
|
+
const limits = options.limits ?? {};
|
|
80
|
+
const hooks = options.hooks ?? {};
|
|
81
|
+
const safety = options.safety ?? {};
|
|
82
|
+
const logFile = normalizeLogFile(options.logFile, "CLI logFile");
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...options,
|
|
86
|
+
debug: options.debug ?? false,
|
|
87
|
+
logFile,
|
|
88
|
+
maxSteps: options.maxSteps ?? limits.steps ?? limits.maxSteps,
|
|
89
|
+
requestOptions: options.requestOptions ?? options.request,
|
|
90
|
+
cwd: options.cwd ?? command.cwd,
|
|
91
|
+
env: options.env ?? command.env,
|
|
92
|
+
shell: options.shell ?? command.shell,
|
|
93
|
+
timeoutMs: options.timeoutMs ?? command.timeoutMs ?? command.timeout,
|
|
94
|
+
allowedCommands: options.allowedCommands ?? command.allow ?? command.allowed ?? command.allowedCommands,
|
|
95
|
+
blockedCommands: options.blockedCommands ?? command.block ?? command.blocked ?? command.blockedCommands,
|
|
96
|
+
confirmCommand,
|
|
97
|
+
approval: options.approval ?? command.approval,
|
|
98
|
+
onStep: options.onStep ?? hooks.step ?? hooks.onStep,
|
|
99
|
+
onComplete: options.onComplete ?? hooks.complete ?? hooks.onComplete,
|
|
100
|
+
safetyIdentifier: options.safetyIdentifier ?? safety.identifier ?? safety.safetyIdentifier
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class CliAutomify {
|
|
105
|
+
constructor(options = {}) {
|
|
106
|
+
const {
|
|
107
|
+
openaiApiKey,
|
|
108
|
+
client,
|
|
109
|
+
model,
|
|
110
|
+
baseURL,
|
|
111
|
+
fetchImpl,
|
|
112
|
+
maxSteps = DEFAULT_MAX_STEPS,
|
|
113
|
+
requestOptions,
|
|
114
|
+
cwd = process.cwd(),
|
|
115
|
+
env,
|
|
116
|
+
shell = true,
|
|
117
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
118
|
+
runner = runShellCommand,
|
|
119
|
+
confirmCommand,
|
|
120
|
+
approval = confirmCommand ? "always" : "never",
|
|
121
|
+
allowedCommands,
|
|
122
|
+
blockedCommands,
|
|
123
|
+
onStep,
|
|
124
|
+
onRequest,
|
|
125
|
+
onResponse,
|
|
126
|
+
onComplete,
|
|
127
|
+
debug,
|
|
128
|
+
logFile,
|
|
129
|
+
silent,
|
|
130
|
+
reasoning,
|
|
131
|
+
safetyIdentifier
|
|
132
|
+
} = normalizeCliOptions(options);
|
|
133
|
+
|
|
134
|
+
this.client = client ?? new OpenAIResponsesClient({ openaiApiKey, baseURL, fetchImpl });
|
|
135
|
+
this.model = model;
|
|
136
|
+
this.maxSteps = maxSteps;
|
|
137
|
+
this.requestOptions = requestOptions;
|
|
138
|
+
this.cwd = cwd;
|
|
139
|
+
this.env = env;
|
|
140
|
+
this.shell = shell;
|
|
141
|
+
this.timeoutMs = timeoutMs;
|
|
142
|
+
this.runner = runner;
|
|
143
|
+
this.confirmCommand = confirmCommand;
|
|
144
|
+
this.approval = approval;
|
|
145
|
+
this.allowedCommands = allowedCommands;
|
|
146
|
+
this.blockedCommands = blockedCommands;
|
|
147
|
+
this.onStep = onStep;
|
|
148
|
+
this.onRequest = onRequest;
|
|
149
|
+
this.onResponse = onResponse;
|
|
150
|
+
this.onComplete = onComplete;
|
|
151
|
+
this.debug = debug;
|
|
152
|
+
this.logFile = logFile;
|
|
153
|
+
this.silent = silent;
|
|
154
|
+
this.reasoning = reasoning;
|
|
155
|
+
this.safetyIdentifier = safetyIdentifier;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async do(instruction, runOptions = {}, maybeOptions) {
|
|
159
|
+
if (typeof instruction !== "string" || instruction.trim() === "") {
|
|
160
|
+
throw new AutomifyError("instruction must be a non-empty string.");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { data, options } = normalizeDoArguments(runOptions, maybeOptions);
|
|
164
|
+
const previousSilent = this.silent;
|
|
165
|
+
if ("silent" in options) this.silent = options.silent;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const maxSteps = options.maxSteps ?? this.maxSteps;
|
|
169
|
+
const steps = [];
|
|
170
|
+
const requestOptions = options.requestOptions ?? this.requestOptions;
|
|
171
|
+
const model = assertModel(options.model ?? this.model);
|
|
172
|
+
const allowedCommands = options.allowedCommands ?? this.allowedCommands;
|
|
173
|
+
const blockedCommands = options.blockedCommands ?? this.blockedCommands;
|
|
174
|
+
const approval = options.approval ?? this.approval;
|
|
175
|
+
let response = await this.#createResponse(
|
|
176
|
+
mergeRequestOptions(requestOptions, {
|
|
177
|
+
model,
|
|
178
|
+
instructions: cliInstructions({
|
|
179
|
+
instructions: options.instructions,
|
|
180
|
+
allowedCommands,
|
|
181
|
+
blockedCommands,
|
|
182
|
+
approval
|
|
183
|
+
}),
|
|
184
|
+
tools: [RUN_COMMAND_TOOL],
|
|
185
|
+
tool_choice: "auto",
|
|
186
|
+
input: [
|
|
187
|
+
{
|
|
188
|
+
role: "user",
|
|
189
|
+
content: [
|
|
190
|
+
{ type: "input_text", text: formatCliInstruction(instruction, data, options.cwd ?? this.cwd) },
|
|
191
|
+
...await evaluationContentFor(options.filesToEvaluate)
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
text: buildTextConfig(options.output),
|
|
196
|
+
reasoning: options.reasoning ?? this.reasoning,
|
|
197
|
+
safety_identifier: options.safetyIdentifier ?? this.safetyIdentifier,
|
|
198
|
+
truncation: "auto"
|
|
199
|
+
}),
|
|
200
|
+
{ phase: "initial", surface: "cli", requestOptions }
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
204
|
+
const calls = findRunCommandCalls(response);
|
|
205
|
+
|
|
206
|
+
if (calls.length === 0) {
|
|
207
|
+
const result = buildRunResult(response, steps, options.output);
|
|
208
|
+
await this.#complete(result, { instruction, data }, options);
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const input = [];
|
|
213
|
+
|
|
214
|
+
for (const call of calls) {
|
|
215
|
+
const command = parseRunCommand(call);
|
|
216
|
+
assertCommandPolicy(command.command, allowedCommands, blockedCommands);
|
|
217
|
+
await this.#confirm(command, call, response, options);
|
|
218
|
+
await this.#emitStep(
|
|
219
|
+
{
|
|
220
|
+
index: steps.length,
|
|
221
|
+
phase: "before_command",
|
|
222
|
+
command,
|
|
223
|
+
call,
|
|
224
|
+
response
|
|
225
|
+
},
|
|
226
|
+
options
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
this.#debug("command", {
|
|
230
|
+
command,
|
|
231
|
+
cwd: command.cwd ?? options.cwd ?? this.cwd,
|
|
232
|
+
timeoutMs: command.timeoutMs ?? options.timeoutMs ?? this.timeoutMs
|
|
233
|
+
});
|
|
234
|
+
const output = await this.runner(command.command, {
|
|
235
|
+
cwd: command.cwd ?? options.cwd ?? this.cwd,
|
|
236
|
+
env: options.env ?? this.env,
|
|
237
|
+
shell: options.shell ?? this.shell,
|
|
238
|
+
timeoutMs: command.timeoutMs ?? options.timeoutMs ?? this.timeoutMs
|
|
239
|
+
});
|
|
240
|
+
this.#debug("command_result", {
|
|
241
|
+
command,
|
|
242
|
+
exitCode: output?.exitCode,
|
|
243
|
+
signal: output?.signal,
|
|
244
|
+
timedOut: output?.timedOut,
|
|
245
|
+
stdout: output?.stdout,
|
|
246
|
+
stderr: output?.stderr,
|
|
247
|
+
stdoutLength: typeof output?.stdout === "string" ? output.stdout.length : undefined,
|
|
248
|
+
stderrLength: typeof output?.stderr === "string" ? output.stderr.length : undefined
|
|
249
|
+
});
|
|
250
|
+
await this.#emitStep(
|
|
251
|
+
{
|
|
252
|
+
index: steps.length,
|
|
253
|
+
phase: "after_command",
|
|
254
|
+
command,
|
|
255
|
+
call,
|
|
256
|
+
response,
|
|
257
|
+
output
|
|
258
|
+
},
|
|
259
|
+
options
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
steps.push({
|
|
263
|
+
index: steps.length,
|
|
264
|
+
callId: call.call_id,
|
|
265
|
+
command,
|
|
266
|
+
output
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
input.push({
|
|
270
|
+
type: "function_call_output",
|
|
271
|
+
call_id: call.call_id,
|
|
272
|
+
output: JSON.stringify(output)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
response = await this.#createResponse(
|
|
277
|
+
mergeRequestOptions(requestOptions, {
|
|
278
|
+
model,
|
|
279
|
+
previous_response_id: response.id,
|
|
280
|
+
tools: [RUN_COMMAND_TOOL],
|
|
281
|
+
input,
|
|
282
|
+
text: buildTextConfig(options.output),
|
|
283
|
+
truncation: "auto"
|
|
284
|
+
}),
|
|
285
|
+
{ phase: "continue", surface: "cli", step, requestOptions }
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
throw new MaxStepsExceededError(maxSteps);
|
|
290
|
+
} finally {
|
|
291
|
+
this.silent = previousSilent;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async #confirm(command, call, response, options) {
|
|
296
|
+
const approval = options.approval ?? this.approval;
|
|
297
|
+
if (approval === "never") return;
|
|
298
|
+
|
|
299
|
+
if (typeof this.confirmCommand !== "function" && typeof options.confirmCommand !== "function") {
|
|
300
|
+
throw new AutomifyError("CLI command approval is required. Pass confirmCommand or set approval: 'never'.");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const confirmCommand = options.confirmCommand ?? this.confirmCommand;
|
|
304
|
+
const approved = await confirmCommand({ command, call, response });
|
|
305
|
+
|
|
306
|
+
if (!approved) {
|
|
307
|
+
throw new AutomifyError(`CLI command was not approved: ${command.command}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async #createResponse(payload, meta) {
|
|
312
|
+
await callHook(this.onRequest, payload, meta);
|
|
313
|
+
this.#debug("request", { meta, payload: summarizePayload(payload) });
|
|
314
|
+
const response = await this.client.createResponse(payload, meta);
|
|
315
|
+
await callHook(this.onResponse, response, meta);
|
|
316
|
+
this.#debug("response", { meta, response: summarizeResponse(response) });
|
|
317
|
+
return response;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async #emitStep(event, options) {
|
|
321
|
+
await callHook(this.onStep, event);
|
|
322
|
+
await callHook(options.onStep, event);
|
|
323
|
+
this.#debug("step", event);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async #complete(result, context, options) {
|
|
327
|
+
const event = {
|
|
328
|
+
instruction: context.instruction,
|
|
329
|
+
data: context.data,
|
|
330
|
+
result,
|
|
331
|
+
response: result.response,
|
|
332
|
+
steps: result.steps,
|
|
333
|
+
ok: result.ok,
|
|
334
|
+
status: result.status,
|
|
335
|
+
completed: result.completed,
|
|
336
|
+
stopReason: result.stopReason,
|
|
337
|
+
surface: "cli"
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
await callHook(this.onComplete, event);
|
|
341
|
+
await callHook(options.onComplete, event);
|
|
342
|
+
this.#debug("complete", event);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#debug(message, details) {
|
|
346
|
+
writeDebugLogFile(this.logFile, "automify:cli", message, details, { silent: this.silent });
|
|
347
|
+
debugLog(this.debug, "automify:cli", message, details, { silent: this.silent });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function cliInstructions(options) {
|
|
352
|
+
return [
|
|
353
|
+
options.instructions ??
|
|
354
|
+
[
|
|
355
|
+
"You are controlling a shell through the run_command tool.",
|
|
356
|
+
"Use deterministic, task-directed commands. Do not run exploratory commands as a probe when the next command is already clear. Prefer project-local tooling and explicit working directories.",
|
|
357
|
+
"Inspect first only when the right command, file, or project layout is uncertain; make the inspection narrow and explainable by the task. Prefer fast, focused commands such as pwd, ls, find targets, or ripgrep over broad scans.",
|
|
358
|
+
"Prefer focused, verifiable commands that produce bounded output. Avoid long-running, interactive, destructive, network, or environment-changing commands unless the task requires them and policy allows them.",
|
|
359
|
+
"Do not repeat a command after no useful change unless you change the hypothesis, arguments, working directory, or diagnostic target.",
|
|
360
|
+
"After a command changes files, runs tests, or produces the requested result, decide from its output whether another command is necessary. Stop when the task is complete and return a concise summary instead of calling more tools."
|
|
361
|
+
].join("\n"),
|
|
362
|
+
commandPolicyGuidance(options)
|
|
363
|
+
].filter(Boolean).join("\n\n");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function assertModel(model) {
|
|
367
|
+
if (typeof model !== "string" || model.trim() === "") {
|
|
368
|
+
throw new AutomifyError("A model is required. Pass model to initAutomify(), cli(), or do().");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return model;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function formatCliInstruction(instruction, data, cwd) {
|
|
375
|
+
const parts = [`Task:\n${instruction}`, `Working directory:\n${cwd}`];
|
|
376
|
+
|
|
377
|
+
if (data != null && !(typeof data === "object" && Object.keys(data).length === 0)) {
|
|
378
|
+
parts.push(`Data:\n${JSON.stringify(data, null, 2)}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return parts.join("\n\n");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function evaluationContentFor(files) {
|
|
385
|
+
if (files == null) return [];
|
|
386
|
+
return filesToEvaluate(files);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function findRunCommandCalls(response) {
|
|
390
|
+
return response?.output?.filter((item) => item.type === "function_call" && item.name === "run_command") ?? [];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function parseRunCommand(call) {
|
|
394
|
+
try {
|
|
395
|
+
const args = JSON.parse(call.arguments || "{}");
|
|
396
|
+
if (typeof args.command !== "string" || args.command.trim() === "") {
|
|
397
|
+
throw new AutomifyError("run_command requires a non-empty command argument.");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
command: args.command,
|
|
402
|
+
cwd: args.cwd ?? undefined,
|
|
403
|
+
timeoutMs: args.timeoutMs ?? undefined
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error instanceof AutomifyError) throw error;
|
|
407
|
+
throw new AutomifyError("run_command arguments must be valid JSON.", { cause: error });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function assertCommandPolicy(command, allowedCommands, blockedCommands) {
|
|
412
|
+
if (blockedCommands?.some((rule) => matchesCommandRule(command, rule))) {
|
|
413
|
+
throw new AutomifyError(`CLI command is blocked by policy: ${command}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (allowedCommands?.length && !allowedCommands.some((rule) => matchesCommandRule(command, rule))) {
|
|
417
|
+
throw new AutomifyError(`CLI command is not allowed by policy: ${command}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function commandPolicyGuidance(options) {
|
|
422
|
+
const lines = [];
|
|
423
|
+
const allowed = commandRulesGuidance(options.allowedCommands);
|
|
424
|
+
const blocked = commandRulesGuidance(options.blockedCommands);
|
|
425
|
+
|
|
426
|
+
if (allowed) {
|
|
427
|
+
lines.push(`Only call run_command with commands matching one of these allowed command rules: ${allowed}.`);
|
|
428
|
+
lines.push(
|
|
429
|
+
"This allowlist is mandatory. Before every run_command call, compare the exact full command string you are about to send against the allowed rules."
|
|
430
|
+
);
|
|
431
|
+
lines.push(
|
|
432
|
+
"The policy is checked against the full shell command string, not individual words inside it. Shell operators and conditionals such as &&, ||, ;, pipes, if, test, and [ ] do not make an unlisted command valid."
|
|
433
|
+
);
|
|
434
|
+
lines.push(
|
|
435
|
+
'For example, if only "cat" is allowed, do not call commands like "ls data && cat data/file", "if [ -f data/file ]; then cat data/file; fi", "sh -lc ...", or any other wrapper/listing/test command unless the entire string matches an allowed rule.'
|
|
436
|
+
);
|
|
437
|
+
lines.push(
|
|
438
|
+
"If the task cannot be completed with commands that match the allowlist, stop and explain which command rule is missing instead of trying a near match."
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (blocked) {
|
|
442
|
+
lines.push(`Do not call run_command with commands matching any of these blocked command rules: ${blocked}.`);
|
|
443
|
+
}
|
|
444
|
+
if (options.approval === "always") {
|
|
445
|
+
lines.push("Commands require approval before execution; request only commands that are necessary for the task.");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return lines.length ? `Command policy:\n${lines.join("\n")}` : "";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function commandRulesGuidance(rules) {
|
|
452
|
+
if (!Array.isArray(rules) || rules.length === 0) return "";
|
|
453
|
+
return rules.map(commandRuleGuidance).join(", ");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function commandRuleGuidance(rule) {
|
|
457
|
+
if (rule instanceof RegExp) return rule.toString();
|
|
458
|
+
if (typeof rule === "function") return "[custom command rule]";
|
|
459
|
+
const value = String(rule);
|
|
460
|
+
return `${JSON.stringify(value)} (exact command or command prefix with arguments)`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function matchesCommandRule(command, rule) {
|
|
464
|
+
if (rule instanceof RegExp) return rule.test(command);
|
|
465
|
+
if (typeof rule === "function") return rule(command);
|
|
466
|
+
return command === String(rule) || command.startsWith(`${String(rule)} `);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function runShellCommand(command, options = {}) {
|
|
470
|
+
return new Promise((resolve) => {
|
|
471
|
+
let settled = false;
|
|
472
|
+
const child = spawn(command, {
|
|
473
|
+
cwd: options.cwd,
|
|
474
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
475
|
+
shell: options.shell ?? true,
|
|
476
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
477
|
+
});
|
|
478
|
+
let stdout = "";
|
|
479
|
+
let stderr = "";
|
|
480
|
+
let timedOut = false;
|
|
481
|
+
const settle = (result) => {
|
|
482
|
+
if (settled) return;
|
|
483
|
+
settled = true;
|
|
484
|
+
clearTimeout(timeout);
|
|
485
|
+
resolve(result);
|
|
486
|
+
};
|
|
487
|
+
const timeout = setTimeout(() => {
|
|
488
|
+
timedOut = true;
|
|
489
|
+
child.kill("SIGTERM");
|
|
490
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
491
|
+
|
|
492
|
+
child.stdout.on("data", (chunk) => {
|
|
493
|
+
stdout += chunk.toString();
|
|
494
|
+
});
|
|
495
|
+
child.stderr.on("data", (chunk) => {
|
|
496
|
+
stderr += chunk.toString();
|
|
497
|
+
});
|
|
498
|
+
child.on("error", (error) => {
|
|
499
|
+
settle({
|
|
500
|
+
command,
|
|
501
|
+
cwd: options.cwd,
|
|
502
|
+
exitCode: null,
|
|
503
|
+
stdout,
|
|
504
|
+
stderr: stderr || error.message,
|
|
505
|
+
timedOut
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
child.on("close", (exitCode, signal) => {
|
|
509
|
+
settle({
|
|
510
|
+
command,
|
|
511
|
+
cwd: options.cwd,
|
|
512
|
+
exitCode,
|
|
513
|
+
signal,
|
|
514
|
+
stdout,
|
|
515
|
+
stderr,
|
|
516
|
+
timedOut
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Automify, createAutomify } from "./automify.js";
|
|
2
|
+
import {
|
|
3
|
+
createDockerDesktopComputer,
|
|
4
|
+
DOCKER_DESKTOP_COMPUTER_OPTION_KEYS
|
|
5
|
+
} from "./docker-desktop-computer.js";
|
|
6
|
+
import {
|
|
7
|
+
createLocalDesktopComputer,
|
|
8
|
+
LOCAL_DESKTOP_COMPUTER_OPTION_KEYS
|
|
9
|
+
} from "./local-desktop-computer.js";
|
|
10
|
+
import {
|
|
11
|
+
AUTOMIFY_OPTION_KEYS,
|
|
12
|
+
assertKnownOptions,
|
|
13
|
+
mergeOptionKeys,
|
|
14
|
+
pickKnownOptions
|
|
15
|
+
} from "./runtime.js";
|
|
16
|
+
|
|
17
|
+
const DOCKER_COMPUTER_AUTOMIFY_OPTION_KEYS = mergeOptionKeys(
|
|
18
|
+
AUTOMIFY_OPTION_KEYS,
|
|
19
|
+
DOCKER_DESKTOP_COMPUTER_OPTION_KEYS
|
|
20
|
+
);
|
|
21
|
+
const LOCAL_COMPUTER_AUTOMIFY_OPTION_KEYS = mergeOptionKeys(
|
|
22
|
+
AUTOMIFY_OPTION_KEYS,
|
|
23
|
+
LOCAL_DESKTOP_COMPUTER_OPTION_KEYS
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export function createComputerAutomify(options = {}) {
|
|
27
|
+
return createAutomify({
|
|
28
|
+
environment: defaultComputerEnvironment(),
|
|
29
|
+
...options
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function createDockerComputerAutomify(options = {}) {
|
|
34
|
+
assertKnownOptions("Docker computer adapter", options, DOCKER_COMPUTER_AUTOMIFY_OPTION_KEYS);
|
|
35
|
+
const usesProvidedComputer = Boolean(options.computer);
|
|
36
|
+
const computer = options.computer ?? (await createDockerDesktopComputer(
|
|
37
|
+
pickKnownOptions(options, DOCKER_DESKTOP_COMPUTER_OPTION_KEYS)
|
|
38
|
+
));
|
|
39
|
+
const automifyOptions = pickKnownOptions(options, AUTOMIFY_OPTION_KEYS);
|
|
40
|
+
if (!usesProvidedComputer) {
|
|
41
|
+
delete automifyOptions.instructions;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new DockerComputerAutomify({
|
|
45
|
+
...automifyOptions,
|
|
46
|
+
computer
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function createLocalComputerAutomify(options = {}) {
|
|
51
|
+
assertKnownOptions("local computer adapter", options, LOCAL_COMPUTER_AUTOMIFY_OPTION_KEYS);
|
|
52
|
+
const usesProvidedComputer = Boolean(options.computer);
|
|
53
|
+
const computer = options.computer ?? (await createLocalDesktopComputer(
|
|
54
|
+
pickKnownOptions(options, LOCAL_DESKTOP_COMPUTER_OPTION_KEYS)
|
|
55
|
+
));
|
|
56
|
+
const automifyOptions = pickKnownOptions(options, AUTOMIFY_OPTION_KEYS);
|
|
57
|
+
if (!usesProvidedComputer) {
|
|
58
|
+
delete automifyOptions.instructions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new LocalComputerAutomify({
|
|
62
|
+
sendInitialScreenshot: true,
|
|
63
|
+
...automifyOptions,
|
|
64
|
+
computer
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class LocalComputerAutomify extends Automify {
|
|
69
|
+
constructor(options) {
|
|
70
|
+
super(pickKnownOptions(options, AUTOMIFY_OPTION_KEYS));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async close() {
|
|
74
|
+
if (typeof this.computer.close === "function") {
|
|
75
|
+
await this.computer.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class DockerComputerAutomify extends Automify {
|
|
81
|
+
constructor(options) {
|
|
82
|
+
super(pickKnownOptions(options, AUTOMIFY_OPTION_KEYS));
|
|
83
|
+
this.session = this.computer.session;
|
|
84
|
+
this.sharedFolder = this.computer.sharedFolder;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async close() {
|
|
88
|
+
if (typeof this.computer.close === "function") {
|
|
89
|
+
await this.computer.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function defaultComputerEnvironment() {
|
|
95
|
+
switch (process.platform) {
|
|
96
|
+
case "darwin":
|
|
97
|
+
return "mac";
|
|
98
|
+
case "win32":
|
|
99
|
+
return "windows";
|
|
100
|
+
default:
|
|
101
|
+
return "ubuntu";
|
|
102
|
+
}
|
|
103
|
+
}
|