diff --git a/CHANGELOG.md b/CHANGELOG.md index 709d7f6df..ca352b8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai - Discord: restore model picker back navigation when a provider is missing and document the Discord picker flow. (#21458) Thanks @pejmanjohn and @thewilloftheshadow. - Gateway/Pairing: tolerate legacy paired devices missing `roles`/`scopes` metadata in websocket upgrade checks and backfill metadata on reconnect. (#21447, fixes #21236) Thanks @joshavant. - Gateway/Pairing/CLI: align read-scope compatibility in pairing/device-token checks and add local `openclaw devices` fallback recovery for loopback `pairing required` deadlocks, with explicit fallback notice to unblock approval bootstrap flows. (#21616) Thanks @shakkernerd. +- CLI/Pairing: default `pairing list` and `pairing approve` to the sole available pairing channel when omitted, so TUI-only setups can recover from `pairing required` without guessing channel arguments. (#21527) Thanks @losts1. - Auth/Onboarding: align OAuth profile-id config mapping with stored credential IDs for OpenAI Codex and Chutes flows, preventing `provider:default` mismatches when OAuth returns email-scoped credentials. (#12692) thanks @mudrii. - Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek. - Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow. diff --git a/src/cli/pairing-cli.test.ts b/src/cli/pairing-cli.test.ts index 424ca84d8..81dd81368 100644 --- a/src/cli/pairing-cli.test.ts +++ b/src/cli/pairing-cli.test.ts @@ -163,6 +163,15 @@ describe("pairing cli", () => { expect(listChannelPairingRequests).toHaveBeenCalledWith("zalo"); }); + it("defaults list to the sole available channel", async () => { + listPairingChannels.mockReturnValueOnce(["slack"]); + listChannelPairingRequests.mockResolvedValueOnce([]); + + await runPairing(["pairing", "list"]); + + expect(listChannelPairingRequests).toHaveBeenCalledWith("slack"); + }); + it("accepts channel as positional for approve (npm-run compatible)", async () => { mockApprovedPairing(); @@ -199,4 +208,20 @@ describe("pairing cli", () => { accountId: "yy", }); }); + + it("defaults approve to the sole available channel when only code is provided", async () => { + listPairingChannels.mockReturnValueOnce(["slack"]); + mockApprovedPairing(); + + await runPairing(["pairing", "approve", "ABCDEFGH"]); + + expect(approveChannelPairingCode).toHaveBeenCalledWith({ + channel: "slack", + code: "ABCDEFGH", + }); + }); + + it("keeps approve usage error when multiple channels exist and channel is omitted", async () => { + await expect(runPairing(["pairing", "approve", "ABCDEFGH"])).rejects.toThrow("Usage:"); + }); }); diff --git a/src/cli/pairing-cli.ts b/src/cli/pairing-cli.ts index f028b08fc..6974663bd 100644 --- a/src/cli/pairing-cli.ts +++ b/src/cli/pairing-cli.ts @@ -68,7 +68,7 @@ export function registerPairingCli(program: Command) { .argument("[channel]", `Channel (${channels.join(", ")})`) .option("--json", "Print JSON", false) .action(async (channelArg, opts) => { - const channelRaw = opts.channel ?? channelArg; + const channelRaw = opts.channel ?? channelArg ?? (channels.length === 1 ? channels[0] : ""); if (!channelRaw) { throw new Error( `Channel required. Use --channel or pass it as the first argument (expected one of: ${channels.join(", ")})`, @@ -120,9 +120,20 @@ export function registerPairingCli(program: Command) { .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) { + const defaultChannel = channels.length === 1 ? channels[0] : ""; + const usingExplicitChannel = Boolean(opts.channel); + const hasPositionalCode = code != null; + const channelRaw = usingExplicitChannel + ? opts.channel + : hasPositionalCode + ? codeOrChannel + : defaultChannel; + const resolvedCode = usingExplicitChannel + ? codeOrChannel + : hasPositionalCode + ? code + : codeOrChannel; + if (!channelRaw || !resolvedCode) { throw new Error( `Usage: ${formatCliCommand("openclaw pairing approve ")} (or: ${formatCliCommand("openclaw pairing approve --channel ")})`, );