npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mporenta/pi-discord-remote

v0.2.5

Published

Pi extension: bidirectional Discord remote control for a local Pi coding-agent session.

Readme

discord-remote

Bidirectional Discord remote control for a local Pi coding-agent session. Your Pi instance keeps running on the laptop; you talk to it from Discord.

  • Session threads: every Pi session gets a Discord thread named with the Pi session ID (for example pi-<session-id>), and all session traffic is routed to that thread.
  • Outbound: every assistant message streams to the active Discord thread as the tokens arrive. Tool calls and tool results render as concise Discord embeds.
  • Inbound: messages you type in the allowlisted channel or active session thread inject into Pi via pi.sendUserMessage(...). Mid-turn messages steer the running turn; idle-state messages queue as a normal user turn.
  • Discord commands: authorized Discord users can run Discord-side Pi commands such as /commands, /session, /compact, /abort, and /new. The bot can optionally register these bridge controls as guild-scoped Discord app slash commands. Because Pi only exposes session replacement from command handlers, run /discord-arm in the active local Pi session to enable /new from Discord. Re-run it after local session changes; Discord-created sessions keep the bridge armed.

⚠️ Security

Allowlisted Discord users get effectively the same access to your machine as sitting at the keyboard. Pi will gladly run bash, edit files, and call any configured provider's APIs. Treat your bot token and user allowlist with the same care as an SSH key:

  • Use a fresh, private Discord application dedicated to this. Do not reuse a bot that's in any public server.
  • Put the bot in a private guild with only you (and trusted users) in it.
  • Set PI_DISCORD_USER_IDS to the Discord user IDs that may drive Pi. The extension refuses to start with an empty allowlist.
  • Never commit .env. The repo's .gitignore already excludes it.
  • The extension also refuses to start without a bot token, channel allowlist, and user allowlist. Prefer PI_DISCORD_BOT_TOKEN, PI_DISCORD_CHANNEL_IDS, and PI_DISCORD_USER_IDS; legacy DISCORD_* fallback names are disabled unless PI_DISCORD_ALLOW_LEGACY_ENV=true.

Setup

1. Create a Discord bot

  1. Go to https://discord.com/developers/applications, click New Application, name it pi-remote (or whatever).
  2. In the sidebar, open Bot. Click Reset Token and copy the token — this is PI_DISCORD_BOT_TOKEN.
  3. Still on the Bot page, scroll to Privileged Gateway Intents and enable Message Content Intent. Save. (Reactions and typing use the default non-privileged intents — no extra toggle required.)
  4. In the sidebar, open OAuth2 → URL Generator. Check bot and applications.commands under scopes; under bot permissions, check View Channels, Send Messages, Read Message History, Add Reactions, Embed Links, Create Public Threads, Send Messages in Threads, and Manage Threads if you want the bot to unarchive old session threads. Copy the generated URL, open it in a browser, and invite the bot to your private guild.

2. Find IDs

Enable Discord's Developer Mode (Settings → Advanced → Developer Mode). Then:

  • Right-click the target channel → Copy Channel IDPI_DISCORD_CHANNEL_IDS.
  • Right-click your own avatar → Copy User IDPI_DISCORD_USER_IDS.

3. Configure .env

Copy .env.sample to .env (project root) and fill in:

PI_DISCORD_ENABLED=true
PI_DISCORD_BOT_TOKEN=<your bot token>
PI_DISCORD_CHANNEL_IDS=<channel id>
PI_DISCORD_USER_IDS=<your discord user id>
# Optional: enabled by default; creates one Discord thread per Pi session.
PI_DISCORD_CREATE_THREAD_PER_SESSION=true
PI_DISCORD_THREAD_NAME_PREFIX=pi
# Optional: disabled by default. Set one or more guild IDs to register
# Discord bridge controls as guild-scoped app slash commands.
PI_DISCORD_REGISTER_SLASH_COMMANDS=false
PI_DISCORD_SLASH_COMMAND_GUILD_IDS=<guild id>

