agent-neckbeard 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/dist/index.cjs +670 -0
- package/dist/index.d.cts +215 -0
- package/dist/index.d.ts +215 -0
- package/dist/index.js +628 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
4
|
+
import { dirname, join, relative } from "path";
|
|
5
|
+
import { builtinModules, createRequire } from "module";
|
|
6
|
+
import { readPackageJSON, resolvePackageJSON } from "pkg-types";
|
|
7
|
+
var getEsbuild = () => import("esbuild");
|
|
8
|
+
var getE2b = () => import("e2b");
|
|
9
|
+
var DEFAULT_MAX_DURATION = 300;
|
|
10
|
+
var SANDBOX_COMMAND_TIMEOUT_MS = 3e5;
|
|
11
|
+
var QUICK_COMMAND_TIMEOUT_MS = 3e4;
|
|
12
|
+
var NODE_TARGET = "node20";
|
|
13
|
+
var DEFAULT_EXTERNALS = ["@anthropic-ai/claude-agent-sdk"];
|
|
14
|
+
var SANDBOX_PLATFORM = process.env.NECKBEARD_SANDBOX_PLATFORM ?? "linux";
|
|
15
|
+
var SANDBOX_ARCH = process.env.NECKBEARD_SANDBOX_ARCH ?? "x64";
|
|
16
|
+
var NATIVE_EXT_RE = /\.(node|wasm|gyp|c|cc|cpp|cxx|h|hpp|hxx)$/i;
|
|
17
|
+
var SANDBOX_HOME = "/home/user";
|
|
18
|
+
var SANDBOX_AGENT_DIR = `${SANDBOX_HOME}/agent`;
|
|
19
|
+
var SANDBOX_PATHS = {
|
|
20
|
+
/** Agent code directory */
|
|
21
|
+
agentDir: SANDBOX_AGENT_DIR,
|
|
22
|
+
/** I/O directory for input/output files */
|
|
23
|
+
ioDir: `${SANDBOX_HOME}/io`,
|
|
24
|
+
/** Task input file */
|
|
25
|
+
inputJson: `${SANDBOX_HOME}/io/input.json`,
|
|
26
|
+
/** Result output file (contains success/error) */
|
|
27
|
+
outputJson: `${SANDBOX_HOME}/io/output.json`,
|
|
28
|
+
/** Bundled agent module */
|
|
29
|
+
agentModule: `${SANDBOX_AGENT_DIR}/agent.mjs`,
|
|
30
|
+
/** Runner module that executes the agent */
|
|
31
|
+
runnerModule: `${SANDBOX_AGENT_DIR}/runner.mjs`,
|
|
32
|
+
/** Claude skills and settings directory */
|
|
33
|
+
claudeDir: `${SANDBOX_AGENT_DIR}/.claude`,
|
|
34
|
+
/** Package.json for npm dependencies */
|
|
35
|
+
packageJson: `${SANDBOX_AGENT_DIR}/package.json`
|
|
36
|
+
};
|
|
37
|
+
var NeckbeardError = class extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "NeckbeardError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var DeploymentError = class extends NeckbeardError {
|
|
44
|
+
constructor(message, cause) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.cause = cause;
|
|
47
|
+
this.name = "DeploymentError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var ValidationError = class extends NeckbeardError {
|
|
51
|
+
constructor(message, cause) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.cause = cause;
|
|
54
|
+
this.name = "ValidationError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var ExecutionError = class extends NeckbeardError {
|
|
58
|
+
constructor(message, cause) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.cause = cause;
|
|
61
|
+
this.name = "ExecutionError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var ConfigurationError = class extends NeckbeardError {
|
|
65
|
+
constructor(message) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = "ConfigurationError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var DEFAULT_DEPENDENCIES = {};
|
|
71
|
+
function requireEnv(name) {
|
|
72
|
+
const value = process.env[name];
|
|
73
|
+
if (!value) {
|
|
74
|
+
throw new ConfigurationError(
|
|
75
|
+
`${name} environment variable is required. Please set it before calling deploy() or run().`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function shellEscape(str) {
|
|
81
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
82
|
+
}
|
|
83
|
+
var require2 = createRequire(
|
|
84
|
+
typeof __filename !== "undefined" ? __filename : fileURLToPath(import.meta.url)
|
|
85
|
+
);
|
|
86
|
+
function isBareModuleImport(path) {
|
|
87
|
+
const excludes = [".", "/", "~", "file:", "data:"];
|
|
88
|
+
return !excludes.some((exclude) => path.startsWith(exclude));
|
|
89
|
+
}
|
|
90
|
+
function packageNameForImportPath(importPath) {
|
|
91
|
+
if (importPath.startsWith("@")) {
|
|
92
|
+
return importPath.split("/").slice(0, 2).join("/");
|
|
93
|
+
}
|
|
94
|
+
return importPath.split("/")[0];
|
|
95
|
+
}
|
|
96
|
+
function resolveModulePath(importPath, resolveDir) {
|
|
97
|
+
try {
|
|
98
|
+
return require2.resolve(importPath, { paths: [resolveDir] });
|
|
99
|
+
} catch {
|
|
100
|
+
const importMeta = import.meta ?? void 0;
|
|
101
|
+
const resolver = importMeta && typeof importMeta.resolve === "function" ? importMeta.resolve.bind(importMeta) : void 0;
|
|
102
|
+
if (!resolver) return null;
|
|
103
|
+
try {
|
|
104
|
+
const parentUrl = pathToFileURL(join(resolveDir, "__neckbeard__.js"));
|
|
105
|
+
const resolved = resolver(importPath, parentUrl);
|
|
106
|
+
if (resolved.startsWith("file://")) {
|
|
107
|
+
return fileURLToPath(resolved);
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function globToRegExp(pattern) {
|
|
116
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
117
|
+
return new RegExp(`^${escaped}$`);
|
|
118
|
+
}
|
|
119
|
+
function matchesExternal(matchers, pkgName) {
|
|
120
|
+
return matchers.some((re) => re.test(pkgName));
|
|
121
|
+
}
|
|
122
|
+
async function isMainPackageJson(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
const packageJson = await readPackageJSON(filePath);
|
|
125
|
+
const markerFields = /* @__PURE__ */ new Set([
|
|
126
|
+
"type",
|
|
127
|
+
"sideEffects",
|
|
128
|
+
"browser",
|
|
129
|
+
"main",
|
|
130
|
+
"module",
|
|
131
|
+
"react-native",
|
|
132
|
+
"name"
|
|
133
|
+
]);
|
|
134
|
+
if (!packageJson.type) return true;
|
|
135
|
+
const keys = Object.keys(packageJson);
|
|
136
|
+
return !keys.every((k) => markerFields.has(k));
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function resolvePackageInfo(importPath, resolveDir) {
|
|
142
|
+
try {
|
|
143
|
+
const resolvedPath = resolveModulePath(importPath, resolveDir);
|
|
144
|
+
if (!resolvedPath) return null;
|
|
145
|
+
const packageJsonPath = await resolvePackageJSON(dirname(resolvedPath), {
|
|
146
|
+
test: isMainPackageJson
|
|
147
|
+
});
|
|
148
|
+
if (!packageJsonPath) return null;
|
|
149
|
+
const packageJson = await readPackageJSON(packageJsonPath);
|
|
150
|
+
if (!packageJson.name || !packageJson.version) return null;
|
|
151
|
+
return {
|
|
152
|
+
name: packageJson.name,
|
|
153
|
+
version: packageJson.version,
|
|
154
|
+
root: dirname(packageJsonPath),
|
|
155
|
+
resolvedPath,
|
|
156
|
+
packageJson
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function hasNativeHints(info) {
|
|
163
|
+
const files = Array.isArray(info.packageJson.files) ? info.packageJson.files : [];
|
|
164
|
+
const fields = [info.packageJson.main, info.packageJson.module, info.packageJson.browser].filter(
|
|
165
|
+
(f) => typeof f === "string"
|
|
166
|
+
);
|
|
167
|
+
return files.concat(fields).some((file) => NATIVE_EXT_RE.test(file));
|
|
168
|
+
}
|
|
169
|
+
function shouldAutoExternalize(info) {
|
|
170
|
+
if (info.resolvedPath.endsWith(".node") || info.resolvedPath.endsWith(".wasm")) return true;
|
|
171
|
+
if (hasNativeHints(info)) return true;
|
|
172
|
+
if (existsSync(join(info.root, "binding.gyp"))) return true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
function normalizeList(value) {
|
|
176
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
177
|
+
if (typeof value === "string") return [value];
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function listAllowsTarget(list, target) {
|
|
181
|
+
if (!list || list.length === 0) return true;
|
|
182
|
+
const negated = list.filter((v) => v.startsWith("!")).map((v) => v.slice(1));
|
|
183
|
+
if (negated.includes(target)) return false;
|
|
184
|
+
const positives = list.filter((v) => !v.startsWith("!"));
|
|
185
|
+
if (positives.length === 0) return true;
|
|
186
|
+
return positives.includes(target);
|
|
187
|
+
}
|
|
188
|
+
function isPackageCompatible(info) {
|
|
189
|
+
const osList = normalizeList(info.packageJson.os);
|
|
190
|
+
const cpuList = normalizeList(info.packageJson.cpu);
|
|
191
|
+
return listAllowsTarget(osList, SANDBOX_PLATFORM) && listAllowsTarget(cpuList, SANDBOX_ARCH);
|
|
192
|
+
}
|
|
193
|
+
async function runSandboxCommand(sandbox, cmd, timeoutMs) {
|
|
194
|
+
let stdout = "";
|
|
195
|
+
let stderr = "";
|
|
196
|
+
try {
|
|
197
|
+
const result = await sandbox.commands.run(cmd, {
|
|
198
|
+
timeoutMs,
|
|
199
|
+
onStdout: (data) => {
|
|
200
|
+
stdout += data;
|
|
201
|
+
},
|
|
202
|
+
onStderr: (data) => {
|
|
203
|
+
stderr += data;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
exitCode: result.exitCode,
|
|
208
|
+
stdout: stdout || result.stdout,
|
|
209
|
+
stderr: stderr || result.stderr
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const exitCode = error.exitCode ?? 1;
|
|
213
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
214
|
+
return {
|
|
215
|
+
exitCode,
|
|
216
|
+
stdout,
|
|
217
|
+
stderr: stderr || errorMessage
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function getCallerFile() {
|
|
222
|
+
const stack = new Error().stack?.split("\n") ?? [];
|
|
223
|
+
for (const line of stack.slice(2)) {
|
|
224
|
+
const match = line.match(/\((.+?):\d+:\d+\)/) || line.match(/at (.+?):\d+:\d+/);
|
|
225
|
+
if (match) {
|
|
226
|
+
let file = match[1];
|
|
227
|
+
if (file.startsWith("file://")) file = fileURLToPath(file);
|
|
228
|
+
if (!file.includes("node:") && !file.includes("node_modules/neckbeard-agent") && !file.includes("neckbeard-agent/dist") && !file.includes("node_modules/agent-neckbeard") && !file.includes("agent-neckbeard/dist")) return file;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
throw new Error("Could not determine source file");
|
|
232
|
+
}
|
|
233
|
+
function readDirectoryRecursively(dirPath) {
|
|
234
|
+
const files = [];
|
|
235
|
+
function walkDir(currentPath) {
|
|
236
|
+
const entries = readdirSync(currentPath);
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
const fullPath = join(currentPath, entry);
|
|
239
|
+
const stat = statSync(fullPath);
|
|
240
|
+
if (stat.isDirectory()) {
|
|
241
|
+
walkDir(fullPath);
|
|
242
|
+
} else if (stat.isFile()) {
|
|
243
|
+
const relPath = relative(dirPath, fullPath);
|
|
244
|
+
const isBinary = /\.(png|jpg|jpeg|gif|ico|pdf|zip|tar|gz|bin|exe|dll|so|dylib|wasm)$/i.test(entry);
|
|
245
|
+
if (isBinary) {
|
|
246
|
+
const buffer = readFileSync(fullPath);
|
|
247
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
248
|
+
files.push({ relativePath: relPath, content: arrayBuffer });
|
|
249
|
+
} else {
|
|
250
|
+
files.push({ relativePath: relPath, content: readFileSync(fullPath, "utf-8") });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
walkDir(dirPath);
|
|
256
|
+
return files;
|
|
257
|
+
}
|
|
258
|
+
var Agent = class {
|
|
259
|
+
template;
|
|
260
|
+
inputSchema;
|
|
261
|
+
outputSchema;
|
|
262
|
+
maxDuration;
|
|
263
|
+
dependencies;
|
|
264
|
+
files;
|
|
265
|
+
claudeDir;
|
|
266
|
+
envs;
|
|
267
|
+
build;
|
|
268
|
+
/** @internal Used by the sandbox runner - must be public for bundled code access */
|
|
269
|
+
_run;
|
|
270
|
+
_sourceFile;
|
|
271
|
+
_sandboxId;
|
|
272
|
+
constructor(config) {
|
|
273
|
+
this.template = config.template;
|
|
274
|
+
this.inputSchema = config.inputSchema;
|
|
275
|
+
this.outputSchema = config.outputSchema;
|
|
276
|
+
this.maxDuration = config.maxDuration ?? DEFAULT_MAX_DURATION;
|
|
277
|
+
this._run = config.run;
|
|
278
|
+
this._sourceFile = getCallerFile();
|
|
279
|
+
this.dependencies = config.dependencies ?? DEFAULT_DEPENDENCIES;
|
|
280
|
+
this.files = config.files ?? [];
|
|
281
|
+
this.claudeDir = config.claudeDir;
|
|
282
|
+
this.envs = config.envs ?? {};
|
|
283
|
+
this.build = config.build ?? {};
|
|
284
|
+
}
|
|
285
|
+
get sandboxId() {
|
|
286
|
+
return this._sandboxId;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Deploys the agent to an E2B sandbox.
|
|
290
|
+
*
|
|
291
|
+
* This method bundles the agent code using esbuild, creates a new E2B sandbox,
|
|
292
|
+
* installs any specified OS dependencies, downloads configured files, and
|
|
293
|
+
* uploads the agent code to the sandbox.
|
|
294
|
+
*
|
|
295
|
+
* @returns The sandbox ID, which can be used to reconnect via `run(input, { sandboxId })`
|
|
296
|
+
* @throws {ConfigurationError} If E2B_API_KEY environment variable is not set
|
|
297
|
+
* @throws {DeploymentError} If sandbox creation, dependency installation, or file upload fails
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```typescript
|
|
301
|
+
* const agent = new Agent({ ... });
|
|
302
|
+
* const sandboxId = await agent.deploy();
|
|
303
|
+
* console.log(`Deployed to sandbox: ${sandboxId}`);
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
async deploy() {
|
|
307
|
+
if (this._sandboxId) return this._sandboxId;
|
|
308
|
+
const e2bApiKey = requireEnv("E2B_API_KEY");
|
|
309
|
+
const esbuild = await getEsbuild();
|
|
310
|
+
const { Sandbox } = await getE2b();
|
|
311
|
+
const externalPatterns = Array.from(
|
|
312
|
+
/* @__PURE__ */ new Set([...DEFAULT_EXTERNALS, ...this.build.external ?? []])
|
|
313
|
+
);
|
|
314
|
+
const externalMatchers = externalPatterns.map(globToRegExp);
|
|
315
|
+
const autoDetectExternal = this.build.autoDetectExternal ?? true;
|
|
316
|
+
const collectedExternals = /* @__PURE__ */ new Map();
|
|
317
|
+
const result = await esbuild.build({
|
|
318
|
+
entryPoints: [this._sourceFile],
|
|
319
|
+
bundle: true,
|
|
320
|
+
platform: "node",
|
|
321
|
+
target: NODE_TARGET,
|
|
322
|
+
format: "esm",
|
|
323
|
+
write: false,
|
|
324
|
+
minify: true,
|
|
325
|
+
keepNames: true,
|
|
326
|
+
treeShaking: false,
|
|
327
|
+
// Preserve exports for the sandbox runner to import
|
|
328
|
+
banner: {
|
|
329
|
+
js: `import { fileURLToPath as __neckbeard_fileURLToPath } from 'node:url';
|
|
330
|
+
import { dirname as __neckbeard_dirname } from 'node:path';
|
|
331
|
+
import { createRequire as __neckbeard_createRequire } from 'node:module';
|
|
332
|
+
var __filename = __neckbeard_fileURLToPath(import.meta.url);
|
|
333
|
+
var __dirname = __neckbeard_dirname(__filename);
|
|
334
|
+
var require = __neckbeard_createRequire(import.meta.url);
|
|
335
|
+
`
|
|
336
|
+
},
|
|
337
|
+
plugins: [{
|
|
338
|
+
name: "agent-neckbeard-externals",
|
|
339
|
+
setup(build) {
|
|
340
|
+
build.onResolve({ filter: /^(agent-neckbeard|neckbeard-agent)$/ }, () => ({
|
|
341
|
+
path: "agent-neckbeard",
|
|
342
|
+
namespace: "agent-shim"
|
|
343
|
+
}));
|
|
344
|
+
build.onLoad({ filter: /.*/, namespace: "agent-shim" }, () => ({
|
|
345
|
+
contents: `
|
|
346
|
+
export class Agent {
|
|
347
|
+
constructor(config) {
|
|
348
|
+
this.inputSchema = config.inputSchema;
|
|
349
|
+
this.outputSchema = config.outputSchema;
|
|
350
|
+
this.maxDuration = config.maxDuration ?? 300;
|
|
351
|
+
this._run = config.run;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
`,
|
|
355
|
+
loader: "js"
|
|
356
|
+
}));
|
|
357
|
+
build.onResolve({ filter: /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/ }, async (args) => {
|
|
358
|
+
if (args.path.startsWith("node:")) return null;
|
|
359
|
+
if (builtinModules.includes(args.path.replace("node:", ""))) return null;
|
|
360
|
+
if (!isBareModuleImport(args.path)) return null;
|
|
361
|
+
const pkgName = packageNameForImportPath(args.path);
|
|
362
|
+
if (externalMatchers.length && matchesExternal(externalMatchers, pkgName)) {
|
|
363
|
+
const info2 = await resolvePackageInfo(args.path, args.resolveDir);
|
|
364
|
+
if (!info2) return null;
|
|
365
|
+
collectedExternals.set(info2.name, info2);
|
|
366
|
+
return { external: true };
|
|
367
|
+
}
|
|
368
|
+
if (!autoDetectExternal) return null;
|
|
369
|
+
const info = await resolvePackageInfo(args.path, args.resolveDir);
|
|
370
|
+
if (!info) return null;
|
|
371
|
+
if (!shouldAutoExternalize(info)) return null;
|
|
372
|
+
collectedExternals.set(info.name, info);
|
|
373
|
+
return { external: true };
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}]
|
|
377
|
+
});
|
|
378
|
+
const runnerCode = `
|
|
379
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
380
|
+
|
|
381
|
+
const IO_DIR = '/home/user/io';
|
|
382
|
+
const INPUT_FILE = IO_DIR + '/input.json';
|
|
383
|
+
const OUTPUT_FILE = IO_DIR + '/output.json';
|
|
384
|
+
|
|
385
|
+
mkdirSync(IO_DIR, { recursive: true });
|
|
386
|
+
|
|
387
|
+
let input, executionId;
|
|
388
|
+
try {
|
|
389
|
+
const taskData = JSON.parse(readFileSync(INPUT_FILE, 'utf-8'));
|
|
390
|
+
input = taskData.input;
|
|
391
|
+
executionId = taskData.executionId;
|
|
392
|
+
} catch (parseError) {
|
|
393
|
+
writeFileSync(OUTPUT_FILE, JSON.stringify({ success: false, error: { message: 'Task parse failed: ' + parseError.message, stack: parseError.stack } }));
|
|
394
|
+
process.exit(0);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let mod;
|
|
398
|
+
try {
|
|
399
|
+
mod = await import('./agent.mjs');
|
|
400
|
+
} catch (importError) {
|
|
401
|
+
writeFileSync(OUTPUT_FILE, JSON.stringify({ success: false, error: { message: 'Import failed: ' + importError.message, stack: importError.stack } }));
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const agent = mod.default || Object.values(mod).find(v => v instanceof Object && v._run);
|
|
406
|
+
if (!agent) {
|
|
407
|
+
writeFileSync(OUTPUT_FILE, JSON.stringify({ success: false, error: { message: 'No agent found in module. Exports: ' + Object.keys(mod).join(', ') } }));
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const ctx = {
|
|
412
|
+
executionId,
|
|
413
|
+
signal: AbortSignal.timeout(${this.maxDuration * 1e3}),
|
|
414
|
+
env: process.env,
|
|
415
|
+
logger: {
|
|
416
|
+
debug: (msg, ...args) => console.log('[DEBUG]', msg, ...args),
|
|
417
|
+
info: (msg, ...args) => console.log('[INFO]', msg, ...args),
|
|
418
|
+
warn: (msg, ...args) => console.warn('[WARN]', msg, ...args),
|
|
419
|
+
error: (msg, ...args) => console.error('[ERROR]', msg, ...args),
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const validated = agent.inputSchema.parse(input);
|
|
425
|
+
const output = await agent._run(validated, ctx);
|
|
426
|
+
const validatedOutput = agent.outputSchema.parse(output);
|
|
427
|
+
writeFileSync(OUTPUT_FILE, JSON.stringify({ success: true, output: validatedOutput }));
|
|
428
|
+
} catch (error) {
|
|
429
|
+
writeFileSync(OUTPUT_FILE, JSON.stringify({ success: false, error: { message: error.message, stack: error.stack } }));
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
const sandbox = await Sandbox.create(this.template, {
|
|
433
|
+
apiKey: e2bApiKey
|
|
434
|
+
});
|
|
435
|
+
await runSandboxCommand(sandbox, `mkdir -p ${SANDBOX_PATHS.agentDir}`, QUICK_COMMAND_TIMEOUT_MS);
|
|
436
|
+
const { apt, commands } = this.dependencies;
|
|
437
|
+
if (apt && apt.length > 0) {
|
|
438
|
+
const aptCmd = `sudo apt-get update && sudo apt-get install -y ${apt.join(" ")}`;
|
|
439
|
+
const aptResult = await runSandboxCommand(sandbox, aptCmd, SANDBOX_COMMAND_TIMEOUT_MS);
|
|
440
|
+
if (aptResult.exitCode !== 0) {
|
|
441
|
+
const details = [
|
|
442
|
+
`Failed to install apt packages: ${apt.join(", ")}`,
|
|
443
|
+
aptResult.stderr ? `stderr: ${aptResult.stderr}` : "",
|
|
444
|
+
aptResult.stdout ? `stdout: ${aptResult.stdout}` : ""
|
|
445
|
+
].filter(Boolean).join("\n");
|
|
446
|
+
throw new DeploymentError(details);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (commands && commands.length > 0) {
|
|
450
|
+
for (const cmd of commands) {
|
|
451
|
+
const cmdResult = await runSandboxCommand(sandbox, cmd, SANDBOX_COMMAND_TIMEOUT_MS);
|
|
452
|
+
if (cmdResult.exitCode !== 0) {
|
|
453
|
+
const details = [
|
|
454
|
+
`Failed to run command: ${cmd}`,
|
|
455
|
+
cmdResult.stderr ? `stderr: ${cmdResult.stderr}` : "",
|
|
456
|
+
cmdResult.stdout ? `stdout: ${cmdResult.stdout}` : ""
|
|
457
|
+
].filter(Boolean).join("\n");
|
|
458
|
+
throw new DeploymentError(details);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (this.files.length > 0) {
|
|
463
|
+
for (const file of this.files) {
|
|
464
|
+
const destPath = file.path.startsWith("/") ? file.path : `${SANDBOX_HOME}/${file.path}`;
|
|
465
|
+
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
466
|
+
if (parentDir) {
|
|
467
|
+
await runSandboxCommand(sandbox, `mkdir -p ${shellEscape(parentDir)}`, QUICK_COMMAND_TIMEOUT_MS);
|
|
468
|
+
}
|
|
469
|
+
const curlCmd = `curl -fsSL -o ${shellEscape(destPath)} ${shellEscape(file.url)}`;
|
|
470
|
+
const downloadResult = await runSandboxCommand(sandbox, curlCmd, SANDBOX_COMMAND_TIMEOUT_MS);
|
|
471
|
+
if (downloadResult.exitCode !== 0) {
|
|
472
|
+
const details = [
|
|
473
|
+
`Failed to download file from ${file.url} to ${destPath}`,
|
|
474
|
+
downloadResult.stderr ? `stderr: ${downloadResult.stderr}` : "",
|
|
475
|
+
downloadResult.stdout ? `stdout: ${downloadResult.stdout}` : ""
|
|
476
|
+
].filter(Boolean).join("\n");
|
|
477
|
+
throw new DeploymentError(details);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (this.claudeDir) {
|
|
482
|
+
const claudeFiles = readDirectoryRecursively(this.claudeDir);
|
|
483
|
+
await runSandboxCommand(sandbox, `mkdir -p ${SANDBOX_PATHS.claudeDir}`, QUICK_COMMAND_TIMEOUT_MS);
|
|
484
|
+
for (const file of claudeFiles) {
|
|
485
|
+
const destPath = `${SANDBOX_PATHS.claudeDir}/${file.relativePath}`;
|
|
486
|
+
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
487
|
+
if (parentDir && parentDir !== SANDBOX_PATHS.claudeDir) {
|
|
488
|
+
await runSandboxCommand(sandbox, `mkdir -p ${shellEscape(parentDir)}`, QUICK_COMMAND_TIMEOUT_MS);
|
|
489
|
+
}
|
|
490
|
+
await sandbox.files.write(destPath, file.content);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
await sandbox.files.write(SANDBOX_PATHS.agentModule, result.outputFiles[0].text);
|
|
494
|
+
await sandbox.files.write(SANDBOX_PATHS.runnerModule, runnerCode);
|
|
495
|
+
if (collectedExternals.size > 0) {
|
|
496
|
+
const installable = Array.from(collectedExternals.values()).filter(isPackageCompatible);
|
|
497
|
+
const dependencies = Object.fromEntries(installable.map((info) => [info.name, info.version]));
|
|
498
|
+
if (Object.keys(dependencies).length === 0) {
|
|
499
|
+
this._sandboxId = sandbox.sandboxId;
|
|
500
|
+
return this._sandboxId;
|
|
501
|
+
}
|
|
502
|
+
const pkgJson = JSON.stringify({
|
|
503
|
+
name: "agent-sandbox",
|
|
504
|
+
type: "module",
|
|
505
|
+
dependencies
|
|
506
|
+
});
|
|
507
|
+
await sandbox.files.write(SANDBOX_PATHS.packageJson, pkgJson);
|
|
508
|
+
const installResult = await runSandboxCommand(
|
|
509
|
+
sandbox,
|
|
510
|
+
`cd ${SANDBOX_PATHS.agentDir} && npm install --legacy-peer-deps`,
|
|
511
|
+
SANDBOX_COMMAND_TIMEOUT_MS
|
|
512
|
+
);
|
|
513
|
+
if (installResult.exitCode !== 0) {
|
|
514
|
+
const details = [
|
|
515
|
+
`Failed to install npm packages: ${Object.keys(dependencies).join(", ")}`,
|
|
516
|
+
installResult.stderr ? `stderr: ${installResult.stderr}` : "",
|
|
517
|
+
installResult.stdout ? `stdout: ${installResult.stdout}` : ""
|
|
518
|
+
].filter(Boolean).join("\n");
|
|
519
|
+
throw new DeploymentError(details);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
this._sandboxId = sandbox.sandboxId;
|
|
523
|
+
return this._sandboxId;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Executes the agent with the given input.
|
|
527
|
+
*
|
|
528
|
+
* The agent must be deployed before calling this method, or a sandboxId must
|
|
529
|
+
* be provided in the options. Input is validated against the input schema,
|
|
530
|
+
* then the agent runs in the sandbox with the validated input. Output is
|
|
531
|
+
* validated against the output schema before being returned.
|
|
532
|
+
*
|
|
533
|
+
* @param input - The input data for the agent, must conform to inputSchema
|
|
534
|
+
* @param options - Optional configuration for this run
|
|
535
|
+
* @param options.sandboxId - Sandbox ID to use, overrides the deployed sandbox
|
|
536
|
+
* @returns A result object indicating success or failure with output or error
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* ```typescript
|
|
540
|
+
* // Using deployed sandbox
|
|
541
|
+
* const result = await agent.run({ prompt: 'Hello, world!' });
|
|
542
|
+
*
|
|
543
|
+
* // Using explicit sandboxId (for reconnecting)
|
|
544
|
+
* const result = await agent.run({ prompt: 'Hello!' }, { sandboxId: 'sbx_...' });
|
|
545
|
+
* ```
|
|
546
|
+
*/
|
|
547
|
+
async run(input, options) {
|
|
548
|
+
const executionId = `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
549
|
+
const sandboxId = options?.sandboxId ?? this._sandboxId;
|
|
550
|
+
if (!sandboxId) {
|
|
551
|
+
return {
|
|
552
|
+
ok: false,
|
|
553
|
+
executionId,
|
|
554
|
+
error: new ExecutionError("Agent not deployed. Call agent.deploy() first or pass sandboxId to run().")
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const e2bApiKey = requireEnv("E2B_API_KEY");
|
|
559
|
+
const { Sandbox } = await getE2b();
|
|
560
|
+
const validatedInput = this.inputSchema.parse(input);
|
|
561
|
+
const sandbox = await Sandbox.connect(sandboxId, {
|
|
562
|
+
apiKey: e2bApiKey
|
|
563
|
+
});
|
|
564
|
+
await sandbox.files.write(SANDBOX_PATHS.inputJson, JSON.stringify({ input: validatedInput, executionId }));
|
|
565
|
+
let capturedStdout = "";
|
|
566
|
+
let capturedStderr = "";
|
|
567
|
+
const result = await sandbox.commands.run(`cd ${SANDBOX_PATHS.agentDir} && node runner.mjs`, {
|
|
568
|
+
timeoutMs: this.maxDuration * 1e3,
|
|
569
|
+
envs: Object.fromEntries(
|
|
570
|
+
Object.entries(this.envs).filter(([_, v]) => v !== void 0).map(([k, v]) => [k, v])
|
|
571
|
+
),
|
|
572
|
+
onStdout: (data) => {
|
|
573
|
+
capturedStdout += data;
|
|
574
|
+
},
|
|
575
|
+
onStderr: (data) => {
|
|
576
|
+
capturedStderr += data;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
if (result.exitCode !== 0) {
|
|
580
|
+
let resultError = "";
|
|
581
|
+
try {
|
|
582
|
+
const resultJson = await sandbox.files.read(SANDBOX_PATHS.outputJson);
|
|
583
|
+
const parsed = JSON.parse(resultJson);
|
|
584
|
+
if (!parsed.success && parsed.error) {
|
|
585
|
+
resultError = `
|
|
586
|
+
Result: ${parsed.error.message}`;
|
|
587
|
+
if (parsed.error.stack) resultError += `
|
|
588
|
+
Stack: ${parsed.error.stack}`;
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
const errorDetails = [
|
|
593
|
+
`Agent failed with exit code ${result.exitCode}`,
|
|
594
|
+
result.stderr ? `Stderr: ${result.stderr}` : "",
|
|
595
|
+
capturedStderr ? `Captured stderr: ${capturedStderr}` : "",
|
|
596
|
+
capturedStdout ? `Stdout: ${capturedStdout}` : "",
|
|
597
|
+
resultError
|
|
598
|
+
].filter(Boolean).join("\n");
|
|
599
|
+
return { ok: false, executionId, error: new ExecutionError(errorDetails) };
|
|
600
|
+
}
|
|
601
|
+
const output = JSON.parse(await sandbox.files.read(SANDBOX_PATHS.outputJson));
|
|
602
|
+
if (!output.success) {
|
|
603
|
+
const errorDetails = [
|
|
604
|
+
output.error.message,
|
|
605
|
+
capturedStderr ? `
|
|
606
|
+
Captured stderr: ${capturedStderr}` : "",
|
|
607
|
+
capturedStdout ? `
|
|
608
|
+
Stdout: ${capturedStdout}` : ""
|
|
609
|
+
].filter(Boolean).join("");
|
|
610
|
+
const err = new ExecutionError(errorDetails);
|
|
611
|
+
err.stack = output.error.stack;
|
|
612
|
+
return { ok: false, executionId, error: err };
|
|
613
|
+
}
|
|
614
|
+
return { ok: true, executionId, output: this.outputSchema.parse(output.output) };
|
|
615
|
+
} catch (err) {
|
|
616
|
+
return { ok: false, executionId, error: err instanceof Error ? err : new Error(String(err)) };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
export {
|
|
621
|
+
Agent,
|
|
622
|
+
ConfigurationError,
|
|
623
|
+
DEFAULT_DEPENDENCIES,
|
|
624
|
+
DeploymentError,
|
|
625
|
+
ExecutionError,
|
|
626
|
+
NeckbeardError,
|
|
627
|
+
ValidationError
|
|
628
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-neckbeard",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Deploy AI agents to E2B sandboxes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"require": {
|
|
13
|
+
"types": "./dist/index.d.cts",
|
|
14
|
+
"default": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.cts",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"prepare": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"esbuild": "^0.24.0",
|
|
36
|
+
"pkg-types": "^2.3.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"e2b": "^2.2.0",
|
|
40
|
+
"zod": "^3.0.0 || ^4.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"e2b": {
|
|
44
|
+
"optional": false
|
|
45
|
+
},
|
|
46
|
+
"zod": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22.0.0",
|
|
52
|
+
"e2b": "^2.2.1",
|
|
53
|
+
"tsup": "^8.3.0",
|
|
54
|
+
"typescript": "^5.6.0"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"ai",
|
|
58
|
+
"agent",
|
|
59
|
+
"deploy",
|
|
60
|
+
"e2b",
|
|
61
|
+
"sandbox",
|
|
62
|
+
"claude"
|
|
63
|
+
],
|
|
64
|
+
"license": "MIT",
|
|
65
|
+
"author": "zacwellmer",
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "git+https://github.com/zacwellmer/agent-neckbeard.git"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/zacwellmer/agent-neckbeard#readme",
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/zacwellmer/agent-neckbeard/issues"
|
|
73
|
+
},
|
|
74
|
+
"publishConfig": {
|
|
75
|
+
"access": "public"
|
|
76
|
+
}
|
|
77
|
+
}
|