# πŸŒ‰ cursor-chat-bridge (Telegram, Discord, GitHub) ### Drive the **Cursor** agent from your phone β€” over Telegram, Discord, or GitHub. Say _"start remote chat mode"_ (in any language) and Cursor posts a summary + question to a per-conversation thread at the end of every turn, waits for your reply, and **auto-continues** β€” looping until you stop it. Step away from the keyboard; keep shipping from your phone. [![npm version](https://img.shields.io/npm/v/cursor-telegram-chat?style=for-the-badge&color=cb3837&logo=npm)](https://www.npmjs.com/package/cursor-telegram-chat) [![GitHub stars](https://img.shields.io/github/stars/udah1/cursor-chat-bridge?style=for-the-badge&logo=github&color=f5c518)](https://github.com/udah1/cursor-chat-bridge/stargazers) [![license](https://img.shields.io/npm/l/cursor-telegram-chat?style=for-the-badge&color=blue)](./LICENSE) [![Made for Cursor](https://img.shields.io/badge/built%20for-Cursor-000000?style=for-the-badge)](https://cursor.com) [![npm downloads](https://img.shields.io/npm/dm/cursor-telegram-chat?color=cb3837&logo=npm)](https://www.npmjs.com/package/cursor-telegram-chat) [![node](https://img.shields.io/node/v/cursor-telegram-chat?logo=node.js&logoColor=white)](https://nodejs.org) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org) [![CI](https://img.shields.io/github/actions/workflow/status/udah1/cursor-chat-bridge/publish.yml?branch=master&logo=githubactions&logoColor=white&label=publish)](https://github.com/udah1/cursor-chat-bridge/actions/workflows/publish.yml) [![Glama MCP server](https://glama.ai/mcp/servers/udah1/cursor-chat-bridge/badges/score.svg)](https://glama.ai/mcp/servers/udah1/cursor-chat-bridge) cursor-chat-bridge round-trip flow
A channel per session in the chat app
A channel per session β€” one per Cursor conversation
The agent posts summaries you reply to from your phone
Reply from your phone β€” the agent picks up where it left off

