Repository navigation
Channels (Slack/Telegram/Discord/WhatsApp) + app bridge - #120
Merged
Merged
Conversation
New workspace package @redrob-code/channels-core: the transport contract, channels.json load/save, a 0600 file secret store behind SecretStore, a deny-by-default allowlist, a conversation->session dispatcher persisted in channels/sessions.json, a markdown-to-text renderer with chunking, and the ChannelManager that starts enabled transports and attaches each to the daemon as channel:<name>.
redrob serve --register starts every enabled channel from channels.json through the (still empty) channelFactories registry, turns idle exit off while any transport runs, and stops them on the way out. The server gains GET /channels/status and POST /channels/reload behind a controller seam the serve command fills in, following the clients group pattern.
list | status | enable | disable | allow | set-secret | slack-manifest. Edits channels.json and the 0600 secrets file, then asks a running registered daemon to POST /channels/reload; without one the change waits for the next start. slack-manifest prints the Socket Mode app manifest, or with --url the api.slack.com link that creates the app from it.
Files, in-chat commands, daemon behaviour, the two endpoints, the CLI and the Transport contract Stage 2 transports implement.
bun.lock learns the new workspace package. The manager's daemon attachment
passes the SDK the {url, headers} it actually reads.
A desktop app registers its loopback MCP server with POST /v1/apps/register, heartbeats it, and the daemon drops it when the heartbeats stop (same 90s TTL as /api/clients). Only loopback mcp_url is accepted. Each registration holds a client attachment so the idle-exit clock does not run while an app is bridged; the serve tick sweeps apps before clients.
The MCP service seeds every new instance with the apps already registered and subscribes to the registry so a registration connects app:<name> on every live instance and an unregistration (or TTL expiry) disconnects it and drops its tools. Adds remove(name) to the service. Runtime only: the user's config file is never written.
JS SDK (@redrob-labs/sdk/daemon): apps.register/heartbeat/unregister/list and bridgeApp() that registers and heartbeats at ttl/3 until stop(). Rust (redrob-engine-client): register_app/heartbeat_app/unregister_app/apps and bridge_app() with a heartbeat thread that re-registers on 404. CLI: redrob apps list [--json] and redrob apps unregister <id>.
docs/APP-BRIDGE.md: endpoints and payloads, TTL and heartbeat, the loopback rule, the app:<name> MCP naming, JS and Rust examples, the CLI. A pointer section in LOCAL-ENGINE-API.md.
@redrob-code/channels-telegram: TelegramTransport + telegramTransport factory. Chat id is the conversation, user id the sender; groups answer only mentions and replies; typing indicator; 4096 max chunk. Secret telegram.botToken documented in the package README and the CHANNELS.md secrets table.
New workspace package @redrob-code/channels-discord. One Discord channel (DM, guild channel or thread) is one conversation; the sender is the author's user id. DMs are always delivered, guild messages only when the bot is mentioned, with the mention stripped. maxChunk 2000, typing via sendTyping(), gateway reconnects left to discord.js, start() rejects on a bad token. Secret: discord.botToken. Documents the Message Content privileged gateway intent in the package README and docs/CHANNELS.md, and adds the secrets table there.
…e daemon A Rust Redrob app calls AppBridge::start with its tools: a minimal MCP streamable-HTTP server (initialize, ping, tools/list, tools/call; notifications 202) comes up on 127.0.0.1 at a free port on a std thread, registers through redrob-engine-client's bridge_app (heartbeat and re-register included) and unregisters on stop or drop. No async runtime so egui apps can call it directly. README with an example; a short "Using redrob-app-bridge (Rust)" section in docs/APP-BRIDGE.md.
startAppBridge serves ToolDefs over streamable-HTTP MCP on 127.0.0.1 (port 0, one McpServer per session so tools/list_changed can be pushed), bridges the URL into the daemon via @redrob-labs/sdk/daemon connectOrStart + bridgeApp, and unregisters on stop(). zod or JSON Schema inputs. README with example and a Using section in docs/APP-BRIDGE.md.
New workspace package @redrob-code/channels-whatsapp. Pairs as a linked device via QR (no secret); start() resolves while unpaired and the QR is exposed through qr()/qrDataUrl() and written to channels/whatsapp/qr.png. conversationId = remoteJid, senderId = phone number; fromMe and status broadcasts dropped; maxChunk 4000; composing presence as typing().
Move packages/slack (Bolt 3, Socket Mode) into packages/channels/slack as a Transport for channels-core. One Slack thread is one conversation (`<channel>:<thread_ts ?? ts>`), replies go in-thread, app_mention and message.im/channels are handled with bot_id/subtype dropped and mentions de-duplicated. Tokens come from the channel secret store (slack.botToken, slack.appToken), never the environment. The Slack app manifest YAML now lives in this package (manifestYaml, manifestUrl). The old standalone bot and its workspace entry are removed. The CLI's slack-manifest command keeps its own copy of the manifest for now: pointing it at this package needs a dependency in packages/redrob/package.json, which the parent wires after merge.
…nels-app-bridge # Conflicts: # bun.lock # docs/CHANNELS.md
…els-app-bridge # Conflicts: # bun.lock # docs/CHANNELS.md
…nels-app-bridge # Conflicts: # bun.lock # docs/CHANNELS.md
…nnels-app-bridge # Conflicts: # bun.lock
The daemon now wires all four stage-2 transports into channelFactories and depends on their workspace packages; bun.lock regenerated after the merges.
daemon/autostart.ts registers a systemd user unit, a launchd agent or a Task Scheduler ONLOGON task running `redrob serve --register --idle-exit 0` with the installing binary. `redrob channels enable` installs it on the first enabled channel and `disable` removes it when none is left; `redrob service install|uninstall|status` are the by-hand versions.
Apps outside this repo install the bridge from npm next to @redrob-labs/sdk, so the package takes the published scope and drops private.
This reverts commit bfd2dd9.
Drop push triggers from test, typecheck, engine-client and gitflow and the weekly schedule from upstream-sync. A merge is a pull request that already passed. Document that packages/app-bridge is vendored by apps, not published.
…new routes The generated client lagged the protocol (channels.status, channels.reload, apps.list/register/heartbeat/unregister), which failed check:generated, and the HttpApi exerciser flagged the same six routes as missing scenarios.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's done
@redrob-code/channels-core: Transport, Allowlist, Dispatcher (one conversation = one session,/new), Renderer, config at~/.redrob/channels.json, 0600 file secret store.packages/slack), Telegram (grammY long-poll), Discord (discord.js gateway), WhatsApp (Baileys QR pairing).redrob serve, disables idle exit while a channel is on,POST /channels/reload,GET /channels/status.POST /v1/apps/register, heartbeat,DELETE /v1/apps/:id,GET /v1/apps; loopback only; registered apps appear as runtime MCP serversapp:<name>and keep the daemon alive.packages/app-bridge(TS) andcrates/redrob-app-bridge(Rust, loopback MCP server + register/heartbeat),engine-client::bridge_app().redrob service install|uninstall(systemd user / launchd / schtasks ONLOGON); firstchannels enableregisters, lastdisableunregisters.redrob channels list|status|enable|disable|allow|set-secret|slack-manifest --url,redrob apps list|unregister.docs/CHANNELS.md,docs/APP-BRIDGE.md.What's not done
What to review
channels-core./v1/appsloopback guard and the idle-exit interaction indaemon.autostart.tsplatform branches.Intentions
Phone (Slack/Telegram/Discord/WhatsApp) -> Code daemon -> any registered Redrob app, with no relay server: each user runs their own bot app, tokens stay on their machine.