@upstash/box 0.0.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.
package/dist/client.js ADDED
@@ -0,0 +1,963 @@
1
+ import { readFile as fsReadFile, writeFile as fsWriteFile, mkdir } from "node:fs/promises";
2
+ import { randomUUID, createHmac } from "node:crypto";
3
+ import { basename, join } from "node:path";
4
+ /**
5
+ * Error thrown by the Box SDK
6
+ */
7
+ export class BoxError extends Error {
8
+ statusCode;
9
+ constructor(message, statusCode) {
10
+ super(message);
11
+ this.statusCode = statusCode;
12
+ this.name = "BoxError";
13
+ }
14
+ }
15
+ /**
16
+ * A run represents a single agent or shell execution.
17
+ * Returned by box.agent.run() and box.exec().
18
+ */
19
+ export class Run {
20
+ type;
21
+ /** @internal */
22
+ _id;
23
+ /** @internal */
24
+ _result = null;
25
+ /** @internal */
26
+ _status = "running";
27
+ /** @internal */
28
+ _inputTokens = 0;
29
+ /** @internal */
30
+ _outputTokens = 0;
31
+ /** @internal */
32
+ _computeMs = 0;
33
+ /** @internal */
34
+ _box;
35
+ /** @internal */
36
+ _abortController;
37
+ /** @internal */
38
+ _startTime;
39
+ /** The run ID. Initially a local UUID, replaced by backend run_id from run_start event. */
40
+ get id() {
41
+ return this._id;
42
+ }
43
+ /** @internal */
44
+ constructor(box, type, id) {
45
+ this._id = id ?? randomUUID();
46
+ this.type = type;
47
+ this._box = box;
48
+ this._startTime = Date.now();
49
+ }
50
+ /**
51
+ * Get the current run status. Polls the backend for the latest status if the run may still be active.
52
+ */
53
+ async status() {
54
+ if (["completed", "failed", "cancelled"].includes(this._status)) {
55
+ return this._status;
56
+ }
57
+ try {
58
+ const data = await this._box._request("GET", `/v2/box/${this._box.id}/runs/${this._id}`);
59
+ this._status = data.status;
60
+ }
61
+ catch {
62
+ // Fallback to local status if backend call fails
63
+ }
64
+ return this._status;
65
+ }
66
+ /**
67
+ * Get the run result. Returns the typed output when responseSchema was provided.
68
+ */
69
+ async result() {
70
+ if (this._result === null) {
71
+ return "";
72
+ }
73
+ return this._result;
74
+ }
75
+ /**
76
+ * Get token usage and cost information. Fetches from backend when available.
77
+ */
78
+ async cost() {
79
+ try {
80
+ const data = await this._box._request("GET", `/v2/box/${this._box.id}/runs/${this._id}`);
81
+ return {
82
+ tokens: (data.input_tokens ?? 0) + (data.output_tokens ?? 0),
83
+ computeMs: data.duration_ms ?? 0,
84
+ totalUsd: data.cost_usd ?? 0,
85
+ };
86
+ }
87
+ catch {
88
+ return {
89
+ tokens: this._inputTokens + this._outputTokens,
90
+ computeMs: this._computeMs || Date.now() - this._startTime,
91
+ totalUsd: 0,
92
+ };
93
+ }
94
+ }
95
+ /**
96
+ * Cancel a running execution.
97
+ */
98
+ async cancel() {
99
+ this._abortController?.abort();
100
+ await this._box
101
+ ._request("POST", `/v2/box/${this._box.id}/runs/${this._id}/cancel`)
102
+ .catch(() => { });
103
+ this._status = "cancelled";
104
+ }
105
+ /**
106
+ * Retrieve logs for this run.
107
+ */
108
+ async logs() {
109
+ const allLogs = await this._box.logs();
110
+ // Filter logs around this run's time window
111
+ const startSec = Math.floor(this._startTime / 1000);
112
+ return allLogs
113
+ .filter((l) => l.timestamp >= startSec)
114
+ .map((l) => ({
115
+ timestamp: new Date(l.timestamp * 1000).toISOString(),
116
+ level: l.level,
117
+ message: l.message,
118
+ }));
119
+ }
120
+ }
121
+ /**
122
+ * A sandboxed AI coding environment.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { Box, Runtime, ClaudeCode } from "@upstash/box";
127
+ *
128
+ * const box = await Box.create({
129
+ * runtime: Runtime.Node,
130
+ * agent: { model: ClaudeCode.Sonnet_4_5, apiKey: process.env.CLAUDE_KEY! },
131
+ * });
132
+ *
133
+ * // Non-streaming
134
+ * const run = await box.agent.run({ prompt: "Fix the bug in auth.ts" });
135
+ * console.log(await run.result());
136
+ *
137
+ * // Streaming
138
+ * for await (const chunk of box.agent.stream({ prompt: "Add tests" })) {
139
+ * process.stdout.write(chunk);
140
+ * }
141
+ *
142
+ * await box.delete();
143
+ * ```
144
+ */
145
+ export class Box {
146
+ id;
147
+ /** Agent operations namespace */
148
+ agent;
149
+ /** File operations namespace */
150
+ files;
151
+ /** Git operations namespace */
152
+ git;
153
+ _baseUrl;
154
+ _headers;
155
+ _timeout;
156
+ _debug;
157
+ _gitToken;
158
+ _isAgentConfigured;
159
+ constructor(data, config) {
160
+ this.id = data.id;
161
+ this._baseUrl = config.baseUrl;
162
+ this._headers = config.headers;
163
+ this._timeout = config.timeout;
164
+ this._debug = config.debug;
165
+ this._gitToken = config.gitToken;
166
+ this._isAgentConfigured = config.isAgentConfigured ?? false;
167
+ const self = this;
168
+ this.agent = {
169
+ run(options) {
170
+ if (!self._isAgentConfigured) {
171
+ throw new BoxError('No agent configured. Pass an `agent` option to Box.create() to use box.agent.run().\n\nExample:\n await Box.create({ agent: { model: ClaudeCode.Sonnet_4_5, apiKey: "sk-..." } })');
172
+ }
173
+ return self._run(options);
174
+ },
175
+ stream(options) {
176
+ if (!self._isAgentConfigured) {
177
+ throw new BoxError('No agent configured. Pass an `agent` option to Box.create() to use box.agent.stream().\n\nExample:\n await Box.create({ agent: { model: ClaudeCode.Sonnet_4_5, apiKey: "sk-..." } })');
178
+ }
179
+ return self._stream(options);
180
+ },
181
+ };
182
+ this.files = {
183
+ read: (path) => this._readFile(path),
184
+ write: (opts) => this._writeFile(opts.path, opts.content),
185
+ list: (path) => this._listFiles(path),
186
+ upload: (files) => this._uploadFiles(files),
187
+ download: (opts) => this._downloadFiles(opts?.path),
188
+ };
189
+ this.git = {
190
+ clone: (options) => this._gitClone(options),
191
+ diff: () => this._gitDiff(),
192
+ status: () => this._gitStatus(),
193
+ commit: (options) => this._gitCommit(options),
194
+ push: (options) => this._gitPush(options),
195
+ createPR: (options) => this._gitCreatePR(options),
196
+ };
197
+ }
198
+ /**
199
+ * Create a new sandboxed box.
200
+ */
201
+ static async create(config) {
202
+ const apiKey = config.apiKey ?? process.env.UPSTASH_BOX_API_KEY;
203
+ if (!apiKey) {
204
+ throw new BoxError("apiKey is required. Pass it in config or set UPSTASH_BOX_API_KEY env var.");
205
+ }
206
+ if (config.agent && !config.agent.model) {
207
+ throw new BoxError("agent.model is required when agent is configured");
208
+ }
209
+ const baseUrl = (config.baseUrl ??
210
+ process.env.UPSTASH_BOX_BASE_URL ??
211
+ "https://box.api.upstashdev.com").replace(/\/$/, "");
212
+ const headers = {
213
+ "X-Box-Api-Key": apiKey,
214
+ };
215
+ const timeout = config.timeout ?? 600000;
216
+ const debug = config.debug ?? false;
217
+ const body = {};
218
+ if (config.agent) {
219
+ body.model = config.agent.model;
220
+ body.agent_api_key = config.agent.apiKey;
221
+ }
222
+ if (config.runtime)
223
+ body.runtime = config.runtime;
224
+ if (config.git?.token)
225
+ body.github_token = config.git.token;
226
+ if (config.env)
227
+ body.env_vars = config.env;
228
+ if (config.skills?.length)
229
+ body.skills = config.skills;
230
+ if (config.mcpServers?.length) {
231
+ body.mcp_servers = config.mcpServers.map((s) => ({
232
+ name: s.name,
233
+ source: s.source,
234
+ package_or_url: s.packageOrUrl,
235
+ headers: s.headers,
236
+ }));
237
+ }
238
+ const response = await fetch(`${baseUrl}/v2/box`, {
239
+ method: "POST",
240
+ headers: { ...headers, "Content-Type": "application/json" },
241
+ body: JSON.stringify(body),
242
+ });
243
+ if (!response.ok) {
244
+ const msg = await parseErrorResponse(response);
245
+ throw new BoxError(msg, response.status);
246
+ }
247
+ let data = (await response.json());
248
+ // Poll until ready
249
+ const pollInterval = 2000;
250
+ const maxWait = 300000;
251
+ const start = Date.now();
252
+ while (data.status === "creating" && Date.now() - start < maxWait) {
253
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
254
+ const pollResponse = await fetch(`${baseUrl}/v2/box/${data.id}`, { headers });
255
+ if (pollResponse.ok) {
256
+ data = (await pollResponse.json());
257
+ }
258
+ }
259
+ if (data.status === "creating") {
260
+ throw new BoxError("Box creation timed out");
261
+ }
262
+ if (data.status === "error") {
263
+ throw new BoxError("Box creation failed");
264
+ }
265
+ return new Box(data, {
266
+ baseUrl,
267
+ headers,
268
+ timeout,
269
+ debug,
270
+ gitToken: config.git?.token,
271
+ isAgentConfigured: Boolean(config.agent),
272
+ });
273
+ }
274
+ /**
275
+ * List all boxes for the authenticated user.
276
+ */
277
+ static async list(options) {
278
+ const apiKey = options?.apiKey ?? process.env.UPSTASH_BOX_API_KEY;
279
+ if (!apiKey) {
280
+ throw new BoxError("apiKey is required. Pass it in options or set UPSTASH_BOX_API_KEY env var.");
281
+ }
282
+ const baseUrl = (options?.baseUrl ??
283
+ process.env.UPSTASH_BOX_BASE_URL ??
284
+ "https://box.api.upstashdev.com").replace(/\/$/, "");
285
+ const headers = { "X-Box-Api-Key": apiKey };
286
+ const response = await fetch(`${baseUrl}/v2/box`, { headers });
287
+ if (!response.ok) {
288
+ const msg = await parseErrorResponse(response);
289
+ throw new BoxError(msg, response.status);
290
+ }
291
+ return (await response.json());
292
+ }
293
+ /**
294
+ * Get an existing box by ID.
295
+ */
296
+ static async get(boxId, options) {
297
+ const apiKey = options?.apiKey ?? process.env.UPSTASH_BOX_API_KEY;
298
+ if (!apiKey) {
299
+ throw new BoxError("apiKey is required. Pass it in options or set UPSTASH_BOX_API_KEY env var.");
300
+ }
301
+ const baseUrl = (options?.baseUrl ?? process.env.UPSTASH_BOX_BASE_URL ?? "https://box.api.upstashdev.com").replace(/\/$/, "");
302
+ const headers = { "X-Box-Api-Key": apiKey };
303
+ const timeout = options?.timeout ?? 600000;
304
+ const debug = options?.debug ?? false;
305
+ const response = await fetch(`${baseUrl}/v2/box/${boxId}`, { headers });
306
+ if (!response.ok) {
307
+ const msg = await parseErrorResponse(response);
308
+ throw new BoxError(msg, response.status);
309
+ }
310
+ const data = (await response.json());
311
+ return new Box(data, { baseUrl, headers, timeout, debug, gitToken: options?.gitToken, isAgentConfigured: Boolean(data.model) });
312
+ }
313
+ // ==================== Run ====================
314
+ /** @internal */
315
+ async _run(options) {
316
+ if (!options.prompt)
317
+ throw new BoxError("prompt is required");
318
+ // Webhook mode: fire-and-forget — run in background, POST result to webhook URL
319
+ if (options.webhook) {
320
+ const run = new Run(this, "agent");
321
+ const webhook = options.webhook;
322
+ const boxId = this.id;
323
+ // Run in background, don't await
324
+ this._runWithRetries(options).then(async (completedRun) => {
325
+ const cost = await completedRun.cost();
326
+ const payload = {
327
+ runId: completedRun.id,
328
+ boxId,
329
+ status: "completed",
330
+ result: completedRun._result,
331
+ cost,
332
+ completedAt: new Date().toISOString(),
333
+ };
334
+ await sendWebhook(webhook, payload);
335
+ }, async (err) => {
336
+ const payload = {
337
+ runId: run.id,
338
+ boxId,
339
+ status: "failed",
340
+ result: null,
341
+ cost: { tokens: 0, computeMs: 0, totalUsd: 0 },
342
+ completedAt: new Date().toISOString(),
343
+ error: err instanceof Error ? err.message : String(err),
344
+ };
345
+ await sendWebhook(webhook, payload);
346
+ });
347
+ return run;
348
+ }
349
+ return this._runWithRetries(options);
350
+ }
351
+ async _runWithRetries(options) {
352
+ const maxRetries = options.maxRetries ?? 0;
353
+ let lastError;
354
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
355
+ try {
356
+ return await this._executeRun(options, attempt);
357
+ }
358
+ catch (e) {
359
+ lastError = e instanceof Error ? e : new Error(String(e));
360
+ if (attempt < maxRetries) {
361
+ const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
362
+ await new Promise((resolve) => setTimeout(resolve, delay));
363
+ }
364
+ }
365
+ }
366
+ throw lastError;
367
+ }
368
+ async _executeRun(options, _attempt) {
369
+ // Build prompt with schema instructions if needed
370
+ let prompt = options.prompt;
371
+ if (options.responseSchema) {
372
+ const shape = extractSchemaShape(options.responseSchema);
373
+ prompt += `\n\nRespond with ONLY a valid JSON object. No markdown, no code blocks, no explanation — just raw JSON. Use proper JSON types: numbers must be numbers (not strings), arrays must be arrays (not strings).`;
374
+ if (shape) {
375
+ prompt += `\n\nThe JSON must use these exact field names and types:\n${shape}`;
376
+ }
377
+ }
378
+ const run = new Run(this, "agent");
379
+ const abortController = new AbortController();
380
+ run._abortController = abortController;
381
+ if (options.timeout) {
382
+ setTimeout(() => abortController.abort(), options.timeout);
383
+ }
384
+ const url = `${this._baseUrl}/v2/box/${this.id}/run/stream`;
385
+ const response = await fetch(url, {
386
+ method: "POST",
387
+ headers: { ...this._headers, "Content-Type": "application/json" },
388
+ body: JSON.stringify({ prompt }),
389
+ signal: abortController.signal,
390
+ });
391
+ if (!response.ok) {
392
+ const msg = await parseErrorResponse(response);
393
+ throw new BoxError(msg, response.status);
394
+ }
395
+ const reader = response.body?.getReader();
396
+ if (!reader)
397
+ throw new BoxError("Streaming not supported");
398
+ const decoder = new TextDecoder();
399
+ let buffer = "";
400
+ let eventType = "";
401
+ let eventData = "";
402
+ let rawOutput = "";
403
+ const processEvent = (type, data) => {
404
+ try {
405
+ const parsed = JSON.parse(data);
406
+ switch (type) {
407
+ case "run_start": {
408
+ if (parsed.run_id)
409
+ run._id = parsed.run_id;
410
+ break;
411
+ }
412
+ case "text": {
413
+ const text = parsed.text ?? "";
414
+ if (text) {
415
+ rawOutput += text;
416
+ options.onStream?.(text);
417
+ }
418
+ break;
419
+ }
420
+ case "tool": {
421
+ options.onToolUse?.({ name: parsed.name, input: parsed.input });
422
+ break;
423
+ }
424
+ case "done": {
425
+ run._inputTokens = parsed.input_tokens ?? 0;
426
+ run._outputTokens = parsed.output_tokens ?? 0;
427
+ if (parsed.output)
428
+ rawOutput = parsed.output;
429
+ break;
430
+ }
431
+ case "error":
432
+ throw new BoxError(parsed.error ?? "Stream error");
433
+ }
434
+ }
435
+ catch (e) {
436
+ if (e instanceof BoxError)
437
+ throw e;
438
+ }
439
+ };
440
+ try {
441
+ while (true) {
442
+ const { done, value } = await reader.read();
443
+ if (done)
444
+ break;
445
+ let chunk = decoder.decode(value, { stream: true });
446
+ chunk = chunk.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
447
+ buffer += chunk;
448
+ const lines = buffer.split("\n");
449
+ buffer = lines.pop() || "";
450
+ for (let line of lines) {
451
+ line = line.replace(/\r$/, "").replace(/^[\\\|\/\-\s]*/, "");
452
+ if (line.startsWith("event: ")) {
453
+ eventType = line.slice(7).trim();
454
+ }
455
+ else if (line.startsWith("data: ")) {
456
+ eventData = line.slice(6);
457
+ }
458
+ else if ((line === "" || line.trim() === "") && eventType && eventData) {
459
+ processEvent(eventType, eventData);
460
+ eventType = "";
461
+ eventData = "";
462
+ }
463
+ }
464
+ if (eventType && eventData && (buffer === "" || buffer.trim() === "")) {
465
+ processEvent(eventType, eventData);
466
+ eventType = "";
467
+ eventData = "";
468
+ }
469
+ }
470
+ // Process remaining buffer
471
+ if (eventType && eventData) {
472
+ processEvent(eventType, eventData);
473
+ }
474
+ }
475
+ catch (e) {
476
+ if (e instanceof Error && e.name === "AbortError") {
477
+ run._status = "cancelled";
478
+ run._computeMs = Date.now() - run._startTime;
479
+ throw new BoxError("Run timed out");
480
+ }
481
+ throw e;
482
+ }
483
+ // Parse structured output if schema provided
484
+ let output = rawOutput.trim();
485
+ if (options.responseSchema) {
486
+ let jsonStr = output.trim();
487
+ const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
488
+ if (jsonMatch?.[1])
489
+ jsonStr = jsonMatch[1].trim();
490
+ try {
491
+ const parsed = JSON.parse(jsonStr);
492
+ output = options.responseSchema.parse(parsed);
493
+ }
494
+ catch (e) {
495
+ throw new BoxError(`Failed to parse structured output: ${e instanceof Error ? e.message : e}\n\nRaw output: ${output.slice(0, 500)}`);
496
+ }
497
+ }
498
+ run._result = output;
499
+ run._status = "completed";
500
+ run._computeMs = Date.now() - run._startTime;
501
+ return run;
502
+ }
503
+ /** @internal */
504
+ async *_stream(options) {
505
+ if (!options.prompt)
506
+ throw new BoxError("prompt is required");
507
+ const abortController = new AbortController();
508
+ if (options.timeout) {
509
+ setTimeout(() => abortController.abort(), options.timeout);
510
+ }
511
+ const url = `${this._baseUrl}/v2/box/${this.id}/run/stream`;
512
+ const response = await fetch(url, {
513
+ method: "POST",
514
+ headers: { ...this._headers, "Content-Type": "application/json" },
515
+ body: JSON.stringify({ prompt: options.prompt }),
516
+ signal: abortController.signal,
517
+ });
518
+ if (!response.ok) {
519
+ const msg = await parseErrorResponse(response);
520
+ throw new BoxError(msg, response.status);
521
+ }
522
+ const reader = response.body?.getReader();
523
+ if (!reader)
524
+ throw new BoxError("Streaming not supported");
525
+ const decoder = new TextDecoder();
526
+ let buffer = "";
527
+ let eventType = "";
528
+ let eventData = "";
529
+ try {
530
+ while (true) {
531
+ const { done, value } = await reader.read();
532
+ if (done)
533
+ break;
534
+ let chunk = decoder.decode(value, { stream: true });
535
+ chunk = chunk.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
536
+ buffer += chunk;
537
+ const lines = buffer.split("\n");
538
+ buffer = lines.pop() || "";
539
+ for (let line of lines) {
540
+ line = line.replace(/\r$/, "").replace(/^[\\\|\/\-\s]*/, "");
541
+ if (line.startsWith("event: ")) {
542
+ eventType = line.slice(7).trim();
543
+ }
544
+ else if (line.startsWith("data: ")) {
545
+ eventData = line.slice(6);
546
+ }
547
+ else if ((line === "" || line.trim() === "") && eventType && eventData) {
548
+ const text = this._processStreamEvent(eventType, eventData, options);
549
+ if (text !== null)
550
+ yield text;
551
+ eventType = "";
552
+ eventData = "";
553
+ }
554
+ }
555
+ if (eventType && eventData && (buffer === "" || buffer.trim() === "")) {
556
+ const text = this._processStreamEvent(eventType, eventData, options);
557
+ if (text !== null)
558
+ yield text;
559
+ eventType = "";
560
+ eventData = "";
561
+ }
562
+ }
563
+ if (eventType && eventData) {
564
+ const text = this._processStreamEvent(eventType, eventData, options);
565
+ if (text !== null)
566
+ yield text;
567
+ }
568
+ }
569
+ catch (e) {
570
+ if (e instanceof Error && e.name === "AbortError") {
571
+ throw new BoxError("Stream timed out");
572
+ }
573
+ throw e;
574
+ }
575
+ }
576
+ _processStreamEvent(type, data, options) {
577
+ try {
578
+ const parsed = JSON.parse(data);
579
+ switch (type) {
580
+ case "text": {
581
+ const text = parsed.text ?? "";
582
+ return text || null;
583
+ }
584
+ case "tool": {
585
+ options.onToolUse?.({ name: parsed.name, input: parsed.input });
586
+ return null;
587
+ }
588
+ case "error":
589
+ throw new BoxError(parsed.error ?? "Stream error");
590
+ default:
591
+ return null;
592
+ }
593
+ }
594
+ catch (e) {
595
+ if (e instanceof BoxError)
596
+ throw e;
597
+ return null;
598
+ }
599
+ }
600
+ // ==================== Shell ====================
601
+ /**
602
+ * Execute an OS-level command in the box.
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * const run = await box.exec("node /work/index.js");
607
+ * console.log(await run.result());
608
+ * console.log(await run.status()); // "completed"
609
+ * ```
610
+ */
611
+ async exec(command) {
612
+ const start = Date.now();
613
+ const result = await this._request("POST", `/v2/box/${this.id}/exec`, {
614
+ body: { command: ["sh", "-c", command] },
615
+ });
616
+ const run = new Run(this, "shell");
617
+ run._result = result.output;
618
+ run._status = result.exit_code === 0 ? "completed" : "failed";
619
+ run._computeMs = Date.now() - start;
620
+ return run;
621
+ }
622
+ // ==================== File Operations ====================
623
+ static WORKSPACE = "/workspace/home";
624
+ _resolvePath(p) {
625
+ if (p.startsWith("/"))
626
+ return p;
627
+ return `${Box.WORKSPACE}/${p}`;
628
+ }
629
+ async _readFile(path) {
630
+ const resolved = this._resolvePath(path);
631
+ const data = await this._request("GET", `/v2/box/${this.id}/files/read?path=${encodeURIComponent(resolved)}`);
632
+ return data.content;
633
+ }
634
+ async _writeFile(path, content) {
635
+ const resolved = this._resolvePath(path);
636
+ await this._request("POST", `/v2/box/${this.id}/files/write`, {
637
+ body: { path: resolved, content },
638
+ });
639
+ }
640
+ async _listFiles(path) {
641
+ const resolved = path ? this._resolvePath(path) : "";
642
+ const p = resolved ? `?path=${encodeURIComponent(resolved)}` : "";
643
+ const data = await this._request("GET", `/v2/box/${this.id}/files/list${p}`);
644
+ return data.files;
645
+ }
646
+ async _uploadFiles(files) {
647
+ for (const file of files) {
648
+ const content = await fsReadFile(file.path);
649
+ const base64 = content.toString("base64");
650
+ const resolved = this._resolvePath(file.destination);
651
+ await this._request("POST", `/v2/box/${this.id}/files/write`, {
652
+ body: { path: resolved, content: base64, encoding: "base64" },
653
+ });
654
+ }
655
+ }
656
+ async _downloadFiles(remotePath) {
657
+ const resolved = remotePath ? this._resolvePath(remotePath) : Box.WORKSPACE;
658
+ const dest = remotePath ? `./${basename(resolved)}` : "./workspace";
659
+ const files = await this._listFiles(resolved);
660
+ await mkdir(dest, { recursive: true });
661
+ for (const file of files) {
662
+ if (file.is_dir)
663
+ continue;
664
+ const url = `${this._baseUrl}/v2/box/${this.id}/files/download?path=${encodeURIComponent(file.path)}`;
665
+ const response = await fetch(url, { headers: this._headers });
666
+ if (!response.ok) {
667
+ throw new BoxError(`Failed to download ${file.path}`, response.status);
668
+ }
669
+ const buf = Buffer.from(await response.arrayBuffer());
670
+ await fsWriteFile(join(dest, file.name), buf);
671
+ }
672
+ }
673
+ // ==================== Lifecycle ====================
674
+ /**
675
+ * Get the current box status.
676
+ */
677
+ async getStatus() {
678
+ return this._request("GET", `/v2/box/${this.id}/status`);
679
+ }
680
+ /**
681
+ * Stop the box (release compute, preserve state).
682
+ */
683
+ async stop() {
684
+ await this._request("POST", `/v2/box/${this.id}/stop`);
685
+ }
686
+ /**
687
+ * Start a stopped box.
688
+ */
689
+ async start() {
690
+ await this._request("POST", `/v2/box/${this.id}/start`);
691
+ }
692
+ /**
693
+ * Delete this box permanently.
694
+ */
695
+ async delete() {
696
+ await this._request("DELETE", `/v2/box/${this.id}`);
697
+ }
698
+ /**
699
+ * Save workspace state as a snapshot for later restore.
700
+ * Creates the snapshot asynchronously and polls until ready.
701
+ */
702
+ async snapshot(options) {
703
+ const data = await this._request("POST", `/v2/box/${this.id}/snapshots`, {
704
+ body: { name: options.name },
705
+ });
706
+ // Poll until snapshot is ready
707
+ const pollInterval = 2000;
708
+ const maxWait = 300000;
709
+ const start = Date.now();
710
+ let snapshot = data;
711
+ while (snapshot.status === "creating" && Date.now() - start < maxWait) {
712
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
713
+ const snapshots = await this.listSnapshots();
714
+ const found = snapshots.find((s) => s.id === snapshot.id);
715
+ if (found)
716
+ snapshot = found;
717
+ }
718
+ if (snapshot.status === "creating") {
719
+ throw new BoxError("Snapshot creation timed out");
720
+ }
721
+ if (snapshot.status === "error") {
722
+ throw new BoxError("Snapshot creation failed");
723
+ }
724
+ return snapshot;
725
+ }
726
+ /**
727
+ * List all snapshots for this box.
728
+ */
729
+ async listSnapshots() {
730
+ const data = await this._request("GET", `/v2/box/${this.id}/snapshots`);
731
+ return data.snapshots ?? [];
732
+ }
733
+ /**
734
+ * Delete a snapshot.
735
+ */
736
+ async deleteSnapshot(snapshotId) {
737
+ await this._request("DELETE", `/v2/box/${this.id}/snapshots/${snapshotId}`);
738
+ }
739
+ /**
740
+ * Create a new box from a saved snapshot.
741
+ */
742
+ static async fromSnapshot(snapshotId, config) {
743
+ const apiKey = config.apiKey ?? process.env.UPSTASH_BOX_API_KEY;
744
+ if (!apiKey) {
745
+ throw new BoxError("apiKey is required. Pass it in config or set UPSTASH_BOX_API_KEY env var.");
746
+ }
747
+ const baseUrl = (config.baseUrl ??
748
+ process.env.UPSTASH_BOX_BASE_URL ??
749
+ "https://box.api.upstashdev.com").replace(/\/$/, "");
750
+ const headers = { "X-Box-Api-Key": apiKey };
751
+ const timeout = config.timeout ?? 600000;
752
+ const debug = config.debug ?? false;
753
+ const body = {
754
+ snapshot_id: snapshotId,
755
+ };
756
+ if (config.agent) {
757
+ body.model = config.agent.model;
758
+ body.agent_api_key = config.agent.apiKey;
759
+ }
760
+ if (config.runtime)
761
+ body.runtime = config.runtime;
762
+ if (config.git?.token)
763
+ body.github_token = config.git.token;
764
+ const response = await fetch(`${baseUrl}/v2/box/from-snapshot`, {
765
+ method: "POST",
766
+ headers: { ...headers, "Content-Type": "application/json" },
767
+ body: JSON.stringify(body),
768
+ });
769
+ if (!response.ok) {
770
+ const msg = await parseErrorResponse(response);
771
+ throw new BoxError(msg, response.status);
772
+ }
773
+ let data = (await response.json());
774
+ // Poll until ready
775
+ const pollInterval = 2000;
776
+ const maxWait = 300000;
777
+ const start = Date.now();
778
+ while (data.status === "creating" && Date.now() - start < maxWait) {
779
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
780
+ const pollResponse = await fetch(`${baseUrl}/v2/box/${data.id}`, { headers });
781
+ if (pollResponse.ok) {
782
+ data = (await pollResponse.json());
783
+ }
784
+ }
785
+ if (data.status === "creating") {
786
+ throw new BoxError("Box creation from snapshot timed out");
787
+ }
788
+ if (data.status === "error") {
789
+ throw new BoxError("Box creation from snapshot failed");
790
+ }
791
+ return new Box(data, {
792
+ baseUrl,
793
+ headers,
794
+ timeout,
795
+ debug,
796
+ gitToken: config.git?.token,
797
+ isAgentConfigured: Boolean(config.agent),
798
+ });
799
+ }
800
+ /**
801
+ * Get structured logs for this box.
802
+ */
803
+ async logs(options) {
804
+ const params = new URLSearchParams();
805
+ if (options?.offset)
806
+ params.set("offset", String(options.offset));
807
+ if (options?.limit)
808
+ params.set("limit", String(options.limit));
809
+ const qs = params.toString() ? `?${params.toString()}` : "";
810
+ const data = await this._request("GET", `/v2/box/${this.id}/logs${qs}`);
811
+ return data.logs;
812
+ }
813
+ /**
814
+ * List all runs for this box, newest first.
815
+ */
816
+ async listRuns() {
817
+ const data = await this._request("GET", `/v2/box/${this.id}/runs`);
818
+ return data.runs;
819
+ }
820
+ // ==================== Internal ====================
821
+ log(...args) {
822
+ if (this._debug)
823
+ console.log("[Box]", ...args);
824
+ }
825
+ /** @internal */
826
+ async _request(method, path, options = {}) {
827
+ const url = `${this._baseUrl}${path}`;
828
+ const controller = new AbortController();
829
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this._timeout);
830
+ try {
831
+ const headers = { ...this._headers };
832
+ let body;
833
+ if (options.body) {
834
+ headers["Content-Type"] = "application/json";
835
+ body = JSON.stringify(options.body);
836
+ }
837
+ this.log(`${method} ${url}`);
838
+ const response = await fetch(url, {
839
+ method,
840
+ headers,
841
+ body,
842
+ signal: controller.signal,
843
+ });
844
+ this.log(`Response status: ${response.status}`);
845
+ if (!response.ok) {
846
+ const msg = await parseErrorResponse(response);
847
+ throw new BoxError(msg, response.status);
848
+ }
849
+ const text = await response.text();
850
+ if (!text)
851
+ return {};
852
+ return JSON.parse(text);
853
+ }
854
+ catch (error) {
855
+ if (error instanceof BoxError)
856
+ throw error;
857
+ if (error instanceof Error && error.name === "AbortError") {
858
+ throw new BoxError("Request timeout");
859
+ }
860
+ throw new BoxError(error instanceof Error ? error.message : "Unknown error");
861
+ }
862
+ finally {
863
+ clearTimeout(timeoutId);
864
+ }
865
+ }
866
+ // ==================== Git (private, exposed via this.git) ====================
867
+ async _gitClone(options) {
868
+ await this._request("POST", `/v2/box/${this.id}/git/clone`, {
869
+ body: {
870
+ repo: options.repo,
871
+ branch: options.branch,
872
+ github_token: this._gitToken,
873
+ },
874
+ });
875
+ }
876
+ async _gitDiff() {
877
+ const data = await this._request("GET", `/v2/box/${this.id}/git/diff`);
878
+ return data.diff;
879
+ }
880
+ async _gitStatus() {
881
+ const data = await this._request("GET", `/v2/box/${this.id}/git/status`);
882
+ return data.status;
883
+ }
884
+ async _gitCommit(options) {
885
+ return this._request("POST", `/v2/box/${this.id}/git/commit`, {
886
+ body: { message: options.message },
887
+ });
888
+ }
889
+ async _gitPush(options) {
890
+ await this._request("POST", `/v2/box/${this.id}/git/push`, {
891
+ body: { branch: options?.branch },
892
+ });
893
+ }
894
+ async _gitCreatePR(options) {
895
+ return this._request("POST", `/v2/box/${this.id}/git/create-pr`, {
896
+ body: { title: options.title, body: options.body, base: options.base },
897
+ });
898
+ }
899
+ }
900
+ // ==================== Helpers ====================
901
+ /** @internal */
902
+ export function extractSchemaShape(schema) {
903
+ try {
904
+ const s = schema;
905
+ if (s.shape && typeof s.shape === "object") {
906
+ const shape = s.shape;
907
+ const fields = {};
908
+ for (const [key, val] of Object.entries(shape)) {
909
+ fields[key] = zodTypeToExample(val);
910
+ }
911
+ return JSON.stringify(fields, null, 2);
912
+ }
913
+ }
914
+ catch {
915
+ // Not a Zod schema or can't introspect
916
+ }
917
+ return null;
918
+ }
919
+ /** @internal */
920
+ export function zodTypeToExample(field) {
921
+ const f = field;
922
+ const typeName = f?._def?.typeName;
923
+ switch (typeName) {
924
+ case "ZodString":
925
+ return "string";
926
+ case "ZodNumber":
927
+ return "number";
928
+ case "ZodBoolean":
929
+ return "boolean";
930
+ case "ZodArray":
931
+ return `[${zodTypeToExample(f._def?.type)}]`;
932
+ default:
933
+ return "any";
934
+ }
935
+ }
936
+ /** @internal */
937
+ export async function parseErrorResponse(response) {
938
+ try {
939
+ const data = (await response.json());
940
+ return data.error ?? `Request failed with status ${response.status}`;
941
+ }
942
+ catch {
943
+ return `Request failed with status ${response.status}`;
944
+ }
945
+ }
946
+ async function sendWebhook(config, payload) {
947
+ try {
948
+ const body = JSON.stringify(payload);
949
+ const headers = {
950
+ "Content-Type": "application/json",
951
+ ...config.headers,
952
+ };
953
+ if (config.secret) {
954
+ const signature = createHmac("sha256", config.secret).update(body).digest("hex");
955
+ headers["X-Box-Signature"] = signature;
956
+ }
957
+ await fetch(config.url, { method: "POST", headers, body });
958
+ }
959
+ catch {
960
+ // Webhook delivery is best-effort
961
+ }
962
+ }
963
+ //# sourceMappingURL=client.js.map