skydive-cli 0.1.0-beta.351 → 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.351";
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)}`);
@@ -1033,15 +1038,6 @@ function createRestClient({ appUrl, sessionToken }) {
1033
1038
  ...input,
1034
1039
  clientSurface
1035
1040
  }, sendResultSchema),
1036
- activeRun: async ({ conversationId }) => {
1037
- const { run } = await get(`/api/v1/chat/active-run?${new URLSearchParams({ conversationId }).toString()}`, activeRunResponseSchema);
1038
- if (!run) return null;
1039
- return {
1040
- runId: run.runId,
1041
- agentId: run.agentId ?? "",
1042
- agentName: run.agentName ?? ""
1043
- };
1044
- },
1045
1041
  cancelRun: async ({ runId }) => {
1046
1042
  await post(`/api/v1/chat/runs/${encodeURIComponent(runId)}/cancel`, {}, z.object({ ok: z.boolean() }));
1047
1043
  },
@@ -1236,11 +1232,6 @@ const finalizeResponseSchema = z.object({
1236
1232
  mediaType: z.string(),
1237
1233
  sizeBytes: z.number().nullable().optional()
1238
1234
  });
1239
- const activeRunResponseSchema = z.object({ run: z.object({
1240
- runId: z.string(),
1241
- agentId: z.string().nullish(),
1242
- agentName: z.string().nullish()
1243
- }).nullable() });
1244
1235
  const oauthConnectResponseSchema = z.object({ connectLink: z.string() }).passthrough();
1245
1236
  const externalOauthConnectResponseSchema = z.object({ authorizationUrl: z.string().optional() }).passthrough();
1246
1237
  const runStreamEventSchema = z.union([z.object({
@@ -1252,11 +1243,14 @@ const runStreamEventSchema = z.union([z.object({
1252
1243
  error: z.string().nullish()
1253
1244
  })]);
1254
1245
  const streamErrorSchema = z.object({ error: z.string() });
1255
- const conversationStreamEventSchema = z.object({
1246
+ const conversationStreamEventSchema = z.discriminatedUnion("kind", [z.object({
1256
1247
  kind: z.literal("conversation"),
1257
1248
  id: z.string(),
1258
1249
  title: z.string().nullable()
1259
- });
1250
+ }), z.object({
1251
+ kind: z.literal("run"),
1252
+ runId: z.string()
1253
+ })]);
1260
1254
 
1261
1255
  //#endregion
1262
1256
  //#region src/commands/session.ts
@@ -2622,7 +2616,7 @@ const chatCommand = {
2622
2616
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2623
2617
  process.exit(1);
2624
2618
  }
2625
- const { runChat } = await import("./boot-C2plQe9S.mjs");
2619
+ const { runChat } = await import("./boot-C1WUzzUo.mjs");
2626
2620
  await runChat({
2627
2621
  appUrl,
2628
2622
  sessionToken: session.value.sessionToken,
@@ -2664,7 +2658,7 @@ async function runPrintMode({ argv, appUrl }) {
2664
2658
  }
2665
2659
  let machineShare = null;
2666
2660
  if (resolveShareMachine(argv)) {
2667
- const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
2661
+ const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
2668
2662
  let signalConnected;
2669
2663
  const connected = new Promise((resolve) => {
2670
2664
  signalConnected = resolve;
@@ -3416,7 +3410,7 @@ const switchCommand = {
3416
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.");
3417
3411
  process.exit(1);
3418
3412
  }
3419
- const { runWorkspacePicker } = await import("./boot-C2plQe9S.mjs");
3413
+ const { runWorkspacePicker } = await import("./boot-C1WUzzUo.mjs");
3420
3414
  await runWorkspacePicker(session);
3421
3415
  return;
3422
3416
  }
@@ -3633,7 +3627,7 @@ const openCommand = {
3633
3627
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
3634
3628
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
3635
3629
  const { machineName } = machineIdentity();
3636
- const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
3630
+ const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
3637
3631
  let lastLine = "";
3638
3632
  let signalConnected;
3639
3633
  const connected = new Promise((resolve) => {
@@ -3764,10 +3758,298 @@ const portalCommand = {
3764
3758
  handler: () => {}
3765
3759
  };
3766
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
+
3767
4049
  //#endregion
3768
4050
  //#region src/cli.ts
3769
4051
  function createCli(argv) {
3770
- 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", {
3771
4053
  type: "boolean",
3772
4054
  default: false,
3773
4055
  global: true,
@@ -3781,7 +4063,7 @@ function createCli(argv) {
3781
4063
  type: "string",
3782
4064
  global: true,
3783
4065
  describe: "Override API base URL"
3784
- }).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) => {
3785
4067
  printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
3786
4068
  process.exit(1);
3787
4069
  });
@@ -3855,4 +4137,4 @@ function resolveArgv(args, tty = {
3855
4137
  createCli(resolveArgv(hideBin(process.argv))).parse();
3856
4138
 
3857
4139
  //#endregion
3858
- 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
@@ -5648,6 +5423,7 @@ function ChatScreen({ agent, conversation }) {
5648
5423
  inputRef.current = input;
5649
5424
  const credPromptRef = useRef(credPrompt);
5650
5425
  credPromptRef.current = credPrompt;
5426
+ const seenRunsRef = useRef(/* @__PURE__ */ new Set());
5651
5427
  const shellSessionRef = useRef(null);
5652
5428
  const hostState = grantPrompt || credPrompt ? "blocked" : run.kind === "idle" ? "idle" : "working";
5653
5429
  useEffect(() => {
@@ -5721,6 +5497,7 @@ function ChatScreen({ agent, conversation }) {
5721
5497
  }, [rest, initialConversationId]);
5722
5498
  const attachToRun = useCallback((runId, runAgentName) => {
5723
5499
  if (!rest) return;
5500
+ seenRunsRef.current.add(runId);
5724
5501
  const abort = new AbortController();
5725
5502
  const responsePreview = createResponsePreview();
5726
5503
  const streamAgentName = runAgentName ?? agent.name;
@@ -5781,6 +5558,12 @@ function ChatScreen({ agent, conversation }) {
5781
5558
  conversationId,
5782
5559
  signal: abort.signal,
5783
5560
  onEvent: (event) => {
5561
+ if (event.kind === "run") {
5562
+ if (seenRunsRef.current.has(event.runId)) return;
5563
+ if (runRef.current.kind !== "idle") return;
5564
+ attachToRun(event.runId);
5565
+ return;
5566
+ }
5784
5567
  if (event.title) setChatTitle(event.title);
5785
5568
  }
5786
5569
  }).catch(() => {});
@@ -5788,22 +5571,9 @@ function ChatScreen({ agent, conversation }) {
5788
5571
  }, [
5789
5572
  rest,
5790
5573
  conversationId,
5791
- setChatTitle
5574
+ setChatTitle,
5575
+ attachToRun
5792
5576
  ]);
5793
- useEffect(() => {
5794
- if (!rest || !conversationId) return;
5795
- let cancelled = false;
5796
- (async () => {
5797
- try {
5798
- const active = await rest.activeRun({ conversationId });
5799
- if (cancelled || !active) return;
5800
- attachToRun(active.runId, active.agentName || null);
5801
- } catch (_error) {}
5802
- })();
5803
- return () => {
5804
- cancelled = true;
5805
- };
5806
- }, []);
5807
5577
  const sendContent = useCallback(async (content, staged, opts) => {
5808
5578
  if (!rest) return;
5809
5579
  const trimmed = content.trim();
@@ -6147,6 +5917,10 @@ function ChatScreen({ agent, conversation }) {
6147
5917
  append(`\n${e.message}`);
6148
5918
  stamp(1);
6149
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
+ }
6150
5924
  }
6151
5925
  });
6152
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.351",
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",