skydive-cli 0.1.0-beta.353 → 0.1.0-beta.363
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/js/bin.mjs
CHANGED
|
@@ -14,9 +14,10 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import zlib from "node:zlib";
|
|
16
16
|
import os from "node:os";
|
|
17
|
+
import { WebSocket } from "ws";
|
|
17
18
|
|
|
18
19
|
//#region package.json
|
|
19
|
-
var version$1 = "0.1.0-beta.
|
|
20
|
+
var version$1 = "0.1.0-beta.363";
|
|
20
21
|
|
|
21
22
|
//#endregion
|
|
22
23
|
//#region src/types.ts
|
|
@@ -838,6 +839,10 @@ const authCommand = {
|
|
|
838
839
|
|
|
839
840
|
//#endregion
|
|
840
841
|
//#region src/chat/api/rest.ts
|
|
842
|
+
var rest_exports = /* @__PURE__ */ __exportAll({
|
|
843
|
+
HttpError: () => HttpError,
|
|
844
|
+
createRestClient: () => createRestClient
|
|
845
|
+
});
|
|
841
846
|
var HttpError = class extends Error {
|
|
842
847
|
constructor(status, body) {
|
|
843
848
|
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
@@ -2611,7 +2616,7 @@ const chatCommand = {
|
|
|
2611
2616
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2612
2617
|
process.exit(1);
|
|
2613
2618
|
}
|
|
2614
|
-
const { runChat } = await import("./boot-
|
|
2619
|
+
const { runChat } = await import("./boot-C1WUzzUo.mjs");
|
|
2615
2620
|
await runChat({
|
|
2616
2621
|
appUrl,
|
|
2617
2622
|
sessionToken: session.value.sessionToken,
|
|
@@ -2653,7 +2658,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2653
2658
|
}
|
|
2654
2659
|
let machineShare = null;
|
|
2655
2660
|
if (resolveShareMachine(argv)) {
|
|
2656
|
-
const { PortalClient } = await import("./client-
|
|
2661
|
+
const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
|
|
2657
2662
|
let signalConnected;
|
|
2658
2663
|
const connected = new Promise((resolve) => {
|
|
2659
2664
|
signalConnected = resolve;
|
|
@@ -3405,7 +3410,7 @@ const switchCommand = {
|
|
|
3405
3410
|
printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
|
|
3406
3411
|
process.exit(1);
|
|
3407
3412
|
}
|
|
3408
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
3413
|
+
const { runWorkspacePicker } = await import("./boot-C1WUzzUo.mjs");
|
|
3409
3414
|
await runWorkspacePicker(session);
|
|
3410
3415
|
return;
|
|
3411
3416
|
}
|
|
@@ -3622,7 +3627,7 @@ const openCommand = {
|
|
|
3622
3627
|
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
3623
3628
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
3624
3629
|
const { machineName } = machineIdentity();
|
|
3625
|
-
const { PortalClient } = await import("./client-
|
|
3630
|
+
const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
|
|
3626
3631
|
let lastLine = "";
|
|
3627
3632
|
let signalConnected;
|
|
3628
3633
|
const connected = new Promise((resolve) => {
|
|
@@ -3753,10 +3758,298 @@ const portalCommand = {
|
|
|
3753
3758
|
handler: () => {}
|
|
3754
3759
|
};
|
|
3755
3760
|
|
|
3761
|
+
//#endregion
|
|
3762
|
+
//#region ../sandbox-stream-protocol/src/index.ts
|
|
3763
|
+
const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
|
|
3764
|
+
const FRAME = {
|
|
3765
|
+
DATA: 1,
|
|
3766
|
+
EXIT: 2,
|
|
3767
|
+
ERROR: 3,
|
|
3768
|
+
INPUT: 16,
|
|
3769
|
+
RESIZE: 17
|
|
3770
|
+
};
|
|
3771
|
+
const MAX_INPUT_BYTES = 1 * 1024 * 1024;
|
|
3772
|
+
/** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
|
|
3773
|
+
function streamSpecToQuery(spec) {
|
|
3774
|
+
if (spec.mode === "pty") return {
|
|
3775
|
+
agentId: spec.agentId,
|
|
3776
|
+
mode: "pty",
|
|
3777
|
+
cols: String(spec.cols),
|
|
3778
|
+
rows: String(spec.rows)
|
|
3779
|
+
};
|
|
3780
|
+
return {
|
|
3781
|
+
agentId: spec.agentId,
|
|
3782
|
+
mode: "exec",
|
|
3783
|
+
command: spec.command
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
function withType(type, payload) {
|
|
3787
|
+
const frame = new Uint8Array(1 + payload.length);
|
|
3788
|
+
frame[0] = type;
|
|
3789
|
+
frame.set(payload, 1);
|
|
3790
|
+
return frame;
|
|
3791
|
+
}
|
|
3792
|
+
/** client → server: keystroke bytes for the pty stdin. */
|
|
3793
|
+
function encodeInput(data) {
|
|
3794
|
+
return withType(FRAME.INPUT, data);
|
|
3795
|
+
}
|
|
3796
|
+
/** client → server: the client terminal was resized. */
|
|
3797
|
+
function encodeResize(cols, rows) {
|
|
3798
|
+
const frame = new Uint8Array(5);
|
|
3799
|
+
frame[0] = FRAME.RESIZE;
|
|
3800
|
+
const view = new DataView(frame.buffer);
|
|
3801
|
+
view.setUint16(1, cols & 65535);
|
|
3802
|
+
view.setUint16(3, rows & 65535);
|
|
3803
|
+
return frame;
|
|
3804
|
+
}
|
|
3805
|
+
const view = (frame) => new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
3806
|
+
/**
|
|
3807
|
+
* Decode a frame the server sent. Returns null for an empty, unknown, or
|
|
3808
|
+
* truncated frame — a peer speaking a newer protocol must not crash us.
|
|
3809
|
+
*/
|
|
3810
|
+
function decodeServerFrame(frame) {
|
|
3811
|
+
const payload = frame.subarray(1);
|
|
3812
|
+
switch (frame[0]) {
|
|
3813
|
+
case FRAME.DATA: return {
|
|
3814
|
+
type: "data",
|
|
3815
|
+
payload
|
|
3816
|
+
};
|
|
3817
|
+
case FRAME.EXIT: return {
|
|
3818
|
+
type: "exit",
|
|
3819
|
+
code: payload.length >= 4 ? view(frame).getInt32(1) : 0
|
|
3820
|
+
};
|
|
3821
|
+
case FRAME.ERROR: return {
|
|
3822
|
+
type: "error",
|
|
3823
|
+
message: new TextDecoder().decode(payload)
|
|
3824
|
+
};
|
|
3825
|
+
default: return null;
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
//#endregion
|
|
3830
|
+
//#region src/chat/sandbox/client.ts
|
|
3831
|
+
function wsBase(appUrl) {
|
|
3832
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
3833
|
+
if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
|
|
3834
|
+
if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
|
|
3835
|
+
return `wss://${base}`;
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
|
|
3839
|
+
* the write side (keystrokes / resize for pty mode) and teardown.
|
|
3840
|
+
*/
|
|
3841
|
+
var SandboxStream = class SandboxStream {
|
|
3842
|
+
ws;
|
|
3843
|
+
closed = false;
|
|
3844
|
+
constructor(ws, onEvent) {
|
|
3845
|
+
this.ws = ws;
|
|
3846
|
+
let ended = false;
|
|
3847
|
+
const emitEnd = (event) => {
|
|
3848
|
+
if (ended) return;
|
|
3849
|
+
ended = true;
|
|
3850
|
+
onEvent(event);
|
|
3851
|
+
};
|
|
3852
|
+
ws.on("message", (data, isBinary) => {
|
|
3853
|
+
if (!isBinary) return;
|
|
3854
|
+
const frame = decodeServerFrame(toBuffer(data));
|
|
3855
|
+
if (!frame) return;
|
|
3856
|
+
switch (frame.type) {
|
|
3857
|
+
case "data":
|
|
3858
|
+
onEvent({
|
|
3859
|
+
type: "data",
|
|
3860
|
+
bytes: new Uint8Array(frame.payload)
|
|
3861
|
+
});
|
|
3862
|
+
break;
|
|
3863
|
+
case "exit":
|
|
3864
|
+
emitEnd({
|
|
3865
|
+
type: "exit",
|
|
3866
|
+
code: frame.code
|
|
3867
|
+
});
|
|
3868
|
+
break;
|
|
3869
|
+
case "error":
|
|
3870
|
+
emitEnd({
|
|
3871
|
+
type: "error",
|
|
3872
|
+
message: frame.message
|
|
3873
|
+
});
|
|
3874
|
+
break;
|
|
3875
|
+
}
|
|
3876
|
+
});
|
|
3877
|
+
let failure = null;
|
|
3878
|
+
ws.on("error", (err) => {
|
|
3879
|
+
failure = err.message;
|
|
3880
|
+
});
|
|
3881
|
+
ws.on("close", () => {
|
|
3882
|
+
this.closed = true;
|
|
3883
|
+
emitEnd({
|
|
3884
|
+
type: "close",
|
|
3885
|
+
failure
|
|
3886
|
+
});
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
/** Feed keystroke bytes to the pty stdin. */
|
|
3890
|
+
sendInput(data) {
|
|
3891
|
+
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
3892
|
+
this.ws.send(encodeInput(data));
|
|
3893
|
+
}
|
|
3894
|
+
/** Notify the pty of a terminal resize. */
|
|
3895
|
+
resize(cols, rows) {
|
|
3896
|
+
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
3897
|
+
this.ws.send(encodeResize(cols, rows));
|
|
3898
|
+
}
|
|
3899
|
+
close() {
|
|
3900
|
+
this.closed = true;
|
|
3901
|
+
this.ws.close();
|
|
3902
|
+
}
|
|
3903
|
+
/** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
|
|
3904
|
+
static open(opts) {
|
|
3905
|
+
const spec = opts.mode === "pty" ? {
|
|
3906
|
+
mode: "pty",
|
|
3907
|
+
agentId: opts.agentId,
|
|
3908
|
+
cols: opts.cols,
|
|
3909
|
+
rows: opts.rows
|
|
3910
|
+
} : {
|
|
3911
|
+
mode: "exec",
|
|
3912
|
+
agentId: opts.agentId,
|
|
3913
|
+
command: opts.command
|
|
3914
|
+
};
|
|
3915
|
+
const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
|
|
3916
|
+
for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
|
|
3917
|
+
return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
function toBuffer(data) {
|
|
3921
|
+
if (Buffer.isBuffer(data)) return data;
|
|
3922
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
3923
|
+
return Buffer.from(data);
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
//#endregion
|
|
3927
|
+
//#region src/commands/sandbox.ts
|
|
3928
|
+
/** POSIX single-quote one word so the remote shell treats it as one token. */
|
|
3929
|
+
function shellQuote(word) {
|
|
3930
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word)) return word;
|
|
3931
|
+
return `'${word.replace(/'/g, `'\\''`)}'`;
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Serialize the command words into the single shell string the relay runs.
|
|
3935
|
+
*
|
|
3936
|
+
* Words arrive already split by the caller's shell, so each must be re-quoted
|
|
3937
|
+
* or the remote shell re-splits any word containing spaces or metacharacters:
|
|
3938
|
+
* `sandbox -- sh -c 'echo hi; whoami'` would otherwise run `sh -c echo hi`
|
|
3939
|
+
* and then, separately, `whoami`. A pipeline still works the explicit way —
|
|
3940
|
+
* `sandbox -- sh -c 'ls | wc -l'` — which is what `docker`/`kubectl exec` ask
|
|
3941
|
+
* for too. yargs number-coerces bare numerals, so words are stringified.
|
|
3942
|
+
*/
|
|
3943
|
+
function joinCommandWords(words) {
|
|
3944
|
+
return words.map(String).map(shellQuote).join(" ").trim();
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* `skydive sandbox` — the standalone counterpart to the chat TUI's `/sandbox`
|
|
3948
|
+
* composer command (ANY-4928): a live terminal (or one-shot exec) in the
|
|
3949
|
+
* agent's own sandbox without opening the TUI. Runs under Node (no Bun/
|
|
3950
|
+
* OpenTUI): the PTY is a raw byte passthrough on the caller's real terminal.
|
|
3951
|
+
* The relay gates on EDIT access to the agent and the sandbox-terminal-enabled
|
|
3952
|
+
* kill switch, and boots the sandbox when it isn't running.
|
|
3953
|
+
*/
|
|
3954
|
+
const sandboxCommand = {
|
|
3955
|
+
command: "sandbox [command..]",
|
|
3956
|
+
describe: "Open a live terminal in an agent's sandbox, or run a one-shot command there",
|
|
3957
|
+
builder: (y) => y.option("agent", {
|
|
3958
|
+
type: "string",
|
|
3959
|
+
describe: "Target agent, by id, slug, or name. Optional when the account has exactly one agent."
|
|
3960
|
+
}).positional("command", {
|
|
3961
|
+
type: "string",
|
|
3962
|
+
array: true,
|
|
3963
|
+
describe: "Command to run one-shot (streams output, exits with its exit code). Omit for a live interactive terminal. Put it after `--` if it has flags of its own."
|
|
3964
|
+
}).example("skydive sandbox --agent grace", "Live terminal (Ctrl-] detaches)").example("skydive sandbox --agent grace -- tail -n 50 /tmp/harness.log", "One-shot command (use `--` so its flags reach the sandbox)").example("skydive sandbox --agent grace -- sh -c 'ls /tmp | wc -l'", "Shell features go through an explicit `sh -c`"),
|
|
3965
|
+
handler: async (argv) => {
|
|
3966
|
+
const session = requireSession(argv);
|
|
3967
|
+
const { createRestClient } = await Promise.resolve().then(() => rest_exports);
|
|
3968
|
+
const { resolveAgent } = await Promise.resolve().then(() => print_exports);
|
|
3969
|
+
const client = createRestClient({
|
|
3970
|
+
appUrl: session.appUrl,
|
|
3971
|
+
sessionToken: session.sessionToken
|
|
3972
|
+
});
|
|
3973
|
+
let agent;
|
|
3974
|
+
try {
|
|
3975
|
+
agent = resolveAgent(await client.listAgents({
|
|
3976
|
+
scope: "org",
|
|
3977
|
+
onPage: null
|
|
3978
|
+
}), argv.agent ?? null);
|
|
3979
|
+
} catch (error) {
|
|
3980
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
3981
|
+
process.exit(1);
|
|
3982
|
+
}
|
|
3983
|
+
const command = joinCommandWords([...argv.command ?? [], ...argv["--"] ?? []]);
|
|
3984
|
+
const code = command ? await runExec({
|
|
3985
|
+
session,
|
|
3986
|
+
agentId: agent.id,
|
|
3987
|
+
command
|
|
3988
|
+
}) : await runPty({
|
|
3989
|
+
session,
|
|
3990
|
+
agentId: agent.id,
|
|
3991
|
+
agentName: agent.name
|
|
3992
|
+
});
|
|
3993
|
+
process.exit(code);
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3996
|
+
/** One-shot exec: stream output to stdout, resolve to the command's exit code. */
|
|
3997
|
+
function runExec({ session, agentId, command }) {
|
|
3998
|
+
console.error("Connecting to the sandbox…");
|
|
3999
|
+
return new Promise((resolve) => {
|
|
4000
|
+
const finish = (code) => {
|
|
4001
|
+
process.stdout.write("", () => resolve(code));
|
|
4002
|
+
};
|
|
4003
|
+
SandboxStream.open({
|
|
4004
|
+
mode: "exec",
|
|
4005
|
+
appUrl: session.appUrl,
|
|
4006
|
+
sessionToken: session.sessionToken,
|
|
4007
|
+
agentId,
|
|
4008
|
+
command,
|
|
4009
|
+
onEvent: (e) => {
|
|
4010
|
+
switch (e.type) {
|
|
4011
|
+
case "data":
|
|
4012
|
+
process.stdout.write(e.bytes);
|
|
4013
|
+
break;
|
|
4014
|
+
case "exit":
|
|
4015
|
+
finish(e.code);
|
|
4016
|
+
break;
|
|
4017
|
+
case "error":
|
|
4018
|
+
printError(e.message);
|
|
4019
|
+
finish(1);
|
|
4020
|
+
break;
|
|
4021
|
+
case "close":
|
|
4022
|
+
printError(e.failure ? `Could not run the command in the sandbox: ${e.failure}` : "The connection to the sandbox closed before the command finished.");
|
|
4023
|
+
finish(1);
|
|
4024
|
+
break;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
});
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
/** Live terminal on the caller's real TTY. */
|
|
4031
|
+
async function runPty({ session, agentId, agentName }) {
|
|
4032
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4033
|
+
printError("A live sandbox terminal needs an interactive TTY. For scripted use, pass a command: `skydive sandbox -- <cmd>`.");
|
|
4034
|
+
return 1;
|
|
4035
|
+
}
|
|
4036
|
+
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
4037
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-C1DXKms6.mjs").then((n) => n.t);
|
|
4038
|
+
const result = await runRawPtyPassthrough({
|
|
4039
|
+
stdin: process.stdin,
|
|
4040
|
+
stdout: process.stdout,
|
|
4041
|
+
appUrl: session.appUrl,
|
|
4042
|
+
sessionToken: session.sessionToken,
|
|
4043
|
+
agentId
|
|
4044
|
+
});
|
|
4045
|
+
if (result.reason === "detach") console.error("\nDetached.");
|
|
4046
|
+
return result.code;
|
|
4047
|
+
}
|
|
4048
|
+
|
|
3756
4049
|
//#endregion
|
|
3757
4050
|
//#region src/cli.ts
|
|
3758
4051
|
function createCli(argv) {
|
|
3759
|
-
return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
|
|
4052
|
+
return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").parserConfiguration({ "populate--": true }).example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch the workspace all `skydive` commands act on").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
|
|
3760
4053
|
type: "boolean",
|
|
3761
4054
|
default: false,
|
|
3762
4055
|
global: true,
|
|
@@ -3770,7 +4063,7 @@ function createCli(argv) {
|
|
|
3770
4063
|
type: "string",
|
|
3771
4064
|
global: true,
|
|
3772
4065
|
describe: "Override API base URL"
|
|
3773
|
-
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
4066
|
+
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
3774
4067
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
3775
4068
|
process.exit(1);
|
|
3776
4069
|
});
|
|
@@ -3844,4 +4137,4 @@ function resolveArgv(args, tty = {
|
|
|
3844
4137
|
createCli(resolveArgv(hideBin(process.argv))).parse();
|
|
3845
4138
|
|
|
3846
4139
|
//#endregion
|
|
3847
|
-
export {
|
|
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
|
|
@@ -6142,6 +5917,10 @@ function ChatScreen({ agent, conversation }) {
|
|
|
6142
5917
|
append(`\n${e.message}`);
|
|
6143
5918
|
stamp(1);
|
|
6144
5919
|
} else if (e.type === "exit") stamp(e.code);
|
|
5920
|
+
else if (e.type === "close") {
|
|
5921
|
+
append(`\n${e.failure ? `connection failed: ${e.failure}` : "connection closed before the command finished"}`);
|
|
5922
|
+
stamp(1);
|
|
5923
|
+
}
|
|
6145
5924
|
}
|
|
6146
5925
|
});
|
|
6147
5926
|
}, [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as __exportAll } from "./rolldown-runtime-Cz4Tg37Z.mjs";
|
|
3
|
-
import { a as
|
|
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 };
|