skydive-cli 0.1.0-beta.353 → 0.1.0-beta.378
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/README.md
CHANGED
|
@@ -8,9 +8,45 @@ npx skydive-cli --help
|
|
|
8
8
|
|
|
9
9
|
## Authentication
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Sign in once via the browser — the same shape as `doppler login`: the CLI
|
|
12
|
+
opens the approval page, shows a short code, waits for you to approve, and
|
|
13
|
+
then automatically mints an API key for this machine:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
skydive auth login
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That single flow yields both credentials the CLI uses, stored together in a
|
|
20
|
+
single `config.json`:
|
|
21
|
+
|
|
22
|
+
- an **API key** (`sky_live_…`) named after this machine and the workspace it
|
|
23
|
+
was minted in (`CLI (host) — Acme`) — the durable credential for the
|
|
24
|
+
management commands (`agents`, `keys`, `secrets`). Logging in again from the
|
|
25
|
+
same machine replaces it rather than accumulating rows, and
|
|
26
|
+
`skydive auth logout` revokes it, so signing out doesn't leave a live key
|
|
27
|
+
behind. The workspace is in the name because the key is pinned to it: unlike
|
|
28
|
+
the session, a key never follows `workspace switch`.
|
|
29
|
+
- a **user session** for `skydive chat`. Chat is user-level and multi-agent,
|
|
30
|
+
so it authenticates as you — and unlike the API key (which is pinned to the
|
|
31
|
+
workspace that minted it), the session follows `skydive workspace switch`.
|
|
32
|
+
The management commands prefer the session while it's valid and fall back
|
|
33
|
+
to the API key.
|
|
34
|
+
|
|
35
|
+
The flow doesn't require a TTY: in a non-interactive shell (an agent harness,
|
|
36
|
+
SSH without a display) `auth login` still prints the verification URL + code
|
|
37
|
+
and waits — open the link in any browser to approve. For unattended CI, skip
|
|
38
|
+
the wait entirely and pass an existing key (mint one at
|
|
39
|
+
`skydive.com/settings/account`):
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
skydive auth login --api-key sky_live_… # or set SKYDIVE_API_KEY
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`skydive auth status` shows both credentials; `skydive auth logout` revokes
|
|
46
|
+
the auto-minted key and clears them.
|
|
47
|
+
|
|
48
|
+
The config location is platform-conventional (resolved via `env-paths`), so
|
|
49
|
+
it is **not** `~/.config/skydive/` everywhere:
|
|
14
50
|
|
|
15
51
|
| Platform | Path |
|
|
16
52
|
| -------- | ---------------------------------------------------------------- |
|
|
@@ -21,29 +57,6 @@ location is platform-conventional (resolved via `env-paths`), so it is **not**
|
|
|
21
57
|
`skydive auth status` prints the real resolved path (`Config: …`) — trust that
|
|
22
58
|
over this table if they ever disagree.
|
|
23
59
|
|
|
24
|
-
- **API key** (`sky_live_…`). Mint one at `skydive.com/settings/account`, then:
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
skydive auth login # paste the key
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
- **User session** for `skydive chat`. Chat is user-level and multi-agent,
|
|
31
|
-
so it signs you in via the browser (device flow) rather than a per-agent
|
|
32
|
-
API key. `skydive chat` does this automatically on first run; you can also
|
|
33
|
-
do it up front:
|
|
34
|
-
|
|
35
|
-
```sh
|
|
36
|
-
skydive auth login --web # opens the browser, prompts for approval
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Either credential authenticates the management commands (`agents`, `keys`,
|
|
40
|
-
`secrets`): the CLI prefers the API key and falls back to the `--web` chat
|
|
41
|
-
session, so a `--web` login alone is enough to run them — you do **not** need a
|
|
42
|
-
separate API key. The API key is only required when you have no chat session
|
|
43
|
-
(e.g. CI). `skydive chat` requires the chat session specifically.
|
|
44
|
-
|
|
45
|
-
`skydive auth status` shows both; `skydive auth logout` clears them.
|
|
46
|
-
|
|
47
60
|
## Commands
|
|
48
61
|
|
|
49
62
|
```sh
|
|
@@ -164,7 +177,7 @@ skydive chat -p "status?" --agent grace --json # structured envelo
|
|
|
164
177
|
- `--conversation <id>` continues an existing thread; omitted, it starts a new
|
|
165
178
|
one. The conversation id is included in `--json` output for chaining.
|
|
166
179
|
- `-p` needs a user session just like `chat`, but never opens the interactive
|
|
167
|
-
browser login — sign in first with `skydive auth login
|
|
180
|
+
browser login — sign in first with `skydive auth login` or set
|
|
168
181
|
`SKYDIVE_SESSION_TOKEN`.
|
|
169
182
|
- `--json` prints `{ agentId, agentName, conversationId, isNewConversation,
|
|
170
183
|
messageId, text }` instead of streaming the raw text.
|
package/dist/js/bin.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { t as __exportAll } from "./rolldown-runtime-Cz4Tg37Z.mjs";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
4
|
import yargs from "yargs";
|
|
5
|
-
import {
|
|
5
|
+
import os, { hostname } from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import Conf from "conf";
|
|
8
8
|
import { err, ok } from "neverthrow";
|
|
@@ -13,10 +13,10 @@ import { spawnSync } from "node:child_process";
|
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import zlib from "node:zlib";
|
|
16
|
-
import
|
|
16
|
+
import { WebSocket } from "ws";
|
|
17
17
|
|
|
18
18
|
//#region package.json
|
|
19
|
-
var version$1 = "0.1.0-beta.
|
|
19
|
+
var version$1 = "0.1.0-beta.378";
|
|
20
20
|
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/types.ts
|
|
@@ -56,10 +56,18 @@ const DEFAULT_WEB_URL = "https://skydive.com";
|
|
|
56
56
|
function resolveWebUrl(appUrl) {
|
|
57
57
|
return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
|
|
58
58
|
}
|
|
59
|
-
/** Prefix on
|
|
59
|
+
/** Prefix on workspace-scoped Skydive API keys. Kept in sync with the API's
|
|
60
60
|
* `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
|
|
61
61
|
* standalone published package so it can't import the backend constant. */
|
|
62
62
|
const API_KEY_PREFIX = "sky_live_";
|
|
63
|
+
/**
|
|
64
|
+
* Common prefix across all Skydive API key kinds — `sky_live_…` workspace
|
|
65
|
+
* keys today, `sky_user_…` account keys when ANY-5105 lands. The CLI only
|
|
66
|
+
* sanity-checks the family on `--api-key`; the server authoritatively rejects
|
|
67
|
+
* a kind that can't drive a given route, with a clearer message than the
|
|
68
|
+
* client could produce.
|
|
69
|
+
*/
|
|
70
|
+
const API_KEY_FAMILY_PREFIX = "sky_";
|
|
63
71
|
/** Where users mint and copy API keys. Shown in the login prompt. */
|
|
64
72
|
const API_KEYS_URL = "skydive.com/settings/account";
|
|
65
73
|
const store = new Conf({
|
|
@@ -78,9 +86,9 @@ function resolveConfig(opts) {
|
|
|
78
86
|
}
|
|
79
87
|
/**
|
|
80
88
|
* Resolve the bearer credential for the management API (`agents` / `keys` /
|
|
81
|
-
* `secrets`). The server's `/v1` gate accepts either an API key or the
|
|
82
|
-
* session bearer, so both work — but only one of them tracks the
|
|
83
|
-
* workspace.
|
|
89
|
+
* `secrets`). The server's `/v1` gate accepts either an API key or the
|
|
90
|
+
* device-flow session bearer, so both work — but only one of them tracks the
|
|
91
|
+
* active workspace.
|
|
84
92
|
*
|
|
85
93
|
* An API key is pinned server-side to the organization that minted it and
|
|
86
94
|
* ignores the workspace header by design, so it can never follow `skydive
|
|
@@ -104,11 +112,17 @@ function resolveManagementAuth(opts) {
|
|
|
104
112
|
apiUrl: process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL,
|
|
105
113
|
kind: "api-key"
|
|
106
114
|
});
|
|
107
|
-
return err({ message: "Not authenticated. Run `skydive auth login
|
|
115
|
+
return err({ message: "Not authenticated. Run `skydive auth login`." });
|
|
108
116
|
}
|
|
109
117
|
function saveConfig(config) {
|
|
110
118
|
store.set("apiKey", config.apiKey);
|
|
111
119
|
store.set("apiUrl", config.apiUrl);
|
|
120
|
+
if (config.apiKeyId) store.set("apiKeyId", config.apiKeyId);
|
|
121
|
+
else store.delete("apiKeyId");
|
|
122
|
+
}
|
|
123
|
+
/** Server-side id of the auto-minted key, if login minted one. */
|
|
124
|
+
function getStoredApiKeyId() {
|
|
125
|
+
return store.get("apiKeyId") ?? null;
|
|
112
126
|
}
|
|
113
127
|
function deleteConfig() {
|
|
114
128
|
store.clear();
|
|
@@ -320,7 +334,7 @@ var SkydiveApiClient = class {
|
|
|
320
334
|
if (body.error && typeof body.error === "string") message = body.error;
|
|
321
335
|
else if (body.error?.message) message = body.error.message;
|
|
322
336
|
} catch {}
|
|
323
|
-
if (response.status === 401 && this.authKind === "session") message = `${message} — your chat session may have expired. Run \`skydive auth login
|
|
337
|
+
if (response.status === 401 && this.authKind === "session") message = `${message} — your chat session may have expired. Run \`skydive auth login\`.`;
|
|
324
338
|
return err({
|
|
325
339
|
message,
|
|
326
340
|
status: response.status
|
|
@@ -647,6 +661,94 @@ function sleep$1(ms) {
|
|
|
647
661
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
648
662
|
}
|
|
649
663
|
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/auth/mint-key.ts
|
|
666
|
+
const mintedKeySchema = z.object({
|
|
667
|
+
id: z.string(),
|
|
668
|
+
name: z.string(),
|
|
669
|
+
prefix: z.string(),
|
|
670
|
+
key: z.string()
|
|
671
|
+
});
|
|
672
|
+
/**
|
|
673
|
+
* Mint an org-scoped (account-level) API key off a fresh device-flow session,
|
|
674
|
+
* so `auth login` ends with a durable machine credential the way
|
|
675
|
+
* `doppler login` does. Calls the same internal endpoint the web settings page
|
|
676
|
+
* uses; no `agentId` is sent, so the key is account-level, not agent-bound.
|
|
677
|
+
*
|
|
678
|
+
* Scopes are `use` + `edit` because the management commands the key powers
|
|
679
|
+
* (`agents create`, `keys create`, `secrets set`) are edit-tier operations.
|
|
680
|
+
*/
|
|
681
|
+
async function mintCliApiKey({ appUrl, sessionToken, name }) {
|
|
682
|
+
try {
|
|
683
|
+
const res = await fetch(`${appUrl}/api/v1/api-keys`, {
|
|
684
|
+
method: "POST",
|
|
685
|
+
headers: {
|
|
686
|
+
authorization: `Bearer ${sessionToken}`,
|
|
687
|
+
"content-type": "application/json"
|
|
688
|
+
},
|
|
689
|
+
body: JSON.stringify({
|
|
690
|
+
name,
|
|
691
|
+
scopes: ["use", "edit"]
|
|
692
|
+
})
|
|
693
|
+
});
|
|
694
|
+
if (!res.ok) return err({ message: `failed to mint API key (${res.status})` });
|
|
695
|
+
const parsed = mintedKeySchema.safeParse(await res.json());
|
|
696
|
+
if (!parsed.success) return err({ message: "unexpected mint response" });
|
|
697
|
+
return ok(parsed.data);
|
|
698
|
+
} catch (e) {
|
|
699
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const keyListSchema = z.object({ keys: z.array(z.object({
|
|
703
|
+
id: z.string(),
|
|
704
|
+
name: z.string(),
|
|
705
|
+
agentId: z.string().nullish()
|
|
706
|
+
})) });
|
|
707
|
+
/**
|
|
708
|
+
* Revoke every active account-level key with the given name. Login calls this
|
|
709
|
+
* before minting so repeated logins from the same machine + workspace replace
|
|
710
|
+
* their key instead of piling up indistinguishable rows (`logout`'s revoke is
|
|
711
|
+
* best-effort, and login-without-logout is the normal case — a wiped
|
|
712
|
+
* container, an expired session). Agent-scoped keys are never touched even on
|
|
713
|
+
* a name collision.
|
|
714
|
+
*/
|
|
715
|
+
async function revokeKeysByName({ appUrl, sessionToken, name }) {
|
|
716
|
+
try {
|
|
717
|
+
const res = await fetch(`${appUrl}/api/v1/api-keys`, { headers: { authorization: `Bearer ${sessionToken}` } });
|
|
718
|
+
if (!res.ok) return err({ message: `failed to list API keys (${res.status})` });
|
|
719
|
+
const parsed = keyListSchema.safeParse(await res.json());
|
|
720
|
+
if (!parsed.success) return err({ message: "unexpected key list response" });
|
|
721
|
+
const matches = parsed.data.keys.filter((key) => key.name === name && !key.agentId);
|
|
722
|
+
const failures = (await Promise.all(matches.map((key) => revokeApiKey({
|
|
723
|
+
appUrl,
|
|
724
|
+
token: sessionToken,
|
|
725
|
+
id: key.id
|
|
726
|
+
})))).filter((result) => result.isErr());
|
|
727
|
+
if (failures.length > 0) return err({ message: `failed to revoke ${failures.length} of ${matches.length} existing keys` });
|
|
728
|
+
return ok(matches.length);
|
|
729
|
+
} catch (e) {
|
|
730
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Revoke an API key by id. Used by `auth logout` to clean up the key that
|
|
735
|
+
* login auto-minted, so signing out on a machine doesn't leave a live
|
|
736
|
+
* credential behind. `token` may be a session bearer or an API key — the
|
|
737
|
+
* server accepts both on this route.
|
|
738
|
+
*/
|
|
739
|
+
async function revokeApiKey({ appUrl, token, id }) {
|
|
740
|
+
try {
|
|
741
|
+
const res = await fetch(`${appUrl}/api/v1/api-keys/${id}`, {
|
|
742
|
+
method: "DELETE",
|
|
743
|
+
headers: { authorization: `Bearer ${token}` }
|
|
744
|
+
});
|
|
745
|
+
if (!res.ok && res.status !== 404) return err({ message: `failed to revoke API key (${res.status})` });
|
|
746
|
+
return ok(void 0);
|
|
747
|
+
} catch (e) {
|
|
748
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
650
752
|
//#endregion
|
|
651
753
|
//#region src/output.ts
|
|
652
754
|
function output(argv, data) {
|
|
@@ -676,99 +778,160 @@ function printError(message) {
|
|
|
676
778
|
//#region src/commands/auth.ts
|
|
677
779
|
const loginCommand = {
|
|
678
780
|
command: "login",
|
|
679
|
-
describe: "
|
|
781
|
+
describe: "Sign in via the browser (use --api-key for CI / headless)",
|
|
680
782
|
builder: (y) => y.option("api-key", {
|
|
681
783
|
type: "string",
|
|
682
|
-
describe: `API key (${API_KEY_PREFIX}...)`
|
|
784
|
+
describe: `Skip the browser and authenticate with an existing API key (${API_KEY_PREFIX}...)`
|
|
683
785
|
}).option("web", {
|
|
684
786
|
type: "boolean",
|
|
685
787
|
default: false,
|
|
686
|
-
|
|
788
|
+
hidden: true,
|
|
789
|
+
describe: "Deprecated: browser sign-in is now the default"
|
|
687
790
|
}),
|
|
688
791
|
handler: async (argv) => {
|
|
689
|
-
if (argv
|
|
690
|
-
await
|
|
792
|
+
if (argv["api-key"]) {
|
|
793
|
+
await runApiKeyLogin(argv, argv["api-key"]);
|
|
691
794
|
return;
|
|
692
795
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
apiKey = await new Promise((resolve) => {
|
|
700
|
-
rl.question(`Enter your API key (from ${API_KEYS_URL}): `, (answer) => {
|
|
701
|
-
rl.close();
|
|
702
|
-
resolve(answer.trim());
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
if (!apiKey) {
|
|
707
|
-
printError("No API key provided.");
|
|
708
|
-
process.exit(1);
|
|
709
|
-
}
|
|
710
|
-
if (!apiKey.startsWith(API_KEY_PREFIX)) {
|
|
711
|
-
printError(`API key must start with ${API_KEY_PREFIX}`);
|
|
712
|
-
process.exit(1);
|
|
713
|
-
}
|
|
714
|
-
const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
|
|
715
|
-
const result = await new SkydiveApiClient({
|
|
716
|
-
token: apiKey,
|
|
717
|
-
apiUrl,
|
|
718
|
-
kind: "api-key"
|
|
719
|
-
}).listAgents({
|
|
720
|
-
limit: 1,
|
|
721
|
-
scope: null
|
|
722
|
-
});
|
|
723
|
-
if (result.isErr()) {
|
|
724
|
-
printError(`Invalid API key or unreachable server: ${result.error.message}`);
|
|
725
|
-
process.exit(1);
|
|
796
|
+
if (isNonInteractive()) {
|
|
797
|
+
const envKey = process.env["SKYDIVE_API_KEY"];
|
|
798
|
+
if (envKey) {
|
|
799
|
+
await runApiKeyLogin(argv, envKey);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
726
802
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
803
|
+
await runBrowserLogin(argv);
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
/**
|
|
807
|
+
* Browser sign-in, shaped like `doppler login`: open the approval page with a
|
|
808
|
+
* short code, wait for the user to approve, then automatically mint an API
|
|
809
|
+
* key for this machine. One flow yields both credentials — the session (chat,
|
|
810
|
+
* workspace-following management calls) and a durable `sky_live_…` key that
|
|
811
|
+
* keeps headless/management use working after the session expires.
|
|
812
|
+
*
|
|
813
|
+
* Deliberately no TTY requirement: the flow only prints the verification URL
|
|
814
|
+
* + code and polls, so it also works from a non-interactive shell — an agent
|
|
815
|
+
* driving the CLI relays the URL to its human, who approves in any browser
|
|
816
|
+
* (the auto-open is best-effort). The cost is that an unattended run with no
|
|
817
|
+
* key configured waits out the device code's expiry instead of failing fast.
|
|
818
|
+
*/
|
|
819
|
+
async function runBrowserLogin(argv) {
|
|
820
|
+
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
821
|
+
const login = await loginWithDevice({ appUrl });
|
|
822
|
+
if (login.isErr()) {
|
|
823
|
+
printError(login.error.message);
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
const { sessionToken } = login.value;
|
|
827
|
+
const identity = (await getSessionIdentity({
|
|
828
|
+
appUrl,
|
|
829
|
+
sessionToken
|
|
830
|
+
})).unwrapOr(null);
|
|
831
|
+
const keyName = identity?.activeWorkspaceName ? `CLI (${hostname()}) — ${identity.activeWorkspaceName}` : `CLI (${hostname()})`;
|
|
832
|
+
const replaced = await revokeKeysByName({
|
|
833
|
+
appUrl,
|
|
834
|
+
sessionToken,
|
|
835
|
+
name: keyName
|
|
836
|
+
});
|
|
837
|
+
if (replaced.isErr()) process.stderr.write(`Warning: could not revoke this machine's previous API key (${replaced.error.message}); minting a new one anyway.\n`);
|
|
838
|
+
const minted = await mintCliApiKey({
|
|
839
|
+
appUrl,
|
|
840
|
+
sessionToken,
|
|
841
|
+
name: keyName
|
|
842
|
+
});
|
|
843
|
+
if (minted.isOk()) saveConfig({
|
|
844
|
+
apiKey: minted.value.key,
|
|
845
|
+
apiUrl: appUrl,
|
|
846
|
+
apiKeyId: minted.value.id
|
|
847
|
+
});
|
|
848
|
+
else process.stderr.write(`Warning: signed in, but could not mint an API key (${minted.error.message}). Management commands will use your session; run \`skydive auth login\` again to retry.
|
|
849
|
+
`);
|
|
850
|
+
if (argv.json) {
|
|
851
|
+
output(argv, {
|
|
732
852
|
authenticated: true,
|
|
733
|
-
|
|
853
|
+
mode: "browser",
|
|
854
|
+
appUrl,
|
|
855
|
+
prefix: minted.isOk() ? minted.value.prefix : null,
|
|
856
|
+
email: identity?.email ?? null,
|
|
857
|
+
workspaceName: identity?.activeWorkspaceName ?? null,
|
|
734
858
|
configPath: getConfigPath()
|
|
735
859
|
});
|
|
736
|
-
|
|
737
|
-
console.log(`Authenticated successfully.`);
|
|
738
|
-
console.log(` Key: ${apiKey.slice(0, 12)}...`);
|
|
739
|
-
console.log(` Config: ${getConfigPath()}`);
|
|
740
|
-
}
|
|
860
|
+
return;
|
|
741
861
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
862
|
+
if (argv.quiet) return;
|
|
863
|
+
const who = identity?.name ?? identity?.email;
|
|
864
|
+
console.log(who ? `Welcome, ${who}!` : "Signed in.");
|
|
865
|
+
if (identity?.activeWorkspaceName) console.log(` Workspace: ${identity.activeWorkspaceName}`);
|
|
866
|
+
if (minted.isOk()) console.log(` API key: ${minted.value.prefix}… (${minted.value.name})`);
|
|
867
|
+
console.log(` Config: ${getConfigPath()}`);
|
|
868
|
+
}
|
|
869
|
+
/** CI / headless path: validate and persist an existing key, no browser. */
|
|
870
|
+
async function runApiKeyLogin(argv, apiKey) {
|
|
871
|
+
if (!apiKey.startsWith(API_KEY_FAMILY_PREFIX)) {
|
|
872
|
+
printError(`API key must start with ${API_KEY_FAMILY_PREFIX}`);
|
|
746
873
|
process.exit(1);
|
|
747
874
|
}
|
|
748
|
-
const
|
|
875
|
+
const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
|
|
876
|
+
const result = await new SkydiveApiClient({
|
|
877
|
+
token: apiKey,
|
|
878
|
+
apiUrl,
|
|
879
|
+
kind: "api-key"
|
|
880
|
+
}).listAgents({
|
|
881
|
+
limit: 1,
|
|
882
|
+
scope: null
|
|
883
|
+
});
|
|
749
884
|
if (result.isErr()) {
|
|
750
|
-
printError(result.error.message);
|
|
885
|
+
printError(`Invalid API key or unreachable server: ${result.error.message}`);
|
|
751
886
|
process.exit(1);
|
|
752
887
|
}
|
|
888
|
+
saveConfig({
|
|
889
|
+
apiKey,
|
|
890
|
+
apiUrl,
|
|
891
|
+
apiKeyId: null
|
|
892
|
+
});
|
|
753
893
|
if (argv.json) output(argv, {
|
|
754
894
|
authenticated: true,
|
|
755
|
-
|
|
756
|
-
appUrl: result.value.appUrl,
|
|
895
|
+
prefix: apiKey.slice(0, 12),
|
|
757
896
|
configPath: getConfigPath()
|
|
758
897
|
});
|
|
759
898
|
else if (!argv.quiet) {
|
|
760
|
-
console.log(
|
|
761
|
-
console.log(`
|
|
899
|
+
console.log(`Authenticated successfully.`);
|
|
900
|
+
console.log(` Key: ${apiKey.slice(0, 12)}...`);
|
|
762
901
|
console.log(` Config: ${getConfigPath()}`);
|
|
763
902
|
}
|
|
764
903
|
}
|
|
765
904
|
const logoutCommand = {
|
|
766
905
|
command: "logout",
|
|
767
|
-
describe: "
|
|
906
|
+
describe: "Revoke the auto-minted API key and clear stored credentials",
|
|
768
907
|
handler: async (argv) => {
|
|
908
|
+
const keyId = getStoredApiKeyId();
|
|
909
|
+
let keyRevoked = false;
|
|
910
|
+
if (keyId) {
|
|
911
|
+
const session = resolveSession({ appUrl: argv["api-url"] });
|
|
912
|
+
const config = resolveConfig({ apiUrl: argv["api-url"] });
|
|
913
|
+
const credential = session.isOk() ? {
|
|
914
|
+
appUrl: session.value.appUrl,
|
|
915
|
+
token: session.value.sessionToken
|
|
916
|
+
} : config.isOk() ? {
|
|
917
|
+
appUrl: config.value.apiUrl,
|
|
918
|
+
token: config.value.apiKey
|
|
919
|
+
} : null;
|
|
920
|
+
if (credential) {
|
|
921
|
+
const revoked = await revokeApiKey({
|
|
922
|
+
...credential,
|
|
923
|
+
id: keyId
|
|
924
|
+
});
|
|
925
|
+
keyRevoked = revoked.isOk();
|
|
926
|
+
if (revoked.isErr()) process.stderr.write(`Warning: could not revoke this machine's API key (${revoked.error.message}). Revoke it manually at ${API_KEYS_URL}.\n`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
769
929
|
deleteConfig();
|
|
770
|
-
if (argv.json) output(argv, {
|
|
771
|
-
|
|
930
|
+
if (argv.json) output(argv, {
|
|
931
|
+
authenticated: false,
|
|
932
|
+
keyRevoked
|
|
933
|
+
});
|
|
934
|
+
else if (!argv.quiet) console.log(keyRevoked ? "Logged out. API key revoked." : "Logged out.");
|
|
772
935
|
}
|
|
773
936
|
};
|
|
774
937
|
const statusCommand$1 = {
|
|
@@ -801,7 +964,7 @@ const statusCommand$1 = {
|
|
|
801
964
|
return;
|
|
802
965
|
}
|
|
803
966
|
if (apiKey.isErr() && session.isErr()) {
|
|
804
|
-
console.log("Not authenticated. Run `skydive auth login
|
|
967
|
+
console.log("Not authenticated. Run `skydive auth login`.");
|
|
805
968
|
return;
|
|
806
969
|
}
|
|
807
970
|
if (apiKey.isOk()) {
|
|
@@ -838,6 +1001,10 @@ const authCommand = {
|
|
|
838
1001
|
|
|
839
1002
|
//#endregion
|
|
840
1003
|
//#region src/chat/api/rest.ts
|
|
1004
|
+
var rest_exports = /* @__PURE__ */ __exportAll({
|
|
1005
|
+
HttpError: () => HttpError,
|
|
1006
|
+
createRestClient: () => createRestClient
|
|
1007
|
+
});
|
|
841
1008
|
var HttpError = class extends Error {
|
|
842
1009
|
constructor(status, body) {
|
|
843
1010
|
super(`HTTP ${status}: ${body.slice(0, 200)}`);
|
|
@@ -1257,7 +1424,7 @@ const conversationStreamEventSchema = z.discriminatedUnion("kind", [z.object({
|
|
|
1257
1424
|
function requireSession(argv) {
|
|
1258
1425
|
const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1259
1426
|
if (session.isErr()) {
|
|
1260
|
-
printError(`${session.error.message} Run \`skydive auth login
|
|
1427
|
+
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1261
1428
|
process.exit(1);
|
|
1262
1429
|
}
|
|
1263
1430
|
return session.value;
|
|
@@ -2592,7 +2759,7 @@ const chatCommand = {
|
|
|
2592
2759
|
let session = resolveSession({ appUrl });
|
|
2593
2760
|
if (session.isErr()) {
|
|
2594
2761
|
if (isNonInteractive()) {
|
|
2595
|
-
printError("Not signed in for chat and no interactive terminal. Run `skydive auth login
|
|
2762
|
+
printError("Not signed in for chat and no interactive terminal. Run `skydive auth login`, or set SKYDIVE_SESSION_TOKEN.");
|
|
2596
2763
|
process.exit(1);
|
|
2597
2764
|
}
|
|
2598
2765
|
const login = await loginWithDevice({ appUrl });
|
|
@@ -2611,7 +2778,7 @@ const chatCommand = {
|
|
|
2611
2778
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2612
2779
|
process.exit(1);
|
|
2613
2780
|
}
|
|
2614
|
-
const { runChat } = await import("./boot-
|
|
2781
|
+
const { runChat } = await import("./boot-QnNiy1cA.mjs");
|
|
2615
2782
|
await runChat({
|
|
2616
2783
|
appUrl,
|
|
2617
2784
|
sessionToken: session.value.sessionToken,
|
|
@@ -2635,7 +2802,7 @@ function resolveShareMachine(argv) {
|
|
|
2635
2802
|
async function runPrintMode({ argv, appUrl }) {
|
|
2636
2803
|
const session = resolveSession({ appUrl });
|
|
2637
2804
|
if (session.isErr()) {
|
|
2638
|
-
printError(`${session.error.message} For non-interactive use, run \`skydive auth login
|
|
2805
|
+
printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2639
2806
|
process.exit(1);
|
|
2640
2807
|
}
|
|
2641
2808
|
const { runPrint, readStdin } = await Promise.resolve().then(() => print_exports);
|
|
@@ -2653,7 +2820,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2653
2820
|
}
|
|
2654
2821
|
let machineShare = null;
|
|
2655
2822
|
if (resolveShareMachine(argv)) {
|
|
2656
|
-
const { PortalClient } = await import("./client-
|
|
2823
|
+
const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
|
|
2657
2824
|
let signalConnected;
|
|
2658
2825
|
const connected = new Promise((resolve) => {
|
|
2659
2826
|
signalConnected = resolve;
|
|
@@ -2723,7 +2890,7 @@ const getCommand = {
|
|
|
2723
2890
|
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
2724
2891
|
const session = resolveSession({ appUrl });
|
|
2725
2892
|
if (session.isErr()) {
|
|
2726
|
-
printError(`${session.error.message} Run \`skydive auth login
|
|
2893
|
+
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2727
2894
|
process.exit(1);
|
|
2728
2895
|
}
|
|
2729
2896
|
const { messageGet } = await Promise.resolve().then(() => print_exports);
|
|
@@ -3405,7 +3572,7 @@ const switchCommand = {
|
|
|
3405
3572
|
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
3573
|
process.exit(1);
|
|
3407
3574
|
}
|
|
3408
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
3575
|
+
const { runWorkspacePicker } = await import("./boot-QnNiy1cA.mjs");
|
|
3409
3576
|
await runWorkspacePicker(session);
|
|
3410
3577
|
return;
|
|
3411
3578
|
}
|
|
@@ -3622,7 +3789,7 @@ const openCommand = {
|
|
|
3622
3789
|
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
3623
3790
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
3624
3791
|
const { machineName } = machineIdentity();
|
|
3625
|
-
const { PortalClient } = await import("./client-
|
|
3792
|
+
const { PortalClient } = await import("./client-XFsd0Wy9.mjs").then((n) => n.n);
|
|
3626
3793
|
let lastLine = "";
|
|
3627
3794
|
let signalConnected;
|
|
3628
3795
|
const connected = new Promise((resolve) => {
|
|
@@ -3753,10 +3920,298 @@ const portalCommand = {
|
|
|
3753
3920
|
handler: () => {}
|
|
3754
3921
|
};
|
|
3755
3922
|
|
|
3923
|
+
//#endregion
|
|
3924
|
+
//#region ../sandbox-stream-protocol/src/index.ts
|
|
3925
|
+
const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
|
|
3926
|
+
const FRAME = {
|
|
3927
|
+
DATA: 1,
|
|
3928
|
+
EXIT: 2,
|
|
3929
|
+
ERROR: 3,
|
|
3930
|
+
INPUT: 16,
|
|
3931
|
+
RESIZE: 17
|
|
3932
|
+
};
|
|
3933
|
+
const MAX_INPUT_BYTES = 1 * 1024 * 1024;
|
|
3934
|
+
/** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
|
|
3935
|
+
function streamSpecToQuery(spec) {
|
|
3936
|
+
if (spec.mode === "pty") return {
|
|
3937
|
+
agentId: spec.agentId,
|
|
3938
|
+
mode: "pty",
|
|
3939
|
+
cols: String(spec.cols),
|
|
3940
|
+
rows: String(spec.rows)
|
|
3941
|
+
};
|
|
3942
|
+
return {
|
|
3943
|
+
agentId: spec.agentId,
|
|
3944
|
+
mode: "exec",
|
|
3945
|
+
command: spec.command
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
function withType(type, payload) {
|
|
3949
|
+
const frame = new Uint8Array(1 + payload.length);
|
|
3950
|
+
frame[0] = type;
|
|
3951
|
+
frame.set(payload, 1);
|
|
3952
|
+
return frame;
|
|
3953
|
+
}
|
|
3954
|
+
/** client → server: keystroke bytes for the pty stdin. */
|
|
3955
|
+
function encodeInput(data) {
|
|
3956
|
+
return withType(FRAME.INPUT, data);
|
|
3957
|
+
}
|
|
3958
|
+
/** client → server: the client terminal was resized. */
|
|
3959
|
+
function encodeResize(cols, rows) {
|
|
3960
|
+
const frame = new Uint8Array(5);
|
|
3961
|
+
frame[0] = FRAME.RESIZE;
|
|
3962
|
+
const view = new DataView(frame.buffer);
|
|
3963
|
+
view.setUint16(1, cols & 65535);
|
|
3964
|
+
view.setUint16(3, rows & 65535);
|
|
3965
|
+
return frame;
|
|
3966
|
+
}
|
|
3967
|
+
const view = (frame) => new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
3968
|
+
/**
|
|
3969
|
+
* Decode a frame the server sent. Returns null for an empty, unknown, or
|
|
3970
|
+
* truncated frame — a peer speaking a newer protocol must not crash us.
|
|
3971
|
+
*/
|
|
3972
|
+
function decodeServerFrame(frame) {
|
|
3973
|
+
const payload = frame.subarray(1);
|
|
3974
|
+
switch (frame[0]) {
|
|
3975
|
+
case FRAME.DATA: return {
|
|
3976
|
+
type: "data",
|
|
3977
|
+
payload
|
|
3978
|
+
};
|
|
3979
|
+
case FRAME.EXIT: return {
|
|
3980
|
+
type: "exit",
|
|
3981
|
+
code: payload.length >= 4 ? view(frame).getInt32(1) : 0
|
|
3982
|
+
};
|
|
3983
|
+
case FRAME.ERROR: return {
|
|
3984
|
+
type: "error",
|
|
3985
|
+
message: new TextDecoder().decode(payload)
|
|
3986
|
+
};
|
|
3987
|
+
default: return null;
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
//#endregion
|
|
3992
|
+
//#region src/chat/sandbox/client.ts
|
|
3993
|
+
function wsBase(appUrl) {
|
|
3994
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
3995
|
+
if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
|
|
3996
|
+
if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
|
|
3997
|
+
return `wss://${base}`;
|
|
3998
|
+
}
|
|
3999
|
+
/**
|
|
4000
|
+
* A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
|
|
4001
|
+
* the write side (keystrokes / resize for pty mode) and teardown.
|
|
4002
|
+
*/
|
|
4003
|
+
var SandboxStream = class SandboxStream {
|
|
4004
|
+
ws;
|
|
4005
|
+
closed = false;
|
|
4006
|
+
constructor(ws, onEvent) {
|
|
4007
|
+
this.ws = ws;
|
|
4008
|
+
let ended = false;
|
|
4009
|
+
const emitEnd = (event) => {
|
|
4010
|
+
if (ended) return;
|
|
4011
|
+
ended = true;
|
|
4012
|
+
onEvent(event);
|
|
4013
|
+
};
|
|
4014
|
+
ws.on("message", (data, isBinary) => {
|
|
4015
|
+
if (!isBinary) return;
|
|
4016
|
+
const frame = decodeServerFrame(toBuffer(data));
|
|
4017
|
+
if (!frame) return;
|
|
4018
|
+
switch (frame.type) {
|
|
4019
|
+
case "data":
|
|
4020
|
+
onEvent({
|
|
4021
|
+
type: "data",
|
|
4022
|
+
bytes: new Uint8Array(frame.payload)
|
|
4023
|
+
});
|
|
4024
|
+
break;
|
|
4025
|
+
case "exit":
|
|
4026
|
+
emitEnd({
|
|
4027
|
+
type: "exit",
|
|
4028
|
+
code: frame.code
|
|
4029
|
+
});
|
|
4030
|
+
break;
|
|
4031
|
+
case "error":
|
|
4032
|
+
emitEnd({
|
|
4033
|
+
type: "error",
|
|
4034
|
+
message: frame.message
|
|
4035
|
+
});
|
|
4036
|
+
break;
|
|
4037
|
+
}
|
|
4038
|
+
});
|
|
4039
|
+
let failure = null;
|
|
4040
|
+
ws.on("error", (err) => {
|
|
4041
|
+
failure = err.message;
|
|
4042
|
+
});
|
|
4043
|
+
ws.on("close", () => {
|
|
4044
|
+
this.closed = true;
|
|
4045
|
+
emitEnd({
|
|
4046
|
+
type: "close",
|
|
4047
|
+
failure
|
|
4048
|
+
});
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
/** Feed keystroke bytes to the pty stdin. */
|
|
4052
|
+
sendInput(data) {
|
|
4053
|
+
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
4054
|
+
this.ws.send(encodeInput(data));
|
|
4055
|
+
}
|
|
4056
|
+
/** Notify the pty of a terminal resize. */
|
|
4057
|
+
resize(cols, rows) {
|
|
4058
|
+
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
4059
|
+
this.ws.send(encodeResize(cols, rows));
|
|
4060
|
+
}
|
|
4061
|
+
close() {
|
|
4062
|
+
this.closed = true;
|
|
4063
|
+
this.ws.close();
|
|
4064
|
+
}
|
|
4065
|
+
/** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
|
|
4066
|
+
static open(opts) {
|
|
4067
|
+
const spec = opts.mode === "pty" ? {
|
|
4068
|
+
mode: "pty",
|
|
4069
|
+
agentId: opts.agentId,
|
|
4070
|
+
cols: opts.cols,
|
|
4071
|
+
rows: opts.rows
|
|
4072
|
+
} : {
|
|
4073
|
+
mode: "exec",
|
|
4074
|
+
agentId: opts.agentId,
|
|
4075
|
+
command: opts.command
|
|
4076
|
+
};
|
|
4077
|
+
const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
|
|
4078
|
+
for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
|
|
4079
|
+
return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
|
|
4080
|
+
}
|
|
4081
|
+
};
|
|
4082
|
+
function toBuffer(data) {
|
|
4083
|
+
if (Buffer.isBuffer(data)) return data;
|
|
4084
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
4085
|
+
return Buffer.from(data);
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
//#endregion
|
|
4089
|
+
//#region src/commands/sandbox.ts
|
|
4090
|
+
/** POSIX single-quote one word so the remote shell treats it as one token. */
|
|
4091
|
+
function shellQuote(word) {
|
|
4092
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word)) return word;
|
|
4093
|
+
return `'${word.replace(/'/g, `'\\''`)}'`;
|
|
4094
|
+
}
|
|
4095
|
+
/**
|
|
4096
|
+
* Serialize the command words into the single shell string the relay runs.
|
|
4097
|
+
*
|
|
4098
|
+
* Words arrive already split by the caller's shell, so each must be re-quoted
|
|
4099
|
+
* or the remote shell re-splits any word containing spaces or metacharacters:
|
|
4100
|
+
* `sandbox -- sh -c 'echo hi; whoami'` would otherwise run `sh -c echo hi`
|
|
4101
|
+
* and then, separately, `whoami`. A pipeline still works the explicit way —
|
|
4102
|
+
* `sandbox -- sh -c 'ls | wc -l'` — which is what `docker`/`kubectl exec` ask
|
|
4103
|
+
* for too. yargs number-coerces bare numerals, so words are stringified.
|
|
4104
|
+
*/
|
|
4105
|
+
function joinCommandWords(words) {
|
|
4106
|
+
return words.map(String).map(shellQuote).join(" ").trim();
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* `skydive sandbox` — the standalone counterpart to the chat TUI's `/sandbox`
|
|
4110
|
+
* composer command (ANY-4928): a live terminal (or one-shot exec) in the
|
|
4111
|
+
* agent's own sandbox without opening the TUI. Runs under Node (no Bun/
|
|
4112
|
+
* OpenTUI): the PTY is a raw byte passthrough on the caller's real terminal.
|
|
4113
|
+
* The relay gates on EDIT access to the agent and the sandbox-terminal-enabled
|
|
4114
|
+
* kill switch, and boots the sandbox when it isn't running.
|
|
4115
|
+
*/
|
|
4116
|
+
const sandboxCommand = {
|
|
4117
|
+
command: "sandbox [command..]",
|
|
4118
|
+
describe: "Open a live terminal in an agent's sandbox, or run a one-shot command there",
|
|
4119
|
+
builder: (y) => y.option("agent", {
|
|
4120
|
+
type: "string",
|
|
4121
|
+
describe: "Target agent, by id, slug, or name. Optional when the account has exactly one agent."
|
|
4122
|
+
}).positional("command", {
|
|
4123
|
+
type: "string",
|
|
4124
|
+
array: true,
|
|
4125
|
+
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."
|
|
4126
|
+
}).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`"),
|
|
4127
|
+
handler: async (argv) => {
|
|
4128
|
+
const session = requireSession(argv);
|
|
4129
|
+
const { createRestClient } = await Promise.resolve().then(() => rest_exports);
|
|
4130
|
+
const { resolveAgent } = await Promise.resolve().then(() => print_exports);
|
|
4131
|
+
const client = createRestClient({
|
|
4132
|
+
appUrl: session.appUrl,
|
|
4133
|
+
sessionToken: session.sessionToken
|
|
4134
|
+
});
|
|
4135
|
+
let agent;
|
|
4136
|
+
try {
|
|
4137
|
+
agent = resolveAgent(await client.listAgents({
|
|
4138
|
+
scope: "org",
|
|
4139
|
+
onPage: null
|
|
4140
|
+
}), argv.agent ?? null);
|
|
4141
|
+
} catch (error) {
|
|
4142
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
4143
|
+
process.exit(1);
|
|
4144
|
+
}
|
|
4145
|
+
const command = joinCommandWords([...argv.command ?? [], ...argv["--"] ?? []]);
|
|
4146
|
+
const code = command ? await runExec({
|
|
4147
|
+
session,
|
|
4148
|
+
agentId: agent.id,
|
|
4149
|
+
command
|
|
4150
|
+
}) : await runPty({
|
|
4151
|
+
session,
|
|
4152
|
+
agentId: agent.id,
|
|
4153
|
+
agentName: agent.name
|
|
4154
|
+
});
|
|
4155
|
+
process.exit(code);
|
|
4156
|
+
}
|
|
4157
|
+
};
|
|
4158
|
+
/** One-shot exec: stream output to stdout, resolve to the command's exit code. */
|
|
4159
|
+
function runExec({ session, agentId, command }) {
|
|
4160
|
+
console.error("Connecting to the sandbox…");
|
|
4161
|
+
return new Promise((resolve) => {
|
|
4162
|
+
const finish = (code) => {
|
|
4163
|
+
process.stdout.write("", () => resolve(code));
|
|
4164
|
+
};
|
|
4165
|
+
SandboxStream.open({
|
|
4166
|
+
mode: "exec",
|
|
4167
|
+
appUrl: session.appUrl,
|
|
4168
|
+
sessionToken: session.sessionToken,
|
|
4169
|
+
agentId,
|
|
4170
|
+
command,
|
|
4171
|
+
onEvent: (e) => {
|
|
4172
|
+
switch (e.type) {
|
|
4173
|
+
case "data":
|
|
4174
|
+
process.stdout.write(e.bytes);
|
|
4175
|
+
break;
|
|
4176
|
+
case "exit":
|
|
4177
|
+
finish(e.code);
|
|
4178
|
+
break;
|
|
4179
|
+
case "error":
|
|
4180
|
+
printError(e.message);
|
|
4181
|
+
finish(1);
|
|
4182
|
+
break;
|
|
4183
|
+
case "close":
|
|
4184
|
+
printError(e.failure ? `Could not run the command in the sandbox: ${e.failure}` : "The connection to the sandbox closed before the command finished.");
|
|
4185
|
+
finish(1);
|
|
4186
|
+
break;
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
});
|
|
4190
|
+
});
|
|
4191
|
+
}
|
|
4192
|
+
/** Live terminal on the caller's real TTY. */
|
|
4193
|
+
async function runPty({ session, agentId, agentName }) {
|
|
4194
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4195
|
+
printError("A live sandbox terminal needs an interactive TTY. For scripted use, pass a command: `skydive sandbox -- <cmd>`.");
|
|
4196
|
+
return 1;
|
|
4197
|
+
}
|
|
4198
|
+
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
4199
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-C1DXKms6.mjs").then((n) => n.t);
|
|
4200
|
+
const result = await runRawPtyPassthrough({
|
|
4201
|
+
stdin: process.stdin,
|
|
4202
|
+
stdout: process.stdout,
|
|
4203
|
+
appUrl: session.appUrl,
|
|
4204
|
+
sessionToken: session.sessionToken,
|
|
4205
|
+
agentId
|
|
4206
|
+
});
|
|
4207
|
+
if (result.reason === "detach") console.error("\nDetached.");
|
|
4208
|
+
return result.code;
|
|
4209
|
+
}
|
|
4210
|
+
|
|
3756
4211
|
//#endregion
|
|
3757
4212
|
//#region src/cli.ts
|
|
3758
4213
|
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", {
|
|
4214
|
+
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
4215
|
type: "boolean",
|
|
3761
4216
|
default: false,
|
|
3762
4217
|
global: true,
|
|
@@ -3770,7 +4225,7 @@ function createCli(argv) {
|
|
|
3770
4225
|
type: "string",
|
|
3771
4226
|
global: true,
|
|
3772
4227
|
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) => {
|
|
4228
|
+
}).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
4229
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
3775
4230
|
process.exit(1);
|
|
3776
4231
|
});
|
|
@@ -3844,4 +4299,4 @@ function resolveArgv(args, tty = {
|
|
|
3844
4299
|
createCli(resolveArgv(hideBin(process.argv))).parse();
|
|
3845
4300
|
|
|
3846
4301
|
//#endregion
|
|
3847
|
-
export {
|
|
4302
|
+
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,19 +1,19 @@
|
|
|
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";
|
|
5
|
+
import * as os$1 from "node:os";
|
|
6
|
+
import { homedir, platform, release, tmpdir } from "node:os";
|
|
4
7
|
import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
|
|
5
8
|
import { z } from "zod";
|
|
6
9
|
import open from "open";
|
|
7
10
|
import { execFile, spawn } from "node:child_process";
|
|
8
11
|
import { createHash } from "node:crypto";
|
|
9
12
|
import { constants } from "node:fs";
|
|
10
|
-
import * as os$1 from "node:os";
|
|
11
|
-
import { homedir, platform, release, tmpdir } from "node:os";
|
|
12
13
|
import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks } from "@opentui/core";
|
|
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 };
|