agent-neckbeard 1.0.3 → 1.0.4
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.
Potentially problematic release.
This version of agent-neckbeard might be problematic. Click here for more details.
- package/dist/index.cjs +150 -30
- package/dist/index.d.cts +78 -2
- package/dist/index.d.ts +78 -2
- package/dist/index.js +144 -29
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -31,7 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
Agent: () => Agent,
|
|
34
|
-
|
|
34
|
+
ConfigurationError: () => ConfigurationError,
|
|
35
|
+
DEFAULT_DEPENDENCIES: () => DEFAULT_DEPENDENCIES,
|
|
36
|
+
DeploymentError: () => DeploymentError,
|
|
37
|
+
ExecutionError: () => ExecutionError,
|
|
38
|
+
NeckbeardError: () => NeckbeardError,
|
|
39
|
+
ValidationError: () => ValidationError
|
|
35
40
|
});
|
|
36
41
|
module.exports = __toCommonJS(index_exports);
|
|
37
42
|
var import_node_url = require("url");
|
|
@@ -39,7 +44,67 @@ var import_node_fs = require("fs");
|
|
|
39
44
|
var import_node_path = require("path");
|
|
40
45
|
var getEsbuild = () => import("esbuild");
|
|
41
46
|
var getE2b = () => import("e2b");
|
|
47
|
+
var DEFAULT_MAX_DURATION = 300;
|
|
48
|
+
var SANDBOX_COMMAND_TIMEOUT_MS = 3e5;
|
|
49
|
+
var QUICK_COMMAND_TIMEOUT_MS = 3e4;
|
|
50
|
+
var NODE_TARGET = "node20";
|
|
51
|
+
var SANDBOX_HOME = "/home/user";
|
|
52
|
+
var SANDBOX_PATHS = {
|
|
53
|
+
input: `${SANDBOX_HOME}/input`,
|
|
54
|
+
output: `${SANDBOX_HOME}/output`,
|
|
55
|
+
taskJson: `${SANDBOX_HOME}/input/task.json`,
|
|
56
|
+
resultJson: `${SANDBOX_HOME}/output/result.json`,
|
|
57
|
+
agentModule: `${SANDBOX_HOME}/agent.mjs`,
|
|
58
|
+
runnerModule: `${SANDBOX_HOME}/runner.mjs`,
|
|
59
|
+
claudeDir: `${SANDBOX_HOME}/.claude`,
|
|
60
|
+
packageJson: `${SANDBOX_HOME}/package.json`
|
|
61
|
+
};
|
|
62
|
+
var NeckbeardError = class extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "NeckbeardError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var DeploymentError = class extends NeckbeardError {
|
|
69
|
+
constructor(message, cause) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.cause = cause;
|
|
72
|
+
this.name = "DeploymentError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var ValidationError = class extends NeckbeardError {
|
|
76
|
+
constructor(message, cause) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.cause = cause;
|
|
79
|
+
this.name = "ValidationError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ExecutionError = class extends NeckbeardError {
|
|
83
|
+
constructor(message, cause) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.cause = cause;
|
|
86
|
+
this.name = "ExecutionError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var ConfigurationError = class extends NeckbeardError {
|
|
90
|
+
constructor(message) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "ConfigurationError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
42
95
|
var DEFAULT_DEPENDENCIES = {};
|
|
96
|
+
function requireEnv(name) {
|
|
97
|
+
const value = process.env[name];
|
|
98
|
+
if (!value) {
|
|
99
|
+
throw new ConfigurationError(
|
|
100
|
+
`${name} environment variable is required. Please set it before calling deploy() or run().`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function shellEscape(str) {
|
|
106
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
107
|
+
}
|
|
43
108
|
function getCallerFile() {
|
|
44
109
|
const stack = new Error().stack?.split("\n") ?? [];
|
|
45
110
|
for (const line of stack.slice(2)) {
|
|
@@ -93,7 +158,7 @@ var Agent = class {
|
|
|
93
158
|
this.id = config.id;
|
|
94
159
|
this.inputSchema = config.inputSchema;
|
|
95
160
|
this.outputSchema = config.outputSchema;
|
|
96
|
-
this.maxDuration = config.maxDuration ??
|
|
161
|
+
this.maxDuration = config.maxDuration ?? DEFAULT_MAX_DURATION;
|
|
97
162
|
this._run = config.run;
|
|
98
163
|
this._sourceFile = getCallerFile();
|
|
99
164
|
this._sandboxId = config.sandboxId;
|
|
@@ -104,8 +169,29 @@ var Agent = class {
|
|
|
104
169
|
get sandboxId() {
|
|
105
170
|
return this._sandboxId;
|
|
106
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Deploys the agent to an E2B sandbox.
|
|
174
|
+
*
|
|
175
|
+
* This method bundles the agent code using esbuild, creates a new E2B sandbox,
|
|
176
|
+
* installs any specified OS dependencies, downloads configured files, and
|
|
177
|
+
* uploads the agent code to the sandbox.
|
|
178
|
+
*
|
|
179
|
+
* The sandbox ID is stored and can be accessed via the `sandboxId` property
|
|
180
|
+
* after deployment completes.
|
|
181
|
+
*
|
|
182
|
+
* @throws {ConfigurationError} If E2B_API_KEY environment variable is not set
|
|
183
|
+
* @throws {DeploymentError} If sandbox creation, dependency installation, or file upload fails
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const agent = new Agent({ ... });
|
|
188
|
+
* await agent.deploy();
|
|
189
|
+
* console.log(`Deployed to sandbox: ${agent.sandboxId}`);
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
107
192
|
async deploy() {
|
|
108
193
|
if (this._sandboxId) return;
|
|
194
|
+
const e2bApiKey = requireEnv("E2B_API_KEY");
|
|
109
195
|
const esbuild = await getEsbuild();
|
|
110
196
|
const { Sandbox } = await getE2b();
|
|
111
197
|
const collectedExternals = /* @__PURE__ */ new Set();
|
|
@@ -117,13 +203,20 @@ var Agent = class {
|
|
|
117
203
|
entryPoints: [this._sourceFile],
|
|
118
204
|
bundle: true,
|
|
119
205
|
platform: "node",
|
|
120
|
-
target:
|
|
206
|
+
target: NODE_TARGET,
|
|
121
207
|
format: "esm",
|
|
122
208
|
write: false,
|
|
123
209
|
minify: true,
|
|
124
210
|
keepNames: true,
|
|
125
211
|
treeShaking: false,
|
|
126
212
|
// Preserve exports for the sandbox runner to import
|
|
213
|
+
banner: {
|
|
214
|
+
js: `import { fileURLToPath as __neckbeard_fileURLToPath } from 'node:url';
|
|
215
|
+
import { dirname as __neckbeard_dirname } from 'node:path';
|
|
216
|
+
const __filename = __neckbeard_fileURLToPath(import.meta.url);
|
|
217
|
+
const __dirname = __neckbeard_dirname(__filename);
|
|
218
|
+
`
|
|
219
|
+
},
|
|
127
220
|
plugins: [{
|
|
128
221
|
name: "agent-neckbeard-externals",
|
|
129
222
|
setup(build) {
|
|
@@ -196,52 +289,52 @@ try {
|
|
|
196
289
|
}
|
|
197
290
|
`;
|
|
198
291
|
const sandbox = await Sandbox.create("base", {
|
|
199
|
-
apiKey:
|
|
292
|
+
apiKey: e2bApiKey
|
|
200
293
|
});
|
|
201
294
|
const { apt, commands } = this.dependencies;
|
|
202
295
|
if (apt && apt.length > 0) {
|
|
203
296
|
const aptCmd = `sudo apt-get update && sudo apt-get install -y ${apt.join(" ")}`;
|
|
204
|
-
const aptResult = await sandbox.commands.run(aptCmd, { timeoutMs:
|
|
297
|
+
const aptResult = await sandbox.commands.run(aptCmd, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
205
298
|
if (aptResult.exitCode !== 0) {
|
|
206
|
-
throw new
|
|
299
|
+
throw new DeploymentError(`Failed to install apt packages: ${aptResult.stderr}`);
|
|
207
300
|
}
|
|
208
301
|
}
|
|
209
302
|
if (commands && commands.length > 0) {
|
|
210
303
|
for (const cmd of commands) {
|
|
211
|
-
const cmdResult = await sandbox.commands.run(cmd, { timeoutMs:
|
|
304
|
+
const cmdResult = await sandbox.commands.run(cmd, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
212
305
|
if (cmdResult.exitCode !== 0) {
|
|
213
|
-
throw new
|
|
306
|
+
throw new DeploymentError(`Failed to run command "${cmd}": ${cmdResult.stderr}`);
|
|
214
307
|
}
|
|
215
308
|
}
|
|
216
309
|
}
|
|
217
310
|
if (this.files.length > 0) {
|
|
218
311
|
for (const file of this.files) {
|
|
219
|
-
const destPath = file.path.startsWith("/") ? file.path :
|
|
312
|
+
const destPath = file.path.startsWith("/") ? file.path : `${SANDBOX_HOME}/${file.path}`;
|
|
220
313
|
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
221
314
|
if (parentDir) {
|
|
222
|
-
await sandbox.commands.run(`mkdir -p
|
|
315
|
+
await sandbox.commands.run(`mkdir -p ${shellEscape(parentDir)}`, { timeoutMs: QUICK_COMMAND_TIMEOUT_MS });
|
|
223
316
|
}
|
|
224
|
-
const curlCmd = `curl -fsSL -o
|
|
225
|
-
const downloadResult = await sandbox.commands.run(curlCmd, { timeoutMs:
|
|
317
|
+
const curlCmd = `curl -fsSL -o ${shellEscape(destPath)} ${shellEscape(file.url)}`;
|
|
318
|
+
const downloadResult = await sandbox.commands.run(curlCmd, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
226
319
|
if (downloadResult.exitCode !== 0) {
|
|
227
|
-
throw new
|
|
320
|
+
throw new DeploymentError(`Failed to download file from ${file.url} to ${destPath}: ${downloadResult.stderr}`);
|
|
228
321
|
}
|
|
229
322
|
}
|
|
230
323
|
}
|
|
231
324
|
if (this.claudeDir) {
|
|
232
325
|
const claudeFiles = readDirectoryRecursively(this.claudeDir);
|
|
233
|
-
await sandbox.commands.run(
|
|
326
|
+
await sandbox.commands.run(`mkdir -p ${SANDBOX_PATHS.claudeDir}`, { timeoutMs: QUICK_COMMAND_TIMEOUT_MS });
|
|
234
327
|
for (const file of claudeFiles) {
|
|
235
|
-
const destPath =
|
|
328
|
+
const destPath = `${SANDBOX_PATHS.claudeDir}/${file.relativePath}`;
|
|
236
329
|
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
237
|
-
if (parentDir && parentDir !==
|
|
238
|
-
await sandbox.commands.run(`mkdir -p
|
|
330
|
+
if (parentDir && parentDir !== SANDBOX_PATHS.claudeDir) {
|
|
331
|
+
await sandbox.commands.run(`mkdir -p ${shellEscape(parentDir)}`, { timeoutMs: QUICK_COMMAND_TIMEOUT_MS });
|
|
239
332
|
}
|
|
240
333
|
await sandbox.files.write(destPath, file.content);
|
|
241
334
|
}
|
|
242
335
|
}
|
|
243
|
-
await sandbox.files.write(
|
|
244
|
-
await sandbox.files.write(
|
|
336
|
+
await sandbox.files.write(SANDBOX_PATHS.agentModule, result.outputFiles[0].text);
|
|
337
|
+
await sandbox.files.write(SANDBOX_PATHS.runnerModule, runnerCode);
|
|
245
338
|
if (collectedExternals.size > 0) {
|
|
246
339
|
const dependencies = {};
|
|
247
340
|
for (const pkg of collectedExternals) {
|
|
@@ -252,42 +345,64 @@ try {
|
|
|
252
345
|
type: "module",
|
|
253
346
|
dependencies
|
|
254
347
|
});
|
|
255
|
-
await sandbox.files.write(
|
|
256
|
-
const installResult = await sandbox.commands.run(
|
|
348
|
+
await sandbox.files.write(SANDBOX_PATHS.packageJson, pkgJson);
|
|
349
|
+
const installResult = await sandbox.commands.run(`cd ${SANDBOX_HOME} && npm install`, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
257
350
|
if (installResult.exitCode !== 0) {
|
|
258
|
-
throw new
|
|
351
|
+
throw new DeploymentError(`Failed to install external packages: ${installResult.stderr}`);
|
|
259
352
|
}
|
|
260
353
|
}
|
|
261
354
|
this._sandboxId = sandbox.sandboxId;
|
|
262
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Executes the agent with the given input.
|
|
358
|
+
*
|
|
359
|
+
* The agent must be deployed before calling this method. Input is validated
|
|
360
|
+
* against the input schema, then the agent runs in the sandbox with the
|
|
361
|
+
* validated input. Output is validated against the output schema before
|
|
362
|
+
* being returned.
|
|
363
|
+
*
|
|
364
|
+
* @param input - The input data for the agent, must conform to inputSchema
|
|
365
|
+
* @returns A result object indicating success or failure with output or error
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* const result = await agent.run({ prompt: 'Hello, world!' });
|
|
370
|
+
* if (result.ok) {
|
|
371
|
+
* console.log('Output:', result.output);
|
|
372
|
+
* } else {
|
|
373
|
+
* console.error('Error:', result.error.message);
|
|
374
|
+
* }
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
263
377
|
async run(input) {
|
|
264
378
|
const executionId = `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
265
379
|
if (!this._sandboxId) {
|
|
266
380
|
return {
|
|
267
381
|
ok: false,
|
|
268
382
|
executionId,
|
|
269
|
-
error: new
|
|
383
|
+
error: new ExecutionError("Agent not deployed. Call agent.deploy() first or pass sandboxId to constructor.")
|
|
270
384
|
};
|
|
271
385
|
}
|
|
272
386
|
try {
|
|
387
|
+
const e2bApiKey = requireEnv("E2B_API_KEY");
|
|
273
388
|
const { Sandbox } = await getE2b();
|
|
274
389
|
const validatedInput = this.inputSchema.parse(input);
|
|
275
390
|
const sandbox = await Sandbox.connect(this._sandboxId, {
|
|
276
|
-
apiKey:
|
|
391
|
+
apiKey: e2bApiKey
|
|
277
392
|
});
|
|
278
|
-
await sandbox.files.write(
|
|
279
|
-
const result = await sandbox.commands.run(
|
|
393
|
+
await sandbox.files.write(SANDBOX_PATHS.taskJson, JSON.stringify({ input: validatedInput, executionId }));
|
|
394
|
+
const result = await sandbox.commands.run(`node ${SANDBOX_PATHS.runnerModule}`, {
|
|
280
395
|
timeoutMs: this.maxDuration * 1e3,
|
|
281
396
|
envs: {
|
|
282
397
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? ""
|
|
283
398
|
}
|
|
284
399
|
});
|
|
285
400
|
if (result.exitCode !== 0) {
|
|
286
|
-
return { ok: false, executionId, error: new
|
|
401
|
+
return { ok: false, executionId, error: new ExecutionError(`Agent failed: ${result.stderr}`) };
|
|
287
402
|
}
|
|
288
|
-
const output = JSON.parse(await sandbox.files.read(
|
|
403
|
+
const output = JSON.parse(await sandbox.files.read(SANDBOX_PATHS.resultJson));
|
|
289
404
|
if (!output.success) {
|
|
290
|
-
const err = new
|
|
405
|
+
const err = new ExecutionError(output.error.message);
|
|
291
406
|
err.stack = output.error.stack;
|
|
292
407
|
return { ok: false, executionId, error: err };
|
|
293
408
|
}
|
|
@@ -300,5 +415,10 @@ try {
|
|
|
300
415
|
// Annotate the CommonJS export names for ESM import in node:
|
|
301
416
|
0 && (module.exports = {
|
|
302
417
|
Agent,
|
|
303
|
-
|
|
418
|
+
ConfigurationError,
|
|
419
|
+
DEFAULT_DEPENDENCIES,
|
|
420
|
+
DeploymentError,
|
|
421
|
+
ExecutionError,
|
|
422
|
+
NeckbeardError,
|
|
423
|
+
ValidationError
|
|
304
424
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-neckbeard - Deploy AI agents to E2B sandboxes
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Base error class for agent-neckbeard errors.
|
|
6
|
+
* All custom errors extend this class.
|
|
7
|
+
*/
|
|
8
|
+
declare class NeckbeardError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Error thrown when sandbox deployment fails.
|
|
13
|
+
* This includes sandbox creation, dependency installation, and file upload failures.
|
|
14
|
+
*/
|
|
15
|
+
declare class DeploymentError extends NeckbeardError {
|
|
16
|
+
readonly cause?: Error | undefined;
|
|
17
|
+
constructor(message: string, cause?: Error | undefined);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Error thrown when input or output schema validation fails.
|
|
21
|
+
*/
|
|
22
|
+
declare class ValidationError extends NeckbeardError {
|
|
23
|
+
readonly cause?: Error | undefined;
|
|
24
|
+
constructor(message: string, cause?: Error | undefined);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Error thrown when agent execution fails in the sandbox.
|
|
28
|
+
*/
|
|
29
|
+
declare class ExecutionError extends NeckbeardError {
|
|
30
|
+
readonly cause?: Error | undefined;
|
|
31
|
+
constructor(message: string, cause?: Error | undefined);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when required environment variables are missing.
|
|
35
|
+
*/
|
|
36
|
+
declare class ConfigurationError extends NeckbeardError {
|
|
37
|
+
constructor(message: string);
|
|
38
|
+
}
|
|
4
39
|
interface AgentRunContext {
|
|
5
40
|
executionId: string;
|
|
6
41
|
signal: AbortSignal;
|
|
@@ -19,7 +54,7 @@ type AgentRunResult<TOutput> = {
|
|
|
19
54
|
} | {
|
|
20
55
|
ok: false;
|
|
21
56
|
executionId: string;
|
|
22
|
-
error: Error;
|
|
57
|
+
error: Error | NeckbeardError;
|
|
23
58
|
};
|
|
24
59
|
interface SchemaLike<T> {
|
|
25
60
|
parse: (data: unknown) => T;
|
|
@@ -91,8 +126,49 @@ declare class Agent<TInput, TOutput> {
|
|
|
91
126
|
private _sandboxId?;
|
|
92
127
|
constructor(config: AgentConfig<TInput, TOutput>);
|
|
93
128
|
get sandboxId(): string | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Deploys the agent to an E2B sandbox.
|
|
131
|
+
*
|
|
132
|
+
* This method bundles the agent code using esbuild, creates a new E2B sandbox,
|
|
133
|
+
* installs any specified OS dependencies, downloads configured files, and
|
|
134
|
+
* uploads the agent code to the sandbox.
|
|
135
|
+
*
|
|
136
|
+
* The sandbox ID is stored and can be accessed via the `sandboxId` property
|
|
137
|
+
* after deployment completes.
|
|
138
|
+
*
|
|
139
|
+
* @throws {ConfigurationError} If E2B_API_KEY environment variable is not set
|
|
140
|
+
* @throws {DeploymentError} If sandbox creation, dependency installation, or file upload fails
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const agent = new Agent({ ... });
|
|
145
|
+
* await agent.deploy();
|
|
146
|
+
* console.log(`Deployed to sandbox: ${agent.sandboxId}`);
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
94
149
|
deploy(): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Executes the agent with the given input.
|
|
152
|
+
*
|
|
153
|
+
* The agent must be deployed before calling this method. Input is validated
|
|
154
|
+
* against the input schema, then the agent runs in the sandbox with the
|
|
155
|
+
* validated input. Output is validated against the output schema before
|
|
156
|
+
* being returned.
|
|
157
|
+
*
|
|
158
|
+
* @param input - The input data for the agent, must conform to inputSchema
|
|
159
|
+
* @returns A result object indicating success or failure with output or error
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const result = await agent.run({ prompt: 'Hello, world!' });
|
|
164
|
+
* if (result.ok) {
|
|
165
|
+
* console.log('Output:', result.output);
|
|
166
|
+
* } else {
|
|
167
|
+
* console.error('Error:', result.error.message);
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
95
171
|
run(input: TInput): Promise<AgentRunResult<TOutput>>;
|
|
96
172
|
}
|
|
97
173
|
|
|
98
|
-
export { Agent, type AgentConfig, type AgentRunContext, type AgentRunResult, DEFAULT_DEPENDENCIES, type FileDownload, type OsDependencies };
|
|
174
|
+
export { Agent, type AgentConfig, type AgentRunContext, type AgentRunResult, ConfigurationError, DEFAULT_DEPENDENCIES, DeploymentError, ExecutionError, type FileDownload, NeckbeardError, type OsDependencies, ValidationError };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-neckbeard - Deploy AI agents to E2B sandboxes
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Base error class for agent-neckbeard errors.
|
|
6
|
+
* All custom errors extend this class.
|
|
7
|
+
*/
|
|
8
|
+
declare class NeckbeardError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Error thrown when sandbox deployment fails.
|
|
13
|
+
* This includes sandbox creation, dependency installation, and file upload failures.
|
|
14
|
+
*/
|
|
15
|
+
declare class DeploymentError extends NeckbeardError {
|
|
16
|
+
readonly cause?: Error | undefined;
|
|
17
|
+
constructor(message: string, cause?: Error | undefined);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Error thrown when input or output schema validation fails.
|
|
21
|
+
*/
|
|
22
|
+
declare class ValidationError extends NeckbeardError {
|
|
23
|
+
readonly cause?: Error | undefined;
|
|
24
|
+
constructor(message: string, cause?: Error | undefined);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Error thrown when agent execution fails in the sandbox.
|
|
28
|
+
*/
|
|
29
|
+
declare class ExecutionError extends NeckbeardError {
|
|
30
|
+
readonly cause?: Error | undefined;
|
|
31
|
+
constructor(message: string, cause?: Error | undefined);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when required environment variables are missing.
|
|
35
|
+
*/
|
|
36
|
+
declare class ConfigurationError extends NeckbeardError {
|
|
37
|
+
constructor(message: string);
|
|
38
|
+
}
|
|
4
39
|
interface AgentRunContext {
|
|
5
40
|
executionId: string;
|
|
6
41
|
signal: AbortSignal;
|
|
@@ -19,7 +54,7 @@ type AgentRunResult<TOutput> = {
|
|
|
19
54
|
} | {
|
|
20
55
|
ok: false;
|
|
21
56
|
executionId: string;
|
|
22
|
-
error: Error;
|
|
57
|
+
error: Error | NeckbeardError;
|
|
23
58
|
};
|
|
24
59
|
interface SchemaLike<T> {
|
|
25
60
|
parse: (data: unknown) => T;
|
|
@@ -91,8 +126,49 @@ declare class Agent<TInput, TOutput> {
|
|
|
91
126
|
private _sandboxId?;
|
|
92
127
|
constructor(config: AgentConfig<TInput, TOutput>);
|
|
93
128
|
get sandboxId(): string | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Deploys the agent to an E2B sandbox.
|
|
131
|
+
*
|
|
132
|
+
* This method bundles the agent code using esbuild, creates a new E2B sandbox,
|
|
133
|
+
* installs any specified OS dependencies, downloads configured files, and
|
|
134
|
+
* uploads the agent code to the sandbox.
|
|
135
|
+
*
|
|
136
|
+
* The sandbox ID is stored and can be accessed via the `sandboxId` property
|
|
137
|
+
* after deployment completes.
|
|
138
|
+
*
|
|
139
|
+
* @throws {ConfigurationError} If E2B_API_KEY environment variable is not set
|
|
140
|
+
* @throws {DeploymentError} If sandbox creation, dependency installation, or file upload fails
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const agent = new Agent({ ... });
|
|
145
|
+
* await agent.deploy();
|
|
146
|
+
* console.log(`Deployed to sandbox: ${agent.sandboxId}`);
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
94
149
|
deploy(): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Executes the agent with the given input.
|
|
152
|
+
*
|
|
153
|
+
* The agent must be deployed before calling this method. Input is validated
|
|
154
|
+
* against the input schema, then the agent runs in the sandbox with the
|
|
155
|
+
* validated input. Output is validated against the output schema before
|
|
156
|
+
* being returned.
|
|
157
|
+
*
|
|
158
|
+
* @param input - The input data for the agent, must conform to inputSchema
|
|
159
|
+
* @returns A result object indicating success or failure with output or error
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const result = await agent.run({ prompt: 'Hello, world!' });
|
|
164
|
+
* if (result.ok) {
|
|
165
|
+
* console.log('Output:', result.output);
|
|
166
|
+
* } else {
|
|
167
|
+
* console.error('Error:', result.error.message);
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
95
171
|
run(input: TInput): Promise<AgentRunResult<TOutput>>;
|
|
96
172
|
}
|
|
97
173
|
|
|
98
|
-
export { Agent, type AgentConfig, type AgentRunContext, type AgentRunResult, DEFAULT_DEPENDENCIES, type FileDownload, type OsDependencies };
|
|
174
|
+
export { Agent, type AgentConfig, type AgentRunContext, type AgentRunResult, ConfigurationError, DEFAULT_DEPENDENCIES, DeploymentError, ExecutionError, type FileDownload, NeckbeardError, type OsDependencies, ValidationError };
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,67 @@ import { readdirSync, readFileSync, statSync } from "fs";
|
|
|
4
4
|
import { join, relative } from "path";
|
|
5
5
|
var getEsbuild = () => import("esbuild");
|
|
6
6
|
var getE2b = () => import("e2b");
|
|
7
|
+
var DEFAULT_MAX_DURATION = 300;
|
|
8
|
+
var SANDBOX_COMMAND_TIMEOUT_MS = 3e5;
|
|
9
|
+
var QUICK_COMMAND_TIMEOUT_MS = 3e4;
|
|
10
|
+
var NODE_TARGET = "node20";
|
|
11
|
+
var SANDBOX_HOME = "/home/user";
|
|
12
|
+
var SANDBOX_PATHS = {
|
|
13
|
+
input: `${SANDBOX_HOME}/input`,
|
|
14
|
+
output: `${SANDBOX_HOME}/output`,
|
|
15
|
+
taskJson: `${SANDBOX_HOME}/input/task.json`,
|
|
16
|
+
resultJson: `${SANDBOX_HOME}/output/result.json`,
|
|
17
|
+
agentModule: `${SANDBOX_HOME}/agent.mjs`,
|
|
18
|
+
runnerModule: `${SANDBOX_HOME}/runner.mjs`,
|
|
19
|
+
claudeDir: `${SANDBOX_HOME}/.claude`,
|
|
20
|
+
packageJson: `${SANDBOX_HOME}/package.json`
|
|
21
|
+
};
|
|
22
|
+
var NeckbeardError = class extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "NeckbeardError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var DeploymentError = class extends NeckbeardError {
|
|
29
|
+
constructor(message, cause) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.cause = cause;
|
|
32
|
+
this.name = "DeploymentError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var ValidationError = class extends NeckbeardError {
|
|
36
|
+
constructor(message, cause) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.cause = cause;
|
|
39
|
+
this.name = "ValidationError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var ExecutionError = class extends NeckbeardError {
|
|
43
|
+
constructor(message, cause) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.cause = cause;
|
|
46
|
+
this.name = "ExecutionError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var ConfigurationError = class extends NeckbeardError {
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "ConfigurationError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
7
55
|
var DEFAULT_DEPENDENCIES = {};
|
|
56
|
+
function requireEnv(name) {
|
|
57
|
+
const value = process.env[name];
|
|
58
|
+
if (!value) {
|
|
59
|
+
throw new ConfigurationError(
|
|
60
|
+
`${name} environment variable is required. Please set it before calling deploy() or run().`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function shellEscape(str) {
|
|
66
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
67
|
+
}
|
|
8
68
|
function getCallerFile() {
|
|
9
69
|
const stack = new Error().stack?.split("\n") ?? [];
|
|
10
70
|
for (const line of stack.slice(2)) {
|
|
@@ -58,7 +118,7 @@ var Agent = class {
|
|
|
58
118
|
this.id = config.id;
|
|
59
119
|
this.inputSchema = config.inputSchema;
|
|
60
120
|
this.outputSchema = config.outputSchema;
|
|
61
|
-
this.maxDuration = config.maxDuration ??
|
|
121
|
+
this.maxDuration = config.maxDuration ?? DEFAULT_MAX_DURATION;
|
|
62
122
|
this._run = config.run;
|
|
63
123
|
this._sourceFile = getCallerFile();
|
|
64
124
|
this._sandboxId = config.sandboxId;
|
|
@@ -69,8 +129,29 @@ var Agent = class {
|
|
|
69
129
|
get sandboxId() {
|
|
70
130
|
return this._sandboxId;
|
|
71
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Deploys the agent to an E2B sandbox.
|
|
134
|
+
*
|
|
135
|
+
* This method bundles the agent code using esbuild, creates a new E2B sandbox,
|
|
136
|
+
* installs any specified OS dependencies, downloads configured files, and
|
|
137
|
+
* uploads the agent code to the sandbox.
|
|
138
|
+
*
|
|
139
|
+
* The sandbox ID is stored and can be accessed via the `sandboxId` property
|
|
140
|
+
* after deployment completes.
|
|
141
|
+
*
|
|
142
|
+
* @throws {ConfigurationError} If E2B_API_KEY environment variable is not set
|
|
143
|
+
* @throws {DeploymentError} If sandbox creation, dependency installation, or file upload fails
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const agent = new Agent({ ... });
|
|
148
|
+
* await agent.deploy();
|
|
149
|
+
* console.log(`Deployed to sandbox: ${agent.sandboxId}`);
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
72
152
|
async deploy() {
|
|
73
153
|
if (this._sandboxId) return;
|
|
154
|
+
const e2bApiKey = requireEnv("E2B_API_KEY");
|
|
74
155
|
const esbuild = await getEsbuild();
|
|
75
156
|
const { Sandbox } = await getE2b();
|
|
76
157
|
const collectedExternals = /* @__PURE__ */ new Set();
|
|
@@ -82,13 +163,20 @@ var Agent = class {
|
|
|
82
163
|
entryPoints: [this._sourceFile],
|
|
83
164
|
bundle: true,
|
|
84
165
|
platform: "node",
|
|
85
|
-
target:
|
|
166
|
+
target: NODE_TARGET,
|
|
86
167
|
format: "esm",
|
|
87
168
|
write: false,
|
|
88
169
|
minify: true,
|
|
89
170
|
keepNames: true,
|
|
90
171
|
treeShaking: false,
|
|
91
172
|
// Preserve exports for the sandbox runner to import
|
|
173
|
+
banner: {
|
|
174
|
+
js: `import { fileURLToPath as __neckbeard_fileURLToPath } from 'node:url';
|
|
175
|
+
import { dirname as __neckbeard_dirname } from 'node:path';
|
|
176
|
+
const __filename = __neckbeard_fileURLToPath(import.meta.url);
|
|
177
|
+
const __dirname = __neckbeard_dirname(__filename);
|
|
178
|
+
`
|
|
179
|
+
},
|
|
92
180
|
plugins: [{
|
|
93
181
|
name: "agent-neckbeard-externals",
|
|
94
182
|
setup(build) {
|
|
@@ -161,52 +249,52 @@ try {
|
|
|
161
249
|
}
|
|
162
250
|
`;
|
|
163
251
|
const sandbox = await Sandbox.create("base", {
|
|
164
|
-
apiKey:
|
|
252
|
+
apiKey: e2bApiKey
|
|
165
253
|
});
|
|
166
254
|
const { apt, commands } = this.dependencies;
|
|
167
255
|
if (apt && apt.length > 0) {
|
|
168
256
|
const aptCmd = `sudo apt-get update && sudo apt-get install -y ${apt.join(" ")}`;
|
|
169
|
-
const aptResult = await sandbox.commands.run(aptCmd, { timeoutMs:
|
|
257
|
+
const aptResult = await sandbox.commands.run(aptCmd, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
170
258
|
if (aptResult.exitCode !== 0) {
|
|
171
|
-
throw new
|
|
259
|
+
throw new DeploymentError(`Failed to install apt packages: ${aptResult.stderr}`);
|
|
172
260
|
}
|
|
173
261
|
}
|
|
174
262
|
if (commands && commands.length > 0) {
|
|
175
263
|
for (const cmd of commands) {
|
|
176
|
-
const cmdResult = await sandbox.commands.run(cmd, { timeoutMs:
|
|
264
|
+
const cmdResult = await sandbox.commands.run(cmd, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
177
265
|
if (cmdResult.exitCode !== 0) {
|
|
178
|
-
throw new
|
|
266
|
+
throw new DeploymentError(`Failed to run command "${cmd}": ${cmdResult.stderr}`);
|
|
179
267
|
}
|
|
180
268
|
}
|
|
181
269
|
}
|
|
182
270
|
if (this.files.length > 0) {
|
|
183
271
|
for (const file of this.files) {
|
|
184
|
-
const destPath = file.path.startsWith("/") ? file.path :
|
|
272
|
+
const destPath = file.path.startsWith("/") ? file.path : `${SANDBOX_HOME}/${file.path}`;
|
|
185
273
|
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
186
274
|
if (parentDir) {
|
|
187
|
-
await sandbox.commands.run(`mkdir -p
|
|
275
|
+
await sandbox.commands.run(`mkdir -p ${shellEscape(parentDir)}`, { timeoutMs: QUICK_COMMAND_TIMEOUT_MS });
|
|
188
276
|
}
|
|
189
|
-
const curlCmd = `curl -fsSL -o
|
|
190
|
-
const downloadResult = await sandbox.commands.run(curlCmd, { timeoutMs:
|
|
277
|
+
const curlCmd = `curl -fsSL -o ${shellEscape(destPath)} ${shellEscape(file.url)}`;
|
|
278
|
+
const downloadResult = await sandbox.commands.run(curlCmd, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
191
279
|
if (downloadResult.exitCode !== 0) {
|
|
192
|
-
throw new
|
|
280
|
+
throw new DeploymentError(`Failed to download file from ${file.url} to ${destPath}: ${downloadResult.stderr}`);
|
|
193
281
|
}
|
|
194
282
|
}
|
|
195
283
|
}
|
|
196
284
|
if (this.claudeDir) {
|
|
197
285
|
const claudeFiles = readDirectoryRecursively(this.claudeDir);
|
|
198
|
-
await sandbox.commands.run(
|
|
286
|
+
await sandbox.commands.run(`mkdir -p ${SANDBOX_PATHS.claudeDir}`, { timeoutMs: QUICK_COMMAND_TIMEOUT_MS });
|
|
199
287
|
for (const file of claudeFiles) {
|
|
200
|
-
const destPath =
|
|
288
|
+
const destPath = `${SANDBOX_PATHS.claudeDir}/${file.relativePath}`;
|
|
201
289
|
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
202
|
-
if (parentDir && parentDir !==
|
|
203
|
-
await sandbox.commands.run(`mkdir -p
|
|
290
|
+
if (parentDir && parentDir !== SANDBOX_PATHS.claudeDir) {
|
|
291
|
+
await sandbox.commands.run(`mkdir -p ${shellEscape(parentDir)}`, { timeoutMs: QUICK_COMMAND_TIMEOUT_MS });
|
|
204
292
|
}
|
|
205
293
|
await sandbox.files.write(destPath, file.content);
|
|
206
294
|
}
|
|
207
295
|
}
|
|
208
|
-
await sandbox.files.write(
|
|
209
|
-
await sandbox.files.write(
|
|
296
|
+
await sandbox.files.write(SANDBOX_PATHS.agentModule, result.outputFiles[0].text);
|
|
297
|
+
await sandbox.files.write(SANDBOX_PATHS.runnerModule, runnerCode);
|
|
210
298
|
if (collectedExternals.size > 0) {
|
|
211
299
|
const dependencies = {};
|
|
212
300
|
for (const pkg of collectedExternals) {
|
|
@@ -217,42 +305,64 @@ try {
|
|
|
217
305
|
type: "module",
|
|
218
306
|
dependencies
|
|
219
307
|
});
|
|
220
|
-
await sandbox.files.write(
|
|
221
|
-
const installResult = await sandbox.commands.run(
|
|
308
|
+
await sandbox.files.write(SANDBOX_PATHS.packageJson, pkgJson);
|
|
309
|
+
const installResult = await sandbox.commands.run(`cd ${SANDBOX_HOME} && npm install`, { timeoutMs: SANDBOX_COMMAND_TIMEOUT_MS });
|
|
222
310
|
if (installResult.exitCode !== 0) {
|
|
223
|
-
throw new
|
|
311
|
+
throw new DeploymentError(`Failed to install external packages: ${installResult.stderr}`);
|
|
224
312
|
}
|
|
225
313
|
}
|
|
226
314
|
this._sandboxId = sandbox.sandboxId;
|
|
227
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Executes the agent with the given input.
|
|
318
|
+
*
|
|
319
|
+
* The agent must be deployed before calling this method. Input is validated
|
|
320
|
+
* against the input schema, then the agent runs in the sandbox with the
|
|
321
|
+
* validated input. Output is validated against the output schema before
|
|
322
|
+
* being returned.
|
|
323
|
+
*
|
|
324
|
+
* @param input - The input data for the agent, must conform to inputSchema
|
|
325
|
+
* @returns A result object indicating success or failure with output or error
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```typescript
|
|
329
|
+
* const result = await agent.run({ prompt: 'Hello, world!' });
|
|
330
|
+
* if (result.ok) {
|
|
331
|
+
* console.log('Output:', result.output);
|
|
332
|
+
* } else {
|
|
333
|
+
* console.error('Error:', result.error.message);
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
228
337
|
async run(input) {
|
|
229
338
|
const executionId = `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
230
339
|
if (!this._sandboxId) {
|
|
231
340
|
return {
|
|
232
341
|
ok: false,
|
|
233
342
|
executionId,
|
|
234
|
-
error: new
|
|
343
|
+
error: new ExecutionError("Agent not deployed. Call agent.deploy() first or pass sandboxId to constructor.")
|
|
235
344
|
};
|
|
236
345
|
}
|
|
237
346
|
try {
|
|
347
|
+
const e2bApiKey = requireEnv("E2B_API_KEY");
|
|
238
348
|
const { Sandbox } = await getE2b();
|
|
239
349
|
const validatedInput = this.inputSchema.parse(input);
|
|
240
350
|
const sandbox = await Sandbox.connect(this._sandboxId, {
|
|
241
|
-
apiKey:
|
|
351
|
+
apiKey: e2bApiKey
|
|
242
352
|
});
|
|
243
|
-
await sandbox.files.write(
|
|
244
|
-
const result = await sandbox.commands.run(
|
|
353
|
+
await sandbox.files.write(SANDBOX_PATHS.taskJson, JSON.stringify({ input: validatedInput, executionId }));
|
|
354
|
+
const result = await sandbox.commands.run(`node ${SANDBOX_PATHS.runnerModule}`, {
|
|
245
355
|
timeoutMs: this.maxDuration * 1e3,
|
|
246
356
|
envs: {
|
|
247
357
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? ""
|
|
248
358
|
}
|
|
249
359
|
});
|
|
250
360
|
if (result.exitCode !== 0) {
|
|
251
|
-
return { ok: false, executionId, error: new
|
|
361
|
+
return { ok: false, executionId, error: new ExecutionError(`Agent failed: ${result.stderr}`) };
|
|
252
362
|
}
|
|
253
|
-
const output = JSON.parse(await sandbox.files.read(
|
|
363
|
+
const output = JSON.parse(await sandbox.files.read(SANDBOX_PATHS.resultJson));
|
|
254
364
|
if (!output.success) {
|
|
255
|
-
const err = new
|
|
365
|
+
const err = new ExecutionError(output.error.message);
|
|
256
366
|
err.stack = output.error.stack;
|
|
257
367
|
return { ok: false, executionId, error: err };
|
|
258
368
|
}
|
|
@@ -264,5 +374,10 @@ try {
|
|
|
264
374
|
};
|
|
265
375
|
export {
|
|
266
376
|
Agent,
|
|
267
|
-
|
|
377
|
+
ConfigurationError,
|
|
378
|
+
DEFAULT_DEPENDENCIES,
|
|
379
|
+
DeploymentError,
|
|
380
|
+
ExecutionError,
|
|
381
|
+
NeckbeardError,
|
|
382
|
+
ValidationError
|
|
268
383
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-neckbeard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Deploy AI agents to E2B sandboxes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
19
20
|
"types": "./dist/index.d.cts",
|
|
21
|
+
"sideEffects": false,
|
|
20
22
|
"files": [
|
|
21
23
|
"dist"
|
|
22
24
|
],
|
|
@@ -26,7 +28,8 @@
|
|
|
26
28
|
"scripts": {
|
|
27
29
|
"build": "tsup",
|
|
28
30
|
"dev": "tsup --watch",
|
|
29
|
-
"typecheck": "tsc --noEmit"
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"prepare": "npm run build"
|
|
30
33
|
},
|
|
31
34
|
"dependencies": {
|
|
32
35
|
"e2b": "^1.0.0",
|