skydive-cli 0.2.0-beta.615 → 0.2.0-beta.618

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/dist/js/bin.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { C as setActiveWorkspace, S as listWorkspaces, T as version, _ as themes, b as getActiveWorkspaceId, o as brandHelpArt, t as installCrashHandler, w as name, x as getSessionIdentity, y as ensureActiveOrganization } from "./install-B5sKLfRc.mjs";
2
+ import { C as setActiveWorkspace, S as listWorkspaces, T as version, _ as themes, b as getActiveWorkspaceId, o as brandHelpArt, t as installCrashHandler, w as name, x as getSessionIdentity, y as ensureActiveOrganization } from "./install-BhPuzBxW.mjs";
3
3
  import { A as getUpdateCheckDisabled, C as getLastSeenVersion, D as getShareMachineDefault, I as saveConfig, L as saveSession, M as resolveConfig, N as resolveManagementAuth, O as getStoredApiKeyId, P as resolveSession, S as getConfigPath, _ as API_KEY_PREFIX, b as DEFAULT_WEB_URL, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as resolveAppUrl, k as getStoredApiKeyWorkspaceName, v as DEFAULT_API_URL, w as getPromptHistoryPath, x as deleteConfig, z as setLastSeenVersion } from "./print-CNIvmpqW.mjs";
4
4
  import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
5
5
  import { n as createRestClient } from "./rest-Q5yl_b2-.mjs";
6
- import { a as registerPortalDevice, c as machineIdentity, n as findThisDevice, o as revokePortalAccess, r as grantPortalAccess, t as fetchPortalDevices } from "./api-CLTp-mKM.mjs";
7
- import { t as SandboxStream } from "./client-BVOAwU8M.mjs";
6
+ import { a as registerPortalDevice, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice, s as machineIdentity } from "./client-DT9bWi1D.mjs";
7
+ import { c as PORTAL_DAEMON_FLAG, i as queryDaemonStatus, l as daemonPaths, n as ensureDaemonRunning, o as stopDaemon } from "./daemon-DfMgVlYE.mjs";
8
+ import { t as SandboxStream } from "./client-DfcJFEbh.mjs";
8
9
  import { hideBin } from "yargs/helpers";
9
10
  import yargs from "yargs";
10
11
  import { hostname } from "node:os";
@@ -1195,7 +1196,7 @@ const importCommand = {
1195
1196
  process.exit(1);
1196
1197
  }
1197
1198
  }
