SLOPSHOPPER

agent-comms

A band above the prompt showing every message to another agent that is still waiting on a reply.

bandguardtoasttimer
A shopper browsing a rack in a slop shop
README

sanduhr-mods

Claude Code plugins, published from Sanduhr.

Add the marketplace once, then install any plugin in it by name:

claude plugin marketplace add estevanhernandez-stack-ed/sanduhr-mods
claude plugin install <plugin>@sanduhr-mods

Plugins

<!-- sanduhr:plugins -->

  • now-playing 0.3.1: A live now-playing line above the prompt: artist, title and a moving progress bar. On Windows from the media session (YouTube Music in Chrome included); on a Mac from every player through Sanduhr's adapter, or Music and Spotify without it.
  claude plugin install now-playing --marketplace estevanhernandez-stack-ed/sanduhr-mods
  • finish-line 0.1.1: When a turn creates a tag (git tag, gh release create), a Claude sprite runs across the band above the prompt and crosses a checkered finish line, and the tag's name lands beside it. /finish-line [tag] replays it. Reads nothing, writes nothing, no network.
  claude plugin install finish-line --marketplace estevanhernandez-stack-ed/sanduhr-mods
  • agent-comms 0.2.1: A band above the prompt showing every message to another agent that is still waiting on a reply.
  claude plugin install agent-comms --marketplace estevanhernandez-stack-ed/sanduhr-mods
  • decision-logger 0.1.1: Pushes the dashboard decision log hard: a standing instruction, a nudge after commits and edit-heavy turns, and a running count.
  claude plugin install decision-logger --marketplace estevanhernandez-stack-ed/sanduhr-mods
  • page-pane 0.1.1: /page <project> opens a side pane with that project's 626labs.dev portfolio card: summary, banner and link.
  claude plugin install page-pane --marketplace estevanhernandez-stack-ed/sanduhr-mods
  • desk-setup 0.1.0: A Claude Code skill that sets up your Sanduhr Desk as a conversation: it reads the screen and wallpaper, asks a few short questions, proposes a small look you Apply on the live preview, then offers a finishing widget such as a pixel companion or song looks. Windows, Sanduhr 3.7 or later; widgets need 3.8.
  claude plugin install desk-setup --marketplace estevanhernandez-stack-ed/sanduhr-mods

Then ask Claude Code to set up your Desk ("set up my Desk on the second screen"). Every change previews on the desktop and waits for your Apply.

<!-- /sanduhr:plugins -->

Mods to build for your own setup

Some mods are tied to one person's machines: a home server, a media box, a dashboard. Those aren't published here as code, because the code would carry someone else's hosts. What travels instead is the idea and a prompt. Paste a prompt into Claude Code (the plugin-authoring skill ships with it), fill in the parts in angle brackets, and Claude builds the mod for your setup; Sanduhr's New Mod page then keeps it, checks it and turns it on per folder.

Home lab pulse

A status-line glyph and a toast when something at home goes down or comes back, read from whatever already knows (an Uptime Kuma instance, a Tailscale tailnet, a Home Assistant), polled slowly over SSH or HTTP. /lab lists every monitor and peer on demand.

Build me a Claude Code mod called homelab-pulse with the plugin-authoring skill.
It polls my home lab every 60 seconds and shows a short status-line entry: a
check mark when everything is up, the count of what is down otherwise, and
"lab ?" when the source can't be reached. It toasts once when a monitor drops
and once when it recovers, never on every poll. A /lab command prints every
monitor and peer with its state.
Source of truth: <how to read your monitors, e.g. `ssh <host> …` printing JSON,
an HTTP endpoint, or `tailscale status --json`>. Run it with $.process.run with
a 20 s timeout, or $.http.fetch; parse only the fields you show.
Keep all state in an atom; no files, no network beyond that one source; no
credentials in the mod (use an SSH host alias or a local-network URL).
Add register.test.ts covering: all up, one down, source unreachable, recovery.

A quiet pip for a media server

Two characters in the status line while a media server answers, nothing otherwise. No alerts, slow polling, no token.

Build me a Claude Code mod called plex-pip with the plugin-authoring skill.
Every <5> minutes it fetches <your server's unauthenticated health URL, e.g.
http://<host>:32400/identity for Plex, or /System/Ping for Jellyfin> with
$.http.fetch. While the response is ok it shows "🎬🎵" in the status line via
$.ui.status; on any error or non-2xx it shows nothing. Read the URL and the
interval from plugin options (plexUrl, pollMinutes) so the mod ships without my
address in it; do nothing when the URL option is empty. No toasts, no commands,
no state beyond the last result. Add register.test.ts for ok, non-2xx, a thrown
fetch and an empty option.

