skydive-cli 0.1.0-beta.189 → 0.1.0-beta.199

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
@@ -31,7 +31,7 @@ var __exportAll = (all, no_symbols) => {
31
31
 
32
32
  //#endregion
33
33
  //#region package.json
34
- var version$1 = "0.1.0-beta.189";
34
+ var version$1 = "0.1.0-beta.199";
35
35
 
36
36
  //#endregion
37
37
  //#region src/types.ts
@@ -2152,7 +2152,7 @@ const chatCommand = {
2152
2152
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
2153
2153
  process.exit(1);
2154
2154
  }
2155
- const { runChat } = await import("./boot-DaU-F_bM.mjs");
2155
+ const { runChat } = await import("./boot-DGdoNlG2.mjs");
2156
2156
  await runChat({
2157
2157
  appUrl,
2158
2158
  sessionToken: session.value.sessionToken,
@@ -3330,7 +3330,7 @@ const switchCommand = {
3330
3330
  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.");
3331
3331
  process.exit(1);
3332
3332
  }
3333
- const { runWorkspacePicker } = await import("./boot-DaU-F_bM.mjs");
3333
+ const { runWorkspacePicker } = await import("./boot-DGdoNlG2.mjs");
3334
3334
  await runWorkspacePicker(session);
3335
3335
  return;
3336
3336
  }
@@ -62,6 +62,7 @@ const useStore = create((set, get) => ({
62
62
  themeId: DEFAULT_THEME_ID.dark,
63
63
  themeLocked: false,
64
64
  appUrl: null,
65
+ sessionToken: null,
65
66
  workspaceName: null,
66
67
  rest: null,
67
68
  promptHistoryPath: null,
@@ -98,8 +99,9 @@ const useStore = create((set, get) => ({
98
99
  themeLocked: true
99
100
  });
100
101
  },
101
- setClients: ({ appUrl, rest, promptHistoryPath }) => set({
102
+ setClients: ({ appUrl, sessionToken, rest, promptHistoryPath }) => set({
102
103
  appUrl,
104
+ sessionToken,
103
105
  rest,
104
106
  promptHistoryPath
105
107
  }),
@@ -534,7 +536,7 @@ var PortalClient = class {
534
536
  this.refreshDevice();
535
537
  });
536
538
  ws.on("message", (data, isBinary) => {
537
- if (isBinary) jobs.handleFrame(toBuffer(data));
539
+ if (isBinary) jobs.handleFrame(toBuffer$1(data));
538
540
  });
539
541
  ws.on("error", (err) => {
540
542
  this.error = errorMessage(err);
@@ -585,7 +587,7 @@ var PortalClient = class {
585
587
  });
586
588
  }
587
589
  };
588
- function toBuffer(data) {
590
+ function toBuffer$1(data) {
589
591
  if (Buffer.isBuffer(data)) return data;
590
592
  if (Array.isArray(data)) return Buffer.concat(data);
591
593
  return Buffer.from(data);
@@ -5071,6 +5073,203 @@ function formatShellContext(command, output, exitCode) {
5071
5073
  ].join("\n");
5072
5074
  }
5073
5075
 
5076
+ //#endregion
5077
+ //#region src/chat/sandbox/client.ts
5078
+ /**
5079
+ * Client for the api's `/api/v1/sandbox/stream` WebSocket: relays a `/sandbox`
5080
+ * session into the agent's own sandbox — a live interactive PTY or a one-shot
5081
+ * streamed exec. Mirrors the transport shape of `portal/client.ts`, but dials a
5082
+ * different endpoint and speaks the sandbox-stream frame protocol (see the
5083
+ * server's sandbox-stream-ws.ts).
5084
+ */
5085
+ const T = {
5086
+ DATA: 1,
5087
+ EXIT: 2,
5088
+ ERROR: 3,
5089
+ INPUT: 16,
5090
+ RESIZE: 17
5091
+ };
5092
+ function wsBase(appUrl) {
5093
+ const base = appUrl.replace(/\/+$/, "");
5094
+ if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
5095
+ if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
5096
+ return `wss://${base}`;
5097
+ }
5098
+ /**
5099
+ * A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
5100
+ * the write side (keystrokes / resize for pty mode) and teardown.
5101
+ */
5102
+ var SandboxStream = class SandboxStream {
5103
+ ws;
5104
+ closed = false;
5105
+ constructor(ws, onEvent) {
5106
+ this.ws = ws;
5107
+ ws.on("message", (data, isBinary) => {
5108
+ if (!isBinary) return;
5109
+ const buf = toBuffer(data);
5110
+ if (buf.length === 0) return;
5111
+ const body = buf.subarray(1);
5112
+ switch (buf[0]) {
5113
+ case T.DATA:
5114
+ onEvent({
5115
+ type: "data",
5116
+ bytes: new Uint8Array(body)
5117
+ });
5118
+ break;
5119
+ case T.EXIT:
5120
+ onEvent({
5121
+ type: "exit",
5122
+ code: body.length >= 4 ? body.readInt32BE(0) : 0
5123
+ });
5124
+ break;
5125
+ case T.ERROR:
5126
+ onEvent({
5127
+ type: "error",
5128
+ message: body.toString("utf8")
5129
+ });
5130
+ break;
5131
+ }
5132
+ });
5133
+ ws.on("close", () => {
5134
+ this.closed = true;
5135
+ onEvent({ type: "close" });
5136
+ });
5137
+ ws.on("error", () => {});
5138
+ }
5139
+ /** Feed keystroke bytes to the pty stdin. */
5140
+ sendInput(data) {
5141
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
5142
+ this.ws.send(Buffer.concat([Buffer.from([T.INPUT]), Buffer.from(data)]));
5143
+ }
5144
+ /** Notify the pty of a terminal resize. */
5145
+ resize(cols, rows) {
5146
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
5147
+ const b = Buffer.allocUnsafe(5);
5148
+ b[0] = T.RESIZE;
5149
+ b.writeUInt16BE(cols & 65535, 1);
5150
+ b.writeUInt16BE(rows & 65535, 3);
5151
+ this.ws.send(b);
5152
+ }
5153
+ close() {
5154
+ this.closed = true;
5155
+ this.ws.close();
5156
+ }
5157
+ /** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
5158
+ static open(opts) {
5159
+ const url = new URL(`${wsBase(opts.appUrl)}/api/v1/sandbox/stream`);
5160
+ url.searchParams.set("agentId", opts.agentId);
5161
+ url.searchParams.set("mode", opts.mode);
5162
+ if (opts.mode === "pty") {
5163
+ url.searchParams.set("cols", String(opts.cols));
5164
+ url.searchParams.set("rows", String(opts.rows));
5165
+ } else url.searchParams.set("command", opts.command);
5166
+ return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
5167
+ }
5168
+ };
5169
+ function toBuffer(data) {
5170
+ if (Buffer.isBuffer(data)) return data;
5171
+ if (Array.isArray(data)) return Buffer.concat(data);
5172
+ return Buffer.from(data);
5173
+ }
5174
+
5175
+ //#endregion
5176
+ //#region src/chat/sandbox/pty-session.ts
5177
+ /**
5178
+ * Runs a live interactive PTY into the agent's sandbox as a full-screen raw
5179
+ * terminal, by SUSPENDING the opentui renderer for the duration and doing a
5180
+ * direct byte passthrough:
5181
+ *
5182
+ * local stdin → INPUT frames → sandbox pty
5183
+ * sandbox pty → DATA frames → local stdout
5184
+ *
5185
+ * This is the correct shape for a real terminal: we don't reimplement a
5186
+ * terminal emulator, we hand the actual TTY to the remote shell. On exit
5187
+ * (the shell exits, the socket drops, or the user hits the detach key) we
5188
+ * restore the terminal and resume the TUI.
5189
+ *
5190
+ * Detach key: Ctrl-] (0x1d), the classic telnet/ssh escape — leaves the shell
5191
+ * running server-side is NOT a goal here; detaching closes the session.
5192
+ */
5193
+ const DETACH_BYTE = 29;
5194
+ async function runPtySession(opts) {
5195
+ const { renderer } = opts;
5196
+ const stdin = renderer.stdin;
5197
+ const stdout = process.stdout;
5198
+ const size = () => ({
5199
+ cols: stdout.columns ?? 80,
5200
+ rows: stdout.rows ?? 24
5201
+ });
5202
+ renderer.suspend();
5203
+ return await new Promise((resolve) => {
5204
+ let settled = false;
5205
+ const initial = size();
5206
+ const stream = SandboxStream.open({
5207
+ mode: "pty",
5208
+ appUrl: opts.appUrl,
5209
+ sessionToken: opts.sessionToken,
5210
+ agentId: opts.agentId,
5211
+ cols: initial.cols,
5212
+ rows: initial.rows,
5213
+ onEvent: (e) => {
5214
+ switch (e.type) {
5215
+ case "data":
5216
+ stdout.write(e.bytes);
5217
+ break;
5218
+ case "error":
5219
+ stdout.write(`\r\n\x1b[31m${e.message}\x1b[0m\r\n`);
5220
+ finish({
5221
+ reason: "error",
5222
+ code: 1
5223
+ });
5224
+ break;
5225
+ case "exit":
5226
+ finish({
5227
+ reason: "exit",
5228
+ code: e.code
5229
+ });
5230
+ break;
5231
+ case "close":
5232
+ finish({
5233
+ reason: "exit",
5234
+ code: 0
5235
+ });
5236
+ break;
5237
+ }
5238
+ }
5239
+ });
5240
+ const onStdin = (chunk) => {
5241
+ if (chunk.length === 1 && chunk[0] === DETACH_BYTE) {
5242
+ finish({
5243
+ reason: "detach",
5244
+ code: 0
5245
+ });
5246
+ return;
5247
+ }
5248
+ stream.sendInput(new Uint8Array(chunk));
5249
+ };
5250
+ const onResize = () => {
5251
+ const s = size();
5252
+ stream.resize(s.cols, s.rows);
5253
+ };
5254
+ const wasRaw = stdin.isRaw ?? false;
5255
+ stdin.setRawMode?.(true);
5256
+ stdin.resume();
5257
+ stdin.on("data", onStdin);
5258
+ stdout.on("resize", onResize);
5259
+ stream.resize(initial.cols, initial.rows);
5260
+ function finish(result) {
5261
+ if (settled) return;
5262
+ settled = true;
5263
+ stdin.off("data", onStdin);
5264
+ stdout.off("resize", onResize);
5265
+ stdin.setRawMode?.(wasRaw);
5266
+ stream.close();
5267
+ renderer.resume();
5268
+ resolve(result);
5269
+ }
5270
+ });
5271
+ }
5272
+
5074
5273
  //#endregion
5075
5274
  //#region src/chat/tui/screens/chat.tsx
5076
5275
  /**
@@ -5133,6 +5332,7 @@ function ChatScreen({ agent, conversation }) {
5133
5332
  const rest = useStore((s) => s.rest);
5134
5333
  const history = usePromptHistory({ filePath: useStore((s) => s.promptHistoryPath) });
5135
5334
  const appUrl = useStore((s) => s.appUrl);
5335
+ const sessionToken = useStore((s) => s.sessionToken);
5136
5336
  const goTo = useStore((s) => s.goTo);
5137
5337
  const setChatTitle = useStore((s) => s.setChatTitle);
5138
5338
  const portal = useStore((s) => s.portal);
@@ -5591,6 +5791,65 @@ function ChatScreen({ agent, conversation }) {
5591
5791
  }
5592
5792
  sendContent(formatShellContext(command, captured, exitCode), [], { echo: false });
5593
5793
  }, [sendContent]);
5794
+ const runSandboxExec = useCallback((command) => {
5795
+ if (!appUrl || !sessionToken) return;
5796
+ const id = crypto.randomUUID();
5797
+ setItems((prev) => [...prev, {
5798
+ kind: "shell",
5799
+ id,
5800
+ command: `☁ ${command}`,
5801
+ output: "",
5802
+ exit: null
5803
+ }]);
5804
+ const dec = new TextDecoder();
5805
+ const append = (chunk) => setItems((prev) => prev.map((m) => m.id === id && m.kind === "shell" ? {
5806
+ ...m,
5807
+ output: m.output + chunk
5808
+ } : m));
5809
+ const stamp = (exit) => setItems((prev) => prev.map((m) => m.id === id && m.kind === "shell" ? {
5810
+ ...m,
5811
+ exit
5812
+ } : m));
5813
+ SandboxStream.open({
5814
+ mode: "exec",
5815
+ appUrl,
5816
+ sessionToken,
5817
+ agentId: agent.id,
5818
+ command,
5819
+ onEvent: (e) => {
5820
+ if (e.type === "data") append(dec.decode(e.bytes));
5821
+ else if (e.type === "error") {
5822
+ append(`\n${e.message}`);
5823
+ stamp(1);
5824
+ } else if (e.type === "exit") stamp(e.code);
5825
+ }
5826
+ });
5827
+ }, [
5828
+ appUrl,
5829
+ sessionToken,
5830
+ agent.id
5831
+ ]);
5832
+ const runSandboxPty = useCallback(async () => {
5833
+ if (!appUrl || !sessionToken) return;
5834
+ const result = await runPtySession({
5835
+ renderer,
5836
+ appUrl,
5837
+ sessionToken,
5838
+ agentId: agent.id
5839
+ });
5840
+ setItems((prev) => [...prev, {
5841
+ kind: "shell",
5842
+ id: crypto.randomUUID(),
5843
+ command: "☁ sandbox terminal",
5844
+ output: result.reason === "error" ? "terminal ended with an error" : result.reason === "detach" ? "detached" : "session ended",
5845
+ exit: result.code
5846
+ }]);
5847
+ }, [
5848
+ appUrl,
5849
+ sessionToken,
5850
+ agent.id,
5851
+ renderer
5852
+ ]);
5594
5853
  const submit = useCallback(() => {
5595
5854
  const content = input.trim();
5596
5855
  const attachments = pending;
@@ -5602,11 +5861,8 @@ function ChatScreen({ agent, conversation }) {
5602
5861
  composerRef.current?.clear();
5603
5862
  setComposerRows(1);
5604
5863
  if (routed.kind === "local-shell") runLocalShell(routed.command);
5605
- else if (routed.kind === "sandbox-exec" || routed.kind === "sandbox-pty") setItems((prev) => [...prev, {
5606
- kind: "error",
5607
- id: crypto.randomUUID(),
5608
- text: "/sandbox is coming soon — run local commands with ! for now."
5609
- }]);
5864
+ else if (routed.kind === "sandbox-exec") runSandboxExec(routed.command);
5865
+ else if (routed.kind === "sandbox-pty") runSandboxPty();
5610
5866
  return;
5611
5867
  }
5612
5868
  history.append(content);
@@ -5620,7 +5876,9 @@ function ChatScreen({ agent, conversation }) {
5620
5876
  pending,
5621
5877
  sendContent,
5622
5878
  history,
5623
- runLocalShell
5879
+ runLocalShell,
5880
+ runSandboxExec,
5881
+ runSandboxPty
5624
5882
  ]);
5625
5883
  const handleComposerChange = useCallback(() => {
5626
5884
  const composer = composerRef.current;
@@ -6311,6 +6569,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
6311
6569
  });
6312
6570
  setClients({
6313
6571
  appUrl,
6572
+ sessionToken,
6314
6573
  rest,
6315
6574
  promptHistoryPath
6316
6575
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.1.0-beta.189",
3
+ "version": "0.1.0-beta.199",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",