A little pixel dude who walks onto your screen and hands you a glass of water — and a delightful face for any notification you throw at him.
Because a notification is easy to ignore — a character strolling across your desktop is not.
I miss recurring self-care reminders when I'm heads-down at work. A plain macOS notification slides in, I glance past it, it's gone. A notification is easy to ignore. A character walking across your screen is not.
So Remi is a menu-bar app that sends a small pixel-art buddy walking in from the corner holding a glass of water. He waits, you tap Had it 💧 (he cheers) or Snooze 15m (he sulks off), and your progress ticks up. That's v0.1 — make the nudge delightful instead of nagging, and you'll actually drink the water.
v0.2 grows him up: the same buddy is now a face for any notification — a meeting, a Slack DM that needs a reply, an email worth your attention — driven by a tiny local API that cron jobs and AI agents can call. Remi stays dumb and delightful; the smarts live outside. See Reminders vs Notifications for the mental model.
The water buddy
- 💧 Water reminders on a timer, only inside your configured work hours.
- 🚶 A buddy who walks in — 4-frame walk cycle into the corner, arrives holding a glass, breathes with an idle bob, walks back out. Soft two-note chime on entrance.
- ✅ Snooze / Had-it — two buttons (or a 30s no-answer soft snooze).
- 📅 Daily history, 🔥 streaks (shown in the menu-bar title + a one-off "🔥 N-day streak!" bubble), and a 🗓️ calendar view (tray → View progress, a month grid tinted by how close you got each day).
- ⚙️ Live Settings submenu — interval / work-hours / goal / snooze, applied instantly.
The notification framework (new in v0.2)
- 🔔 Local notification API — a loopback HTTP server (
127.0.0.1, token-auth) any producer can call. Two kinds:infopeeks (buddy leans in from the edge, says one thing, retracts) andactionnotifications (message + buttons whose result is returned to the caller). See Notifications. - 🎭 Expressive poses — Remi reacts per state: 😔 sad when you snooze water, 🎉 fists-up when you drink it, 👋 hand-up when peeking in, 🧍 hands-folded for an action ask.
- 🛠️
remi-notifyCLI + client sample + a local test UI to drive the API by hand. - 🪶 Stays out of your way — frameless, transparent, always-on-top corner window, hidden ~99% of the time, never steals focus, lives in the menu bar with no dock icon.
git clone https://github.com/Weekend-Labs/remi.git
cd remi
npm install # pulls Electron (dev dependency)
npm start # run the app — buddy appears on the reminder timer
npm run demo # same, but the buddy walks in ~2.5s after launch (no waiting)
npm test # pure-logic + API unit/e2e tests (node --test, 59 tests)
npm run package # build Remi.app into dist/ (macOS arm64, with the .icns icon)
# poke the notification API (with Remi running):
node examples/test-ui.js # a clickable test page at http://localhost:7788
node examples/notify-client.js # or drive it from the command lineOr just download the latest release
and drag Remi.app to /Applications (unsigned — first launch: right-click → Open, or
xattr -cr /Applications/Remi.app).
npm start puts Remi in your menu bar (no dock icon). Click the 💧 0/8 title for
the tray menu — Remind now, View progress, Pause, Settings, Test peek, Quit.
Remi runs a loopback-only HTTP server (127.0.0.1, bearer-token, never exposed to the
network). Any producer — a cron job, a shell script, or an AI agent — fires a notification
and (for actions) hears your reply back:
POST http://127.0.0.1:7777/notify Authorization: Bearer <token>
{ "type": "action", "message": "Reply to Sam's DM?",
"actions": [ { "label": "Draft reply", "result": "draft" }, { "label": "Snooze", "result": "snooze" } ] }
→ { "id": "n_123" }
GET http://127.0.0.1:7777/notify/n_123 → { "reply": "draft" } # the producer hears back and actsinfo(peek) — buddy leans in, delivers, strolls off. Fire-and-forget. "Standup in 5."action— a message + buttons; your choice is returned to the producer, so an agent can then send the reply / archive the mail / open the meeting.
The token is generated on first run into config.json. Grab it, then drive the API from:
- AI agents — an MCP server (
claude mcp add … remi …) or a Claude skill. - Scripts / CLI —
bin/remi-notifyor the client sample. - By hand — the clickable test UI at
localhost:7788.
Full contract: F001 spec · mental model: docs/CONCEPTS.md.
The one rule: Remi stays dumb — no Gmail/Slack SDKs in the app. Every "is this worth interrupting me?" decision is the producer's (the model's) job. Remi is just the megaphone.
The character is me, rendered as Stardew-style pixel art from a few selfies. You can replace it with anyone — a photo of you, your cat, a robot — with the same repeatable pipeline (AI image → magenta-keyed sprite → cutout), including per-mood poses. Copy-paste prompts and all in:
Shipped in v0.2 — the notification framework: loopback API, info peeks + action
replies, remi-notify CLI, client sample + test UI, expressive per-state poses, and
agent producers — an MCP server and a Claude skill so an AI agent can
fire notifications and read your reply.
Next — the framework's whole point is that a new reminder type is just another producer:
| What | Powered by | |
|---|---|---|
| Smart triage | An agent reads Slack & email, filters the noise, only interrupts you for what matters | Claude / Codex |
| Presence-aware delivery | Buddy at the desk; WhatsApp / push when you're away or locked | presence + bridge |
Reminder types on deck: 📅 meetings · 💬 pending Slack DMs · 📧 reply-worthy email · ⏰ ad-hoc.
💡 A design direction, not a promise of dates. Ideas and PRs welcome.
- docs/CONCEPTS.md — Reminders vs Notifications — the core mental model (start here).
- docs/DESIGN-YOUR-OWN-BUDDY.md — build your own character (AI prompts + the
tools/*.pypipeline). - docs/specs/ — feature specs (F001 notification framework, F002 Buddy V2).
- SPEC.md — the product story: goal, scope, stack decisions.
- BACKLOG.md — the code map and how features were scoped.
Producers (each lives with its code): mcp/ (MCP server) · skill/ (Claude skill) · examples/ (CLI client + test UI).
macOS on Apple Silicon (arm64) only, for now. The npm run package script builds a
darwin/arm64 bundle (Remi.app) with a macOS .icns icon. Nothing in the code is deeply
macOS-specific — it's Electron — but the transparent, click-through, always-on-top corner
overlay is only exercised on macOS, and the packaging target is hard-coded to Apple Silicon.
Other targets would need a tweak to the package script and a pass over the overlay window
flags. PRs welcome.
Remi is deliberately tiny: a timer, a loopback API, two JSON files, and some pixel sprites. No database, no framework beyond Electron, no animation library.
┌─ main process (src/main.js) ──────────────────────────────┐
│ setInterval every 30s → rollover + shouldRemind? │
│ Loopback notification API (src/api.js, src/notify.js) │
│ Tray menu (title + Settings submenu + calendar/pause) │
│ Owns two BrowserWindows: │
│ • overlay → transparent corner window (the buddy) │
│ • calendar → framed month-grid window (View progress) │
└──────────┬───────────────────────────────┬────────────────┘
│ IPC (via src/preload.js) │ IPC
▼ ▼
┌─ overlay renderer ──────────┐ ┌─ calendar renderer ────────┐
│ renderer/index.html + CSS │ │ renderer/calendar.html │
│ renderer/overlay.js │ │ renderer/calendar.js │
│ water: walk-in→react→out │ │ month grid tinted by │
│ peek / action / poses │ │ history[date] │
│ sprites: walk.png + │ └────────────────────────────┘
│ buddy-{hold,sad,peek, │
│ action,cheer}.png │
└─────────────────────────────┘
▲
producers │ POST /notify (cron · scripts · agents)
───────────┘ over 127.0.0.1 loopback + token
Pure, tested logic (no Electron, no I/O):
src/reminder.js rollover · shouldRemind · applyAction · streak · todayStr
src/calendar-grid.js monthGrid · dayLevel
src/notify.js request validation · queue + lifecycle · ttl
src/state.js load/save state.json + config.json · isValidConfig
test/*.test.js node --test (reminder + settings + calendar + notify + API e2e)
The split that matters: all the interesting logic is pure functions in reminder.js,
calendar-grid.js, notify.js, and state.js, tested with plain node --test and zero
Electron in the loop. main.js is just wiring — timer, tray, windows, API, IPC. The renderer
is HTML/CSS sprite animation. That's why the suite runs in milliseconds and stays green
regardless of the UI.
State and config are plain JSON under Electron's per-app userData directory
(app.getPath('userData')). On macOS:
~/Library/Application Support/Remi/state.json # date, glassesHad, goal, snooze, paused, history{}
~/Library/Application Support/Remi/config.json # intervalMinutes, workHours, snoozeMinutes, goal, apiToken, apiPort
Both are created with sane defaults on first run and migrate forward if a field is missing —
you can hand-edit them, but the Settings submenu covers everything. apiToken is the
bearer token for the notification API. Delete the files to reset.
MIT © 2026 Vijay Chhuttani. Free for any purpose — fork it, reskin it, ship it.