Watch the cursor-chat-bridge demo on YouTube
β–Ά Watch the demo (YouTube)
--- ## Table of contents - [Why](#why) - [Features](#features) - [Channels at a glance](#channels-at-a-glance) - [Quick start](#quick-start) - [How it works](#how-it-works) - [Image attachments](#image-attachments) - [Voice messages (speech-to-text)](#voice-messages-speech-to-text) - [Read receipts (acknowledgments)](#read-receipts-acknowledgments) - [Configuration](#configuration) - [Environment overrides](#environment-overrides) - [The wait loop](#the-wait-loop-stop-hook) - [Push notifications (ntfy)](#push-notifications-ntfy) - [Per-platform setup](#per-platform-setup) - [Writing a new adapter](#writing-a-new-adapter) - [Security](#security) - [Verification status](#verification-status) - [Contributing](#contributing) - [License](#license) --- ## Why You kick off a task in Cursor, then need to leave your desk. Normally the agent stalls the moment it needs a decision. **cursor-chat-bridge** turns any chat app into a remote control: the agent reports back and asks its questions in a thread you can answer from your phone, and it resumes on its own the instant you reply β€” no laptop required. - **A thread per conversation.** Every Cursor chat maps to its own issue / channel / topic β€” even multiple chats in the same workspace stay separate. - **Hands-free loop.** Replies are re-injected automatically; you don't touch Cursor to continue. - **Pluggable channels.** Telegram, Discord, and GitHub Issues today β€” add your own in ~100 lines. - **Safe by default.** Remote replies are treated as untrusted; destructive actions require an explicit confirmation sent back through the thread. - **Proxy-friendly.** GitHub and Discord tunnel through TLS-intercepting corporate proxies. ## Features | Capability | What it does | |---|---| | **Phone-first** | Answer the agent from the GitHub / Discord / Telegram mobile app, with native push. | | **Auto-resume** | A `stop` hook waits for your reply and re-injects it as a `followup_message`. | | **Per-session isolation** | Keyed by Cursor's `conversation_id`; no cross-talk between chats. | | **Long, cheap waits** | One ~60-min blocking window per re-arm β‡’ minimal paid turns while idle. | | **Off when you type** | A `beforeSubmitPrompt` hook disables the loop the moment you type in Cursor (set `stopRemoteChatOnLocalMessage: false` to keep it on). | | **Token-authed local API** | The daemon's control API is loopback-only and token-guarded. | | **Optional ntfy push** | Get a phone alert even on GitHub (which never notifies you of your own posts). | | **Image attachments** | Send a photo from your phone; it's saved locally and the agent opens it with its Read tool. | | **Voice β†’ text** | Optional speech-to-text (OpenAI or local): a voice note reaches the agent as transcribed text. | | **Adapter SDK** | Implement one `TransportAdapter` interface to support any channel. | | **Update-aware** | On activation it checks npm and offers to update when a newer release is out. | ## Channels at a glance | Adapter | Status | Model | Mobile push | |---|---|---|---| | **Telegram** _(default)_ | βœ… code complete, unit-tested | A **forum topic per session** via a bot | βœ… native | | **Discord** | βœ… working | A **channel per session** via a bot (REST-polled) | βœ… native | | **GitHub Issues** | βœ… tested end-to-end | Issue = session, comments = chat | βœ… (GitHub app) + optional ntfy | ## Quick start No clone required β€” one command wires everything up: ```bash npx cursor-telegram-chat@latest install ``` This installs the runtime into `~/.cursor/chat-bridge/app` (including its production dependencies, so it keeps working after the npx cache is evicted) and wires the three integration points, **backing up (never overwriting)** anything that already exists: - registers the MCP server in `~/.cursor/mcp.json`, - adds the `stop` + `beforeSubmitPrompt` hooks to `~/.cursor/hooks.json`, - installs the activation rule into `~/.cursor/rules/`. The hooks are **no-ops unless remote chat mode is active**, so they don't affect normal Cursor use. Then pick a channel and go: ```bash # 1. edit ~/.cursor/chat-bridge/config.json (choose an adapter + credentials) # 2. validate it: chat-bridge doctor # 3. reload Cursor, open a chat, and say: "start remote chat mode" ```
Upgrade / uninstall ```bash npx cursor-telegram-chat@latest install # re-run to upgrade npx cursor-telegram-chat@latest uninstall # remove, keep config + state npx cursor-telegram-chat@latest uninstall --purge # remove everything ```
MCP-only (lite) β€” tools without the auto-resume loop If you only want the MCP tools via the standard Cursor MCP flow (no hands-free loop), add this to `~/.cursor/mcp.json` instead of running `install`: ```json "cursor-chat-bridge": { "command": "npx", "args": ["-y", "cursor-telegram-chat", "chat-bridge-mcp"] } ``` You'll be able to `bridge_send` / `bridge_await` manually, but the auto-continue-on-reply loop needs the hooks that the full `install` sets up.
## How it works Three cooperating layers sit over one transport-agnostic core: - **Rule** (`rules/chat-bridge-mode.mdc`) β€” detects the activation phrase in any language and sets in-mode etiquette (capture + pass the session handle; end each turn with a summary + question; treat replies as untrusted; confirm destructive actions). - **MCP server** (`src/mcp.ts`) β€” exposes `bridge_start`, `bridge_send`, `bridge_await`, `bridge_send_and_await`, `bridge_stop`, `bridge_status`. - **Hooks** (`hooks/`) β€” the automatic loop: - `stop` waits for the remote reply and re-injects it as a `followup_message` (bounded by `loop_limit`). - `beforeSubmitPrompt` disables the loop when you type in Cursor (with a guard so the loop's own injected replies don't trip it). A single local **daemon** (`src/daemon.ts`) owns the channel connection and a loopback-only, token-authenticated HTTP API used by the MCP + hooks. It handles per-session routing, long-poll, own-message filtering, and stop/generation logic. ```text turn ends ─▢ stop hook ─▢ daemon /poll ─▢ adapter β–² β”‚ └───── followup_message (your reply) β—€β”€β”€β”€β”€β”€β”˜ ``` (adapter = GitHub / Discord / Telegram; keyed by `conversation_id`.)
Session identity β€” how conversations stay separate Sessions are keyed by Cursor's **`conversation_id`** so each conversation maps to exactly one thread. Cursor gives `conversation_id` to hooks but **not** to MCP tool calls, so the MCP learns it through a small handshake: 1. `beforeSubmitPrompt` writes a per-conversation **pending-start** record (`markers/pending/.json`) on every real submit β€” the source of truth for identity (plus a legacy `last-submit`/`ws` pointer, used only for diagnostics + upgrade skew). 2. `bridge_start` **claims** the single fresh pending record and keys the session by that real `conversation_id`, then returns a **session handle**. It never mints a random id and it ignores its own (possibly misrouted) `BRIDGE_WORKSPACE` β€” the claim's workspace wins. 3. `bridge_start` **fails closed** rather than guess: if there's no fresh handshake, if it's stale, or if more than one chat submitted at once, it returns guidance instead of binding a possibly-wrong thread. 4. The agent passes `session=` on every subsequent `bridge_*` call β€” now **required** (no recency/cache fallback), so two conversations in the same workspace can never cross threads. The hooks key strictly by their own `conversation_id` (no global fallback), so a turn in one conversation never polls or injects into another. Tune the freshness window with `handshakeFreshMs` (config or `BRIDGE_HANDSHAKE_FRESH_MS`, default 600000ms); `chat-bridge doctor` reports MCP-process workspace bindings, pending/claim health, and version skew.
Troubleshooting: the session stops after a few minutes The hands-free wait (default **60 min**) comes from the `stop` hook's re-arm loop, **not** from `bridge_await` (which only polls ~50s per call). Cursor kills a `stop` hook after a short, undocumented ceiling unless `~/.cursor/hooks.json` sets a large `timeout` β€” the installer sets `timeout: 3660`. If your session stops after a couple of minutes: 1. **Using the MCP-only (lite) setup?** It has no hooks, so there's no auto-resume. Run the full `npx cursor-telegram-chat@latest install`. 2. **Fully quit and reopen Cursor** after installing β€” a reload doesn't always reload `hooks.json`. 3. Confirm `~/.cursor/hooks.json` has a `stop` hook with `timeout: 3660` (re-running the latest `install` fixes an older one). 4. Still killed early on your Cursor build? Shrink each wait window so it re-arms sooner: set `"stopWindowMin": 5` in `~/.cursor/chat-bridge/config.json` (the 60-min total is `stopBudgetMin`).
## Image attachments Send a photo (or an image file) in the chat thread and the agent can see it: 1. The adapter captures the attachment on the message (Discord `attachments`, Telegram `photo` / image `document`). 2. The daemon downloads the bytes to `~/.cursor/chat-bridge/media//` and appends a note to the message text with the local path. 3. The agent opens that path with its **Read** tool β€” so the image reaches the model as vision input. > **Behind a corporate TLS proxy:** Discord's `cdn.discordapp.com` is often blocked while > `media.discordapp.net` is allowed. The Discord adapter automatically rewrites attachment URLs to > the `media` host, so downloads work on such networks. ## Voice messages (speech-to-text) Send a **voice note** (Telegram) or an **audio attachment** (Discord) and the agent receives a text transcription as if you'd typed it β€” **off by default**. Enable it under `stt` in the config: ```jsonc "stt": { "enabled": true, "provider": "openai", // "openai" (OpenAI-compatible via baseUrl) or "local" "tryLocalSttFirst": false,// true = force the local transcriber even if a cloud provider is set "apiKeyCommand": "…", // or "apiKey", or env BRIDGE_STT_API_KEY "language": "auto", // auto-detect, or force "he" / "en" "keepAudio": true // false = delete the audio after transcribing } ``` - **`local` provider** (offline, recommended for sensitive audio): set `localBin`/`localArgs` to a CLI that prints the transcript to stdout (e.g. `whisper.cpp`). The configured `provider` is always respected β€” the bridge never silently falls back to `local`. To force the local transcriber even when a cloud provider is set, use `"tryLocalSttFirst": true` (or `BRIDGE_STT_TRY_LOCAL_FIRST=1`). - Transcription runs **asynchronously** in the daemon (never blocks the poll window); the transcript is delivered on the next reply cycle. See [`docs/stt-plan.md`](docs/stt-plan.md) for the full design. ## Read receipts (acknowledgments) Without this, sending a message from your phone is silent until the agent finishes a whole turn β€” you can't tell whether it arrived at all. Turn it on and the **daemon** replies the moment a message lands, before any slow work (attachment downloads, transcription). **Off by default:** ```jsonc "ack": { "enabled": true, "language": "auto", // "auto" matches you; force with "en" or "he" "adapters": ["discord", "telegram"], // bot-identity channels only (see below) "echoTranscript": true, // repeat back what a voice note said "maxTranscriptChars": 200 } ``` What you get, per message kind: | You send | The bridge replies | |---|---| | text | _Got it, on it…_ | | an image | _Got the image, taking a look…_ | | a voice note | _Got your voice message, transcribing…_ then _Got your request: "…"_ once transcribed | | `stop` | _Remote chat session stopped._ | The ack comes from the daemon, not the agent β€” the agent doesn't even see your message until its next turn begins, so an agent-authored ack would cost an LLM turn and arrive late. On `"auto"` the reply matches the language you wrote in: Hebrew text gets a Hebrew reply, and for a voice note the transcriber's own detected language decides. Messages with nothing to go on (an uncaptioned image, a bare `stop`) reuse the last language you used in that session, defaulting to English. > **Timing:** the daemon only reads the channel while a poll is in flight β€” that is, while the agent > is awaiting a reply or the `stop` hook is waiting. Messages sent while the agent is **mid-turn** > aren't seen (and so aren't acked) until it finishes. They're never lost, just delayed. > **Why not GitHub:** there the agent comments under *your own* account, so an ack would read as you > replying to yourself. You can still opt in by adding `"github"` to `adapters`. ## Configuration `~/.cursor/chat-bridge/config.json` (edits to this file are picked up automatically on the next poll β€” no daemon restart needed; only `mcp.json` env changes and adapter credentials need a restart): ```jsonc { "activeAdapter": "telegram", "pollIntervalMs": 10000, // check for replies every N ms (lower = snappier, more API calls) "minPollIntervalMs": 2000, // floor for the above; lower it if you set pollIntervalMs < 10000 "stopBudgetMin": 60, // wait budget (mins); resets on every reply "stopWindowMin": 60, // mins per window (keep < hooks.json timeout) "caCertPath": "", // corporate CA bundle (PEM) if behind a TLS proxy "requireConfirmForDestructive": true, "stopRemoteChatOnLocalMessage": true, // typing in Cursor turns remote mode off (set false to keep it on) "adapters": { "github": { "owner": "you", "repo": "cursor-bridge-inbox", "tokenCommand": "gh auth token --user you" }, "discord": { "botToken": "", "channelId": "", "allowedUserIds": [] }, "telegram": { "botToken": "", "chatId": "", "allowedUserIds": [] } }, "ack": { "enabled": false, "language": "auto" } // read receipts; see above } ``` > `caCertPath` is usually empty β€” leave it unless things fail. Some corporate networks intercept > HTTPS with their own root cert that Node doesn't trust, so requests fail with `TypeError: fetch > failed` while `curl` works. If you hit that, point `caCertPath` (or `BRIDGE_CA_CERT`) at your > machine's CA bundle (PEM); `doctor`, the daemon, and the update check all honor it. > > ntfy push is **off by default** and isn't part of this file β€” enable it only via `BRIDGE_NTFY_*` > env vars (see below). ## Environment overrides Set these in the `env` block of the `cursor-chat-bridge` entry in `~/.cursor/mcp.json` (or the shell) to override `config.json` without editing it. All namespaced `BRIDGE_*`. A change needs a daemon restart (`chat-bridge shutdown`) to affect a running daemon. | Env var | Overrides | Example | |---|---|---| | BRIDGE_PLATFORM | `activeAdapter` | `github` \| `telegram` \| `discord` | | BRIDGE_POLL_INTERVAL | poll interval (**seconds**) | `30` | | BRIDGE_STOP_BUDGET_MIN | wait budget, mins; resets on reply ΒΉ | `60` | | BRIDGE_STOP_WINDOW_MIN | mins per blocking window Β² | `60` | | BRIDGE_CA_CERT | `caCertPath` | `/path/to/ca.pem` | | BRIDGE_GITHUB_REPO | github `owner/repo` | `you/inbox` | | BRIDGE_GITHUB_TOKEN | github token | `gho_…` | | BRIDGE_TELEGRAM_BOT_TOKEN | telegram bot token | β€” | | BRIDGE_TELEGRAM_CHAT_ID | telegram forum group id | β€” | | BRIDGE_TELEGRAM_ALLOWED_USER_IDS | whitelist (csv) | `123,456` | | BRIDGE_DISCORD_BOT_TOKEN | discord bot token | β€” | | BRIDGE_DISCORD_CHANNEL_ID | discord **channel** id Β³ | β€” | | BRIDGE_DISCORD_ALLOWED_USER_IDS | whitelist (csv) | `123,456` | | BRIDGE_WORKSPACE | per-window session key | `${workspaceFolder}` | | BRIDGE_NTFY_TOPIC | enable ntfy + set topic | `cursor-bridge-…` | | BRIDGE_NTFY_PRIORITY | push priority 0–5 (0 = off) | `2` | | BRIDGE_NTFY_SERVER | ntfy server base URL | `https://ntfy.sh` | ΒΉ Also `stopBudgetMin` in `config.json` β€” the reliable knob, since the hook doesn't inherit the MCP entry's env. Β² Also `stopWindowMin` in `config.json`. Keep below the `stop` hook `timeout` in `~/.cursor/hooks.json`. Β³ Any existing text channel in your server; used to locate the server + category β€” a fresh channel is created per session alongside it (For example #General). Per-conversation platform can also be chosen at runtime: say _"start remote chat in Telegram"_ and the agent passes `bridge_start(adapter: "telegram")` for that conversation only. ## The wait loop (stop hook) While remote mode is active, the `stop` hook blocks at the end of each turn waiting for your reply. Two knobs control it: - **`stopWindowMin`** (default **60**) β€” how long a single hook invocation blocks before it returns a silent keep-alive and re-arms. Cursor caps `stop`-hook runtime at the `timeout` in `~/.cursor/hooks.json` (default **3660s / 61 min**); probing showed no hidden cap below that, so one ~60-min window means just **one paid keep-alive turn per hour** while you're away. - **`stopBudgetMin`** (default **60**) β€” the total time to keep waiting across re-arms. It **resets on every reply**, so it's really "keep waiting up to N minutes since your last message." The loop ends when you reply, type in Cursor, send `stop` in the thread, or call `bridge_stop`. ## Push notifications (ntfy) GitHub never notifies you about your **own** activity β€” and the agent posts as _you_ (self-mentions and self-assignment don't notify either). So to get a phone alert on the GitHub channel without a second account, cursor-chat-bridge can fire an out-of-band push via [ntfy](https://ntfy.sh) on every summary. It's free, account-less, open-source, self-hostable, and deep-links to the issue. It's **off by default**. Enable it via env on the MCP entry: 1. Install the **ntfy** app (iOS/Android) or use the web app. 2. Pick a long, unguessable topic (topics are public-by-obscurity) and **subscribe** to it. 3. Set `BRIDGE_NTFY_TOPIC=cursor-bridge-` in the `env` block of the `cursor-chat-bridge` entry in `~/.cursor/mcp.json`. `BRIDGE_NTFY_PRIORITY` is the on/off dial: **0 = off (default)**, 1=min … 5=max. A push is sent only when priority β‰₯ 1 **and** a topic is set. Pushes are skipped for **Telegram** and **Discord**, which already notify natively. ## Per-platform setup
Telegram β€” default, best chat UX 1. Create a bot with **@BotFather** β†’ bot token. 2. Create a group, enable **Topics**, add the bot as admin with _Manage Topics_. 3. Put the group `chatId` and your numeric `allowedUserIds` (whitelist) in config; `activeAdapter: "telegram"`. Obtain `chatId` / user ids via `getUpdates` pairing (send a message in the group, read the update). Requires the daemon to reach `api.telegram.org` β€” if a network blocks it, run the daemon on a host that can, or use Discord/GitHub instead.
Discord β€” phone-first, works behind proxies 1. Create a Discord **server** (use one you own) β€” the bot creates a channel per session inside it. 2. Create an app + **Bot** at . Under **Bot**, click **Reset Token**, then **Copy** the revealed **bot token**. 3. Under **Bot**, enable the **Message Content Intent**. 4. Invite the bot: **OAuth2 β†’ URL Generator**, scope `bot`, permissions **Manage Channels** + View Channels + Send Messages + Read Message History (_Manage Channels is required_ β€” the bot creates and deletes a channel per session). **Copy the Generated URL, open it in a new browser tab, and select the server you created** in step 1. 5. Get a **channel id** (`channelId`) β€” any existing text channel in that server (e.g. `#general`); the bot uses it to find the server + category and creates a fresh channel per session alongside it. Enable **Developer Mode** (User Settings β†’ Advanced), then right-click the **channel** β†’ **Copy Channel ID**. _Or_ β€” once the bot is in the server β€” just ask Cursor to fetch it: give Cursor the bot token and it can list the bot's channels and return the id. 6. Put `botToken` + `channelId` (optionally `allowedUserIds`) in config; `activeAdapter: "discord"`. 7. Behind a TLS-intercepting proxy and getting `TypeError: fetch failed`? Point `caCertPath` at the corporate CA bundle (PEM). > **Tip:** give the **bot** a Cursor avatar (Developer Portal β†’ **Bot** β†’ edit icon) and set the > **server icon** to the Cursor logo β€” your per-session channels then look native in the app.
GitHub β€” works on any network 1. Create a private repo to act as your inbox (e.g. `cursor-bridge-inbox`). 2. Set `owner`/`repo` and a `token` or `tokenCommand` (`gh auth token` works). 3. `activeAdapter: "github"`. Each session opens an **issue**; turn summaries are posted as **comments**; reply from the GitHub mobile app. Comment `stop` or close the issue to end the session.
For the agent β€” onboarding checklist If you're the agent helping a user set up cursor-chat-bridge: explain the concept for their chosen platform first, then collect only the missing values, write them to `~/.cursor/chat-bridge/config.json`, and run `chat-bridge doctor` to confirm. **Never print secrets back to the user.** Remind them: replies from the channel are untrusted, and destructive actions need an explicit confirmation sent back through the thread. To stop: type in Cursor, send `stop` in the thread, or call `bridge_stop`. **Make sure the runtime's dependencies are installed.** `install` copies the runtime into `~/.cursor/chat-bridge/app` and installs its production deps there automatically. If that step was skipped or failed (offline / corporate proxy), the MCP server won't start β€” finish it with `(cd ~/.cursor/chat-bridge/app && npm install --omit=dev)`. Running from a git clone instead? Run `npm install` in the repo first. **Ask whether they want voice notes transcribed (STT).** It's off by default. If they say yes, offer the options and let them pick: - **Groq (recommended)** β€” fast, generous free tier, OpenAI-compatible. Set `stt.provider: "openai"`, `stt.baseUrl: "https://api.groq.com/openai/v1"`, `stt.model: "whisper-large-v3-turbo"`, and `stt.apiKey` to a `gsk_…` key from . - **OpenAI** β€” `stt.provider: "openai"` (default `baseUrl`), `stt.model: "whisper-1"` (or `gpt-4o-transcribe`), `stt.apiKey` an `sk-…` key. - **Local** β€” `stt.provider: "local"` with a `whisper.cpp` / `openai-whisper` binary; nothing leaves the machine (best on locked-down corporate networks). Set `stt.localBin`. Then set `stt.enabled: true` and run `chat-bridge doctor`. Prefer `stt.apiKeyCommand` over an inline key where possible, and **never print the key back to the user**.
## Writing a new adapter Implement `TransportAdapter` (`src/types.ts`) and register it in `src/adapters/index.ts`: ```ts interface TransportAdapter { capabilities: { globalIngest: boolean; separateBotIdentity: boolean }; init(): Promise; ensureThread(sessionId: string, title: string, meta?: object): Promise; send(thread: ThreadRef, text: string): Promise<{ messageId: string }>; // pull adapters (GitHub / Discord): poll?(thread: ThreadRef, cursor?: string): Promise; // push / global adapters (Telegram): startIngest?(router: Router): Promise<() => void>; stop?(thread: ThreadRef): Promise; } ``` | Member | Required | Purpose | |---|---|---| | `capabilities` | βœ… | `globalIngest`: one stream for all sessions (Telegram) vs per-thread polling. `separateBotIdentity`: posts appear as a bot, not you. | | `init()` | βœ… | Validate credentials + connectivity. | | `ensureThread()` | βœ… | Create/lookup the per-session thread/channel; returns a `ThreadRef`. | | `send()` | βœ… | Post a message (handle the platform's length limits / chunking). | | `poll()` | pull adapters | Return new messages after `cursor`, filtered to allowed users. | | `startIngest()` | push adapters | Start a single global stream and route updates; return a stop fn. | | `stop()` | optional | Clean up (e.g. Discord deletes its per-session channel). | ## Security - The loopback control API is **token-authenticated** β€” only local processes with the token (the MCP + hooks) can drive the daemon. - Inbound messages are filtered by an `allowedUserIds` whitelist (Telegram/Discord). - Every remote reply is wrapped and marked **untrusted**; the rule forbids destructive actions without an explicit confirmation sent back through the thread. - Tokens live in `~/.cursor/chat-bridge/config.json` (chmod 600) and are never committed. Prefer `tokenCommand` over a stored token where possible. ### Permissions β€” what the daemon needs (and why scanners flag it) Driving an agent from a chat app inherently needs some powerful capabilities. This package uses: - **Child processes** β€” to spawn the daemon, run your `tokenCommand` (e.g. `gh auth token`), the local `whisper` binary (only if you choose local STT), and `npm install` inside the installer. - **Network access** β€” a loopback-only control server plus outbound calls to your chosen channel (Telegram/Discord/GitHub), speech-to-text upload (if enabled), and an npm update check. - **Filesystem access** β€” read/write `~/.cursor/chat-bridge/` for config, media, and logs. Because of these, supply-chain scanners (Socket, Snyk, etc.) show a lower **Supply Chain Security** score than a pure-logic library β€” even though there are **no known vulnerabilities**. That score reflects *capability + package age* (this project is new, single-maintainer), not a detected exploit. For transparency: there is **no `postinstall`/`install` script** β€” the only lifecycle script is `prepublishOnly` (a build that runs on the publisher's machine, never on your install). Everything network/filesystem/process happens only when **you** run the daemon, and the control API is loopback-only and token-guarded. The source is MIT and auditable in this repo. ## Verification status Verified end-to-end on-machine (no Cursor restart needed): - **GitHub adapter** β€” create issue, send, poll, own-message filtering, `stop` keyword, close-detection. - **Daemon** β€” token auth, long-poll, stop/generation, persistence. - **MCP server** β€” all tools over a real stdio JSON-RPC handshake. - **Hooks** β€” stop-loop `followup_message` injection, before-submit off-switch + injection guard, instant no-op when inactive. - **Per-conversation routing** (`node scripts/e2e-conv.mjs`) β€” distinct conversations open distinct issues; two conversations in the same workspace stay separate; re-activating a stopped session opens a fresh thread; unconfigured channels return onboarding guidance. - **Unit tests** β€” `npm test` (routing, store semantics, message filtering). ## Contributing Issues and PRs welcome. Local dev: ```bash npm install npm run build # tsc β†’ dist/ npm run typecheck npm test # node --test npm run dev:daemon # run the daemon from source (tsx) ``` New channels are the easiest contribution β€” implement one `TransportAdapter` (see above). ## License [MIT](./LICENSE)