@boxcompute/cli 0.2.1 → 0.2.3

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 CHANGED
@@ -48,6 +48,7 @@ bxc workspaces
48
48
  bxc sandboxes
49
49
  bxc sandbox start WORKSPACE_ID
50
50
  # Use the returned sandbox instance ID for later commands.
51
+ bxc sandbox logs SANDBOX_ID --source execute
51
52
  bxc sandbox exec SANDBOX_ID -- python -m pytest
52
53
  ```
53
54
 
@@ -61,9 +62,11 @@ Deploy the server release with both Sandbox API v1 and v2 before publishing CLI
61
62
  contract. After that, clients need only upgrade the package:
62
63
 
63
64
  ```sh
64
- npm install --global @boxcompute/cli@latest
65
+ bxc update
65
66
  ```
66
67
 
67
- The installation refreshes untouched managed skills before the client's next
68
- agent session. The new CLI uses v2 for multi-instance sandboxes and gives a
69
- server-first upgrade message if it reaches an older deployment.
68
+ `bxc up` is the short alias. If the automatic update cannot invoke npm, use
69
+ `npm install --global @boxcompute/cli@latest` manually. Updating preserves the
70
+ saved BoxCompute credential and refreshes untouched managed skills before the
71
+ client's next agent session. The new CLI uses v2 for multi-instance sandboxes
72
+ and gives a server-first upgrade message if it reaches an older deployment.
package/dist/cli.d.ts CHANGED
@@ -13,6 +13,7 @@ export type CliDependencies = {
13
13
  now?: () => number;
14
14
  sleep?: (milliseconds: number) => Promise<void>;
15
15
  openBrowser?: (url: string) => void;
16
+ installUpdate?: (version: string) => Promise<void>;
16
17
  loadConnection?: (env: NodeJS.ProcessEnv) => Promise<Connection>;
17
18
  loadSavedUrl?: (env: NodeJS.ProcessEnv) => Promise<string | null>;
18
19
  saveConnection?: (url: string, token: string, env: NodeJS.ProcessEnv) => Promise<void>;
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ Usage: bxc [options] [command]
15
15
  Commands:
16
16
 
17
17
  version Print the version number and exit
18
+ update [alias: up] Update the CLI to the latest npm release
18
19
  login Log in through BoxCompute in your browser
19
20
  logout Revoke and remove the saved CLI credential
20
21
  auth [alias: login] Authentication commands
@@ -25,6 +26,7 @@ Commands:
25
26
  sandbox Manage isolated BoxCompute sandboxes
26
27
  start Create and start a workspace sandbox
27
28
  status Inspect one sandbox
29
+ logs Read current or retained sandbox logs
28
30
  exec Execute a program inside a sandbox
29
31
  delete [alias: rm] Destroy the runtime; the workspace remains
30
32
  skill [alias: skills] Manage coding-harness skills
@@ -48,10 +50,12 @@ Login options:
48
50
  Examples:
49
51
 
50
52
  $ bxc login
53
+ $ bxc update
51
54
  $ bxc skill detect
52
55
  $ bxc skill install
53
56
  $ bxc workspaces
54
57
  $ bxc sandbox start WORKSPACE_ID
58
+ $ bxc sandbox logs SANDBOX_ID --source execute
55
59
  $ bxc sandbox exec SANDBOX_ID -- python -m pytest
56
60
 
57
61
  Compatibility:
@@ -66,6 +70,7 @@ Commands:
66
70
 
67
71
  start WORKSPACE_ID Create and start a new sandbox instance
68
72
  status SANDBOX_ID Inspect one sandbox
73
+ logs SANDBOX_ID [options] Read logs without starting the runtime
69
74
  exec SANDBOX_ID [options] -- PROGRAM [ARG...]
70
75
  Execute a program inside a sandbox
71
76
  delete SANDBOX_ID --yes [alias: rm] Destroy the runtime; keep the workspace
@@ -76,6 +81,15 @@ Exec options:
76
81
  --env KEY=VALUE Set an environment variable; repeatable
77
82
  --timeout SECONDS Command timeout
78
83
  --max-output-bytes BYTES Maximum captured output
84
+
85
+ Log options:
86
+
87
+ --since TIMESTAMP Include entries at or after an RFC 3339 time
88
+ --until TIMESTAMP Include entries before an RFC 3339 time
89
+ --stream stdout|stderr Filter by output stream
90
+ --source workload|execute|process
91
+ Filter by log source
92
+ --limit ENTRIES Maximum entries (default: 1000, max: 5000)
79
93
  `;
