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/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
+ }