skydive-cli 0.5.0-beta.6 → 0.5.0-beta.8

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.
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
3
+ import { z } from "zod";
4
+
5
+ //#region ../portal-daemon/src/api.ts
6
+ /**
7
+ * The portal's session-authed REST surface, shared by `PortalClient` (the
8
+ * TUI/`portal open` connection) and the `skydive portal` management
9
+ * commands, so the endpoint contracts and response schemas live in exactly
10
+ * one place.
11
+ */
12
+ const deviceSchema = z.object({
13
+ id: z.string(),
14
+ machineName: z.string(),
15
+ friendlyName: z.string(),
16
+ connected: z.boolean(),
17
+ lastSeen: z.string().nullable(),
18
+ grantedAgentIds: z.array(z.string())
19
+ });
20
+ const devicesResponseSchema = z.object({
21
+ devices: z.array(deviceSchema),
22
+ agents: z.array(z.object({
23
+ id: z.string(),
24
+ name: z.string()
25
+ }))
26
+ });
27
+ const deviceTokenSchema = z.object({ token: z.string().min(1) });
28
+ async function portalFetch(auth, path, init) {
29
+ const res = await fetch(`${auth.appUrl}${path}`, {
30
+ method: init.method,
31
+ headers: {
32
+ authorization: `Bearer ${auth.sessionToken}`,
33
+ accept: "application/json",
34
+ ...init.body ? { "content-type": "application/json" } : {}
35
+ },
36
+ ...init.body ? { body: init.body } : {}
37
+ });
38
+ if (!res.ok) {
39
+ const body = await res.text().catch(() => "");
40
+ throw new HttpError(res.status, body);
41
+ }
42
+ return res.json();
43
+ }
44
+ async function fetchPortalDevices(auth) {
45
+ const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
46
+ return devicesResponseSchema.parse(json);
47
+ }
48
+ const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
49
+ /**
50
+ * Register this machine's device row without connecting. Connecting registers
51
+ * as a side effect; this covers granting an agent on a machine that has never
52
+ * shared yet (the grant references the device row).
53
+ */
54
+ async function registerPortalDevice(auth, { machineName, friendlyName }) {
55
+ const json = await portalFetch(auth, "/api/v1/portal/devices", {
56
+ method: "POST",
57
+ body: JSON.stringify({
58
+ machineName,
59
+ friendlyName
60
+ })
61
+ });
62
+ return registerResponseSchema.parse(json).device;
63
+ }
64
+ /** Short-lived token the machine presents when dialing the portal WebSocket. */
65
+ async function mintPortalDeviceToken(auth) {
66
+ const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
67
+ return deviceTokenSchema.parse(json).token;
68
+ }
69
+ const forwardTargetSchema = z.object({
70
+ daemonOrigin: z.string().min(1),
71
+ token: z.string().min(1),
72
+ expiresInSeconds: z.number()
73
+ });
74
+ /**
75
+ * Everything `portal forward` needs to dial an agent's sandbox daemon through
76
+ * the agent-webserver edge Worker: the daemon's stable public origin and a
77
+ * daemon auth token (canUse-gated server-side).
78
+ */
79
+ async function fetchForwardTarget(auth, agentId) {
80
+ const json = await portalFetch(auth, "/api/v1/portal/forward-target", {
81
+ method: "POST",
82
+ body: JSON.stringify({ agentId })
83
+ });
84
+ return forwardTargetSchema.parse(json);
85
+ }
86
+ async function grantPortalAccess(auth, { deviceId, agentId, conversationId }) {
87
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
88
+ method: "POST",
89
+ body: JSON.stringify({
90
+ agentId,
91
+ conversationId
92
+ })
93
+ });
94
+ }
95
+ async function revokePortalAccess(auth, { deviceId, agentId }) {
96
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
97
+ }
98
+ /**
99
+ * The device row for a given machine identity. Matching is by `machineName`
100
+ * equality — the stable handle the machine registers under, not the display
101
+ * label.
102
+ */
103
+ function findThisDevice(devices, machineName) {
104
+ return devices.find((device) => device.machineName === machineName) ?? null;
105
+ }
106
+ /**
107
+ * One-time grant migration onto the merged device. Earlier CLI builds
108
+ * registered a separate `<machineName>-cli` device, so a user's existing
109
+ * approvals hang off that row; the merged device would start with zero grants
110
+ * and every already-authorized agent would ask again. Copy any grant the
111
+ * merged device is missing (the grant endpoint upserts, so re-runs are
112
+ * no-ops). The legacy row is left in place — an old CLI build may still
113
+ * connect under it. Returns how many grants were copied.
114
+ */
115
+ async function unifyLegacyCliGrants(auth, machineName) {
116
+ const { devices } = await fetchPortalDevices(auth);
117
+ const merged = findThisDevice(devices, machineName);
118
+ const legacy = findThisDevice(devices, `${machineName}-cli`);
119
+ if (!merged || !legacy) return 0;
120
+ const have = new Set(merged.grantedAgentIds);
121
+ const missing = legacy.grantedAgentIds.filter((id) => !have.has(id));
122
+ for (const agentId of missing) await grantPortalAccess(auth, {
123
+ deviceId: merged.id,
124
+ agentId,
125
+ conversationId: null
126
+ });
127
+ return missing.length;
128
+ }
129
+
130
+ //#endregion
131
+ export { mintPortalDeviceToken as a, unifyLegacyCliGrants as c, grantPortalAccess as i, fetchPortalDevices as n, registerPortalDevice as o, findThisDevice as r, revokePortalAccess as s, fetchForwardTarget as t };
package/dist/js/bin.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-BFMEL3KX.mjs";
3
- import { A as getStoredApiKeyWorkspaceName, C as getLastSeenVersion, F as resolveManagementAuth, I as resolveSession, M as resolveAppUrl, N as resolveChatAuth, O as getShareMachineDefault, P as resolveConfig, R as saveConfig, S as getConfigPath, T as getPromptHistoryPath, V as setLastSeenVersion, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getUpdateCheckDisabled, k as getStoredApiKeyId, v as DEFAULT_API_URL, w as getPreference, x as deleteConfig, z as saveSession } from "./print-CxygaEtp.mjs";
2
+ import { C as ensureActiveOrganization, D as setActiveWorkspace, E as listWorkspaces, O as name, T as getSessionIdentity, a as installCrashHandler, k as version, t as maybeStartProfiling, u as brandHelpArt, w as getActiveWorkspaceId, x as themes } from "./profiler-DawY0V0Z.mjs";
3
+ import { A as getStoredApiKeyWorkspaceName, C as getLastSeenVersion, F as resolveManagementAuth, I as resolveSession, M as resolveAppUrl, N as resolveChatAuth, O as getShareMachineDefault, P as resolveConfig, R as saveConfig, S as getConfigPath, T as getPromptHistoryPath, V as setLastSeenVersion, _ as API_KEY_PREFIX, b as PREFERENCES, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as getUpdateCheckDisabled, k as getStoredApiKeyId, v as DEFAULT_API_URL, w as getPreference, x as deleteConfig, z as saveSession } from "./print-BpuyEfWX.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
6
6
  import { t as createRestClient } from "./rest-I3imNduB.mjs";
