@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
1,147 lines (1,137 loc) • 51.8 kB
JavaScript
import "./paths-BDd7_JUB.js";
import { c as resolveDefaultAgentId, n as resolveAgentConfig } from "./agent-scope-CrgUOY3f.js";
import { L as isRich, R as theme, c as defaultRuntime } from "./subsystem-46MXi6Ip.js";
import { _ as shortenHomePath, g as shortenHomeInString } from "./utils-Dg0Xbl6w.js";
import "./exec-CTo4hK94.js";
import "./model-selection-Cp1maz7B.js";
import "./github-copilot-token-VZsS4013.js";
import "./boolean-CE7i9tBR.js";
import "./env-DOcCob95.js";
import { h as parseDurationMs, r as loadConfig } from "./config-qgIz1lbh.js";
import "./manifest-registry-BFpLJJDB.js";
import "./message-channel-CQGWXVL4.js";
import "./client-zqMhLTAX.js";
import { r as randomIdempotencyKey } from "./call-CPBhMXxo.js";
import { t as formatDocsLink } from "./links-C9fyAH-V.js";
import { r as runCommandWithRuntime } from "./cli-utils-CukoNm8O.js";
import "./progress-uNDQDtGB.js";
import { o as maxAsk, p as resolveExecApprovalsFromFile, s as minSecurity } from "./exec-approvals-CK-Umdr3.js";
import { a as canvasSnapshotTempPath, c as parseCameraClipPayload, d as buildNodeShellCommand, i as parseEnvPairs, l as parseCameraSnapPayload, n as screenRecordTempPath, o as parseCanvasSnapshotPayload, r as writeScreenRecordToFile, s as cameraTempPath, t as parseScreenRecordPayload, u as writeBase64ToFile } from "./nodes-screen-Ln98EX_f.js";
import { t as parseTimeoutMs } from "./parse-timeout-CmBmmqZZ.js";
import { t as renderTable } from "./table-DuNe7Qes.js";
import { a as formatAge, c as parsePairingList, i as unauthorizedHintForMessage, n as nodesCallOpts, o as formatPermissions, r as resolveNodeId, s as parseNodeList, t as callGatewayCli } from "./rpc-HdKLb6jx.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/cli/nodes-cli/cli-utils.ts
function getNodesTheme() {
const rich = isRich();
const color = (fn) => (value) => rich ? fn(value) : value;
return {
rich,
heading: color(theme.heading),
ok: color(theme.success),
warn: color(theme.warn),
muted: color(theme.muted),
error: color(theme.error)
};
}
function runNodesCommand(label, action) {
return runCommandWithRuntime(defaultRuntime, action, (err) => {
const message = String(err);
const { error, warn } = getNodesTheme();
defaultRuntime.error(error(`nodes ${label} failed: ${message}`));
const hint = unauthorizedHintForMessage(message);
if (hint) defaultRuntime.error(warn(hint));
defaultRuntime.exit(1);
});
}
//#endregion
//#region src/cli/nodes-cli/register.camera.ts
const parseFacing = (value) => {
const v = String(value ?? "").trim().toLowerCase();
if (v === "front" || v === "back") return v;
throw new Error(`invalid facing: ${value} (expected front|back)`);
};
function registerNodesCameraCommands(nodes) {
const camera = nodes.command("camera").description("Capture camera media from a paired node");
nodesCallOpts(camera.command("list").description("List available cameras on a node").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").action(async (opts) => {
await runNodesCommand("camera list", async () => {
const raw = await callGatewayCli("node.invoke", opts, {
nodeId: await resolveNodeId(opts, String(opts.node ?? "")),
command: "camera.list",
params: {},
idempotencyKey: randomIdempotencyKey()
});
const res = typeof raw === "object" && raw !== null ? raw : {};
const payload = typeof res.payload === "object" && res.payload !== null ? res.payload : {};
const devices = Array.isArray(payload.devices) ? payload.devices : [];
if (opts.json) {
defaultRuntime.log(JSON.stringify(devices, null, 2));
return;
}
if (devices.length === 0) {
const { muted } = getNodesTheme();
defaultRuntime.log(muted("No cameras reported."));
return;
}
const { heading, muted } = getNodesTheme();
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const rows = devices.map((device) => ({
Name: typeof device.name === "string" ? device.name : "Unknown Camera",
Position: typeof device.position === "string" ? device.position : muted("unspecified"),
ID: typeof device.id === "string" ? device.id : ""
}));
defaultRuntime.log(heading("Cameras"));
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [
{
key: "Name",
header: "Name",
minWidth: 14,
flex: true
},
{
key: "Position",
header: "Position",
minWidth: 10
},
{
key: "ID",
header: "ID",
minWidth: 10,
flex: true
}
],
rows
}).trimEnd());
});
}), { timeoutMs: 6e4 });
nodesCallOpts(camera.command("snap").description("Capture a photo from a node camera (prints MEDIA:<path>)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--facing <front|back|both>", "Camera facing", "both").option("--device-id <id>", "Camera device id (from nodes camera list)").option("--max-width <px>", "Max width in px (optional)").option("--quality <0-1>", "JPEG quality (default 0.9)").option("--delay-ms <ms>", "Delay before capture in ms (macOS default 2000)").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000").action(async (opts) => {
await runNodesCommand("camera snap", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const facingOpt = String(opts.facing ?? "both").trim().toLowerCase();
const facings = facingOpt === "both" ? ["front", "back"] : facingOpt === "front" || facingOpt === "back" ? [facingOpt] : (() => {
throw new Error(`invalid facing: ${String(opts.facing)} (expected front|back|both)`);
})();
const maxWidth = opts.maxWidth ? Number.parseInt(String(opts.maxWidth), 10) : void 0;
const quality = opts.quality ? Number.parseFloat(String(opts.quality)) : void 0;
const delayMs = opts.delayMs ? Number.parseInt(String(opts.delayMs), 10) : void 0;
const deviceId = opts.deviceId ? String(opts.deviceId).trim() : void 0;
const timeoutMs = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) : void 0;
const results = [];
for (const facing of facings) {
const invokeParams = {
nodeId,
command: "camera.snap",
params: {
facing,
maxWidth: Number.isFinite(maxWidth) ? maxWidth : void 0,
quality: Number.isFinite(quality) ? quality : void 0,
format: "jpg",
delayMs: Number.isFinite(delayMs) ? delayMs : void 0,
deviceId: deviceId || void 0
},
idempotencyKey: randomIdempotencyKey()
};
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) invokeParams.timeoutMs = timeoutMs;
const raw = await callGatewayCli("node.invoke", opts, invokeParams);
const payload = parseCameraSnapPayload((typeof raw === "object" && raw !== null ? raw : {}).payload);
const filePath = cameraTempPath({
kind: "snap",
facing,
ext: payload.format === "jpeg" ? "jpg" : payload.format
});
await writeBase64ToFile(filePath, payload.base64);
results.push({
facing,
path: filePath,
width: payload.width,
height: payload.height
});
}
if (opts.json) {
defaultRuntime.log(JSON.stringify({ files: results }, null, 2));
return;
}
defaultRuntime.log(results.map((r) => `MEDIA:${shortenHomePath(r.path)}`).join("\n"));
});
}), { timeoutMs: 6e4 });
nodesCallOpts(camera.command("clip").description("Capture a short video clip from a node camera (prints MEDIA:<path>)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--facing <front|back>", "Camera facing", "front").option("--device-id <id>", "Camera device id (from nodes camera list)").option("--duration <ms|10s|1m>", "Duration (default 3000ms; supports ms/s/m, e.g. 10s)", "3000").option("--no-audio", "Disable audio capture").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 90000)", "90000").action(async (opts) => {
await runNodesCommand("camera clip", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const facing = parseFacing(String(opts.facing ?? "front"));
const durationMs = parseDurationMs(String(opts.duration ?? "3000"));
const includeAudio = opts.audio !== false;
const timeoutMs = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) : void 0;
const deviceId = opts.deviceId ? String(opts.deviceId).trim() : void 0;
const invokeParams = {
nodeId,
command: "camera.clip",
params: {
facing,
durationMs: Number.isFinite(durationMs) ? durationMs : void 0,
includeAudio,
format: "mp4",
deviceId: deviceId || void 0
},
idempotencyKey: randomIdempotencyKey()
};
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) invokeParams.timeoutMs = timeoutMs;
const raw = await callGatewayCli("node.invoke", opts, invokeParams);
const payload = parseCameraClipPayload((typeof raw === "object" && raw !== null ? raw : {}).payload);
const filePath = cameraTempPath({
kind: "clip",
facing,
ext: payload.format
});
await writeBase64ToFile(filePath, payload.base64);
if (opts.json) {
defaultRuntime.log(JSON.stringify({ file: {
facing,
path: filePath,
durationMs: payload.durationMs,
hasAudio: payload.hasAudio
} }, null, 2));
return;
}
defaultRuntime.log(`MEDIA:${shortenHomePath(filePath)}`);
});
}), { timeoutMs: 9e4 });
}
//#endregion
//#region src/cli/nodes-cli/a2ui-jsonl.ts
const A2UI_ACTION_KEYS = [
"beginRendering",
"surfaceUpdate",
"dataModelUpdate",
"deleteSurface",
"createSurface"
];
function buildA2UITextJsonl(text) {
const surfaceId = "main";
const rootId = "root";
const textId = "text";
return [{ surfaceUpdate: {
surfaceId,
components: [{
id: rootId,
component: { Column: { children: { explicitList: [textId] } } }
}, {
id: textId,
component: { Text: {
text: { literalString: text },
usageHint: "body"
} }
}]
} }, { beginRendering: {
surfaceId,
root: rootId
} }].map((payload) => JSON.stringify(payload)).join("\n");
}
function validateA2UIJsonl(jsonl) {
const lines = jsonl.split(/\r?\n/);
const errors = [];
let sawV08 = false;
let sawV09 = false;
let messageCount = 0;
lines.forEach((line, idx) => {
const trimmed = line.trim();
if (!trimmed) return;
messageCount += 1;
let obj;
try {
obj = JSON.parse(trimmed);
} catch (err) {
errors.push(`line ${idx + 1}: ${String(err)}`);
return;
}
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
errors.push(`line ${idx + 1}: expected JSON object`);
return;
}
const record = obj;
const actionKeys = A2UI_ACTION_KEYS.filter((key) => key in record);
if (actionKeys.length !== 1) {
errors.push(`line ${idx + 1}: expected exactly one action key (${A2UI_ACTION_KEYS.join(", ")})`);
return;
}
if (actionKeys[0] === "createSurface") sawV09 = true;
else sawV08 = true;
});
if (messageCount === 0) errors.push("no JSONL messages found");
if (sawV08 && sawV09) errors.push("mixed A2UI v0.8 and v0.9 messages in one file");
if (errors.length > 0) throw new Error(`Invalid A2UI JSONL:\n- ${errors.join("\n- ")}`);
return {
version: sawV09 ? "v0.9" : "v0.8",
messageCount
};
}
//#endregion
//#region src/cli/nodes-cli/register.canvas.ts
async function invokeCanvas(opts, command, params) {
const invokeParams = {
nodeId: await resolveNodeId(opts, String(opts.node ?? "")),
command,
params,
idempotencyKey: randomIdempotencyKey()
};
const timeoutMs = parseTimeoutMs(opts.invokeTimeout);
if (typeof timeoutMs === "number") invokeParams.timeoutMs = timeoutMs;
return await callGatewayCli("node.invoke", opts, invokeParams);
}
function registerNodesCanvasCommands(nodes) {
const canvas = nodes.command("canvas").description("Capture or render canvas content from a paired node");
nodesCallOpts(canvas.command("snapshot").description("Capture a canvas snapshot (prints MEDIA:<path>)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--format <png|jpg|jpeg>", "Image format", "jpg").option("--max-width <px>", "Max width in px (optional)").option("--quality <0-1>", "JPEG quality (optional)").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000").action(async (opts) => {
await runNodesCommand("canvas snapshot", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const formatOpt = String(opts.format ?? "jpg").trim().toLowerCase();
const formatForParams = formatOpt === "jpg" ? "jpeg" : formatOpt === "jpeg" ? "jpeg" : "png";
if (formatForParams !== "png" && formatForParams !== "jpeg") throw new Error(`invalid format: ${String(opts.format)} (expected png|jpg|jpeg)`);
const maxWidth = opts.maxWidth ? Number.parseInt(String(opts.maxWidth), 10) : void 0;
const quality = opts.quality ? Number.parseFloat(String(opts.quality)) : void 0;
const timeoutMs = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) : void 0;
const invokeParams = {
nodeId,
command: "canvas.snapshot",
params: {
format: formatForParams,
maxWidth: Number.isFinite(maxWidth) ? maxWidth : void 0,
quality: Number.isFinite(quality) ? quality : void 0
},
idempotencyKey: randomIdempotencyKey()
};
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) invokeParams.timeoutMs = timeoutMs;
const raw = await callGatewayCli("node.invoke", opts, invokeParams);
const payload = parseCanvasSnapshotPayload((typeof raw === "object" && raw !== null ? raw : {}).payload);
const filePath = canvasSnapshotTempPath({ ext: payload.format === "jpeg" ? "jpg" : payload.format });
await writeBase64ToFile(filePath, payload.base64);
if (opts.json) {
defaultRuntime.log(JSON.stringify({ file: {
path: filePath,
format: payload.format
} }, null, 2));
return;
}
defaultRuntime.log(`MEDIA:${shortenHomePath(filePath)}`);
});
}), { timeoutMs: 6e4 });
nodesCallOpts(canvas.command("present").description("Show the canvas (optionally with a target URL/path)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--target <urlOrPath>", "Target URL/path (optional)").option("--x <px>", "Placement x coordinate").option("--y <px>", "Placement y coordinate").option("--width <px>", "Placement width").option("--height <px>", "Placement height").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => {
await runNodesCommand("canvas present", async () => {
const placement = {
x: opts.x ? Number.parseFloat(opts.x) : void 0,
y: opts.y ? Number.parseFloat(opts.y) : void 0,
width: opts.width ? Number.parseFloat(opts.width) : void 0,
height: opts.height ? Number.parseFloat(opts.height) : void 0
};
const params = {};
if (opts.target) params.url = String(opts.target);
if (Number.isFinite(placement.x) || Number.isFinite(placement.y) || Number.isFinite(placement.width) || Number.isFinite(placement.height)) params.placement = placement;
await invokeCanvas(opts, "canvas.present", params);
if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas present ok"));
}
});
}));
nodesCallOpts(canvas.command("hide").description("Hide the canvas").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => {
await runNodesCommand("canvas hide", async () => {
await invokeCanvas(opts, "canvas.hide", void 0);
if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas hide ok"));
}
});
}));
nodesCallOpts(canvas.command("navigate").description("Navigate the canvas to a URL").argument("<url>", "Target URL/path").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (url, opts) => {
await runNodesCommand("canvas navigate", async () => {
await invokeCanvas(opts, "canvas.navigate", { url });
if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas navigate ok"));
}
});
}));
nodesCallOpts(canvas.command("eval").description("Evaluate JavaScript in the canvas").argument("[js]", "JavaScript to evaluate").option("--js <code>", "JavaScript to evaluate").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (jsArg, opts) => {
await runNodesCommand("canvas eval", async () => {
const js = opts.js ?? jsArg;
if (!js) throw new Error("missing --js or <js>");
const raw = await invokeCanvas(opts, "canvas.eval", { javaScript: js });
if (opts.json) {
defaultRuntime.log(JSON.stringify(raw, null, 2));
return;
}
const payload = typeof raw === "object" && raw !== null ? raw.payload : void 0;
if (payload?.result) defaultRuntime.log(payload.result);
else {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas eval ok"));
}
});
}));
const a2ui = canvas.command("a2ui").description("Render A2UI content on the canvas");
nodesCallOpts(a2ui.command("push").description("Push A2UI JSONL to the canvas").option("--jsonl <path>", "Path to JSONL payload").option("--text <text>", "Render a quick A2UI text payload").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => {
await runNodesCommand("canvas a2ui push", async () => {
const hasJsonl = Boolean(opts.jsonl);
const hasText = typeof opts.text === "string";
if (hasJsonl === hasText) throw new Error("provide exactly one of --jsonl or --text");
const jsonl = hasText ? buildA2UITextJsonl(String(opts.text ?? "")) : await fs.readFile(String(opts.jsonl), "utf8");
const { version, messageCount } = validateA2UIJsonl(jsonl);
if (version === "v0.9") throw new Error("Detected A2UI v0.9 JSONL (createSurface). OpenClaw currently supports v0.8 only.");
await invokeCanvas(opts, "canvas.a2ui.pushJSONL", { jsonl });
if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok(`canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`));
}
});
}));
nodesCallOpts(a2ui.command("reset").description("Reset A2UI renderer state").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--invoke-timeout <ms>", "Node invoke timeout in ms").action(async (opts) => {
await runNodesCommand("canvas a2ui reset", async () => {
await invokeCanvas(opts, "canvas.a2ui.reset", void 0);
if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas a2ui reset ok"));
}
});
}));
}
//#endregion
//#region src/cli/nodes-cli/register.invoke.ts
function normalizeExecSecurity(value) {
const normalized = value?.trim().toLowerCase();
if (normalized === "deny" || normalized === "allowlist" || normalized === "full") return normalized;
return null;
}
function normalizeExecAsk(value) {
const normalized = value?.trim().toLowerCase();
if (normalized === "off" || normalized === "on-miss" || normalized === "always") return normalized;
return null;
}
function mergePathPrepend(existing, prepend) {
if (prepend.length === 0) return existing;
const partsExisting = (existing ?? "").split(path.delimiter).map((part) => part.trim()).filter(Boolean);
const merged = [];
const seen = /* @__PURE__ */ new Set();
for (const part of [...prepend, ...partsExisting]) {
if (seen.has(part)) continue;
seen.add(part);
merged.push(part);
}
return merged.join(path.delimiter);
}
function applyPathPrepend(env, prepend, options) {
if (!Array.isArray(prepend) || prepend.length === 0) return;
if (options?.requireExisting && !env.PATH) return;
const merged = mergePathPrepend(env.PATH, prepend);
if (merged) env.PATH = merged;
}
function resolveExecDefaults(cfg, agentId) {
const globalExec = cfg?.tools?.exec;
if (!agentId) return globalExec ? {
security: globalExec.security,
ask: globalExec.ask,
node: globalExec.node,
pathPrepend: globalExec.pathPrepend,
safeBins: globalExec.safeBins
} : void 0;
const agentExec = resolveAgentConfig(cfg, agentId)?.tools?.exec;
return {
security: agentExec?.security ?? globalExec?.security,
ask: agentExec?.ask ?? globalExec?.ask,
node: agentExec?.node ?? globalExec?.node,
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
safeBins: agentExec?.safeBins ?? globalExec?.safeBins
};
}
async function resolveNodePlatform(opts, nodeId) {
try {
const match = parseNodeList(await callGatewayCli("node.list", opts, {})).find((node) => node.nodeId === nodeId);
return typeof match?.platform === "string" ? match.platform : null;
} catch {
return null;
}
}
function registerNodesInvokeCommands(nodes) {
nodesCallOpts(nodes.command("invoke").description("Invoke a command on a paired node").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").requiredOption("--command <command>", "Command (e.g. canvas.eval)").option("--params <json>", "JSON object string for params", "{}").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 15000)", "15000").option("--idempotency-key <key>", "Idempotency key (optional)").action(async (opts) => {
await runNodesCommand("invoke", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const command = String(opts.command ?? "").trim();
if (!nodeId || !command) {
const { error } = getNodesTheme();
defaultRuntime.error(error("--node and --command required"));
defaultRuntime.exit(1);
return;
}
const params = JSON.parse(String(opts.params ?? "{}"));
const timeoutMs = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) : void 0;
const invokeParams = {
nodeId,
command,
params,
idempotencyKey: String(opts.idempotencyKey ?? randomIdempotencyKey())
};
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) invokeParams.timeoutMs = timeoutMs;
const result = await callGatewayCli("node.invoke", opts, invokeParams);
defaultRuntime.log(JSON.stringify(result, null, 2));
});
}), { timeoutMs: 3e4 });
nodesCallOpts(nodes.command("run").description("Run a shell command on a node (mac only)").option("--node <idOrNameOrIp>", "Node id, name, or IP").option("--cwd <path>", "Working directory").option("--env <key=val>", "Environment override (repeatable)", (value, prev = []) => [...prev, value]).option("--raw <command>", "Run a raw shell command string (sh -lc / cmd.exe /c)").option("--agent <id>", "Agent id (default: configured default agent)").option("--ask <mode>", "Exec ask mode (off|on-miss|always)").option("--security <mode>", "Exec security mode (deny|allowlist|full)").option("--command-timeout <ms>", "Command timeout (ms)").option("--needs-screen-recording", "Require screen recording permission").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 30000)", "30000").argument("[command...]", "Command and args").action(async (command, opts) => {
await runNodesCommand("run", async () => {
const cfg = loadConfig();
const agentId = opts.agent?.trim() || resolveDefaultAgentId(cfg);
const execDefaults = resolveExecDefaults(cfg, agentId);
const raw = typeof opts.raw === "string" ? opts.raw.trim() : "";
if (raw && Array.isArray(command) && command.length > 0) throw new Error("use --raw or argv, not both");
if (!raw && (!Array.isArray(command) || command.length === 0)) throw new Error("command required");
const nodeQuery = String(opts.node ?? "").trim() || execDefaults?.node?.trim() || "";
if (!nodeQuery) throw new Error("node required (set --node or tools.exec.node)");
const nodeId = await resolveNodeId(opts, nodeQuery);
const env = parseEnvPairs(opts.env);
const timeoutMs = parseTimeoutMs(opts.commandTimeout);
const invokeTimeout = parseTimeoutMs(opts.invokeTimeout);
let argv = Array.isArray(command) ? command : [];
let rawCommand;
if (raw) {
rawCommand = raw;
const platform = await resolveNodePlatform(opts, nodeId);
argv = buildNodeShellCommand(rawCommand, platform ?? void 0);
}
const nodeEnv = env ? { ...env } : void 0;
if (nodeEnv) applyPathPrepend(nodeEnv, execDefaults?.pathPrepend, { requireExisting: true });
let approvedByAsk = false;
let approvalDecision = null;
const configuredSecurity = normalizeExecSecurity(execDefaults?.security) ?? "allowlist";
const requestedSecurity = normalizeExecSecurity(opts.security);
if (opts.security && !requestedSecurity) throw new Error("invalid --security (use deny|allowlist|full)");
const configuredAsk = normalizeExecAsk(execDefaults?.ask) ?? "on-miss";
const requestedAsk = normalizeExecAsk(opts.ask);
if (opts.ask && !requestedAsk) throw new Error("invalid --ask (use off|on-miss|always)");
const security = minSecurity(configuredSecurity, requestedSecurity ?? configuredSecurity);
const ask = maxAsk(configuredAsk, requestedAsk ?? configuredAsk);
const approvalsSnapshot = await callGatewayCli("exec.approvals.node.get", opts, { nodeId });
const approvalsFile = approvalsSnapshot && typeof approvalsSnapshot === "object" ? approvalsSnapshot.file : void 0;
if (!approvalsFile || typeof approvalsFile !== "object") throw new Error("exec approvals unavailable");
const approvals = resolveExecApprovalsFromFile({
file: approvalsFile,
agentId,
overrides: {
security,
ask
}
});
const hostSecurity = minSecurity(security, approvals.agent.security);
const hostAsk = maxAsk(ask, approvals.agent.ask);
const askFallback = approvals.agent.askFallback;
if (hostSecurity === "deny") throw new Error("exec denied: host=node security=deny");
if (hostAsk === "always" || hostAsk === "on-miss") {
const decisionResult = await callGatewayCli("exec.approval.request", opts, {
command: rawCommand ?? argv.join(" "),
cwd: opts.cwd,
host: "node",
security: hostSecurity,
ask: hostAsk,
agentId,
resolvedPath: void 0,
sessionKey: void 0,
timeoutMs: 12e4
});
const decision = decisionResult && typeof decisionResult === "object" ? decisionResult.decision ?? null : null;
if (decision === "deny") throw new Error("exec denied: user denied");
if (!decision) if (askFallback === "full") {
approvedByAsk = true;
approvalDecision = "allow-once";
} else if (askFallback === "allowlist") {} else throw new Error("exec denied: approval required (approval UI not available)");
if (decision === "allow-once") {
approvedByAsk = true;
approvalDecision = "allow-once";
}
if (decision === "allow-always") {
approvedByAsk = true;
approvalDecision = "allow-always";
}
}
const invokeParams = {
nodeId,
command: "system.run",
params: {
command: argv,
cwd: opts.cwd,
env: nodeEnv,
timeoutMs,
needsScreenRecording: opts.needsScreenRecording === true
},
idempotencyKey: String(opts.idempotencyKey ?? randomIdempotencyKey())
};
if (agentId) invokeParams.params.agentId = agentId;
if (rawCommand) invokeParams.params.rawCommand = rawCommand;
invokeParams.params.approved = approvedByAsk;
if (approvalDecision) invokeParams.params.approvalDecision = approvalDecision;
if (invokeTimeout !== void 0) invokeParams.timeoutMs = invokeTimeout;
const result = await callGatewayCli("node.invoke", opts, invokeParams);
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
const payload = typeof result === "object" && result !== null ? result.payload : void 0;
const stdout = typeof payload?.stdout === "string" ? payload.stdout : "";
const stderr = typeof payload?.stderr === "string" ? payload.stderr : "";
const exitCode = typeof payload?.exitCode === "number" ? payload.exitCode : null;
const timedOut = payload?.timedOut === true;
const success = payload?.success === true;
if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr);
if (timedOut) {
const { error } = getNodesTheme();
defaultRuntime.error(error("run timed out"));
defaultRuntime.exit(1);
return;
}
if (exitCode !== null && exitCode !== 0) {
const hint = unauthorizedHintForMessage(`${stderr}\n${stdout}`);
if (hint) {
const { warn } = getNodesTheme();
defaultRuntime.error(warn(hint));
}
}
if (exitCode !== null && exitCode !== 0 && !success) {
const { error } = getNodesTheme();
defaultRuntime.error(error(`run exit ${exitCode}`));
defaultRuntime.exit(1);
return;
}
});
}), { timeoutMs: 35e3 });
}
//#endregion
//#region src/cli/nodes-cli/register.location.ts
function registerNodesLocationCommands(nodes) {
nodesCallOpts(nodes.command("location").description("Fetch location from a paired node").command("get").description("Fetch the current location from a node").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--max-age <ms>", "Use cached location newer than this (ms)").option("--accuracy <coarse|balanced|precise>", "Desired accuracy (default: balanced/precise depending on node setting)").option("--location-timeout <ms>", "Location fix timeout (ms)", "10000").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 20000)", "20000").action(async (opts) => {
await runNodesCommand("location get", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const maxAgeMs = opts.maxAge ? Number.parseInt(String(opts.maxAge), 10) : void 0;
const desiredAccuracyRaw = typeof opts.accuracy === "string" ? opts.accuracy.trim().toLowerCase() : void 0;
const desiredAccuracy = desiredAccuracyRaw === "coarse" || desiredAccuracyRaw === "balanced" || desiredAccuracyRaw === "precise" ? desiredAccuracyRaw : void 0;
const timeoutMs = opts.locationTimeout ? Number.parseInt(String(opts.locationTimeout), 10) : void 0;
const invokeTimeoutMs = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) : void 0;
const invokeParams = {
nodeId,
command: "location.get",
params: {
maxAgeMs: Number.isFinite(maxAgeMs) ? maxAgeMs : void 0,
desiredAccuracy,
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0
},
idempotencyKey: randomIdempotencyKey()
};
if (typeof invokeTimeoutMs === "number" && Number.isFinite(invokeTimeoutMs)) invokeParams.timeoutMs = invokeTimeoutMs;
const raw = await callGatewayCli("node.invoke", opts, invokeParams);
const res = typeof raw === "object" && raw !== null ? raw : {};
const payload = res.payload && typeof res.payload === "object" ? res.payload : {};
if (opts.json) {
defaultRuntime.log(JSON.stringify(payload, null, 2));
return;
}
const lat = payload.lat;
const lon = payload.lon;
const acc = payload.accuracyMeters;
if (typeof lat === "number" && typeof lon === "number") {
const accText = typeof acc === "number" ? ` ±${acc.toFixed(1)}m` : "";
defaultRuntime.log(`${lat},${lon}${accText}`);
return;
}
defaultRuntime.log(JSON.stringify(payload));
});
}), { timeoutMs: 3e4 });
}
//#endregion
//#region src/cli/nodes-cli/register.notify.ts
function registerNodesNotifyCommand(nodes) {
nodesCallOpts(nodes.command("notify").description("Send a local notification on a node (mac only)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--title <text>", "Notification title").option("--body <text>", "Notification body").option("--sound <name>", "Notification sound").option("--priority <passive|active|timeSensitive>", "Notification priority").option("--delivery <system|overlay|auto>", "Delivery mode", "system").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 15000)", "15000").action(async (opts) => {
await runNodesCommand("notify", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const title = String(opts.title ?? "").trim();
const body = String(opts.body ?? "").trim();
if (!title && !body) throw new Error("missing --title or --body");
const invokeTimeout = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) : void 0;
const invokeParams = {
nodeId,
command: "system.notify",
params: {
title,
body,
sound: opts.sound,
priority: opts.priority,
delivery: opts.delivery
},
idempotencyKey: String(opts.idempotencyKey ?? randomIdempotencyKey())
};
if (typeof invokeTimeout === "number" && Number.isFinite(invokeTimeout)) invokeParams.timeoutMs = invokeTimeout;
const result = await callGatewayCli("node.invoke", opts, invokeParams);
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
const { ok } = getNodesTheme();
defaultRuntime.log(ok("notify ok"));
});
}));
}
//#endregion
//#region src/cli/nodes-cli/register.pairing.ts
function registerNodesPairingCommands(nodes) {
nodesCallOpts(nodes.command("pending").description("List pending pairing requests").action(async (opts) => {
await runNodesCommand("pending", async () => {
const { pending } = parsePairingList(await callGatewayCli("node.pair.list", opts, {}));
if (opts.json) {
defaultRuntime.log(JSON.stringify(pending, null, 2));
return;
}
if (pending.length === 0) {
const { muted } = getNodesTheme();
defaultRuntime.log(muted("No pending pairing requests."));
return;
}
const { heading, warn, muted } = getNodesTheme();
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const now = Date.now();
const rows = pending.map((r) => ({
Request: r.requestId,
Node: r.displayName?.trim() ? r.displayName.trim() : r.nodeId,
IP: r.remoteIp ?? "",
Requested: typeof r.ts === "number" ? `${formatAge(Math.max(0, now - r.ts))} ago` : muted("unknown"),
Repair: r.isRepair ? warn("yes") : ""
}));
defaultRuntime.log(heading("Pending"));
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [
{
key: "Request",
header: "Request",
minWidth: 8
},
{
key: "Node",
header: "Node",
minWidth: 14,
flex: true
},
{
key: "IP",
header: "IP",
minWidth: 10
},
{
key: "Requested",
header: "Requested",
minWidth: 12
},
{
key: "Repair",
header: "Repair",
minWidth: 6
}
],
rows
}).trimEnd());
});
}));
nodesCallOpts(nodes.command("approve").description("Approve a pending pairing request").argument("<requestId>", "Pending request id").action(async (requestId, opts) => {
await runNodesCommand("approve", async () => {
const result = await callGatewayCli("node.pair.approve", opts, { requestId });
defaultRuntime.log(JSON.stringify(result, null, 2));
});
}));
nodesCallOpts(nodes.command("reject").description("Reject a pending pairing request").argument("<requestId>", "Pending request id").action(async (requestId, opts) => {
await runNodesCommand("reject", async () => {
const result = await callGatewayCli("node.pair.reject", opts, { requestId });
defaultRuntime.log(JSON.stringify(result, null, 2));
});
}));
nodesCallOpts(nodes.command("rename").description("Rename a paired node (display name override)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").requiredOption("--name <displayName>", "New display name").action(async (opts) => {
await runNodesCommand("rename", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const name = String(opts.name ?? "").trim();
if (!nodeId || !name) {
defaultRuntime.error("--node and --name required");
defaultRuntime.exit(1);
return;
}
const result = await callGatewayCli("node.rename", opts, {
nodeId,
displayName: name
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
const { ok } = getNodesTheme();
defaultRuntime.log(ok(`node rename ok: ${nodeId} -> ${name}`));
});
}));
}
//#endregion
//#region src/cli/nodes-cli/register.screen.ts
function registerNodesScreenCommands(nodes) {
nodesCallOpts(nodes.command("screen").description("Capture screen recordings from a paired node").command("record").description("Capture a short screen recording from a node (prints MEDIA:<path>)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").option("--screen <index>", "Screen index (0 = primary)", "0").option("--duration <ms|10s>", "Clip duration (ms or 10s)", "10000").option("--fps <fps>", "Frames per second", "10").option("--no-audio", "Disable microphone audio capture").option("--out <path>", "Output path").option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 120000)", "120000").action(async (opts) => {
await runNodesCommand("screen record", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const durationMs = parseDurationMs(opts.duration ?? "");
const screenIndex = Number.parseInt(String(opts.screen ?? "0"), 10);
const fps = Number.parseFloat(String(opts.fps ?? "10"));
const timeoutMs = opts.invokeTimeout ? Number.parseInt(String(opts.invokeTimeout), 10) : void 0;
const invokeParams = {
nodeId,
command: "screen.record",
params: {
durationMs: Number.isFinite(durationMs) ? durationMs : void 0,
screenIndex: Number.isFinite(screenIndex) ? screenIndex : void 0,
fps: Number.isFinite(fps) ? fps : void 0,
format: "mp4",
includeAudio: opts.audio !== false
},
idempotencyKey: randomIdempotencyKey()
};
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) invokeParams.timeoutMs = timeoutMs;
const raw = await callGatewayCli("node.invoke", opts, invokeParams);
const parsed = parseScreenRecordPayload((typeof raw === "object" && raw !== null ? raw : {}).payload);
const written = await writeScreenRecordToFile(opts.out ?? screenRecordTempPath({ ext: parsed.format || "mp4" }), parsed.base64);
if (opts.json) {
defaultRuntime.log(JSON.stringify({ file: {
path: written.path,
durationMs: parsed.durationMs,
fps: parsed.fps,
screenIndex: parsed.screenIndex,
hasAudio: parsed.hasAudio
} }, null, 2));
return;
}
defaultRuntime.log(`MEDIA:${shortenHomePath(written.path)}`);
});
}), { timeoutMs: 18e4 });
}
//#endregion
//#region src/cli/nodes-cli/register.status.ts
function formatVersionLabel(raw) {
const trimmed = raw.trim();
if (!trimmed) return raw;
if (trimmed.toLowerCase().startsWith("v")) return trimmed;
return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed;
}
function resolveNodeVersions(node) {
const core = node.coreVersion?.trim() || void 0;
const ui = node.uiVersion?.trim() || void 0;
if (core || ui) return {
core,
ui
};
const legacy = node.version?.trim();
if (!legacy) return {
core: void 0,
ui: void 0
};
const platform = node.platform?.trim().toLowerCase() ?? "";
return platform === "darwin" || platform === "linux" || platform === "win32" || platform === "windows" ? {
core: legacy,
ui: void 0
} : {
core: void 0,
ui: legacy
};
}
function formatNodeVersions(node) {
const { core, ui } = resolveNodeVersions(node);
const parts = [];
if (core) parts.push(`core ${formatVersionLabel(core)}`);
if (ui) parts.push(`ui ${formatVersionLabel(ui)}`);
return parts.length > 0 ? parts.join(" · ") : null;
}
function formatPathEnv(raw) {
if (typeof raw !== "string") return null;
const trimmed = raw.trim();
if (!trimmed) return null;
const parts = trimmed.split(":").filter(Boolean);
return shortenHomeInString(parts.length <= 3 ? trimmed : `${parts.slice(0, 2).join(":")}:…:${parts.slice(-1)[0]}`);
}
function parseSinceMs(raw, label) {
if (raw === void 0 || raw === null) return;
const value = typeof raw === "string" ? raw.trim() : typeof raw === "number" ? String(raw).trim() : null;
if (value === null) {
defaultRuntime.error(`${label}: invalid duration value`);
defaultRuntime.exit(1);
return;
}
if (!value) return;
try {
return parseDurationMs(value);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
defaultRuntime.error(`${label}: ${message}`);
defaultRuntime.exit(1);
return;
}
}
function registerNodesStatusCommands(nodes) {
nodesCallOpts(nodes.command("status").description("List known nodes with connection status and capabilities").option("--connected", "Only show connected nodes").option("--last-connected <duration>", "Only show nodes connected within duration (e.g. 24h)").action(async (opts) => {
await runNodesCommand("status", async () => {
const connectedOnly = Boolean(opts.connected);
const sinceMs = parseSinceMs(opts.lastConnected, "Invalid --last-connected");
const result = await callGatewayCli("node.list", opts, {});
const obj = typeof result === "object" && result !== null ? result : {};
const { ok, warn, muted } = getNodesTheme();
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const now = Date.now();
const nodes = parseNodeList(result);
const lastConnectedById = sinceMs !== void 0 ? new Map(parsePairingList(await callGatewayCli("node.pair.list", opts, {})).paired.map((entry) => [entry.nodeId, entry])) : null;
const filtered = nodes.filter((n) => {
if (connectedOnly && !n.connected) return false;
if (sinceMs !== void 0) {
const paired = lastConnectedById?.get(n.nodeId);
const lastConnectedAtMs = typeof paired?.lastConnectedAtMs === "number" ? paired.lastConnectedAtMs : typeof n.connectedAtMs === "number" ? n.connectedAtMs : void 0;
if (typeof lastConnectedAtMs !== "number") return false;
if (now - lastConnectedAtMs > sinceMs) return false;
}
return true;
});
if (opts.json) {
const ts = typeof obj.ts === "number" ? obj.ts : Date.now();
defaultRuntime.log(JSON.stringify({
...obj,
ts,
nodes: filtered
}, null, 2));
return;
}
const pairedCount = filtered.filter((n) => Boolean(n.paired)).length;
const connectedCount = filtered.filter((n) => Boolean(n.connected)).length;
const filteredLabel = filtered.length !== nodes.length ? ` (of ${nodes.length})` : "";
defaultRuntime.log(`Known: ${filtered.length}${filteredLabel} · Paired: ${pairedCount} · Connected: ${connectedCount}`);
if (filtered.length === 0) return;
const rows = filtered.map((n) => {
const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId;
const perms = formatPermissions(n.permissions);
const versions = formatNodeVersions(n);
const pathEnv = formatPathEnv(n.pathEnv);
const detailParts = [
n.deviceFamily ? `device: ${n.deviceFamily}` : null,
n.modelIdentifier ? `hw: ${n.modelIdentifier}` : null,
perms ? `perms: ${perms}` : null,
versions,
pathEnv ? `path: ${pathEnv}` : null
].filter(Boolean);
const caps = Array.isArray(n.caps) ? n.caps.map(String).filter(Boolean).toSorted().join(", ") : "?";
const paired = n.paired ? ok("paired") : warn("unpaired");
const connected = n.connected ? ok("connected") : muted("disconnected");
const since = typeof n.connectedAtMs === "number" ? ` (${formatAge(Math.max(0, now - n.connectedAtMs))} ago)` : "";
return {
Node: name,
ID: n.nodeId,
IP: n.remoteIp ?? "",
Detail: detailParts.join(" · "),
Status: `${paired} · ${connected}${since}`,
Caps: caps
};
});
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [
{
key: "Node",
header: "Node",
minWidth: 14,
flex: true
},
{
key: "ID",
header: "ID",
minWidth: 10
},
{
key: "IP",
header: "IP",
minWidth: 10
},
{
key: "Detail",
header: "Detail",
minWidth: 18,
flex: true
},
{
key: "Status",
header: "Status",
minWidth: 18
},
{
key: "Caps",
header: "Caps",
minWidth: 12,
flex: true
}
],
rows
}).trimEnd());
});
}));
nodesCallOpts(nodes.command("describe").description("Describe a node (capabilities + supported invoke commands)").requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP").action(async (opts) => {
await runNodesCommand("describe", async () => {
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const result = await callGatewayCli("node.describe", opts, { nodeId });
if (opts.json) {
defaultRuntime.log(JSON.stringify(result, null, 2));
return;
}
const obj = typeof result === "object" && result !== null ? result : {};
const displayName = typeof obj.displayName === "string" ? obj.displayName : nodeId;
const connected = Boolean(obj.connected);
const paired = Boolean(obj.paired);
const caps = Array.isArray(obj.caps) ? obj.caps.map(String).filter(Boolean).toSorted() : null;
const commands = Array.isArray(obj.commands) ? obj.commands.map(String).filter(Boolean).toSorted() : [];
const perms = formatPermissions(obj.permissions);
const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null;
const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null;
const pathEnv = typeof obj.pathEnv === "string" ? obj.pathEnv : null;
const versions = formatNodeVersions(obj);
const { heading, ok, warn, muted } = getNodesTheme();
const status = `${paired ? ok("paired") : warn("unpaired")} · ${connected ? ok("connected") : muted("disconnected")}`;
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const rows = [
{
Field: "ID",
Value: nodeId
},
displayName ? {
Field: "Name",
Value: displayName
} : null,
ip ? {
Field: "IP",
Value: ip
} : null,
family ? {
Field: "Device",
Value: family
} : null,
model ? {
Field: "Model",
Value: model
} : null,
perms ? {
Field: "Perms",
Value: perms
} : null,
versions ? {
Field: "Version",
Value: versions
} : null,
pathEnv ? {
Field: "PATH",
Value: pathEnv
} : null,
{
Field: "Status",
Value: status
},
{
Field: "Caps",
Value: caps ? caps.join(", ") : "?"
}
].filter(Boolean);
defaultRuntime.log(heading("Node"));
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [{
key: "Field",
header: "Field",
minWidth: 8
}, {
key: "Value",
header: "Value",
minWidth: 24,
flex: true
}],
rows
}).trimEnd());
defaultRuntime.log("");
defaultRuntime.log(heading("Commands"));
if (commands.length === 0) {
defaultRuntime.log(muted("- (none reported)"));
return;
}
for (const c of commands) defaultRuntime.log(`- ${c}`);
});
}));
nodesCallOpts(nodes.command("list").description("List pending and paired nodes").option("--connected", "Only show connected nodes").option("--last-connected <duration>", "Only show nodes connected within duration (e.g. 24h)").action(async (opts) => {
await runNodesCommand("list", async () => {
const connectedOnly = Boolean(opts.connected);
const sinceMs = parseSinceMs(opts.lastConnected, "Invalid --last-connected");
const { pending, paired } = parsePairingList(await callGatewayCli("node.pair.list", opts, {}));
const { heading, muted, warn } = getNodesTheme();
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const now = Date.now();
const hasFilters = connectedOnly || sinceMs !== void 0;
const pendingRows = hasFilters ? [] : pending;
const connectedById = hasFilters ? new Map(parseNodeList(await callGatewayCli("node.list", opts, {})).map((node) => [node.nodeId, node])) : null;
const filteredPaired = paired.filter((node) => {
if (connectedOnly) {
if (!(connectedById?.get(node.nodeId))?.connected) return false;
}
if (sinceMs !== void 0) {
const live = connectedById?.get(node.nodeId);
const lastConnectedAtMs = typeof node.lastConnectedAtMs === "number" ? node.lastConnectedAtMs : typeof live?.connectedAtMs === "number" ? live.connectedAtMs : void 0;
if (typeof lastConnectedAtMs !== "number") return false;
if (now - lastConnectedAtMs > sinceMs) return false;
}
return true;
});
const filteredLabel = hasFilters && filteredPaired.length !== paired.length ? ` (of ${paired.length})` : "";
defaultRuntime.log(`Pending: ${pendingRows.length} · Paired: ${filteredPaired.length}${filteredLabel}`);
if (opts.json) {
defaultRuntime.log(JSON.stringify({
pending: pendingRows,
paired: filteredPaired
}, null, 2));
return;
}
if (pendingRows.length > 0) {
const pendingRowsRendered = pendingRows.map((r) => ({
Request: r.requestId,
Node: r.displayName?.trim() ? r.displayName.trim() : r.nodeId,
IP: r.remoteIp ?? "",
Requested: typeof r.ts === "number" ? `${formatAge(Math.max(0, now - r.ts))} ago` : muted("unknown"),
Repair: r.isRepair ? warn("yes") : ""
}));
defaultRuntime.log("");
defaultRuntime.log(heading("Pending"));
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [
{
key: "Request",
header: "Request",
minWidth: 8
},
{
key: "Node",
header: "Node",
minWidth: 14,
flex: true
},
{
key: "IP",
header: "IP",
minWidth: 10
},
{
key: "Requested",
header: "Requested",
minWidth: 12
},
{
key: "Repair",
header: "Repair",
minWidth: 6
}
],
rows: pendingRows