@boxcompute/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.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # BoxCompute CLI
2
+
3
+ Connect Claude Code, Codex, Amp, OpenCode, and other local coding agents to
4
+ isolated BoxCompute sandboxes.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install --global @boxcompute/cli
10
+ bxc login
11
+ ```
12
+
13
+ `bxc login` opens BoxCompute in your browser. After you approve the device, the
14
+ CLI stores its credential outside the project under `~/.config/boxcompute` with
15
+ restrictive file permissions. The credential is never included in the agent
16
+ skill.
17
+
18
+ For a self-hosted BoxCompute deployment, provide its web origin:
19
+
20
+ ```sh
21
+ bxc login --url https://boxcompute.example.com
22
+ ```
23
+
24
+ ## Install the agent skill
25
+
26
+ ```sh
27
+ bxc skill detect
28
+ bxc skill install
29
+ ```
30
+
31
+ The installer checks known configuration directories and executables for
32
+ compatible coding harnesses. It does not recursively scan your home directory.
33
+ Use `bxc skill install all` to target every supported harness, or name one or
34
+ more explicitly. Run `bxc skill remove --yes` to uninstall matching copies.
35
+
36
+ ## Use sandboxes
37
+
38
+ ```sh
39
+ bxc doctor
40
+ bxc sandboxes
41
+ bxc sandbox start WORKSPACE_ID
42
+ bxc sandbox exec WORKSPACE_ID -- python -m pytest
43
+ ```
44
+
45
+ Run `bxc` or `bxc --help` for the complete command reference. The previous
46
+ `bcompute` executable remains available as a compatibility alias.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { type Connection } from "./config.js";
3
+ import { detectHarnesses, installSkill, readSkill, removeSkill } from "./skill.js";
4
+ type Io = {
5
+ stdout: NodeJS.WritableStream;
6
+ stderr: NodeJS.WritableStream;
7
+ };
8
+ export type CliDependencies = {
9
+ env?: NodeJS.ProcessEnv;
10
+ io?: Io;
11
+ fetch?: typeof fetch;
12
+ now?: () => number;
13
+ sleep?: (milliseconds: number) => Promise<void>;
14
+ openBrowser?: (url: string) => void;
15
+ loadConnection?: (env: NodeJS.ProcessEnv) => Promise<Connection>;
16
+ loadSavedUrl?: (env: NodeJS.ProcessEnv) => Promise<string | null>;
17
+ saveConnection?: (url: string, token: string, env: NodeJS.ProcessEnv) => Promise<void>;
18
+ clearConnection?: (env: NodeJS.ProcessEnv) => Promise<void>;
19
+ detectHarnesses?: typeof detectHarnesses;
20
+ installSkill?: typeof installSkill;
21
+ removeSkill?: typeof removeSkill;
22
+ readSkill?: typeof readSkill;
23
+ };
24
+ export declare function runCli(argv: string[], supplied?: CliDependencies): Promise<number>;
25
+ export declare function formatCliError(error: unknown): string;
26
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { readFileSync, realpathSync } from "node:fs";
4
+ import { hostname, platform } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+ import { BoxComputeClient, BoxComputeHttpError, publicRequest, } from "./client.js";
7
+ import { clearConnection, loadConnection, loadSavedUrl, saveConnection, } from "./config.js";
8
+ import { HARNESS_IDS, detectHarnesses, installSkill, readSkill, removeSkill, } from "./skill.js";
9
+ const DEFAULT_URL = "https://app.boxcompute.ai";
10
+ const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
11
+ const rootHelp = `BoxCompute CLI
12
+
13
+ Usage: bxc [options] [command]
14
+
15
+ Commands:
16
+
17
+ version Print the version number and exit
18
+ login Log in through BoxCompute in your browser
19
+ logout Revoke and remove the saved CLI credential
20
+ auth [alias: login] Authentication commands
21
+ logout Revoke and remove the saved CLI credential
22
+ doctor Verify the saved connection
23
+ sandboxes [aliases: list, ls] List workspaces and sandbox state
24
+ sandbox Manage isolated BoxCompute sandboxes
25
+ start Start or resume a workspace sandbox
26
+ status Inspect one sandbox
27
+ exec Execute a program inside a sandbox
28
+ delete [alias: rm] Destroy the runtime; the workspace remains
29
+ skill [alias: skills] Manage coding-harness skills
30
+ detect Detect compatible coding harnesses
31
+ list [alias: ls] List detected or installed harnesses
32
+ install [alias: add] Install the BoxCompute skill
33
+ remove [aliases: rm, uninstall] Remove the BoxCompute skill
34
+ info [alias: print] Print the packaged skill
35
+
36
+ Options:
37
+
38
+ -V, --version Print the version number and exit
39
+ --json Emit machine-readable JSON
40
+ -h, --help Display help for a command
41
+
42
+ Login options:
43
+
44
+ --url URL BoxCompute web URL (default: https://app.boxcompute.ai)
45
+ --no-open Print the approval URL without opening a browser
46
+
47
+ Examples:
48
+
49
+ $ bxc login
50
+ $ bxc skill detect
51
+ $ bxc skill install
52
+ $ bxc sandbox exec WORKSPACE_ID -- python -m pytest
53
+
54
+ Compatibility:
55
+
56
+ The previous bcompute command remains available as an alias.
57
+ `;
58
+ const sandboxHelp = `Manage isolated BoxCompute sandboxes
59
+
60
+ Usage: bxc sandbox <command> [options]
61
+
62
+ Commands:
63
+
64
+ start WORKSPACE_ID Start or resume a workspace sandbox
65
+ status SANDBOX_ID Inspect one sandbox
66
+ exec SANDBOX_ID [options] -- PROGRAM [ARG...]
67
+ Execute a program inside a sandbox
68
+ delete SANDBOX_ID --yes [alias: rm] Destroy the runtime; keep the workspace
69
+
70
+ Exec options:
71
+
72
+ --cwd PATH Working directory under /workspace
73
+ --env KEY=VALUE Set an environment variable; repeatable
74
+ --timeout SECONDS Command timeout
75
+ --max-output-bytes BYTES Maximum captured output
76
+ `;
77
+ const skillHelp = `Manage coding-harness skills
78
+
79
+ Usage: bxc skill <command> [options]
80
+
81
+ Commands:
82
+
83
+ detect Detect compatible coding harnesses
84
+ list [alias: ls] List detected or installed harnesses
85
+ install [auto|all|HARNESS...] [alias: add] Install to detected harnesses by default
86
+ remove [auto|all|HARNESS...] [aliases: rm, uninstall] Remove from detected harnesses
87
+ info [alias: print] Print the packaged BoxCompute skill
88
+
89
+ Install options:
90
+
91
+ --force Replace a locally modified skill
92
+
93
+ Remove options:
94
+
95
+ --yes Confirm removal
96
+ --force Remove a locally modified skill
97
+
98
+ Supported harnesses:
99
+ claude, codex, amp, opencode, cursor, gemini, copilot, cline, roo,
100
+ goose, pi, windsurf, and the shared agents directory
101
+ `;
102
+ class UsageError extends Error {
103
+ constructor(message) { super(message); this.name = "UsageError"; }
104
+ }
105
+ function browser(url) {
106
+ const system = platform();
107
+ const command = system === "darwin" ? "open" : system === "win32" ? "cmd" : "xdg-open";
108
+ const args = system === "win32" ? ["/c", "start", "", url] : [url];
109
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
110
+ child.on("error", () => undefined);
111
+ child.unref();
112
+ }
113
+ const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
114
+ const write = (stream, value) => { stream.write(value); };
115
+ const emit = (io, json, value, human) => {
116
+ write(io.stdout, json ? `${JSON.stringify(value)}\n` : human);
117
+ };
118
+ function option(tokens, name) {
119
+ const index = tokens.indexOf(`--${name}`);
120
+ if (index < 0)
121
+ return undefined;
122
+ const value = tokens[index + 1];
123
+ if (!value || value.startsWith("--"))
124
+ throw new UsageError(`--${name} requires a value`);
125
+ tokens.splice(index, 2);
126
+ return value;
127
+ }
128
+ function flag(tokens, name) {
129
+ const index = tokens.indexOf(`--${name}`);
130
+ if (index < 0)
131
+ return false;
132
+ tokens.splice(index, 1);
133
+ return true;
134
+ }
135
+ function anyFlag(tokens, ...names) {
136
+ const index = tokens.findIndex((token) => names.includes(token));
137
+ if (index < 0)
138
+ return false;
139
+ tokens.splice(index, 1);
140
+ return true;
141
+ }
142
+ function positive(value, name) {
143
+ if (value === undefined)
144
+ return undefined;
145
+ const parsed = Number(value);
146
+ if (!Number.isSafeInteger(parsed) || parsed <= 0)
147
+ throw new UsageError(`${name} must be a positive integer`);
148
+ return parsed;
149
+ }
150
+ function environment(tokens) {
151
+ const values = [];
152
+ for (;;) {
153
+ const index = tokens.indexOf("--env");
154
+ if (index < 0)
155
+ break;
156
+ const value = tokens[index + 1];
157
+ if (!value)
158
+ throw new UsageError("--env requires KEY=VALUE");
159
+ values.push(value);
160
+ tokens.splice(index, 2);
161
+ }
162
+ if (!values.length)
163
+ return undefined;
164
+ return Object.fromEntries(values.map((value) => {
165
+ const at = value.indexOf("=");
166
+ if (at < 1)
167
+ throw new UsageError("--env requires KEY=VALUE");
168
+ return [value.slice(0, at), value.slice(at + 1)];
169
+ }));
170
+ }
171
+ async function authenticate(args, dependencies) {
172
+ const url = option(args, "url") ?? await dependencies.loadSavedUrl(dependencies.env) ?? DEFAULT_URL;
173
+ const noOpen = flag(args, "no-open");
174
+ if (args.length)
175
+ throw new UsageError(`Unexpected auth argument: ${args[0]}`);
176
+ let started;
177
+ try {
178
+ started = await publicRequest(url, "/api/cli-auth/device", {
179
+ method: "POST",
180
+ headers: { "content-type": "application/json" },
181
+ body: JSON.stringify({ clientName: `${hostname()} (${platform()})` }),
182
+ }, dependencies.fetch);
183
+ }
184
+ catch (error) {
185
+ if (error instanceof BoxComputeHttpError && [401, 404, 405].includes(error.status)) {
186
+ throw new Error(`Browser login is not available at ${url} (HTTP ${error.status}). ` +
187
+ "The BoxCompute server must be updated to support CLI authentication. " +
188
+ "For a local or self-hosted server, run `bxc login --url WEB_URL`.");
189
+ }
190
+ throw error;
191
+ }
192
+ const verificationUrl = new URL(started.verificationUriComplete);
193
+ if (verificationUrl.origin !== new URL(url).origin ||
194
+ !started.deviceCode.startsWith("bcd_") ||
195
+ !/^[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(started.userCode) ||
196
+ !Number.isFinite(started.expiresIn) ||
197
+ started.expiresIn <= 0 ||
198
+ !Number.isFinite(started.interval) ||
199
+ started.interval <= 0) {
200
+ throw new Error("BoxCompute returned an invalid browser authentication response");
201
+ }
202
+ write(dependencies.io.stderr, `\nOpen this URL to authenticate:\n${verificationUrl.href}\n\nCode: ${started.userCode}\n\n`);
203
+ if (!noOpen)
204
+ dependencies.openBrowser(verificationUrl.href);
205
+ write(dependencies.io.stderr, "Waiting for browser approval…\n");
206
+ const deadline = dependencies.now() + started.expiresIn * 1000;
207
+ while (dependencies.now() < deadline) {
208
+ await dependencies.sleep(Math.max(1, started.interval) * 1000);
209
+ try {
210
+ const result = await publicRequest(url, "/api/cli-auth/token", {
211
+ method: "POST",
212
+ headers: { "content-type": "application/json" },
213
+ body: JSON.stringify({ deviceCode: started.deviceCode }),
214
+ }, dependencies.fetch);
215
+ await dependencies.saveConnection(url, result.token, dependencies.env);
216
+ emit(dependencies.io, dependencies.json, { authenticated: true, url }, `Authenticated with ${url}.\nNext: bxc skill install\n`);
217
+ return 0;
218
+ }
219
+ catch (error) {
220
+ if (error instanceof BoxComputeHttpError && error.status === 428 && error.message === "authorization_pending")
221
+ continue;
222
+ if (error instanceof BoxComputeHttpError && error.status === 410)
223
+ throw new Error("Browser authentication expired. Run `bxc auth` again.");
224
+ throw error;
225
+ }
226
+ }
227
+ throw new Error("Browser authentication expired. Run `bxc auth` again.");
228
+ }
229
+ function sandboxLine(sandbox) {
230
+ return `${sandbox.id}\t${sandbox.state}\t${sandbox.name}\n`;
231
+ }
232
+ function executionOutput(io, json, sandboxId, result) {
233
+ if (json)
234
+ emit(io, true, { sandboxId, ...result }, "");
235
+ else {
236
+ write(io.stdout, result.stdout);
237
+ write(io.stderr, result.stderr);
238
+ write(io.stderr, `sandbox=${sandboxId} exitCode=${result.exitCode ?? "null"} timedOut=${result.timedOut}\n`);
239
+ }
240
+ return result.exitCode ?? 1;
241
+ }
242
+ export async function runCli(argv, supplied = {}) {
243
+ const env = supplied.env ?? process.env;
244
+ const io = supplied.io ?? { stdout: process.stdout, stderr: process.stderr };
245
+ const fetchImpl = supplied.fetch ?? fetch;
246
+ const now = supplied.now ?? Date.now;
247
+ const sleep = supplied.sleep ?? delay;
248
+ const openBrowser = supplied.openBrowser ?? browser;
249
+ const load = supplied.loadConnection ?? loadConnection;
250
+ const savedUrl = supplied.loadSavedUrl ?? loadSavedUrl;
251
+ const save = supplied.saveConnection ?? saveConnection;
252
+ const clear = supplied.clearConnection ?? clearConnection;
253
+ const detect = supplied.detectHarnesses ?? detectHarnesses;
254
+ const install = supplied.installSkill ?? installSkill;
255
+ const remove = supplied.removeSkill ?? removeSkill;
256
+ const skillText = supplied.readSkill ?? readSkill;
257
+ const args = [...argv];
258
+ const json = flag(args, "json");
259
+ const versionRequested = args[0] === "version" || anyFlag(args, "--version", "-V", "-v");
260
+ if (versionRequested) {
261
+ emit(io, json, { version: CLI_VERSION }, `${CLI_VERSION}\n`);
262
+ return 0;
263
+ }
264
+ const helpRequested = anyFlag(args, "--help", "-h");
265
+ if (!args.length || args[0] === "help" || helpRequested) {
266
+ const helpTarget = args[0] === "help" ? args[1] : args[0];
267
+ write(io.stdout, helpFor(helpTarget));
268
+ return 0;
269
+ }
270
+ let command = args.shift();
271
+ if (command === "login")
272
+ command = "auth";
273
+ if (command === "skills")
274
+ command = "skill";
275
+ if (command === "list" || command === "ls")
276
+ command = "sandboxes";
277
+ if (command === "logout") {
278
+ if (args.length)
279
+ throw new UsageError("logout takes no options");
280
+ const connection = await load(env);
281
+ await new BoxComputeClient(connection, fetchImpl).logout();
282
+ await clear(env);
283
+ emit(io, json, { authenticated: false }, "BoxCompute CLI credential revoked and removed.\n");
284
+ return 0;
285
+ }
286
+ if (command === "auth") {
287
+ if (args[0] === "login")
288
+ args.shift();
289
+ if (args[0] === "logout") {
290
+ args.shift();
291
+ if (args.length)
292
+ throw new UsageError("auth logout takes no options");
293
+ const connection = await load(env);
294
+ await new BoxComputeClient(connection, fetchImpl).logout();
295
+ await clear(env);
296
+ emit(io, json, { authenticated: false }, "BoxCompute CLI credential revoked and removed.\n");
297
+ return 0;
298
+ }
299
+ return authenticate(args, { env, io, json, fetch: fetchImpl, now, sleep, openBrowser, loadSavedUrl: savedUrl, saveConnection: save });
300
+ }
301
+ if (command === "skill") {
302
+ let action = args.shift();
303
+ if (!action) {
304
+ write(io.stdout, skillHelp);
305
+ return 0;
306
+ }
307
+ if (action === "ls")
308
+ action = "list";
309
+ if (action === "add")
310
+ action = "install";
311
+ if (action === "rm" || action === "uninstall")
312
+ action = "remove";
313
+ if (action === "print")
314
+ action = "info";
315
+ if (action === "detect") {
316
+ if (args.length)
317
+ throw new UsageError("skill detect takes no options");
318
+ const harnesses = (await detect(env)).filter((item) => item.detected);
319
+ emit(io, json, { harnesses }, harnesses.length
320
+ ? `Detected coding harnesses:\n${harnesses.map(harnessLine).join("")}\nRun \`bxc skill install\` to install automatically.\n`
321
+ : "No supported coding harnesses detected. You can name one explicitly or use `bxc skill install all`.\n");
322
+ return 0;
323
+ }
324
+ if (action === "list") {
325
+ if (args.length)
326
+ throw new UsageError("skill list takes no options");
327
+ const harnesses = (await detect(env)).filter((item) => item.detected || item.installed);
328
+ emit(io, json, { harnesses }, harnesses.length
329
+ ? `Coding harness skills:\n${harnesses.map(harnessLine).join("")}`
330
+ : "No supported coding harnesses detected and no BoxCompute skills installed.\n");
331
+ return 0;
332
+ }
333
+ if (action === "info") {
334
+ if (args.length)
335
+ throw new UsageError("skill info takes no options");
336
+ write(io.stdout, await skillText());
337
+ return 0;
338
+ }
339
+ if (action !== "install" && action !== "remove") {
340
+ throw new UsageError("skill requires `detect`, `list`, `install`, `remove`, or `info`");
341
+ }
342
+ const force = flag(args, "force");
343
+ const confirmed = flag(args, "yes");
344
+ const targets = (args.length ? args : ["auto"]);
345
+ const supported = new Set([...HARNESS_IDS, "auto", "all", "both"]);
346
+ const invalid = targets.find((target) => !supported.has(target));
347
+ if (invalid)
348
+ throw new UsageError(`Unknown coding harness: ${invalid}`);
349
+ if (action === "remove") {
350
+ if (!confirmed)
351
+ throw new UsageError("skill remove requires --yes");
352
+ const removed = await remove(targets, { force, env });
353
+ emit(io, json, { removed }, `${removed.map((item) => `${removalStatus(item.status)} for ${item.agents.join(", ")}: ${item.path}`).join("\n")}\n`);
354
+ return 0;
355
+ }
356
+ if (confirmed)
357
+ throw new UsageError("--yes is only valid with skill remove");
358
+ const installed = await install(targets, { force, env });
359
+ emit(io, json, { installed }, `${installed.map((item) => `${skillStatus(item.status)} for ${item.agents.join(", ")}: ${item.path}`).join("\n")}\n`);
360
+ return 0;
361
+ }
362
+ if (command === "sandbox" && !args.length) {
363
+ write(io.stdout, sandboxHelp);
364
+ return 0;
365
+ }
366
+ const connection = await load(env);
367
+ const client = new BoxComputeClient(connection, fetchImpl);
368
+ if (command === "doctor") {
369
+ const sandboxes = await client.list();
370
+ emit(io, json, { connected: true, url: connection.url, sandboxes: sandboxes.length }, `Connected to ${connection.url} · ${sandboxes.length} workspace${sandboxes.length === 1 ? "" : "s"}\n`);
371
+ return 0;
372
+ }
373
+ if (command === "sandboxes") {
374
+ if (args.length)
375
+ throw new UsageError("sandboxes takes no options");
376
+ const sandboxes = await client.list();
377
+ emit(io, json, { sandboxes }, sandboxes.length ? sandboxes.map(sandboxLine).join("") : "No workspaces found. Create one in BoxCompute first.\n");
378
+ return 0;
379
+ }
380
+ if (command !== "sandbox")
381
+ throw new UsageError(`Unknown command: ${command}`);
382
+ let action = args.shift();
383
+ if (action === "rm")
384
+ action = "delete";
385
+ const id = args.shift();
386
+ if (!action || !id)
387
+ throw new UsageError("sandbox requires an action and sandbox ID");
388
+ if (action === "start") {
389
+ if (args.length)
390
+ throw new UsageError("sandbox start takes one workspace ID");
391
+ const sandbox = await client.start(id);
392
+ emit(io, json, { sandbox }, sandboxLine(sandbox));
393
+ return 0;
394
+ }
395
+ if (action === "status") {
396
+ if (args.length)
397
+ throw new UsageError("sandbox status takes one sandbox ID");
398
+ const sandbox = await client.inspect(id);
399
+ emit(io, json, { sandbox }, sandboxLine(sandbox));
400
+ return 0;
401
+ }
402
+ if (action === "delete") {
403
+ if (!flag(args, "yes") || args.length)
404
+ throw new UsageError("sandbox delete requires SANDBOX_ID --yes");
405
+ await client.delete(id);
406
+ emit(io, json, { sandboxId: id, deleted: true }, `Destroyed sandbox runtime ${id}; its workspace remains.\n`);
407
+ return 0;
408
+ }
409
+ if (action === "exec") {
410
+ const separator = args.indexOf("--");
411
+ if (separator < 0 || separator === args.length - 1)
412
+ throw new UsageError("sandbox exec requires a program after --");
413
+ const options = args.splice(0, separator);
414
+ args.shift();
415
+ const cwd = option(options, "cwd");
416
+ const timeoutSeconds = positive(option(options, "timeout"), "--timeout");
417
+ const maxOutputBytes = positive(option(options, "max-output-bytes"), "--max-output-bytes");
418
+ const envInput = environment(options);
419
+ if (options.length)
420
+ throw new UsageError(`Unknown sandbox exec option: ${options[0]}`);
421
+ return executionOutput(io, json, id, await client.execute(id, { argv: args, cwd, timeoutSeconds, maxOutputBytes, env: envInput }));
422
+ }
423
+ throw new UsageError(`Unknown sandbox action: ${action}`);
424
+ }
425
+ function harnessLine(harness) {
426
+ const status = harness.installed ? "installed" : "detected";
427
+ return ` ${harness.label}\t${status}\t${harness.path}\n`;
428
+ }
429
+ function skillStatus(status) {
430
+ if (status === "updated")
431
+ return "Updated";
432
+ if (status === "unchanged")
433
+ return "Already installed";
434
+ return "Installed";
435
+ }
436
+ function removalStatus(status) {
437
+ return status === "removed" ? "Removed" : "Not installed";
438
+ }
439
+ function helpFor(command) {
440
+ if (command === "sandbox")
441
+ return sandboxHelp;
442
+ if (command === "skill" || command === "skills")
443
+ return skillHelp;
444
+ return rootHelp;
445
+ }
446
+ export function formatCliError(error) {
447
+ if (error instanceof BoxComputeHttpError)
448
+ return `${error.message} (HTTP ${error.status})`;
449
+ return error instanceof Error ? error.message : String(error);
450
+ }
451
+ async function main() {
452
+ try {
453
+ process.exitCode = await runCli(process.argv.slice(2));
454
+ }
455
+ catch (error) {
456
+ write(process.stderr, `${error instanceof UsageError ? "Usage" : "Error"}: ${formatCliError(error)}\n`);
457
+ process.exitCode = error instanceof UsageError ? 2 : 1;
458
+ }
459
+ }
460
+ function isMainModule() {
461
+ if (!process.argv[1])
462
+ return false;
463
+ try {
464
+ // npm exposes package binaries through a symlink. Comparing the raw argv
465
+ // path with import.meta.url makes the installed CLI look like a library and
466
+ // silently skips main(), so resolve both sides before comparing them.
467
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
468
+ }
469
+ catch {
470
+ return false;
471
+ }
472
+ }
473
+ if (isMainModule())
474
+ await main();
@@ -0,0 +1,44 @@
1
+ import type { Connection } from "./config.js";
2
+ export type Sandbox = {
3
+ id: string;
4
+ workspaceId: string;
5
+ name: string;
6
+ state: "not-created" | "cold" | "running";
7
+ runtimeId: string | null;
8
+ image: string | null;
9
+ createdAt: number;
10
+ lastUsedAt: number | null;
11
+ };
12
+ export type Execution = {
13
+ stdout: string;
14
+ stderr: string;
15
+ exitCode: number | null;
16
+ timedOut: boolean;
17
+ stdoutTruncated: boolean;
18
+ stderrTruncated: boolean;
19
+ wallTimeSeconds: number;
20
+ };
21
+ export declare class BoxComputeHttpError extends Error {
22
+ readonly status: number;
23
+ readonly code?: string | undefined;
24
+ constructor(status: number, message: string, code?: string | undefined);
25
+ }
26
+ export declare function publicRequest<T>(url: string, pathname: string, init: RequestInit, fetchImpl?: typeof fetch): Promise<T>;
27
+ export declare class BoxComputeClient {
28
+ private readonly connection;
29
+ private readonly fetchImpl;
30
+ constructor(connection: Connection, fetchImpl?: typeof fetch);
31
+ private request;
32
+ list(): Promise<Sandbox[]>;
33
+ logout(): Promise<void>;
34
+ inspect(id: string): Promise<Sandbox>;
35
+ start(workspaceId: string): Promise<Sandbox>;
36
+ execute(id: string, input: {
37
+ argv: string[];
38
+ cwd?: string;
39
+ timeoutSeconds?: number;
40
+ maxOutputBytes?: number;
41
+ env?: Record<string, string>;
42
+ }): Promise<Execution>;
43
+ delete(id: string): Promise<void>;
44
+ }
package/dist/client.js ADDED
@@ -0,0 +1,59 @@
1
+ export class BoxComputeHttpError extends Error {
2
+ status;
3
+ code;
4
+ constructor(status, message, code) {
5
+ super(message);
6
+ this.status = status;
7
+ this.code = code;
8
+ this.name = "BoxComputeHttpError";
9
+ }
10
+ }
11
+ async function responseError(response) {
12
+ const body = await response.json().catch(() => ({}));
13
+ return new BoxComputeHttpError(response.status, body.error ?? `BoxCompute returned HTTP ${response.status}`, body.code);
14
+ }
15
+ export async function publicRequest(url, pathname, init, fetchImpl = fetch) {
16
+ const response = await fetchImpl(new URL(pathname, `${url}/`), init);
17
+ if (!response.ok)
18
+ throw await responseError(response);
19
+ return response.status === 204 ? undefined : await response.json();
20
+ }
21
+ export class BoxComputeClient {
22
+ connection;
23
+ fetchImpl;
24
+ constructor(connection, fetchImpl = fetch) {
25
+ this.connection = connection;
26
+ this.fetchImpl = fetchImpl;
27
+ }
28
+ request(pathname, init = {}) {
29
+ const headers = new Headers(init.headers);
30
+ headers.set("authorization", `Bearer ${this.connection.token}`);
31
+ return publicRequest(this.connection.url, pathname, { ...init, headers }, this.fetchImpl);
32
+ }
33
+ async list() {
34
+ return (await this.request("/api/v1/sandboxes")).sandboxes;
35
+ }
36
+ async logout() {
37
+ await this.request("/api/v1/auth", { method: "DELETE" });
38
+ }
39
+ async inspect(id) {
40
+ return (await this.request(`/api/v1/sandboxes/${encodeURIComponent(id)}`)).sandbox;
41
+ }
42
+ async start(workspaceId) {
43
+ return (await this.request("/api/v1/sandboxes", {
44
+ method: "POST",
45
+ headers: { "content-type": "application/json" },
46
+ body: JSON.stringify({ workspaceId }),
47
+ })).sandbox;
48
+ }
49
+ async execute(id, input) {
50
+ return (await this.request(`/api/v1/sandboxes/${encodeURIComponent(id)}/execute`, {
51
+ method: "POST",
52
+ headers: { "content-type": "application/json" },
53
+ body: JSON.stringify(input),
54
+ })).result;
55
+ }
56
+ async delete(id) {
57
+ await this.request(`/api/v1/sandboxes/${encodeURIComponent(id)}`, { method: "DELETE" });
58
+ }
59
+ }
@@ -0,0 +1,15 @@
1
+ export type Connection = {
2
+ url: string;
3
+ token: string;
4
+ tokenFile: string;
5
+ };
6
+ export declare function normalizeBaseUrl(value: string): string;
7
+ export declare function connectionPaths(env?: NodeJS.ProcessEnv): {
8
+ directory: string;
9
+ configFile: string;
10
+ tokenFile: string;
11
+ };
12
+ export declare function loadConnection(env?: NodeJS.ProcessEnv): Promise<Connection>;
13
+ export declare function loadSavedUrl(env?: NodeJS.ProcessEnv): Promise<string | null>;
14
+ export declare function saveConnection(url: string, token: string, env?: NodeJS.ProcessEnv): Promise<void>;
15
+ export declare function clearConnection(env?: NodeJS.ProcessEnv): Promise<void>;
package/dist/config.js ADDED
@@ -0,0 +1,101 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ export function normalizeBaseUrl(value) {
6
+ let url;
7
+ try {
8
+ url = new URL(value);
9
+ }
10
+ catch {
11
+ throw new Error("BoxCompute URL must be an absolute http or https URL");
12
+ }
13
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
14
+ throw new Error("BoxCompute URL must be an http or https origin without credentials");
15
+ }
16
+ if ((url.pathname !== "/" && url.pathname !== "") || url.search || url.hash) {
17
+ throw new Error("BoxCompute URL must be an origin without a path, query, or fragment");
18
+ }
19
+ return url.origin;
20
+ }
21
+ export function connectionPaths(env = process.env) {
22
+ const directory = env.BOXCOMPUTE_CONFIG_DIR
23
+ ? path.resolve(env.BOXCOMPUTE_CONFIG_DIR)
24
+ : path.join(env.XDG_CONFIG_HOME ? path.resolve(env.XDG_CONFIG_HOME) : homedir(), "boxcompute");
25
+ return {
26
+ directory,
27
+ configFile: path.join(directory, "config.json"),
28
+ tokenFile: path.join(directory, "credential"),
29
+ };
30
+ }
31
+ async function secureFile(file) {
32
+ const mode = (await stat(file)).mode & 0o777;
33
+ if ((mode & 0o077) !== 0) {
34
+ throw new Error(`${file} must not be readable by group or other users (run chmod 600)`);
35
+ }
36
+ }
37
+ export async function loadConnection(env = process.env) {
38
+ const paths = connectionPaths(env);
39
+ let url = env.BOXCOMPUTE_URL;
40
+ let tokenFile = env.BOXCOMPUTE_TOKEN_FILE;
41
+ if (!url || !tokenFile) {
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(await readFile(paths.configFile, "utf8"));
45
+ }
46
+ catch {
47
+ throw new Error("Not authenticated. Run `bxc auth` first.");
48
+ }
49
+ url ??= typeof parsed.url === "string" ? parsed.url : undefined;
50
+ tokenFile ??= typeof parsed.tokenFile === "string" ? parsed.tokenFile : undefined;
51
+ }
52
+ if (!url || !tokenFile)
53
+ throw new Error("Not authenticated. Run `bxc auth` first.");
54
+ const resolvedTokenFile = path.resolve(tokenFile);
55
+ await secureFile(resolvedTokenFile);
56
+ const token = (await readFile(resolvedTokenFile, "utf8")).trim();
57
+ if (!token.startsWith("bc_live_"))
58
+ throw new Error("The saved BoxCompute credential is invalid. Run `bxc auth` again.");
59
+ return { url: normalizeBaseUrl(url), token, tokenFile: resolvedTokenFile };
60
+ }
61
+ export async function loadSavedUrl(env = process.env) {
62
+ if (env.BOXCOMPUTE_URL)
63
+ return normalizeBaseUrl(env.BOXCOMPUTE_URL);
64
+ try {
65
+ const parsed = JSON.parse(await readFile(connectionPaths(env).configFile, "utf8"));
66
+ return typeof parsed.url === "string" ? normalizeBaseUrl(parsed.url) : null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ export async function saveConnection(url, token, env = process.env) {
73
+ if (!token.startsWith("bc_live_"))
74
+ throw new Error("BoxCompute returned an invalid credential");
75
+ const paths = connectionPaths(env);
76
+ await mkdir(paths.directory, { recursive: true, mode: 0o700 });
77
+ await chmod(paths.directory, 0o700);
78
+ const suffix = randomUUID();
79
+ const tokenTemp = `${paths.tokenFile}.${suffix}.tmp`;
80
+ const configTemp = `${paths.configFile}.${suffix}.tmp`;
81
+ try {
82
+ await writeFile(tokenTemp, `${token}\n`, { mode: 0o600, flag: "wx" });
83
+ await writeFile(configTemp, `${JSON.stringify({ url: normalizeBaseUrl(url), tokenFile: paths.tokenFile }, null, 2)}\n`, {
84
+ mode: 0o600,
85
+ flag: "wx",
86
+ });
87
+ await rename(tokenTemp, paths.tokenFile);
88
+ await chmod(paths.tokenFile, 0o600);
89
+ await rename(configTemp, paths.configFile);
90
+ await chmod(paths.configFile, 0o600);
91
+ }
92
+ finally {
93
+ await rm(tokenTemp, { force: true }).catch(() => undefined);
94
+ await rm(configTemp, { force: true }).catch(() => undefined);
95
+ }
96
+ }
97
+ export async function clearConnection(env = process.env) {
98
+ const paths = connectionPaths(env);
99
+ await rm(paths.tokenFile, { force: true });
100
+ await rm(paths.configFile, { force: true });
101
+ }
@@ -0,0 +1,31 @@
1
+ export declare const HARNESS_IDS: readonly ["claude", "codex", "amp", "opencode", "cursor", "gemini", "copilot", "cline", "roo", "goose", "pi", "windsurf", "agents"];
2
+ export type HarnessId = (typeof HARNESS_IDS)[number];
3
+ export type AgentTarget = HarnessId | "auto" | "all" | "both";
4
+ export type HarnessDetection = {
5
+ id: HarnessId;
6
+ label: string;
7
+ detected: boolean;
8
+ signals: string[];
9
+ path: string;
10
+ installed: boolean;
11
+ };
12
+ export type SkillInstallation = {
13
+ agents: string[];
14
+ path: string;
15
+ };
16
+ export type SkillInstallationResult = SkillInstallation & {
17
+ status: "installed" | "updated" | "unchanged";
18
+ };
19
+ export type SkillRemovalResult = SkillInstallation & {
20
+ status: "removed" | "missing";
21
+ };
22
+ export declare function detectHarnesses(env?: NodeJS.ProcessEnv): Promise<HarnessDetection[]>;
23
+ export declare function installSkill(requested?: AgentTarget | AgentTarget[], options?: {
24
+ force?: boolean;
25
+ env?: NodeJS.ProcessEnv;
26
+ }): Promise<SkillInstallationResult[]>;
27
+ export declare function removeSkill(requested?: AgentTarget | AgentTarget[], options?: {
28
+ force?: boolean;
29
+ env?: NodeJS.ProcessEnv;
30
+ }): Promise<SkillRemovalResult[]>;
31
+ export declare function readSkill(): Promise<string>;
package/dist/skill.js ADDED
@@ -0,0 +1,213 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { access, cp, mkdir, readFile, readdir, rename, rm, stat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ export const HARNESS_IDS = [
8
+ "claude",
9
+ "codex",
10
+ "amp",
11
+ "opencode",
12
+ "cursor",
13
+ "gemini",
14
+ "copilot",
15
+ "cline",
16
+ "roo",
17
+ "goose",
18
+ "pi",
19
+ "windsurf",
20
+ "agents",
21
+ ];
22
+ const sourceSkill = fileURLToPath(new URL("../skills/boxcompute-sandbox", import.meta.url));
23
+ function locations(env) {
24
+ const home = env.HOME || homedir();
25
+ return {
26
+ home,
27
+ config: env.XDG_CONFIG_HOME ? path.resolve(env.XDG_CONFIG_HOME) : path.join(home, ".config"),
28
+ codex: env.CODEX_HOME ? path.resolve(env.CODEX_HOME) : path.join(home, ".codex"),
29
+ };
30
+ }
31
+ const sharedRoot = ({ home }) => path.join(home, ".agents", "skills");
32
+ const definitions = [
33
+ { id: "claude", label: "Claude Code", commands: ["claude"], markers: ({ home }) => [path.join(home, ".claude")], skillRoot: ({ home }) => path.join(home, ".claude", "skills") },
34
+ { id: "codex", label: "Codex", commands: ["codex"], markers: ({ codex }) => [codex], skillRoot: ({ codex }) => path.join(codex, "skills") },
35
+ { id: "amp", label: "Amp", commands: ["amp"], markers: ({ config }) => [path.join(config, "amp")], skillRoot: ({ home }) => path.join(home, ".claude", "skills") },
36
+ { id: "opencode", label: "OpenCode", commands: ["opencode"], markers: ({ config, home }) => [path.join(config, "opencode"), path.join(home, ".opencode")], skillRoot: ({ config }) => path.join(config, "opencode", "skills") },
37
+ { id: "cursor", label: "Cursor", commands: ["cursor", "cursor-agent"], markers: ({ home }) => [path.join(home, ".cursor")], skillRoot: ({ home }) => path.join(home, ".cursor", "skills") },
38
+ { id: "gemini", label: "Gemini CLI", commands: ["gemini"], markers: ({ home }) => [path.join(home, ".gemini")], skillRoot: ({ home }) => path.join(home, ".gemini", "skills") },
39
+ { id: "copilot", label: "GitHub Copilot", commands: ["copilot"], markers: ({ home }) => [path.join(home, ".copilot")], skillRoot: ({ home }) => path.join(home, ".copilot", "skills") },
40
+ { id: "cline", label: "Cline", commands: ["cline"], markers: ({ home }) => [path.join(home, ".cline")], skillRoot: ({ home }) => path.join(home, ".cline", "skills") },
41
+ { id: "roo", label: "Roo Code", commands: ["roo"], markers: ({ home }) => [path.join(home, ".roo")], skillRoot: ({ home }) => path.join(home, ".roo", "skills") },
42
+ { id: "goose", label: "goose", commands: ["goose"], markers: ({ config }) => [path.join(config, "goose")], skillRoot: sharedRoot },
43
+ { id: "pi", label: "Pi", commands: ["pi"], markers: ({ home }) => [path.join(home, ".pi")], skillRoot: ({ home }) => path.join(home, ".pi", "agent", "skills") },
44
+ { id: "windsurf", label: "Windsurf", commands: ["windsurf"], markers: ({ home }) => [path.join(home, ".codeium", "windsurf")], skillRoot: sharedRoot },
45
+ { id: "agents", label: "Agent Skills compatible harnesses", commands: [], markers: ({ home }) => [path.join(home, ".agents")], skillRoot: sharedRoot },
46
+ ];
47
+ async function exists(candidate) {
48
+ return stat(candidate).then(() => true, () => false);
49
+ }
50
+ async function directoryDigest(directory) {
51
+ const digest = createHash("sha256");
52
+ const walk = async (current, prefix = "") => {
53
+ const entries = await readdir(current, { withFileTypes: true });
54
+ entries.sort((left, right) => left.name.localeCompare(right.name));
55
+ for (const entry of entries) {
56
+ const relative = path.posix.join(prefix, entry.name);
57
+ const absolute = path.join(current, entry.name);
58
+ if (entry.isDirectory())
59
+ await walk(absolute, relative);
60
+ else if (entry.isFile()) {
61
+ digest.update(relative);
62
+ digest.update("\0");
63
+ digest.update(await readFile(absolute));
64
+ digest.update("\0");
65
+ }
66
+ else {
67
+ throw new Error(`Unsupported skill entry: ${relative}`);
68
+ }
69
+ }
70
+ };
71
+ await walk(directory);
72
+ return digest.digest("hex");
73
+ }
74
+ async function matchesPackagedSkill(candidate) {
75
+ try {
76
+ return await directoryDigest(candidate) === await directoryDigest(sourceSkill);
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ async function commandExists(command, env) {
83
+ if (!env.PATH)
84
+ return false;
85
+ const extensions = process.platform === "win32"
86
+ ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
87
+ : [""];
88
+ for (const directory of env.PATH.split(path.delimiter).filter(Boolean)) {
89
+ for (const extension of extensions) {
90
+ const executable = path.join(directory, `${command}${extension}`);
91
+ if (await access(executable, constants.X_OK).then(() => true, () => false))
92
+ return true;
93
+ }
94
+ }
95
+ return false;
96
+ }
97
+ export async function detectHarnesses(env = process.env) {
98
+ const resolved = locations(env);
99
+ return Promise.all(definitions.map(async (definition) => {
100
+ const signals = [];
101
+ const skillPath = path.join(definition.skillRoot(resolved), "boxcompute-sandbox");
102
+ for (const marker of definition.markers(resolved)) {
103
+ if (await exists(marker))
104
+ signals.push(marker);
105
+ }
106
+ for (const command of definition.commands) {
107
+ if (await commandExists(command, env))
108
+ signals.push(`command:${command}`);
109
+ }
110
+ return {
111
+ id: definition.id,
112
+ label: definition.label,
113
+ detected: signals.length > 0,
114
+ signals,
115
+ path: skillPath,
116
+ installed: await exists(skillPath),
117
+ };
118
+ }));
119
+ }
120
+ function expandedTargets(targets) {
121
+ if (targets.includes("auto")) {
122
+ if (targets.length !== 1)
123
+ throw new Error("auto cannot be combined with other harnesses");
124
+ return "auto";
125
+ }
126
+ if (targets.includes("all")) {
127
+ if (targets.length !== 1)
128
+ throw new Error("all cannot be combined with other harnesses");
129
+ return "all";
130
+ }
131
+ return [...new Set(targets.flatMap((target) => target === "both" ? ["claude", "codex"] : [target]))];
132
+ }
133
+ export async function installSkill(requested = "auto", options = {}) {
134
+ const env = options.env ?? process.env;
135
+ const detections = await detectHarnesses(env);
136
+ const targets = expandedTargets(Array.isArray(requested) ? requested : [requested]);
137
+ const selected = targets === "all"
138
+ ? detections
139
+ : targets === "auto"
140
+ ? detections.filter((item) => item.detected)
141
+ : targets.map((id) => detections.find((item) => item.id === id));
142
+ if (!selected.length) {
143
+ throw new Error("No supported coding harnesses detected. Name one explicitly or use `bxc skill install all`.");
144
+ }
145
+ const destinations = new Map();
146
+ for (const target of selected) {
147
+ const current = destinations.get(target.path);
148
+ if (current)
149
+ current.agents.push(target.label);
150
+ else
151
+ destinations.set(target.path, { agents: [target.label], path: target.path });
152
+ }
153
+ const installations = [...destinations.values()];
154
+ const planned = await Promise.all(installations.map(async (item) => {
155
+ if (!await exists(item.path))
156
+ return { ...item, status: "installed" };
157
+ if (options.force)
158
+ return { ...item, status: "updated" };
159
+ if (await matchesPackagedSkill(item.path))
160
+ return { ...item, status: "unchanged" };
161
+ throw new Error(`${item.path} already exists and differs; pass --force to replace it`);
162
+ }));
163
+ for (const item of planned) {
164
+ if (item.status === "unchanged")
165
+ continue;
166
+ await mkdir(path.dirname(item.path), { recursive: true });
167
+ const temporary = `${item.path}.tmp-${process.pid}`;
168
+ await rm(temporary, { recursive: true, force: true });
169
+ await cp(sourceSkill, temporary, { recursive: true });
170
+ if (options.force)
171
+ await rm(item.path, { recursive: true, force: true });
172
+ await rename(temporary, item.path);
173
+ }
174
+ return planned;
175
+ }
176
+ export async function removeSkill(requested = "auto", options = {}) {
177
+ const env = options.env ?? process.env;
178
+ const detections = await detectHarnesses(env);
179
+ const targets = expandedTargets(Array.isArray(requested) ? requested : [requested]);
180
+ const selected = targets === "all"
181
+ ? detections
182
+ : targets === "auto"
183
+ ? detections.filter((item) => item.detected || item.installed)
184
+ : targets.map((id) => detections.find((item) => item.id === id));
185
+ if (!selected.length) {
186
+ throw new Error("No supported coding harnesses detected. Name one explicitly or use `bxc skill remove all --yes`.");
187
+ }
188
+ const destinations = new Map();
189
+ for (const target of selected) {
190
+ const current = destinations.get(target.path);
191
+ if (current)
192
+ current.agents.push(target.label);
193
+ else
194
+ destinations.set(target.path, { agents: [target.label], path: target.path });
195
+ }
196
+ const removals = await Promise.all([...destinations.values()].map(async (item) => {
197
+ if (!await exists(item.path))
198
+ return { ...item, status: "missing" };
199
+ if (!options.force && !await matchesPackagedSkill(item.path)) {
200
+ throw new Error(`${item.path} differs from the packaged skill; pass --force to remove it`);
201
+ }
202
+ return { ...item, status: "removed" };
203
+ }));
204
+ for (const item of removals) {
205
+ if (item.status === "removed")
206
+ await rm(item.path, { recursive: true, force: true });
207
+ }
208
+ return removals;
209
+ }
210
+ export async function readSkill() {
211
+ const { readFile } = await import("node:fs/promises");
212
+ return readFile(path.join(sourceSkill, "SKILL.md"), "utf8");
213
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@boxcompute/cli",
3
+ "version": "0.1.1",
4
+ "description": "Connect local AI agents to BoxCompute sandboxes",
5
+ "keywords": [
6
+ "boxcompute",
7
+ "sandbox",
8
+ "ai-agent",
9
+ "cli"
10
+ ],
11
+ "homepage": "https://app.boxcompute.ai",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/boxcompute/web-agent.git",
15
+ "directory": "apps/cli"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/boxcompute/web-agent/issues"
19
+ },
20
+ "type": "module",
21
+ "bin": {
22
+ "bxc": "dist/cli.js",
23
+ "bcompute": "dist/cli.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "skills"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "lint": "oxlint --max-warnings 0 src test",
38
+ "prepack": "bun run build",
39
+ "test": "bun test",
40
+ "typecheck": "tsc --noEmit -p tsconfig.json"
41
+ },
42
+ "devDependencies": {
43
+ "@types/bun": "latest",
44
+ "@types/node": "^26.0.0",
45
+ "oxlint": "^1.75.0",
46
+ "typescript": "~6.0.2"
47
+ }
48
+ }
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: boxcompute-sandbox
3
+ description: Use the authenticated bxc CLI to run code, data work, builds, tests, or service experiments in an isolated BoxCompute workspace. Use when work needs remote compute or should not run directly on the user's machine; do not use for ordinary local file edits that need no execution.
4
+ ---
5
+
6
+ # BoxCompute Sandbox
7
+
8
+ Use `bxc` as the only interface. Authentication belongs to the human: if
9
+ `bxc doctor` says the client is not authenticated, ask the user to run
10
+ `bxc auth`; never request, read, print, or transmit their saved credential.
11
+
12
+ ## Choose the workspace
13
+
14
+ Run `bxc --json sandboxes` and select the workspace whose name matches the
15
+ task. Do not assume the first result is correct. If no workspace fits, tell the
16
+ user to create one in BoxCompute; the CLI deliberately does not create account
17
+ workspaces.
18
+
19
+ Start or resume it with:
20
+
21
+ ```sh
22
+ bxc --json sandbox start WORKSPACE_ID
23
+ ```
24
+
25
+ The stable workspace ID is also the sandbox ID used by later commands.
26
+
27
+ ## Execute work
28
+
29
+ Prefer argument-vector execution, which avoids a local shell:
30
+
31
+ ```sh
32
+ bxc sandbox exec SANDBOX_ID -- python -m pytest
33
+ bxc sandbox exec SANDBOX_ID --cwd /workspace/project -- npm test
34
+ ```
35
+
36
+ Use `bash -lc` only when the remote operation genuinely needs shell syntax such
37
+ as pipes or redirection. Paths must stay at `/workspace` or below it. Separate
38
+ executions do not share shell variables or a changed working directory, so pass
39
+ `--cwd` and `--env KEY=VALUE` explicitly when needed.
40
+
41
+ Use `--json` when inspecting results programmatically. A non-zero command exit
42
+ is task evidence, not a reason to repeat blindly: read stdout/stderr, correct
43
+ the cause, and then run the revised command. Never send local secrets into the
44
+ sandbox unless the user explicitly places those secrets in scope.
45
+
46
+ ## Lifecycle and safety
47
+
48
+ - Inspect uncertain state with `bxc --json sandbox status SANDBOX_ID`.
49
+ - Sandboxes persist across commands; do not destroy one merely because the
50
+ current task is finished.
51
+ - `bxc sandbox delete SANDBOX_ID --yes` destroys the remote runtime and is
52
+ intentionally explicit. Use it only when the user asks to remove the runtime
53
+ or clearly designated it as disposable. The BoxCompute workspace remains.
54
+ - The CLI does not retry mutating requests. If a connection drops during a
55
+ start or delete, inspect state before deciding what to do next.
56
+
57
+ Summarize which workspace was used, the meaningful command results, and whether
58
+ the sandbox was left running.
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "BoxCompute Sandbox"
3
+ short_description: "Run local AI work safely in BoxCompute"
4
+ default_prompt: "Use $boxcompute-sandbox to run this task in an isolated BoxCompute sandbox."
5
+ brand_color: "#CE704C"