openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
384 lines (383 loc) • 14.4 kB
JavaScript
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.js";
import { s as readConfigFileSnapshot } from "./io.runtime-B9iJRs3w.js";
import "./config-Cs0XXL3x.js";
import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js";
import { n as isWSL2Sync } from "./wsl-DjKrZ_YH.js";
import { r as promptYesNo } from "./prompt-BvBir9Yc.js";
import { n as openUrl, t as detectBrowserOpenSupport } from "./browser-open-m1tKJTvZ.js";
import { i as formatControlUiSshHint } from "./onboard-helpers-BDY-6Z-t.js";
import { t as isRemoteEnvironment } from "./remote-env-Ci47uLxB.js";
import { l as gatewayProbeResultSawGateway } from "./gateway-health-auth-diagnostic-6ZBqs-mo.js";
import { i as waitForControlUiDocument, n as issueControlUiBrowserHandoff, r as resolveControlUiHandoffTarget, t as hasVerifiedControlUiLoopbackAlias } from "./control-ui-handoff-CipgXwfB.js";
import { Buffer } from "node:buffer";
//#region src/infra/clipboard.ts
const WSL_CLIPBOARD_ARGV = [
"/bin/sh",
"-c",
"exec /mnt/c/Windows/System32/clip.exe"
];
const POWERSHELL_CLIPBOARD_ARGV = [
"powershell",
"-NoProfile",
"-NonInteractive",
"-Command",
"$encoded = [Console]::In.ReadToEnd(); $bytes = [Convert]::FromBase64String($encoded); Set-Clipboard -Value ([Text.Encoding]::UTF8.GetString($bytes))"
];
async function copyToClipboard(value) {
const attempts = [
...isWSL2Sync() ? [{
argv: WSL_CLIPBOARD_ARGV,
input: value
}] : [],
{
argv: ["pbcopy"],
input: value
},
{
argv: [
"xclip",
"-selection",
"clipboard"
],
input: value
},
{
argv: ["wl-copy"],
input: value
},
{
argv: ["clip.exe"],
input: value
},
{
argv: POWERSHELL_CLIPBOARD_ARGV,
input: Buffer.from(value, "utf8").toString("base64")
}
];
for (const attempt of attempts) try {
const result = await runCommandWithTimeout(attempt.argv, {
timeoutMs: 3e3,
input: attempt.input
});
if (result.code === 0 && !result.killed) return true;
} catch {}
return false;
}
//#endregion
//#region src/commands/gateway-readiness.ts
const daemonStatusModuleLoader = createLazyImportLoader(() => import("./status.gather-Y5dRhLh6.js"));
const daemonInstallModuleLoader = createLazyImportLoader(() => import("./install.runtime-EE8HGFUx.js"));
const daemonLifecycleModuleLoader = createLazyImportLoader(() => import("./lifecycle-_o0MTt1R.js"));
async function defaultGatherStatus(params) {
const { gatherDaemonStatus } = await daemonStatusModuleLoader.load();
return gatherDaemonStatus({
rpc: params.probeUrl ? { url: params.probeUrl } : {},
probe: true,
requireRpc: params.requireRpc,
deep: false
});
}
function activeProbePortStatus(status) {
const probeUrl = status.rpc?.url ?? status.gateway?.probeUrl;
const probePort = probeUrl ? (() => {
try {
return Number(new URL(probeUrl).port);
} catch {
return NaN;
}
})() : NaN;
if (Number.isFinite(probePort) && status.portCli?.port === probePort) return status.portCli;
return status.port;
}
function gatewayIsReady(status, readyWhenReachable) {
return status.rpc?.ok === true || readyWhenReachable === true && activeProbePortStatus(status)?.status === "busy" && Boolean(status.rpc && gatewayProbeResultSawGateway(status.rpc));
}
function gatewayLooksStopped(status) {
if (status.rpc && gatewayProbeResultSawGateway(status.rpc)) return false;
const port = activeProbePortStatus(status);
if (port?.status === "busy") return false;
if (port?.status === "free") return true;
if (status.service.runtime?.status === "stopped") return true;
const error = status.rpc?.error ?? "";
return /\bECONNREFUSED\b|couldn't connect|connection refused/i.test(error);
}
function gatewayServiceIsInstalled(status) {
return Boolean(status.service.command || status.service.loadState.status === "loaded");
}
function nativeServiceTargetsGateway(status) {
return status.service.targetRole !== "diagnostic-only";
}
function readinessFailureReason(status) {
if (gatewayLooksStopped(status)) return "Gateway is not running.";
return status.rpc?.error ? `Gateway probe failed: ${status.rpc.error}` : "Gateway is not healthy.";
}
function printGatewayNotReadyHints(runtime, reason, canStartService = true) {
runtime.log(reason);
runtime.log("Run `openclaw gateway status --deep` for details.");
if (!canStartService) {
runtime.log("Use the owning environment or supervisor to start or repair the selected Gateway.");
return;
}
runtime.log("Run `openclaw gateway start` to start a managed gateway.");
runtime.log("Run `openclaw gateway run` for a foreground gateway.");
}
async function confirmRecovery(params) {
if (params.yes) return true;
if (!(params.interactive ?? process.stdin.isTTY)) return false;
return params.confirm(params.message, true);
}
async function waitForGatewayReady(params) {
const attempts = params.attempts ?? 20;
const delayMs = params.delayMs ?? 500;
let latest = await params.gatherStatus();
for (let attempt = 1; attempt < attempts && !gatewayIsReady(latest, params.readyWhenReachable); attempt += 1) {
await new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
latest = await params.gatherStatus();
}
return latest;
}
/** Checks readiness and, when approved, recovers by installing or starting the gateway. */
async function ensureGatewayReadyForOperation(options) {
const requireRpc = options.requireRpc ?? false;
const gatherStatus = options.deps?.gatherStatus ?? (() => defaultGatherStatus({
requireRpc,
probeUrl: options.probeUrl
}));
const confirm = options.deps?.confirm ?? promptYesNo;
const installGateway = options.deps?.installGateway ?? (async () => {
const { runDaemonInstall } = await daemonInstallModuleLoader.load();
await runDaemonInstall({ json: false });
});
const startGateway = options.deps?.startGateway ?? (async () => {
const { runDaemonStart } = await daemonLifecycleModuleLoader.load();
await runDaemonStart({ json: false });
});
const initialStatus = await gatherStatus();
if (gatewayIsReady(initialStatus, options.readyWhenReachable)) return {
ready: true,
status: initialStatus,
recovered: false
};
const reason = readinessFailureReason(initialStatus);
const nativeServiceCanRecover = nativeServiceTargetsGateway(initialStatus);
if (!gatewayLooksStopped(initialStatus) || !nativeServiceCanRecover) {
printGatewayNotReadyHints(options.runtime, reason, false);
return {
ready: false,
status: initialStatus,
reason,
recoverable: false
};
}
const shouldInstall = !gatewayServiceIsInstalled(initialStatus);
if (shouldInstall && options.allowInstall === false) {
printGatewayNotReadyHints(options.runtime, reason);
return {
ready: false,
status: initialStatus,
reason,
recoverable: false
};
}
if (!await confirmRecovery({
message: shouldInstall ? `No background Gateway service was detected for this profile. Install and start one to ${options.operation}?` : `The background Gateway service is not running. Start it to ${options.operation}?`,
yes: options.yes,
interactive: options.interactive,
confirm
})) {
printGatewayNotReadyHints(options.runtime, reason);
return {
ready: false,
status: initialStatus,
reason,
recoverable: true
};
}
if (shouldInstall) await installGateway();
else await startGateway();
const recoveredStatus = await waitForGatewayReady({
gatherStatus,
readyWhenReachable: options.readyWhenReachable
});
if (gatewayIsReady(recoveredStatus, options.readyWhenReachable)) return {
ready: true,
status: recoveredStatus,
recovered: true
};
const recoveredReason = readinessFailureReason(recoveredStatus);
const recoverable = gatewayLooksStopped(recoveredStatus) && nativeServiceTargetsGateway(recoveredStatus);
printGatewayNotReadyHints(options.runtime, recoveredReason, recoverable);
return {
ready: false,
status: recoveredStatus,
reason: recoveredReason,
recoverable
};
}
//#endregion
//#region src/commands/dashboard.ts
const quietRuntime = {
log: () => {},
error: () => {},
exit: () => {}
};
const gatewayPasswordJsonKey = ["gateway", "Password"].join("");
async function resolveDashboardTarget() {
const snapshot = await readConfigFileSnapshot();
if (snapshot.exists && !snapshot.valid) throw new Error(`OpenClaw config is invalid: ${snapshot.path}. Run \`openclaw doctor --fix\` or \`openclaw config validate\`.`);
return await resolveControlUiHandoffTarget({
config: snapshot.valid ? snapshot.sourceConfig ?? snapshot.config : {},
env: process.env
});
}
async function ensureDashboardTargetReady(params) {
return ensureGatewayReadyForOperation({
runtime: params.runtime,
operation: "open the dashboard",
yes: params.yes,
probeUrl: params.target.probeUrl,
readyWhenReachable: true,
...params.allowRecovery === false ? {
allowInstall: false,
interactive: false
} : {}
});
}
function dashboardJsonFailure(runtime, reason) {
writeRuntimeJson(runtime, {
ok: false,
reason
}, 0);
runtime.exit(1);
}
async function dashboardJsonCommand(runtime) {
try {
const target = await resolveDashboardTarget();
const readiness = await ensureDashboardTargetReady({
target,
runtime: quietRuntime,
allowRecovery: false
});
if (!readiness.ready) {
dashboardJsonFailure(runtime, readiness.reason);
return;
}
if (!await hasVerifiedControlUiLoopbackAlias(target)) {
dashboardJsonFailure(runtime, "Dashboard loopback listener could not be verified as the configured Gateway.");
return;
}
const document = await waitForControlUiDocument({
url: target.documentUrl,
tlsConfig: target.tlsConfig,
waitForPending: false
});
if (!document.ready) {
dashboardJsonFailure(runtime, document.reason);
return;
}
const browserHandoff = await issueControlUiBrowserHandoff(target.links.httpUrl);
writeRuntimeJson(runtime, {
ok: true,
url: target.dashboardUrl,
httpUrl: target.links.httpUrl,
wsUrl: target.links.wsUrl,
port: target.port,
tokenIncluded: target.includeTokenInUrl,
browserUrl: browserHandoff.browserUrl,
browserBootstrapExpiresAtMs: browserHandoff.expiresAtMs,
...target.gatewayAuthHandoff ? { [gatewayPasswordJsonKey]: target.gatewayAuthHandoff } : {},
...document.tlsFingerprint ? { tlsFingerprint: document.tlsFingerprint } : {}
}, 0);
} catch (err) {
dashboardJsonFailure(runtime, (err instanceof Error ? err.message : String(err)) || "Dashboard target resolution failed.");
}
}
/** Open or print the Control UI dashboard URL after ensuring the Gateway is reachable. */
async function dashboardCommand(runtime = defaultRuntime, options = {}) {
if (options.json) {
await dashboardJsonCommand(runtime);
return;
}
let initialTarget;
try {
initialTarget = await resolveDashboardTarget();
} catch (error) {
runtime.error(error instanceof Error ? error.message : String(error));
runtime.exit(1);
return;
}
const readiness = await ensureDashboardTargetReady({
target: initialTarget,
runtime,
yes: options.yes
});
if (!readiness.ready) return;
const target = readiness.recovered ? await resolveDashboardTarget() : initialTarget;
const recoveryChangedProbe = target.probeUrl !== initialTarget.probeUrl;
if (readiness.recovered && recoveryChangedProbe) {
if (!(await ensureDashboardTargetReady({
target,
runtime,
allowRecovery: false
})).ready) return;
}
if (!await hasVerifiedControlUiLoopbackAlias(target)) {
runtime.error("Dashboard loopback listener could not be verified as the configured Gateway; refusing to copy or open an authenticated URL.");
runtime.log("Restart the Gateway, then run `openclaw gateway status --deep` for details.");
return;
}
const document = await waitForControlUiDocument({
url: target.documentUrl,
tlsConfig: target.tlsConfig,
onPending: () => runtime.log("Control UI assets are preparing; waiting for the dashboard…")
});
if (!document.ready) {
runtime.error(document.reason);
runtime.log("Run `openclaw gateway status --deep` for details.");
runtime.exit(1);
return;
}
let browserUrl;
try {
browserUrl = (await issueControlUiBrowserHandoff(target.links.httpUrl)).browserUrl;
} catch (error) {
runtime.error(`Could not create a one-time browser pairing link: ${error instanceof Error ? error.message : String(error)}`);
runtime.log("Run `openclaw doctor`, then retry `openclaw dashboard`.");
return;
}
const { port, basePath, links, includeTokenInUrl, tlsConfig } = target;
runtime.log(`Dashboard URL: ${links.httpUrl}`);
runtime.log("One-time browser pairing included in browser/clipboard URL.");
const copied = await copyToClipboard(browserUrl).catch(() => false);
runtime.log(copied ? "Copied to clipboard." : "Copy to clipboard unavailable.");
let opened = false;
let hint;
if (!options.noOpen) {
if ((await detectBrowserOpenSupport()).ok) {
opened = await openUrl(browserUrl);
if (!opened && !copied && isRemoteEnvironment()) hint = formatControlUiSshHint({
port,
basePath,
tlsEnabled: tlsConfig?.enabled === true
});
else hint = opened ? void 0 : copied ? "Browser launch failed. Open the one-time pairing URL copied to clipboard." : "Browser launch failed. Open the Dashboard URL above manually.";
} else hint = formatControlUiSshHint({
port,
basePath,
tlsEnabled: tlsConfig?.enabled === true
});
} else hint = copied ? "Browser launch disabled (--no-open). One-time browser pairing URL copied to clipboard." : "Browser launch disabled (--no-open). Use the URL above.";
const handoffDeliveryFailed = !copied && !opened;
const fallbackToManualAuth = handoffDeliveryFailed && includeTokenInUrl;
const fallbackToJsonHandoff = handoffDeliveryFailed && !includeTokenInUrl;
const suppressNoOpenHint = options.noOpen === true && (fallbackToManualAuth || fallbackToJsonHandoff);
if (opened) runtime.log("Opened in your browser. Keep that tab to control OpenClaw.");
else if (hint && !suppressNoOpenHint) runtime.log(hint);
if (fallbackToManualAuth) runtime.log("Token auto-auth not delivered. Append your gateway token (from OPENCLAW_GATEWAY_TOKEN or gateway.auth.token) as a URL fragment with key `token` to authenticate.");
else if (fallbackToJsonHandoff) runtime.log("One-time pairing URL not delivered. Run `openclaw dashboard --json` and open its `browserUrl` within ten minutes.");
}
//#endregion
export { dashboardCommand };