openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
160 lines (159 loc) • 8.08 kB
JavaScript
import { o as normalizeLowercaseStringOrEmpty, p as normalizeStringifiedOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { r as defaultRuntime } from "./runtime-BRg2NubB.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { t as normalizeAnyChannelId } from "./registry-normalize-DKVgg-js.mjs";
import { r as theme } from "./theme-DzaUZY4q.mjs";
import { r as getRuntimeConfig } from "./io.runtime-B134MOMz.mjs";
import "./config-VnHbY_QV.mjs";
import { t as GATEWAY_SERVER_CAPS } from "./server-capabilities-FfgdQE-U.mjs";
import "./plugins-BXkKReMp.mjs";
import { t as runWithLocalStateOwner } from "./local-state-owner-C_YiiUG8.mjs";
import { n as listPairingChannels, r as notifyPairingApproved } from "./pairing-Bx3sWrDJ.mjs";
import { a as listChannelPairingRequests, n as approveChannelPairingCode } from "./pairing-store-CwzvtRQg.mjs";
import { n as renderTable, t as getTerminalTableWidth } from "./table-D21zVAsn.mjs";
import { t as resolvePairingIdLabel } from "./pairing-labels-CPJKr4nd.mjs";
import { t as formatDocsHelp } from "./help-format-xds_bDSp.mjs";
import { t as bootstrapCommandOwnerFromPairing } from "./command-owner-Dq-nopwF.mjs";
//#region src/cli/pairing-cli.ts
/** Parse channel, allowing extension channels not in core registry. */
function parseChannel(raw, channels) {
const value = normalizeLowercaseStringOrEmpty(normalizeStringifiedOptionalString(raw) ?? "");
if (!value) throw new Error(`Missing channel. Use ${formatCliCommand("openclaw pairing list --channel <channel>")}.`);
const normalized = normalizeAnyChannelId(value);
if (normalized) {
if (!channels.includes(normalized)) throw new Error(`Channel "${normalized}" does not support pairing. Supported pairing channels: ${channels.join(", ") || "none"}.`);
return normalized;
}
if (/^[a-z][a-z0-9_-]{0,63}$/.test(value)) return value;
throw new Error(`Invalid channel "${value}". Use lowercase letters, numbers, "_" or "-", for example "telegram".`);
}
function resolveAccountId(raw) {
const accountId = normalizeStringifiedOptionalString(raw);
if (raw !== void 0 && !accountId) throw new Error("--account must not be blank");
return accountId;
}
function registerPairingCli(program) {
const channels = listPairingChannels();
const channelHint = channels.length > 0 ? channels.join(", ") : "none configured";
const pairing = program.command("pairing").description("Secure DM pairing (approve inbound requests)").addHelpText("after", () => formatDocsHelp("/cli/pairing"));
pairing.command("list").description("List pending pairing requests").option("--channel <channel>", `Channel (${channelHint})`).option("--account <accountId>", "Account id (for multi-account channels)").argument("[channel]", `Channel (${channelHint})`).option("--json", "Print JSON", false).action(async (channelArg, opts) => {
const channelRaw = opts.channel ?? channelArg ?? (channels.length === 1 ? channels[0] : "");
if (!channelRaw) {
if (channels.length === 0) throw new Error(`No chat DM pairing channels are configured. To approve a TUI or device request, use ${formatCliCommand("openclaw devices approve")} instead.`);
throw new Error(`Channel required (expected one of: ${channelHint}).`);
}
const channel = parseChannel(channelRaw, channels);
if (opts.channel && channelArg) {
const positionalChannel = parseChannel(channelArg, channels);
if (channel !== positionalChannel) throw new Error(`Conflicting pairing channels: "${channel}" and "${positionalChannel}". Pass the channel either positionally or with --channel.`);
}
const accountId = resolveAccountId(opts.account);
const requests = await runWithLocalStateOwner({
method: "channels.pairing.list",
params: {
channel,
format: "cli",
...accountId ? { accountId } : {}
},
target: `${channel} pairing requests`,
requiredCapabilities: [GATEWAY_SERVER_CAPS.CHANNELS_PAIRING_LIST_OWNER],
runLocal: async ({ env, assertCurrent }) => await listChannelPairingRequests(channel, env, accountId, assertCurrent)
});
if (opts.json) {
defaultRuntime.writeJson({
channel,
requests
});
return;
}
if (requests.length === 0) {
defaultRuntime.log(theme.muted(`No pending ${channel} pairing requests.`));
return;
}
const idLabel = resolvePairingIdLabel(channel);
const tableWidth = getTerminalTableWidth();
defaultRuntime.log(`${theme.heading("Pairing requests")} ${theme.muted(`(${requests.length})`)}`);
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [
{
key: "Code",
header: "Code",
minWidth: 10
},
{
key: "ID",
header: idLabel,
minWidth: 12,
flex: true
},
{
key: "Meta",
header: "Meta",
minWidth: 8,
flex: true
},
{
key: "Requested",
header: "Requested",
minWidth: 12
}
],
rows: requests.map((r) => ({
Code: r.code,
ID: r.meta?.senderId ?? r.id,
Meta: r.meta ? JSON.stringify(r.meta) : "",
Requested: r.createdAt
}))
}).trimEnd());
});
pairing.command("approve").description("Approve a pairing code and allow that sender").option("--channel <channel>", `Channel (${channelHint})`).option("--account <accountId>", "Account id (for multi-account channels)").argument("<codeOrChannel>", "Pairing code (or channel when using 2 args)").argument("[code]", "Pairing code (when channel is passed as the 1st arg)").option("--notify", "Notify the requester on the same channel", false).action(async (codeOrChannel, code, opts) => {
const defaultChannel = channels.length === 1 ? channels[0] : "";
const usingPositionalChannel = !opts.channel && code != null;
const channelRaw = opts.channel || (usingPositionalChannel ? codeOrChannel : defaultChannel);
const resolvedCode = usingPositionalChannel ? code : codeOrChannel;
if (!channelRaw || !resolvedCode) throw new Error(`Usage: ${formatCliCommand("openclaw pairing approve <channel> <code>")} (or: ${formatCliCommand("openclaw pairing approve --channel <channel> <code>")})`);
if (opts.channel && code != null) throw new Error(`Too many arguments. Use: ${formatCliCommand("openclaw pairing approve --channel <channel> <code>")}`);
const channel = parseChannel(channelRaw, channels);
const accountId = resolveAccountId(opts.account);
const params = {
channel,
code: String(resolvedCode),
...accountId ? { accountId } : {}
};
const approved = await runWithLocalStateOwner({
method: "channels.pairing.approve",
params,
target: `${channel} pairing request`,
recoveryCommand: `openclaw pairing list --channel ${channel}`,
requiredCapabilities: [GATEWAY_SERVER_CAPS.CHANNELS_PAIRING_APPROVE_OWNER],
runLocal: async ({ env, assertCurrent }) => await approveChannelPairingCode({
...params,
env,
assertCurrent
})
});
if (!approved) throw new Error(`No pending pairing request found for code "${String(resolvedCode)}". Run ${formatCliCommand(`openclaw pairing list --channel ${channel}`)} to list pending requests.`);
defaultRuntime.log(`${theme.success("Approved")} ${theme.muted(channel)} sender ${theme.command(approved.entry.meta?.senderId ?? approved.id)}.`);
const ownerBootstrap = await bootstrapCommandOwnerFromPairing({
channel,
id: approved.id
});
if (ownerBootstrap.status === "configured" && ownerBootstrap.ownerEntry) defaultRuntime.log(`${theme.success("Command owner configured")} ${theme.command(ownerBootstrap.ownerEntry)} ${theme.muted("(commands.ownerAllowFrom was empty).")}`);
if (!opts.notify) return;
const approvedAccountId = accountId || normalizeStringifiedOptionalString(approved.entry?.meta?.accountId);
try {
await notifyPairingApproved({
channelId: channel,
id: approved.id,
cfg: getRuntimeConfig(),
...approvedAccountId ? { accountId: approvedAccountId } : {},
...approved.entry.meta ? { meta: approved.entry.meta } : {}
});
} catch (err) {
defaultRuntime.log(theme.warn(`Failed to notify requester: ${String(err)}`));
}
});
}
//#endregion
export { registerPairingCli };