@sandblocks/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +21 -0
  2. package/dist/agents.js +209 -0
  3. package/dist/bin.js +59 -0
  4. package/dist/index.js +1018 -0
  5. package/dist/output.js +54 -0
  6. package/dist/providers/custom.js +49 -0
  7. package/dist/providers/docker.js +257 -0
  8. package/dist/providers/podman.js +257 -0
  9. package/dist/providers/remote.js +341 -0
  10. package/dist/providers/unsafe-host.js +126 -0
  11. package/dist/types/agents.d.ts +63 -0
  12. package/dist/types/agents.d.ts.map +1 -0
  13. package/dist/types/bin.d.ts +3 -0
  14. package/dist/types/bin.d.ts.map +1 -0
  15. package/dist/types/index.d.ts +262 -0
  16. package/dist/types/index.d.ts.map +1 -0
  17. package/dist/types/init.d.ts +6 -0
  18. package/dist/types/init.d.ts.map +1 -0
  19. package/dist/types/orchestration.d.ts +94 -0
  20. package/dist/types/orchestration.d.ts.map +1 -0
  21. package/dist/types/output.d.ts +12 -0
  22. package/dist/types/output.d.ts.map +1 -0
  23. package/dist/types/process.d.ts +6 -0
  24. package/dist/types/process.d.ts.map +1 -0
  25. package/dist/types/providers/custom.d.ts +15 -0
  26. package/dist/types/providers/custom.d.ts.map +1 -0
  27. package/dist/types/providers/docker.d.ts +7 -0
  28. package/dist/types/providers/docker.d.ts.map +1 -0
  29. package/dist/types/providers/oci.d.ts +37 -0
  30. package/dist/types/providers/oci.d.ts.map +1 -0
  31. package/dist/types/providers/podman.d.ts +7 -0
  32. package/dist/types/providers/podman.d.ts.map +1 -0
  33. package/dist/types/providers/remote.d.ts +31 -0
  34. package/dist/types/providers/remote.d.ts.map +1 -0
  35. package/dist/types/providers/unsafe-host.d.ts +14 -0
  36. package/dist/types/providers/unsafe-host.d.ts.map +1 -0
  37. package/dist/types/types.d.ts +196 -0
  38. package/dist/types/types.d.ts.map +1 -0
  39. package/package.json +48 -0
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @sandblocks/sdk
2
+
3
+ Programmatic Sandblocks sandbox and agent orchestration for Node.js, Bun, and TypeScript.
4
+
5
+ ```bash
6
+ npm install --save-dev @sandblocks/sdk
7
+ npx sandblocks-sdk init
8
+ ```
9
+
10
+ ```ts
11
+ import { run, CodexAgent } from "@sandblocks/sdk";
12
+ import { DockerProvider } from "@sandblocks/sdk/providers/docker";
13
+
14
+ const result = await run({
15
+ provider: new DockerProvider({ image: "node:22-bookworm" }),
16
+ agent: new CodexAgent({ model: "gpt-5.3-codex" }),
17
+ prompt: "Implement and verify the requested change.",
18
+ });
19
+ ```
20
+
21
+ See [`docs/SDK.md`](../../../docs/SDK.md) in the Sandblocks repository for providers, reusable sandboxes, hooks, worktrees, logging, structured output, security, and CLI parity.
package/dist/agents.js ADDED
@@ -0,0 +1,209 @@
1
+ // src/agents.ts
2
+ class CommandAgent {
3
+ options;
4
+ id;
5
+ constructor(options) {
6
+ this.options = options;
7
+ this.id = options.id ?? "command";
8
+ }
9
+ async run(context) {
10
+ const startedAt = new Date().toISOString();
11
+ const base = typeof this.options.command === "function" ? this.options.command(context) : [...this.options.command];
12
+ const promptMode = this.options.prompt ?? "stdin";
13
+ const command = promptMode === "argument" ? [...base, context.prompt] : base;
14
+ const idle = new AbortController;
15
+ const completed = new AbortController;
16
+ let idleTimer;
17
+ let completionTimer;
18
+ const resetIdle = () => {
19
+ if (idleTimer)
20
+ clearTimeout(idleTimer);
21
+ idleTimer = setTimeout(() => idle.abort(new Error(`agent idle timeout after ${context.idleTimeoutSeconds}s`)), context.idleTimeoutSeconds * 1000);
22
+ };
23
+ resetIdle();
24
+ let output = "";
25
+ let stdout = "";
26
+ let stderr = "";
27
+ let lineBuffer = "";
28
+ let completionSignal;
29
+ const consume = async (chunk, stream) => {
30
+ resetIdle();
31
+ output += chunk;
32
+ if (stream === "stdout")
33
+ stdout += chunk;
34
+ else
35
+ stderr += chunk;
36
+ lineBuffer += chunk;
37
+ const lines = lineBuffer.split(/\r?\n/);
38
+ lineBuffer = lines.pop() ?? "";
39
+ for (const line of lines)
40
+ await this.emitLine(line, stream, context);
41
+ completionSignal ??= context.completionSignal.find((signal) => output.includes(signal));
42
+ if (completionSignal && !completionTimer) {
43
+ completionTimer = setTimeout(() => completed.abort(new Error("agent did not exit after its completion signal")), context.completionTimeoutSeconds * 1000);
44
+ }
45
+ };
46
+ try {
47
+ const signals = [context.signal, idle.signal, completed.signal].filter(Boolean);
48
+ try {
49
+ const result = await context.runtime.exec(command, {
50
+ env: this.options.env,
51
+ stdin: promptMode === "stdin" ? context.prompt : undefined,
52
+ signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
53
+ onStdout: (chunk) => consume(chunk, "stdout"),
54
+ onStderr: (chunk) => consume(chunk, "stderr")
55
+ });
56
+ stdout = result.stdout;
57
+ stderr = result.stderr;
58
+ return this.result(context, startedAt, result.exitCode, stdout, stderr, completionSignal);
59
+ } catch (error) {
60
+ if (!completionSignal)
61
+ throw error;
62
+ return this.result(context, startedAt, 0, stdout, stderr, completionSignal);
63
+ }
64
+ } finally {
65
+ if (idleTimer)
66
+ clearTimeout(idleTimer);
67
+ if (completionTimer)
68
+ clearTimeout(completionTimer);
69
+ if (lineBuffer)
70
+ await this.emitLine(lineBuffer, "stdout", context);
71
+ }
72
+ }
73
+ result(context, startedAt, exitCode, stdout, stderr, completionSignal) {
74
+ const sessionId = this.options.sessionId?.(stdout, stderr);
75
+ return {
76
+ iteration: context.iteration,
77
+ exitCode,
78
+ stdout,
79
+ stderr,
80
+ ...sessionId ? { sessionId } : {},
81
+ ...completionSignal ? { completionSignal } : {},
82
+ startedAt,
83
+ finishedAt: new Date().toISOString()
84
+ };
85
+ }
86
+ async emitLine(line, stream, context) {
87
+ const timestamp = new Date().toISOString();
88
+ const parsed = this.options.parseEvent?.(line, { iteration: context.iteration, timestamp });
89
+ const events = parsed ? Array.isArray(parsed) ? parsed : [parsed] : [
90
+ {
91
+ type: "raw",
92
+ line: stream === "stderr" ? `[stderr] ${line}` : line,
93
+ iteration: context.iteration,
94
+ timestamp
95
+ }
96
+ ];
97
+ for (const event of events)
98
+ await context.emit(event);
99
+ }
100
+ }
101
+
102
+ class ClaudeCodeAgent extends CommandAgent {
103
+ constructor(options) {
104
+ super({
105
+ id: "claude-code",
106
+ env: credentialEnvironment(options.env, options.credential),
107
+ prompt: "argument",
108
+ command: (context) => [
109
+ options.binary ?? "claude",
110
+ "--print",
111
+ "--model",
112
+ options.model,
113
+ ...options.effort ? ["--effort", options.effort] : [],
114
+ ...options.extraArgs ?? []
115
+ ],
116
+ sessionId: extractSessionId,
117
+ parseEvent: parseJsonAgentEvent
118
+ });
119
+ }
120
+ }
121
+
122
+ class CodexAgent extends CommandAgent {
123
+ constructor(options) {
124
+ super({
125
+ id: "codex",
126
+ env: credentialEnvironment(options.env, options.credential),
127
+ prompt: "argument",
128
+ command: (context) => [
129
+ options.binary ?? "codex",
130
+ "exec",
131
+ "--model",
132
+ options.model,
133
+ ...options.reasoningEffort ? ["--config", `model_reasoning_effort=${options.reasoningEffort}`] : [],
134
+ ...options.extraArgs ?? []
135
+ ],
136
+ sessionId: extractSessionId,
137
+ parseEvent: parseJsonAgentEvent
138
+ });
139
+ }
140
+ }
141
+
142
+ class PiAgent extends CommandAgent {
143
+ constructor(options) {
144
+ super({
145
+ id: "pi",
146
+ env: credentialEnvironment(options.env, options.credential),
147
+ prompt: "argument",
148
+ command: (context) => [
149
+ options.binary ?? "pi",
150
+ "--print",
151
+ "--model",
152
+ options.model,
153
+ ...options.skills?.flatMap((skill) => ["--skill", skill]) ?? [],
154
+ ...options.extraArgs ?? []
155
+ ],
156
+ sessionId: extractSessionId,
157
+ parseEvent: parseJsonAgentEvent
158
+ });
159
+ }
160
+ }
161
+ var claudeCode = (model, options = {}) => new ClaudeCodeAgent({ model, ...options });
162
+ var codex = (model, options = {}) => new CodexAgent({ model, ...options });
163
+ var pi = (model, options = {}) => new PiAgent({ model, ...options });
164
+ function parseJsonAgentEvent(line, context) {
165
+ let value;
166
+ try {
167
+ value = JSON.parse(line);
168
+ } catch {
169
+ return { type: "raw", line, ...context };
170
+ }
171
+ const type = String(value.type ?? "");
172
+ if (/tool/i.test(type)) {
173
+ return {
174
+ type: "toolCall",
175
+ name: String(value.name ?? value.tool ?? value.tool_name ?? "tool"),
176
+ ...value.input !== undefined ? { input: value.input } : {},
177
+ ...context
178
+ };
179
+ }
180
+ const text = value.text ?? value.message ?? value.content;
181
+ return typeof text === "string" ? { type: "text", text, ...context } : { type: "raw", line, ...context };
182
+ }
183
+ function credentialEnvironment(env, credential) {
184
+ if (!credential)
185
+ return env;
186
+ if (!/^[A-Z][A-Z0-9_]*$/.test(credential.environment))
187
+ throw new Error("credential environment name is invalid");
188
+ const value = process.env[credential.environment];
189
+ if (value === undefined)
190
+ return env;
191
+ const target = credential.target ?? credential.environment;
192
+ if (!/^[A-Z][A-Z0-9_]*$/.test(target))
193
+ throw new Error("credential target name is invalid");
194
+ return { ...env, [target]: value };
195
+ }
196
+ function extractSessionId(stdout, stderr) {
197
+ const match = `${stdout}
198
+ ${stderr}`.match(/(?:session[_ -]?id|session)[:=\s]+([A-Za-z0-9._-]{8,})/i);
199
+ return match?.[1];
200
+ }
201
+ export {
202
+ pi,
203
+ codex,
204
+ claudeCode,
205
+ PiAgent,
206
+ CommandAgent,
207
+ CodexAgent,
208
+ ClaudeCodeAgent
209
+ };
package/dist/bin.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/init.ts
4
+ import { access, mkdir, writeFile } from "node:fs/promises";
5
+ import { resolve } from "node:path";
6
+ async function initSdk(options = {}) {
7
+ const cwd = resolve(options.cwd ?? process.cwd());
8
+ const root = resolve(cwd, ".sandblocks");
9
+ const files = {
10
+ "main.ts": `import { ClaudeCodeAgent, createSandbox } from "@sandblocks/sdk";
11
+ import { DockerProvider } from "@sandblocks/sdk/providers/docker";
12
+
13
+ await using sandbox = await createSandbox({
14
+ provider: new DockerProvider({ image: "node:22-bookworm" }),
15
+ cwd: process.cwd(),
16
+ });
17
+
18
+ const result = await sandbox.run({
19
+ agent: new ClaudeCodeAgent({ model: "claude-sonnet-4-6" }),
20
+ promptFile: ".sandblocks/prompts/implement.md",
21
+ logging: { type: "stdout" },
22
+ });
23
+
24
+ console.log(result);
25
+ `,
26
+ "prompts/implement.md": `Implement the requested change, run the relevant checks, and commit the result.
27
+
28
+ <promise>COMPLETE</promise>
29
+ `,
30
+ ".env.example": `# Prefer Sandblocks managed secrets for remote providers.
31
+ CLAUDE_CODE_OAUTH_TOKEN=
32
+ # ANTHROPIC_API_KEY=
33
+ SANDBLOCKS_API_URL=
34
+ SANDBLOCKS_API_KEY=
35
+ `,
36
+ "config.json": `${JSON.stringify({ provider: "docker", image: "node:22-bookworm", agent: "claude-code", model: "claude-sonnet-4-6", branchStrategy: "head" }, null, 2)}
37
+ `
38
+ };
39
+ const written = [];
40
+ for (const [relative, content] of Object.entries(files)) {
41
+ const path = resolve(root, relative);
42
+ await mkdir(resolve(path, ".."), { recursive: true });
43
+ if (!options.force && await access(path).then(() => true).catch(() => false))
44
+ continue;
45
+ await writeFile(path, content, { mode: relative.includes(".env") ? 384 : 420 });
46
+ written.push(path);
47
+ }
48
+ return written;
49
+ }
50
+
51
+ // src/bin.ts
52
+ var [command, directory] = process.argv.slice(2);
53
+ if (command !== "init") {
54
+ console.error("Usage: sandblocks-sdk init [directory]");
55
+ process.exitCode = 1;
56
+ } else {
57
+ const files = await initSdk({ cwd: directory, force: process.argv.includes("--force") });
58
+ console.log(files.length ? `Created ${files.length} SDK files` : "SDK files already exist");
59
+ }