agent-neckbeard 0.0.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 +140 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +114 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# agent-neckbeard
|
|
2
|
+
|
|
3
|
+
Deploy AI agents to E2B sandboxes with structured input/output validation.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When building agentic workflows with the [Claude Agent SDK](https://github.com/anthropics/claude-code), the agent process needs to run **inside** a sandbox—not just the tool calls. This means:
|
|
8
|
+
|
|
9
|
+
- The agent can execute bash commands, write files, and perform complex multi-step tasks
|
|
10
|
+
- All execution is isolated and secure
|
|
11
|
+
- The agent maintains conversation state across tool calls
|
|
12
|
+
|
|
13
|
+
E2B provides sandboxed environments, but wiring up the deployment, bundling, and I/O is tedious. This package handles it for you.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
1. **Define** an agent with typed input/output schemas
|
|
18
|
+
2. **Deploy** bundles your code with esbuild and uploads to E2B
|
|
19
|
+
3. **Run** writes input to `/input/task.json`, executes, reads from `/output/result.json`
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Agent } from 'agent-neckbeard';
|
|
23
|
+
import { query } from '@anthropic-ai/claude-code';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
|
|
26
|
+
const summaryAgent = new Agent({
|
|
27
|
+
id: 'summary',
|
|
28
|
+
maxDuration: 120,
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
topic: z.string(),
|
|
31
|
+
}),
|
|
32
|
+
outputSchema: z.object({
|
|
33
|
+
title: z.string(),
|
|
34
|
+
summary: z.string(),
|
|
35
|
+
keyPoints: z.array(z.string()),
|
|
36
|
+
}),
|
|
37
|
+
run: async (input, ctx) => {
|
|
38
|
+
let result = { title: '', summary: '', keyPoints: [] as string[] };
|
|
39
|
+
|
|
40
|
+
for await (const message of query({
|
|
41
|
+
prompt: `Research "${input.topic}" and return JSON with title, summary, keyPoints`,
|
|
42
|
+
options: { maxTurns: 10, allowedTools: ['write', 'read'] },
|
|
43
|
+
abortSignal: ctx.signal,
|
|
44
|
+
})) {
|
|
45
|
+
if (message.type === 'result') {
|
|
46
|
+
result = JSON.parse(message.result ?? '{}');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Deploy once
|
|
55
|
+
await summaryAgent.deploy();
|
|
56
|
+
|
|
57
|
+
// Run many times
|
|
58
|
+
const result = await summaryAgent.run({ topic: 'TypeScript generics' });
|
|
59
|
+
console.log(result.title); // string - validated
|
|
60
|
+
console.log(result.keyPoints); // string[] - validated
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install agent-neckbeard
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Set your E2B API key:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export E2B_API_KEY=your-api-key
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `new Agent(config)`
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const agent = new Agent({
|
|
81
|
+
id: string, // Unique identifier
|
|
82
|
+
inputSchema: ZodSchema, // Validates input before run
|
|
83
|
+
outputSchema: ZodSchema, // Validates output after run
|
|
84
|
+
run: (input, ctx) => Promise, // Your agent logic
|
|
85
|
+
maxDuration?: number, // Timeout in seconds (default: 300)
|
|
86
|
+
sandboxId?: string, // Reuse existing sandbox (skip deploy)
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `agent.deploy()`
|
|
91
|
+
|
|
92
|
+
Bundles your agent code with esbuild and uploads to a new E2B sandbox. Automatically detects the source file—no entry point needed.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
await agent.deploy();
|
|
96
|
+
console.log(agent.sandboxId); // 'sandbox-abc123'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `agent.run(input)`
|
|
100
|
+
|
|
101
|
+
Executes the agent in the sandbox with validated input/output.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const result = await agent.run({ topic: 'quantum computing' });
|
|
105
|
+
// result is typed and validated against outputSchema
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Reusing a Sandbox
|
|
109
|
+
|
|
110
|
+
If you already have a deployed sandbox, pass `sandboxId` to skip deployment:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const agent = new Agent({
|
|
114
|
+
id: 'summary',
|
|
115
|
+
sandboxId: 'existing-sandbox-id', // Skip deploy()
|
|
116
|
+
inputSchema,
|
|
117
|
+
outputSchema,
|
|
118
|
+
run,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// No deploy needed
|
|
122
|
+
const result = await agent.run({ topic: 'hello' });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Context
|
|
126
|
+
|
|
127
|
+
The `run` function receives a context object:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
run: async (input, ctx) => {
|
|
131
|
+
ctx.executionId // Unique ID for this execution
|
|
132
|
+
ctx.signal // AbortSignal for cancellation
|
|
133
|
+
ctx.env // Environment variables
|
|
134
|
+
ctx.logger // { debug, info, warn, error }
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-neckbeard - Deploy AI agents to E2B sandboxes
|
|
3
|
+
*/
|
|
4
|
+
interface AgentRunContext {
|
|
5
|
+
executionId: string;
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
env: Record<string, string>;
|
|
8
|
+
logger: {
|
|
9
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
10
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
11
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
12
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
interface SchemaLike<T> {
|
|
16
|
+
parse: (data: unknown) => T;
|
|
17
|
+
}
|
|
18
|
+
interface AgentConfig<TInput, TOutput> {
|
|
19
|
+
id: string;
|
|
20
|
+
inputSchema: SchemaLike<TInput>;
|
|
21
|
+
outputSchema: SchemaLike<TOutput>;
|
|
22
|
+
run: (input: TInput, ctx: AgentRunContext) => Promise<TOutput>;
|
|
23
|
+
maxDuration?: number;
|
|
24
|
+
sandboxId?: string;
|
|
25
|
+
}
|
|
26
|
+
declare class Agent<TInput, TOutput> {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly inputSchema: SchemaLike<TInput>;
|
|
29
|
+
readonly outputSchema: SchemaLike<TOutput>;
|
|
30
|
+
readonly maxDuration: number;
|
|
31
|
+
/** @internal Used by the sandbox runner - must be public for bundled code access */
|
|
32
|
+
_run: (input: TInput, ctx: AgentRunContext) => Promise<TOutput>;
|
|
33
|
+
private _sourceFile;
|
|
34
|
+
private _sandboxId?;
|
|
35
|
+
constructor(config: AgentConfig<TInput, TOutput>);
|
|
36
|
+
get sandboxId(): string | undefined;
|
|
37
|
+
deploy(): Promise<void>;
|
|
38
|
+
run(input: TInput): Promise<TOutput>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { Agent, type AgentConfig, type AgentRunContext };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as esbuild from "esbuild";
|
|
3
|
+
import { Sandbox } from "e2b";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
function getCallerFile() {
|
|
6
|
+
const stack = new Error().stack?.split("\n") ?? [];
|
|
7
|
+
for (const line of stack.slice(2)) {
|
|
8
|
+
const match = line.match(/\((.+?):\d+:\d+\)/) || line.match(/at (.+?):\d+:\d+/);
|
|
9
|
+
if (match) {
|
|
10
|
+
let file = match[1];
|
|
11
|
+
if (file.startsWith("file://")) file = fileURLToPath(file);
|
|
12
|
+
if (!file.includes("node:") && !file.includes("agent-neckbeard")) return file;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
throw new Error("Could not determine source file");
|
|
16
|
+
}
|
|
17
|
+
var Agent = class {
|
|
18
|
+
id;
|
|
19
|
+
inputSchema;
|
|
20
|
+
outputSchema;
|
|
21
|
+
maxDuration;
|
|
22
|
+
/** @internal Used by the sandbox runner - must be public for bundled code access */
|
|
23
|
+
_run;
|
|
24
|
+
_sourceFile;
|
|
25
|
+
_sandboxId;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.id = config.id;
|
|
28
|
+
this.inputSchema = config.inputSchema;
|
|
29
|
+
this.outputSchema = config.outputSchema;
|
|
30
|
+
this.maxDuration = config.maxDuration ?? 300;
|
|
31
|
+
this._run = config.run;
|
|
32
|
+
this._sourceFile = getCallerFile();
|
|
33
|
+
this._sandboxId = config.sandboxId;
|
|
34
|
+
}
|
|
35
|
+
get sandboxId() {
|
|
36
|
+
return this._sandboxId;
|
|
37
|
+
}
|
|
38
|
+
async deploy() {
|
|
39
|
+
if (this._sandboxId) return;
|
|
40
|
+
const result = await esbuild.build({
|
|
41
|
+
entryPoints: [this._sourceFile],
|
|
42
|
+
bundle: true,
|
|
43
|
+
platform: "node",
|
|
44
|
+
target: "node20",
|
|
45
|
+
format: "esm",
|
|
46
|
+
write: false,
|
|
47
|
+
minify: true,
|
|
48
|
+
keepNames: true
|
|
49
|
+
});
|
|
50
|
+
const runnerCode = `
|
|
51
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
52
|
+
|
|
53
|
+
mkdirSync('/input', { recursive: true });
|
|
54
|
+
mkdirSync('/output', { recursive: true });
|
|
55
|
+
|
|
56
|
+
const { input, executionId } = JSON.parse(readFileSync('/input/task.json', 'utf-8'));
|
|
57
|
+
|
|
58
|
+
const ctx = {
|
|
59
|
+
executionId,
|
|
60
|
+
signal: AbortSignal.timeout(${this.maxDuration * 1e3}),
|
|
61
|
+
env: process.env,
|
|
62
|
+
logger: {
|
|
63
|
+
debug: (msg, ...args) => console.log('[DEBUG]', msg, ...args),
|
|
64
|
+
info: (msg, ...args) => console.log('[INFO]', msg, ...args),
|
|
65
|
+
warn: (msg, ...args) => console.warn('[WARN]', msg, ...args),
|
|
66
|
+
error: (msg, ...args) => console.error('[ERROR]', msg, ...args),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const mod = await import('./agent.mjs');
|
|
71
|
+
const agent = mod.default || Object.values(mod).find(v => v instanceof Object && v.id);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const validated = agent.inputSchema.parse(input);
|
|
75
|
+
const output = await agent._run(validated, ctx);
|
|
76
|
+
const validatedOutput = agent.outputSchema.parse(output);
|
|
77
|
+
writeFileSync('/output/result.json', JSON.stringify({ success: true, output: validatedOutput }));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
writeFileSync('/output/result.json', JSON.stringify({ success: false, error: { message: error.message, stack: error.stack } }));
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
const sandbox = await Sandbox.create("base", {
|
|
83
|
+
apiKey: process.env.E2B_API_KEY
|
|
84
|
+
});
|
|
85
|
+
await sandbox.files.write("/home/user/agent.mjs", result.outputFiles[0].text);
|
|
86
|
+
await sandbox.files.write("/home/user/runner.mjs", runnerCode);
|
|
87
|
+
this._sandboxId = sandbox.sandboxId;
|
|
88
|
+
}
|
|
89
|
+
async run(input) {
|
|
90
|
+
if (!this._sandboxId) {
|
|
91
|
+
throw new Error("Agent not deployed. Call agent.deploy() first or pass sandboxId to constructor.");
|
|
92
|
+
}
|
|
93
|
+
const validatedInput = this.inputSchema.parse(input);
|
|
94
|
+
const executionId = `exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
95
|
+
const sandbox = await Sandbox.connect(this._sandboxId, {
|
|
96
|
+
apiKey: process.env.E2B_API_KEY
|
|
97
|
+
});
|
|
98
|
+
await sandbox.files.write("/input/task.json", JSON.stringify({ input: validatedInput, executionId }));
|
|
99
|
+
const result = await sandbox.commands.run("node /home/user/runner.mjs", {
|
|
100
|
+
timeoutMs: this.maxDuration * 1e3
|
|
101
|
+
});
|
|
102
|
+
if (result.exitCode !== 0) throw new Error(`Agent failed: ${result.stderr}`);
|
|
103
|
+
const output = JSON.parse(await sandbox.files.read("/output/result.json"));
|
|
104
|
+
if (!output.success) {
|
|
105
|
+
const err = new Error(output.error.message);
|
|
106
|
+
err.stack = output.error.stack;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
return this.outputSchema.parse(output.output);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
export {
|
|
113
|
+
Agent
|
|
114
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-neckbeard",
|
|
3
|
+
"version": "0.0.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
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"files": ["dist"],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.0.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"esbuild": "^0.24.0",
|
|
27
|
+
"e2b": "^1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"zod": "^3.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"zod": { "optional": true }
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.6.0",
|
|
37
|
+
"tsup": "^8.3.0",
|
|
38
|
+
"@types/node": "^22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": ["ai", "agent", "deploy", "e2b", "sandbox", "claude"],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "zacwellmer",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/zacwellmer/agent-neckbeard.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/zacwellmer/agent-neckbeard#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/zacwellmer/agent-neckbeard/issues"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|