Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

Slack To Laptop

БесплатноНе проверен

Enables Claude Code to interact with Slack via MCP tools, allowing thinking steps, text streaming, status updates, and stream closure in Slack threads.

GitHubEmbed

Описание

Enables Claude Code to interact with Slack via MCP tools, allowing thinking steps, text streaming, status updates, and stream closure in Slack threads.

README

One long-lived local process, three hats: Slack @mention listener (Socket Mode), remote HTTP MCP server that Claude Code worktree jobs call to drive the native Slack stream, and SwiftBar menubar status.

threadTs is the correlation key: the worktree job only ever knows threadTs; this process maps it to the real Slack stream id. The map is snapshotted to ~/.cache/slack-to-laptop/registry.json, so restarting the bridge (deploys, crashes) doesn't interrupt running jobs: on boot, restored streams are force-rotated by the first keepalive tick — rotation doubles as recovery.

Slack app (one-time)

  1. https://api.slack.com/apps → Create New App → From scratch.
  2. Agents & AI Apps feature → toggle ON (grants assistant:write, required for streams + setStatus).
  3. Socket Mode → ON → create app-level token with scope connections:write → that's appToken (xapp-…).
  4. OAuth & Permissions → bot scopes: app_mentions:read, chat:write, assistant:write.
  5. Event Subscriptions → subscribe to bot event app_mention.
  6. Install to workspace → botToken (xoxb-…). Invite the bot to your channel: /invite @YourApp.

Note: Slack docs say setStatus is migrating from assistant:write to chat:write — both scopes above cover either.

Run

mkdir -p ~/.config/slack-trigger
cp config.example.json ~/.config/slack-trigger/config.json
# fill botToken + appToken
bun run server.ts

allowedUserIds (Slack user IDs): when non-empty, only those users can trigger jobs — anyone else gets a threaded "only can trigger me" reply. Empty = anyone in the channel.

With jobCommand: null, mentioning the bot runs a smoke demo: instant thinking checklist, two task cards ticking, "hi", stream closed. That validates the whole Slack side before wiring any jobs.

Wire real jobs

Set jobCommand in the config — a zsh command run per mention with env:

var value
SLACK_THREAD_TS correlation token — pass to every MCP tool call
SLACK_CHANNEL channel id
SLACK_PROMPT mention text, bot mention stripped
SLACK_EVENT_TS unique per mention — use for the worktree name
SLACK_MCP_URL http://127.0.0.1:8365/mcp

Example: "jobCommand": "/Users/you/projects/slack-to-laptop/scripts/launch-job.zsh" — see scripts/launch-job.zsh (machine-specific: herdr + repo hardcoded). It spawns a worktree Claude with prompt /slack-ta [threadTs:$SLACK_THREAD_TS] $SLACK_PROMPT: the token travels inside the prompt, since the job's Claude only sees what the skill receives (see "Job-side skill" below).

Connect the worktree's Claude to the MCP (once, user scope):

claude mcp add --transport http --scope user slack-stream http://127.0.0.1:8365/mcp

MCP tools