7
7
  import "./billing-blocked-2wju4gC_.mjs";
8
- import { a as registerPortalDevice, c as resolveMachineIdentity, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice } from "./client-CmEF9zrz.mjs";
9
- import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-8LePusmh.mjs";
10
- import { t as SandboxStream } from "./client-Cn2af31H.mjs";
8
+ import { r as resolveMachineIdentity } from "./client-DabRpc_T.mjs";
9
+ import { i as queryDaemonStatus, l as PORTAL_DAEMON_FLAG, n as ensureDaemonRunning, s as stopDaemon, u as daemonPaths } from "./daemon-k2kVkJ8D.mjs";
10
+ import { i as grantPortalAccess, n as fetchPortalDevices, o as registerPortalDevice, r as findThisDevice, s as revokePortalAccess } from "./api-DG5W6iwx.mjs";
11
+ import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
12
  import { hideBin } from "yargs/helpers";
12
13
  import yargs from "yargs";
13
14
  import os, { hostname, tmpdir } from "node:os";
@@ -1516,7 +1517,7 @@ const importCommand = {
1516
1517
  process.exit(1);
1517
1518
  }
1518
1519
  }
1519
- const { runChat } = await import("./boot-B69z6Iv9.mjs");
1520
+ const { runChat } = await import("./boot-BCClEpCM.mjs");
1520
1521
  await runChat({
1521
1522
  appUrl,
1522
1523
  sessionToken: session.value.sessionToken,
@@ -1543,7 +1544,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1543
1544
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1544
1545
  process.exit(1);
1545
1546
  }
1546
- const { connectMachineShare } = await import("./print-share-D9RczQx8.mjs");
1547
+ const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
1547
1548
  const machineShare = await connectMachineShare({
1548
1549
  appUrl,
1549
1550
  sessionToken: session.value.sessionToken,
@@ -1551,7 +1552,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1551
1552
  });
1552
1553
  const extra = (argv.print ?? "").trim();
1553
1554
  const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
1554
- const { runPrint } = await import("./print-DxB3fc_B.mjs");
1555
+ const { runPrint } = await import("./print-Wakr3GJd.mjs");
1555
1556
  try {
1556
1557
  const result = await runPrint({
1557
1558
  appUrl,
@@ -1928,7 +1929,7 @@ const chatCommand = {
1928
1929
  sessionToken: auth.value.token,
1929
1930
  agentSelector: argv.agent ?? null
1930
1931
  });
1931
- const { runChat } = await import("./boot-B69z6Iv9.mjs");
1932
+ const { runChat } = await import("./boot-BCClEpCM.mjs");
1932
1933
  await runChat({
1933
1934
  appUrl: auth.value.appUrl,
1934
1935
  sessionToken: auth.value.token,
@@ -1995,7 +1996,7 @@ async function runPrintMode({ argv, appUrl }) {
1995
1996
  printError(`${auth.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
1996
1997
  process.exit(1);
1997
1998
  }
1998
- const { runPrint, readStdin } = await import("./print-DxB3fc_B.mjs");
1999
+ const { runPrint, readStdin } = await import("./print-Wakr3GJd.mjs");
1999
2000
  await ensureAgentWorkspace({
2000
2001
  appUrl,
2001
2002
  sessionToken: auth.value.token,
@@ -2020,7 +2021,7 @@ async function runPrintMode({ argv, appUrl }) {
2020
2021
  process.exit(1);
2021
2022
  }
2022
2023
  } else {
2023
- const { connectMachineShare } = await import("./print-share-D9RczQx8.mjs");
2024
+ const { connectMachineShare } = await import("./print-share-CKLPmsg0.mjs");
2024
2025
  machineShare = await connectMachineShare({
2025
2026
  appUrl: auth.value.appUrl,
2026
2027
  sessionToken: auth.value.token,
@@ -2084,7 +2085,7 @@ const getCommand$1 = {
2084
2085
  printError(`${auth.error.message} Run \`skydive auth login\` first, or set SKYDIVE_API_KEY / SKYDIVE_SESSION_TOKEN.`);
2085
2086
  process.exit(1);
2086
2087
  }
2087
- const { messageGet } = await import("./print-DxB3fc_B.mjs");
2088
+ const { messageGet } = await import("./print-Wakr3GJd.mjs");
2088
2089
  try {
2089
2090
  const result = await messageGet({
2090
2091
  appUrl: auth.value.appUrl,
@@ -2287,7 +2288,7 @@ const switchCommand = {
2287
2288
  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.");
2288
2289
  process.exit(1);
2289
2290
  }
2290
- const { runWorkspacePicker } = await import("./boot-B69z6Iv9.mjs");
2291
+ const { runWorkspacePicker } = await import("./boot-BCClEpCM.mjs");
2291
2292
  await runWorkspacePicker(session);
2292
2293
  return;
2293
2294
  }
@@ -2366,7 +2367,7 @@ const openCommand = {
2366
2367
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
2367
2368
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
2368
2369
  const { machineName } = await resolveMachineIdentity();
2369
- const { PortalDaemonClient } = await import("./daemon-client-DV6QAnI4.mjs");
2370
+ const { PortalDaemonClient } = await import("./daemon-client-C2BvZgKO.mjs");
2370
2371
  let lastLine = "";
2371
2372
  let signalConnected;
2372
2373
  const connected = new Promise((resolve) => {
@@ -2593,10 +2594,45 @@ const daemonCommand = {
2593
2594
  builder: (y) => y.command(daemonStatusCommand).command(daemonStopCommand).command(daemonStartCommand).command(daemonRestartCommand).command(daemonLogsCommand).demandCommand(1, "Specify a subcommand: status, stop, start, restart, logs"),
2594
2595
  handler: () => {}
2595
2596
  };
2597
+ const forwardCommand = {
2598
+ command: "forward <port>",
2599
+ describe: "Forward localhost:<port> to the agent sandbox's port, so a dev server running in the sandbox is reachable at http://localhost:<port> (stays in the foreground; ctrl+c to stop)",
2600
+ builder: (y) => y.positional("port", {
2601
+ type: "number",
2602
+ demandOption: true,
2603
+ describe: "Sandbox port to forward to (e.g. 3000 for a dev server)"
2604
+ }).option("agent", {
2605
+ type: "string",
2606
+ describe: "Agent, by id or name (defaults when the account has one)"
2607
+ }).option("local-port", {
2608
+ type: "number",
2609
+ describe: "Local port to listen on (default: same as <port>)"
2610
+ }),
2611
+ handler: async (argv) => {
2612
+ const session = requireSession(argv);
2613
+ const agent = resolveAgent((await fetchPortalDevices(session)).agents, argv.agent ?? null);
2614
+ const targetPort = argv.port;
2615
+ const localPort = argv["local-port"] ?? targetPort;
2616
+ const { startForward } = await import("./forward-18QoL5dO.mjs");
2617
+ const listener = await startForward({
2618
+ auth: session,
2619
+ agentId: agent.id,
2620
+ localPort,
2621
+ targetPort,
2622
+ log: (msg) => console.error(msg)
2623
+ });
2624
+ console.log(`Forwarding http://localhost:${listener.port} -> ${agent.name}'s sandbox port ${targetPort}. ctrl+c to stop.`);
2625
+ await new Promise((resolve) => {
2626
+ process.once("SIGINT", () => resolve());
2627
+ process.once("SIGTERM", () => resolve());
2628
+ });
2629
+ await listener.close();
2630
+ }
2631
+ };
2596
2632
  const portalCommand = {
2597
2633
  command: "portal",
2598
2634
  describe: "Open this machine's portal to agents and manage their access",
2599
- builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, daemon"),
2635
+ builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(forwardCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, forward, daemon"),
2600
2636
  handler: () => {}
2601
2637
  };
2602
2638
 
@@ -2642,7 +2678,7 @@ const sandboxCommand = {
2642
2678
  handler: async (argv) => {
2643
2679
  const session = requireSession(argv);
2644
2680
  const { createRestClient } = await import("./rest-D29qNkto.mjs");
2645
- const { resolveAgent } = await import("./print-DxB3fc_B.mjs");
2681
+ const { resolveAgent } = await import("./print-Wakr3GJd.mjs");
2646
2682
  const client = createRestClient({
2647
2683
  appUrl: session.appUrl,
2648
2684
  sessionToken: session.sessionToken
@@ -2711,7 +2747,7 @@ async function runPty({ session, agentId, agentName }) {
2711
2747
  return 1;
2712
2748
  }
2713
2749
  console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
2714
- const { runRawPtyPassthrough } = await import("./raw-pty-3EkG-jjH.mjs");
2750
+ const { runRawPtyPassthrough } = await import("./raw-pty-DmdUf4_w.mjs");
2715
2751
  const result = await runRawPtyPassthrough({
2716
2752
  stdin: process.stdin,
2717
2753
  stdout: process.stdout,
@@ -2800,7 +2836,7 @@ const fsEditCommand = {
2800
2836
  const session = requireSession(argv);
2801
2837
  const remotePath = argv.path;
2802
2838
  const { createRestClient } = await import("./rest-D29qNkto.mjs");
2803
- const { resolveAgent } = await import("./print-DxB3fc_B.mjs");
2839
+ const { resolveAgent } = await import("./print-Wakr3GJd.mjs");
2804
2840
  const client = createRestClient({
2805
2841
  appUrl: session.appUrl,
2806
2842
  sessionToken: session.sessionToken
@@ -4170,7 +4206,7 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
4170
4206
  process.exit(0);
4171
4207
  }
4172
4208
  if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
4173
- const { runPortalDaemon } = await import("./daemon-DgmqQj99.mjs");
4209
+ const { runPortalDaemon } = await import("./daemon-BxU59xie.mjs");
4174
4210
  runPortalDaemon(process.argv);
4175
4211
  } else runCli();
4176
4212
  function runCli() {
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-BFMEL3KX.mjs";
3
- import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-CxygaEtp.mjs";
2
+ import { D as setActiveWorkspace, E as listWorkspaces, S as themesForMode, _ as themeForMode, a as installCrashHandler, b as themeVersion, c as MARK_CELLS, d as DEFAULT_THEME_ID, f as applyTheme, g as theme, h as noColorRequested, i as writeArtifact, l as WORDMARK, m as monoTheme, n as profilingEnabled, o as buildCrashReport, p as findTheme, r as record, s as writeCrashReport, v as themeMode, w as getActiveWorkspaceId, y as themeModeFromColorFgBg } from "./profiler-DawY0V0Z.mjs";
3
+ import { B as saveTheme, D as getSavedTheme, E as getReviewStateDir, L as resolveWebUrl, S as getConfigPath, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, y as DEFAULT_APP_URL } from "./print-BpuyEfWX.mjs";
4
4
  import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
5
5
  import { a as isRecord, i as errorMessage, n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-I3imNduB.mjs";
6
6
  import { i as billingBlockedOutcomeFromSendResponse } from "./billing-blocked-2wju4gC_.mjs";
7
- import { t as PortalClient } from "./client-CmEF9zrz.mjs";
8
- import "./daemon-8LePusmh.mjs";
9
- import { t as SandboxStream } from "./client-Cn2af31H.mjs";
10
- import { t as PortalDaemonClient } from "./daemon-client-DmwnQi8B.mjs";
11
- import { t as runRawPtyPassthrough } from "./raw-pty-B6mAroiI.mjs";
7
+ import { t as PortalClient } from "./client-DabRpc_T.mjs";
8
+ import "./daemon-k2kVkJ8D.mjs";
9
+ import "./api-DG5W6iwx.mjs";
10
+ import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
11
+ import { t as PortalDaemonClient } from "./daemon-client-fxf1A25Z.mjs";
12
+ import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
12
13
  import * as os$1 from "node:os";
13
14
  import { homedir, platform, release, tmpdir } from "node:os";
14
15
  import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { t as PortalClient } from "./client-DabRpc_T.mjs";
3
+ import "./api-DG5W6iwx.mjs";
4
+
5
+ export { PortalClient };
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { t as HttpError } from "./http-error-DzyrsLAZ.mjs";
2
+ import { a as mintPortalDeviceToken, c as unifyLegacyCliGrants, i as grantPortalAccess, n as fetchPortalDevices, r as findThisDevice } from "./api-DG5W6iwx.mjs";
3
3
  import os from "node:os";
4
4
  import { z } from "zod";
5
5
  import { execFile, spawn } from "node:child_process";
6
- import { WebSocket } from "ws";
6
+ import net from "node:net";
7
+ import { WebSocket, createWebSocketStream } from "ws";
7
8
 
8
9
  //#region ../portal-daemon/src/machine.ts
9
10
  /**
@@ -134,6 +135,18 @@ const ctrlMessageSchema = z.discriminatedUnion("t", [
134
135
  conversationId: z.string().nullable().optional()
135
136
  }),
136
137
  z.object({ t: z.literal("stdin_eof") }),
138
+ z.object({
139
+ t: z.literal("reverse_listen"),
140
+ listenPort: z.number().int().positive(),
141
+ targetPort: z.number().int().positive(),
142
+ dialOrigin: z.string().min(1),
143
+ token: z.string().min(1)
144
+ }),
145
+ z.object({ t: z.literal("reverse_listening") }),
146
+ z.object({
147
+ t: z.literal("reverse_token"),
148
+ token: z.string().min(1)
149
+ }),
137
150
  z.object({ t: z.literal("pause") }),
138
151
  z.object({ t: z.literal("resume") }),
139
152
  z.object({ t: z.literal("cancel") }),
@@ -169,6 +182,62 @@ function decodeFrame(frame) {
169
182
  };
170
183
  }
171
184
 
185
+ //#endregion
186
+ //#region ../portal-daemon/src/reverse-listener.ts
187
+ /**
188
+ * The desktop half of an agent-initiated expose tunnel (`platform portal
189
+ * expose`): listen on the local loopback and pipe each accepted TCP
190
+ * connection to the agent sandbox's daemon (`/portal/tcp`) through the
191
+ * agent-webserver edge Worker — the same per-connection dial-and-pipe as the
192
+ * user-initiated `skydive portal forward`, just started from a directive
193
+ * instead of a terminal. Tunnel bytes never touch the directive stream that
194
+ * created this listener; it only anchors the lifetime and carries the token.
195
+ *
196
+ * `target()` is read per connection so a token refresh (or any future
197
+ * retarget) applies to the next dial without touching established pipes.
198
+ */
199
+ var ReverseListener = class {
200
+ server = null;
201
+ conns = /* @__PURE__ */ new Set();
202
+ constructor(opts) {
203
+ this.opts = opts;
204
+ }
205
+ start() {
206
+ const server = net.createServer((sock) => {
207
+ this.conns.add(sock);
208
+ sock.once("close", () => this.conns.delete(sock));
209
+ sock.pause();
210
+ const { dialOrigin, targetPort, token } = this.opts.target();
211
+ const ws = new WebSocket(`${dialOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${token}` } });
212
+ ws.on("open", () => {
213
+ const stream = createWebSocketStream(ws);
214
+ stream.on("error", () => sock.destroy());
215
+ sock.on("error", () => stream.destroy());
216
+ sock.pipe(stream).pipe(sock);
217
+ sock.resume();
218
+ });
219
+ ws.on("error", (err) => {
220
+ this.opts.log(`expose: tunnel connect failed: ${err.message}`);
221
+ sock.destroy();
222
+ });
223
+ ws.on("unexpected-response", (_req, res) => {
224
+ this.opts.log(`expose: tunnel rejected (HTTP ${res.statusCode ?? "?"})`);
225
+ res.destroy();
226
+ sock.destroy();
227
+ });
228
+ });
229
+ server.once("error", (err) => this.opts.onListening(err));
230
+ server.listen(this.opts.listenPort, "127.0.0.1", () => this.opts.onListening(null));
231
+ this.server = server;
232
+ }
233
+ stop() {
234
+ this.server?.close();
235
+ this.server = null;
236
+ for (const sock of this.conns) sock.destroy();
237
+ this.conns.clear();
238
+ }
239
+ };
240
+
172
241
  //#endregion
173
242
  //#region ../portal-daemon/src/exec.ts
174
243
  /**
@@ -181,6 +250,7 @@ function decodeFrame(frame) {
181
250
  */
182
251
  var JobManager = class {
183
252
  jobs = /* @__PURE__ */ new Map();
253
+ reverse = /* @__PURE__ */ new Map();
184
254
  constructor(opts) {
185
255
  this.opts = opts;
186
256
  }
@@ -200,12 +270,35 @@ var JobManager = class {
200
270
  job.child.kill("SIGKILL");
201
271
  }
202
272
  this.jobs.clear();
273
+ for (const rev of this.reverse.values()) {
274
+ rev.settled = true;
275
+ rev.listener.stop();
276
+ }
277
+ this.reverse.clear();
203
278
  }
204
279
  handleCtrl(id, msg) {
205
280
  switch (msg.t) {
206
281
  case "open":
207
282
  this.startJob(id, msg.argv, msg.env, msg.conversationId ?? null);
208
283
  return;
284
+ case "reverse_listen":
285
+ this.startReverse(id, {
286
+ listenPort: msg.listenPort,
287
+ target: {
288
+ dialOrigin: msg.dialOrigin,
289
+ targetPort: msg.targetPort,
290
+ token: msg.token
291
+ }
292
+ });
293
+ return;
294
+ case "reverse_token": {
295
+ const rev = this.reverse.get(id);
296
+ if (rev) rev.target = {
297
+ ...rev.target,
298
+ token: msg.token
299
+ };
300
+ return;
301
+ }
209
302
  case "stdin_eof":
210
303
  this.jobs.get(id)?.child.stdin.end();
211
304
  return;
@@ -217,12 +310,51 @@ var JobManager = class {
217
310
  return;
218
311
  case "cancel":
219
312
  this.jobs.get(id)?.child.kill("SIGKILL");
313
+ this.stopReverse(id);
220
314
  return;
221
315
  case "close":
222
- case "error": return;
316
+ case "error":
317
+ case "reverse_listening": return;
223
318
  default: return msg;
224
319
  }
225
320
  }
321
+ startReverse(id, { listenPort, target }) {
322
+ const rev = {
323
+ settled: false,
324
+ target,
325
+ seq: 0,
326
+ listener: new ReverseListener({
327
+ listenPort,
328
+ target: () => rev.target,
329
+ log: (msg) => {
330
+ if (rev.settled) return;
331
+ this.opts.send(encodeData(id, STREAM.stderr, rev.seq, Buffer.from(msg)));
332
+ rev.seq = rev.seq + 1 >>> 0;
333
+ },
334
+ onListening: (err) => {
335
+ if (err) {
336
+ rev.settled = true;
337
+ this.reverse.delete(id);
338
+ this.opts.send(encodeCtrl(id, {
339
+ t: "error",
340
+ message: `reverse listen failed: ${err.message}`
341
+ }));
342
+ return;
343
+ }
344
+ this.opts.send(encodeCtrl(id, { t: "reverse_listening" }));
345
+ }
346
+ })
347
+ };
348
+ this.reverse.set(id, rev);
349
+ rev.listener.start();
350
+ }
351
+ stopReverse(id) {
352
+ const rev = this.reverse.get(id);
353
+ if (!rev) return;
354
+ rev.settled = true;
355
+ this.reverse.delete(id);
356
+ rev.listener.stop();
357
+ }
226
358
  setPaused(id, paused) {
227
359
  const job = this.jobs.get(id);
228
360
  if (!job) return;
@@ -296,115 +428,6 @@ var JobManager = class {
296
428
  }
297
429
  };
298
430
 
299
- //#endregion
300
- //#region ../portal-daemon/src/api.ts
301
- /**
302
- * The portal's session-authed REST surface, shared by `PortalClient` (the
303
- * TUI/`portal open` connection) and the `skydive portal` management
304
- * commands, so the endpoint contracts and response schemas live in exactly
305
- * one place.
306
- */
307
- const deviceSchema = z.object({
308
- id: z.string(),
309
- machineName: z.string(),
310
- friendlyName: z.string(),
311
- connected: z.boolean(),
312
- lastSeen: z.string().nullable(),
313
- grantedAgentIds: z.array(z.string())
314
- });
315
- const devicesResponseSchema = z.object({
316
- devices: z.array(deviceSchema),
317
- agents: z.array(z.object({
318
- id: z.string(),
319
- name: z.string()
320
- }))
321
- });
322
- const deviceTokenSchema = z.object({ token: z.string().min(1) });
323
- async function portalFetch(auth, path, init) {
324
- const res = await fetch(`${auth.appUrl}${path}`, {
325
- method: init.method,
326
- headers: {
327
- authorization: `Bearer ${auth.sessionToken}`,
328
- accept: "application/json",
329
- ...init.body ? { "content-type": "application/json" } : {}
330
- },
331
- ...init.body ? { body: init.body } : {}
332
- });
333
- if (!res.ok) {
334
- const body = await res.text().catch(() => "");
335
- throw new HttpError(res.status, body);
336
- }
337
- return res.json();
338
- }
339
- async function fetchPortalDevices(auth) {
340
- const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
341
- return devicesResponseSchema.parse(json);
342
- }
343
- const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
344
- /**
345
- * Register this machine's device row without connecting. Connecting registers
346
- * as a side effect; this covers granting an agent on a machine that has never
347
- * shared yet (the grant references the device row).
348
- */
349
- async function registerPortalDevice(auth, { machineName, friendlyName }) {
350
- const json = await portalFetch(auth, "/api/v1/portal/devices", {
351
- method: "POST",
352
- body: JSON.stringify({
353
- machineName,
354
- friendlyName
355
- })
356
- });
357
- return registerResponseSchema.parse(json).device;
358
- }
359
- /** Short-lived token the machine presents when dialing the portal WebSocket. */
360
- async function mintPortalDeviceToken(auth) {
361
- const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
362
- return deviceTokenSchema.parse(json).token;
363
- }
364
- async function grantPortalAccess(auth, { deviceId, agentId, conversationId }) {
365
- await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
366
- method: "POST",
367
- body: JSON.stringify({
368
- agentId,
369
- conversationId
370
- })
371
- });
372
- }
373
- async function revokePortalAccess(auth, { deviceId, agentId }) {
374
- await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
375
- }
376
- /**
377
- * The device row for a given machine identity. Matching is by `machineName`
378
- * equality — the stable handle the machine registers under, not the display
379
- * label.
380
- */
381
- function findThisDevice(devices, machineName) {
382
- return devices.find((device) => device.machineName === machineName) ?? null;
383
- }
384
- /**
385
- * One-time grant migration onto the merged device. Earlier CLI builds
386
- * registered a separate `<machineName>-cli` device, so a user's existing
387
- * approvals hang off that row; the merged device would start with zero grants
388
- * and every already-authorized agent would ask again. Copy any grant the
389
- * merged device is missing (the grant endpoint upserts, so re-runs are
390
- * no-ops). The legacy row is left in place — an old CLI build may still
391
- * connect under it. Returns how many grants were copied.
392
- */
393
- async function unifyLegacyCliGrants(auth, machineName) {
394
- const { devices } = await fetchPortalDevices(auth);
395
- const merged = findThisDevice(devices, machineName);
396
- const legacy = findThisDevice(devices, `${machineName}-cli`);
397
- if (!merged || !legacy) return 0;
398
- const have = new Set(merged.grantedAgentIds);
399
- const missing = legacy.grantedAgentIds.filter((id) => !have.has(id));
400
- for (const agentId of missing) await grantPortalAccess(auth, {
401
- deviceId: merged.id,
402
- agentId,
403
- conversationId: null
404
- });
405
- return missing.length;
406
- }
407
-
408
431
  //#endregion
409
432
  //#region ../portal-daemon/src/client.ts
410
433
  const INITIAL_BACKOFF_MS = 500;
@@ -627,4 +650,4 @@ function sleep(ms) {
627
650
  }
628
651
 
629
652
  //#endregion
630
- export { registerPortalDevice as a, resolveMachineIdentity as c, grantPortalAccess as i, fetchPortalDevices as n, revokePortalAccess as o, findThisDevice as r, isRecord as s, PortalClient as t };
653
+ export { isRecord as n, resolveMachineIdentity as r, PortalClient as t };
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./client-CmEF9zrz.mjs";
3
- import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-8LePusmh.mjs";
2
+ import "./client-DabRpc_T.mjs";
3
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-k2kVkJ8D.mjs";
4
+ import "./api-DG5W6iwx.mjs";
4
5
 
5
6
  export { runPortalDaemon };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-DabRpc_T.mjs";
3
+ import "./daemon-k2kVkJ8D.mjs";
4
+ import "./api-DG5W6iwx.mjs";
5
+ import { t as PortalDaemonClient } from "./daemon-client-fxf1A25Z.mjs";
6
+
7
+ export { PortalDaemonClient };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-8LePusmh.mjs";
2
+ import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-k2kVkJ8D.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { s as isRecord, t as PortalClient } from "./client-CmEF9zrz.mjs";
2
+ import { n as isRecord, t as PortalClient } from "./client-DabRpc_T.mjs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { z } from "zod";
@@ -20,7 +20,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
20
20
  * the commit time of the built tree is one monotonic clock they share.
21
21
  */
22
22
  function portalDaemonBuild() {
23
- return "1786589044";
23
+ return "1786596877";
24
24
  }
25
25
  /**
26
26
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import { t as fetchForwardTarget } from "./api-DG5W6iwx.mjs";
3
+ import net from "node:net";
4
+ import { WebSocket, createWebSocketStream } from "ws";
5
+
6
+ //#region src/chat/portal/forward.ts
7
+ const TARGET_REFRESH_SAFETY_MS = 12e4;
8
+ /**
9
+ * The reverse portal's client half: listen on the local machine's loopback and
10
+ * pipe each TCP connection to the agent sandbox's daemon (`/portal/tcp`),
11
+ * which pipes to the sandbox's own loopback. The daemon is reached through the
12
+ * agent-webserver edge Worker (sandboxes have public ingress disabled; the
13
+ * Worker owns boot-resolution and injects the sandbox edge-auth token), so a
14
+ * sandbox recycle just costs the next connection a cold-start wait rather
15
+ * than invalidating the forward.
16
+ */
17
+ async function startForward({ auth, agentId, localPort, targetPort, log }) {
18
+ let target = await fetchForwardTarget(auth, agentId);
19
+ let mintedAt = Date.now();
20
+ async function freshTarget() {
21
+ const ttlMs = target.expiresInSeconds * 1e3;
22
+ if (Date.now() - mintedAt > ttlMs - TARGET_REFRESH_SAFETY_MS) {
23
+ target = await fetchForwardTarget(auth, agentId);
24
+ mintedAt = Date.now();
25
+ }
26
+ return target;
27
+ }
28
+ const server = net.createServer((sock) => {
29
+ sock.pause();
30
+ (async () => {
31
+ let resolved;
32
+ try {
33
+ resolved = await freshTarget();
34
+ } catch (err) {
35
+ log(`forward: token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
36
+ sock.destroy();
37
+ return;
38
+ }
39
+ const ws = new WebSocket(`${resolved.daemonOrigin.replace(/^http/, "ws")}/portal/tcp?port=${targetPort}`, { headers: { authorization: `Bearer ${resolved.token}` } });
40
+ ws.on("open", () => {
41
+ const stream = createWebSocketStream(ws);
42
+ stream.on("error", () => sock.destroy());
43
+ sock.on("error", () => stream.destroy());
44
+ sock.pipe(stream).pipe(sock);
45
+ sock.resume();
46
+ });
47
+ ws.on("error", (err) => {
48
+ log(`forward: tunnel connect failed: ${err.message}`);
49
+ sock.destroy();
50
+ });
51
+ })();
52
+ });
53
+ await new Promise((resolve, reject) => {
54
+ server.once("error", reject);
55
+ server.listen(localPort, "127.0.0.1", () => {
56
+ server.removeListener("error", reject);
57
+ resolve();
58
+ });
59
+ });
60
+ const addr = server.address();
61
+ return {
62
+ port: addr && typeof addr === "object" ? addr.port : localPort,
63
+ close: () => new Promise((resolve) => server.close(() => resolve()))
64
+ };
65
+ }
66
+
67
+ //#endregion
68
+ export { startForward };
@@ -9,7 +9,7 @@ import stableStringify from "safe-stable-stringify";
9
9
 
10
10
  //#region src/config.ts
11
11
  /** Default host for the public management API (`/v1`, API-key auth). */
12
- const DEFAULT_API_URL = "https://api.skydive.com";
12
+ const DEFAULT_API_URL = typeof SKYDIVE_BUILD_API_URL === "string" ? SKYDIVE_BUILD_API_URL : "https://api.skydive.com";
13
13
  /**
14
14
  * Default origin for the interactive chat client (`skydive chat`).
15
15
  *
@@ -20,9 +20,9 @@ const DEFAULT_API_URL = "https://api.skydive.com";
20
20
  * `DEFAULT_API_URL`; override with `--api-url` / `SKYDIVE_APP_URL` for local
21
21
  * dev or while the DNS record is still being provisioned.
22
22
  */
23
- const DEFAULT_APP_URL = "https://api.skydive.com";
23
+ const DEFAULT_APP_URL = DEFAULT_API_URL;
24
24
  /** Web front door, for pages opened in the user's browser. */
25
- const DEFAULT_WEB_URL = "https://skydive.com";
25
+ const DEFAULT_WEB_URL = typeof SKYDIVE_BUILD_WEB_URL === "string" ? SKYDIVE_BUILD_WEB_URL : "https://skydive.com";
26
26
  function resolveWebUrl(appUrl) {
27
27
  if (appUrl == null) return appUrl;
28
28
  return appUrl === DEFAULT_APP_URL ? DEFAULT_WEB_URL : appUrl;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-CxygaEtp.mjs";
2
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-BpuyEfWX.mjs";
3
3
  import "./rest-I3imNduB.mjs";
4
4
  import "./billing-blocked-2wju4gC_.mjs";
5
5
 
@@ -14,7 +14,7 @@ import { n as printError } from "./output-DYzzdXYV.mjs";
14
14
  * the reply (and to --json).
15
15
  */
16
16
  async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
17
- const { PortalClient } = await import("./client-CGwes_WT.mjs");
17
+ const { PortalClient } = await import("./client-BuU34IVE.mjs");
18
18
  let signalConnected;
19
19
  const connected = new Promise((resolve) => {
20
20
  signalConnected = resolve;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { S as getConfigPath } from "./print-CxygaEtp.mjs";
2
+ import { S as getConfigPath } from "./print-BpuyEfWX.mjs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { err, ok } from "neverthrow";
@@ -8,7 +8,7 @@ import fs from "node:fs";
8
8
 
9
9
  //#region package.json
10
10
  var name = "skydive-cli";
11
- var version$1 = "0.5.0-beta.6";
11
+ var version$1 = "0.5.0-beta.8";
12
12
 
13
13
  //#endregion
14
14
  //#region src/auth/organization.ts
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as SandboxStream } from "./client-Cn2af31H.mjs";
2
+ import { t as SandboxStream } from "./client-c4c5MmgN.mjs";
3
3
 
4
4
  //#region src/chat/sandbox/raw-pty.ts
5
5
  /**
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./client-c4c5MmgN.mjs";
3
+ import { t as runRawPtyPassthrough } from "./raw-pty-DY4KelZW.mjs";
4
+
5
+ export { runRawPtyPassthrough };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.6",
3
+ "version": "0.5.0-beta.8",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as PortalClient } from "./client-CmEF9zrz.mjs";
3
-
4
- export { PortalClient };
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-CmEF9zrz.mjs";
3
- import "./daemon-8LePusmh.mjs";
4
- import { t as PortalDaemonClient } from "./daemon-client-DmwnQi8B.mjs";
5
-
6
- export { PortalDaemonClient };
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-Cn2af31H.mjs";
3
- import { t as runRawPtyPassthrough } from "./raw-pty-B6mAroiI.mjs";
4
-
5
- export { runRawPtyPassthrough };