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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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
|
|
3
|
-
import { t as PortalClient } from "./client-
|
|
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
|
|
5445
|
-
*
|
|
5446
|
-
*
|
|
5447
|
-
*
|
|
5448
|
-
*
|
|
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
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
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
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
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
|
|
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 };
|