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.jsarbitrarily - Write custom modules
- Fork and maintain your own distribution
- Use commercially
Build Phases
Phase 1 — Foundation (Software-first, no hardware yet)
- Fork MagicMirror² to GitHub (via web UI if gh token lacks fork permission). You can rename the fork — e.g., this user forked as
HermesMirrorrather than keeping the original name. The fork name has no functional impact. - Clone fork to this Mac, set up upstream remote:
git clone https://github.com/<your-username>/<fork-name>.git git remote add upstream https://github.com/MagicMirrorOrg/MagicMirror.git - Secure install procedure (always use these steps, in order):
a. Vet dependencies — before installing, assess the dependency tree. See
supply-chain-hardeningskill, Layer 0 (Pre-Install Dependency Risk Assessment). b. Pin exact versions — strip^from all production dependencies inpackage.jsonto 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, thennpm audit fixfor safe fixes f. Prune —npm pruneto remove orphaned transitive dependencies - Run MagicMirror² on this Mac (runs as Electron on macOS,
npm startfor x11) - Build Hermes Receiver module (HTTP listener + DOM renderer)
- Wire one cron job (e.g., daily brief push)
- 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.jsextracted pure functions — don't keep local copies innode_helper.jsor you getIdentifier has already been declared.gitignoreline 58 (/modules/*) ignores ALL modules — usegit add -f modules/hermes-*- Server-only mode: bridge's
node_helper.jsmust self-start polling instart()becauseDOM_OBJECTS_CREATEDnever 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:
- Fork via GitHub web UI: navigate to upstream repo → click Fork
- Clone locally:
git clone https://github.com/<your>/<fork>.git - Set upstream remote:
git remote add upstream https://github.com/<original>/<repo>.git - 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-hardeningskill 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 viaconfig.js. UseModule.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 fromnode_helper.js. Having bothconst { diff } = require("./board-utils")ANDfunction diff(...)in the same file throwsIdentifier has already been declared. - Module CSS live reloading: MagicMirror² caches CSS aggressively. Changes to
*.cssrequire 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:
::beforepseudo-element withlinear-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
spanin card header withopacity: 0.65 - Priority dots: 6px colored circles with
box-shadowglow — 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:
- Create a self-contained HTML file with 3-4 style variants side-by-side
- Use the same sample tasks across all variants for fair comparison
- Include empty state comparison at the bottom
- Tell the user to open locally with
open docs/file.htmlor view on the mirror atlocalhost:8080/docs/file.html - Once chosen, apply the CSS + JS changes and restart the server
Headless server notes
- Always use
node serveronly/index.js— nevernpm start(launches Electron fullscreen) - Bridge
node_helper.jsmust self-start polling instart()with default config —DOM_OBJECTS_CREATEDnever 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:
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:
{
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:
{ 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):
node -c modules/hermes-bridge/hermes-bridge.js
node -c modules/hermes-bridge/node_helper.js
npm run config:check
Unit tests (Vitest):
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:
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:
git add -f modules/hermes-bridge modules/hermes-dashboard modules/hermes-status
git ls-files modules/ # verify tracked
Pre-Push Checklist
git add -f modules/hermes-*git ls-files modules/— verify trackednpm run config:check— cleannpm run lint:js— no new errorsnpm run lint:css— passesgit push --dry-rungit 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 where0is a valid key (e.g.,statusOrder = { blocked: 0, running: 1 }),|| fallbacktreats0as falsy and skips it. Use?? fallbackinstead. Example bug:blockedtasks sorted last because0 || 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-patternoverride to stylelint config - CSS changes need server restart — MagicMirror caches CSS aggressively
- ESLint
globalIgnoresblocks custom modules — the flat config ateslint.config.mjshasglobalIgnores(["modules/**/*"])which excludes ALL modules from linting. Change tomodules/default/**to lint only the upstream default modules while enabling linting for custom hermes-* modules. Theignores: []override approach does NOT work —globalIgnorescannot be overridden by later config entries. - Unused callback params in
Module.register()— MagicMirror callbacks likenotificationReceived(notification, payload, sender)have required signatures by convention but JS doesn't enforce arity. If you don't usepayload/sender, just remove them from the function definition rather than prefixing with_. The_prefix requiresargsIgnorePattern: "^_"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.jscallsNodeHelper.create()andrequire("logger")at module load time. Mock both before requiring the module:vi.mock("node_helper", () => ({ create: vi.fn((obj) => Object.assign({ name: "test" }, obj)) }))andvi.mock("logger", () => ({ log: vi.fn(), warn: vi.fn(), error: vi.fn() })). Thenrequire("./node_helper")returns the testable object. Usevi.useFakeTimers()for backoff/timer testing.
Integration Module Reference Files
references/hermes-architecture.md— gateway route architecture, plugin system gap, kanban data shape, health checksreferences/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 8644references/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 forkreferences/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 taskssupply-chain-hardening— pre-install dependency risk assessment (run before any npm install on the mirror project)github-repo-management— forking, cloning, upstream remote setup