All take threadTs (from SLACK_THREAD_TS):

  • register_job({threadTs, cwd, paneId?, pid?, branch?}) — call once at boot; where the session lives, for follow-up routing (paneId = $HERDR_PANE_ID, or $TMUX_PANE on the tmux fallback)
  • thinking_step({threadTs, title, status, id?, details?}) — checklist step; status ∈ pending|in_progress|complete|error; same id/title updates the step
  • append_text({threadTs, markdown}) — prose in the progress message (important mid-course findings only)
  • set_status({threadTs, text}) — grey "is …" line; "" clears
  • finish({threadTs, markdown?}) — settle the progress message + post markdown as its own final-report reply (the user's one notification). Call exactly once, last (again after each follow-up).

Follow-ups

Mentioning the bot again in a job's thread does NOT spawn a second job: the bridge looks the thread up in ~/.cache/slack-to-laptop/jobs.json (written by register_job, survives finish for 7 days), verifies the session's pane still sits in the job's worktree (a pane id alone is not a handle — tmux recycles them, herdr re-issues them on pane move — so cwd is ground truth), and types [slack follow-up threadTs:…] <text> into that Claude session — mid-work it's a steering message, after finish a new turn with full context. A stream is reopened first if needed ("Reconnecting to session…"). If the pane is gone, it falls back to spawning a fresh job with a note. Only the typing is multiplexer-specific (src/inject.ts); finding the session is registration-based and generic.

Multiplexers (herdr + tmux)

Jobs run under herdr; tmux is still read for follow-up routing, so sessions started before the migration stay reachable.

  • Launching (scripts/launch-job.zsh) is herdr-only. It sets HERDR_ENV=1 and runs wt spawn directly — no throwaway window. The tmux version needed one because the monorepo's setup-tmux hook exits silently without $TMUX; herdr's layout drives the herdr server over its socket, so nothing has to run inside a pane. If no server is up (fresh boot) it starts one headless, the counterpart of tmux new-session -d.
  • Follow-ups (src/inject.ts) list both multiplexers and merge the panes; a dead one can't hide the other's. Each pane carries its mux, and the injection dispatches on it: herdr pane send-text + an enter key, or tmux send-keys -l + Enter.
  • herdr labels the recognised agent outright ("agent": "claude"), so no command-name guessing is needed on that side; the tmux heuristic (the claude CLI shows up as its version number) still covers tmux panes.

Two gotchas, both measured:

  • herdr agent prompt does not deliver here — the text never reaches Claude's input box and, without --wait, it reports success anyway. pane send-text does deliver. (Same finding as the monorepo's herdr-layout.local, which passes the prompt as claude's own CLI argument.)
  • The herdr CLI resolves its socket from $HOME, which the SwiftBar-launched bridge can't be assumed to have — so herdrSocket is passed explicitly as HERDR_SOCKET_PATH on every call. herdr status server always exits 0; its state is in the output, not the code.

Future idea (deliberately not built): worktree cleanup on job end conflicts with follow-ups — the kept-alive session is what makes them possible. Cleanest shape: an explicit "cleanup" follow-up telling the session itself to remove its worktree and exit.

A job that dies without finish gets swept: streams idle > staleStreamMinutes are stopped and cleared.

Slack hard-kills a stream ~5:00 after it opens, no matter what is appended (undocumented; measured — a true keepalive is impossible, even with changing content), and there is no way to re-stream onto an existing ts. Each job therefore gets ONE progress message: it streams natively while young (full card UI), is stopped cleanly at age ~3.5–4.5 min, and is edited in place via chat.update from then on (works on stopped streamed messages — measured). Conversion keeps the NATIVE cards: task_card is a real Block Kit block (changelog 2026-02-11), accepted by chat.update even on a stopped stream (measured) — the message re-renders from the replay log with identical card UI. No splits, no dup cards, no pings, no visual downgrade. The final report is the only other message — posted on finish(markdown), one notification, exactly when you want it.

Block-form quirks vs the chunk form (measured): task_card blocks REQUIRE status and reject "pending" (enum in_progress|complete|error) — pending steps simply aren't rendered yet; the plan block's title must be a plain string, not a plain_text object.

API gotchas (measured): chat.stopStream with markdown_text only works on a stream with NO chunks appended — streaming_mode_mismatch otherwise (the report is delivered as a chunkless stream + markdown-stop for the native agent look). Frozen in_progress cards render with a ⚠️ — finish completes them before its plain stop.

Job-side skill

Don't make each job improvise the streaming protocol — give your agent a skill (e.g. ~/.claude/skills/slack-ta/) that owns it. The launch command passes the correlation token inside the prompt (/slack-ta [threadTs:…] <task>); the skill should: extract threadTs and pass it to every slack-stream MCP call, open a thinking_step immediately, update the checklist at real milestones only, append_text for the final summary, and ALWAYS finish — also on failure. Two error rules worth copying: if the token is missing, do the work but skip streaming; if a call errors with "no live stream", the stream was swept — continue the work, stop streaming.

GET /healthz lists active streams.

Build your own

Want the same thing but different? The architecture is small enough to rebuild in an afternoon — here's the TL;DR to hand your agent (or read yourself).

The shape. One long-lived local process with three roles:

  1. Listener — Slack Socket Mode (@slack/bolt), subscribed to app_mention. On mention: open the progress message instantly (so the user sees life before any job boots), then spawn the job however you like.
  2. Bridge — a local HTTP MCP server (@modelcontextprotocol/sdk, stateless transport). The job's agent calls 5 tools: register_job, thinking_step, append_text, set_status, finish.
  3. Status — optional (here: SwiftBar menubar). Any observer works; GET /healthz is the hook.

The one design trick: the job never holds Slack credentials or message ids. It only knows threadTs — a correlation token passed inside its prompt — and the bridge maps it to the real channel/message and owns the token. Any runner (herdr, tmux, container, CI, SSH) works as long as the token rides along and the runner can reach 127.0.0.1:8365.

The Slack rendering strategy (the hard-won part — all measured, none documented; see the section above for detail):

  • Native streams (chat.startStream) look great but die ~5:00 in, no keepalive possible, no re-stream onto the same ts.
  • chat.update works on stopped streamed messages, edits never ping.
  • task_card is a real Block Kit block — chat.update can render the SAME native card UI forever. Quirks: status required, "pending" rejected, plan.title must be a plain string.
  • ⇒ one message per job: stream while young, convert to edit-in-place at ~3.5 min, keep native cards throughout. Final report = separate message = the single notification.

State: one JSON map threadTs → {messageTs, mode, replayLog} snapshotted to disk — that's what makes bridge restarts invisible to running jobs (replay log re-renders the whole message). A second file maps threadTs → session location so re-mentions in a thread route INTO the running session (here: herdr pane send-text or tmux send-keys, cwd-verified) instead of spawning a duplicate.

Reliability floor: dedupe redelivered Slack events (3s ack window); a stale sweep for jobs that die without finish; every Slack write has a fallback chain ending in plain chat.postMessage.

Adaptation points — each is one file here: how jobs launch (scripts/launch-job.zsh — swap for docker/ssh/whatever), how follow-ups reach a session (src/inject.ts — the only multiplexer-specific code), what the agent streams (your job-side skill/prompt: milestones as thinking_step, summary in finish(markdown), ALWAYS finish — also on failure).

SwiftBar

cp swiftbar/slack-to-laptop.sh ~/Library/Application\ Support/SwiftBar/Plugins/  # or your plugin dir
chmod +x .../slack-to-laptop.sh

Streamable plugin: SwiftBar owns the process lifecycle; menubar shows 🛰️ + active stream count. Don't also run bun run server.ts manually (port collision). Test from a terminal first: ./swiftbar/slack-to-laptop.sh — you should see blocks starting with ~~~. Logs: ~/.cache/slack-to-laptop/server.log (written by the server itself — NEVER add a stderr redirect to the plugin script: closing SwiftBar's stderr pipe makes it spin at 100%+ CPU).

SwiftBar does NOT respawn the process if it dies. To restart the bridge: open -g "swiftbar://refreshplugin?name=slack-to-laptop" (or SwiftBar menu → plugin → Refresh). If the 🛰️ icon is missing but the process runs, check defaults read com.ameba.SwiftBar for "NSStatusItem VisibleCC slack-to-laptop.sh" = 0 and write it back to -bool true.

from github.com/ArnaudRinquin/slack-to-laptop

Установка Slack To Laptop

У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.

▸ github.com/ArnaudRinquin/slack-to-laptop

FAQ

Slack To Laptop MCP бесплатный?

Да, Slack To Laptop MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для Slack To Laptop?

Нет, Slack To Laptop работает без API-ключей и переменных окружения.

Slack To Laptop — hosted или self-hosted?

Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.

Как установить Slack To Laptop в Claude Desktop, Claude Code или Cursor?

Открой Slack To Laptop на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare Slack To Laptop with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории communication