If you build one, publish it to your own marketplace from Sanduhr (New Mod, a kept mod's menu, Publish to Marketplace…).

Source 2 files
hooks/register.tsx 140 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pending } from '../types'
5
6const pending = atom({ plugin: 'agent-comms', key: 'pending' } as const, [] as Pending[])
7
8// MCP tools that hand a question to another agent, from the agentTools option: "tool=Label; tool=Label".
9// The call is the wait.
10export const parseAgentTools = (spec: unknown): Record<string, string> => {
11  const tools: Record<string, string> = {}
12
13  for (const entry of String(spec ?? '').split(';')) {
14    const i = entry.indexOf('=')
15    const tool = (i === -1 ? entry : entry.slice(0, i)).trim()
16
17    if (tool !== '') {
18      tools[tool] = (i === -1 ? '' : entry.slice(i + 1).trim()) || tool
19    }
20  }
21
22  return tools
23}
24
25// A send nobody answers in this long stops being "waiting" and starts being stale.
26const STALE_MS = 30 * 60 * 1000
27
28const PEER_KINDS = new Set(['peer', 'peer-send-message', 'coordinator'])
29
30const clock = (ms: number): string => {
31  const s = Math.max(0, Math.round(ms / 1000))
32
33  return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
34}
35
36let ticker: { cancel: () => void } | null = null
37
38// The band counts seconds, so a timer redraws it once a second while anything waits.
39function sync(list: Pending[], $: EngineInterface) {
40  if (list.length > 0 && ticker === null) {
41    ticker = $.clock.every(1000, () => $.ui.invalidate('ui.render'))
42  } else if (list.length === 0 && ticker !== null) {
43    ticker.cancel()
44    ticker = null
45  }
46}
47
48export const register: Register = (on, options) => {
49  const AGENT_TOOLS = parseAgentTools(options.agentTools)
50  let seq = 0
51
52  on('tool.call', async ($, e, next) => {
53    let label: string | undefined
54    let kind: Pending['kind'] = 'tool'
55
56    if (e.tool === 'Agent') {
57      kind = 'agent'
58      label = e.name ?? e.description
59    } else if (AGENT_TOOLS[e.tool] !== undefined) {
60      label = AGENT_TOOLS[e.tool]
61    }
62
63    if (label === undefined || e.agentId !== undefined) {
64      return next(e)
65    }
66
67    const id = ++seq
68    const at = await $.clock.now()
69    sync(await update($, pending, list => [...list, { id, kind, label: label as string, at }]), $)
70
71    try {
72      return await next(e)
73    } finally {
74      sync(await update($, pending, list => list.filter(p => p.id !== id)), $)
75    }
76  })
77
78  // SendMessage: delivered means queued, not read. The reply arrives as its own delivery.
79  on('session.send', async ($, e, next) => {
80    const sent = await next(e)
81
82    if (sent.isDelivered && e.agentId === undefined) {
83      const id = ++seq
84      const at = await $.clock.now()
85      sync(await update($, pending, list => [...list, { id, kind: 'send', label: e.to, at }]), $)
86    }
87
88    return sent
89  })
90
91  // A peer delivery answers the oldest open send. The delivery carries no sender, so this is FIFO.
92  on('session.receive', async ($, e, next) => {
93    if (PEER_KINDS.has(e.origin.kind)) {
94      let who: string | undefined
95      sync(
96        await update($, pending, list => {
97          const i = list.findIndex(p => p.kind === 'send')
98
99          if (i === -1) {
100            return list
101          }
102
103          who = list[i].label
104
105          return list.filter((_, j) => j !== i)
106        }),
107        $,
108      )
109      $.ui.toast(who === undefined ? 'Reply came in.' : `Reply from ${who}.`)
110    }
111
112    return next(e)
113  })
114
115  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
116    const list = await read($, pending)
117
118    if (e.props.hasSurvey || list.length === 0) {
119      return next(e)
120    }
121
122    const now = await $.clock.now()
123    const live = list.filter(p => now - p.at < STALE_MS)
124
125    if (live.length === 0) {
126      return next(e)
127    }
128
129    const { Box, Text } = $.ui.resolve(e)
130    const parts = live.map(p => `${p.label} ${clock(now - p.at)}`)
131
132    return (
133      <Box>
134        <Text color="yellow">Waiting on {live.length === 1 ? 'a reply' : `${live.length} replies`}: </Text>
135        <Text dimColor>{parts.join('  |  ')}</Text>
136      </Box>
137    )
138  })
139}
140
types/index.d.ts 13 lines
1export type Pending = {
2  id: number
3  kind: 'send' | 'agent' | 'tool'
4  label: string
5  at: number
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'agent-comms': { pending: Pending[] }
11  }
12}
13