skydive-cli 0.1.0-beta.96 → 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 +187 -41
- package/dist/js/api-CDTKq_5Q.mjs +145 -0
- package/dist/js/bin.mjs +1558 -1292
- package/dist/js/{boot-DRWra2vY.mjs → boot-CQLDUeZ9.mjs} +4629 -1024
- 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/rest-CamHVOce.mjs +457 -0
- package/dist/js/theme-CuQhvqzN.mjs +990 -0
- package/dist/js/util-CeisaZVY.mjs +15 -0
- package/package.json +9 -3
- package/dist/js/print-CQ8b7JUC.mjs +0 -745
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,9 +8,48 @@ 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` — 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
|
+
```
|
|
47
|
+
|
|
48
|
+
`skydive auth status` shows both credentials; `skydive auth logout` revokes
|
|
49
|
+
the auto-minted key and clears them.
|
|
50
|
+
|
|
51
|
+
The config location is platform-conventional (resolved via `env-paths`), so
|
|
52
|
+
it is **not** `~/.config/skydive/` everywhere:
|
|
14
53
|
|
|
15
54
|
| Platform | Path |
|
|
16
55
|
| -------- | ---------------------------------------------------------------- |
|
|
@@ -21,29 +60,6 @@ location is platform-conventional (resolved via `env-paths`), so it is **not**
|
|
|
21
60
|
`skydive auth status` prints the real resolved path (`Config: …`) — trust that
|
|
22
61
|
over this table if they ever disagree.
|
|
23
62
|
|
|
24
|
-
- **API key** (`sky_live_…`). Mint one at `skydive.com/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
63
|
## Commands
|
|
48
64
|
|
|
49
65
|
```sh
|
|
@@ -61,10 +77,62 @@ skydive secrets rm <KEY> --agent-id <id>
|
|
|
61
77
|
|
|
62
78
|
skydive chat # interactive chat TUI (Node; fetches Bun on first run)
|
|
63
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
|
|
64
87
|
```
|
|
65
88
|
|
|
66
89
|
Global flags: `--json`, `--quiet`, `--api-url <url>`.
|
|
67
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
|
+
|
|
68
136
|
## `skydive secrets`
|
|
69
137
|
|
|
70
138
|
Manage the environment secrets injected into an agent's sandbox. All
|
|
@@ -139,21 +207,22 @@ skydive chat -p "status?" --agent grace --json # structured envelo
|
|
|
139
207
|
- `--conversation <id>` continues an existing thread; omitted, it starts a new
|
|
140
208
|
one. The conversation id is included in `--json` output for chaining.
|
|
141
209
|
- `-p` needs a user session just like `chat`, but never opens the interactive
|
|
142
|
-
browser login — sign in first with `skydive auth login
|
|
210
|
+
browser login — sign in first with `skydive auth login` or set
|
|
143
211
|
`SKYDIVE_SESSION_TOKEN`.
|
|
144
212
|
- `--json` prints `{ agentId, agentName, conversationId, isNewConversation,
|
|
145
|
-
|
|
213
|
+
messageId, text }` instead of streaming the raw text.
|
|
146
214
|
- **Recovering a dropped run.** A long run's stream can be cut off at the edge
|
|
147
|
-
(a gateway 502/504) after the
|
|
148
|
-
server-side, so a blind retry would re-execute
|
|
149
|
-
`
|
|
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:
|
|
150
218
|
|
|
151
219
|
```sh
|
|
152
|
-
skydive
|
|
220
|
+
skydive messages get <messageId> # prints the reply; --json for the envelope
|
|
153
221
|
```
|
|
154
222
|
|
|
155
|
-
`
|
|
156
|
-
still streaming or already finished.
|
|
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.
|
|
157
226
|
|
|
158
227
|
It defaults to the production API (`https://api.skydive.com`). For local
|
|
159
228
|
dev, point it at your stack:
|
|
@@ -203,7 +272,10 @@ This is off by default (default-deny) and you opt in explicitly:
|
|
|
203
272
|
|
|
204
273
|
- `skydive chat --share-machine` enables it at launch, or
|
|
205
274
|
- `ctrl+t` toggles it from the chat screen (the status bar shows `local access`
|
|
206
|
-
when on, `revoke access` when off)
|
|
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).
|
|
207
279
|
|
|
208
280
|
What to know before enabling it:
|
|
209
281
|
|
|
@@ -217,12 +289,70 @@ What to know before enabling it:
|
|
|
217
289
|
outside the launch directory. Launch from a scoped directory, but treat the
|
|
218
290
|
grant as "this agent can act as me on this machine," not "this agent can only
|
|
219
291
|
see this folder."
|
|
220
|
-
- **Teardown
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
(
|
|
224
|
-
|
|
225
|
-
|
|
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.
|
|
226
356
|
|
|
227
357
|
## Environment variables
|
|
228
358
|
|
|
@@ -257,3 +387,19 @@ them as a sticky screenshot comment on CLI PRs.
|
|
|
257
387
|
|
|
258
388
|
The pattern (stub `fetch`, seed the store, drive keys, assert on
|
|
259
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 };
|