skydive-cli 0.1.0-beta.363 → 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 +40 -27
- package/dist/js/bin.mjs +240 -78
- package/dist/js/{boot-C1WUzzUo.mjs → boot-QnNiy1cA.mjs} +2 -2
- 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.378";
|
|
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()) {
|
|
@@ -1262,7 +1424,7 @@ const conversationStreamEventSchema = z.discriminatedUnion("kind", [z.object({
|
|
|
1262
1424
|
function requireSession(argv) {
|
|
1263
1425
|
const session = resolveSession({ appUrl: resolveAppUrl({ appUrl: argv["api-url"] }) });
|
|
1264
1426
|
if (session.isErr()) {
|
|
1265
|
-
printError(`${session.error.message} Run \`skydive auth login
|
|
1427
|
+
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1266
1428
|
process.exit(1);
|
|
1267
1429
|
}
|
|
1268
1430
|
return session.value;
|
|
@@ -2597,7 +2759,7 @@ const chatCommand = {
|
|
|
2597
2759
|
let session = resolveSession({ appUrl });
|
|
2598
2760
|
if (session.isErr()) {
|
|
2599
2761
|
if (isNonInteractive()) {
|
|
2600
|
-
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.");
|
|
2601
2763
|
process.exit(1);
|
|
2602
2764
|
}
|
|
2603
2765
|
const login = await loginWithDevice({ appUrl });
|
|
@@ -2616,7 +2778,7 @@ const chatCommand = {
|
|
|
2616
2778
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2617
2779
|
process.exit(1);
|
|
2618
2780
|
}
|
|
2619
|
-
const { runChat } = await import("./boot-
|
|
2781
|
+
const { runChat } = await import("./boot-QnNiy1cA.mjs");
|
|
2620
2782
|
await runChat({
|
|
2621
2783
|
appUrl,
|
|
2622
2784
|
sessionToken: session.value.sessionToken,
|
|
@@ -2640,7 +2802,7 @@ function resolveShareMachine(argv) {
|
|
|
2640
2802
|
async function runPrintMode({ argv, appUrl }) {
|
|
2641
2803
|
const session = resolveSession({ appUrl });
|
|
2642
2804
|
if (session.isErr()) {
|
|
2643
|
-
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.`);
|
|
2644
2806
|
process.exit(1);
|
|
2645
2807
|
}
|
|
2646
2808
|
const { runPrint, readStdin } = await Promise.resolve().then(() => print_exports);
|
|
@@ -2728,7 +2890,7 @@ const getCommand = {
|
|
|
2728
2890
|
const appUrl = resolveAppUrl({ appUrl: argv["api-url"] });
|
|
2729
2891
|
const session = resolveSession({ appUrl });
|
|
2730
2892
|
if (session.isErr()) {
|
|
2731
|
-
printError(`${session.error.message} Run \`skydive auth login
|
|
2893
|
+
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
2732
2894
|
process.exit(1);
|
|
2733
2895
|
}
|
|
2734
2896
|
const { messageGet } = await Promise.resolve().then(() => print_exports);
|
|
@@ -3410,7 +3572,7 @@ const switchCommand = {
|
|
|
3410
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.");
|
|
3411
3573
|
process.exit(1);
|
|
3412
3574
|
}
|
|
3413
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
3575
|
+
const { runWorkspacePicker } = await import("./boot-QnNiy1cA.mjs");
|
|
3414
3576
|
await runWorkspacePicker(session);
|
|
3415
3577
|
return;
|
|
3416
3578
|
}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
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
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";
|