bin/pi-safe.sh (and friends) load .env at launch, so these vars become visible to the extension. The extension also loads .env from Pi's current working directory by default; override with PI_DISCORD_ENV_FILE=/path/to/.env if needed.

In this repo, the extension also accepts newer Pi-prefixed app keys as fallbacks:

  • PI_DISCORD_BOT_CHANNEL_ID → channel allowlist and default primary channel
  • PI_DISCORD_USER_ID → user allowlist

Legacy OpenClaw DISCORD_* names (DISCORD_BOT_TOKEN, DISCORD_BOT_CHANNEL_ID, DISCORD_USER_ID) are disabled by default to avoid silently selecting the wrong bot or channel. Set PI_DISCORD_ALLOW_LEGACY_ENV=true only when you intentionally want those fallbacks. Any fallback still requires PI_DISCORD_ENABLED=true.

4. Install from npm

pi install npm:@mporenta/pi-discord-remote

For local development from this repo instead:

cd extensions/discord-remote
npm install
cd ../..

This populates extensions/discord-remote/node_modules/discord.js and uses the committed package-lock.json. The Pi packages stay shared at the repo root (declared as peerDependencies here).

5. Run Pi with the extension

pi -e extensions/discord-remote/index.ts

Or combine with the team launcher:

npm run pi:safe -- -e extensions/discord-remote/index.ts

The extension is opt-in — it's not in bin/pi-safe.sh by default because it requires the env vars above.

Publishing

The npm package is @mporenta/pi-discord-remote. CI publishes from extensions/discord-remote when changes land on main and the package version is not already on npm. Configure the GitHub repository secret NPM_ACCESS_TOKEN with an npm automation token that can publish to the @mporenta scope.

To release a new version:

cd extensions/discord-remote
npm version patch --no-git-tag-version

Commit the package files and push to main; GitHub Actions handles npm publish --access public.

Slash commands (inside Pi)

  • /discord-arm — arms the Discord command bridge for session-changing commands such as /new from Discord for the active Pi session. This is required because Pi only exposes ctx.newSession(...) to command handlers, not background message events. Re-run it after local /new, /resume, /fork, or /reload; sessions created by Discord /new re-arm themselves.
  • /discord-status — token set?, gateway ready?, mute state, channel allowlist, user allowlist, active session thread, and render flags.
  • /discord-test [message] — post a one-off message to the active session thread (or primary channel before a thread exists). Use to confirm the bot is connected before you start a session.
  • /discord-reconnect — destroy and recreate the client. Useful if the gateway disconnected and didn't auto-recover.
  • /discord-mute — suppress further Pi output to Discord without disconnecting the gateway. Inbound Discord messages still drive Pi. Pair with /discord-unmute to resume.
  • /discord-unmute — resume Pi output to Discord after /discord-mute.

Inbound commands (from Discord)

  • Plain text — forwarded as <steerLabel> <text> via pi.sendUserMessage. When Pi is idle the message queues as a normal user turn (✅ react). When Pi is mid-turn the message steers the running turn (📥 react). Only messages posted in an allowlisted channel or in the current session's thread are forwarded; posts in stale session threads are ignored so they can't cross-steer the live session.
  • React 🛑 — react with 🛑 on any bot message in an allowlisted channel or the current session's thread to abort the running Pi turn. Faster than typing /abort when a long-running tool needs cancelling. Reactions in stale session threads are ignored for the same scoping reason as inbound text.
  • /commands or /help — lists Discord bridge controls plus Pi's currently registered session commands (extension, prompt, and skill metadata from pi.getCommands()). Pi command metadata is visibility-only because the public extension API does not expose a safe background dispatcher for arbitrary Pi command handlers. When PI_DISCORD_REGISTER_SLASH_COMMANDS=true and PI_DISCORD_SLASH_COMMAND_GUILD_IDS is set, the explicit Discord bridge controls are registered as native Discord slash commands via the @discordjs/rest module (re-exported by discord.js) and the explicit Routes.applicationGuildCommands(applicationId, guildId) route, so registration is scoped to the private guild(s) and never escapes to global application commands. Each command is defined with SlashCommandBuilder and the bot listens for interactionCreate events over the gateway.
  • /session — shows the active Pi session ID and Discord thread binding.
  • /compact — calls ctx.compact() for the active session.
  • /abort or !abort — calls ctx.abort() to cancel an in-flight turn. 🛑 react confirms. (Reacting 🛑 on a bot message does the same thing.)
  • /mute — suppress all further Pi output (streaming assistant edits, tool embeds, raw posts, and typing indicator) without disconnecting the gateway. Any pending stream-buffer flush is cancelled so an in-flight turn goes quiet immediately. Inbound messages still drive Pi. 🔇 react confirms.
  • /unmute — resume Pi output. 🔈 react confirms.
  • /new [message] — creates a new Pi session and a new Discord session thread, then optionally sends message as the first prompt. Run /discord-arm in the active local Pi TUI session to enable this command; re-run it after local session changes.

Reaction shortcuts

| React | Where | Effect | | ----- | ------------------------------------------------------------------------- | --------------------------------------------------- | | 🛑 | Any bot message in an allowlisted channel or the current session's thread | Abort the running Pi turn (equivalent to /abort). |

The bot also reacts on your inbound messages to confirm delivery: ✅ (queued while idle), 📥 (steered into a running turn), 🔇 / 🔈 (mute / unmute applied), 🛑 (abort issued), 🧹 (compact), 🆕 / 🚫 (new session created / cancelled).

Live feedback

While Pi is generating an assistant message, the bot shows a Discord typing indicator in the active session thread. The indicator refreshes every 8 seconds and stops when the turn ends, so the channel always reflects whether Pi is actively working.

Tool-result embeds also include a Duration field showing how long the tool took to run (e.g. 850ms, 12.3s, 1m 42s), so you can spot slow tools without scrolling the local TUI.

Pi's public extension API currently executes extension slash commands only from Pi prompt handling and deliberately skips command expansion for pi.sendUserMessage(...). The Discord bridge therefore implements the safe Discord-side built-ins above and lists all available Pi slash commands for visibility; additional custom command execution should be added as explicit Discord handlers when the command requires command-context-only methods. The bridge validates that the armed command context still belongs to the active session before honoring Discord /new, and asks you to re-arm if a local session change or reload made the context stale.

Configuration reference

| Var | Default | Effect | | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | PI_DISCORD_ENABLED | false | Master toggle. Default off so a stray .env doesn't auto-start the bot. | | PI_DISCORD_ALLOW_LEGACY_ENV | false | Enable legacy DISCORD_* fallbacks. | | PI_DISCORD_BOT_TOKEN | DISCORD_BOT_TOKEN only when legacy fallback is enabled | Bot token. Required. | | PI_DISCORD_CHANNEL_IDS | PI_DISCORD_BOT_CHANNEL_ID, then legacy DISCORD_BOT_CHANNEL_ID only when enabled | Comma-separated channel ID allowlist. Required. | | PI_DISCORD_USER_IDS | PI_DISCORD_USER_ID, then legacy DISCORD_USER_ID only when enabled | Comma-separated Discord user ID allowlist. Required. | | PI_DISCORD_PRIMARY_CHANNEL_ID | first allowlisted channel | Parent channel where session threads are created. | | PI_DISCORD_CREATE_THREAD_PER_SESSION | true | Create a Discord thread for each Pi session and route traffic there. | | PI_DISCORD_THREAD_NAME_PREFIX | pi | Prefix for session thread names; the Pi session ID is appended. | | PI_DISCORD_THREAD_AUTO_ARCHIVE_MINUTES | 10080 | Thread auto-archive duration (60, 1440, 4320, or 10080). | | PI_DISCORD_EDIT_INTERVAL_MS | 1100 | Min ms between edits per streamed message. | | PI_DISCORD_CREATE_INTERVAL_MS | 600 | Min ms between new-message creates per channel. | | PI_DISCORD_MAX_CHARS | 1900 | Soft cap per Discord message (Discord hard-limits at 2000). | | PI_DISCORD_RENDER_USER_INPUT | true | Mirror locally-typed user input to Discord. | | PI_DISCORD_RENDER_TOOL_CALLS | true | Post readable tool-call embeds with action + key inputs. | | PI_DISCORD_RENDER_TOOL_RESULTS | true | Update tool-call embeds with concise result summaries. | | PI_DISCORD_RENDER_REASONING | false | Include thinking content blocks (italicized). | | PI_DISCORD_TOOL_ARGS_MAX_CHARS | 800 | Truncate tool-call argument JSON to this length. | | PI_DISCORD_TOOL_RESULT_MAX_CHARS | 1000 | Truncate tool-result text to this length. | | PI_DISCORD_PREFIX | "" | Optional prefix on relayed messages (e.g. 🤖). | | PI_DISCORD_STEER_LABEL | [discord] | Tag prepended to inbound text so local view shows source. | | PI_DISCORD_REGISTER_SLASH_COMMANDS | false | Register explicit Discord bridge controls as app slash commands. | | PI_DISCORD_SLASH_COMMAND_GUILD_IDS | "" | Required comma-separated guild IDs for slash-command registration. |

Rendering behavior

Tool calls and tool results are rendered as Discord embeds instead of raw JSON. The embed shows the tool action, selected non-secret inputs, and a concise result summary. Secret-like argument keys (token, secret, password, api_key, webhook, etc.), common token formats, and sensitive environment variable values are redacted before rendering. Discord mentions are disabled on relayed messages to avoid accidental pings.

Known limitations

  • Discord supports explicit bridge commands (/commands, /session, /compact, /abort, and /new) and lists Pi slash command metadata. Pi's public API does not expose a general command dispatcher to background Discord message events, so arbitrary custom slash commands need explicit Discord-side handlers if they require command-context-only methods. Discord /new is intentionally session-scoped: re-run /discord-arm after local session replacements or /reload.
  • Discord rate limits: edits to a single message ≈ 5/5s; messages per channel ≈ 1/0.5s. The defaults stay under both. Pathologically fast streams will briefly buffer.
  • 2000-char hard cap → long assistant messages span multiple Discord posts. Once page 2 exists, page 1 is locked; late retroactive shrinkage of early content does not re-flow pages.
  • No image / file attachment relay (inbound or outbound) in v1. Inbound attachments are ignored; only msg.content is forwarded.
  • One active Discord output target at a time. By default this is the current session thread; if thread creation is disabled or fails, the primary channel is used. Multi-channel inbound is allowed via the allowlist.
  • The tool_call/tool_result pairing relies on Pi running tool calls in order; if parallel tool execution interleaves rendering, results still attach to the right call (lookup is by toolCallId).

Troubleshooting

  • /discord-status shows ready=false — check PI_DISCORD_BOT_TOKEN, confirm Message Content Intent is enabled in the developer portal, and that your network allows outbound WSS to gateway.discord.gg.
  • refusing to start on stderr — one of the required env vars is empty.
  • Messages in channel ignored — you're not in PI_DISCORD_USER_IDS, or the channel isn't in PI_DISCORD_CHANNEL_IDS.
  • Missing Access or send fails — the bot is in the guild but cannot see the target text channel. Grant the bot role (or the bot user) View Channel, Send Messages, Send Messages in Threads, Create Public Threads, Read Message History, Add Reactions, and Embed Links on the channel/category, then rerun /discord-test.
  • Bot stays online after Pi quitssession_shutdown should destroy the client; if it didn't (e.g. you killed Pi with kill -9), Discord still shows the bot online for ~30s of heartbeat timeout.