/page <project> opens a side pane with that project's 626labs.dev portfolio card: summary, banner and link.

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
<!-- sanduhr:plugins -->
claude plugin install now-playing --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install finish-line --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install agent-comms --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install decision-logger --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install page-pane --marketplace estevanhernandez-stack-ed/sanduhr-mods
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 -->
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.
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.
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…).
hooks/register.tsx 188 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { View } from '../types'
5import { SITE_URL, VERSIONS_URL, bannerUrl, cardMarkdown, findProducts, listIds, resolveLink } from './site'
6import type { Site } from './site'
7
8const PANE = 'page-pane'
9const MAX_BANNER_BYTES = 2 * 1024 * 1024
10
11const view = atom({ plugin: 'page-pane', key: 'view' } as const, null as View | null)
12
13type Loaded =
14 | { kind: 'ok'; view: View }
15 | { kind: 'none' }
16 | { kind: 'many'; ids: string[] }
17 | { kind: 'error'; message: string }
18
19async function fetchJson($: EngineInterface, url: string): Promise<unknown> {
20 const res = await $.http.fetch(url)
21
22 if (!res.ok) {
23 throw new Error(`${url} answered ${res.status}`)
24 }
25
26 return JSON.parse(res.text)
27}
28
29// The terminal reads the picture off disk itself, so the banner goes to a file, not through `$`.
30async function downloadBanner($: EngineInterface, id: string, url: string): Promise<string | null> {
31 try {
32 const tmp = (await $.env.get('TEMP')) ?? (await $.env.get('TMPDIR')) ?? '/tmp'
33 const sep = tmp.includes('\\') ? '\\' : '/'
34 const file = `${tmp.replace(/[\\/]+$/, '')}${sep}page-pane${sep}${id}.png`
35 const ran = await $.process.run(
36 ['curl', '-sL', '--create-dirs', '-o', file, '-w', '%{http_code} %{size_download}', url],
37 { timeoutMs: 20000 },
38 )
39 const [code, size] = ran.stdout.trim().split(' ')
40 const bytes = Number(size)
41 const isUsable = ran.exitCode === 0 && code === '200' && bytes > 0 && bytes <= MAX_BANNER_BYTES
42
43 return isUsable ? file : null
44 } catch {
45 return null
46 }
47}
48
49async function fetchSite($: EngineInterface): Promise<Site> {
50 return (await fetchJson($, SITE_URL)) as Site
51}
52
53async function loadCard($: EngineInterface, query: string, generation: number): Promise<Loaded> {
54 let site: Site
55
56 try {
57 site = await fetchSite($)
58 } catch (error) {
59 return { kind: 'error', message: `Could not read the portfolio feed. ${String(error)}` }
60 }
61
62 const found = findProducts(site, query)
63
64 if (found.length === 0) {
65 return { kind: 'none' }
66 }
67
68 if (found.length > 1) {
69 return { kind: 'many', ids: found.map(p => p.id) }
70 }
71
72 const product = found[0]
73 let versions: Record<string, string> = {}
74
75 try {
76 versions = (await fetchJson($, VERSIONS_URL)) as Record<string, string>
77 } catch {
78 // Versions are garnish. The card stands without them.
79 }
80
81 const bannerFile = await downloadBanner($, product.id, bannerUrl(product))
82
83 return {
84 kind: 'ok',
85 view: {
86 id: product.id,
87 title: product.title ?? product.id,
88 link: resolveLink(product),
89 markdown: cardMarkdown(product, versions[product.id]),
90 bannerFile,
91 generation,
92 error: null,
93 },
94 }
95}
96
97async function openInBrowser($: EngineInterface, url: string): Promise<void> {
98 const isWindows = (await $.env.get('OS')) === 'Windows_NT'
99 const isMac = ((await $.env.get('HOME')) ?? '').startsWith('/Users/')
100 const argv = isWindows ? ['rundll32', 'url.dll,FileProtocolHandler', url] : [isMac ? 'open' : 'xdg-open', url]
101
102 await $.process.run(argv)
103}
104
105export const register: Register = on => {
106 on('session.start', async ($, e, next) => {
107 await $.command.register({
108 name: 'page',
109 description: "Open a project's portfolio card in a side pane",
110 argumentHint: '<project>',
111 })
112
113 return next(e)
114 })
115
116 on('command.run', { command: 'page' }, async ($, e) => {
117 const query = e.args.trim()
118
119 if (query === '') {
120 try {
121 return { text: `Projects: ${listIds(await fetchSite($)).join(', ')}` }
122 } catch (error) {
123 return { text: `Could not read the portfolio feed. ${String(error)}` }
124 }
125 }
126
127 const loaded = await loadCard($, query, 0)
128
129 if (loaded.kind === 'error') {
130 return { text: loaded.message }
131 }
132
133 if (loaded.kind === 'none') {
134 return { text: `No project matches "${query}". Run /page with no argument for the list.` }
135 }
136
137 if (loaded.kind === 'many') {
138 return { text: `Several match "${query}": ${loaded.ids.join(', ')}. Be more specific.` }
139 }
140
141 await update($, view, () => loaded.view)
142 await $.ui.open({ id: PANE, title: loaded.view.title })
143
144 return { text: `${loaded.view.title} opened in the pane.` }
145 })
146
147 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
148 const { Box, Button, Image, Link, Markdown, Text } = $.ui.resolve(e)
149 const current = await read($, view)
150
151 if (current === null) {
152 return <Text dimColor>Run /page <project> to open a card.</Text>
153 }
154
155 const refresh = async () => {
156 const loaded = await loadCard($, current.id, current.generation + 1)
157
158 if (loaded.kind === 'ok') {
159 await update($, view, () => loaded.view)
160 } else if (loaded.kind === 'error') {
161 await update($, view, v => (v === null ? v : { ...v, error: loaded.message }))
162 }
163 }
164
165 return (
166 <Box flexDirection="column" gap={1}>
167 <Text bold>{current.title}</Text>
168 {current.error !== null && <Text color="red">{current.error}</Text>}
169 {current.bannerFile !== null && (
170 <Image
171 key="banner"
172 source={{ file: current.bannerFile, format: 'png', generation: current.generation }}
173 columns={60}
174 rows={15}
175 alt={`${current.title} banner`}
176 />
177 )}
178 <Markdown text={current.markdown} />
179 <Link href={current.link} label={current.link} />
180 <Box gap={2}>
181 <Button key="refresh" label="Refresh" onPress={refresh} />
182 <Button key="open" label="Open in browser" onPress={() => openInBrowser($, current.link)} />
183 </Box>
184 </Box>
185 )
186 })
187}
188hooks/site.ts 87 lines1// Pure helpers over the 626labs.dev feed. No `$` in here, so these test without an engine.
2
3export const BASE = 'https://626labs.dev/'
4export const SITE_URL = 'https://raw.githubusercontent.com/626Labs-LLC/626Labs-LLC.github.io/main/content/site.json'
5export const VERSIONS_URL = `${BASE}data/plugin-versions.json`
6
7export type Tag = { label?: string; tone?: string }
8
9export type Product = {
10 id: string
11 title?: string
12 tagline?: string
13 description?: string
14 status?: string
15 tags?: Tag[]
16 banner?: string | null
17 productPage?: string | null
18 liveUrl?: string | null
19 storeUrl?: string | null
20 meta?: string | null
21}
22
23export type Site = { products?: Product[] }
24
25const squash = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]/g, '')
26
27// A path from the feed is relative to the site root, with or without a leading slash.
28export function absolute(path: string): string {
29 if (/^https?:\/\//i.test(path)) {
30 return path
31 }
32
33 return BASE + path.replace(/^\/+/, '')
34}
35
36// productPage, else liveUrl, else storeUrl, else the home-page row. Same order the site's own cards use.
37export function resolveLink(product: Product): string {
38 if (product.productPage) {
39 return absolute(product.productPage)
40 }
41
42 return product.liveUrl || product.storeUrl || `${BASE}#work`
43}
44
45export function bannerUrl(product: Product): string {
46 return absolute(product.banner || `assets/brand/plugins/${product.id}-banner-1280x640.png`)
47}
48
49export function listIds(site: Site): string[] {
50 return (site.products ?? []).map(p => p.id)
51}
52
53// Exact id or title beats prefix beats substring. Only the best rank comes back, so a tie is a real ambiguity.
54export function findProducts(site: Site, query: string): Product[] {
55 const q = squash(query)
56
57 if (q === '') {
58 return []
59 }
60
61 const ranked = (site.products ?? [])
62 .map(p => {
63 const keys = [squash(p.id), squash(p.title ?? '')]
64 const rank = keys.some(k => k === q) ? 0 : keys.some(k => k.startsWith(q)) ? 1 : keys.some(k => k.includes(q)) ? 2 : 3
65
66 return { p, rank }
67 })
68 .filter(r => r.rank < 3)
69
70 const best = Math.min(...ranked.map(r => r.rank), 3)
71
72 return ranked.filter(r => r.rank === best).map(r => r.p)
73}
74
75export function cardMarkdown(product: Product, version: string | undefined): string {
76 const tags = (product.tags ?? []).map(t => t.label).filter(Boolean)
77 const facts = [product.status, version, ...tags].filter(Boolean).join(' | ')
78 const lines = [
79 product.tagline ? `**${product.tagline}**` : '',
80 facts,
81 product.meta ?? '',
82 product.description ?? '',
83 ].filter(l => l !== '')
84
85 return lines.join('\n\n')
86}
87types/index.d.ts 16 lines1export type View = {
2 id: string
3 title: string
4 link: string
5 markdown: string
6 bannerFile: string | null
7 generation: number
8 error: string | null
9}
10
11declare module 'claude-code' {
12 interface PluginState {
13 'page-pane': { view: View | null }
14 }
15}
16