skydive-cli 0.1.0-beta.353 → 0.1.0-beta.363

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/js/bin.mjs CHANGED
@@ -14,9 +14,10 @@ import { createHash } from "node:crypto";
14
14
  import fs from "node:fs";
15
15
  import zlib from "node:zlib";
16
16
  import os from "node:os";
17
+ import { WebSocket } from "ws";
17
18
 
18
19
  //#region package.json
19
- var version$1 = "0.1.0-beta.353";
20
+ var version$1 = "0.1.0-beta.363";
20
21
 
21
22
  //#endregion
22
23
  //#region src/types.ts
@@ -838,6 +839,10 @@ const authCommand = {
838
839
 
839
840
  //#endregion
840
841
  //#region src/chat/api/rest.ts
842
+ var rest_exports = /* @__PURE__ */ __exportAll({
843
+ HttpError: () => HttpError,
844
+ createRestClient: () => createRestClient
845
+ });
841
846
  var HttpError = class extends Error {
842
847
  constructor(status, body) {
843
848
  super(`HTTP ${status}: ${body.slice(0, 200)}`);
@@ -2611,7 +2616,7 @@ const chatCommand = {
2611
2616
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2612
2617
  process.exit(1);
2613
2618
  }
2614
- const { runChat } = await import("./boot-DLHvjtxn.mjs");
2619
+ const { runChat } = await import("./boot-C1WUzzUo.mjs");
2615
2620
  await runChat({
2616
2621
  appUrl,
2617
2622
  sessionToken: session.value.sessionToken,
@@ -2653,7 +2658,7 @@ async function runPrintMode({ argv, appUrl }) {
2653
2658
  }
2654
2659
  let machineShare = null;
2655
2660
  if (resolveShareMachine(argv)) {
2656
- const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
2661
+ const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
2657
2662
  let signalConnected;
2658
2663
  const connected = new Promise((resolve) => {
2659
2664
  signalConnected = resolve;
@@ -3405,7 +3410,7 @@ const switchCommand = {
3405
3410
  printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
3406
3411
  process.exit(1);
3407
3412
  }
3408
- const { runWorkspacePicker } = await import("./boot-DLHvjtxn.mjs");
3413
+ const { runWorkspacePicker } = await import("./boot-C1WUzzUo.mjs");
3409
3414
  await runWorkspacePicker(session);
3410
3415
  return;
3411
3416
  }
@@ -3622,7 +3627,7 @@ const openCommand = {
3622
3627
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
3623
3628
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
3624
3629
  const { machineName } = machineIdentity();
3625
- const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
3630
+ const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
3626
3631
  let lastLine = "";
3627
3632
  let signalConnected;
3628
3633
  const connected = new Promise((resolve) => {
@@ -3753,10 +3758,298 @@ const portalCommand = {
3753
3758
  handler: () => {}
3754
3759
  };
3755
3760
 
3761
+ //#endregion
3762
+ //#region ../sandbox-stream-protocol/src/index.ts
3763
+ const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
3764
+ const FRAME = {
3765
+ DATA: 1,
3766
+ EXIT: 2,
3767
+ ERROR: 3,
3768
+ INPUT: 16,
3769
+ RESIZE: 17
3770
+ };
3771
+ const MAX_INPUT_BYTES = 1 * 1024 * 1024;
3772
+ /** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
3773
+ function streamSpecToQuery(spec) {
3774
+ if (spec.mode === "pty") return {
3775
+ agentId: spec.agentId,
3776
+ mode: "pty",
3777
+ cols: String(spec.cols),
3778
+ rows: String(spec.rows)
3779
+ };
3780
+ return {
3781
+ agentId: spec.agentId,
3782
+ mode: "exec",
3783
+ command: spec.command
3784
+ };
3785
+ }
3786
+ function withType(type, payload) {
3787
+ const frame = new Uint8Array(1 + payload.length);
3788
+ frame[0] = type;
3789
+ frame.set(payload, 1);
3790
+ return frame;
3791
+ }
3792
+ /** client → server: keystroke bytes for the pty stdin. */
3793
+ function encodeInput(data) {
3794
+ return withType(FRAME.INPUT, data);
3795
+ }
3796
+ /** client → server: the client terminal was resized. */
3797
+ function encodeResize(cols, rows) {
3798
+ const frame = new Uint8Array(5);
3799
+ frame[0] = FRAME.RESIZE;
3800
+ const view = new DataView(frame.buffer);
3801
+ view.setUint16(1, cols & 65535);
3802
+ view.setUint16(3, rows & 65535);
3803
+ return frame;
3804
+ }
3805
+ const view = (frame) => new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
3806
+ /**
3807
+ * Decode a frame the server sent. Returns null for an empty, unknown, or
3808
+ * truncated frame — a peer speaking a newer protocol must not crash us.
3809
+ */
3810
+ function decodeServerFrame(frame) {
3811
+ const payload = frame.subarray(1);
3812
+ switch (frame[0]) {
3813
+ case FRAME.DATA: return {
3814
+ type: "data",
3815
+ payload
3816
+ };
3817
+ case FRAME.EXIT: return {
3818
+ type: "exit",
3819
+ code: payload.length >= 4 ? view(frame).getInt32(1) : 0
3820
+ };
3821
+ case FRAME.ERROR: return {
3822
+ type: "error",
3823
+ message: new TextDecoder().decode(payload)
3824
+ };
3825
+ default: return null;
3826
+ }
3827
+ }
3828
+
3829
+ //#endregion
3830
+ //#region src/chat/sandbox/client.ts
3831
+ function wsBase(appUrl) {
3832
+ const base = appUrl.replace(/\/+$/, "");
3833
+ if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
3834
+ if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
3835
+ return `wss://${base}`;
3836
+ }
3837
+ /**
3838
+ * A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
3839
+ * the write side (keystrokes / resize for pty mode) and teardown.
3840
+ */
3841
+ var SandboxStream = class SandboxStream {
3842
+ ws;
3843
+ closed = false;
3844
+ constructor(ws, onEvent) {
3845
+ this.ws = ws;
3846
+ let ended = false;
3847
+ const emitEnd = (event) => {
3848
+ if (ended) return;
3849
+ ended = true;
3850
+ onEvent(event);
3851
+ };
3852
+ ws.on("message", (data, isBinary) => {
3853
+ if (!isBinary) return;
3854
+ const frame = decodeServerFrame(toBuffer(data));
3855
+ if (!frame) return;
3856
+ switch (frame.type) {
3857
+ case "data":
3858
+ onEvent({
3859
+ type: "data",
3860
+ bytes: new Uint8Array(frame.payload)
3861
+ });
3862
+ break;
3863
+ case "exit":
3864
+ emitEnd({
3865
+ type: "exit",
3866
+ code: frame.code
3867
+ });
3868
+ break;
3869
+ case "error":
3870
+ emitEnd({
3871
+ type: "error",
3872
+ message: frame.message
3873
+ });
3874
+ break;
3875
+ }
3876
+ });
3877
+ let failure = null;
3878
+ ws.on("error", (err) => {
3879
+ failure = err.message;
3880
+ });
3881
+ ws.on("close", () => {
3882
+ this.closed = true;
3883
+ emitEnd({
3884
+ type: "close",
3885
+ failure
3886
+ });
3887
+ });
3888
+ }
3889
+ /** Feed keystroke bytes to the pty stdin. */
3890
+ sendInput(data) {
3891
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
3892
+ this.ws.send(encodeInput(data));
3893
+ }
3894
+ /** Notify the pty of a terminal resize. */
3895
+ resize(cols, rows) {
3896
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
3897
+ this.ws.send(encodeResize(cols, rows));
3898
+ }
3899
+ close() {
3900
+ this.closed = true;
3901
+ this.ws.close();
3902
+ }
3903
+ /** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
3904
+ static open(opts) {
3905
+ const spec = opts.mode === "pty" ? {
3906
+ mode: "pty",
3907
+ agentId: opts.agentId,
3908
+ cols: opts.cols,
3909
+ rows: opts.rows
3910
+ } : {
3911
+ mode: "exec",
3912
+ agentId: opts.agentId,
3913
+ command: opts.command
3914
+ };
3915
+ const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
3916
+ for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
3917
+ return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
3918
+ }
3919
+ };
3920
+ function toBuffer(data) {
3921
+ if (Buffer.isBuffer(data)) return data;
3922
+ if (Array.isArray(data)) return Buffer.concat(data);
3923
+ return Buffer.from(data);
3924
+ }
3925
+
3926
+ //#endregion
3927
+ //#region src/commands/sandbox.ts
3928
+ /** POSIX single-quote one word so the remote shell treats it as one token. */
3929
+ function shellQuote(word) {
3930
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word)) return word;
3931
+ return `'${word.replace(/'/g, `'\\''`)}'`;
3932
+ }
3933
+ /**
3934
+ * Serialize the command words into the single shell string the relay runs.
3935
+ *
3936
+ * Words arrive already split by the caller's shell, so each must be re-quoted
3937
+ * or the remote shell re-splits any word containing spaces or metacharacters:
3938
+ * `sandbox -- sh -c 'echo hi; whoami'` would otherwise run `sh -c echo hi`
3939
+ * and then, separately, `whoami`. A pipeline still works the explicit way —
3940
+ * `sandbox -- sh -c 'ls | wc -l'` — which is what `docker`/`kubectl exec` ask
3941
+ * for too. yargs number-coerces bare numerals, so words are stringified.
3942
+ */
3943
+ function joinCommandWords(words) {
3944
+ return words.map(String).map(shellQuote).join(" ").trim();
3945
+ }
3946
+ /**
3947
+ * `skydive sandbox` — the standalone counterpart to the chat TUI's `/sandbox`
3948
+ * composer command (ANY-4928): a live terminal (or one-shot exec) in the
3949
+ * agent's own sandbox without opening the TUI. Runs under Node (no Bun/
3950
+ * OpenTUI): the PTY is a raw byte passthrough on the caller's real terminal.
3951
+ * The relay gates on EDIT access to the agent and the sandbox-terminal-enabled
3952
+ * kill switch, and boots the sandbox when it isn't running.
3953
+ */
3954
+ const sandboxCommand = {
3955
+ command: "sandbox [command..]",
3956
+ describe: "Open a live terminal in an agent's sandbox, or run a one-shot command there",
3957
+ builder: (y) => y.option("agent", {
3958
+ type: "string",
3959
+ describe: "Target agent, by id, slug, or name. Optional when the account has exactly one agent."
3960
+ }).positional("command", {
3961
+ type: "string",
3962
+ array: true,
3963
+ describe: "Command to run one-shot (streams output, exits with its exit code). Omit for a live interactive terminal. Put it after `--` if it has flags of its own."
3964
+ }).example("skydive sandbox --agent grace", "Live terminal (Ctrl-] detaches)").example("skydive sandbox --agent grace -- tail -n 50 /tmp/harness.log", "One-shot command (use `--` so its flags reach the sandbox)").example("skydive sandbox --agent grace -- sh -c 'ls /tmp | wc -l'", "Shell features go through an explicit `sh -c`"),
3965
+ handler: async (argv) => {
3966
+ const session = requireSession(argv);
3967
+ const { createRestClient } = await Promise.resolve().then(() => rest_exports);
3968
+ const { resolveAgent } = await Promise.resolve().then(() => print_exports);
3969
+ const client = createRestClient({
3970
+ appUrl: session.appUrl,
3971
+ sessionToken: session.sessionToken
3972
+ });
3973
+ let agent;
3974
+ try {
3975
+ agent = resolveAgent(await client.listAgents({
3976
+ scope: "org",
3977
+ onPage: null
3978
+ }), argv.agent ?? null);
3979
+ } catch (error) {
3980
+ printError(error instanceof Error ? error.message : String(error));
3981
+ process.exit(1);
3982
+ }
3983
+ const command = joinCommandWords([...argv.command ?? [], ...argv["--"] ?? []]);
3984
+ const code = command ? await runExec({
3985
+ session,
3986
+ agentId: agent.id,
3987
+ command
3988
+ }) : await runPty({
3989
+ session,
3990
+ agentId: agent.id,
3991
+ agentName: agent.name
3992
+ });
3993
+ process.exit(code);
3994
+ }
3995
+ };
3996
+ /** One-shot exec: stream output to stdout, resolve to the command's exit code. */
3997
+ function runExec({ session, agentId, command }) {
3998
+ console.error("Connecting to the sandbox…");
3999
+ return new Promise((resolve) => {
4000
+ const finish = (code) => {
4001
+ process.stdout.write("", () => resolve(code));
4002
+ };
4003
+ SandboxStream.open({
4004
+ mode: "exec",
4005
+ appUrl: session.appUrl,
4006
+ sessionToken: session.sessionToken,
4007
+ agentId,
4008
+ command,
4009
+ onEvent: (e) => {
4010
+ switch (e.type) {
4011
+ case "data":
4012
+ process.stdout.write(e.bytes);
4013
+ break;
4014
+ case "exit":
4015
+ finish(e.code);
4016
+ break;
4017
+ case "error":
4018
+ printError(e.message);
4019
+ finish(1);
4020
+ break;
4021
+ case "close":
4022
+ printError(e.failure ? `Could not run the command in the sandbox: ${e.failure}` : "The connection to the sandbox closed before the command finished.");
4023
+ finish(1);
4024
+ break;
4025
+ }
4026
+ }
4027
+ });
4028
+ });
4029
+ }
4030
+ /** Live terminal on the caller's real TTY. */
4031
+ async function runPty({ session, agentId, agentName }) {
4032
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
4033
+ printError("A live sandbox terminal needs an interactive TTY. For scripted use, pass a command: `skydive sandbox -- <cmd>`.");
4034
+ return 1;
4035
+ }
4036
+ console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
4037
+ const { runRawPtyPassthrough } = await import("./raw-pty-C1DXKms6.mjs").then((n) => n.t);
4038
+ const result = await runRawPtyPassthrough({
4039
+ stdin: process.stdin,
4040
+ stdout: process.stdout,
4041
+ appUrl: session.appUrl,
4042
+ sessionToken: session.sessionToken,
4043
+ agentId
4044
+ });
4045
+ if (result.reason === "detach") console.error("\nDetached.");
4046
+ return result.code;
4047
+ }
4048
+
3756
4049
  //#endregion
3757
4050
  //#region src/cli.ts
3758
4051
  function createCli(argv) {
3759
- return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
4052
+ return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").parserConfiguration({ "populate--": true }).example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
3760
4053
  type: "boolean",
3761
4054
  default: false,
3762
4055
  global: true,
@@ -3770,7 +4063,7 @@ function createCli(argv) {
3770
4063
  type: "string",
3771
4064
  global: true,
3772
4065
  describe: "Override API base URL"
3773
- }).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
4066
+ }).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
3774
4067
  printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
3775
4068
  process.exit(1);
3776
4069
  });
@@ -3844,4 +4137,4 @@ function resolveArgv(args, tty = {
3844
4137
  createCli(resolveArgv(hideBin(process.argv))).parse();
3845
4138
 
3846
4139
  //#endregion
3847
- export { createRestClient as A, theme as C, themeVersion as D, themeModeFromColorFgBg as E, DEFAULT_APP_URL as F, getConfigPath as I, getSavedTheme as L, listWorkspaces as M, setActiveWorkspace as N, themesForMode as O, DEFAULT_API_URL as P, resolveWebUrl as R, noColorRequested as S, themeMode as T, isRecord as _, buildEnv as a, findTheme as b, resolveAgent as c, parseExternalOauthConnectParams as d, parseOauthConnectParams as f, errorMessage as g, parseConnectCard as h, mintPortalDeviceToken as i, getActiveWorkspaceId as j, HttpError as k, MASK_CHAR as l, resolveConnectUrl as m, findThisDevice as n, machineIdentity as o, reconcileMaskedInput as p, grantPortalAccess as r, portalWsUrl as s, fetchPortalDevices as t, cardActionErrorMessage as u, DEFAULT_THEME_ID as v, themeForMode as w, monoTheme as x, applyTheme as y, saveTheme as z };
4140
+ export { HttpError as A, saveTheme as B, noColorRequested as C, themeModeFromColorFgBg as D, themeMode as E, DEFAULT_API_URL as F, DEFAULT_APP_URL as I, getConfigPath as L, getActiveWorkspaceId as M, listWorkspaces as N, themeVersion as O, setActiveWorkspace as P, getSavedTheme as R, monoTheme as S, themeForMode as T, errorMessage as _, mintPortalDeviceToken as a, applyTheme as b, portalWsUrl as c, cardActionErrorMessage as d, parseExternalOauthConnectParams as f, parseConnectCard as g, resolveConnectUrl as h, grantPortalAccess as i, createRestClient as j, themesForMode as k, resolveAgent as l, reconcileMaskedInput as m, fetchPortalDevices as n, buildEnv as o, parseOauthConnectParams as p, findThisDevice as r, machineIdentity as s, SandboxStream as t, MASK_CHAR as u, isRecord as v, theme as w, findTheme as x, DEFAULT_THEME_ID as y, resolveWebUrl as z };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { A as createRestClient, C as theme, D as themeVersion, E as themeModeFromColorFgBg, F as DEFAULT_APP_URL, I as getConfigPath, L as getSavedTheme, M as listWorkspaces, N as setActiveWorkspace, O as themesForMode, P as DEFAULT_API_URL, R as resolveWebUrl, S as noColorRequested, T as themeMode, _ as isRecord, b as findTheme, c as resolveAgent, d as parseExternalOauthConnectParams, f as parseOauthConnectParams, g as errorMessage, h as parseConnectCard, j as getActiveWorkspaceId, k as HttpError, l as MASK_CHAR, m as resolveConnectUrl, p as reconcileMaskedInput, u as cardActionErrorMessage, v as DEFAULT_THEME_ID, w as themeForMode, x as monoTheme, y as applyTheme, z as saveTheme } from "./bin.mjs";
3
- import { t as PortalClient } from "./client-_OL8-XGH.mjs";
2
+ import { A as HttpError, B as saveTheme, C as noColorRequested, D as themeModeFromColorFgBg, E as themeMode, F as DEFAULT_API_URL, I as DEFAULT_APP_URL, L as getConfigPath, M as getActiveWorkspaceId, N as listWorkspaces, O as themeVersion, P as setActiveWorkspace, R as getSavedTheme, S as monoTheme, T as themeForMode, _ as errorMessage, b as applyTheme, d as cardActionErrorMessage, f as parseExternalOauthConnectParams, g as parseConnectCard, h as resolveConnectUrl, j as createRestClient, k as themesForMode, l as resolveAgent, m as reconcileMaskedInput, p as parseOauthConnectParams, t as SandboxStream, u as MASK_CHAR, v as isRecord, w as theme, x as findTheme, y as DEFAULT_THEME_ID, z as resolveWebUrl } from "./bin.mjs";
3
+ import { t as PortalClient } from "./client-XFsd0Wy9.mjs";
4
+ import { n as runRawPtyPassthrough } from "./raw-pty-C1DXKms6.mjs";
4
5
  import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
5
6
  import { z } from "zod";
6
7
  import open from "open";
@@ -13,7 +14,6 @@ import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, d
13
14
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
14
15
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
15
16
  import { create } from "zustand";
16
- import { WebSocket } from "ws";
17
17
  import { createConnection } from "node:net";
18
18
  import { access, appendFile, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
19
19
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
@@ -5284,255 +5284,30 @@ function formatShellContext(command, output, exitCode) {
5284
5284
  ].join("\n");
5285
5285
  }
5286
5286
 
5287
- //#endregion
5288
- //#region ../sandbox-stream-protocol/src/index.ts
5289
- const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
5290
- const FRAME = {
5291
- DATA: 1,
5292
- EXIT: 2,
5293
- ERROR: 3,
5294
- INPUT: 16,
5295
- RESIZE: 17
5296
- };
5297
- const MAX_INPUT_BYTES = 1 * 1024 * 1024;
5298
- /** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
5299
- function streamSpecToQuery(spec) {
5300
- if (spec.mode === "pty") return {
5301
- agentId: spec.agentId,
5302
- mode: "pty",
5303
- cols: String(spec.cols),
5304
- rows: String(spec.rows)
5305
- };
5306
- return {
5307
- agentId: spec.agentId,
5308
- mode: "exec",
5309
- command: spec.command
5310
- };
5311
- }
5312
- function withType(type, payload) {
5313
- const frame = new Uint8Array(1 + payload.length);
5314
- frame[0] = type;
5315
- frame.set(payload, 1);
5316
- return frame;
5317
- }
5318
- /** client → server: keystroke bytes for the pty stdin. */
5319
- function encodeInput(data) {
5320
- return withType(FRAME.INPUT, data);
5321
- }
5322
- /** client → server: the client terminal was resized. */
5323
- function encodeResize(cols, rows) {
5324
- const frame = new Uint8Array(5);
5325
- frame[0] = FRAME.RESIZE;
5326
- const view = new DataView(frame.buffer);
5327
- view.setUint16(1, cols & 65535);
5328
- view.setUint16(3, rows & 65535);
5329
- return frame;
5330
- }
5331
- const view = (frame) => new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
5332
- /**
5333
- * Decode a frame the server sent. Returns null for an empty, unknown, or
5334
- * truncated frame — a peer speaking a newer protocol must not crash us.
5335
- */
5336
- function decodeServerFrame(frame) {
5337
- const payload = frame.subarray(1);
5338
- switch (frame[0]) {
5339
- case FRAME.DATA: return {
5340
- type: "data",
5341
- payload
5342
- };
5343
- case FRAME.EXIT: return {
5344
- type: "exit",
5345
- code: payload.length >= 4 ? view(frame).getInt32(1) : 0
5346
- };
5347
- case FRAME.ERROR: return {
5348
- type: "error",
5349
- message: new TextDecoder().decode(payload)
5350
- };
5351
- default: return null;
5352
- }
5353
- }
5354
-
5355
- //#endregion
5356
- //#region src/chat/sandbox/client.ts
5357
- function wsBase(appUrl) {
5358
- const base = appUrl.replace(/\/+$/, "");
5359
- if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
5360
- if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
5361
- return `wss://${base}`;
5362
- }
5363
- /**
5364
- * A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
5365
- * the write side (keystrokes / resize for pty mode) and teardown.
5366
- */
5367
- var SandboxStream = class SandboxStream {
5368
- ws;
5369
- closed = false;
5370
- constructor(ws, onEvent) {
5371
- this.ws = ws;
5372
- ws.on("message", (data, isBinary) => {
5373
- if (!isBinary) return;
5374
- const frame = decodeServerFrame(toBuffer(data));
5375
- if (!frame) return;
5376
- switch (frame.type) {
5377
- case "data":
5378
- onEvent({
5379
- type: "data",
5380
- bytes: new Uint8Array(frame.payload)
5381
- });
5382
- break;
5383
- case "exit":
5384
- onEvent({
5385
- type: "exit",
5386
- code: frame.code
5387
- });
5388
- break;
5389
- case "error":
5390
- onEvent({
5391
- type: "error",
5392
- message: frame.message
5393
- });
5394
- break;
5395
- }
5396
- });
5397
- ws.on("close", () => {
5398
- this.closed = true;
5399
- onEvent({ type: "close" });
5400
- });
5401
- ws.on("error", () => {});
5402
- }
5403
- /** Feed keystroke bytes to the pty stdin. */
5404
- sendInput(data) {
5405
- if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
5406
- this.ws.send(encodeInput(data));
5407
- }
5408
- /** Notify the pty of a terminal resize. */
5409
- resize(cols, rows) {
5410
- if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
5411
- this.ws.send(encodeResize(cols, rows));
5412
- }
5413
- close() {
5414
- this.closed = true;
5415
- this.ws.close();
5416
- }
5417
- /** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
5418
- static open(opts) {
5419
- const spec = opts.mode === "pty" ? {
5420
- mode: "pty",
5421
- agentId: opts.agentId,
5422
- cols: opts.cols,
5423
- rows: opts.rows
5424
- } : {
5425
- mode: "exec",
5426
- agentId: opts.agentId,
5427
- command: opts.command
5428
- };
5429
- const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
5430
- for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
5431
- return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
5432
- }
5433
- };
5434
- function toBuffer(data) {
5435
- if (Buffer.isBuffer(data)) return data;
5436
- if (Array.isArray(data)) return Buffer.concat(data);
5437
- return Buffer.from(data);
5438
- }
5439
-
5440
5287
  //#endregion
5441
5288
  //#region src/chat/sandbox/pty-session.ts
5442
5289
  /**
5443
5290
  * Runs a live interactive PTY into the agent's sandbox as a full-screen raw
5444
- * terminal, by SUSPENDING the opentui renderer for the duration and doing a
5445
- * direct byte passthrough:
5446
- *
5447
- * local stdin → INPUT frames sandbox pty
5448
- * sandbox pty → DATA frames → local stdout
5449
- *
5450
- * This is the correct shape for a real terminal: we don't reimplement a
5451
- * terminal emulator, we hand the actual TTY to the remote shell. On exit
5452
- * (the shell exits, the socket drops, or the user hits the detach key) we
5453
- * restore the terminal and resume the TUI.
5454
- *
5455
- * Detach key: Ctrl-] (0x1d), the classic telnet/ssh escape — leaves the shell
5456
- * running server-side is NOT a goal here; detaching closes the session.
5291
+ * terminal from inside the chat TUI, by SUSPENDING the opentui renderer for
5292
+ * the duration (restoring the normal screen buffer and releasing the terminal
5293
+ * and its input to us) and handing the TTY to the shared raw passthrough
5294
+ * (raw-pty.ts). On exit (the shell exits, the socket drops, or the user hits
5295
+ * Ctrl-]) the terminal is restored and the TUI resumes.
5457
5296
  */
5458
- const DETACH_BYTE = 29;
5459
5297
  async function runPtySession(opts) {
5460
5298
  const { renderer } = opts;
5461
- const stdin = renderer.stdin;
5462
- const stdout = process.stdout;
5463
- const size = () => ({
5464
- cols: stdout.columns ?? 80,
5465
- rows: stdout.rows ?? 24
5466
- });
5467
5299
  renderer.suspend();
5468
- return await new Promise((resolve) => {
5469
- let settled = false;
5470
- const initial = size();
5471
- const stream = SandboxStream.open({
5472
- mode: "pty",
5300
+ try {
5301
+ return await runRawPtyPassthrough({
5302
+ stdin: renderer.stdin,
5303
+ stdout: process.stdout,
5473
5304
  appUrl: opts.appUrl,
5474
5305
  sessionToken: opts.sessionToken,
5475
- agentId: opts.agentId,
5476
- cols: initial.cols,
5477
- rows: initial.rows,
5478
- onEvent: (e) => {
5479
- switch (e.type) {
5480
- case "data":
5481
- stdout.write(e.bytes);
5482
- break;
5483
- case "error":
5484
- stdout.write(`\r\n\x1b[31m${e.message}\x1b[0m\r\n`);
5485
- finish({
5486
- reason: "error",
5487
- code: 1
5488
- });
5489
- break;
5490
- case "exit":
5491
- finish({
5492
- reason: "exit",
5493
- code: e.code
5494
- });
5495
- break;
5496
- case "close":
5497
- finish({
5498
- reason: "exit",
5499
- code: 0
5500
- });
5501
- break;
5502
- }
5503
- }
5306
+ agentId: opts.agentId
5504
5307
  });
5505
- const onStdin = (chunk) => {
5506
- if (chunk.length === 1 && chunk[0] === DETACH_BYTE) {
5507
- finish({
5508
- reason: "detach",
5509
- code: 0
5510
- });
5511
- return;
5512
- }
5513
- stream.sendInput(new Uint8Array(chunk));
5514
- };
5515
- const onResize = () => {
5516
- const s = size();
5517
- stream.resize(s.cols, s.rows);
5518
- };
5519
- const wasRaw = stdin.isRaw ?? false;
5520
- stdin.setRawMode?.(true);
5521
- stdin.resume();
5522
- stdin.on("data", onStdin);
5523
- stdout.on("resize", onResize);
5524
- stream.resize(initial.cols, initial.rows);
5525
- function finish(result) {
5526
- if (settled) return;
5527
- settled = true;
5528
- stdin.off("data", onStdin);
5529
- stdout.off("resize", onResize);
5530
- stdin.setRawMode?.(wasRaw);
5531
- stream.close();
5532
- renderer.resume();
5533
- resolve(result);
5534
- }
5535
- });
5308
+ } finally {
5309
+ renderer.resume();
5310
+ }
5536
5311
  }
5537
5312
 
5538
5313
  //#endregion
@@ -6142,6 +5917,10 @@ function ChatScreen({ agent, conversation }) {
6142
5917
  append(`\n${e.message}`);
6143
5918
  stamp(1);
6144
5919
  } else if (e.type === "exit") stamp(e.code);
5920
+ else if (e.type === "close") {
5921
+ append(`\n${e.failure ? `connection failed: ${e.failure}` : "connection closed before the command finished"}`);
5922
+ stamp(1);
5923
+ }
6145
5924
  }
6146
5925
  });
6147
5926
  }, [
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as __exportAll } from "./rolldown-runtime-Cz4Tg37Z.mjs";
3
- import { a as buildEnv, g as errorMessage, i as mintPortalDeviceToken, n as findThisDevice, o as machineIdentity, r as grantPortalAccess, s as portalWsUrl, t as fetchPortalDevices } from "./bin.mjs";
3
+ import { _ as errorMessage, a as mintPortalDeviceToken, c as portalWsUrl, i as grantPortalAccess, n as fetchPortalDevices, o as buildEnv, r as findThisDevice, s as machineIdentity } from "./bin.mjs";
4
4
  import { z } from "zod";
5
5
  import { spawn } from "node:child_process";
6
6
  import { WebSocket } from "ws";
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { t as __exportAll } from "./rolldown-runtime-Cz4Tg37Z.mjs";
3
+ import { t as SandboxStream } from "./bin.mjs";
4
+
5
+ //#region src/chat/sandbox/raw-pty.ts
6
+ var raw_pty_exports = /* @__PURE__ */ __exportAll({ runRawPtyPassthrough: () => runRawPtyPassthrough });
7
+ /**
8
+ * The transport-and-TTY core of a live sandbox terminal: direct byte
9
+ * passthrough between a raw local TTY and the remote pty.
10
+ *
11
+ * local stdin → INPUT frames → sandbox pty
12
+ * sandbox pty → DATA frames → local stdout
13
+ *
14
+ * This is the correct shape for a real terminal: we don't reimplement a
15
+ * terminal emulator, we hand the actual TTY to the remote shell. Callers own
16
+ * the surrounding lifecycle — the chat TUI suspends/resumes its renderer
17
+ * around this (pty-session.ts), the standalone `skydive sandbox` command runs
18
+ * it bare.
19
+ *
20
+ * Detach key: Ctrl-] (0x1d), the classic telnet/ssh escape. Leaving the shell
21
+ * running server-side is NOT a goal here; detaching closes the session.
22
+ */
23
+ const DETACH_BYTE = 29;
24
+ async function runRawPtyPassthrough(opts) {
25
+ const { stdin, stdout } = opts;
26
+ const size = () => ({
27
+ cols: stdout.columns ?? 80,
28
+ rows: stdout.rows ?? 24
29
+ });
30
+ return await new Promise((resolve) => {
31
+ let settled = false;
32
+ const initial = size();
33
+ const stream = SandboxStream.open({
34
+ mode: "pty",
35
+ appUrl: opts.appUrl,
36
+ sessionToken: opts.sessionToken,
37
+ agentId: opts.agentId,
38
+ cols: initial.cols,
39
+ rows: initial.rows,
40
+ onEvent: (e) => {
41
+ switch (e.type) {
42
+ case "data":
43
+ stdout.write(e.bytes);
44
+ break;
45
+ case "error":
46
+ stdout.write(`\r\n\x1b[31m${e.message}\x1b[0m\r\n`);
47
+ finish({
48
+ reason: "error",
49
+ code: 1
50
+ });
51
+ break;
52
+ case "exit":
53
+ finish({
54
+ reason: "exit",
55
+ code: e.code
56
+ });
57
+ break;
58
+ case "close":
59
+ stdout.write(`\r\n\x1b[31m${e.failure ? `Could not open the sandbox terminal: ${e.failure}` : "The sandbox terminal connection closed unexpectedly."}\x1b[0m\r\n`);
60
+ finish({
61
+ reason: "error",
62
+ code: 1
63
+ });
64
+ break;
65
+ }
66
+ }
67
+ });
68
+ const onStdin = (chunk) => {
69
+ if (chunk.length === 1 && chunk[0] === DETACH_BYTE) {
70
+ finish({
71
+ reason: "detach",
72
+ code: 0
73
+ });
74
+ return;
75
+ }
76
+ stream.sendInput(new Uint8Array(chunk));
77
+ };
78
+ const onResize = () => {
79
+ const s = size();
80
+ stream.resize(s.cols, s.rows);
81
+ };
82
+ const wasRaw = stdin.isRaw ?? false;
83
+ stdin.setRawMode?.(true);
84
+ stdin.resume();
85
+ stdin.on("data", onStdin);
86
+ stdout.on("resize", onResize);
87
+ stream.resize(initial.cols, initial.rows);
88
+ function finish(result) {
89
+ if (settled) return;
90
+ settled = true;
91
+ stdin.off("data", onStdin);
92
+ stdout.off("resize", onResize);
93
+ stdin.setRawMode?.(wasRaw);
94
+ stream.close();
95
+ resolve(result);
96
+ }
97
+ });
98
+ }
99
+
100
+ //#endregion
101
+ export { runRawPtyPassthrough as n, raw_pty_exports as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.353",
3
+ "version": "0.1.0-beta.363",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",