80
94
  const skillHelp = `Manage coding-harness skills
81
95
 
@@ -133,6 +147,77 @@ export function openBrowser(url, runtime = {}) {
133
147
  // polling so headless shells and restricted WSL interop can authenticate.
134
148
  }
135
149
  }
150
+ function releaseVersion(value) {
151
+ if (typeof value !== "string")
152
+ return null;
153
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
154
+ if (!match)
155
+ return null;
156
+ const parts = match.slice(1).map(Number);
157
+ return parts.every(Number.isSafeInteger) ? parts : null;
158
+ }
159
+ function compareReleaseVersions(left, right) {
160
+ for (let index = 0; index < left.length; index += 1) {
161
+ if (left[index] !== right[index])
162
+ return left[index] - right[index];
163
+ }
164
+ return 0;
165
+ }
166
+ async function installCliUpdate(version) {
167
+ const executable = platform() === "win32" ? "npm.cmd" : "npm";
168
+ await new Promise((resolve, reject) => {
169
+ let child;
170
+ try {
171
+ child = spawn(executable, ["install", "--global", `@boxcompute/cli@${version}`], {
172
+ stdio: ["ignore", "ignore", "inherit"],
173
+ });
174
+ }
175
+ catch (error) {
176
+ reject(error);
177
+ return;
178
+ }
179
+ child.once("error", reject);
180
+ child.once("exit", (code) => {
181
+ if (code === 0)
182
+ resolve();
183
+ else
184
+ reject(new Error(`npm exited with code ${code ?? "unknown"}`));
185
+ });
186
+ });
187
+ }
188
+ async function updateCli(dependencies) {
189
+ let response;
190
+ try {
191
+ response = await dependencies.fetch("https://registry.npmjs.org/%40boxcompute%2Fcli/latest", {
192
+ headers: { accept: "application/json" },
193
+ });
194
+ }
195
+ catch (error) {
196
+ throw new Error(`Could not check npm for updates: ${error?.message ?? String(error)}`);
197
+ }
198
+ if (!response.ok)
199
+ throw new Error(`Could not check npm for updates (HTTP ${response.status})`);
200
+ const latest = (await response.json()).version;
201
+ const currentParts = releaseVersion(CLI_VERSION);
202
+ const latestParts = releaseVersion(latest);
203
+ if (!currentParts || !latestParts || typeof latest !== "string") {
204
+ throw new Error("npm returned an invalid BoxCompute CLI version");
205
+ }
206
+ if (compareReleaseVersions(latestParts, currentParts) <= 0) {
207
+ emit(dependencies.io, dependencies.json, { updated: false, version: CLI_VERSION }, `BoxCompute CLI is already up to date (${CLI_VERSION}).\n`);
208
+ return 0;
209
+ }
210
+ write(dependencies.io.stderr, `Updating BoxCompute CLI from ${CLI_VERSION} to ${latest}…\n`);
211
+ try {
212
+ await dependencies.install(latest);
213
+ }
214
+ catch (error) {
215
+ throw new Error(`Could not install @boxcompute/cli@${latest}: ${error?.message ?? String(error)}. ` +
216
+ `Run \`npm install --global @boxcompute/cli@${latest}\` manually.`);
217
+ }
218
+ emit(dependencies.io, dependencies.json, { updated: true, previousVersion: CLI_VERSION, version: latest }, `Updated BoxCompute CLI to ${latest}.\n`);
219
+ return 0;
220
+ }
136
221
  const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
137
222
  const write = (stream, value) => { stream.write(value); };
