skydive-cli 0.1.0-beta.87 → 0.1.0-beta.90
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 +89 -15
- package/dist/js/bin.mjs +57 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,10 +8,20 @@ npx skydive-cli --help
|
|
|
8
8
|
|
|
9
9
|
## Authentication
|
|
10
10
|
|
|
11
|
-
The CLI has two credentials, stored together in
|
|
11
|
+
The CLI has two credentials, stored together in a single `config.json`. The
|
|
12
|
+
location is platform-conventional (resolved via `env-paths`), so it is **not**
|
|
13
|
+
`~/.config/skydive/` everywhere:
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
| Platform | Path |
|
|
16
|
+
| -------- | ---------------------------------------------------------------- |
|
|
17
|
+
| macOS | `~/Library/Preferences/skydive/config.json` |
|
|
18
|
+
| Linux | `~/.config/skydive/config.json` (or `$XDG_CONFIG_HOME/skydive/`) |
|
|
19
|
+
| Windows | `%APPDATA%\skydive\config.json` |
|
|
20
|
+
|
|
21
|
+
`skydive auth status` prints the real resolved path (`Config: …`) — trust that
|
|
22
|
+
over this table if they ever disagree.
|
|
23
|
+
|
|
24
|
+
- **API key** (`sky_live_…`). Mint one at `skydive.com/account`, then:
|
|
15
25
|
|
|
16
26
|
```sh
|
|
17
27
|
skydive auth login # paste the key
|
|
@@ -26,6 +36,12 @@ The CLI has two credentials, stored together in `~/.config/skydive/config.json`:
|
|
|
26
36
|
skydive auth login --web # opens the browser, prompts for approval
|
|
27
37
|
```
|
|
28
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
|
+
|
|
29
45
|
`skydive auth status` shows both; `skydive auth logout` clears them.
|
|
30
46
|
|
|
31
47
|
## Commands
|
|
@@ -58,7 +74,7 @@ API key with edit access to that agent.
|
|
|
58
74
|
```sh
|
|
59
75
|
skydive secrets list --agent-id <id> # names only — values are never returned
|
|
60
76
|
skydive secrets set OPENAI_API_KEY --agent-id <id> # reads the value from stdin
|
|
61
|
-
|
|
77
|
+
printf '%s' "$TOKEN" | skydive secrets set MY_TOKEN --agent-id <id>
|
|
62
78
|
skydive secrets set MY_TOKEN "literal-value" --agent-id <id>
|
|
63
79
|
skydive secrets rm MY_TOKEN --agent-id <id>
|
|
64
80
|
```
|
|
@@ -67,6 +83,11 @@ skydive secrets rm MY_TOKEN --agent-id <id>
|
|
|
67
83
|
- `set` reads the value from **stdin** when no value argument is given, so the
|
|
68
84
|
secret stays out of shell history. Passing it inline is supported for
|
|
69
85
|
scripting but avoid it in an interactive shell.
|
|
86
|
+
- Use `printf '%s'` rather than `echo -n` when piping a value: `echo -n` is not
|
|
87
|
+
portable (some `sh` builtins emit `-n` literally instead of suppressing the
|
|
88
|
+
newline), which would silently store a corrupted value. Since values are
|
|
89
|
+
write-only and cannot be read back to verify, that failure stays invisible
|
|
90
|
+
until something downstream breaks.
|
|
70
91
|
- **Values are write-only over the API.** `list` returns key names, never
|
|
71
92
|
values — the same contract as the in-sandbox `platform secrets` surface.
|
|
72
93
|
- Setting a secret updates the agent's vault. If an outbound-proxy rule
|
|
@@ -122,6 +143,17 @@ skydive chat -p "status?" --agent grace --json # structured envelo
|
|
|
122
143
|
`SKYDIVE_SESSION_TOKEN`.
|
|
123
144
|
- `--json` prints `{ agentId, agentName, conversationId, isNewConversation,
|
|
124
145
|
runId, text }` instead of streaming the raw text.
|
|
146
|
+
- **Recovering a dropped run.** A long run's stream can be cut off at the edge
|
|
147
|
+
(a gateway 502/504) after the run was created. The run keeps going
|
|
148
|
+
server-side, so a blind retry would re-execute the agent. Instead, note the
|
|
149
|
+
`runId` (always in `--json` output) and fetch the result once it's done:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
skydive runs get <runId> # prints the reply; --json for the envelope
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`runs get` re-attaches to the run and replays its full reply whether it's
|
|
156
|
+
still streaming or already finished.
|
|
125
157
|
|
|
126
158
|
It defaults to the production API (`https://api.skydive.com`). For local
|
|
127
159
|
dev, point it at your stack:
|
|
@@ -136,22 +168,62 @@ management API used by `agents`/`keys`.
|
|
|
136
168
|
|
|
137
169
|
### Keybindings
|
|
138
170
|
|
|
139
|
-
| Context
|
|
140
|
-
|
|
|
141
|
-
| Pickers
|
|
142
|
-
| Pickers
|
|
143
|
-
| Pickers
|
|
144
|
-
| Pickers
|
|
145
|
-
| Agent picker
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
| Chat
|
|
171
|
+
| Context | Key | Action |
|
|
172
|
+
| ------------------- | ------------- | ------------------------------------- |
|
|
173
|
+
| Pickers | _type_ | filter the list |
|
|
174
|
+
| Pickers | `↑` / `↓` | move selection |
|
|
175
|
+
| Pickers | `↵` | open / select |
|
|
176
|
+
| Pickers | `esc` | back |
|
|
177
|
+
| Agent picker | `ctrl+n` | create an agent |
|
|
178
|
+
| Agent picker | `ctrl+w` | switch workspace |
|
|
179
|
+
| Agent picker | `tab` | toggle mine / whole-org agents |
|
|
180
|
+
| Conversation picker | `ctrl+d` | delete the highlighted conversation |
|
|
181
|
+
| Chat | `↵` | send (or queue, while streaming) |
|
|
182
|
+
| Chat | `pgup`/`pgdn` | scroll the transcript |
|
|
183
|
+
| Chat | `ctrl+c` | cancel the active run; again to quit |
|
|
184
|
+
| Chat | `esc` | back to the conversation picker |
|
|
185
|
+
| Chat | `ctrl+p` | switch model |
|
|
186
|
+
| Chat | `ctrl+o` | switch theme |
|
|
187
|
+
| Chat | `ctrl+t` | grant / revoke local machine access |
|
|
188
|
+
| Chat | `ctrl+l` | open this conversation in the browser |
|
|
189
|
+
| Chat | `ctrl+r` | run the newest pending connect card |
|
|
190
|
+
| Chat | `ctrl+v` | paste an image from the clipboard |
|
|
191
|
+
| Chat | `ctrl+x` | drop the most recently staged image |
|
|
192
|
+
| Chat | `?` | show the keybinding reference |
|
|
150
193
|
|
|
151
194
|
Messages typed while a response is streaming are **queued** and sent in
|
|
152
195
|
order as each run finishes; `ctrl+c` cancels the current run and clears the
|
|
153
196
|
queue.
|
|
154
197
|
|
|
198
|
+
### Local machine access (`--share-machine` / `ctrl+t`)
|
|
199
|
+
|
|
200
|
+
The chat TUI can grant the agent the ability to run commands **on your local
|
|
201
|
+
machine** so it can read the repo you launched from, run local tooling, etc.
|
|
202
|
+
This is off by default (default-deny) and you opt in explicitly:
|
|
203
|
+
|
|
204
|
+
- `skydive chat --share-machine` enables it at launch, or
|
|
205
|
+
- `ctrl+t` toggles it from the chat screen (the status bar shows `local access`
|
|
206
|
+
when on, `revoke access` when off).
|
|
207
|
+
|
|
208
|
+
What to know before enabling it:
|
|
209
|
+
|
|
210
|
+
- **Per-agent.** The grant is scoped to the agent you're chatting with; other
|
|
211
|
+
agents don't inherit it.
|
|
212
|
+
- **The working directory is a starting point, not a jail.** Commands run with
|
|
213
|
+
their working directory set to wherever you launched `skydive chat`, but this
|
|
214
|
+
is **not** a hard filesystem boundary. This is genuine remote command
|
|
215
|
+
execution (a WebSocket streaming stdin/stdout/stderr), so the agent runs
|
|
216
|
+
commands as your user and can reach anything your user can, including paths
|
|
217
|
+
outside the launch directory. Launch from a scoped directory, but treat the
|
|
218
|
+
grant as "this agent can act as me on this machine," not "this agent can only
|
|
219
|
+
see this folder."
|
|
220
|
+
- **Teardown is automatic.** Access is revoked when the TUI unmounts, so
|
|
221
|
+
quitting the chat drops the grant and reaps any processes the agent spawned.
|
|
222
|
+
- **Interactive only.** Machine sharing is **not** available in `-p`
|
|
223
|
+
(non-interactive) mode \u2014 `--share-machine` has no effect there and the
|
|
224
|
+
print path carries no portal code. You can have scripted (`-p`) or local
|
|
225
|
+
access, not both, in a single invocation.
|
|
226
|
+
|
|
155
227
|
## Environment variables
|
|
156
228
|
|
|
157
229
|
| Variable | Effect |
|
|
@@ -160,6 +232,8 @@ queue.
|
|
|
160
232
|
| `SKYDIVE_API_URL` | Management API base URL |
|
|
161
233
|
| `SKYDIVE_SESSION_TOKEN` | Chat session token (for non-interactive use) |
|
|
162
234
|
| `SKYDIVE_APP_URL` | Chat app/session origin (overrides stored value) |
|
|
235
|
+
| `SKYDIVE_THEME` | Chat colorscheme id (flag `--theme` overrides) |
|
|
236
|
+
| `SKYDIVE_CONFIG_NAME` | On-disk config profile name (default `skydive`) |
|
|
163
237
|
|
|
164
238
|
## Testing the TUI
|
|
165
239
|
|
package/dist/js/bin.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import zlib from "node:zlib";
|
|
14
14
|
|
|
15
15
|
//#region package.json
|
|
16
|
-
var version$1 = "0.1.0-beta.
|
|
16
|
+
var version$1 = "0.1.0-beta.90";
|
|
17
17
|
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/types.ts
|
|
@@ -433,6 +433,44 @@ async function listWorkspaces({ appUrl, sessionToken }) {
|
|
|
433
433
|
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Resolve who the current chat session belongs to: the signed-in user's
|
|
438
|
+
* email/name and the active workspace. Used by `auth status` so a user running
|
|
439
|
+
* multiple accounts can answer "am I logged into the right one?" without
|
|
440
|
+
* another command. Best-effort: any failure yields nulls rather than throwing,
|
|
441
|
+
* since `auth status` should still report the rest of the state.
|
|
442
|
+
*/
|
|
443
|
+
async function getSessionIdentity({ appUrl, sessionToken }) {
|
|
444
|
+
try {
|
|
445
|
+
const res = await fetch(`${appUrl}/api/auth/get-session?disableCookieCache=true`, { headers: authHeaders(sessionToken) });
|
|
446
|
+
if (!res.ok) return err({ message: `failed to read session (${res.status})` });
|
|
447
|
+
const parsed = z.object({
|
|
448
|
+
user: z.object({
|
|
449
|
+
email: z.string().nullable().optional(),
|
|
450
|
+
name: z.string().nullable().optional()
|
|
451
|
+
}).nullable().optional(),
|
|
452
|
+
session: z.object({ activeOrganizationId: z.string().nullable().optional() }).nullable().optional()
|
|
453
|
+
}).nullable().safeParse(await res.json());
|
|
454
|
+
if (!parsed.success) return err({ message: "unexpected session response" });
|
|
455
|
+
const activeWorkspaceId = parsed.data?.session?.activeOrganizationId ?? null;
|
|
456
|
+
let activeWorkspaceName = null;
|
|
457
|
+
if (activeWorkspaceId) {
|
|
458
|
+
const workspaces = await listWorkspaces({
|
|
459
|
+
appUrl,
|
|
460
|
+
sessionToken
|
|
461
|
+
});
|
|
462
|
+
if (workspaces.isOk()) activeWorkspaceName = workspaces.value.find((w) => w.id === activeWorkspaceId)?.name ?? null;
|
|
463
|
+
}
|
|
464
|
+
return ok({
|
|
465
|
+
email: parsed.data?.user?.email ?? null,
|
|
466
|
+
name: parsed.data?.user?.name ?? null,
|
|
467
|
+
activeWorkspaceId,
|
|
468
|
+
activeWorkspaceName
|
|
469
|
+
});
|
|
470
|
+
} catch (e) {
|
|
471
|
+
return err({ message: e instanceof Error ? e.message : String(e) });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
436
474
|
/** The workspace bound to the current session, or `null` if none is set. */
|
|
437
475
|
async function getActiveWorkspaceId({ appUrl, sessionToken }) {
|
|
438
476
|
try {
|
|
@@ -693,6 +731,10 @@ const statusCommand = {
|
|
|
693
731
|
handler: async (argv) => {
|
|
694
732
|
const apiKey = resolveConfig({ apiUrl: argv["api-url"] });
|
|
695
733
|
const session = resolveSession({});
|
|
734
|
+
const identity = session.isOk() ? (await getSessionIdentity({
|
|
735
|
+
appUrl: session.value.appUrl,
|
|
736
|
+
sessionToken: session.value.sessionToken
|
|
737
|
+
})).unwrapOr(null) : null;
|
|
696
738
|
if (argv.json) {
|
|
697
739
|
output(argv, {
|
|
698
740
|
authenticated: apiKey.isOk(),
|
|
@@ -700,7 +742,11 @@ const statusCommand = {
|
|
|
700
742
|
apiUrl: apiKey.isOk() ? apiKey.value.apiUrl : null,
|
|
701
743
|
chat: {
|
|
702
744
|
authenticated: session.isOk(),
|
|
703
|
-
appUrl: session.isOk() ? session.value.appUrl : null
|
|
745
|
+
appUrl: session.isOk() ? session.value.appUrl : null,
|
|
746
|
+
email: identity?.email ?? null,
|
|
747
|
+
name: identity?.name ?? null,
|
|
748
|
+
workspaceId: identity?.activeWorkspaceId ?? null,
|
|
749
|
+
workspaceName: identity?.activeWorkspaceName ?? null
|
|
704
750
|
},
|
|
705
751
|
configPath: getConfigPath()
|
|
706
752
|
});
|
|
@@ -718,6 +764,15 @@ const statusCommand = {
|
|
|
718
764
|
if (session.isOk()) {
|
|
719
765
|
console.log("Chat session:");
|
|
720
766
|
console.log(` App: ${session.value.appUrl}`);
|
|
767
|
+
if (identity?.email || identity?.name) {
|
|
768
|
+
const who = identity.email ?? identity.name;
|
|
769
|
+
const suffix = identity.email && identity.name ? ` (${identity.name})` : "";
|
|
770
|
+
console.log(` User: ${who}${suffix}`);
|
|
771
|
+
}
|
|
772
|
+
if (identity?.activeWorkspaceName || identity?.activeWorkspaceId) {
|
|
773
|
+
const ws = identity.activeWorkspaceName ?? identity.activeWorkspaceId;
|
|
774
|
+
console.log(` Space: ${ws}`);
|
|
775
|
+
}
|
|
721
776
|
} else console.log("Chat session: not signed in.");
|
|
722
777
|
console.log(`Config: ${getConfigPath()}`);
|
|
723
778
|
}
|