1198
- const { runChat } = await import("./boot-BJZrdTk0.mjs");
1199
+ const { runChat } = await import("./boot-CaWHIM4R.mjs");
1199
1200
  await runChat({
1200
1201
  appUrl,
1201
1202
  sessionToken: session.value.sessionToken,
@@ -1222,7 +1223,7 @@ async function runImportPrintMode({ argv, appUrl }) {
1222
1223
  printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
1223
1224
  process.exit(1);
1224
1225
  }
1225
- const { connectMachineShare } = await import("./print-share-rM8kNeEw.mjs");
1226
+ const { connectMachineShare } = await import("./print-share-D8Nn1qFq.mjs");
1226
1227
  const machineShare = await connectMachineShare({
1227
1228
  appUrl,
1228
1229
  sessionToken: session.value.sessionToken,
@@ -1511,7 +1512,7 @@ const chatCommand = {
1511
1512
  printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
1512
1513
  process.exit(1);
1513
1514
  }
1514
- const { runChat } = await import("./boot-BJZrdTk0.mjs");
1515
+ const { runChat } = await import("./boot-CaWHIM4R.mjs");
1515
1516
  await runChat({
1516
1517
  appUrl,
1517
1518
  sessionToken: session.value.sessionToken,
@@ -1563,7 +1564,7 @@ async function runPrintMode({ argv, appUrl }) {
1563
1564
  }
1564
1565
  let machineShare = null;
1565
1566
  if (resolveShareMachine(argv)) {
1566
- const { connectMachineShare } = await import("./print-share-rM8kNeEw.mjs");
1567
+ const { connectMachineShare } = await import("./print-share-D8Nn1qFq.mjs");
1567
1568
  machineShare = await connectMachineShare({
1568
1569
  appUrl,
1569
1570
  sessionToken: session.value.sessionToken,
@@ -1819,7 +1820,7 @@ const switchCommand = {
1819
1820
  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.");
1820
1821
  process.exit(1);
1821
1822
  }
1822
- const { runWorkspacePicker } = await import("./boot-BJZrdTk0.mjs");
1823
+ const { runWorkspacePicker } = await import("./boot-CaWHIM4R.mjs");
1823
1824
  await runWorkspacePicker(session);
1824
1825
  return;
1825
1826
  }
@@ -1897,7 +1898,7 @@ const openCommand = {
1897
1898
  const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
1898
1899
  const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
1899
1900
  const { machineName } = machineIdentity();
1900
- const { PortalClient } = await import("./client-D6UUwESi.mjs");
1901
+ const { PortalClient } = await import("./client-CKlIpyAb.mjs");
1901
1902
  let lastLine = "";
1902
1903
  let signalConnected;
1903
1904
  const connected = new Promise((resolve) => {
@@ -1906,7 +1907,7 @@ const openCommand = {
1906
1907
  const client = new PortalClient({
1907
1908
  appUrl: session.appUrl,
1908
1909
  sessionToken: session.sessionToken,
1909
- cwd,
1910
+ resolveCwd: () => cwd,
1910
1911
  onState: (state) => {
1911
1912
  if (state.status === "connected") signalConnected();
1912
1913
  const line = state.status === "error" ? `portal: connection error: ${state.error ?? "unknown"} — retrying` : `portal: ${state.status}`;
@@ -2021,10 +2022,111 @@ function buildDeviceTable(devices, agents, thisMachineName) {
2021
2022
  ])
2022
2023
  };
2023
2024
  }
2025
+ /**
2026
+ * `skydive portal daemon <cmd>` — operator control of the per-host portal daemon
2027
+ * (the process that owns the single portal connection and routes exec per
2028
+ * conversation). Distinct from the agent-facing `platform portal`: this manages
2029
+ * the local daemon a human runs, so it lives on the skydive CLI only.
2030
+ */
2031
+ const daemonStatusCommand = {
2032
+ command: "status",
2033
+ describe: "Show the portal daemon: running state, connection, and routing map",
2034
+ handler: async (argv) => {
2035
+ const status = await queryDaemonStatus(requireSession(argv).appUrl);
2036
+ if (argv.json) {
2037
+ output(argv, status ?? { running: false });
2038
+ return;
2039
+ }
2040
+ if (!status) {
2041
+ console.log("portal daemon: not running");
2042
+ return;
2043
+ }
2044
+ const cwdCount = Object.keys(status.cwds).length;
2045
+ console.log(`portal daemon: running (pid ${status.pid})`);
2046
+ console.log(` app: ${status.appUrl}`);
2047
+ console.log(` connection: ${status.portal.status}${status.portal.error ? ` (${status.portal.error})` : ""}`);
2048
+ if (status.portal.machineName) console.log(` machine: ${status.portal.friendlyName}`);
2049
+ console.log(` attached: ${status.clientCount} CLI session(s)`);
2050
+ console.log(` granted: ${status.portal.grantedAgentIds.length} agent(s)`);
2051
+ console.log(` routing: ${cwdCount} conversation(s)`);
2052
+ }
2053
+ };
2054
+ const daemonStopCommand = {
2055
+ command: "stop",
2056
+ describe: "Stop the portal daemon, cutting every attached session and live exec/tunnel",
2057
+ handler: async (argv) => {
2058
+ const result = await stopDaemon(requireSession(argv).appUrl);
2059
+ if (argv.json) {
2060
+ output(argv, { result });
2061
+ return;
2062
+ }
2063
+ console.log(result === "not-running" ? "portal daemon: not running" : result === "failed" ? "portal daemon: could not be stopped" : `portal daemon: stopped${result === "killed" ? " (force-killed)" : ""}`);
2064
+ }
2065
+ };
2066
+ const daemonStartCommand = {
2067
+ command: "start",
2068
+ describe: "Start the portal daemon if it is not already running",
2069
+ handler: async (argv) => {
2070
+ const session = requireSession(argv);
2071
+ await ensureDaemonRunning(session.appUrl);
2072
+ const status = await queryDaemonStatus(session.appUrl);
2073
+ if (argv.json) {
2074
+ output(argv, {
2075
+ running: !!status,
2076
+ pid: status?.pid ?? null
2077
+ });
2078
+ return;
2079
+ }
2080
+ console.log(status ? `portal daemon: running (pid ${status.pid})` : "portal daemon: failed to start");
2081
+ }
2082
+ };
2083
+ const daemonRestartCommand = {
2084
+ command: "restart",
2085
+ describe: "Stop and restart the portal daemon (e.g. to pick up a new build)",
2086
+ handler: async (argv) => {
2087
+ const session = requireSession(argv);
2088
+ await stopDaemon(session.appUrl);
2089
+ await ensureDaemonRunning(session.appUrl);
2090
+ const status = await queryDaemonStatus(session.appUrl);
2091
+ if (argv.json) {
2092
+ output(argv, {
2093
+ running: !!status,
2094
+ pid: status?.pid ?? null
2095
+ });
2096
+ return;
2097
+ }
2098
+ console.log(status ? `portal daemon: restarted (pid ${status.pid})` : "portal daemon: failed to restart");
2099
+ }
2100
+ };
2101
+ const daemonLogsCommand = {
2102
+ command: "logs",
2103
+ describe: "Print the portal daemon's log",
2104
+ builder: (y) => y.option("follow", {
2105
+ alias: "f",
2106
+ type: "boolean",
2107
+ default: false,
2108
+ describe: "Follow the log (like tail -f)"
2109
+ }),
2110
+ handler: async (argv) => {
2111
+ const { logPath } = daemonPaths(requireSession(argv).appUrl);
2112
+ const child = spawn("tail", argv.follow === true ? ["-f", logPath] : [
2113
+ "-n",
2114
+ "200",
2115
+ logPath
2116
+ ], { stdio: "inherit" });
2117
+ await new Promise((resolve) => child.on("close", () => resolve()));
2118
+ }
2119
+ };
2120
+ const daemonCommand = {
2121
+ command: "daemon",
2122
+ describe: "Control the local portal daemon (status, stop, start, restart, logs)",
2123
+ builder: (y) => y.command(daemonStatusCommand).command(daemonStopCommand).command(daemonStartCommand).command(daemonRestartCommand).command(daemonLogsCommand).demandCommand(1, "Specify a subcommand: status, stop, start, restart, logs"),
2124
+ handler: () => {}
2125
+ };
2024
2126
  const portalCommand = {
2025
2127
  command: "portal",
2026
2128
  describe: "Open this machine's portal to agents and manage their access",
2027
- builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status"),
2129
+ builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, daemon"),
2028
2130
  handler: () => {}
2029
2131
  };
2030
2132
 
@@ -2139,7 +2241,7 @@ async function runPty({ session, agentId, agentName }) {
2139
2241
  return 1;
2140
2242
  }
2141
2243
  console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
2142
- const { runRawPtyPassthrough } = await import("./raw-pty-pspO57gT.mjs");
2244
+ const { runRawPtyPassthrough } = await import("./raw-pty-ChUHav4d.mjs");
2143
2245
  const result = await runRawPtyPassthrough({
2144
2246
  stdin: process.stdin,
2145
2247
  stdout: process.stdout,
@@ -2616,12 +2718,18 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
2616
2718
  await runUpdateCheckWorker();
2617
2719
  process.exit(0);
2618
2720
  }
2619
- maybePrintWhatsNew();
2620
- setupUpdateCheck(hideBin(process.argv));
2621
- const resolved = resolveArgv(hideBin(process.argv));
2622
- const raw = hideBin(process.argv);
2623
- if ((raw.includes("--help") || raw.includes("-h")) && !raw.some((a) => !a.startsWith("-")) && !raw.includes("--json") && !raw.includes("--quiet")) process.stdout.write(brandHelpArt());
2624
- createCli(resolved).parse();
2721
+ if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
2722
+ const { runPortalDaemon } = await import("./daemon-CqhMYzpN.mjs");
2723
+ runPortalDaemon(process.argv);
2724
+ } else runCli();
2725
+ function runCli() {
2726
+ maybePrintWhatsNew();
2727
+ setupUpdateCheck(hideBin(process.argv));
2728
+ const resolved = resolveArgv(hideBin(process.argv));
2729
+ const raw = hideBin(process.argv);
2730
+ if ((raw.includes("--help") || raw.includes("-h")) && !raw.some((a) => !a.startsWith("-")) && !raw.includes("--json") && !raw.includes("--quiet")) process.stdout.write(brandHelpArt());
2731
+ createCli(resolved).parse();
2732
+ }
2625
2733
 
2626
2734
  //#endregion
2627
2735
  export { };
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-B5sKLfRc.mjs";
2
+ import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-BhPuzBxW.mjs";
3
3
  import { E as getSavedTheme, F as resolveWebUrl, R as saveTheme, S as getConfigPath, T as getReviewStateDir, 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-CNIvmpqW.mjs";
4
4
  import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, r as errorDetail, t as HttpError } from "./rest-Q5yl_b2-.mjs";
5
- import "./api-CLTp-mKM.mjs";
6
- import { t as SandboxStream } from "./client-BVOAwU8M.mjs";
7
- import { t as PortalClient } from "./client-BigvBsPk.mjs";
8
- import { t as runRawPtyPassthrough } from "./raw-pty-Dbi2kb9v.mjs";
5
+ import { t as PortalClient } from "./client-DT9bWi1D.mjs";
6
+ import { d as makeLineParser, f as parseDaemonMessage, l as daemonPaths, n as ensureDaemonRunning, s as LOCAL_PROTOCOL_VERSION, u as encodeLine } from "./daemon-DfMgVlYE.mjs";
7
+ import { t as SandboxStream } from "./client-DfcJFEbh.mjs";
8
+ import { t as runRawPtyPassthrough } from "./raw-pty-BcjbTjHJ.mjs";
9
9
  import * as os$1 from "node:os";
10
10
  import { homedir, platform, release, tmpdir } from "node:os";
11
11
  import path, { basename, extname, isAbsolute, join, win32 } from "node:path";
12
12
  import { z } from "zod";
13
13
  import open from "open";
14
14
  import { execFile, spawn } from "node:child_process";
15
- import { createHash } from "node:crypto";
15
+ import { createHash, randomUUID } from "node:crypto";
16
16
  import { constants } from "node:fs";
17
+ import { connect, createConnection } from "node:net";
18
+ import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
17
19
  import { MarkdownRenderable, RenderableEvents, StyledText, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks, fg, link } from "@opentui/core";
18
20
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
19
21
  import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
20
22
  import { create } from "zustand";
21
- import { connect, createConnection } from "node:net";
22
- import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
23
23
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
24
24
  import fuzzysort from "fuzzysort";
25
25
  import { fileURLToPath } from "node:url";
@@ -228,23 +228,222 @@ async function resolveInitialScreen({ rest, agentSelector, conversationId, newCo
228
228
  };
229
229
  }
230
230
 
231
+ //#endregion
232
+ //#region src/chat/portal/daemon-client.ts
233
+ var PortalDaemonClient = class {
234
+ sessionId = randomUUID();
235
+ socket = null;
236
+ disposed = false;
237
+ reconnectTimer = null;
238
+ lastBind = null;
239
+ wantEnabled = false;
240
+ pendingGrants = /* @__PURE__ */ new Set();
241
+ grantedAgentIds = /* @__PURE__ */ new Set();
242
+ constructor(opts) {
243
+ this.opts = opts;
244
+ }
245
+ /** Spawn the daemon if needed and attach. Idempotent. */
246
+ async start() {
247
+ if (this.disposed) return;
248
+ await ensureDaemonRunning(this.opts.appUrl);
249
+ this.connect();
250
+ }
251
+ connect() {
252
+ if (this.disposed) return;
253
+ const { socketPath } = daemonPaths(this.opts.appUrl);
254
+ const socket = connect(socketPath);
255
+ socket.setEncoding("utf8");
256
+ this.socket = socket;
257
+ const parse = makeLineParser();
258
+ socket.on("connect", () => {
259
+ this.send({
260
+ t: "hello",
261
+ v: LOCAL_PROTOCOL_VERSION,
262
+ sessionId: this.sessionId,
263
+ token: this.opts.sessionToken
264
+ });
265
+ if (this.wantEnabled) this.send({ t: "enable" });
266
+ if (this.lastBind) this.send({
267
+ t: "bind",
268
+ sessionId: this.sessionId,
269
+ conversationId: this.lastBind.conversationId,
270
+ cwd: this.lastBind.cwd
271
+ });
272
+ for (const agentId of this.pendingGrants) this.send({
273
+ t: "grant",
274
+ agentId
275
+ });
276
+ });
277
+ socket.on("data", (chunk) => {
278
+ for (const line of parse(chunk)) {
279
+ const msg = parseDaemonMessage(line);
280
+ if (msg) this.onMessage(msg);
281
+ }
282
+ });
283
+ const onClose = () => {
284
+ if (this.socket === socket) this.socket = null;
285
+ this.scheduleReconnect();
286
+ };
287
+ socket.on("close", onClose);
288
+ socket.on("error", onClose);
289
+ }
290
+ scheduleReconnect() {
291
+ if (this.disposed || this.reconnectTimer) return;
292
+ this.reconnectTimer = setTimeout(() => {
293
+ this.reconnectTimer = null;
294
+ this.start();
295
+ }, 500);
296
+ this.reconnectTimer.unref();
297
+ }
298
+ onMessage(msg) {
299
+ if (msg.t === "state") {
300
+ this.grantedAgentIds = new Set(msg.grantedAgentIds);
301
+ this.opts.onState({
302
+ status: msg.status,
303
+ machineName: msg.machineName,
304
+ friendlyName: msg.friendlyName,
305
+ error: msg.error,
306
+ grantedAgentIds: msg.grantedAgentIds
307
+ });
308
+ }
309
+ }
310
+ /** Whether this agent is currently authorized on the machine (last daemon state). */
311
+ isGranted(agentId) {
312
+ return this.grantedAgentIds.has(agentId);
313
+ }
314
+ /** Whether the user has turned machine sharing on from this CLI. */
315
+ isEnabled() {
316
+ return this.wantEnabled;
317
+ }
318
+ send(msg) {
319
+ if (this.socket?.writable) this.socket.write(encodeLine(msg));
320
+ }
321
+ /** Turn machine sharing on (the daemon connects the portal). */
322
+ enable() {
323
+ this.wantEnabled = true;
324
+ this.send({ t: "enable" });
325
+ }
326
+ /** Turn machine sharing off. */
327
+ disable() {
328
+ this.wantEnabled = false;
329
+ this.send({ t: "disable" });
330
+ }
331
+ /** Register / update this conversation's working directory for exec routing. */
332
+ bind(conversationId, cwd) {
333
+ this.lastBind = {
334
+ conversationId,
335
+ cwd
336
+ };
337
+ this.send({
338
+ t: "bind",
339
+ sessionId: this.sessionId,
340
+ conversationId,
341
+ cwd
342
+ });
343
+ }
344
+ /**
345
+ * Authorize one agent to run commands on this machine. Resolves once the
346
+ * request is sent to the daemon (the daemon performs the grant and pushes the
347
+ * updated state); kept async so it's a drop-in for the old in-process client's
348
+ * awaited `grantAgent`.
349
+ */
350
+ grantAgent(agentId) {
351
+ this.pendingGrants.add(agentId);
352
+ this.send({
353
+ t: "grant",
354
+ agentId
355
+ });
356
+ return Promise.resolve();
357
+ }
358
+ /** Detach this CLI. The daemon keeps running for other clients. */
359
+ dispose() {
360
+ this.disposed = true;
361
+ if (this.reconnectTimer) {
362
+ clearTimeout(this.reconnectTimer);
363
+ this.reconnectTimer = null;
364
+ }
365
+ this.send({
366
+ t: "bye",
367
+ sessionId: this.sessionId
368
+ });
369
+ try {
370
+ this.socket?.destroy();
371
+ } catch (_error) {}
372
+ this.socket = null;
373
+ }
374
+ };
375
+
376
+ //#endregion
377
+ //#region src/chat/portal/create-portal-client.ts
378
+ /**
379
+ * Escape hatch: setting `SKYDIVE_NO_DAEMON` (to any non-empty value)
380
+ * makes the CLI bypass the portal daemon and open the portal connection
381
+ * in-process, the pre-daemon behavior. This is a LOCAL env switch, not a remote
382
+ * flag: it takes effect on the next CLI start and lets a user (or us) fall back
383
+ * instantly if the daemon misbehaves in the wild, without shipping a new build.
384
+ * The daemon is the default because it fixes the multi-process presence flap.
385
+ */
386
+ function portalDaemonDisabled() {
387
+ return !!process.env.SKYDIVE_NO_DAEMON;
388
+ }
389
+ /**
390
+ * Build the portal client the app should use: the daemon client by default, or
391
+ * the in-process `PortalClient` when the kill switch is set. The in-process
392
+ * client always runs exec in the launch cwd (it has no conversation map), which
393
+ * is the exact pre-daemon behavior.
394
+ */
395
+ function createPortalClient(opts) {
396
+ if (portalDaemonDisabled()) {
397
+ const client = new PortalClient({
398
+ appUrl: opts.appUrl,
399
+ sessionToken: opts.sessionToken,
400
+ resolveCwd: () => process.cwd(),
401
+ onState: (state) => opts.onState({
402
+ status: state.status,
403
+ machineName: state.machineName,
404
+ friendlyName: state.friendlyName,
405
+ error: state.error,
406
+ grantedAgentIds: state.grantedAgentIds
407
+ })
408
+ });
409
+ return {
410
+ start: () => Promise.resolve(),
411
+ enable: () => client.enable(),
412
+ disable: () => client.disable(),
413
+ grantAgent: (agentId) => client.grantAgent(agentId),
414
+ isGranted: (agentId) => client.isGranted(agentId),
415
+ bind: () => {},
416
+ dispose: () => client.dispose()
417
+ };
418
+ }
419
+ return new PortalDaemonClient(opts);
420
+ }
421
+
231
422
  //#endregion
232
423
  //#region src/chat/portal/use-portal.ts
233
424
  /**
234
- * Owns the `PortalClient` lifecycle for the app session and keeps the store in
235
- * sync with its status. Constructed once from the resolved session; disabling
236
- * happens on unmount so quitting the TUI drops presence and kills any children.
425
+ * Owns the portal DAEMON CLIENT lifecycle for the app session and keeps the
426
+ * store in sync with the daemon's status.
427
+ *
428
+ * The CLI no longer opens the portal connection itself: it attaches to a
429
+ * per-host daemon (spawned on demand) that owns the single portal WebSocket, so
430
+ * multiple `skydive` processes on one machine share one portal identity instead
431
+ * of fighting over it (the flap). This hook just wires the daemon client into
432
+ * the store; the chat screen registers each conversation's working directory via
433
+ * `portalClient.bind(...)` so the daemon runs that conversation's exec in the
434
+ * right place.
435
+ *
237
436
  * `shareMachine` opts in from launch (the `--share-machine` flag); otherwise the
238
- * user turns sharing on from the chat screen.
437
+ * user turns sharing on from the chat screen. Detaching on unmount leaves the
438
+ * daemon running for any other attached CLIs.
239
439
  */
240
440
  function usePortal({ appUrl, sessionToken, shareMachine }) {
241
441
  const syncPortal = useStore((s) => s.syncPortal);
242
442
  const setPortalClient = useStore((s) => s.setPortalClient);
243
443
  useEffect(() => {
244
- const client = new PortalClient({
444
+ const client = createPortalClient({
245
445
  appUrl,
246
446
  sessionToken,
247
- cwd: process.cwd(),
248
447
  onState: (state) => syncPortal({
249
448
  status: state.status,
250
449
  machineName: state.machineName,
@@ -253,7 +452,9 @@ function usePortal({ appUrl, sessionToken, shareMachine }) {
253
452
  })
254
453
  });
255
454
  setPortalClient(client);
256
- if (shareMachine) client.enable();
455
+ client.start().then(() => {
456
+ if (shareMachine) client.enable();
457
+ });
257
458
  return () => {
258
459
  client.dispose();
259
460
  setPortalClient(null);
@@ -9191,6 +9392,16 @@ function ChatScreen({ agent, conversation }) {
9191
9392
  agentHost.setSession(conversationId);
9192
9393
  setResumeConversation(conversationId);
9193
9394
  }, [agentHost, conversationId]);
9395
+ const bindPortalCwd = useCallback(() => {
9396
+ if (!conversationId || !portalClient) return;
9397
+ if (portal.status === "off") return;
9398
+ const cwd = shellSessionRef.current?.cwd ?? process.cwd();
9399
+ portalClient.bind(conversationId, cwd);
9400
+ }, [
9401
+ conversationId,
9402
+ portalClient,
9403
+ portal.status
9404
+ ]);
9194
9405
  const scrollRef = useRef(null);
9195
9406
  const composerRef = useRef(null);
9196
9407
  const jumpToBottom = useCallback(() => {
@@ -9761,6 +9972,7 @@ function ChatScreen({ agent, conversation }) {
9761
9972
  ...m,
9762
9973
  exit: exitCode
9763
9974
  } : m));
9975
+ bindPortalCwd();
9764
9976
  } catch (err) {
9765
9977
  appendOutput(`\n${errorMessage(err)}`);
9766
9978
  exitCode = 1;
@@ -9770,7 +9982,7 @@ function ChatScreen({ agent, conversation }) {
9770
9982
  } : m));
9771
9983
  }
9772
9984
  sendContent(formatShellContext(command, captured, exitCode), [], { echo: false });
9773
- }, [sendContent]);
9985
+ }, [sendContent, bindPortalCwd]);
9774
9986
  const runSandboxExec = useCallback((command) => {
9775
9987
  if (!appUrl || !sessionToken) return;
9776
9988
  const id = crypto.randomUUID();
@@ -10099,6 +10311,7 @@ function ChatScreen({ agent, conversation }) {
10099
10311
  if (portal.status === "off") {
10100
10312
  declinedRef.current.delete(agent.id);
10101
10313
  portalClient.enable();
10314
+ if (conversationId) portalClient.bind(conversationId, shellSessionRef.current?.cwd ?? process.cwd());
10102
10315
  } else {
10103
10316
  portalClient.disable();
10104
10317
  setPortalPrompt(null);
@@ -10107,7 +10320,8 @@ function ChatScreen({ agent, conversation }) {
10107
10320
  portalClient,
10108
10321
  portal.status,
10109
10322
  agent.id,
10110
- setPortalPrompt
10323
+ setPortalPrompt,
10324
+ conversationId
10111
10325
  ]);
10112
10326
  const confirmGrant = useCallback(() => {
10113
10327
  if (!portalClient) return;
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-Q5yl_b2-.mjs";
3
+ import { t as PortalClient } from "./client-DT9bWi1D.mjs";
4
+
5
+ export { PortalClient };