skydive-cli 0.1.0-beta.363 → 0.1.0-beta.380
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 +40 -27
- package/dist/js/bin.mjs +259 -82
- package/dist/js/{boot-C1WUzzUo.mjs → boot-DML2xNb6.mjs} +55 -13
- package/package.json +1 -1
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,11 +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 os from "node:os";
|
|
17
16
|
import { WebSocket } from "ws";
|
|
18
17
|
|
|
19
18
|
//#region package.json
|
|
20
|
-
var version$1 = "0.1.0-beta.
|
|
19
|
+
var version$1 = "0.1.0-beta.380";
|
|
21
20
|
|
|
22
21
|
//#endregion
|
|
23
22
|
//#region src/types.ts
|
|
@@ -57,10 +56,18 @@ const DEFAULT_WEB_URL = "https://skydive.com";
|
|
|
57
56
|
function resolveWebUrl(appUrl) {
|
|
58
57
|
return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
|
|
59
58
|
}
|
|
60
|
-
/** Prefix on
|
|
59
|
+
/** Prefix on workspace-scoped Skydive API keys. Kept in sync with the API's
|
|
61
60
|
* `API_KEY_PREFIX` (`apps/anyone/api/src/lib/api-key.ts`); the CLI is a
|
|
62
61
|
* standalone published package so it can't import the backend constant. */
|
|
63
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_";
|
|
64
71
|
/** Where users mint and copy API keys. Shown in the login prompt. */
|
|
65
72
|
const API_KEYS_URL = "skydive.com/settings/account";
|
|
66
73
|
const store = new Conf({
|
|
@@ -79,9 +86,9 @@ function resolveConfig(opts) {
|
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
88
|
* Resolve the bearer credential for the management API (`agents` / `keys` /
|
|
82
|
-
* `secrets`). The server's `/v1` gate accepts either an API key or the
|
|
83
|
-
* session bearer, so both work — but only one of them tracks the
|
|
84
|
-
* 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.
|
|
85
92
|
*
|
|
86
93
|
* An API key is pinned server-side to the organization that minted it and
|
|
87
94
|
* ignores the workspace header by design, so it can never follow `skydive
|
|
@@ -105,11 +112,17 @@ function resolveManagementAuth(opts) {
|
|
|
105
112
|
apiUrl: process.env["SKYDIVE_API_URL"] ?? opts.apiUrl ?? store.get("apiUrl") ?? DEFAULT_API_URL,
|
|
106
113
|
kind: "api-key"
|
|
107
114
|
});
|
|
108
|
-
return err({ message: "Not authenticated. Run `skydive auth login
|
|
115
|
+
return err({ message: "Not authenticated. Run `skydive auth login`." });
|
|
109
116
|
}
|
|
110
117
|
function saveConfig(config) {
|
|
111
118
|
store.set("apiKey", config.apiKey);
|
|
112
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;
|
|
113
126
|
}
|
|
114
127
|
function deleteConfig() {
|
|
115
128
|
store.clear();
|
|
@@ -321,7 +334,7 @@ var SkydiveApiClient = class {
|
|
|
321
334
|
if (body.error && typeof body.error === "string") message = body.error;
|
|
322
335
|
else if (body.error?.message) message = body.error.message;
|
|
323
336
|
} catch {}
|
|
324
|
-
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\`.`;
|
|
325
338
|
return err({
|
|
326
339
|
message,
|
|
327
340
|
status: response.status
|
|
@@ -648,6 +661,94 @@ function sleep$1(ms) {
|
|
|
648
661
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
649
662
|
}
|
|
650
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
|
+
|
|
651
752
|
//#endregion
|
|
652
753
|
//#region src/output.ts
|
|
653
754
|
function output(argv, data) {
|
|
@@ -677,99 +778,160 @@ function printError(message) {
|
|
|
677
778
|
//#region src/commands/auth.ts
|
|
678
779
|
const loginCommand = {
|
|
679
780
|
command: "login",
|
|
680
|
-
describe: "
|
|
781
|
+
describe: "Sign in via the browser (use --api-key for CI / headless)",
|
|
681
782
|
builder: (y) => y.option("api-key", {
|
|
682
783
|
type: "string",
|
|
683
|
-
describe: `API key (${API_KEY_PREFIX}...)`
|
|
784
|
+
describe: `Skip the browser and authenticate with an existing API key (${API_KEY_PREFIX}...)`
|
|
684
785
|
}).option("web", {
|
|
685
786
|
type: "boolean",
|
|
686
787
|
default: false,
|
|
687
|
-
|
|
788
|
+
hidden: true,
|
|
789
|
+
describe: "Deprecated: browser sign-in is now the default"
|
|
688
790
|
}),
|
|
689
791
|
handler: async (argv) => {
|
|
690
|
-
if (argv
|
|
691
|
-
await
|
|
792
|
+
if (argv["api-key"]) {
|
|
793
|
+
await runApiKeyLogin(argv, argv["api-key"]);
|
|
692
794
|
return;
|
|
693
795
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
700
|
-
apiKey = await new Promise((resolve) => {
|
|
701
|
-
rl.question(`Enter your API key (from ${API_KEYS_URL}): `, (answer) => {
|
|
702
|
-
rl.close();
|
|
703
|
-
resolve(answer.trim());
|
|
704
|
-
});
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
if (!apiKey) {
|
|
708
|
-
printError("No API key provided.");
|
|
709
|
-
process.exit(1);
|
|
710
|
-
}
|
|
711
|
-
if (!apiKey.startsWith(API_KEY_PREFIX)) {
|
|
712
|
-
printError(`API key must start with ${API_KEY_PREFIX}`);
|
|
713
|
-
process.exit(1);
|
|
714
|
-
}
|
|
715
|
-
const apiUrl = argv["api-url"] ?? DEFAULT_API_URL;
|
|
716
|
-
const result = await new SkydiveApiClient({
|
|
717
|
-
token: apiKey,
|
|
718
|
-
apiUrl,
|
|
719
|
-
kind: "api-key"
|
|
720
|
-
}).listAgents({
|
|
721
|
-
limit: 1,
|
|
722
|
-
scope: null
|
|
723
|
-
});
|
|
724
|
-
if (result.isErr()) {
|
|
725
|
-
printError(`Invalid API key or unreachable server: ${result.error.message}`);
|
|
726
|
-
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
|
+
}
|
|
727
802
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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, {
|
|
733
852
|
authenticated: true,
|
|
734
|
-
|
|
853
|
+
mode: "browser",
|
|
854
|
+
appUrl,
|
|
855
|
+
prefix: minted.isOk() ? minted.value.prefix : null,
|
|
856
|
+
email: identity?.email ?? null,
|
|
857
|
+
workspaceName: identity?.activeWorkspaceName ?? null,
|
|
735
858
|
configPath: getConfigPath()
|
|
736
859
|
});
|
|
737
|
-
|
|
738
|
-
console.log(`Authenticated successfully.`);
|
|
739
|
-
console.log(` Key: ${apiKey.slice(0, 12)}...`);
|
|
740
|
-
console.log(` Config: ${getConfigPath()}`);
|
|
741
|
-
}
|
|
860
|
+
return;
|
|
742
861
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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}`);
|
|
747
873
|
process.exit(1);
|
|
748
874
|
}
|
|
749
|
-
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
|
+
});
|
|
750
884
|
if (result.isErr()) {
|
|
751
|
-
printError(result.error.message);
|
|
885
|
+
printError(`Invalid API key or unreachable server: ${result.error.message}`);
|
|
752
886
|
process.exit(1);
|
|
753
887
|
}
|
|
888
|
+
saveConfig({
|
|
889
|
+
apiKey,
|
|
890
|
+
apiUrl,
|
|
891
|
+
apiKeyId: null
|
|
892
|
+
});
|
|
754
893
|
if (argv.json) output(argv, {
|
|
755
894
|
authenticated: true,
|
|
756
|
-
|
|
757
|
-
appUrl: result.value.appUrl,
|
|
895
|
+
prefix: apiKey.slice(0, 12),
|
|
758
896
|
configPath: getConfigPath()
|
|
759
897
|
});
|
|
760
898
|
else if (!argv.quiet) {
|
|
761
|
-
console.log(
|
|
762
|
-
console.log(`
|
|
899
|
+
console.log(`Authenticated successfully.`);
|
|
900
|
+
console.log(` Key: ${apiKey.slice(0, 12)}...`);
|
|
763
901
|
console.log(` Config: ${getConfigPath()}`);
|
|
764
902
|
}
|
|
765
903
|
}
|
|
766
904
|
const logoutCommand = {
|
|
767
905
|
command: "logout",
|
|
768
|
-
describe: "
|
|
906
|
+
describe: "Revoke the auto-minted API key and clear stored credentials",
|
|
769
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
|
+
}
|
|
770
929
|
deleteConfig();
|
|
771
|
-
if (argv.json) output(argv, {
|
|
772
|
-
|
|
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.");
|
|
773
935
|
}
|
|
774
936
|
};
|
|
775
937
|
const statusCommand$1 = {
|
|
@@ -802,7 +964,7 @@ const statusCommand$1 = {
|
|
|
802
964
|
return;
|
|
803
965
|
}
|
|
804
966
|
if (apiKey.isErr() && session.isErr()) {
|
|
805
|
-
console.log("Not authenticated. Run `skydive auth login
|
|
967
|
+
console.log("Not authenticated. Run `skydive auth login`.");
|
|
806
968
|
return;
|
|
807
969
|
}
|
|
808
970
|
if (apiKey.isOk()) {
|
|
@@ -841,7 +1003,8 @@ const authCommand = {
|
|
|
841
1003
|
//#region src/chat/api/rest.ts
|
|
842
1004
|
var rest_exports = /* @__PURE__ */ __exportAll({
|
|
843
1005
|
HttpError: () => HttpError,
|
|
844
|
-
createRestClient: () => createRestClient
|
|
1006
|
+
createRestClient: () => createRestClient,
|
|
1007
|
+
errorDetail: () => errorDetail
|
|
845
1008
|
});
|
|
846
1009
|
var HttpError = class extends Error {
|
|
847
1010
|
constructor(status, body) {
|
|
@@ -851,6 +1014,20 @@ var HttpError = class extends Error {
|
|
|
851
1014
|
this.name = "HttpError";
|
|
852
1015
|
}
|
|
853
1016
|
};
|
|
1017
|
+
const ERROR_DETAIL_MAX_BODY = 2e3;
|
|
1018
|
+
/**
|
|
1019
|
+
* Fullest renderable text for a thrown value. `HttpError.message` clips the
|
|
1020
|
+
* response body to 200 chars (it flows into logs and one-line UIs); the
|
|
1021
|
+
* transcript renders errors collapsed to a single line, so it can afford the
|
|
1022
|
+
* whole body — capped with an explicit marker, never cut silently.
|
|
1023
|
+
*/
|
|
1024
|
+
function errorDetail(err) {
|
|
1025
|
+
if (err instanceof HttpError) {
|
|
1026
|
+
const body = err.body.length > ERROR_DETAIL_MAX_BODY ? `${err.body.slice(0, ERROR_DETAIL_MAX_BODY)}… (+${err.body.length - ERROR_DETAIL_MAX_BODY} chars)` : err.body;
|
|
1027
|
+
return body ? `HTTP ${err.status}: ${body}` : `HTTP ${err.status}`;
|
|
1028
|
+
}
|
|
1029
|
+
return err instanceof Error ? err.message : String(err);
|
|
1030
|
+
}
|
|
854
1031
|
const MAX_STREAM_RECONNECTS = 5;
|
|
855
1032
|
function createRestClient({ appUrl, sessionToken }) {
|
|
856
1033
|
const baseHeaders = {
|
|
@@ -1262,7 +1439,7 @@ const conversationStreamEventSchema = z.discriminatedUnion("kind", [z.object({
|
|
|
1262
1439
|
function requireSession(argv) {
|
|
1263
1440
|
const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1264
1441
|
if (session.isErr()) {
|
|
1265
|
-
printError(`${session.error.message} Run \`skydive auth login
|
|
1442
|
+
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1266
1443
|
process.exit(1);
|
|
1267
1444
|
}
|
|
1268
1445
|
return session.value;
|
|
@@ -2597,7 +2774,7 @@ const chatCommand = {
|
|
|
2597
2774
|
let session = resolveSession({ appUrl });
|
|
2598
2775
|
if (session.isErr()) {
|
|
2599
2776
|
if (isNonInteractive()) {
|
|
2600
|
-
printError("Not signed in for chat and no interactive terminal. Run `skydive auth login
|
|
2777
|
+
printError("Not signed in for chat and no interactive terminal. Run `skydive auth login`, or set SKYDIVE_SESSION_TOKEN.");
|
|
2601
2778
|
process.exit(1);
|
|
2602
2779
|
}
|
|
2603
2780
|
const login = await loginWithDevice({ appUrl });
|
|
@@ -2616,7 +2793,7 @@ const chatCommand = {
|
|
|
2616
2793
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2617
2794
|
process.exit(1);
|
|
2618
2795
|
}
|
|
2619
|
-
const { runChat } = await import("./boot-
|
|
2796
|
+
const { runChat } = await import("./boot-DML2xNb6.mjs");
|
|
2620
2797
|
await runChat({
|
|
2621
2798
|
appUrl,
|
|
2622
2799
|
sessionToken: session.value.sessionToken,
|
|
@@ -2640,7 +2817,7 @@ function resolveShareMachine(argv) {
|
|
|
2640
2817
|
async function runPrintMode({ argv, appUrl }) {
|
|
2641
2818
|
const session = resolveSession({ appUrl });
|
|
2642
2819
|
if (session.isErr()) {
|
|
2643
|
-
printError(`${session.error.message} For non-interactive use, run \`skydive auth login
|
|
2820
|
+
printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2644
2821
|
process.exit(1);
|
|
2645
2822
|
}
|
|
2646
2823
|
const { runPrint, readStdin } = await Promise.resolve().then(() => print_exports);
|
|
@@ -2728,7 +2905,7 @@ const getCommand = {
|
|
|
2728
2905
|
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
2729
2906
|
const session = resolveSession({ appUrl });
|
|
2730
2907
|
if (session.isErr()) {
|
|
2731
|
-
printError(`${session.error.message} Run \`skydive auth login
|
|
2908
|
+
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2732
2909
|
process.exit(1);
|
|
2733
2910
|
}
|
|
2734
2911
|
const { messageGet } = await Promise.resolve().then(() => print_exports);
|
|
@@ -3410,7 +3587,7 @@ const switchCommand = {
|
|
|
3410
3587
|
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.");
|
|
3411
3588
|
process.exit(1);
|
|
3412
3589
|
}
|
|
3413
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
3590
|
+
const { runWorkspacePicker } = await import("./boot-DML2xNb6.mjs");
|
|
3414
3591
|
await runWorkspacePicker(session);
|
|
3415
3592
|
return;
|
|
3416
3593
|
}
|
|
@@ -3538,8 +3715,8 @@ async function portalFetch(auth, path, init) {
|
|
|
3538
3715
|
...init.body ? { body: init.body } : {}
|
|
3539
3716
|
});
|
|
3540
3717
|
if (!res.ok) {
|
|
3541
|
-
const body = await res.text().
|
|
3542
|
-
throw new
|
|
3718
|
+
const body = await res.text().catch(() => "");
|
|
3719
|
+
throw new HttpError(res.status, body);
|
|
3543
3720
|
}
|
|
3544
3721
|
return res.json();
|
|
3545
3722
|
}
|
|
@@ -4137,4 +4314,4 @@ function resolveArgv(args, tty = {
|
|
|
4137
4314
|
createCli(resolveArgv(hideBin(process.argv))).parse();
|
|
4138
4315
|
|
|
4139
4316
|
//#endregion
|
|
4140
|
-
export { HttpError as A,
|
|
4317
|
+
export { HttpError as A, resolveWebUrl as B, noColorRequested as C, themeModeFromColorFgBg as D, themeMode as E, setActiveWorkspace as F, DEFAULT_API_URL as I, DEFAULT_APP_URL as L, errorDetail as M, getActiveWorkspaceId as N, themeVersion as O, listWorkspaces as P, getConfigPath as R, monoTheme as S, themeForMode as T, saveTheme as V, 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, getSavedTheme as z };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as HttpError, B as
|
|
2
|
+
import { A as HttpError, B as resolveWebUrl, C as noColorRequested, D as themeModeFromColorFgBg, E as themeMode, F as setActiveWorkspace, I as DEFAULT_API_URL, L as DEFAULT_APP_URL, M as errorDetail, N as getActiveWorkspaceId, O as themeVersion, P as listWorkspaces, R as getConfigPath, S as monoTheme, T as themeForMode, V as saveTheme, _ 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 getSavedTheme } from "./bin.mjs";
|
|
3
3
|
import { t as PortalClient } from "./client-XFsd0Wy9.mjs";
|
|
4
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";
|
|
5
7
|
import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
|
|
6
8
|
import { z } from "zod";
|
|
7
9
|
import open from "open";
|
|
8
10
|
import { execFile, spawn } from "node:child_process";
|
|
9
11
|
import { createHash } from "node:crypto";
|
|
10
12
|
import { constants } from "node:fs";
|
|
11
|
-
import * as os$1 from "node:os";
|
|
12
|
-
import { homedir, platform, release, tmpdir } from "node:os";
|
|
13
13
|
import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks } from "@opentui/core";
|
|
14
14
|
import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
15
15
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -2106,6 +2106,51 @@ function StatusLine({ item }) {
|
|
|
2106
2106
|
}
|
|
2107
2107
|
}
|
|
2108
2108
|
|
|
2109
|
+
//#endregion
|
|
2110
|
+
//#region src/chat/tui/chunks/error-item.tsx
|
|
2111
|
+
/**
|
|
2112
|
+
* Transcript error block. Anything longer than one line collapses to its
|
|
2113
|
+
* first line — an HTTP error body or a stack must not bury the conversation —
|
|
2114
|
+
* and clicking toggles the full text, matching the reasoning/tool blocks.
|
|
2115
|
+
*/
|
|
2116
|
+
function ErrorItem({ item }) {
|
|
2117
|
+
const [expanded, setExpanded] = useState(false);
|
|
2118
|
+
const text = item.text.trim();
|
|
2119
|
+
const firstLine = text.split("\n").find((line) => line.trim()) ?? text;
|
|
2120
|
+
if (!(text.includes("\n") || firstLine.length > 100)) return /* @__PURE__ */ jsxs("text", {
|
|
2121
|
+
fg: theme.error,
|
|
2122
|
+
children: ["✗ ", text]
|
|
2123
|
+
});
|
|
2124
|
+
if (expanded) return /* @__PURE__ */ jsxs("box", {
|
|
2125
|
+
style: {
|
|
2126
|
+
flexDirection: "column",
|
|
2127
|
+
backgroundColor: theme.surface
|
|
2128
|
+
},
|
|
2129
|
+
onMouseDown: () => setExpanded(false),
|
|
2130
|
+
children: [/* @__PURE__ */ jsx("text", {
|
|
2131
|
+
fg: theme.error,
|
|
2132
|
+
children: "✗ error"
|
|
2133
|
+
}), /* @__PURE__ */ jsxs(Indented, { children: [/* @__PURE__ */ jsx(ExpandedLines, {
|
|
2134
|
+
text,
|
|
2135
|
+
fg: theme.error
|
|
2136
|
+
}), /* @__PURE__ */ jsx(CollapseHint, {})] })]
|
|
2137
|
+
});
|
|
2138
|
+
return /* @__PURE__ */ jsx("box", {
|
|
2139
|
+
onMouseDown: () => setExpanded(true),
|
|
2140
|
+
children: /* @__PURE__ */ jsxs("text", {
|
|
2141
|
+
fg: theme.error,
|
|
2142
|
+
children: [
|
|
2143
|
+
"✗ ",
|
|
2144
|
+
truncate(firstLine, 100),
|
|
2145
|
+
/* @__PURE__ */ jsx("span", {
|
|
2146
|
+
fg: theme.dim,
|
|
2147
|
+
children: " · click to expand"
|
|
2148
|
+
})
|
|
2149
|
+
]
|
|
2150
|
+
})
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2109
2154
|
//#endregion
|
|
2110
2155
|
//#region src/chat/tui/chunks/tool-call-summary.ts
|
|
2111
2156
|
const TOOL_CALL_SUMMARY_KEY = "__skydive_summary__";
|
|
@@ -3018,10 +3063,7 @@ function RenderItem({ item }) {
|
|
|
3018
3063
|
});
|
|
3019
3064
|
case "error": return /* @__PURE__ */ jsx(Row, {
|
|
3020
3065
|
barColor: null,
|
|
3021
|
-
children: /* @__PURE__ */ jsx(
|
|
3022
|
-
fg: theme.error,
|
|
3023
|
-
children: item.text
|
|
3024
|
-
})
|
|
3066
|
+
children: /* @__PURE__ */ jsx(ErrorItem, { item })
|
|
3025
3067
|
});
|
|
3026
3068
|
case "card": return /* @__PURE__ */ jsx(Row, {
|
|
3027
3069
|
barColor: null,
|
|
@@ -5485,7 +5527,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
5485
5527
|
setItems((prev) => [...prev, {
|
|
5486
5528
|
kind: "error",
|
|
5487
5529
|
id: crypto.randomUUID(),
|
|
5488
|
-
text: `failed to load history: ${
|
|
5530
|
+
text: `failed to load history: ${errorDetail(err)}`
|
|
5489
5531
|
}]);
|
|
5490
5532
|
} finally {
|
|
5491
5533
|
if (!cancelled) setHistoryLoaded(true);
|
|
@@ -5541,7 +5583,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
5541
5583
|
setItems((prev) => [...prev, {
|
|
5542
5584
|
kind: "error",
|
|
5543
5585
|
id: crypto.randomUUID(),
|
|
5544
|
-
text:
|
|
5586
|
+
text: errorDetail(err)
|
|
5545
5587
|
}]);
|
|
5546
5588
|
setRun({ kind: "idle" });
|
|
5547
5589
|
});
|
|
@@ -5608,7 +5650,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
5608
5650
|
setItems((prev) => [...prev, {
|
|
5609
5651
|
kind: "error",
|
|
5610
5652
|
id: crypto.randomUUID(),
|
|
5611
|
-
text: `attachment failed: ${row.fileName}${result.status === "rejected" ? ` (${
|
|
5653
|
+
text: `attachment failed: ${row.fileName}${result.status === "rejected" ? ` (${errorDetail(result.reason)})` : ""}`
|
|
5612
5654
|
}]);
|
|
5613
5655
|
}
|
|
5614
5656
|
}
|
|
@@ -5642,7 +5684,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
5642
5684
|
setItems((prev) => [...prev.filter((m) => m.id !== optimisticId), {
|
|
5643
5685
|
kind: "error",
|
|
5644
5686
|
id: crypto.randomUUID(),
|
|
5645
|
-
text:
|
|
5687
|
+
text: errorDetail(err)
|
|
5646
5688
|
}]);
|
|
5647
5689
|
if (echo) setInput((existing) => existing ? existing : trimmed);
|
|
5648
5690
|
const restore = staged.filter((a) => uploadsRef.current.has(a.tempId));
|
|
@@ -5670,7 +5712,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
5670
5712
|
setItems((prev) => [...prev, {
|
|
5671
5713
|
kind: "error",
|
|
5672
5714
|
id: crypto.randomUUID(),
|
|
5673
|
-
text: `steer cancel failed: ${
|
|
5715
|
+
text: `steer cancel failed: ${errorDetail(err)}`
|
|
5674
5716
|
}]);
|
|
5675
5717
|
});
|
|
5676
5718
|
return true;
|
|
@@ -6131,7 +6173,7 @@ function ChatScreen({ agent, conversation }) {
|
|
|
6131
6173
|
})).catch((err) => setItems((prev) => [...prev, {
|
|
6132
6174
|
kind: "error",
|
|
6133
6175
|
id: crypto.randomUUID(),
|
|
6134
|
-
text: `couldn't share machine: ${
|
|
6176
|
+
text: `couldn't share machine: ${errorDetail(err)}`
|
|
6135
6177
|
}]));
|
|
6136
6178
|
}, [
|
|
6137
6179
|
portalClient,
|