138
223
  const emit = (io, json, value, human) => {
@@ -170,6 +255,21 @@ function positive(value, name) {
170
255
  throw new UsageError(`${name} must be a positive integer`);
171
256
  return parsed;
172
257
  }
258
+ function timestamp(value, name) {
259
+ if (value === undefined)
260
+ return undefined;
261
+ const parsed = new Date(value);
262
+ if (Number.isNaN(parsed.valueOf()))
263
+ throw new UsageError(`${name} must be an RFC 3339 timestamp`);
264
+ return parsed.toISOString();
265
+ }
266
+ function oneOf(value, name, values) {
267
+ if (value === undefined)
268
+ return undefined;
269
+ if (!values.includes(value))
270
+ throw new UsageError(`${name} must be one of: ${values.join(", ")}`);
271
+ return value;
272
+ }
173
273
  function environment(tokens) {
174
274
  const values = [];
175
275
  for (;;) {
@@ -265,6 +365,21 @@ function executionOutput(io, json, sandboxId, result) {
265
365
  }
266
366
  return result.exitCode ?? 1;
267
367
  }
368
+ function logsOutput(io, json, logs) {
369
+ if (json) {
370
+ emit(io, true, { logs }, "");
371
+ return 0;
372
+ }
373
+ if (!logs.entries.length)
374
+ write(io.stdout, "No logs found.\n");
375
+ for (const entry of logs.entries) {
376
+ const process = entry.process_id ? ` ${entry.process_id}` : "";
377
+ write(io.stdout, `${entry.timestamp}\t${entry.stream}\t${entry.source}${process}\t${entry.message}\n`);
378
+ }
379
+ write(io.stderr, `sandbox=${logs.sandbox_id} entries=${logs.entries.length} retentionSeconds=${logs.retention_seconds}` +
380
+ `${logs.truncated ? " truncated=true" : ""}\n`);
381
+ return 0;
382
+ }
268
383
  export async function runCli(argv, supplied = {}) {
269
384
  const env = supplied.env ?? process.env;
270
385
  const io = supplied.io ?? { stdout: process.stdout, stderr: process.stderr };
@@ -272,6 +387,7 @@ export async function runCli(argv, supplied = {}) {
272
387
  const now = supplied.now ?? Date.now;
273
388
  const sleep = supplied.sleep ?? delay;
274
389
  const openBrowserImpl = supplied.openBrowser ?? openBrowser;
390
+ const installUpdate = supplied.installUpdate ?? installCliUpdate;
275
391
  const load = supplied.loadConnection ?? loadConnection;
276
392
  const savedUrl = supplied.loadSavedUrl ?? loadSavedUrl;
277
393
  const save = supplied.saveConnection ?? saveConnection;
@@ -297,6 +413,8 @@ export async function runCli(argv, supplied = {}) {
297
413
  let command = args.shift();
298
414
  if (command === "login")
299
415
  command = "auth";
416
+ if (command === "up")
417
+ command = "update";
300
418
  if (command === "skills")
301
419
  command = "skill";
302
420
  if (command === "list" || command === "ls")
@@ -345,6 +463,11 @@ export async function runCli(argv, supplied = {}) {
345
463
  }
346
464
  return authenticate(args, { env, io, json, fetch: fetchImpl, now, sleep, openBrowser: openBrowserImpl, loadSavedUrl: savedUrl, saveConnection: save });
347
465
  }
466
+ if (command === "update") {
467
+ if (args.length)
468
+ throw new UsageError("update takes no options");
469
+ return updateCli({ fetch: fetchImpl, install: installUpdate, io, json });
470
+ }
348
471
  if (command === "skill") {
349
472
  let action = args.shift();
350
473
  if (!action) {
@@ -453,6 +576,20 @@ export async function runCli(argv, supplied = {}) {
453
576
  emit(io, json, { sandbox }, sandboxLine(sandbox));
454
577
  return 0;
455
578
  }
579
+ if (action === "logs") {
580
+ const since = timestamp(option(args, "since"), "--since");
581
+ const until = timestamp(option(args, "until"), "--until");
582
+ const stream = oneOf(option(args, "stream"), "--stream", ["stdout", "stderr"]);
583
+ const source = oneOf(option(args, "source"), "--source", ["workload", "execute", "process"]);
584
+ const limit = positive(option(args, "limit"), "--limit");
585
+ if (limit !== undefined && limit > 5_000)
586
+ throw new UsageError("--limit must be 5000 or fewer");
587
+ if (since && until && since >= until)
588
+ throw new UsageError("--since must be earlier than --until");
589
+ if (args.length)
590
+ throw new UsageError(`Unknown sandbox logs option: ${args[0]}`);
591
+ return logsOutput(io, json, await client.logs(id, { since, until, stream, source, limit }));
592
+ }
456
593
  if (action === "delete") {
457
594
  if (!flag(args, "yes") || args.length)
458
595
  throw new UsageError("sandbox delete requires SANDBOX_ID --yes");
package/dist/client.d.ts CHANGED
@@ -5,6 +5,7 @@ export type Sandbox = {
5
5
  name: string;
6
6
  state: "cold" | "running";
7
7
  runtimeId: string | null;
8
+ retainedRuntimeId: string | null;
8
9
  image: string | null;
9
10
  createdAt: number;
10
11
  lastUsedAt: number | null;
@@ -23,6 +24,20 @@ export type Execution = {
23
24
  stderrTruncated: boolean;
24
25
  wallTimeSeconds: number;
25
26
  };
27
+ export type SandboxLogEntry = {
28
+ timestamp: string;
29
+ stream: "stdout" | "stderr";
30
+ source: "workload" | "execute" | "process";
31
+ message: string;
32
+ pod_uid: string;
33
+ process_id?: string;
34
+ };
35
+ export type SandboxLogs = {
36
+ sandbox_id: string;
37
+ entries: SandboxLogEntry[];
38
+ truncated: boolean;
39
+ retention_seconds: number;
40
+ };
26
41
  export declare class BoxComputeHttpError extends Error {
27
42
  readonly status: number;
28
43
  readonly code?: string | undefined;
@@ -46,5 +61,12 @@ export declare class BoxComputeClient {
46
61
  maxOutputBytes?: number;
47
62
  env?: Record<string, string>;
48
63
  }): Promise<Execution>;
64
+ logs(id: string, input?: {
65
+ since?: string;
66
+ until?: string;
67
+ stream?: "stdout" | "stderr";
68
+ source?: "workload" | "execute" | "process";
69
+ limit?: number;
70
+ }): Promise<SandboxLogs>;
49
71
  delete(id: string): Promise<void>;
50
72
  }
package/dist/client.js CHANGED
@@ -66,6 +66,21 @@ export class BoxComputeClient {
66
66
  body: JSON.stringify(input),
67
67
  })).result;
68
68
  }
69
+ async logs(id, input = {}) {
70
+ const query = new URLSearchParams();
71
+ if (input.since)
72
+ query.set("since", input.since);
73
+ if (input.until)
74
+ query.set("until", input.until);
75
+ if (input.stream)
76
+ query.set("stream", input.stream);
77
+ if (input.source)
78
+ query.set("source", input.source);
79
+ if (input.limit !== undefined)
80
+ query.set("limit", String(input.limit));
81
+ const suffix = query.size ? `?${query}` : "";
82
+ return (await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}/logs${suffix}`)).logs;
83
+ }
69
84
  async delete(id) {
70
85
  await this.request(`/api/v2/sandboxes/${encodeURIComponent(id)}`, { method: "DELETE" });
71
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcompute/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Connect local AI agents to BoxCompute sandboxes",
5
5
  "keywords": [
6
6
  "boxcompute",
@@ -44,6 +44,11 @@ sandbox unless the user explicitly places those secrets in scope.
44
44
  ## Lifecycle and safety
45
45
 
46
46
  - Inspect uncertain state with `bxc --json sandbox status SANDBOX_ID`.
47
+ - Read runtime output with `bxc sandbox logs SANDBOX_ID`. Add `--json` for
48
+ structured entries or narrow results with `--since`, `--until`, `--stream`,
49
+ `--source`, and `--limit`. Log reads do not start stopped compute, and remain
50
+ available for the provider's nominal 30-day retention period after deletion;
51
+ storage pressure may shorten that period, so logs are not an archive.
47
52
  - Sandboxes persist across commands; do not destroy one merely because the
48
53
  current task is finished.
49
54
  - `bxc sandbox delete SANDBOX_ID --yes` destroys the remote runtime and is