Build and maintain a Hermes-driven smart mirror / information display — MagicMirror² on Raspberry Pi, custom module for Hermes push integration, scheduled + on-demand content delivery.
Scanned 9/10/2026
Install to Claude Code
npx -y skills add theheavenlyd3mon/hermes-profiles --skill smart-mirror --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Smart Mirror?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/theheavenlyd3mon-smart-mirror)More formats (shields.io, HTML) on the badges page.
---
name: smart-mirror
description: "Build and maintain a Hermes-driven smart mirror / information display — MagicMirror² on Raspberry Pi, custom module for Hermes push integration, scheduled + on-demand content delivery."
version: 1.0.0
author: senna
license: MIT
platforms: [linux, macos]
metadata:
hermes:
tags: [Smart-Home, IoT, Display, Raspberry-Pi, Dashboard, MagicMirror]
homepage: https://github.com/MagicMirrorOrg/MagicMirror
prerequisites:
commands: [node, git]
---
IDENTITY: Architect.SmartMirrorBuilder. Fork→AuditDeps→SecureInstall→BuildReceiverModule→WireCron→DeployRPi.
Law: PreInstallDependencyAuditIsNonNegotiable.AlwaysVetBeforeInstall.
WHENUSE: User wants smart mirror/MagicMirror²/Hermes display|IoT dashboard|Raspberry Pi information display. ESPECIALLY:ForkAndCustomize|DependencyRiskAssessment|HermesReceiverModule. NoSkip:SupplyChainHardeningLayer0|SecureInstallProcedure|SameLANRequirement.
REDFLAGS: InstallWithoutVetting->RunDependencyAssessment|SkipPinVersions->StripCaretRanges|ProdInstallWithDevDeps->UseOnlyProd.
RATIONALIZATIONS: GitHubStarsEnough->AuditActualDeps|FastSetup->Phase1SoftwareFirst.
QUICKREF: Phase1(Fork+Clone+SecureInstall+ReceiverModule+CronWire)➔Phase2(DataSources{weather,packages,reminders,briefs})➔Phase3(PhysicalBuild{RPi+Display+Frame}).
# Smart Mirror — Hermes-Driven Information Display
A physical smart mirror / dashboard driven by Hermes: MagicMirror² renders the UI on a Raspberry Pi; Hermes pushes data (weather, briefs, reminders, package tracking) over the local network.
## Architecture (Hybrid Approach — Use This)
```
┌─────────────────────┐ HTTP POST (JSON) ┌──────────────────────┐
│ Hermes (this Mac) │ ──────────────────────────→│ Raspberry Pi │
│ │ local network │ MagicMirror² │
│ - cron jobs │ │ Hermes Receiver │
│ - on-demand push │←─ ACK / status ────────────│ module │
│ - daily brief gen │ │ renders content │
└─────────────────────┘ └──────────────────────┘
```
**Why Hybrid over Full Custom or Poll-only:**
- MagicMirror² provides polished UI (Electron, 23.5k☆, modular)
- Hermes is the brain — generates and pushes content, doesn't render
- Custom "Hermes Receiver" module listens on a port, receives JSON, updates DOM
- Lowest effort to working loop, highest flexibility long-term
## Hardware
| Component | Recommendation | Notes |
|---|---|---|
| SBC | Raspberry Pi 4 (2GB+) or Pi 5 | More RAM = smoother Electron |
| Display | HDMI monitor (size of mirror) | 1080p+ recommended |
| Frame | Custom wood/alu frame | Hides RPi, bezel |
| Optional | Two-way mirror glass | True magic mirror look |
| Optional | PIR motion sensor | Wakes/sleeps display |
## Software Stack
- **MagicMirror²** — v2.36.0+, MIT license (full modification allowed)
- **Node.js** — ≥22.21.1 (on RPi)
- **Hermes Receiver module** — custom MagicMirror² module (to be built)
- **Hermes cron jobs** — push schedules on this Mac
## License Note
MagicMirror² is **MIT licensed** — you can freely modify, redistribute, and use it. The only requirement is keeping the original copyright notice. This means you can:
- Modify `config.js` arbitrarily
- Write custom modules
- Fork and maintain your own distribution
- Use commercially
## Build Phases
### Phase 1 — Foundation (Software-first, no hardware yet)
1. Fork MagicMirror² to GitHub (via web UI if gh token lacks fork permission). You can rename the fork — e.g., this user forked as `HermesMirror` rather than keeping the original name. The fork name has no functional impact.
2. Clone fork to this Mac, set up upstream remote:
```bash
git clone https://github.com/<your-username>/<fork-name>.git
git remote add upstream https://github.com/MagicMirrorOrg/MagicMirror.git
```
3. **Secure install procedure** (always use these steps, in order):
a. **Vet dependencies** — before installing, assess the dependency tree. See `supply-chain-hardening` skill, Layer 0 (Pre-Install Dependency Risk Assessment).
b. **Pin exact versions** — strip `^` from all production dependencies in `package.json` to prevent range resolution from introducing unexpected code
c. **Remove unnecessary deps** — if features won't be used (e.g., pm2 process manager), uninstall them before install
d. **Install production-only** — `npm install --only=prod --omit=dev` (excludes devDependencies entirely)
e. **Audit and fix** — `npm audit`, then `npm audit fix` for safe fixes
f. **Prune** — `npm prune` to remove orphaned transitive dependencies
4. Run MagicMirror² on this Mac (runs as Electron on macOS, `npm start` for x11)
5. Build Hermes Receiver module (HTTP listener + DOM renderer)
6. Wire one cron job (e.g., daily brief push)
7. Test end-to-end pipeline: Hermes cron → POST → mirror renders
### Phase 1.5 — Dashboard UI Polish
- Glass + Glow theme for hermes-dashboard (glassmorphism, backdrop-filter, status emoji icons 🚫🔄⚡✅, priority dots with glow)
- Empty state design: checkmark icon + "All clear" + "no active tasks" stacked vertically
- Visual mockup workflow: create HTML preview with side-by-side style options (docs/dashboard-styles.html), let user choose before committing
- Common pitfalls:
- `board-utils.js` extracted pure functions — don't keep local copies in `node_helper.js` or you get `Identifier has already been declared`
- `.gitignore` line 58 (`/modules/*`) ignores ALL modules — use `git add -f modules/hermes-*`
- Server-only mode: bridge's `node_helper.js` must self-start polling in `start()` because `DOM_OBJECTS_CREATED` never fires without a browser
### Phase 2 — hermes-chat (✅ COMPLETE — 2026-06-04)
User chose **passive display** model: mirror shows the current Hermes session in real-time as a scrolling transcript. All interaction stays on phone/CLI/TUI — no mic/keyboard input on the mirror.
**Architecture:** `session-api` plugin (port 8644) reads from `lcm.db` (LCM SQLite) and serves conversation messages via HTTP. hermes-chat module has its own `node_helper.js` that polls session-api independently (does NOT extend hermes-bridge). Full design doc at `docs/HERMES-CHAT-DESIGN.md`.
**Display design choices** (user-selected via visual mockup at `docs/hermes-chat-mockup.html`):
- Layout: `bottom_left`
- Content: markdown rendering (bold, `code`, ```blocks```)
- Tool calls: configurable toggle (`showTools: true/false`)
- Long messages: truncated with fade gradient (max-height 120px)
- States: empty (💬 No active session), offline (📡 Gateway unreachable)
**Module files:** `modules/hermes-chat/` — node_helper.js, hermes-chat.js, hermes-chat.css, README.md
**Plugin files:** `~/.hermes/plugins/session-api/` — plugin.yaml, __init__.py
**Tests:** 38 client + 24 node_helper unit tests
**Config:** added to `config/config.js.sample` with all options
**Data flow:**
```
lcm.db → session-api plugin (8644) → hermes-chat node_helper (polls 5s) → Socket.IO → hermes-chat.js (renders)
```
**Key decisions:**
- Separate plugin (not extending kanban-api) — clean separation of concerns
- hermes-chat has its own node_helper — doesn't couple to hermes-bridge
- Filters `role IN ('user', 'assistant')` — tool calls collapsed or hidden
- Liquid Glass styling matching hermes-dashboard
- User wants visual mockup before committing to style choices — see `docs/hermes-chat-mockup.html`
**Session-api plugin design:** See `references/session-api-plugin.md`
### Phase 3 — Physical Build
- Set up RPi with MagicMirror² + Receiver module
- Display framing
- Two-way mirror installation (optional)
- Presence sensor for wake/sleep
- Touch overlay (optional)
## Hermes Push Mechanics
### HTTP (Recommended for Phase 1)
- Receiver module spins up a tiny HTTP server on the RPi
- Hermes POSTs JSON payloads via cron or on-demand
- Payload format: `{ "type": "brief|weather|reminder", "content": "...", "timestamp": "..." }`
- Fire-and-forget with optional ACK
### Upgrade Paths
- **WebSocket** — persistent connection for sub-second push
- **MQTT** — standard IoT pub/sub for multi-device setups
## GitHub Fork Workflow
When the fine-grained PAT can't fork/create repos:
1. Fork via GitHub web UI: navigate to upstream repo → click Fork
2. Clone locally: `git clone https://github.com/<your>/<fork>.git`
3. Set upstream remote: `git remote add upstream https://github.com/<original>/<repo>.git`
4. Sync later: `git fetch upstream && git merge upstream/main`
## Cron Schedule Patterns
| Data | Frequency | Example |
|---|---|---|
| Daily brief | Once (morning) | 07:00 |
| Weather | Every 30-60min | */30 * * * * |
| Package tracking | Every few hours | 0 */4 * * * |
| Reminders | On change or periodic | Cron + manual trigger |
| On-demand | User says "push to mirror" | Immediate |
## Pitfalls
- **Pre-install dependency audit is non-negotiable.** If the user is security-conscious (especially after a previous npm incident), always run the risk assessment before installing. Load `supply-chain-hardening` skill and use its Layer 0 (Pre-Install Risk Assessment) methodology. Categorize deps by function: static assets, parsers, network-facing — and explain the riskiest ones.
- **RPi performance**: MagicMirror² is an Electron app — Pi 3B+ struggles. Use Pi 4 (2GB+) or Pi 5.
- **Network**: RPi and Hermes must be on the same LAN. VPN or reverse proxy needed for remote access.
- **Public exposure**: MagicMirror² disables CORS proxy by default in v2.36.0 — use reverse proxy if exposing publicly.
- **Module dev**: MagicMirror² modules live in `modules/` directory, register via `config.js`. Use `Module.register()` pattern.
- **Display format**: Decide before Phase 3 — two-way mirror vs naked monitor affects framing and lighting significantly. This user's plan: monitor with a decorative frame first, possibly upgrading to two-way mirror glass later. The Receiver module is identical either way — only the physical build changes.
- **Chrome/Electrum quirks**: MagicMirror² runs in kiosk mode; JavaScript console is the primary debugging tool.
- **Duplicate pure function declarations**: When extracting pure logic to `board-utils.js`, remove the local function definitions from `node_helper.js`. Having both `const { diff } = require("./board-utils")` AND `function diff(...)` in the same file throws `Identifier has already been declared`.
- **Module CSS live reloading**: MagicMirror² caches CSS aggressively. Changes to `*.css` require a server restart to appear. For rapid iteration, preview in a standalone HTML mockup first.
## Dashboard UI Styling
### Liquid Glass theme (user's chosen style — iOS 26.5 inspired)
- **Clear frosted glass cards**: `background: rgba(255,255,255,0.03)`, `backdrop-filter: blur(24px)`, `border-radius: 14px`
- **No color tint** — pure transparency only, no pink/purple/colored gradients
- **Diagonal shine overlay**: `::before` pseudo-element with `linear-gradient(135deg, rgba(255,255,255,0.03), transparent)` for subtle depth
- **Inner highlight**: `box-shadow: inset 0 1px 0 rgba(255,255,255,0.06)` for light-catching edge
- **Status icons** (emoji): 🚫 blocked, 🔄 running, ⚡ ready, ✅ done — rendered as `span` in card header with `opacity: 0.65`
- **Priority dots**: 6px colored circles with `box-shadow` glow — red (p1), amber (p2), green (p3)
- **Status left borders**: subtle white variations — `rgba(255,255,255,0.15)` for ready/done, `rgba(255,255,255,0.25)` for running, red glow for blocked
- **Blocked card glow**: `box-shadow: 0 0 24px rgba(255,107,107,0.08)` — visible from across the room
- **Empty state**: frosted glass widget with stacked layout — ✓ checkmark (32px), "All clear" (15px), "no active tasks" (11px muted)
### Research-first design workflow
When a visual design task requires CSS effects or techniques outside agent expertise (e.g. Liquid Glass implementation), create a kanban research task for the `researcher` profile with explicit scopes and deliverable format rather than iterating on guesses. The user prefers this over trial-and-error CSS iteration.
### Visual mockup workflow
When the user wants to see UI options before committing:
1. Create a self-contained HTML file with 3-4 style variants side-by-side
2. Use the same sample tasks across all variants for fair comparison
3. Include empty state comparison at the bottom
4. Tell the user to open locally with `open docs/file.html` or view on the mirror at `localhost:8080/docs/file.html`
5. Once chosen, apply the CSS + JS changes and restart the server
### Headless server notes
- Always use `node serveronly/index.js` — never `npm start` (launches Electron fullscreen)
- Bridge `node_helper.js` must self-start polling in `start()` with default config — `DOM_OBJECTS_CREATED` never fires in headless mode
- Default gateway URL should match the kanban API plugin port (8643)
## MagicMirror² Integration Modules
The Hermes smart mirror uses centralized integration modules that plug into MagicMirror² without core file changes. Everything lives in `modules/`. The key patterns are summarized here; full reference files live under `references/`.
### Architecture: Event Bridge + Display Modules
A **centralized bridge module** (`hermes-bridge`) polls the Hermes gateway (or the kanban API plugin at port 8643), diffs state, and broadcasts `HERMES_KANBAN_*` notifications. **Display modules** consume those notifications and render DOM.
```
Bridge (node_helper.js):
- Polls GET {gatewayUrl}/api/kanban/board every 30s
- Diffs current board against lastState (task_id Map)
- Emits per-change events: HERMES_KANBAN_TASK_CREATED, _DISPATCHED, _COMPLETED, _BLOCKED, _ARCHIVED
- Emits HERMES_BOARD_STATE snapshots every 5th poll as heartbeat
- Retries with exponential backoff (1s→2s→4s→...→cap 30s)
- Self-starts in start() so headless mode works (DOM_OBJECTS_CREATED never fires without browser)
Client (hermes-bridge.js): pure relay, receives socket notifications from node_helper, re-broadcasts to all modules via sendNotification()
```
### Module Inventory
| Module | Purpose | Has node_helper? |
|--------|---------|-----------------|
| **hermes-bridge** | Centralized poller + event relay | Yes |
| **hermes-dashboard** | Kanban task cards grouped by status | No |
| **hermes-status** | Ambient 4px activity bar | No |
| **hermes-chat** | Real-time session transcript display | Yes (polls session-api on port 8644) |
### Config Format
Add to `config/config.js`:
```js
hermes: {
gatewayUrl: "http://127.0.0.1:8643", // kanban API plugin port
enabled: true,
refreshInterval: 30
}
```
hermes-chat uses a separate gatewayUrl pointing to session-api:
```js
{
module: "hermes-chat",
position: "bottom_left",
config: {
gatewayUrl: "http://127.0.0.1:8644", // session API plugin port
refreshInterval: 5,
maxMessages: 30,
showTimestamps: false,
showTools: true
}
}
```
Module order matters — bridge must be FIRST in `config.modules[]` so the broadcast channel exists before consumers start.
### Event Payload Shape
All events use this envelope:
```js
{ task_id, title, assignee, status, created_at, started_at?, completed_at?, summary?, block_reason? }
```
### Local Testing (No Pi / No Browser)
Run `node serveronly/index.js` to validate config, module structure, and bridge polling. The bridge self-initializes in headless mode. For log capture when Node.js buffers stdout in the background, use `scripts/run-server.js` (writes to both terminal and `/tmp/hermesmirror-server.log`).
**Syntax checks (no server needed):**
```bash
node -c modules/hermes-bridge/hermes-bridge.js
node -c modules/hermes-bridge/node_helper.js
npm run config:check
```
**Unit tests (Vitest):**
```bash
npm test # full suite
npx vitest run tests/unit # unit tests only
npx vitest run tests/unit/modules/hermes-bridge/ # specific module
```
For MagicMirror module testing patterns (mocking `Module.register`, jsdom environment, method testing), see `references/vitest-module-testing.md`.
**Pure function testability pattern:** Extract pure logic to a separate file with zero MagicMirror dependencies (e.g., `board-utils.js`). This makes functions independently testable without mocking `Module`, `Log`, or `document`. Export via `module.exports = { diffBoardState, statusToEvent, clamp }`. The consuming `node_helper.js` does `const { diffBoardState } = require("./board-utils")`.
### Kanban API Endpoint
The bridge polls the kanban API via a standalone Hermes plugin at port 8643. The plugin is implemented at `~/.hermes/plugins/kanban-api/` and survives Hermes updates because it lives outside the core repo. Its architecture:
```
Hermes Gateway (8642) — health/chat/jobs ← Hermes CLI/TUI
Kanban API Plugin (8643) — /api/kanban/board ← hermes-bridge poller
Session API Plugin (8644) — /api/session/messages ← hermes-chat poller
```
The plugin uses `aiohttp.web.AppRunner(handle_signals=False)` in a background thread (standard pattern for thread-safe aiohttp — signal handlers don't work in threads).
**Verification:**
```bash
curl http://127.0.0.1:8643/health # {"status": "ok", "service": "kanban-api"}
curl http://127.0.0.1:8643/api/kanban/board # {"tasks": [...], "updated_at": ...}
curl http://127.0.0.1:8644/health # {"status": "ok", "service": "session-api"}
curl http://127.0.0.1:8644/api/session/messages?limit=5 # {"session_id": "...", "messages": [...]}
```
### Git Tracking Caveat
MagicMirror's `.gitignore` line 58 (`/modules/*`) ignores ALL modules. Force-add Hermes modules:
```bash
git add -f modules/hermes-bridge modules/hermes-dashboard modules/hermes-status
git ls-files modules/ # verify tracked
```
### Pre-Push Checklist
1. `git add -f modules/hermes-*`
2. `git ls-files modules/` — verify tracked
3. `npm run config:check` — clean
4. `npm run lint:js` — no new errors
5. `npm run lint:css` — passes
6. `git push --dry-run`
7. `git push origin <branch>`
### Integration Module Pitfalls
- **Bridge loads BEFORE display modules in config.modules[]** — broadcast channel must exist first
- **Don't poll too fast** — 30s default, under 10s hammers gateway
- **Don't emit full snapshots every poll** — per-change events + heartbeat every 5th poll
- **Shallow-copy archived task payloads** — `{ ...prevTask, status: 'archived' }`, never mutate references from previous-state Map
- **Exponential backoff: separate retryDelay from poll interval** — start 1s, double, cap 30s
- **Keep fork baseline on separate branch** — don't mix upstream translation/lint updates with feature commits
- **`||` vs `??` in sort comparators** — when sorting with a lookup table where `0` is a valid key (e.g., `statusOrder = { blocked: 0, running: 1 }`), `|| fallback` treats `0` as falsy and skips it. Use `?? fallback` instead. Example bug: `blocked` tasks sorted last because `0 || 4 = 4`.
- **Markdown rendering on mirror** — use regex-based approach (escape HTML first, then apply bold/code/block patterns). No external markdown libraries needed for simple chat display. Always escape HTML before applying markdown transforms to prevent XSS.
- **BEM selectors fail stylelint kebab-case rule** — add `selector-class-pattern` override to stylelint config
- **CSS changes need server restart** — MagicMirror caches CSS aggressively
- **ESLint `globalIgnores` blocks custom modules** — the flat config at `eslint.config.mjs` has `globalIgnores(["modules/**/*"])` which excludes ALL modules from linting. Change to `modules/default/**` to lint only the upstream default modules while enabling linting for custom hermes-* modules. The `ignores: []` override approach does NOT work — `globalIgnores` cannot be overridden by later config entries.
- **Unused callback params in `Module.register()`** — MagicMirror callbacks like `notificationReceived(notification, payload, sender)` have required signatures by convention but JS doesn't enforce arity. If you don't use `payload`/`sender`, just remove them from the function definition rather than prefixing with `_`. The `_` prefix requires `argsIgnorePattern: "^_"` in ESLint config which isn't set by default. Example: `notificationReceived (notification) {` works fine.
- **Testing node_helper.js requires mocking NodeHelper and Log** — `node_helper.js` calls `NodeHelper.create()` and `require("logger")` at module load time. Mock both before requiring the module: `vi.mock("node_helper", () => ({ create: vi.fn((obj) => Object.assign({ name: "test" }, obj)) }))` and `vi.mock("logger", () => ({ log: vi.fn(), warn: vi.fn(), error: vi.fn() }))`. Then `require("./node_helper")` returns the testable object. Use `vi.useFakeTimers()` for backoff/timer testing.
### Integration Module Reference Files
- `references/hermes-architecture.md` — gateway route architecture, plugin system gap, kanban data shape, health checks
- `references/kanban-api-plugin.md` — kanban API plugin implementation (aiohttp thread-safe startup, plugin.yaml, CORS)
- `references/session-api-plugin.md` — session API plugin: reads LCM SQLite, serves conversation messages on port 8644
- `references/standalone-api-plugin-pattern.md` — general pattern for building HTTP API plugins (skeleton, port allocation, pitfalls)
- `references/session-api-plugin.md` — session-api plugin for hermes-chat (LCM database schema, session data endpoints)
- `references/integration-test-protocol.md` — step-by-step integration test procedure for the HermesMirror fork
- `references/push-checklist.md` — condensed pre-push checklist
## Related Skills
- `openhue` — Philips Hue control for smart home lighting (complementary)
- `macos-computer-use` — useful if automating macOS-side setup tasks
- `supply-chain-hardening` — pre-install dependency risk assessment (run before any npm install on the mirror project)
- `github-repo-management` — forking, cloning, upstream remote setup
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!