Skip to content

feat: add 'messages command' to execute slash commands and capture ephemeral replies (browser auth) #50

Description

@Eduard-Kuzhyr

Motivation

Slack slash commands like /genie, /jira, /giphy, and many internal team commands respond ephemerally — only the invoker sees the reply, and there is no public Web API to drive them. Today slackcli can send messages and add reactions but cannot execute a slash command or read what comes back, which blocks any AI agent or shell script that wants to interact with these flows.

Browser-auth (xoxc + xoxd) already gives us everything the Slack desktop client uses — the missing piece is wiring up the undocumented chat.command endpoint plus the MS WebSocket where the ephemeral reply arrives. With those in place we can offer a clean CLI verb that returns either rendered output or JSON.

Proposal — CLI shape

slackcli messages command \
  --recipient-id <C... | U...> \
  --command /genie \
  [--text "args"] \
  [--timeout 15] \
  [--workspace <id|name>] \
  [--json]
  • Requires browser auth. Standard xoxb/xoxp tokens get an early error pointing the user at slackcli auth login-browser.
  • --recipient-id accepts either a channel ID or a user ID (DM is opened automatically — same convention as messages send / messages draft).
  • Exit codes: 0 reply captured (or timed out but a synchronous body was returned), 1 execution / arg error, 2 no ephemeral reply within --timeout.
  • --json emits { channel_id, command, text, timed_out, messages } for scripting.
  • Default human output renders the captured message(s) through the existing formatMessage pipeline so block-kit and legacy attachments show up in the terminal.

Implementation notes

To keep the new command action small and testable, the work is split across a few focused modules:

  • src/lib/slack-client.tsexecuteSlashCommand (calls chat.command with disp + client_token) and rtmConnect (returns the wss-primary.slack.com URL plus the Cookie/Origin/User-Agent headers required for the WebSocket upgrade). The public rtm.connect endpoint returns a LEGACY_BOT URL that rejects browser sessions, hence the manual URL build.
  • src/lib/ephemeral-capture.ts — pure helpers: matchesEphemeral (correlate frame to invocation via is_ephemeral / subtype / client_msg_id / self-targeted), payloadToMessage, syncResponseToMessage.
  • src/lib/slash-command-runner.ts — pure orchestrator. Opens the socket via an injectable factory, waits for hello, invokes the command, dispatches subsequent frames through matchesEphemeral, runs a single cleanup() on every settle path (success, timeout, error, invoke rejection).
  • src/lib/block-renderer.ts — block-kit + legacy-attachment renderer extracted from formatter.ts so slash-command replies (header / section / context / divider / rich_text / attachments) render in the terminal.
  • src/lib/recipient.tsresolveRecipientChannel helper consumed by send / draft / command to remove three copies of the if (id.startsWith('U')) openConversation block.

Tests

Unit tests for each module:

  • block-renderer.test.ts — coverage per block type (header, section text+fields, context, divider, rich_text grammar, attachments fields/title_link/footer/nested blocks).
  • slash-command-runner.test.ts — fake SocketLike factory; hello → invoke, sync-only timeout, ephemeral capture, invoke rejection, error event, frame filtering.
  • ephemeral-capture.test.ts — match matrix and payload conversion.
  • slack-client.test.ts — pins chat.command params and the wss-primary URL + Cookie/Origin shape.
  • recipient.test.ts — channel passthrough vs DM open.

Notes / risk

chat.command and wss-primary.slack.com are undocumented surfaces used by the desktop client. The unit tests pin the URL and header shape so regressions are greppable when Slack tweaks query params or hostnames.

Status

Working implementation is ready locally. Happy to open a PR linked to this issue once it is acknowledged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions