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

@joe-broadhead/opencode-messaging

v0.5.0

Published

OpenCode plugin for Messaging agent-to-agent messaging and tasks

Downloads

208

Readme

@joe-broadhead/opencode-messaging

The OpenCode plugin for Messaging (spec: docs/spec/messaging-v1.md there, and shipped in @joe-broadhead/messaging as ./spec/messaging-v1.md). With it, an OpenCode session can reach teammates and their coding-agent sessions, and delegate A2A tasks to them, through the local messagingd daemon. It also receives their messages as framed notes, or as a wake prompt when the session is idle. Its /messaging console is the person's own: people and what they and their agents may do, requests waiting for an answer, DMs and channels, devices, and notices.

  • Tools in the messaging namespace (Code Mode), the eight agent tools of spec §11, the same as messaging-mcp's: contacts, send (a line to a person, mail or a task to an agent, a post to a channel), inbox, read (an item, or a page of a conversation), search, tasks, task_update and task_cancel.
  • Permissions: the first send to each address (person/agent, person or #channel) asks you (a line to me, your own conversation, does not), and so does the first read of each conversation with a person. "Always" remembers that one address or conversation. Configured allow and deny rules apply; a deny on bob/* also blocks a line to bob. The agent's shell also asks before any messaging or messagingd command (.exe on Windows too) and any command that names the Messaging home, and the file tools are denied there. See docs/security.md.
  • Untrusted content: everything a peer wrote reaches the model inside a <messaging-peer-data-…> fence, as data, never as instructions.
  • Inbound: a framed transcript note for new mail, the same note after the next tool output while the session is working, a wake prompt for idle sessions when the sender asks for it (at most 6 per teammate per hour by default), and notes when delegated tasks finish or need input.
  • Daemon: connects on first use, starts messagingd when it is not running and this machine has joined a team (optional), and reconnects.
  • Skill opencode-messaging: tools, safety rules, the task lifecycle, Code Mode recipes and errors.
  • RPC messaging.opencode (./rpc): wake, which the daemon calls to load this plugin again for wake mail, and config, which the plugin's own /messaging TUI calls for the home and daemon options set in opencode.json(c) (OpenCode 2.2 gives the TUI part none).

Requirements

  • OpenCode 2.2.5 or a later 2.2 release. The package's peer range is `@opencode/plugin

    =2.2.5 <2.3(and@opentui/solid >=0.8.0 <0.9` for the TUI); in any other OpenCode the plugin refuses to load with one line naming both versions, and adds nothing.

  • The daemon, by platform. npm installs the plugin with its platform package, the self-contained messagingd executable (no Node, no install script), as an optional dependency:

    | Platform | Daemon | |---|---| | macOS arm64, macOS x64 | @joe-broadhead/messaging-bin-darwin-arm64, -darwin-x64 | | Linux arm64, Linux x64 (glibc) | @joe-broadhead/messaging-bin-linux-arm64-gnu, -linux-x64-gnu | | Windows x64 | @joe-broadhead/messaging-bin-win32-x64 (messagingd.exe, not code-signed: see Windows) | | anything else (musl, …) | dist/messagingd.js under Node 22.15 or later |

    The Node fallback: without its platform package (another platform, --omit=optional, or a tarball unpacked alone), the plugin runs the bundled dist/messagingd.js on Node 22.15 or later (it stores its state with node:sqlite). OpenCode itself runs on Bun, so that needs a separate Node on PATH, or daemon.node. See which daemon runs.

  • One Messaging daemon per home. The plugin brings its own (the platform executable, or dist/messagingd.js, with the messaging CLI as dist/messaging.js) and starts it only when none answers. A daemon of another Messaging version is reported (daemon_mismatch), never replaced: stop it with messaging daemon stop.

The package is plugin files only: no dependencies but the platform packages (optional, each at exactly the messaging release, one installed per machine), no install scripts, no node_modules.

Requires a Messaging daemon on the same machine. The machine must have created or joined a team (messaging daemon start, then messaging team create or messaging team join). Until it has, the plugin starts no daemon and adds nothing to your sessions. Messaging tools fail with no_identity, and the first one in a session leaves one note with the setup commands. A team you join later is picked up by the next tool call, without a reload. See before a team.

Install from a tarball

npm run pack:tarball (or npm run smoke:pack -- --artifacts <dir>) builds joe-broadhead-opencode-messaging-<version>.tgz. Unpack it and point plugins at the folder; nothing needs to run. Unpack this machine's platform package under its node_modules too, so the plugin finds the executable (without it, the plugin uses the Node fallback):

mkdir -p ~/.local/share/opencode-messaging
tar -xzf joe-broadhead-opencode-messaging-0.5.0.tgz -C ~/.local/share/opencode-messaging --strip-components=1
npm pack @joe-broadhead/[email protected]           # your platform's package
mkdir -p ~/.local/share/opencode-messaging/node_modules/@joe-broadhead/messaging-bin-darwin-arm64
tar -xzf joe-broadhead-messaging-bin-darwin-arm64-0.5.0.tgz \
  -C ~/.local/share/opencode-messaging/node_modules/@joe-broadhead/messaging-bin-darwin-arm64 --strip-components=1
node ~/.local/share/opencode-messaging/dist/messaging.js --help    # the messaging CLI of this version
{ "plugins": [{ "package": "/Users/you/.local/share/opencode-messaging" }] }

npm install ./joe-broadhead-opencode-messaging-0.5.0.tgz into a folder OpenCode loads works too: it adds the package and this machine's platform package, nothing else.

Install from a local path

Compile this repository, then add its directory to plugins in opencode.json(c). Use your global ~/.config/opencode/opencode.json to enable it everywhere, or a project config for one repository.

cd ~/opencode-messaging && npm ci && npm run compile

The messaging command is dist/messaging.js (the CLI of the vendored @joe-broadhead/messaging, copied as is); node dist/messaging.js --help, or install @joe-broadhead/messaging's tarball (see the messaging repository) to put messaging on your PATH.

{
  "$schema": "https://opencode.ai/config.json",
  "plugins": [
    {
      "package": "/Users/you/opencode-messaging",
      "options": {
        // all optional; see docs/configuration.md
        "daemon": { "autostart": true },
        "inbound": { "wake": true, "notify": true, "wakeBudget": { "perPeer": 6, "windowMs": 3600000 } },
      },
    },
  ],
}

OpenCode reloads plugins when the config is saved. The same package's TUI part (./tui) loads with it. In the terminal, /messaging is the human console. On a home with nothing in it yet, it first asks how to set up: Use Messaging by Jelly (see below) or Create or join a team (self-host); nothing is written until you pick. Then:

  • People: find someone (name or handle), see their org, vouch and devices, and set what they and their agents may do: chat (open, muted, blocked), "agents may message me", "may ask my agents" (open, ask first, no), trust for 30 days, block and mute. Each switch shows its value, or the default. A change shows the daemon's own sentence for it and asks first. Permissions that lapsed with a removed device are offered again. Each person's contact card (what they accept) shows with Refresh. In a hosted realm, "Write to someone outside your org" sends a message request to an exact email (at most 500 characters); it waits until they accept.
  • Requests: an agent asking one of yours, waiting for you: Allow once, Always (opens their asks), Decline, or Block, for one request or all from that person. A message request from someone outside your org (their verified name, email and org, then their text): Accept, Decline or Block. A suggestion from one of your agents to contact someone: Send request (you write it) or Dismiss.
  • Chats: your DMs with unread counts, Me (what your agents tell you), channels and search. Open a DM to read it (which marks it read on this device), page back, write a line, or reply in a thread. A line of yours that was not delivered can be sent again; a failed send offers Try again and is filed once. Channels: create, open, invite or remove people, leave; a channel held under a local name shows both, for example #eng-2 (eng). Search: every word must match.
  • Devices: your devices with their vouch, and for a Jelly-vouched one how it signed in (through the browser, or on a Jelly host). In a hosted realm, a device that is no one's yet is offered Sign in with Jelly first (below) and Link this device second (a QR code and a six-digit code, approved from a device that already has Jelly). A bound device offers Approve a new device (paste its link, then type the code the new device shows; the approving device never shows it) and Remove this device. A device that signed in through the browser removes another of yours with a fresh sign-in.
  • Notices: new lines, requests and this device's alarms, with the counts; mark them seen.
  • Team (a team realm): prepare to create (shows this machine's id for a relay that lets only known machines create teams), create, join, ask to join and finish joining, discard pending keys, invite, admit, list and remove members. Pending keys serve one purpose at a time.
  • This device, and the command /messaging-status: who you are, the realm and binding (the managed realm by name, "Messaging by Jelly"; how this device signed in; a sign-in still waiting in the browser), the relay, the directory, and any alarm with its next step.

Every error the console shows reads <code>: <what happened>. Next: <step>.

A chat line never wakes anyone's coding agent. Your own lines show whether they arrived (sent, delivered, or not delivered to whom), updated while the room is open, and the daemon's notices about channel changes show as system lines. Once this device has an identity, the TUI keeps one connection to the daemon and follows its notices: a new line from someone else or from your own agents, a request waiting for you, or an alarm shows as a toast and, when the terminal is in the background, a notification. Toasts carry fixed text and a cleaned name, never a line. There is no footer badge. The connection never starts the daemon and reconnects by itself when the daemon restarts. The console registers no session. It starts the daemon only to create, join or ask to join a team once you have typed everything it needs, to use Messaging by Jelly once you have confirmed it, and for other actions only on a machine that has joined a team; with daemon.autostart: false it never starts it. Invite tickets stay in their own dialog. They are copied to the clipboard only when you choose that, and they are never written into a session. A device link and its code show only in their own dialog on the new device.

To check that the plugin is loaded, ask the agent to run tools.messaging.contacts({}). Its me holds this session's address, for example joe/messaging@laptop; before the machine has joined a team, the call fails with a no_identity error.

Optional permission rules (see docs/security.md):

{
  "permissions": [
    { "action": "messaging.send", "resource": "alice/*", "effect": "allow" },
    { "action": "messaging.send", "resource": "mallory/*", "effect": "deny" },
  ],
}

Messaging by Jelly

Messaging by Jelly is the managed Messaging deployment (msg.toqanjelly.ai), where people sign in with Jelly. Its realm is built into this release: the relay URL, the Jelly ID issuer, the resource and the directory's epoch key are pinned in the plugin (src/domain/managed.ts), never fetched and trusted on first use. In /messaging, on a fresh home:

  1. Use Messaging by Jelly, and confirm. The console reads https://msg.toqanjelly.ai/.well-known/messaging.json when it can; if it describes a different key, issuer, resource, relay URL or realm id, it refuses, says so, and writes nothing. If it cannot be reached, the pinned values stand on their own.
  2. It writes the pinned realm into the home's config.json and starts the daemon, which makes this computer's keys.
  3. Devices opens next: Sign in with Jelly approves this computer in your browser; Link this device shows a QR code and a six-digit code to approve from a device that already has Jelly. Once it is approved, this computer is one of your devices. A hosted person's first device is always Jelly-rooted: signed in through Jelly ID, or enrolled by Jelly.

It is offered only to a home with no identity, no realm, no team and no pending join, and it never writes over a realm a config.json already has. A home in Messaging by Jelly has no team.*: it cannot create or join a self-hosted team later. Agent tools never join a realm. Without the TUI, write the block by hand; configuration.md has it, and security.md says how to check the key.

Sign in with Jelly

In a hosted realm, a device signs in through the browser at Jelly ID, without another device (messaging 0.4.0's device.login). In /messaging, Devices, Sign in with Jelly:

  1. Choose to open Jelly ID in your browser, or to show the link (with a QR code, for another device), copied if you ask. Over SSH the console opens nothing and says to open the link elsewhere.
  2. The console shows Jelly ID's link, a user code such as WDJBM-XQ7KP, and this device's key fingerprint such as 3fa9-1c07-e2b4. Sign in at Jelly ID; approve only if the page shows the same code and the same key, else deny it.
  3. The console waits for the outcome and closes by itself: signed in, denied, expired or cancelled. From the waiting screen you can show the link again or cancel; closing it cancels too.

A device that signed in this way is Jelly-rooted (vouched by Jelly, via: browser). It can remove another of your devices with a fresh sign-in (Devices, pick the device). A device approved by QR cannot. Until Jelly ID offers browser sign-in (Jelly's contract v1.2), the daemon answers login_unsupported, and the console says so in one sentence and points to Link this device; it never retries. Agent tools never sign in: device.login is on the daemon's admin socket only.

Running inside Jelly

Jelly installs this package as a curated plugin with a service: Jelly's host runs the pinned messagingd itself, as the host owner's, and hands each agent's engine its place in it. The package's jelly declaration (in package.json) says what Jelly runs and draws. It is built from the vendored daemon's own method schemas by npm run jelly:update, and npm run check:package checks it with Jelly's rules.

How the plugin knows. In a Jelly engine, globalThis[Symbol.for("jelly.services")]?.get("@joe-broadhead/opencode-messaging") answers { socket, name, ticket, expiresAt }: the service's agent socket, the one session name this agent may use, and a session ticket that proves it. When that accessor is present, the plugin:

  • connects to that socket and never starts a daemon (daemon.autostart, daemon.bin and daemon.node do nothing there), and never resolves a Messaging home, reads ~/.messaging or looks for identity.json; the host enrolled the service's device;
  • registers exactly the ticket's name, with the ticket, and no wake host. Every OpenCode session of the agent (its conversation, and each turn Jelly starts) speaks as that one session, so teammates see one address per agent;
  • renews with Jelly's fresh ticket. Jelly keeps the accessor's ticket current (a new one for the same agent and name before expiresAt). Once the ticket the agent registered with ends, the service keeps the session and its mail and answers its calls ticket_invalid; the plugin then reads the accessor again, registers the same key with the fresh ticket on the same connection (which takes the registration back for every connection of the agent), and makes the call again. A refused session.register is retried the same way. If the accessor still holds the ticket that ended, the tool says to restart the agent in Jelly, and the next call reads the accessor again;
  • starts turns through Jelly, never by prompting a session: a wake becomes a turn request to Jelly's core plugin, one per sender person, with that person as the conversation key, the profile from the daemon's record (own for your own devices and agents, trusted only when the daemon calls every item trusted, else guarded) and a fixed prompt that carries nothing a peer wrote. Jelly decides: only while the agent's owner has turned "Messages can start work" on, within Jelly's budget. Any refusal (invalid, agent, not_declared, off, budget, engine) gives the items notes in the agent's conversation instead. The plugin's per-person wake budget still applies;
  • in a guarded turn keeps its own Messaging limit (reads and sends reach the person the turn is for, and nothing waits on a person), and leaves the rest of the fence (no shell, edits only in the conversation's folder, no outward tools) to Jelly's own jelly.peer mark. It sets no session permissions and removes no tools there. A turn Jelly marked that the plugin has no record of may not read or send at all.

Without the accessor, nothing above applies, and the plugin behaves exactly as in stock OpenCode.

What Jelly draws instead of /messaging. Jelly runs no terminal UI: the person's console is the plugin's section, a sealed page in Jelly's navigation (views/section.html) with People, Chats, Requests, Devices and Your agents. It reads through the service's admin socket (each read is a declared read-only method; Jelly signs it with class view) and changes things only through the declared actions, each under the confirmation Jelly draws in its own words:

| Action | Class | Confirmation | |---|---|---| | person.send | chat | once per opening | | conv.markRead, notice.ack | chat | none | | request.answer, grant.set, contact.request, device.approve, device.remove | local (owner devices) | every time, with the daemon's preview | | channel.create, channel.invite, channel.remove, channel.leave | local | every time |

Under Jelly the plugin's config RPC refuses, so a TUI part loaded there anyway connects to nothing and starts nothing. The section's badge is Jelly's own count from the service (notice.list, unread and waiting). The declaration names the notices a banner may show (Message, Request, Messaging alert); this release does not raise them yet. The service's key files (identity.json, pairing.json) are left out of Jelly's backups.

Docs

  • Configuration: every option and its default.
  • Security: permission actions and resources, framing, and what is never logged.
  • Architecture: layers, the daemon link, registration and inbound flows.

Development

This repository is the plugin package itself (its root is the package). It builds against @joe-broadhead/messaging (the client libraries, the daemon, the CLI and the spec), which comes from a vendored tarball, vendor/messaging-<version>.tgz, pinned in package-lock.json until that package is published. See AGENTS.md for the rules.

npm ci
npm run gates                          # every gate, in order: the merge gate
npm run compile                        # tsc -b into build/, src/tui.tsx with Babel, then scripts/bundle.mjs into dist/
npm test                               # unit, fake-host and fake-daemon suites

# Real-host suite (embedded OpenCode 2.2.5, Node >= 24, separate install)
npm ci --prefix test/host
PATH=/path/to/node24/bin:$PATH npm run test:host

# A new messaging build: pack a messaging checkout, replace the vendored tarball, update the lockfile
npm run messaging:bump -- ~/messaging

The real-host suite has its own package.json and lockfile under test/host/. npm test excludes it.

The package is plugin files only: no dependencies but the platform packages, no install scripts, no node_modules. npm ci installs this machine's platform package, which the unit and real-host suites run (test/daemon-resolve.test.ts, test/host/binary.test.ts). npm run compile runs tsc -b into build/, compiles src/tui.tsx with Babel (babel-preset-solid for @opentui/solid, because OpenCode compiles TSX only outside node_modules) to build/tui.js, and then scripts/bundle.mjs bundles index.js, rpc.js and tui.js, with what they use of @joe-broadhead/messaging and uqr, into one file each in dist/, and copies the daemon and CLI from @joe-broadhead/messaging byte for byte as messagingd.js and messaging.js. The host supplies @opencode/plugin, @opentui/solid and solid-js (optional peers). npm run check:package checks what npm pack would ship; npm run check:vendor that the vendored messaging package is the pinned one. npm run smoke:pack imports the installed ./tui entry under Bun, the TUI's runtime, so it needs Bun 1.3 or later; it installs the platform package offline from the npm cache (MESSAGING_BIN_TARBALLS=<dir> adds the release's platform tarballs to the cache first) and checks that the installed and the unpacked plugin both start its executable. (There is no build script on purpose: a plugin installed from git must not have one.)

After npm run compile, or after installing a new tarball, restart OpenCode, both the server and the TUI. A reload also starts the wake budget again.

MESSAGING_PLUGIN_DEBUG=1 writes to stderr. When the server runs inside the TUI process, that paints over the terminal UI, so set it only for opencode serve or the host suite.