SLOPSHOPPER

mesh-toast

Toast when a new cortex mesh message arrives

newtoastprocesstimer
v0.1.0no licenseupdated 2026-10-09Mark-Hickey/claude-nine/mesh-toast
A shopper browsing a rack in a slop shop
README

mesh-toast

Shows a toast in Claude Code when a new message comes to your cortex mesh inbox.

What it does

  • Every minute, it reads your mesh inbox with cortex-mesh.py inbox --peek.
  • For each new message, it shows a toast for 10 seconds with the sender and the subject.
  • It shows each message one time in a session. After a restart, a message that is still in your inbox shows one more time.

It uses --peek, so it never marks a message as read on the mesh. The message stays in your inbox until you or your agent read it.

Requirements

  • The cortex-join plugin, installed and joined (/cortex-join). The mod uses its cortex-mesh.py script.
  • Tailscale connected, so the mesh can be reached.

When the mesh cannot be reached, the mod shows nothing and tries again one minute later.

Install

/plugin install mesh-toast --marketplace Mark-Hickey/claude-mods

mesh-toaster shows the same mail as a toaster above the prompt. You can use one of the two mods, or both.

Develop

claude plugin validate mesh-toast
claude plugin test mesh-toast
Source 1 files
hooks/register.ts 64 lines
1import type { Register } from 'claude-code'
2
3// Reads the inbox with --peek, so checking never marks anything read.
4const INBOX = [
5  'bash',
6  '-c',
7  'python3 "$(ls ~/.claude/plugins/cache/cortex/cortex-join/*/scripts/cortex-mesh.py | sort | tail -1)" inbox --peek',
8]
9const EVERY_MS = 60_000
10
11// "  [1791070009649-0] msg <uuid> from chris@todie.io @ session-…: subject"
12const LINE = /^\s*\[[^\]]+\]\s+\S+\s+(\S+)\s+from\s+(\S+)\s+@\s+([^:]+):\s*(.*)$/
13
14export type Message = { id: string; from: string; peer: string; subject: string }
15
16export function parseInbox(stdout: string): Message[] {
17  return stdout.split('\n').flatMap(line => {
18    const m = LINE.exec(line)
19
20    return m ? [{ id: m[1], from: m[2], peer: m[3].trim(), subject: m[4] }] : []
21  })
22}
23
24export function toastText(msg: Message): string {
25  const who = msg.from.split('@')[0]
26
27  return `🍞 Toast's up! Mesh mail from ${who} (${msg.peer}): ${msg.subject}`
28}
29
30export const register: Register = on => {
31  const seen = new Set<string>()
32  let isChecking = false
33
34  on('session.start', async ($, e, next) => {
35    const check = async () => {
36      if (isChecking) {
37        return
38      }
39      isChecking = true
40
41      try {
42        const { exitCode, stdout } = await $.process.run(INBOX, { timeoutMs: 20_000 })
43        if (exitCode !== 0) {
44          return
45        }
46
47        for (const msg of parseInbox(stdout)) {
48          if (!seen.has(msg.id)) {
49            seen.add(msg.id)
50            $.ui.toast(toastText(msg), { timeoutMs: 10_000 })
51          }
52        }
53      } finally {
54        isChecking = false
55      }
56    }
57
58    void check()
59    $.clock.every(EVERY_MS, () => void check())
60
61    return next(e)
62  })
63}
64