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 +126 -18
- package/dist/js/{boot-BJZrdTk0.mjs → boot-CaWHIM4R.mjs} +231 -17
- package/dist/js/client-CKlIpyAb.mjs +5 -0
- package/dist/js/{client-BigvBsPk.mjs → client-DT9bWi1D.mjs} +148 -8
- package/dist/js/daemon-CqhMYzpN.mjs +6 -0
- package/dist/js/daemon-DfMgVlYE.mjs +607 -0
- package/dist/js/{install-B5sKLfRc.mjs → install-BhPuzBxW.mjs} +1 -1
- package/dist/js/{print-share-rM8kNeEw.mjs → print-share-D8Nn1qFq.mjs} +2 -2
- package/dist/js/{raw-pty-Dbi2kb9v.mjs → raw-pty-BcjbTjHJ.mjs} +1 -1
- package/dist/js/raw-pty-ChUHav4d.mjs +5 -0
- package/package.json +1 -1
- package/dist/js/api-CLTp-mKM.mjs +0 -145
- package/dist/js/client-D6UUwESi.mjs +0 -6
- package/dist/js/raw-pty-pspO57gT.mjs +0 -5
- /package/dist/js/{client-BVOAwU8M.mjs → client-DfcJFEbh.mjs} +0 -0
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-
|
|
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,
|
|
7
|
-
import {
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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-
|
|
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 "./
|
|
6
|
-
import {
|
|
7
|
-
import { t as
|
|
8
|
-
import { t as runRawPtyPassthrough } from "./raw-pty-
|
|
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
|
|
235
|
-
* sync with
|
|
236
|
-
*
|
|
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 =
|
|
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
|
-
|
|
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;
|