import type { Command } from "commander"; import { listPairingChannels, notifyPairingApproved } from "../channels/plugins/pairing.js"; import { normalizeChannelId } from "../channels/plugins/index.js"; import { loadConfig } from "../config/config.js"; import { resolvePairingIdLabel } from "../pairing/pairing-labels.js"; import { approveChannelPairingCode, listChannelPairingRequests, type PairingChannel, } from "../pairing/pairing-store.js"; import { formatDocsLink } from "../terminal/links.js"; import { theme } from "../terminal/theme.js"; /** Parse channel, allowing extension channels not in core registry. */ function parseChannel(raw: unknown, channels: PairingChannel[]): PairingChannel { const value = ( typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) : "" ) .trim() .toLowerCase(); if (!value) throw new Error("Channel required"); const normalized = normalizeChannelId(value); if (normalized) { if (!channels.includes(normalized as PairingChannel)) { throw new Error(`Channel ${normalized} does not support pairing`); } return normalized as PairingChannel; } // Allow extension channels: validate format but don't require registry if (/^[a-z][a-z0-9_-]{0,63}$/.test(value)) return value as PairingChannel; throw new Error(`Invalid channel: ${value}`); } async function notifyApproved(channel: PairingChannel, id: string) { const cfg = loadConfig(); await notifyPairingApproved({ channelId: channel, id, cfg }); } export function registerPairingCli(program: Command) { const channels = listPairingChannels(); const pairing = program .command("pairing") .description("Secure DM pairing (approve inbound requests)") .addHelpText( "after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/pairing", "docs.clawd.bot/cli/pairing")}\n`, ); pairing .command("list") .description("List pending pairing requests") .option("--channel ", `Channel (${channels.join(", ")})`) .argument("[channel]", `Channel (${channels.join(", ")})`) .option("--json", "Print JSON", false) .action(async (channelArg, opts) => { const channelRaw = opts.channel ?? channelArg; if (!channelRaw) { throw new Error( `Channel required. Use --channel or pass it as the first argument (expected one of: ${channels.join(", ")})`, ); } const channel = parseChannel(channelRaw, channels); const requests = await listChannelPairingRequests(channel); if (opts.json) { console.log(JSON.stringify({ channel, requests }, null, 2)); return; } if (requests.length === 0) { console.log(`No pending ${channel} pairing requests.`); return; } for (const r of requests) { const meta = r.meta ? JSON.stringify(r.meta) : ""; const idLabel = resolvePairingIdLabel(channel); console.log(`${r.code} ${idLabel}=${r.id}${meta ? ` meta=${meta}` : ""} ${r.createdAt}`); } }); pairing .command("approve") .description("Approve a pairing code and allow that sender") .option("--channel ", `Channel (${channels.join(", ")})`) .argument("", "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 channelRaw = opts.channel ?? codeOrChannel; const resolvedCode = opts.channel ? codeOrChannel : code; if (!opts.channel && !code) { throw new Error( `Usage: clawdbot pairing approve (or: clawdbot pairing approve --channel )`, ); } if (opts.channel && code != null) { throw new Error( `Too many arguments. Use: clawdbot pairing approve --channel `, ); } const channel = parseChannel(channelRaw, channels); const approved = await approveChannelPairingCode({ channel, code: String(resolvedCode), }); if (!approved) { throw new Error(`No pending pairing request found for code: ${String(resolvedCode)}`); } console.log(`Approved ${channel} sender ${approved.id}.`); if (!opts.notify) return; await notifyApproved(channel, approved.id).catch((err) => { console.log(`Failed to notify requester: ${String(err)}`); }); }); }