skydive-cli 0.1.0 → 0.2.0-beta.421
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/CHANGELOG.md +68 -0
- package/README.md +271 -28
- package/dist/js/api-CDTKq_5Q.mjs +145 -0
- package/dist/js/bin.mjs +1752 -1359
- package/dist/js/boot-CQLDUeZ9.mjs +9827 -0
- package/dist/js/client-B3ZhhF7e.mjs +366 -0
- package/dist/js/client-CpEvH2Pq.mjs +169 -0
- package/dist/js/client-D6NAkL9e.mjs +6 -0
- package/dist/js/output-B4cW10Ph.mjs +27 -0
- package/dist/js/print-B6AO13SA.mjs +5 -0
- package/dist/js/print-DausK_KZ.mjs +484 -0
- package/dist/js/print-share-D7OSxvE2.mjs +41 -0
- package/dist/js/raw-pty-2_VA1kw_.mjs +5 -0
- package/dist/js/raw-pty-GAvxm2ol.mjs +99 -0
- package/dist/js/rest-COkLEZOB.mjs +4 -0
- package/dist/js/{print-BHbFMxQv.mjs → rest-CamHVOce.mjs} +173 -158
- package/dist/js/theme-CuQhvqzN.mjs +990 -0
- package/dist/js/util-CeisaZVY.mjs +15 -0
- package/package.json +20 -9
- package/dist/js/boot-ChlVx-ts.mjs +0 -5525
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Skydive CLI are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-07-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
**Sandbox & machine access**
|
|
13
|
+
|
|
14
|
+
- Standalone `skydive sandbox` command for direct access to an agent's sandbox.
|
|
15
|
+
- `/sandbox` in the chat TUI — live PTY session and one-shot command execution.
|
|
16
|
+
- Headless machine sharing via `skydive portal`, with self-registering portal grants.
|
|
17
|
+
- `shareMachineDefault` config key for always-on portal sharing.
|
|
18
|
+
|
|
19
|
+
**Chat TUI**
|
|
20
|
+
|
|
21
|
+
- File pane (`ctrl+g`) with Changes and Files tabs: a shared file tree plus source viewer and a workspace browser, mouse-resizable and responsive to terminal size.
|
|
22
|
+
- Agent todo list rendered in the chat TUI, above the composer, mirroring the web chat's todo card.
|
|
23
|
+
- Fuzzy finder in the conversation and agent pickers.
|
|
24
|
+
- Slash-command autocomplete menu.
|
|
25
|
+
- Paste or drag-and-drop any file into the composer; paste clipboard images with Cmd+V.
|
|
26
|
+
- Run local shell commands with `!` in the composer.
|
|
27
|
+
- Conversation recaps, streamed live title updates, and per-agent attribution on assistant turns.
|
|
28
|
+
- Working timer rolls up into minutes and hours.
|
|
29
|
+
- Conversation picker paginates past 50 conversations and shows only your own conversations.
|
|
30
|
+
- Esc leaves a live run; typing `exit` quits the chat.
|
|
31
|
+
- Cursor Dark theme.
|
|
32
|
+
- Picker rows no longer overlap when a conversation title contains a newline, tab, or control character.
|
|
33
|
+
|
|
34
|
+
**Headless & scripting**
|
|
35
|
+
|
|
36
|
+
- Resume a conversation by id.
|
|
37
|
+
- `conversations list` and `conversations show` for transcript reads.
|
|
38
|
+
- `messages get`, with run recovery keyed on message id.
|
|
39
|
+
- Connect cards surface in headless `-p` mode so a driving agent never gets stuck.
|
|
40
|
+
|
|
41
|
+
**Authentication**
|
|
42
|
+
|
|
43
|
+
- `skydive auth login` via the browser now auto-mints an API key, so one login yields both a chat session and a usable management credential. Management commands announce the key's pinned workspace when it drives them, so a workspace mismatch is visible at use time.
|
|
44
|
+
- Workspace picker on the device authorization page.
|
|
45
|
+
- Account and workspace identity shown in `auth status`.
|
|
46
|
+
|
|
47
|
+
**Platform**
|
|
48
|
+
|
|
49
|
+
- Standalone binary builds compiling the CLI into a per-target executable.
|
|
50
|
+
- Interactive workspace switcher; management commands follow the active workspace.
|
|
51
|
+
- Terminal host integrations and agent notifications.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Transcript errors collapse to one line, click to expand (REST and portal errors keep their full body).
|
|
56
|
+
- Dragged/pasted image file paths attach the file instead of inserting path text, including macOS paths with literal parentheses.
|
|
57
|
+
- Bare URLs in chat markdown are hyperlinked so they survive text wrap.
|
|
58
|
+
- Composer draft is preserved across TUI overlays.
|
|
59
|
+
- Relative connect links resolve before opening the browser.
|
|
60
|
+
- Numbered markdown headings render colored in the TUI.
|
|
61
|
+
- Chat transcript pages by 75% of a screen; picker rows stay on one line.
|
|
62
|
+
- Run starts push to the TUI over the conversation stream.
|
|
63
|
+
- Security: remediated high-severity dependency findings and cleared tar/shell-quote CVEs.
|
|
64
|
+
|
|
65
|
+
## [0.1.0] - 2026-07-21
|
|
66
|
+
|
|
67
|
+
Initial public release: `skydive chat` TUI, agent and conversation management,
|
|
68
|
+
device authorization, and headless `-p` mode.
|
package/README.md
CHANGED
|
@@ -8,25 +8,57 @@ npx skydive-cli --help
|
|
|
8
8
|
|
|
9
9
|
## Authentication
|
|
10
10
|
|
|
11
|
-
|
|
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:
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
```sh
|
|
16
|
+
skydive auth login
|
|
17
|
+
```
|
|
15
18
|
|
|
16
|
-
|
|
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` — and whenever the key
|
|
29
|
+
(not the session) is the credential actually driving a management command,
|
|
30
|
+
the CLI says so on stderr, naming the pinned workspace, so commands never
|
|
31
|
+
silently act in a workspace you switched away from.
|
|
32
|
+
- a **user session** for `skydive chat`. Chat is user-level and multi-agent,
|
|
33
|
+
so it authenticates as you — and unlike the API key (which is pinned to the
|
|
34
|
+
workspace that minted it), the session follows `skydive workspace switch`.
|
|
35
|
+
The management commands prefer the session while it's valid and fall back
|
|
36
|
+
to the API key.
|
|
37
|
+
|
|
38
|
+
The flow doesn't require a TTY: in a non-interactive shell (an agent harness,
|
|
39
|
+
SSH without a display) `auth login` still prints the verification URL + code
|
|
40
|
+
and waits — open the link in any browser to approve. For unattended CI, skip
|
|
41
|
+
the wait entirely and pass an existing key (mint one at
|
|
42
|
+
`skydive.com/settings/account`):
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
skydive auth login --api-key sky_live_… # or set SKYDIVE_API_KEY
|
|
46
|
+
```
|
|
19
47
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
API key. `skydive chat` does this automatically on first run; you can also
|
|
23
|
-
do it up front:
|
|
48
|
+
`skydive auth status` shows both credentials; `skydive auth logout` revokes
|
|
49
|
+
the auto-minted key and clears them.
|
|
24
50
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```
|
|
51
|
+
The config location is platform-conventional (resolved via `env-paths`), so
|
|
52
|
+
it is **not** `~/.config/skydive/` everywhere:
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
| Platform | Path |
|
|
55
|
+
| -------- | ---------------------------------------------------------------- |
|
|
56
|
+
| macOS | `~/Library/Preferences/skydive/config.json` |
|
|
57
|
+
| Linux | `~/.config/skydive/config.json` (or `$XDG_CONFIG_HOME/skydive/`) |
|
|
58
|
+
| Windows | `%APPDATA%\skydive\config.json` |
|
|
59
|
+
|
|
60
|
+
`skydive auth status` prints the real resolved path (`Config: …`) — trust that
|
|
61
|
+
over this table if they ever disagree.
|
|
30
62
|
|
|
31
63
|
## Commands
|
|
32
64
|
|
|
@@ -45,10 +77,62 @@ skydive secrets rm <KEY> --agent-id <id>
|
|
|
45
77
|
|
|
46
78
|
skydive chat # interactive chat TUI (Node; fetches Bun on first run)
|
|
47
79
|
skydive chat -p "<prompt>" # one-shot, non-interactive (runs under Node)
|
|
80
|
+
|
|
81
|
+
skydive import # agent imports your Claude Code / Cursor / OpenCode setup
|
|
82
|
+
|
|
83
|
+
skydive portal open # open the portal to this machine, headless
|
|
84
|
+
skydive portal grant --agent <id|name>
|
|
85
|
+
skydive portal revoke --agent <id|name>
|
|
86
|
+
skydive portal status
|
|
48
87
|
```
|
|
49
88
|
|
|
50
89
|
Global flags: `--json`, `--quiet`, `--api-url <url>`.
|
|
51
90
|
|
|
91
|
+
## `skydive import`
|
|
92
|
+
|
|
93
|
+
Migrating from Claude Code, Cursor, or OpenCode? `skydive import` opens a
|
|
94
|
+
chat with your agent, shares this machine with it, and asks it to import
|
|
95
|
+
your setup — instruction files, skills, slash commands, memory, MCP
|
|
96
|
+
servers. The agent inventories what you have, shows you a plan (including
|
|
97
|
+
what it will NOT bring: credentials never come along; hooks and
|
|
98
|
+
settings have no equivalent here), and only commits to its repo after you
|
|
99
|
+
say yes. Run it from a project directory so project-level config is found
|
|
100
|
+
too.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
skydive import # picks your only agent, or opens the picker
|
|
104
|
+
skydive import --agent kit # import into a specific agent
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The heavy lifting is the agent's `import-config` skill, so you can also just
|
|
108
|
+
ask in any machine-shared chat: "import my Claude Code setup".
|
|
109
|
+
|
|
110
|
+
Non-interactive (scripts, or driving it from another coding agent):
|
|
111
|
+
`skydive import -p --agent kit` sends the request as a one-shot and prints
|
|
112
|
+
the agent's plan — the agent still waits for approval, so continue with
|
|
113
|
+
`skydive chat -p "go ahead" --agent <id> --resume <conversation> --share-machine`
|
|
114
|
+
to apply it. Runs under Node, no TTY needed.
|
|
115
|
+
|
|
116
|
+
## Workspaces
|
|
117
|
+
|
|
118
|
+
Every `skydive` command acts inside one **workspace** — the org/team boundary
|
|
119
|
+
that owns your agents, conversations, and secrets. If your account belongs to
|
|
120
|
+
several workspaces (say, your company org and a personal one), the same agent
|
|
121
|
+
name can exist in both, so which workspace is active determines what every
|
|
122
|
+
command sees.
|
|
123
|
+
|
|
124
|
+
- `skydive workspace list` — workspaces on your account, with the active one
|
|
125
|
+
marked.
|
|
126
|
+
- `skydive workspace switch [workspace]` — change the active workspace. All
|
|
127
|
+
commands — `agents`, `secrets`, `keys`, `chat`, `conversations` — follow it.
|
|
128
|
+
- `skydive auth status` — shows who you're logged in as and which workspace is
|
|
129
|
+
active.
|
|
130
|
+
- In the chat TUI, `ctrl+w` switches workspace from the agent picker.
|
|
131
|
+
|
|
132
|
+
The active workspace is stored in the CLI config file and persists across
|
|
133
|
+
invocations. If a command can't find an agent you know exists, check
|
|
134
|
+
`skydive auth status` first — you're probably in the wrong workspace.
|
|
135
|
+
|
|
52
136
|
## `skydive secrets`
|
|
53
137
|
|
|
54
138
|
Manage the environment secrets injected into an agent's sandbox. All
|
|
@@ -58,7 +142,7 @@ API key with edit access to that agent.
|
|
|
58
142
|
```sh
|
|
59
143
|
skydive secrets list --agent-id <id> # names only — values are never returned
|
|
60
144
|
skydive secrets set OPENAI_API_KEY --agent-id <id> # reads the value from stdin
|
|
61
|
-
|
|
145
|
+
printf '%s' "$TOKEN" | skydive secrets set MY_TOKEN --agent-id <id>
|
|
62
146
|
skydive secrets set MY_TOKEN "literal-value" --agent-id <id>
|
|
63
147
|
skydive secrets rm MY_TOKEN --agent-id <id>
|
|
64
148
|
```
|
|
@@ -67,6 +151,11 @@ skydive secrets rm MY_TOKEN --agent-id <id>
|
|
|
67
151
|
- `set` reads the value from **stdin** when no value argument is given, so the
|
|
68
152
|
secret stays out of shell history. Passing it inline is supported for
|
|
69
153
|
scripting but avoid it in an interactive shell.
|
|
154
|
+
- Use `printf '%s'` rather than `echo -n` when piping a value: `echo -n` is not
|
|
155
|
+
portable (some `sh` builtins emit `-n` literally instead of suppressing the
|
|
156
|
+
newline), which would silently store a corrupted value. Since values are
|
|
157
|
+
write-only and cannot be read back to verify, that failure stays invisible
|
|
158
|
+
until something downstream breaks.
|
|
70
159
|
- **Values are write-only over the API.** `list` returns key names, never
|
|
71
160
|
values — the same contract as the in-sandbox `platform secrets` surface.
|
|
72
161
|
- Setting a secret updates the agent's vault. If an outbound-proxy rule
|
|
@@ -118,10 +207,22 @@ skydive chat -p "status?" --agent grace --json # structured envelo
|
|
|
118
207
|
- `--conversation <id>` continues an existing thread; omitted, it starts a new
|
|
119
208
|
one. The conversation id is included in `--json` output for chaining.
|
|
120
209
|
- `-p` needs a user session just like `chat`, but never opens the interactive
|
|
121
|
-
browser login — sign in first with `skydive auth login
|
|
210
|
+
browser login — sign in first with `skydive auth login` or set
|
|
122
211
|
`SKYDIVE_SESSION_TOKEN`.
|
|
123
212
|
- `--json` prints `{ agentId, agentName, conversationId, isNewConversation,
|
|
124
|
-
|
|
213
|
+
messageId, text }` instead of streaming the raw text.
|
|
214
|
+
- **Recovering a dropped run.** A long run's stream can be cut off at the edge
|
|
215
|
+
(a gateway 502/504) after the message was accepted. The agent keeps working
|
|
216
|
+
server-side, so a blind retry would re-execute it. Instead, note the
|
|
217
|
+
`messageId` (always in `--json` output) and fetch the result once it's done:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
skydive messages get <messageId> # prints the reply; --json for the envelope
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`messages get` re-attaches to the exchange behind that message id and replays
|
|
224
|
+
the full reply whether it's still streaming or already finished. Message ids
|
|
225
|
+
are the handle this API works in; run ids stay server-side.
|
|
125
226
|
|
|
126
227
|
It defaults to the production API (`https://api.skydive.com`). For local
|
|
127
228
|
dev, point it at your stack:
|
|
@@ -136,22 +237,123 @@ management API used by `agents`/`keys`.
|
|
|
136
237
|
|
|
137
238
|
### Keybindings
|
|
138
239
|
|
|
139
|
-
| Context
|
|
140
|
-
|
|
|
141
|
-
| Pickers
|
|
142
|
-
| Pickers
|
|
143
|
-
| Pickers
|
|
144
|
-
| Pickers
|
|
145
|
-
| Agent picker
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
149
|
-
| Chat
|
|
240
|
+
| Context | Key | Action |
|
|
241
|
+
| ------------------- | ------------- | ------------------------------------- |
|
|
242
|
+
| Pickers | _type_ | filter the list |
|
|
243
|
+
| Pickers | `↑` / `↓` | move selection |
|
|
244
|
+
| Pickers | `↵` | open / select |
|
|
245
|
+
| Pickers | `esc` | back |
|
|
246
|
+
| Agent picker | `ctrl+n` | create an agent |
|
|
247
|
+
| Agent picker | `ctrl+w` | switch workspace |
|
|
248
|
+
| Agent picker | `tab` | toggle mine / whole-org agents |
|
|
249
|
+
| Conversation picker | `ctrl+d` | delete the highlighted conversation |
|
|
250
|
+
| Chat | `↵` | send (or queue, while streaming) |
|
|
251
|
+
| Chat | `pgup`/`pgdn` | scroll the transcript |
|
|
252
|
+
| Chat | `ctrl+c` | cancel the active run; again to quit |
|
|
253
|
+
| Chat | `esc` | back to the conversation picker |
|
|
254
|
+
| Chat | `ctrl+p` | switch model |
|
|
255
|
+
| Chat | `ctrl+o` | switch theme |
|
|
256
|
+
| Chat | `ctrl+t` | grant / revoke local machine access |
|
|
257
|
+
| Chat | `ctrl+l` | open this conversation in the browser |
|
|
258
|
+
| Chat | `ctrl+r` | run the newest pending connect card |
|
|
259
|
+
| Chat | `ctrl+v` | paste an image from the clipboard |
|
|
260
|
+
| Chat | `ctrl+x` | drop the most recently staged image |
|
|
261
|
+
| Chat | `?` | show the keybinding reference |
|
|
150
262
|
|
|
151
263
|
Messages typed while a response is streaming are **queued** and sent in
|
|
152
264
|
order as each run finishes; `ctrl+c` cancels the current run and clears the
|
|
153
265
|
queue.
|
|
154
266
|
|
|
267
|
+
### Local machine access (`--share-machine` / `ctrl+t`)
|
|
268
|
+
|
|
269
|
+
The chat TUI can grant the agent the ability to run commands **on your local
|
|
270
|
+
machine** so it can read the repo you launched from, run local tooling, etc.
|
|
271
|
+
This is off by default (default-deny) and you opt in explicitly:
|
|
272
|
+
|
|
273
|
+
- `skydive chat --share-machine` enables it at launch, or
|
|
274
|
+
- `ctrl+t` toggles it from the chat screen (the status bar shows `local access`
|
|
275
|
+
when on, `revoke access` when off), or
|
|
276
|
+
- set `"shareMachineDefault": true` in the CLI config file to share on every
|
|
277
|
+
launch (see [Configuration](#configuration); `--no-share-machine` overrides
|
|
278
|
+
once).
|
|
279
|
+
|
|
280
|
+
What to know before enabling it:
|
|
281
|
+
|
|
282
|
+
- **Per-agent.** The grant is scoped to the agent you're chatting with; other
|
|
283
|
+
agents don't inherit it.
|
|
284
|
+
- **The working directory is a starting point, not a jail.** Commands run with
|
|
285
|
+
their working directory set to wherever you launched `skydive chat`, but this
|
|
286
|
+
is **not** a hard filesystem boundary. This is genuine remote command
|
|
287
|
+
execution (a WebSocket streaming stdin/stdout/stderr), so the agent runs
|
|
288
|
+
commands as your user and can reach anything your user can, including paths
|
|
289
|
+
outside the launch directory. Launch from a scoped directory, but treat the
|
|
290
|
+
grant as "this agent can act as me on this machine," not "this agent can only
|
|
291
|
+
see this folder."
|
|
292
|
+
- **Teardown drops reachability, not the grant.** Quitting the chat closes the
|
|
293
|
+
portal connection (the machine becomes unreachable) and reaps any processes
|
|
294
|
+
the agent spawned, but the per-agent grant itself **persists** until you
|
|
295
|
+
revoke it (`skydive portal revoke` or the web app). The next time this
|
|
296
|
+
machine shares, previously granted agents can run commands immediately, with
|
|
297
|
+
no new prompt.
|
|
298
|
+
- **The TUI is not required.** With `-p`, `--share-machine` shares this machine
|
|
299
|
+
for the duration of that one run: the portal connects before the prompt is
|
|
300
|
+
sent, the target agent is granted access (the flag is the consent — there is
|
|
301
|
+
no prompt to approve), and the connection is torn down when the run ends.
|
|
302
|
+
For access that outlives a single run, use `skydive portal open` (below).
|
|
303
|
+
When sharing comes from `shareMachineDefault` rather than the explicit
|
|
304
|
+
flag, no new grant is minted — the machine connects and already-granted
|
|
305
|
+
agents can run, but an ungranted agent still needs `skydive portal grant`
|
|
306
|
+
(or an approved request).
|
|
307
|
+
|
|
308
|
+
### Headless machine access (`skydive portal`)
|
|
309
|
+
|
|
310
|
+
`skydive portal` manages machine sharing without the TUI, for scripts, CI-ish
|
|
311
|
+
environments, and driving agents from other tools:
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
skydive portal open # connect and stay in the foreground (ctrl+c to close)
|
|
315
|
+
skydive portal open --agent grace # …and grant that agent on first connect
|
|
316
|
+
skydive portal open --cwd ~/some/repo # working directory for the agent's commands (a starting point, not a boundary — see above)
|
|
317
|
+
skydive portal grant --agent grace # allow an agent (registers this machine if needed)
|
|
318
|
+
skydive portal revoke --agent grace # remove an agent's access
|
|
319
|
+
skydive portal status # machines, connection state, granted agents
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Semantics match the TUI exactly \u2014 same default-deny grants, same audit log,
|
|
323
|
+
same server-side kill switch. `open` keeps the machine reachable only while
|
|
324
|
+
the process runs (park it in tmux for long-lived access); `grant`/`revoke`
|
|
325
|
+
edit the persistent per-agent access.
|
|
326
|
+
|
|
327
|
+
Two unattended patterns:
|
|
328
|
+
|
|
329
|
+
- **One-shot:** `skydive chat -p "<prompt>" --agent <name> --share-machine` \u2014
|
|
330
|
+
shares, grants the target agent, runs the prompt, tears down.
|
|
331
|
+
- **Daemon:** one `skydive portal open` process, plus plain
|
|
332
|
+
`skydive chat -p "<prompt>" --agent <name>` one-shots from another shell \u2014
|
|
333
|
+
cheaper when firing many prompts, since each one skips the connect/grant.
|
|
334
|
+
|
|
335
|
+
## Configuration
|
|
336
|
+
|
|
337
|
+
Persistent state lives in a per-user JSON file managed by
|
|
338
|
+
[`conf`](https://github.com/sindresorhus/conf):
|
|
339
|
+
`~/Library/Preferences/skydive/config.json` on macOS,
|
|
340
|
+
`~/.config/skydive/config.json` on Linux (profile name follows
|
|
341
|
+
`SKYDIVE_CONFIG_NAME`). Most keys are written by flows — `auth login`, the
|
|
342
|
+
device-login flow, the in-TUI theme picker — but the file is plain JSON and
|
|
343
|
+
read fresh on every invocation, so preference keys can be set by editing it
|
|
344
|
+
directly. There is deliberately no `skydive config` command.
|
|
345
|
+
|
|
346
|
+
Hand-editable keys:
|
|
347
|
+
|
|
348
|
+
| Key | Type | Effect |
|
|
349
|
+
| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
350
|
+
| `shareMachineDefault` | boolean | When `true`, `skydive chat` (TUI and `-p`) shares this machine over the portal on launch, as if `--share-machine` were passed. An explicit `--share-machine`/`--no-share-machine` overrides it per invocation. Default `false`. |
|
|
351
|
+
| `updateCheck` | boolean | When `false`, disables the daily background update check and its "Update available" notice. The persistent equivalent of setting `SKYDIVE_NO_UPDATE_CHECK` (or `NO_UPDATE_NOTIFIER`) in the environment. Default `true`. |
|
|
352
|
+
|
|
353
|
+
The remaining keys (`apiKey`, `apiUrl`, `sessionToken`, `appUrl`, `themeDark`,
|
|
354
|
+
`themeLight`, …) are credentials or flow-managed state — leave them to the
|
|
355
|
+
CLI.
|
|
356
|
+
|
|
155
357
|
## Environment variables
|
|
156
358
|
|
|
157
359
|
| Variable | Effect |
|
|
@@ -160,3 +362,44 @@ queue.
|
|
|
160
362
|
| `SKYDIVE_API_URL` | Management API base URL |
|
|
161
363
|
| `SKYDIVE_SESSION_TOKEN` | Chat session token (for non-interactive use) |
|
|
162
364
|
| `SKYDIVE_APP_URL` | Chat app/session origin (overrides stored value) |
|
|
365
|
+
| `SKYDIVE_THEME` | Chat colorscheme id (flag `--theme` overrides) |
|
|
366
|
+
| `SKYDIVE_CONFIG_NAME` | On-disk config profile name (default `skydive`) |
|
|
367
|
+
|
|
368
|
+
## Testing the TUI
|
|
369
|
+
|
|
370
|
+
`yarn test:unit` runs both suites: unit tests (`*.test.ts`) under vitest,
|
|
371
|
+
then TUI tests (`*.tui.test.tsx`) under `bun test` (needs [Bun](https://bun.sh)
|
|
372
|
+
≥ 1.3.14 on your PATH — see `engines`).
|
|
373
|
+
|
|
374
|
+
TUI tests render the real OpenTUI screens headlessly — fixed terminal size,
|
|
375
|
+
mock keyboard input, frames captured as plain text — via
|
|
376
|
+
`@opentui/react/test-utils`. OpenTUI needs Bun's FFI, which is why they run
|
|
377
|
+
under `bun test`, not vitest:
|
|
378
|
+
|
|
379
|
+
```sh
|
|
380
|
+
bun test .tui.test # just the TUI suite
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Frame snapshots live in `__snapshots__/` next to each test; a diff there is
|
|
384
|
+
a visual diff of the screen. `yarn render:frames <dir>` writes true-color
|
|
385
|
+
ANSI captures of key screens — CI converts them to PNGs (textimg) and posts
|
|
386
|
+
them as a sticky screenshot comment on CLI PRs.
|
|
387
|
+
|
|
388
|
+
The pattern (stub `fetch`, seed the store, drive keys, assert on
|
|
389
|
+
`captureCharFrame()`) is in `src/chat/tui/screens/agent-picker.tui.test.tsx`.
|
|
390
|
+
|
|
391
|
+
## Building a standalone binary
|
|
392
|
+
|
|
393
|
+
`yarn build:binary` compiles the CLI into a single self-contained executable
|
|
394
|
+
(`dist/binaries/skydive-<target>`) with `bun build --compile` — no Node or
|
|
395
|
+
Bun needed on the machine that runs it, including for the OpenTUI chat
|
|
396
|
+
(`ensureBunAndReexec` no-ops because the binary _is_ Bun). Needs Bun ≥ 1.3.14
|
|
397
|
+
to build; pass `--target` for one of `bun-darwin-arm64`, `bun-darwin-x64`,
|
|
398
|
+
`bun-linux-x64`, `bun-linux-arm64` (defaults to the host, and the target's
|
|
399
|
+
`@opentui/core-*` optional dependency must be installed).
|
|
400
|
+
|
|
401
|
+
The script encodes three non-obvious constraints (older Bun emits unsignable
|
|
402
|
+
Mach-Os, OpenTUI's tree-sitter worker needs explicit embedding, and
|
|
403
|
+
`NODE_ENV=production` silently breaks highlight repaints) — see the header
|
|
404
|
+
comment in `scripts/build-binary.ts` before changing any of the flags it
|
|
405
|
+
passes.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as HttpError } from "./rest-CamHVOce.mjs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/chat/portal/machine.ts
|
|
7
|
+
/**
|
|
8
|
+
* Identity this machine registers under when the CLI shares it via the portal.
|
|
9
|
+
*
|
|
10
|
+
* The `-cli` suffix / `(CLI)` label keep a CLI-shared machine a DISTINCT portal
|
|
11
|
+
* device from the same host's Skydive Desktop app. `portal_device` is unique on
|
|
12
|
+
* (org, user, machineName), and directives route to whichever socket holds the
|
|
13
|
+
* device — if the CLI and desktop registered the same name they'd share a
|
|
14
|
+
* device row and both execute every directive. Distinct names also make the
|
|
15
|
+
* grant UI unambiguous about which surface is being authorized.
|
|
16
|
+
*/
|
|
17
|
+
function machineIdentity() {
|
|
18
|
+
const host = (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
|
|
19
|
+
return {
|
|
20
|
+
machineName: `${host}-cli`,
|
|
21
|
+
friendlyName: `${host} (CLI)`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const INHERITED_ENV = [
|
|
25
|
+
"HOME",
|
|
26
|
+
"USER",
|
|
27
|
+
"LOGNAME",
|
|
28
|
+
"SHELL",
|
|
29
|
+
"LANG",
|
|
30
|
+
"LC_ALL",
|
|
31
|
+
"TMPDIR",
|
|
32
|
+
"TERM",
|
|
33
|
+
"PATH"
|
|
34
|
+
];
|
|
35
|
+
function buildEnv(extra) {
|
|
36
|
+
const env = {};
|
|
37
|
+
for (const key of INHERITED_ENV) {
|
|
38
|
+
const value = process.env[key];
|
|
39
|
+
if (value !== void 0) env[key] = value;
|
|
40
|
+
}
|
|
41
|
+
if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
|
|
42
|
+
return env;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build the desktop-portal WebSocket URL from the chat origin. Mirrors the Rust
|
|
46
|
+
* desktop client: http→ws, https→wss, scheme-less defaults to wss, and the
|
|
47
|
+
* machine/label ride as query pairs (percent-encoded by URL).
|
|
48
|
+
*/
|
|
49
|
+
function portalWsUrl(appUrl, machine, label) {
|
|
50
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
51
|
+
let wsBase;
|
|
52
|
+
if (base.startsWith("https://")) wsBase = `wss://${base.slice(8)}`;
|
|
53
|
+
else if (base.startsWith("http://")) wsBase = `ws://${base.slice(7)}`;
|
|
54
|
+
else wsBase = `wss://${base}`;
|
|
55
|
+
const url = new URL(`${wsBase}/api/v1/portal/desktop`);
|
|
56
|
+
url.searchParams.set("machine", machine);
|
|
57
|
+
url.searchParams.set("label", label);
|
|
58
|
+
return url.toString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/chat/portal/api.ts
|
|
63
|
+
/**
|
|
64
|
+
* The portal's session-authed REST surface, shared by `PortalClient` (the
|
|
65
|
+
* TUI/`portal open` connection) and the `skydive portal` management
|
|
66
|
+
* commands, so the endpoint contracts and response schemas live in exactly
|
|
67
|
+
* one place.
|
|
68
|
+
*/
|
|
69
|
+
const deviceSchema = z.object({
|
|
70
|
+
id: z.string(),
|
|
71
|
+
machineName: z.string(),
|
|
72
|
+
friendlyName: z.string(),
|
|
73
|
+
connected: z.boolean(),
|
|
74
|
+
lastSeen: z.string().nullable(),
|
|
75
|
+
grantedAgentIds: z.array(z.string())
|
|
76
|
+
});
|
|
77
|
+
const devicesResponseSchema = z.object({
|
|
78
|
+
devices: z.array(deviceSchema),
|
|
79
|
+
agents: z.array(z.object({
|
|
80
|
+
id: z.string(),
|
|
81
|
+
name: z.string()
|
|
82
|
+
}))
|
|
83
|
+
});
|
|
84
|
+
const deviceTokenSchema = z.object({ token: z.string().min(1) });
|
|
85
|
+
async function portalFetch(auth, path, init) {
|
|
86
|
+
const res = await fetch(`${auth.appUrl}${path}`, {
|
|
87
|
+
method: init.method,
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: `Bearer ${auth.sessionToken}`,
|
|
90
|
+
accept: "application/json",
|
|
91
|
+
...init.body ? { "content-type": "application/json" } : {}
|
|
92
|
+
},
|
|
93
|
+
...init.body ? { body: init.body } : {}
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const body = await res.text().catch(() => "");
|
|
97
|
+
throw new HttpError(res.status, body);
|
|
98
|
+
}
|
|
99
|
+
return res.json();
|
|
100
|
+
}
|
|
101
|
+
async function fetchPortalDevices(auth) {
|
|
102
|
+
const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
|
|
103
|
+
return devicesResponseSchema.parse(json);
|
|
104
|
+
}
|
|
105
|
+
const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
|
|
106
|
+
/**
|
|
107
|
+
* Register this machine's device row without connecting. Connecting registers
|
|
108
|
+
* as a side effect; this covers granting an agent on a machine that has never
|
|
109
|
+
* shared yet (the grant references the device row).
|
|
110
|
+
*/
|
|
111
|
+
async function registerPortalDevice(auth, { machineName, friendlyName }) {
|
|
112
|
+
const json = await portalFetch(auth, "/api/v1/portal/devices", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
machineName,
|
|
116
|
+
friendlyName
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
return registerResponseSchema.parse(json).device;
|
|
120
|
+
}
|
|
121
|
+
/** Short-lived token the machine presents when dialing the portal WebSocket. */
|
|
122
|
+
async function mintPortalDeviceToken(auth) {
|
|
123
|
+
const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
|
|
124
|
+
return deviceTokenSchema.parse(json).token;
|
|
125
|
+
}
|
|
126
|
+
async function grantPortalAccess(auth, { deviceId, agentId }) {
|
|
127
|
+
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: JSON.stringify({ agentId })
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function revokePortalAccess(auth, { deviceId, agentId }) {
|
|
133
|
+
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The device row for a given machine identity. Matching is by `machineName`
|
|
137
|
+
* equality — the stable per-surface handle (`<host>-cli` vs the desktop's
|
|
138
|
+
* `<host>`), not the display label.
|
|
139
|
+
*/
|
|
140
|
+
function findThisDevice(devices, machineName) {
|
|
141
|
+
return devices.find((device) => device.machineName === machineName) ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
export { registerPortalDevice as a, machineIdentity as c, mintPortalDeviceToken as i, portalWsUrl as l, findThisDevice as n, revokePortalAccess as o, grantPortalAccess as r, buildEnv as s, fetchPortalDevices as t };
|