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

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.218";
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-CIdJ5Als.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-CIdJ5Als.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);
@@ -3919,6 +3921,10 @@ const keybindGroups = [
3919
3921
  keys: "ctrl+c ctrl+c",
3920
3922
  action: "quit"
3921
3923
  },
3924
+ {
3925
+ keys: "exit",
3926
+ action: "quit (type it as a message)"
3927
+ },
3922
3928
  {
3923
3929
  keys: "ctrl+p",
3924
3930
  action: "switch model"
@@ -5034,6 +5040,7 @@ function routeInput(raw) {
5034
5040
  kind: "sandbox-exec",
5035
5041
  command: sandbox
5036
5042
  } : { kind: "sandbox-pty" };
5043
+ if (line.trim().toLowerCase() === "exit") return { kind: "quit" };
5037
5044
  return {
5038
5045
  kind: "message",
5039
5046
  content: raw
@@ -5071,6 +5078,203 @@ function formatShellContext(command, output, exitCode) {
5071
5078
  ].join("\n");
5072
5079
  }
5073
5080
 
5081
+ //#endregion
5082
+ //#region src/chat/sandbox/client.ts
5083
+ /**
5084
+ * Client for the api's `/api/v1/sandbox/stream` WebSocket: relays a `/sandbox`
5085
+ * session into the agent's own sandbox — a live interactive PTY or a one-shot
5086
+ * streamed exec. Mirrors the transport shape of `portal/client.ts`, but dials a
5087
+ * different endpoint and speaks the sandbox-stream frame protocol (see the
5088
+ * server's sandbox-stream-ws.ts).
5089
+ */
5090
+ const T = {
5091
+ DATA: 1,
5092
+ EXIT: 2,
5093
+ ERROR: 3,
5094
+ INPUT: 16,
5095
+ RESIZE: 17
5096
+ };
5097
+ function wsBase(appUrl) {
5098
+ const base = appUrl.replace(/\/+$/, "");
5099
+ if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
5100
+ if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
5101
+ return `wss://${base}`;
5102
+ }
5103
+ /**
5104
+ * A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
5105
+ * the write side (keystrokes / resize for pty mode) and teardown.
5106
+ */
5107
+ var SandboxStream = class SandboxStream {
5108
+ ws;
5109
+ closed = false;
5110
+ constructor(ws, onEvent) {
5111
+ this.ws = ws;
5112
+ ws.on("message", (data, isBinary) => {
5113
+ if (!isBinary) return;
5114
+ const buf = toBuffer(data);
5115
+ if (buf.length === 0) return;
5116
+ const body = buf.subarray(1);
5117
+ switch (buf[0]) {
5118
+ case T.DATA:
5119
+ onEvent({
5120
+ type: "data",
5121
+ bytes: new Uint8Array(body)
5122
+ });
5123
+ break;
5124
+ case T.EXIT:
5125
+ onEvent({
5126
+ type: "exit",
5127
+ code: body.length >= 4 ? body.readInt32BE(0) : 0
5128
+ });
5129
+ break;
5130
+ case T.ERROR:
5131
+ onEvent({
5132
+ type: "error",
5133
+ message: body.toString("utf8")
5134
+ });
5135
+ break;
5136
+ }
5137
+ });
5138
+ ws.on("close", () => {
5139
+ this.closed = true;
5140
+ onEvent({ type: "close" });
5141
+ });
5142
+ ws.on("error", () => {});
5143
+ }
5144
+ /** Feed keystroke bytes to the pty stdin. */
5145
+ sendInput(data) {
5146
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
5147
+ this.ws.send(Buffer.concat([Buffer.from([T.INPUT]), Buffer.from(data)]));
5148
+ }
5149
+ /** Notify the pty of a terminal resize. */
5150
+ resize(cols, rows) {
5151
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
5152
+ const b = Buffer.allocUnsafe(5);
5153
+ b[0] = T.RESIZE;
5154
+ b.writeUInt16BE(cols & 65535, 1);
5155
+ b.writeUInt16BE(rows & 65535, 3);
5156
+ this.ws.send(b);
5157
+ }
5158
+ close() {
5159
+ this.closed = true;
5160
+ this.ws.close();
5161
+ }
5162
+ /** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
5163
+ static open(opts) {
5164
+ const url = new URL(`${wsBase(opts.appUrl)}/api/v1/sandbox/stream`);
5165
+ url.searchParams.set("agentId", opts.agentId);
5166
+ url.searchParams.set("mode", opts.mode);
5167
+ if (opts.mode === "pty") {
5168
+ url.searchParams.set("cols", String(opts.cols));
5169
+ url.searchParams.set("rows", String(opts.rows));
5170
+ } else url.searchParams.set("command", opts.command);
5171
+ return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
5172
+ }
5173
+ };
5174
+ function toBuffer(data) {
5175
+ if (Buffer.isBuffer(data)) return data;
5176
+ if (Array.isArray(data)) return Buffer.concat(data);
5177
+ return Buffer.from(data);
5178
+ }
5179
+
5180
+ //#endregion
5181
+ //#region src/chat/sandbox/pty-session.ts
5182
+ /**
5183
+ * Runs a live interactive PTY into the agent's sandbox as a full-screen raw
5184
+ * terminal, by SUSPENDING the opentui renderer for the duration and doing a
5185
+ * direct byte passthrough:
5186
+ *
5187
+ * local stdin → INPUT frames → sandbox pty
5188
+ * sandbox pty → DATA frames → local stdout
5189
+ *
5190
+ * This is the correct shape for a real terminal: we don't reimplement a
5191
+ * terminal emulator, we hand the actual TTY to the remote shell. On exit
5192
+ * (the shell exits, the socket drops, or the user hits the detach key) we
5193
+ * restore the terminal and resume the TUI.
5194
+ *
5195
+ * Detach key: Ctrl-] (0x1d), the classic telnet/ssh escape — leaves the shell
5196
+ * running server-side is NOT a goal here; detaching closes the session.
5197
+ */
5198
+ const DETACH_BYTE = 29;
5199
+ async function runPtySession(opts) {
5200
+ const { renderer } = opts;
5201
+ const stdin = renderer.stdin;
5202
+ const stdout = process.stdout;
5203
+ const size = () => ({
5204
+ cols: stdout.columns ?? 80,
5205
+ rows: stdout.rows ?? 24
5206
+ });
5207
+ renderer.suspend();
5208
+ return await new Promise((resolve) => {
5209
+ let settled = false;
5210
+ const initial = size();
5211
+ const stream = SandboxStream.open({
5212
+ mode: "pty",
5213
+ appUrl: opts.appUrl,
5214
+ sessionToken: opts.sessionToken,
5215
+ agentId: opts.agentId,
5216
+ cols: initial.cols,
5217
+ rows: initial.rows,
5218
+ onEvent: (e) => {
5219
+ switch (e.type) {
5220
+ case "data":
5221
+ stdout.write(e.bytes);
5222
+ break;
5223
+ case "error":
5224
+ stdout.write(`\r\n\x1b[31m${e.message}\x1b[0m\r\n`);
5225
+ finish({
5226
+ reason: "error",
5227
+ code: 1
5228
+ });
5229
+ break;
5230
+ case "exit":
5231
+ finish({
5232
+ reason: "exit",
5233
+ code: e.code
5234
+ });
5235
+ break;
5236
+ case "close":
5237
+ finish({
5238
+ reason: "exit",
5239
+ code: 0
5240
+ });
5241
+ break;
5242
+ }
5243
+ }
5244
+ });
5245
+ const onStdin = (chunk) => {
5246
+ if (chunk.length === 1 && chunk[0] === DETACH_BYTE) {
5247
+ finish({
5248
+ reason: "detach",
5249
+ code: 0
5250
+ });
5251
+ return;
5252
+ }
5253
+ stream.sendInput(new Uint8Array(chunk));
5254
+ };
5255
+ const onResize = () => {
5256
+ const s = size();
5257
+ stream.resize(s.cols, s.rows);
5258
+ };
5259
+ const wasRaw = stdin.isRaw ?? false;
5260
+ stdin.setRawMode?.(true);
5261
+ stdin.resume();
5262
+ stdin.on("data", onStdin);
5263
+ stdout.on("resize", onResize);
5264
+ stream.resize(initial.cols, initial.rows);
5265
+ function finish(result) {
5266
+ if (settled) return;
5267
+ settled = true;
5268
+ stdin.off("data", onStdin);
5269
+ stdout.off("resize", onResize);
5270
+ stdin.setRawMode?.(wasRaw);
5271
+ stream.close();
5272
+ renderer.resume();
5273
+ resolve(result);
5274
+ }
5275
+ });
5276
+ }
5277
+
5074
5278
  //#endregion
5075
5279
  //#region src/chat/tui/screens/chat.tsx
5076
5280
  /**
@@ -5133,6 +5337,7 @@ function ChatScreen({ agent, conversation }) {
5133
5337
  const rest = useStore((s) => s.rest);
5134
5338
  const history = usePromptHistory({ filePath: useStore((s) => s.promptHistoryPath) });
5135
5339
  const appUrl = useStore((s) => s.appUrl);
5340
+ const sessionToken = useStore((s) => s.sessionToken);
5136
5341
  const goTo = useStore((s) => s.goTo);
5137
5342
  const setChatTitle = useStore((s) => s.setChatTitle);
5138
5343
  const portal = useStore((s) => s.portal);
@@ -5591,6 +5796,74 @@ function ChatScreen({ agent, conversation }) {
5591
5796
  }
5592
5797
  sendContent(formatShellContext(command, captured, exitCode), [], { echo: false });
5593
5798
  }, [sendContent]);
5799
+ const runSandboxExec = useCallback((command) => {
5800
+ if (!appUrl || !sessionToken) return;
5801
+ const id = crypto.randomUUID();
5802
+ setItems((prev) => [...prev, {
5803
+ kind: "shell",
5804
+ id,
5805
+ command: `☁ ${command}`,
5806
+ output: "",
5807
+ exit: null
5808
+ }]);
5809
+ const dec = new TextDecoder();
5810
+ const append = (chunk) => setItems((prev) => prev.map((m) => m.id === id && m.kind === "shell" ? {
5811
+ ...m,
5812
+ output: m.output + chunk
5813
+ } : m));
5814
+ const stamp = (exit) => setItems((prev) => prev.map((m) => m.id === id && m.kind === "shell" ? {
5815
+ ...m,
5816
+ exit
5817
+ } : m));
5818
+ SandboxStream.open({
5819
+ mode: "exec",
5820
+ appUrl,
5821
+ sessionToken,
5822
+ agentId: agent.id,
5823
+ command,
5824
+ onEvent: (e) => {
5825
+ if (e.type === "data") append(dec.decode(e.bytes));
5826
+ else if (e.type === "error") {
5827
+ append(`\n${e.message}`);
5828
+ stamp(1);
5829
+ } else if (e.type === "exit") stamp(e.code);
5830
+ }
5831
+ });
5832
+ }, [
5833
+ appUrl,
5834
+ sessionToken,
5835
+ agent.id
5836
+ ]);
5837
+ const runSandboxPty = useCallback(async () => {
5838
+ if (!appUrl || !sessionToken) return;
5839
+ const result = await runPtySession({
5840
+ renderer,
5841
+ appUrl,
5842
+ sessionToken,
5843
+ agentId: agent.id
5844
+ });
5845
+ setItems((prev) => [...prev, {
5846
+ kind: "shell",
5847
+ id: crypto.randomUUID(),
5848
+ command: "☁ sandbox terminal",
5849
+ output: result.reason === "error" ? "terminal ended with an error" : result.reason === "detach" ? "detached" : "session ended",
5850
+ exit: result.code
5851
+ }]);
5852
+ }, [
5853
+ appUrl,
5854
+ sessionToken,
5855
+ agent.id,
5856
+ renderer
5857
+ ]);
5858
+ const quit = useCallback(() => {
5859
+ portalClient?.dispose();
5860
+ agentHost.dispose();
5861
+ renderer.destroy();
5862
+ }, [
5863
+ portalClient,
5864
+ agentHost,
5865
+ renderer
5866
+ ]);
5594
5867
  const submit = useCallback(() => {
5595
5868
  const content = input.trim();
5596
5869
  const attachments = pending;
@@ -5602,11 +5875,9 @@ function ChatScreen({ agent, conversation }) {
5602
5875
  composerRef.current?.clear();
5603
5876
  setComposerRows(1);
5604
5877
  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
- }]);
5878
+ else if (routed.kind === "sandbox-exec") runSandboxExec(routed.command);
5879
+ else if (routed.kind === "sandbox-pty") runSandboxPty();
5880
+ else if (routed.kind === "quit") quit();
5610
5881
  return;
5611
5882
  }
5612
5883
  history.append(content);
@@ -5620,7 +5891,10 @@ function ChatScreen({ agent, conversation }) {
5620
5891
  pending,
5621
5892
  sendContent,
5622
5893
  history,
5623
- runLocalShell
5894
+ runLocalShell,
5895
+ runSandboxExec,
5896
+ runSandboxPty,
5897
+ quit
5624
5898
  ]);
5625
5899
  const handleComposerChange = useCallback(() => {
5626
5900
  const composer = composerRef.current;
@@ -5907,9 +6181,7 @@ function ChatScreen({ agent, conversation }) {
5907
6181
  return;
5908
6182
  }
5909
6183
  if (ctrlCArmed) {
5910
- portalClient?.dispose();
5911
- agentHost.dispose();
5912
- renderer.destroy();
6184
+ quit();
5913
6185
  return;
5914
6186
  }
5915
6187
  setCtrlCArmed(true);
@@ -6311,6 +6583,7 @@ function App({ appUrl, sessionToken, shareMachine, promptHistoryPath, notificati
6311
6583
  });
6312
6584
  setClients({
6313
6585
  appUrl,
6586
+ sessionToken,
6314
6587
  rest,
6315
6588
  promptHistoryPath
6316
6589
  });
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.218",
4
4
  "description": "Skydive CLI — manage AI agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",