skydive-cli 0.1.0-beta.239 → 0.1.0-beta.276
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 +43 -6
- package/dist/js/bin.mjs +336 -30
- package/dist/js/{boot-CIdJ5Als.mjs → boot-FPpnUcyM.mjs} +124 -489
- package/dist/js/client-_OL8-XGH.mjs +367 -0
- package/dist/js/rolldown-runtime-Cz4Tg37Z.mjs +19 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -61,6 +61,11 @@ skydive secrets rm <KEY> --agent-id <id>
|
|
|
61
61
|
|
|
62
62
|
skydive chat # interactive chat TUI (Node; fetches Bun on first run)
|
|
63
63
|
skydive chat -p "<prompt>" # one-shot, non-interactive (runs under Node)
|
|
64
|
+
|
|
65
|
+
skydive portal open # open the portal to this machine, headless
|
|
66
|
+
skydive portal grant --agent <id|name>
|
|
67
|
+
skydive portal revoke --agent <id|name>
|
|
68
|
+
skydive portal status
|
|
64
69
|
```
|
|
65
70
|
|
|
66
71
|
Global flags: `--json`, `--quiet`, `--api-url <url>`.
|
|
@@ -218,12 +223,44 @@ What to know before enabling it:
|
|
|
218
223
|
outside the launch directory. Launch from a scoped directory, but treat the
|
|
219
224
|
grant as "this agent can act as me on this machine," not "this agent can only
|
|
220
225
|
see this folder."
|
|
221
|
-
- **Teardown
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
(
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
- **Teardown drops reachability, not the grant.** Quitting the chat closes the
|
|
227
|
+
portal connection (the machine becomes unreachable) and reaps any processes
|
|
228
|
+
the agent spawned, but the per-agent grant itself **persists** until you
|
|
229
|
+
revoke it (`skydive portal revoke` or the web app). The next time this
|
|
230
|
+
machine shares, previously granted agents can run commands immediately, with
|
|
231
|
+
no new prompt.
|
|
232
|
+
- **The TUI is not required.** With `-p`, `--share-machine` shares this machine
|
|
233
|
+
for the duration of that one run: the portal connects before the prompt is
|
|
234
|
+
sent, the target agent is granted access (the flag is the consent — there is
|
|
235
|
+
no prompt to approve), and the connection is torn down when the run ends.
|
|
236
|
+
For access that outlives a single run, use `skydive portal open` (below).
|
|
237
|
+
|
|
238
|
+
### Headless machine access (`skydive portal`)
|
|
239
|
+
|
|
240
|
+
`skydive portal` manages machine sharing without the TUI, for scripts, CI-ish
|
|
241
|
+
environments, and driving agents from other tools:
|
|
242
|
+
|
|
243
|
+
```sh
|
|
244
|
+
skydive portal open # connect and stay in the foreground (ctrl+c to close)
|
|
245
|
+
skydive portal open --agent grace # \u2026and grant that agent on first connect
|
|
246
|
+
skydive portal open --cwd ~/some/repo # working directory for the agent's commands
|
|
247
|
+
skydive portal grant --agent grace # allow an agent (machine must have opened once)
|
|
248
|
+
skydive portal revoke --agent grace # remove an agent's access
|
|
249
|
+
skydive portal status # machines, connection state, granted agents
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Semantics match the TUI exactly \u2014 same default-deny grants, same audit log,
|
|
253
|
+
same server-side kill switch. `open` keeps the machine reachable only while
|
|
254
|
+
the process runs (park it in tmux for long-lived access); `grant`/`revoke`
|
|
255
|
+
edit the persistent per-agent access.
|
|
256
|
+
|
|
257
|
+
Two unattended patterns:
|
|
258
|
+
|
|
259
|
+
- **One-shot:** `skydive chat -p "<prompt>" --agent <name> --share-machine` \u2014
|
|
260
|
+
shares, grants the target agent, runs the prompt, tears down.
|
|
261
|
+
- **Daemon:** one `skydive portal open` process, plus plain
|
|
262
|
+
`skydive chat -p "<prompt>" --agent <name>` one-shots from another shell \u2014
|
|
263
|
+
cheaper when firing many prompts, since each one skips the connect/grant.
|
|
227
264
|
|
|
228
265
|
## Environment variables
|
|
229
266
|
|
package/dist/js/bin.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-Cz4Tg37Z.mjs";
|
|
2
3
|
import { hideBin } from "yargs/helpers";
|
|
3
4
|
import yargs from "yargs";
|
|
4
5
|
import { createInterface } from "node:readline";
|
|
@@ -12,26 +13,10 @@ import { createHash } from "node:crypto";
|
|
|
12
13
|
import fs from "node:fs";
|
|
13
14
|
import zlib from "node:zlib";
|
|
14
15
|
import { createParser } from "eventsource-parser";
|
|
16
|
+
import os from "node:os";
|
|
15
17
|
|
|
16
|
-
//#region \0rolldown/runtime.js
|
|
17
|
-
var __defProp = Object.defineProperty;
|
|
18
|
-
var __exportAll = (all, no_symbols) => {
|
|
19
|
-
let target = {};
|
|
20
|
-
for (var name in all) {
|
|
21
|
-
__defProp(target, name, {
|
|
22
|
-
get: all[name],
|
|
23
|
-
enumerable: true
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
if (!no_symbols) {
|
|
27
|
-
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
28
|
-
}
|
|
29
|
-
return target;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
//#endregion
|
|
33
18
|
//#region package.json
|
|
34
|
-
var version$1 = "0.1.0-beta.
|
|
19
|
+
var version$1 = "0.1.0-beta.276";
|
|
35
20
|
|
|
36
21
|
//#endregion
|
|
37
22
|
//#region src/types.ts
|
|
@@ -743,7 +728,7 @@ const logoutCommand = {
|
|
|
743
728
|
else if (!argv.quiet) console.log("Logged out.");
|
|
744
729
|
}
|
|
745
730
|
};
|
|
746
|
-
const statusCommand = {
|
|
731
|
+
const statusCommand$1 = {
|
|
747
732
|
command: "status",
|
|
748
733
|
describe: "Show current authentication state",
|
|
749
734
|
handler: async (argv) => {
|
|
@@ -798,7 +783,7 @@ const statusCommand = {
|
|
|
798
783
|
const authCommand = {
|
|
799
784
|
command: "auth",
|
|
800
785
|
describe: "Manage authentication",
|
|
801
|
-
builder: (y) => y.command(loginCommand).command(logoutCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: login, logout, status"),
|
|
786
|
+
builder: (y) => y.command(loginCommand).command(logoutCommand).command(statusCommand$1).demandCommand(1, "Specify a subcommand: login, logout, status"),
|
|
802
787
|
handler: () => {}
|
|
803
788
|
};
|
|
804
789
|
|
|
@@ -1031,7 +1016,7 @@ const createCommand = {
|
|
|
1031
1016
|
console.log(" Save this key — it will not be shown again.");
|
|
1032
1017
|
}
|
|
1033
1018
|
};
|
|
1034
|
-
const revokeCommand = {
|
|
1019
|
+
const revokeCommand$1 = {
|
|
1035
1020
|
command: "revoke <id>",
|
|
1036
1021
|
describe: "Revoke an API key",
|
|
1037
1022
|
builder: (y) => y.positional("id", {
|
|
@@ -1059,7 +1044,7 @@ const keysCommand = {
|
|
|
1059
1044
|
type: "string",
|
|
1060
1045
|
demandOption: true,
|
|
1061
1046
|
describe: "Agent ID"
|
|
1062
|
-
}).command(listCommand$3).command(createCommand).command(revokeCommand).demandCommand(1, "Specify a subcommand: list, create, revoke"),
|
|
1047
|
+
}).command(listCommand$3).command(createCommand).command(revokeCommand$1).demandCommand(1, "Specify a subcommand: list, create, revoke"),
|
|
1063
1048
|
handler: () => {}
|
|
1064
1049
|
};
|
|
1065
1050
|
|
|
@@ -2108,7 +2093,7 @@ const chatCommand = {
|
|
|
2108
2093
|
}).option("share-machine", {
|
|
2109
2094
|
type: "boolean",
|
|
2110
2095
|
default: false,
|
|
2111
|
-
describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; you approve per agent)"
|
|
2096
|
+
describe: "Share this machine with the agent over the portal so it can run commands here (default-deny; in the TUI you approve per agent, with -p the flag grants the target agent for the run)"
|
|
2112
2097
|
}).option("theme", {
|
|
2113
2098
|
type: "string",
|
|
2114
2099
|
describe: `Pin a colorscheme (disables automatic light/dark switching). One of: ${themes.map((t) => t.id).join(", ")}. Defaults to following the terminal background; also settable via SKYDIVE_THEME.`
|
|
@@ -2152,7 +2137,7 @@ const chatCommand = {
|
|
|
2152
2137
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
2153
2138
|
process.exit(1);
|
|
2154
2139
|
}
|
|
2155
|
-
const { runChat } = await import("./boot-
|
|
2140
|
+
const { runChat } = await import("./boot-FPpnUcyM.mjs");
|
|
2156
2141
|
await runChat({
|
|
2157
2142
|
appUrl,
|
|
2158
2143
|
sessionToken: session.value.sessionToken,
|
|
@@ -2183,6 +2168,29 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2183
2168
|
process.exit(1);
|
|
2184
2169
|
}
|
|
2185
2170
|
}
|
|
2171
|
+
let machineShare = null;
|
|
2172
|
+
if (argv["share-machine"]) {
|
|
2173
|
+
const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
|
|
2174
|
+
let signalConnected;
|
|
2175
|
+
const connected = new Promise((resolve) => {
|
|
2176
|
+
signalConnected = resolve;
|
|
2177
|
+
});
|
|
2178
|
+
machineShare = new PortalClient({
|
|
2179
|
+
appUrl,
|
|
2180
|
+
sessionToken: session.value.sessionToken,
|
|
2181
|
+
cwd: process.cwd(),
|
|
2182
|
+
onState: (state) => {
|
|
2183
|
+
if (state.status === "connected") signalConnected();
|
|
2184
|
+
if (state.status === "error") console.error(`portal: connection error: ${state.error ?? "unknown"} — retrying`);
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
machineShare.enable();
|
|
2188
|
+
if (await Promise.race([connected.then(() => false), new Promise((resolve) => setTimeout(() => resolve(true), 3e4).unref())])) {
|
|
2189
|
+
machineShare.dispose();
|
|
2190
|
+
printError("Could not connect the portal within 30s — machine sharing is unavailable (network, or the portal kill switch is off). Re-run without --share-machine, or check `skydive portal status`.");
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2186
2194
|
try {
|
|
2187
2195
|
const result = await runPrint({
|
|
2188
2196
|
appUrl,
|
|
@@ -2190,12 +2198,15 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
2190
2198
|
prompt,
|
|
2191
2199
|
agentSelector: argv.agent ?? null,
|
|
2192
2200
|
conversationId: argv.conversation ?? null,
|
|
2193
|
-
json: argv.json
|
|
2201
|
+
json: argv.json,
|
|
2202
|
+
machineShare
|
|
2194
2203
|
});
|
|
2195
2204
|
if (argv.json) output(argv, result);
|
|
2196
2205
|
} catch (error) {
|
|
2197
2206
|
printError(error instanceof Error ? error.message : String(error));
|
|
2198
2207
|
process.exit(1);
|
|
2208
|
+
} finally {
|
|
2209
|
+
machineShare?.dispose();
|
|
2199
2210
|
}
|
|
2200
2211
|
}
|
|
2201
2212
|
|
|
@@ -2963,7 +2974,7 @@ var print_exports = /* @__PURE__ */ __exportAll({
|
|
|
2963
2974
|
* disambiguate: an `--agent` selector must match exactly one agent, and
|
|
2964
2975
|
* when it's omitted we only auto-pick if the account has exactly one.
|
|
2965
2976
|
*/
|
|
2966
|
-
async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json }) {
|
|
2977
|
+
async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversationId, json, machineShare }) {
|
|
2967
2978
|
const client = createRestClient({
|
|
2968
2979
|
appUrl,
|
|
2969
2980
|
sessionToken
|
|
@@ -2972,6 +2983,10 @@ async function runPrint({ appUrl, sessionToken, prompt, agentSelector, conversat
|
|
|
2972
2983
|
scope: "org",
|
|
2973
2984
|
onPage: null
|
|
2974
2985
|
}), agentSelector);
|
|
2986
|
+
if (machineShare) {
|
|
2987
|
+
if (!machineShare.isGranted(agent.id)) await machineShare.grantAgent(agent.id);
|
|
2988
|
+
console.error(`portal: shared this machine with ${agent.name} for this run (grant persists until revoked)`);
|
|
2989
|
+
}
|
|
2975
2990
|
let send;
|
|
2976
2991
|
try {
|
|
2977
2992
|
send = await client.sendMessage({
|
|
@@ -3330,7 +3345,7 @@ const switchCommand = {
|
|
|
3330
3345
|
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.");
|
|
3331
3346
|
process.exit(1);
|
|
3332
3347
|
}
|
|
3333
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
3348
|
+
const { runWorkspacePicker } = await import("./boot-FPpnUcyM.mjs");
|
|
3334
3349
|
await runWorkspacePicker(session);
|
|
3335
3350
|
return;
|
|
3336
3351
|
}
|
|
@@ -3367,10 +3382,301 @@ const workspaceCommand = {
|
|
|
3367
3382
|
handler: (argv) => runList(argv, { hint: true })
|
|
3368
3383
|
};
|
|
3369
3384
|
|
|
3385
|
+
//#endregion
|
|
3386
|
+
//#region src/chat/portal/machine.ts
|
|
3387
|
+
/**
|
|
3388
|
+
* Identity this machine registers under when the CLI shares it via the portal.
|
|
3389
|
+
*
|
|
3390
|
+
* The `-cli` suffix / `(CLI)` label keep a CLI-shared machine a DISTINCT portal
|
|
3391
|
+
* device from the same host's Skydive Desktop app. `portal_device` is unique on
|
|
3392
|
+
* (org, user, machineName), and directives route to whichever socket holds the
|
|
3393
|
+
* device — if the CLI and desktop registered the same name they'd share a
|
|
3394
|
+
* device row and both execute every directive. Distinct names also make the
|
|
3395
|
+
* grant UI unambiguous about which surface is being authorized.
|
|
3396
|
+
*/
|
|
3397
|
+
function machineIdentity() {
|
|
3398
|
+
const host = (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
|
|
3399
|
+
return {
|
|
3400
|
+
machineName: `${host}-cli`,
|
|
3401
|
+
friendlyName: `${host} (CLI)`
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
const INHERITED_ENV = [
|
|
3405
|
+
"HOME",
|
|
3406
|
+
"USER",
|
|
3407
|
+
"LOGNAME",
|
|
3408
|
+
"SHELL",
|
|
3409
|
+
"LANG",
|
|
3410
|
+
"LC_ALL",
|
|
3411
|
+
"TMPDIR",
|
|
3412
|
+
"TERM",
|
|
3413
|
+
"PATH"
|
|
3414
|
+
];
|
|
3415
|
+
function buildEnv(extra) {
|
|
3416
|
+
const env = {};
|
|
3417
|
+
for (const key of INHERITED_ENV) {
|
|
3418
|
+
const value = process.env[key];
|
|
3419
|
+
if (value !== void 0) env[key] = value;
|
|
3420
|
+
}
|
|
3421
|
+
if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
|
|
3422
|
+
return env;
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Build the desktop-portal WebSocket URL from the chat origin. Mirrors the Rust
|
|
3426
|
+
* desktop client: http→ws, https→wss, scheme-less defaults to wss, and the
|
|
3427
|
+
* machine/label ride as query pairs (percent-encoded by URL).
|
|
3428
|
+
*/
|
|
3429
|
+
function portalWsUrl(appUrl, machine, label) {
|
|
3430
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
3431
|
+
let wsBase;
|
|
3432
|
+
if (base.startsWith("https://")) wsBase = `wss://${base.slice(8)}`;
|
|
3433
|
+
else if (base.startsWith("http://")) wsBase = `ws://${base.slice(7)}`;
|
|
3434
|
+
else wsBase = `wss://${base}`;
|
|
3435
|
+
const url = new URL(`${wsBase}/api/v1/portal/desktop`);
|
|
3436
|
+
url.searchParams.set("machine", machine);
|
|
3437
|
+
url.searchParams.set("label", label);
|
|
3438
|
+
return url.toString();
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
//#endregion
|
|
3442
|
+
//#region src/chat/portal/api.ts
|
|
3443
|
+
/**
|
|
3444
|
+
* The portal's session-authed REST surface, shared by `PortalClient` (the
|
|
3445
|
+
* TUI/`portal open` connection) and the `skydive portal` management
|
|
3446
|
+
* commands, so the endpoint contracts and response schemas live in exactly
|
|
3447
|
+
* one place.
|
|
3448
|
+
*/
|
|
3449
|
+
const deviceSchema = z.object({
|
|
3450
|
+
id: z.string(),
|
|
3451
|
+
machineName: z.string(),
|
|
3452
|
+
friendlyName: z.string(),
|
|
3453
|
+
connected: z.boolean(),
|
|
3454
|
+
lastSeen: z.string().nullable(),
|
|
3455
|
+
grantedAgentIds: z.array(z.string())
|
|
3456
|
+
});
|
|
3457
|
+
const devicesResponseSchema = z.object({
|
|
3458
|
+
devices: z.array(deviceSchema),
|
|
3459
|
+
agents: z.array(z.object({
|
|
3460
|
+
id: z.string(),
|
|
3461
|
+
name: z.string()
|
|
3462
|
+
}))
|
|
3463
|
+
});
|
|
3464
|
+
const deviceTokenSchema = z.object({ token: z.string().min(1) });
|
|
3465
|
+
async function portalFetch(auth, path, init) {
|
|
3466
|
+
const res = await fetch(`${auth.appUrl}${path}`, {
|
|
3467
|
+
method: init.method,
|
|
3468
|
+
headers: {
|
|
3469
|
+
authorization: `Bearer ${auth.sessionToken}`,
|
|
3470
|
+
accept: "application/json",
|
|
3471
|
+
...init.body ? { "content-type": "application/json" } : {}
|
|
3472
|
+
},
|
|
3473
|
+
...init.body ? { body: init.body } : {}
|
|
3474
|
+
});
|
|
3475
|
+
if (!res.ok) {
|
|
3476
|
+
const body = await res.text().then((text) => text.slice(0, 120)).catch(() => "");
|
|
3477
|
+
throw new Error(`${init.method} ${path} failed (${res.status}): ${body}`);
|
|
3478
|
+
}
|
|
3479
|
+
return res.json();
|
|
3480
|
+
}
|
|
3481
|
+
async function fetchPortalDevices(auth) {
|
|
3482
|
+
const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
|
|
3483
|
+
return devicesResponseSchema.parse(json);
|
|
3484
|
+
}
|
|
3485
|
+
/** Short-lived token the machine presents when dialing the portal WebSocket. */
|
|
3486
|
+
async function mintPortalDeviceToken(auth) {
|
|
3487
|
+
const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
|
|
3488
|
+
return deviceTokenSchema.parse(json).token;
|
|
3489
|
+
}
|
|
3490
|
+
async function grantPortalAccess(auth, { deviceId, agentId }) {
|
|
3491
|
+
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
|
|
3492
|
+
method: "POST",
|
|
3493
|
+
body: JSON.stringify({ agentId })
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
async function revokePortalAccess(auth, { deviceId, agentId }) {
|
|
3497
|
+
await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* The device row for a given machine identity. Matching is by `machineName`
|
|
3501
|
+
* equality — the stable per-surface handle (`<host>-cli` vs the desktop's
|
|
3502
|
+
* `<host>`), not the display label.
|
|
3503
|
+
*/
|
|
3504
|
+
function findThisDevice(devices, machineName) {
|
|
3505
|
+
return devices.find((device) => device.machineName === machineName) ?? null;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
//#endregion
|
|
3509
|
+
//#region src/commands/portal.ts
|
|
3510
|
+
/**
|
|
3511
|
+
* Resolve everything a grant/revoke needs in one round trip: the devices
|
|
3512
|
+
* response carries both the user's machines and the org's agent roster, so
|
|
3513
|
+
* the `--agent` selector (id or name) resolves without a separate paginated
|
|
3514
|
+
* agent listing.
|
|
3515
|
+
*/
|
|
3516
|
+
async function resolveGrantTarget(argv) {
|
|
3517
|
+
const session = requireSession(argv);
|
|
3518
|
+
const { devices, agents } = await fetchPortalDevices(session);
|
|
3519
|
+
const agent = resolveAgent(agents, argv.agent);
|
|
3520
|
+
const { machineName } = machineIdentity();
|
|
3521
|
+
const device = findThisDevice(devices, machineName);
|
|
3522
|
+
if (!device) throw new Error(`this machine (${machineName}) isn't registered with the portal yet — run \`skydive portal open\` once to register it.`);
|
|
3523
|
+
return {
|
|
3524
|
+
session,
|
|
3525
|
+
agent,
|
|
3526
|
+
device
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
function agentOption(y) {
|
|
3530
|
+
return y.option("agent", {
|
|
3531
|
+
type: "string",
|
|
3532
|
+
demandOption: true,
|
|
3533
|
+
describe: "Agent, by id or name"
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
const openCommand = {
|
|
3537
|
+
command: "open",
|
|
3538
|
+
describe: "Open the portal to this machine so granted agents can run commands here (stays in the foreground; ctrl+c to close)",
|
|
3539
|
+
builder: (y) => y.option("agent", {
|
|
3540
|
+
type: "string",
|
|
3541
|
+
describe: "Also grant this agent (by id or name) access to the machine once connected"
|
|
3542
|
+
}).option("cwd", {
|
|
3543
|
+
type: "string",
|
|
3544
|
+
describe: "Working directory for the agent's commands (default: the current directory)"
|
|
3545
|
+
}),
|
|
3546
|
+
handler: async (argv) => {
|
|
3547
|
+
const session = requireSession(argv);
|
|
3548
|
+
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
3549
|
+
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
3550
|
+
const { machineName } = machineIdentity();
|
|
3551
|
+
const { PortalClient } = await import("./client-_OL8-XGH.mjs").then((n) => n.n);
|
|
3552
|
+
let lastLine = "";
|
|
3553
|
+
let signalConnected;
|
|
3554
|
+
const connected = new Promise((resolve) => {
|
|
3555
|
+
signalConnected = resolve;
|
|
3556
|
+
});
|
|
3557
|
+
const client = new PortalClient({
|
|
3558
|
+
appUrl: session.appUrl,
|
|
3559
|
+
sessionToken: session.sessionToken,
|
|
3560
|
+
cwd,
|
|
3561
|
+
onState: (state) => {
|
|
3562
|
+
if (state.status === "connected") signalConnected();
|
|
3563
|
+
const line = state.status === "error" ? `portal: connection error: ${state.error ?? "unknown"} — retrying` : `portal: ${state.status}`;
|
|
3564
|
+
if (line === lastLine) return;
|
|
3565
|
+
lastLine = line;
|
|
3566
|
+
if (state.status === "error") console.error(line);
|
|
3567
|
+
else if (!argv.quiet) console.log(line);
|
|
3568
|
+
}
|
|
3569
|
+
});
|
|
3570
|
+
const stop = () => {
|
|
3571
|
+
client.dispose();
|
|
3572
|
+
process.exit(0);
|
|
3573
|
+
};
|
|
3574
|
+
process.on("SIGINT", stop);
|
|
3575
|
+
process.on("SIGTERM", stop);
|
|
3576
|
+
client.enable();
|
|
3577
|
+
await connected;
|
|
3578
|
+
if (agent) {
|
|
3579
|
+
await client.grantAgent(agent.id);
|
|
3580
|
+
console.log(`portal: granted ${agent.name} access to this machine (persists until revoked)`);
|
|
3581
|
+
}
|
|
3582
|
+
console.log(`portal: open — granted agents can run commands on ${machineName} as your user, cwd ${cwd}. ctrl+c to close.`);
|
|
3583
|
+
await new Promise(() => {});
|
|
3584
|
+
}
|
|
3585
|
+
};
|
|
3586
|
+
const grantCommand = {
|
|
3587
|
+
command: "grant",
|
|
3588
|
+
describe: "Allow an agent to run commands on this machine (persists until revoked)",
|
|
3589
|
+
builder: agentOption,
|
|
3590
|
+
handler: async (argv) => {
|
|
3591
|
+
const { session, agent, device } = await resolveGrantTarget(argv);
|
|
3592
|
+
await grantPortalAccess(session, {
|
|
3593
|
+
deviceId: device.id,
|
|
3594
|
+
agentId: agent.id
|
|
3595
|
+
});
|
|
3596
|
+
if (argv.json) {
|
|
3597
|
+
output(argv, {
|
|
3598
|
+
deviceId: device.id,
|
|
3599
|
+
agentId: agent.id
|
|
3600
|
+
});
|
|
3601
|
+
return;
|
|
3602
|
+
}
|
|
3603
|
+
console.log(`Granted ${agent.name} access to ${device.friendlyName}. The agent can run commands here whenever its portal is open (\`skydive portal open\` or the chat TUI). Revoke with \`skydive portal revoke --agent ${agent.name}\`.`);
|
|
3604
|
+
}
|
|
3605
|
+
};
|
|
3606
|
+
const revokeCommand = {
|
|
3607
|
+
command: "revoke",
|
|
3608
|
+
describe: "Remove an agent's access to this machine",
|
|
3609
|
+
builder: agentOption,
|
|
3610
|
+
handler: async (argv) => {
|
|
3611
|
+
const { session, agent, device } = await resolveGrantTarget(argv);
|
|
3612
|
+
await revokePortalAccess(session, {
|
|
3613
|
+
deviceId: device.id,
|
|
3614
|
+
agentId: agent.id
|
|
3615
|
+
});
|
|
3616
|
+
if (argv.json) {
|
|
3617
|
+
output(argv, {
|
|
3618
|
+
deviceId: device.id,
|
|
3619
|
+
agentId: agent.id
|
|
3620
|
+
});
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
console.log(`Revoked ${agent.name}'s access to ${device.friendlyName}.`);
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
const statusCommand = {
|
|
3627
|
+
command: "status",
|
|
3628
|
+
describe: "Show your portal machines, connection state, and granted agents",
|
|
3629
|
+
handler: async (argv) => {
|
|
3630
|
+
const { devices, agents } = await fetchPortalDevices(requireSession(argv));
|
|
3631
|
+
if (argv.json) {
|
|
3632
|
+
output(argv, {
|
|
3633
|
+
devices,
|
|
3634
|
+
agents
|
|
3635
|
+
});
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
if (devices.length === 0) {
|
|
3639
|
+
console.log("No machines registered. Run `skydive portal open` to register this one.");
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
const { headers, rows } = buildDeviceTable(devices, agents, machineIdentity().machineName);
|
|
3643
|
+
printTable(headers, rows);
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
/**
|
|
3647
|
+
* Build the `portal status` table. Grant ids are shown as agent names when the
|
|
3648
|
+
* org roster resolves them (deleted agents fall back to the raw id), and the
|
|
3649
|
+
* device this CLI would register as is marked so "which row is me" doesn't
|
|
3650
|
+
* depend on knowing the `-cli` naming convention.
|
|
3651
|
+
*/
|
|
3652
|
+
function buildDeviceTable(devices, agents, thisMachineName) {
|
|
3653
|
+
const agentNames = new Map(agents.map((a) => [a.id, a.name]));
|
|
3654
|
+
return {
|
|
3655
|
+
headers: [
|
|
3656
|
+
"Machine",
|
|
3657
|
+
"Connected",
|
|
3658
|
+
"Last seen",
|
|
3659
|
+
"Granted agents"
|
|
3660
|
+
],
|
|
3661
|
+
rows: devices.map((device) => [
|
|
3662
|
+
device.machineName === thisMachineName ? `${device.friendlyName} *` : device.friendlyName,
|
|
3663
|
+
device.connected ? "yes" : "no",
|
|
3664
|
+
device.lastSeen ?? "-",
|
|
3665
|
+
device.grantedAgentIds.length === 0 ? "-" : device.grantedAgentIds.map((id) => agentNames.get(id) ?? id).join(", ")
|
|
3666
|
+
])
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
const portalCommand = {
|
|
3670
|
+
command: "portal",
|
|
3671
|
+
describe: "Open this machine's portal to agents and manage their access",
|
|
3672
|
+
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status"),
|
|
3673
|
+
handler: () => {}
|
|
3674
|
+
};
|
|
3675
|
+
|
|
3370
3676
|
//#endregion
|
|
3371
3677
|
//#region src/cli.ts
|
|
3372
3678
|
function createCli(argv) {
|
|
3373
|
-
return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch which workspace `skydive chat` uses").option("json", {
|
|
3679
|
+
return yargs(argv).scriptName("skydive").usage("$0 <command> [options]").example("skydive auth login", "Store your API key").example("skydive chat", "Open the interactive chat TUI (prod)").example("skydive chat --api-url http://localhost:4500", "Chat against a local dev API").example("skydive agents list", "List your agents").example("skydive keys create \"my-key\"", "Create an API key").example("skydive secrets set OPENAI_API_KEY --agent-id <id>", "Set a secret (value from stdin)").example("skydive workspace list", "List workspaces on your account").example("skydive workspace switch acme-team", "Switch which workspace `skydive chat` uses").example("skydive portal open --agent grace", "Open the portal to this machine for an agent, headless (no TUI)").option("json", {
|
|
3374
3680
|
type: "boolean",
|
|
3375
3681
|
default: false,
|
|
3376
3682
|
global: true,
|
|
@@ -3384,7 +3690,7 @@ function createCli(argv) {
|
|
|
3384
3690
|
type: "string",
|
|
3385
3691
|
global: true,
|
|
3386
3692
|
describe: "Override API base URL"
|
|
3387
|
-
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
3693
|
+
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version$1).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
3388
3694
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
3389
3695
|
process.exit(1);
|
|
3390
3696
|
});
|
|
@@ -3457,4 +3763,4 @@ function resolveArgv(args, tty = {
|
|
|
3457
3763
|
createCli(resolveArgv(hideBin(process.argv))).parse();
|
|
3458
3764
|
|
|
3459
3765
|
//#endregion
|
|
3460
|
-
export {
|
|
3766
|
+
export { themesForMode as A, monoTheme as C, themeMode as D, themeForMode as E, DEFAULT_APP_URL as F, getConfigPath as I, getSavedTheme as L, listWorkspaces as M, setActiveWorkspace as N, themeModeFromColorFgBg as O, DEFAULT_API_URL as P, resolveWebUrl as R, findTheme as S, theme as T, isRecord as _, buildEnv as a, DEFAULT_THEME_ID as b, resolveAgent as c, parseExternalOauthConnectParams as d, parseOauthConnectParams as f, errorMessage as g, parseConnectCard as h, mintPortalDeviceToken as i, getActiveWorkspaceId as j, themeVersion as k, MASK_CHAR as l, resolveConnectUrl as m, findThisDevice as n, machineIdentity as o, reconcileMaskedInput as p, grantPortalAccess as r, portalWsUrl as s, fetchPortalDevices as t, cardActionErrorMessage as u, HttpError as v, noColorRequested as w, applyTheme as x, createRestClient as y, saveTheme as z };
|