@agent-sandbox/cli 0.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.
Files changed (3) hide show
  1. package/README.md +89 -0
  2. package/dist/index.js +718 -0
  3. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @agent-sandbox/cli
2
+
3
+ Command-line interface for Agent Sandbox.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @agent-sandbox/cli
9
+ ```
10
+
11
+ Install the agent skill:
12
+
13
+ ```bash
14
+ npx skills add https://github.com/usamaasfar/agent-sandbox/tree/main/skills/agent-sandbox-cli
15
+ ```
16
+
17
+ ## Requirements
18
+
19
+ - Docker running locally
20
+
21
+ ## Quick Start
22
+
23
+ ```bash
24
+ agent-sandbox create
25
+ agent-sandbox write <sandboxId> "printf 'hello\n' > /proc/1/fd/1"
26
+ agent-sandbox read <sandboxId>
27
+ agent-sandbox delete <sandboxId>
28
+ ```
29
+
30
+ ## Command Reference
31
+
32
+ ### `create`
33
+
34
+ ```bash
35
+ agent-sandbox create [--name <name>] [--image <image>]
36
+ ```
37
+
38
+ Prints the `sandboxId`.
39
+
40
+ ### `delete`
41
+
42
+ ```bash
43
+ agent-sandbox delete <sandboxId>
44
+ ```
45
+
46
+ ### `list`
47
+
48
+ ```bash
49
+ agent-sandbox list
50
+ ```
51
+
52
+ Prints a JSON array of managed sandboxes.
53
+
54
+ ### `read`
55
+
56
+ ```bash
57
+ agent-sandbox read <sandboxId> [--tail <n>]
58
+ ```
59
+
60
+ Returns container logs. `--tail` limits to the last N lines.
61
+
62
+ ### `write`
63
+
64
+ ```bash
65
+ agent-sandbox write <sandboxId> <input> [--detach]
66
+ ```
67
+
68
+ Runs the command and returns its output and exit code. Use `--detach` to fire-and-forget
69
+ (returns immediately with no output, useful for starting background processes).
70
+
71
+ ### `upload`
72
+
73
+ ```bash
74
+ agent-sandbox upload <sandboxId> <localPath> <remotePath>
75
+ ```
76
+
77
+ Both paths must be absolute.
78
+
79
+ ### `download`
80
+
81
+ ```bash
82
+ agent-sandbox download <sandboxId> <remotePath> <localPath>
83
+ ```
84
+
85
+ `remotePath` may be a file or directory. Both paths must be absolute.
86
+
87
+ ## License
88
+
89
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,718 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // ../../packages/core/src/errors.ts
5
+ class SandboxError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "SandboxError";
9
+ }
10
+ }
11
+
12
+ class ContainerNotFoundError extends SandboxError {
13
+ constructor(sandboxId) {
14
+ super(`Container not found: ${sandboxId}`);
15
+ this.name = "ContainerNotFoundError";
16
+ }
17
+ }
18
+
19
+ class DockerError extends SandboxError {
20
+ status;
21
+ constructor(status, message) {
22
+ super(`Docker API error ${status}: ${message}`);
23
+ this.name = "DockerError";
24
+ this.status = status;
25
+ }
26
+ }
27
+
28
+ class ExecError extends SandboxError {
29
+ constructor(message) {
30
+ super(`Exec failed: ${message}`);
31
+ this.name = "ExecError";
32
+ }
33
+ }
34
+ // ../../packages/core/src/docker/client.ts
35
+ var DOCKER_HOST = "http://localhost";
36
+ var CLIENT_API_VERSION = "1.53";
37
+ var DOCKER_SOCKET_PATH = "/var/run/docker.sock";
38
+ var cachedApiVersion = null;
39
+ var resolvingApiVersion = null;
40
+ function isRawBody(body) {
41
+ return typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || body instanceof FormData || body instanceof URLSearchParams || body instanceof ReadableStream || ArrayBuffer.isView(body);
42
+ }
43
+ function parseApiVersion(version) {
44
+ const match = version.match(/^(\d+)\.(\d+)$/);
45
+ if (!match) {
46
+ return null;
47
+ }
48
+ return [Number(match[1]), Number(match[2])];
49
+ }
50
+ function selectApiVersion(serverVersion) {
51
+ const client = parseApiVersion(CLIENT_API_VERSION);
52
+ const server = parseApiVersion(serverVersion);
53
+ if (!client || !server) {
54
+ return CLIENT_API_VERSION;
55
+ }
56
+ if (server[0] < client[0]) {
57
+ return serverVersion;
58
+ }
59
+ if (server[0] > client[0]) {
60
+ return CLIENT_API_VERSION;
61
+ }
62
+ return server[1] < client[1] ? serverVersion : CLIENT_API_VERSION;
63
+ }
64
+ function buildUrl(path, query, apiVersion = CLIENT_API_VERSION) {
65
+ const url = new URL(`${DOCKER_HOST}/v${apiVersion}${path}`);
66
+ if (query) {
67
+ for (const [key, value] of Object.entries(query)) {
68
+ url.searchParams.set(key, value);
69
+ }
70
+ }
71
+ return url.toString();
72
+ }
73
+ async function dockerFetch(input, init) {
74
+ return fetch(input, {
75
+ ...init,
76
+ unix: DOCKER_SOCKET_PATH
77
+ });
78
+ }
79
+ async function readDockerErrorMessage(response) {
80
+ let text = "";
81
+ try {
82
+ text = await response.text();
83
+ } catch {
84
+ return response.statusText || "Request failed";
85
+ }
86
+ if (!text) {
87
+ return response.statusText || "Request failed";
88
+ }
89
+ try {
90
+ const parsed = JSON.parse(text);
91
+ if (typeof parsed.message === "string" && parsed.message.trim()) {
92
+ return parsed.message;
93
+ }
94
+ } catch {}
95
+ return text;
96
+ }
97
+ async function probeApiVersion() {
98
+ try {
99
+ const response = await dockerFetch(`${DOCKER_HOST}/version`, {
100
+ method: "GET"
101
+ });
102
+ if (!response.ok) {
103
+ return null;
104
+ }
105
+ const payload = await response.json();
106
+ if (typeof payload.ApiVersion !== "string" || !payload.ApiVersion.trim()) {
107
+ return null;
108
+ }
109
+ return selectApiVersion(payload.ApiVersion);
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ async function resolveApiVersion() {
115
+ if (cachedApiVersion) {
116
+ return cachedApiVersion;
117
+ }
118
+ if (!resolvingApiVersion) {
119
+ resolvingApiVersion = probeApiVersion().then((apiVersion) => {
120
+ if (apiVersion) {
121
+ cachedApiVersion = apiVersion;
122
+ return apiVersion;
123
+ }
124
+ return CLIENT_API_VERSION;
125
+ }).finally(() => {
126
+ resolvingApiVersion = null;
127
+ });
128
+ }
129
+ return resolvingApiVersion;
130
+ }
131
+ async function dockerRequest(options) {
132
+ const { method = "GET", path, query, body, headers = {}, allowStatus = [] } = options;
133
+ const requestHeaders = { ...headers };
134
+ let requestBody;
135
+ if (body !== undefined) {
136
+ if (isRawBody(body)) {
137
+ requestBody = body;
138
+ } else {
139
+ requestBody = JSON.stringify(body);
140
+ if (!Object.keys(requestHeaders).some((key) => key.toLowerCase() === "content-type")) {
141
+ requestHeaders["Content-Type"] = "application/json";
142
+ }
143
+ }
144
+ }
145
+ let response;
146
+ try {
147
+ const apiVersion = await resolveApiVersion();
148
+ response = await dockerFetch(buildUrl(path, query, apiVersion), {
149
+ method,
150
+ headers: requestHeaders,
151
+ body: requestBody
152
+ });
153
+ } catch (error) {
154
+ const message = error instanceof Error ? error.message : "Unknown Docker transport error";
155
+ throw new DockerError(0, message);
156
+ }
157
+ if (!response.ok && !allowStatus.includes(response.status)) {
158
+ throw new DockerError(response.status, await readDockerErrorMessage(response));
159
+ }
160
+ return response;
161
+ }
162
+ async function dockerJSON(options) {
163
+ const response = await dockerRequest(options);
164
+ return await response.json();
165
+ }
166
+
167
+ // ../../packages/core/src/primitives/create.ts
168
+ async function create(options = {}) {
169
+ const { name, image = "agent-sandbox" } = options;
170
+ const createResponse = await dockerJSON({
171
+ method: "POST",
172
+ path: "/containers/create",
173
+ query: name ? { name } : undefined,
174
+ body: {
175
+ Image: image,
176
+ Labels: {
177
+ "agent-sandbox": "true"
178
+ }
179
+ }
180
+ });
181
+ await dockerRequest({
182
+ method: "POST",
183
+ path: `/containers/${createResponse.Id}/start`
184
+ });
185
+ return {
186
+ sandboxId: createResponse.Id.slice(0, 12)
187
+ };
188
+ }
189
+ // ../../packages/core/src/primitives/delete.ts
190
+ function remapContainerNotFound(error, sandboxId) {
191
+ if (error instanceof DockerError && error.status === 404) {
192
+ throw new ContainerNotFoundError(sandboxId);
193
+ }
194
+ throw error;
195
+ }
196
+ async function deleteContainer(options) {
197
+ const { sandboxId } = options;
198
+ try {
199
+ await dockerRequest({
200
+ method: "POST",
201
+ path: `/containers/${sandboxId}/stop`,
202
+ allowStatus: [304]
203
+ });
204
+ } catch (error) {
205
+ remapContainerNotFound(error, sandboxId);
206
+ }
207
+ try {
208
+ await dockerRequest({
209
+ method: "DELETE",
210
+ path: `/containers/${sandboxId}`
211
+ });
212
+ } catch (error) {
213
+ remapContainerNotFound(error, sandboxId);
214
+ }
215
+ return {
216
+ ok: true
217
+ };
218
+ }
219
+ // ../../packages/core/src/primitives/download.ts
220
+ import path2 from "path";
221
+
222
+ // ../../packages/core/src/archive.ts
223
+ import { tmpdir } from "os";
224
+ import path from "path";
225
+ import { mkdir, mkdtemp, readdir, rename, rm, stat } from "fs/promises";
226
+ function ensureAbsoluteHostPath(targetPath) {
227
+ if (!path.isAbsolute(targetPath)) {
228
+ throw new SandboxError(`Host path must be absolute: ${targetPath}`);
229
+ }
230
+ }
231
+ async function ensurePathExists(targetPath) {
232
+ try {
233
+ await stat(targetPath);
234
+ } catch (error) {
235
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
236
+ throw new SandboxError(`Host path does not exist: ${targetPath}`);
237
+ }
238
+ if (error instanceof Error) {
239
+ throw new SandboxError(`Failed to access host path ${targetPath}: ${error.message}`);
240
+ }
241
+ throw error;
242
+ }
243
+ }
244
+ async function ensureTarSuccess(process2, action) {
245
+ const exitCode = await process2.exited;
246
+ if (exitCode === 0) {
247
+ return;
248
+ }
249
+ const stderr = process2.stderr;
250
+ const detail = stderr ? (await new Response(stderr).text()).trim() : "";
251
+ const suffix = detail ? `: ${detail}` : "";
252
+ throw new SandboxError(`${action} failed${suffix}`);
253
+ }
254
+ function spawnTar(command, options) {
255
+ try {
256
+ return Bun.spawn(command, options);
257
+ } catch (error) {
258
+ const message = error instanceof Error ? error.message : "Unknown tar error";
259
+ throw new SandboxError(`Failed to start tar: ${message}`);
260
+ }
261
+ }
262
+ async function createTarStream(localPath) {
263
+ ensureAbsoluteHostPath(localPath);
264
+ await ensurePathExists(localPath);
265
+ const directory = path.dirname(localPath);
266
+ const name = path.basename(localPath);
267
+ const process2 = spawnTar(["tar", "-C", directory, "-cf", "-", name], {
268
+ stdout: "pipe",
269
+ stderr: "pipe"
270
+ });
271
+ const stream = process2.stdout;
272
+ if (!stream) {
273
+ throw new SandboxError("Tar create failed: tar did not produce an archive stream");
274
+ }
275
+ return {
276
+ stream,
277
+ complete: () => ensureTarSuccess(process2, "Tar create"),
278
+ abort: () => {
279
+ try {
280
+ process2.kill();
281
+ } catch {}
282
+ }
283
+ };
284
+ }
285
+ async function extractTarStream(stream, localPath) {
286
+ ensureAbsoluteHostPath(localPath);
287
+ const targetDirectory = path.dirname(localPath);
288
+ await mkdir(targetDirectory, { recursive: true });
289
+ const extractDirectory = await mkdtemp(path.join(tmpdir(), "agent-sandbox-download-"));
290
+ try {
291
+ const process2 = spawnTar(["tar", "-C", extractDirectory, "-xf", "-"], {
292
+ stdin: stream,
293
+ stdout: "ignore",
294
+ stderr: "pipe"
295
+ });
296
+ await ensureTarSuccess(process2, "Tar extract");
297
+ const extractedEntries = await readdir(extractDirectory);
298
+ const [firstEntry] = extractedEntries;
299
+ if (extractedEntries.length !== 1 || !firstEntry) {
300
+ throw new SandboxError("Tar extract failed: archive did not contain exactly one top-level entry");
301
+ }
302
+ const extractedPath = path.join(extractDirectory, firstEntry);
303
+ const extractedStats = await stat(extractedPath);
304
+ if (!extractedStats.isFile() && !extractedStats.isDirectory()) {
305
+ throw new SandboxError("Tar extract failed: archive did not contain a regular file or directory");
306
+ }
307
+ await rm(localPath, { recursive: true, force: true });
308
+ await rename(extractedPath, localPath);
309
+ } finally {
310
+ await rm(extractDirectory, { recursive: true, force: true });
311
+ }
312
+ }
313
+
314
+ // ../../packages/core/src/primitives/download.ts
315
+ function ensureAbsoluteContainerPath(targetPath) {
316
+ if (!path2.posix.isAbsolute(targetPath)) {
317
+ throw new SandboxError(`Container path must be absolute: ${targetPath}`);
318
+ }
319
+ }
320
+ async function download(options) {
321
+ ensureAbsoluteContainerPath(options.remotePath);
322
+ if (!path2.isAbsolute(options.localPath)) {
323
+ throw new SandboxError(`Host path must be absolute: ${options.localPath}`);
324
+ }
325
+ let response;
326
+ try {
327
+ response = await dockerRequest({
328
+ path: `/containers/${options.sandboxId}/archive`,
329
+ query: {
330
+ path: options.remotePath
331
+ }
332
+ });
333
+ } catch (error) {
334
+ if (error instanceof DockerError && error.status === 404 && error.message.includes("No such container")) {
335
+ throw new ContainerNotFoundError(options.sandboxId);
336
+ }
337
+ throw error;
338
+ }
339
+ if (!response.body) {
340
+ throw new SandboxError("Docker archive response was empty");
341
+ }
342
+ await extractTarStream(response.body, options.localPath);
343
+ return {
344
+ ok: true
345
+ };
346
+ }
347
+ // ../../packages/core/src/primitives/list.ts
348
+ async function list() {
349
+ const containers = await dockerJSON({
350
+ path: "/containers/json",
351
+ query: {
352
+ filters: JSON.stringify({
353
+ label: ["agent-sandbox=true"]
354
+ })
355
+ }
356
+ });
357
+ const sandboxes = containers.map((container) => {
358
+ const name = (container.Names[0] ?? "").replace(/^\//, "");
359
+ return {
360
+ id: container.Id.slice(0, 12),
361
+ name,
362
+ status: container.State,
363
+ createdAt: new Date(container.Created * 1000).toISOString()
364
+ };
365
+ });
366
+ return { sandboxes };
367
+ }
368
+ // ../../packages/core/src/docker/parse-log.ts
369
+ function parseLogOutput(bytes) {
370
+ const chunks = [];
371
+ let totalLength = 0;
372
+ let offset = 0;
373
+ while (offset < bytes.length) {
374
+ if (bytes.length - offset < 8) {
375
+ throw new SandboxError("Malformed Docker log stream");
376
+ }
377
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset + 4, 4);
378
+ const payloadLength = view.getUint32(0);
379
+ const payloadStart = offset + 8;
380
+ const payloadEnd = payloadStart + payloadLength;
381
+ if (payloadEnd > bytes.length) {
382
+ throw new SandboxError("Malformed Docker log stream");
383
+ }
384
+ const chunk = bytes.slice(payloadStart, payloadEnd);
385
+ chunks.push(chunk);
386
+ totalLength += chunk.length;
387
+ offset = payloadEnd;
388
+ }
389
+ const output = new Uint8Array(totalLength);
390
+ let writeOffset = 0;
391
+ for (const chunk of chunks) {
392
+ output.set(chunk, writeOffset);
393
+ writeOffset += chunk.length;
394
+ }
395
+ return new TextDecoder().decode(output);
396
+ }
397
+
398
+ // ../../packages/core/src/primitives/read.ts
399
+ async function read(options) {
400
+ const query = {
401
+ stdout: "1",
402
+ stderr: "1"
403
+ };
404
+ if (options.tail !== undefined) {
405
+ query.tail = String(options.tail);
406
+ }
407
+ let response;
408
+ try {
409
+ response = await dockerRequest({
410
+ path: `/containers/${options.sandboxId}/logs`,
411
+ query
412
+ });
413
+ } catch (error) {
414
+ if (error instanceof DockerError && error.status === 404) {
415
+ throw new ContainerNotFoundError(options.sandboxId);
416
+ }
417
+ throw error;
418
+ }
419
+ const bytes = new Uint8Array(await response.arrayBuffer());
420
+ return {
421
+ output: parseLogOutput(bytes)
422
+ };
423
+ }
424
+ // ../../packages/core/src/primitives/upload.ts
425
+ import path3 from "path";
426
+ function ensureAbsoluteContainerPath2(targetPath) {
427
+ if (!path3.posix.isAbsolute(targetPath)) {
428
+ throw new SandboxError(`Container path must be absolute: ${targetPath}`);
429
+ }
430
+ }
431
+ function remapUploadError(error, sandboxId) {
432
+ if (error instanceof DockerError && error.status === 404) {
433
+ throw new ContainerNotFoundError(sandboxId);
434
+ }
435
+ throw error;
436
+ }
437
+ async function upload(options) {
438
+ ensureAbsoluteContainerPath2(options.remotePath);
439
+ const archive = await createTarStream(options.localPath);
440
+ try {
441
+ await dockerRequest({
442
+ method: "PUT",
443
+ path: `/containers/${options.sandboxId}/archive`,
444
+ query: {
445
+ path: path3.posix.dirname(options.remotePath)
446
+ },
447
+ body: archive.stream,
448
+ headers: {
449
+ "Content-Type": "application/x-tar"
450
+ }
451
+ });
452
+ } catch (error) {
453
+ archive.abort();
454
+ remapUploadError(error, options.sandboxId);
455
+ }
456
+ await archive.complete();
457
+ return {
458
+ ok: true
459
+ };
460
+ }
461
+ // ../../packages/core/src/primitives/write.ts
462
+ function getDockerMessage(error) {
463
+ return error.message.replace(/^Docker API error \d+: /, "");
464
+ }
465
+ async function write(options) {
466
+ const detach = options.detach ?? false;
467
+ let execId;
468
+ try {
469
+ const response = await dockerJSON({
470
+ method: "POST",
471
+ path: `/containers/${options.sandboxId}/exec`,
472
+ body: {
473
+ Cmd: ["/bin/sh", "-c", options.input],
474
+ AttachStdout: !detach,
475
+ AttachStderr: !detach
476
+ }
477
+ });
478
+ execId = response.Id;
479
+ } catch (error) {
480
+ if (error instanceof DockerError && error.status === 404) {
481
+ throw new ContainerNotFoundError(options.sandboxId);
482
+ }
483
+ throw error;
484
+ }
485
+ if (detach) {
486
+ try {
487
+ await dockerRequest({
488
+ method: "POST",
489
+ path: `/exec/${execId}/start`,
490
+ body: {
491
+ Detach: true
492
+ }
493
+ });
494
+ } catch (error) {
495
+ if (error instanceof DockerError) {
496
+ throw new ExecError(getDockerMessage(error));
497
+ }
498
+ throw error;
499
+ }
500
+ return { ok: true };
501
+ }
502
+ let startResponse;
503
+ try {
504
+ startResponse = await dockerRequest({
505
+ method: "POST",
506
+ path: `/exec/${execId}/start`,
507
+ body: {
508
+ Detach: false,
509
+ Tty: false
510
+ }
511
+ });
512
+ } catch (error) {
513
+ if (error instanceof DockerError) {
514
+ throw new ExecError(getDockerMessage(error));
515
+ }
516
+ throw error;
517
+ }
518
+ const bytes = new Uint8Array(await startResponse.arrayBuffer());
519
+ const output = parseLogOutput(bytes);
520
+ const inspect = await dockerJSON({
521
+ method: "GET",
522
+ path: `/exec/${execId}/json`
523
+ });
524
+ return {
525
+ ok: true,
526
+ output,
527
+ exitCode: inspect.ExitCode
528
+ };
529
+ }
530
+ // src/commands/utils.ts
531
+ function requireArg(value, name) {
532
+ if (value === undefined) {
533
+ throw new Error(`Missing required argument: ${name}`);
534
+ }
535
+ return value;
536
+ }
537
+ function readOption(args, index, name) {
538
+ const value = args[index + 1];
539
+ if (value === undefined) {
540
+ throw new Error(`Missing value for option: ${name}`);
541
+ }
542
+ return { value, nextIndex: index + 2 };
543
+ }
544
+
545
+ // src/commands/create.ts
546
+ async function runCreateCommand(args) {
547
+ let name;
548
+ let image;
549
+ for (let i = 0;i < args.length; ) {
550
+ const arg = args[i];
551
+ switch (arg) {
552
+ case "--name": {
553
+ const option = readOption(args, i, "--name");
554
+ name = option.value;
555
+ i = option.nextIndex;
556
+ break;
557
+ }
558
+ case "--image": {
559
+ const option = readOption(args, i, "--image");
560
+ image = option.value;
561
+ i = option.nextIndex;
562
+ break;
563
+ }
564
+ default:
565
+ throw new Error(`Unknown argument: ${arg}`);
566
+ }
567
+ }
568
+ const options = { name, image };
569
+ const result = await create(options);
570
+ console.log(result.sandboxId);
571
+ }
572
+
573
+ // src/commands/delete.ts
574
+ async function runDeleteCommand(args) {
575
+ const options = {
576
+ sandboxId: requireArg(args[0], "sandboxId")
577
+ };
578
+ const result = await deleteContainer(options);
579
+ if (!result.ok) {
580
+ throw new Error("Failed to delete sandbox");
581
+ }
582
+ }
583
+
584
+ // src/commands/download.ts
585
+ async function runDownloadCommand(args) {
586
+ const options = {
587
+ sandboxId: requireArg(args[0], "sandboxId"),
588
+ remotePath: requireArg(args[1], "remotePath"),
589
+ localPath: requireArg(args[2], "localPath")
590
+ };
591
+ const result = await download(options);
592
+ if (!result.ok) {
593
+ throw new Error("Failed to download from sandbox");
594
+ }
595
+ }
596
+
597
+ // src/commands/list.ts
598
+ async function runListCommand() {
599
+ const result = await list();
600
+ console.log(JSON.stringify(result.sandboxes, null, 2));
601
+ }
602
+
603
+ // src/commands/read.ts
604
+ async function runReadCommand(args) {
605
+ const sandboxId = requireArg(args[0], "sandboxId");
606
+ let tail;
607
+ for (let i = 1;i < args.length; ) {
608
+ const arg = args[i];
609
+ switch (arg) {
610
+ case "--tail": {
611
+ const option = readOption(args, i, "--tail");
612
+ const parsedTail = Number(option.value);
613
+ if (!Number.isFinite(parsedTail) || !Number.isInteger(parsedTail) || parsedTail <= 0) {
614
+ throw new Error(`Invalid value for --tail: expected a positive integer, got "${option.value}"`);
615
+ }
616
+ tail = parsedTail;
617
+ i = option.nextIndex;
618
+ break;
619
+ }
620
+ default:
621
+ throw new Error(`Unknown argument: ${arg}`);
622
+ }
623
+ }
624
+ const options = { sandboxId, tail };
625
+ const result = await read(options);
626
+ process.stdout.write(result.output);
627
+ }
628
+
629
+ // src/commands/upload.ts
630
+ async function runUploadCommand(args) {
631
+ const options = {
632
+ sandboxId: requireArg(args[0], "sandboxId"),
633
+ localPath: requireArg(args[1], "localPath"),
634
+ remotePath: requireArg(args[2], "remotePath")
635
+ };
636
+ const result = await upload(options);
637
+ if (!result.ok) {
638
+ throw new Error("Failed to upload to sandbox");
639
+ }
640
+ }
641
+
642
+ // src/commands/write.ts
643
+ async function runWriteCommand(args) {
644
+ let detach = false;
645
+ const positional = [];
646
+ for (const arg of args) {
647
+ if (arg === "--detach") {
648
+ detach = true;
649
+ } else {
650
+ positional.push(arg);
651
+ }
652
+ }
653
+ const options = {
654
+ sandboxId: requireArg(positional[0], "sandboxId"),
655
+ input: requireArg(positional[1], "input"),
656
+ detach
657
+ };
658
+ const result = await write(options);
659
+ if (!result.ok) {
660
+ throw new Error("Failed to write to sandbox");
661
+ }
662
+ if (result.output !== undefined) {
663
+ process.stdout.write(result.output);
664
+ }
665
+ if (typeof result.exitCode === "number") {
666
+ process.exitCode = result.exitCode;
667
+ if (result.exitCode !== 0) {
668
+ process.stderr.write(`write command exited with code ${result.exitCode}
669
+ `);
670
+ }
671
+ }
672
+ }
673
+
674
+ // index.ts
675
+ var USAGE = "Usage: agent-sandbox <create|delete|list|read|write|upload|download> ...";
676
+ function printHelp() {
677
+ console.log(USAGE);
678
+ }
679
+ async function main() {
680
+ const [command, ...rest] = process.argv.slice(2);
681
+ if (command === undefined || command === "--help" || command === "-h" || command === "help") {
682
+ printHelp();
683
+ return;
684
+ }
685
+ switch (command) {
686
+ case "create": {
687
+ await runCreateCommand(rest);
688
+ return;
689
+ }
690
+ case "delete": {
691
+ await runDeleteCommand(rest);
692
+ return;
693
+ }
694
+ case "list": {
695
+ await runListCommand();
696
+ return;
697
+ }
698
+ case "read": {
699
+ await runReadCommand(rest);
700
+ return;
701
+ }
702
+ case "write": {
703
+ await runWriteCommand(rest);
704
+ return;
705
+ }
706
+ case "upload": {
707
+ await runUploadCommand(rest);
708
+ return;
709
+ }
710
+ case "download": {
711
+ await runDownloadCommand(rest);
712
+ return;
713
+ }
714
+ default:
715
+ throw new Error(USAGE);
716
+ }
717
+ }
718
+ await main();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@agent-sandbox/cli",
3
+ "version": "0.1.1",
4
+ "description": "CLI to spin up isolated terminal sandboxes for AI agents",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "agent-sandbox": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "dev": "bun run index.ts",
15
+ "build": "bun build ./index.ts --outfile dist/index.js --target bun",
16
+ "build:binary": "bun build ./index.ts --compile --minify --bytecode --outfile dist/agent-sandbox"
17
+ },
18
+ "devDependencies": {
19
+ "@agent-sandbox/core": "*",
20
+ "@types/bun": "^1.3.9"
21
+ },
22
+ "engines": {
23
+ "bun": ">=1.0.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/usamaasfar/agent-sandbox.git",
28
+ "directory": "apps/cli"
29
+ },
30
+ "homepage": "https://github.com/usamaasfar/agent-sandbox#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/usamaasfar/agent-sandbox/issues"
33
+ },
34
+ "keywords": [
35
+ "sandbox",
36
+ "agent",
37
+ "ai",
38
+ "terminal",
39
+ "docker",
40
+ "cli"
41
+ ]
42
+ }