DeepSeek Harness Remote
DeepSeek Harness Remote: DeepSeek Harness 실행 PC와 모바일·PC를 안전하게 연결해 어디서나 작업 이어받기
행동할 때까지 남는 팝업, 실시간 카운트다운과 원터치 줌·미트·팀즈·웹엑스 참여, Mac이 없으면 전화 푸시 알림으로 지속
A popup that stays until you act, with a live countdown and one-tap Join for Zoom, Meet, Teams and Webex. Away from the Mac? A phone push that escalates until you acknowledge it. Reads every macOS cal
생산성Swift · ShellGitHub @roypadina온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
Never miss a meeting: unmissable popup with live countdown, one-tap Join, plus phone push escalation. Free, open-source macOS menu-bar app.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Desktop popup + phone push + urgent escalation — so "I didn't see the calendar reminder" stops being an excuse.
brew install --cask roypadina/tap/meetalert
↩/J join · 5 snooze 5m · 1 snooze 1m · T until start · esc snooze. The two final actions need a modifier on purpose, so ordinary typing can never kill an alert: ⌘D dismiss, ⌘⇧I ignore.escalationRepeats re-pushes (3 by default, never past 15 minutes after the meeting starts) until you ack or snooze, not just one.brew install --cask roypadina/tap/meetalert
macOS 14 Sonoma or later · Apple Silicon and Intel (universal binary)
MeetAlert is ad-hoc signed (not notarized). If macOS blocks it, open System Settings → Privacy & Security, scroll down and click Open Anyway next to MeetAlert (right-click → Open no longer works on macOS 15+). Or clear quarantine once:
xattr -dr com.apple.quarantine /Applications/MeetAlert.app
On first launch macOS will prompt for Calendar access — MeetAlert can't see your meetings without it. A Welcome window then offers to open MeetAlert at login (you can change it any time in Settings → Alerts, or in System Settings → General → Login Items & Extensions); MeetAlert never adds itself silently. Reopen the Welcome window any time from Welcome Guide… in the status menu. See the Installation wiki page.
git clone https://github.com/roypadina/MeetAlert.git
cd MeetAlert
./build.sh
open build/MeetAlert.app
build.sh runs swift build -c release --arch arm64 --arch x86_64 (universal) and assembles/ad-hoc-signs build/MeetAlert.app.
No Xcode project — it's a plain Swift Package executable.
This is the part that makes escalation actually reach your phone — don't skip it.
meetalert-9f3a1c7b2e.
A topic name is effectively a password: on the public ntfy.sh server, anyone who knows the
name can publish to it or read it. Don't use anything guessable.ntfy.sh, unless
you're self-hosting).ntfy.sh) into MeetAlert: menu bar icon →
Settings… → ntfy section, or edit ~/.config/meetalert/config.json directly.
ntfyTopicis empty by default — phone push, and escalation with it, stays completely off until you set one. This step is required for anything beyond the desktop popup.
Self-hosted ntfy servers work the same way — just set ntfyServer to your own instance. If it
requires a login, paste an ntfy access token in Settings → ntfy (stored in the macOS Keychain,
sent only to that server, including inside the ACK / Snooze buttons); an optional fallback topic
URL gets the alert (without buttons) if the main server is unreachable. Details in the
ntfy Setup wiki page. Keep
message caching enabled on the server (the default) — with cache-duration: 0, there's no
message history left to poll, so MeetAlert can never see an ACK/snooze reply and escalates the
full escalationRepeats times regardless of whether you actually acked.
The ntfy Android app has a "Keep alerting for highest priority" setting (off by default) that loops an alarm sound for every priority-5 notification until it's stopped. It sounds like exactly what MeetAlert wants. It isn't — tapping ACK cannot stop it.
In the app, that loop is one shared MediaPlayer, and only two things ever stop it:
An action button with clear: true — which is what MeetAlert's ACK and Snooze 5m are —
cancels the notification programmatically. A programmatic cancel does not fire the delete
intent, so the loop keeps playing over a notification that is already gone and already marked
read. Same for a view action with clear: true. There is no publish-side way to stop it either:
nothing you can send to the topic silences a ring that's already started.
The result, if you enable it: ACK reaches MeetAlert and stops the escalation correctly, and the phone keeps ringing anyway until you separately swipe the (vanished) notification or open the topic. Swiping first doesn't help — ntfy has no dismiss callback, so a swipe tells MeetAlert nothing, isn't an ack, and the next escalation re-push starts the ring again.
So leave that setting off and let MeetAlert own the repetition: set a short escalationSeconds
and a higher escalationRepeats (e.g. 45 and 8 ≈ six minutes of pings). Every ping is a
normal priority-5 notification you can silence, and a single ACK genuinely ends all of them.
One instrument, read from across the room: a big monospaced-digit countdown, and a colour that changes with urgency rather than decoration.
| State | When | Colour | Countdown |
|---|---|---|---|
| Starting in | more than 60 s to go | cyan | minutes (3 / min) |
| Starting in | 60 s or less | amber | seconds (35 / sec) |
| Started | at or after the start | coral | now, then +2 / min late |
| Not acknowledged — phone alerted | escalation has pushed to your phone | keeps the state colour | unchanged, rim thickens and pulses |
Urgency is never colour alone — the state word, the icon, the rim width and the countdown's unit all change together, so it survives colour-blindness, Increase Contrast (which drops the translucency for an opaque background and a heavier border) and Reduce Motion (which keeps the fades and drops the movement).
The row of actions is Join (the one filled button, ↩), Snooze 5m,
Dismiss (⌘D), Ignore/Ignore series (⌘⇧I), and More ▾ holding the two
remaining snoozes. With no meeting link, Dismiss becomes the filled button — there's always
exactly one. Nothing about the layout changes on hover; only the buttons themselves light up.
| When | What happens |
|---|---|
Each offset in alertMinutesBefore (before, at, or after start), plus one travelLeadMinutes offset for meetings with a physical address | Desktop popup appears on whichever screen your mouse is on, and an ntfy push goes out with ACK, Join (if there's a meeting link), and Snooze 5m actions. Tapping the push body itself opens the meeting link. |
You're away from the Mac (idle past awayIdleSeconds, or the screen is locked) | That first push skips the high-priority grace window and goes straight out at urgent priority — nobody's at the desk to see the popup anyway. |
| Several offsets of the same meeting come due in one scan (e.g. the event synced in late) | They collapse into one alert — the latest due one — instead of stacking popups. |
| Anything up to escalation | Clicking Dismiss, Join, Snooze, Till start, or Ignore forever on the popup — or tapping ACK on the push — counts as acknowledged; escalation stops. Tapping Snooze 5m on the push snoozes it (clamped to the meeting's start) and also stops escalation. Tapping ACK or Snooze 5m on the push also clears that notification from the phone and marks it read — but it does not stop ntfy's insistent ring if you enabled that; see Why not ntfy's insistent ring?. Tapping Join on the push only opens the meeting — it does not ack (that action never reports back to MeetAlert); the desktop popup's Join button does both. |
| Dismiss/ACK/Join on any of a meeting's alerts | That whole meeting occurrence is done: every remaining offset (including a not-yet-fired travel-lead or at-start alert) is suppressed, and any pending snooze for it is dropped. Snoozing, by contrast, only quiets that one alert until the snooze comes due. |
escalationSeconds after an unacked alert, repeating | The push resends at urgent priority (rotating_light tag, titled and numbered ) and the desktop popup switches to a pulsing state — up to times ( by default), and never past after the meeting's start. If an alert itself first fires later than that (e.g. a very late offset), there's no escalation window left at all — just the one initial push. |
Each offset of a meeting occurrence alerts once (tracked in state.json); a snooze that comes
back due re-shows the popup without re-sending a push or re-arming escalation. MeetAlert also
rescans immediately on system wake, not just every 30 seconds.
Everything lives in ~/.config/meetalert/config.json, written with defaults on first launch,
and re-read every 30 seconds — so a hand edit (or a Settings-window change) applies without
restarting the app. Menu bar icon → Settings… gives you a native GUI for all of this except
raw file editing.
| Field | Default | Meaning |
|---|---|---|
alertMinutesBefore | [3, 0] | Minutes before start to fire an alert — one entry per alert. 0 = at start, negative = that many minutes after start (a late alert). Deduplicated and sorted (descending) on load. |
lateAlertMinutes | 10 | Still fire a missed alert up to this many minutes after its scheduled time (sync-lag cushion). |
escalationSeconds | 120 | Seconds between each escalation re-push (never past 15 minutes after the meeting's start). |
escalationRepeats | 3 | How many urgent re-pushes before MeetAlert gives up. Raise it (with a shorter escalationSeconds) to replace ntfy's insistent ring with repetition MeetAlert controls — an ACK stops these, insistent can't be stopped. |
awayIdleSeconds | 120 | Idle time (or an immediate screen lock) before MeetAlert treats you as away and sends the first push at urgent priority instead of high. |
travelLeadMinutes | 30 | Extra early alert for meetings with a physical (non-video-call) location. |
agendaTime | null | Minutes since midnight (local time) to push today's agenda — e.g. 619 = 10:19; use the Settings time picker instead of computing this by hand. null = off. A legacy agendaHour value migrates automatically. |
ignoreAllDay | true | Skip all-day events entirely. |
ignoreKeywords | [] | Case-insensitive substrings — any event title containing one is skipped. |
ntfyServer | "https://ntfy.sh" | Base URL of your ntfy server. |
ntfyTopic | "" (empty) | Your private ntfy topic. Empty means phone push is off — desktop popups still work, nothing goes to your phone. See ntfy setup. |
Declined invites are always skipped — there's no config knob for it.
state.json in the same folder tracks which occurrences already alerted (alertedKeys, pruned
after 24h) and which are permanently ignored (ignoredKeys) — see the
Configuration wiki page for the
exact key format if you ever need to hand-edit it (e.g. to un-ignore something).
ignoreAllDay).ignoreKeywords) to
skip any event whose title matches, e.g. focus time, lunch.If MeetAlert finds a Zoom, Google Meet, Microsoft Teams, Webex, Whereby, GoToMeeting, Jitsi, Chime, BlueJeans, or RingCentral link in the event's URL, location, or notes (checked in that order), the desktop popup, the ntfy push, and the menu bar all get a Join action — but they behave slightly differently. On the desktop popup, Join opens the link and acks in one step, same as Dismiss. On the phone push, Join only opens the link — it can't ack, since that kind of action never reports back to MeetAlert; use the ACK button on the push if you want to ack from your phone. In the menu bar, any upcoming meeting with a link is a clickable Join row — clicking it only opens the link, so the alerts for that meeting still fire as scheduled.
agendaTime, off by default) — pick a time (to the minute) in Settings →
Phone and MeetAlert pushes a single ntfy summary once a day: how many meetings, the first one,
up to 6 upcoming, and your largest free gap before 7pm.Set MEETALERT_TEST=1 to run a fully headless self-test: no calendar access is requested (no
TCC prompt, no EventKit touched at all), and 10 seconds after launch it fires a synthetic
[TEST] MeetAlert pipeline meeting 3 minutes out, with the escalation window forced to 20
seconds. State transitions print to stdout so you can watch the whole pipeline:
MEETALERT_TEST=1 build/MeetAlert.app/Contents/MacOS/MeetAlert
# fired a1b2c3d4
# acked a1b2c3d4 (if you click a button in the popup, or ACK the push)
# escalated a1b2c3d4 (if nothing acks it within 20s)
The token is a short hash of the alert's own key (meeting + offset), not a timestamp — it's just an opaque id shared between the "fired"/"acked"/"escalated" lines for the same alert.
Run the executable inside the built .app directly (not .build/release/MeetAlert, and not
via open) so the bundle's Info.plist/LSUIElement context is intact and stdout stays
attached to your terminal.
Test mode forces a few things for determinism: you're always treated as "present" (never away),
the escalation loop caps at a single urgent resend instead of escalationRepeats, and the morning-agenda push
is skipped entirely.
MeetAlert doesn't talk to any calendar API — it reads whatever macOS's own EventKit already has, which is every calendar synced through System Settings → Internet Accounts: Google, iCloud, Exchange, CalDAV, whatever. Zero setup on MeetAlert's side.
The tradeoff is that you're at the mercy of macOS's own sync interval for that account. If
last-minute meetings are showing up late, open Calendar.app → Settings → General and set
"Refresh calendars" to Every minute. lateAlertMinutes exists specifically to cushion
whatever lag remains.
| Alerts settings | Phone settings |
|---|---|
Have an idea for MeetAlert? Request a feature and say what you're trying to do; that matters more than the exact feature. Found a bug? Report it.
Code is welcome too: small, focused pull requests, and an issue first for anything bigger. See CONTRIBUTING.md.
Tipped on Ko-fi? Add your Ko-fi name to the request and it goes to the front of my queue. It's a thank-you, not a purchase: no promise on what ships or when.
Free, open-source Mac menu-bar apps and terminal tools, all on one site: apps.roypadina.synology.me.
If MeetAlert saves you from walking into a meeting 10 minutes late, you can support its development — it's optional and always appreciated.
A ⭐ on the repo helps just as much.
MIT © 2026 Roy Padina
meetalert://settings|snooze|dismiss is there for Shortcuts and Raycast.escalationRepeatsUp to lateAlertMinutes after an offset's scheduled time | MeetAlert still fires that alert even if it only saw the event this late — covers sync lag between Google/Exchange and macOS's local calendar cache. |
Once a day, at or after agendaTime (if set) | A single ntfy push (default priority, no actions) summarizes today's meetings and your largest free gap. |
calendarColors | {} | Per-calendar colour override for the popup's calendar dot, as calendarIdentifier → "#RRGGBB". Anything missing (or an unparseable value) falls back to that calendar's own macOS colour. Set it from Settings → Calendars rather than by hand. |
calendarIds | null | null = every calendar. [] (untick every calendar in Settings) = watch nothing, on purpose — menu bar shows "no calendars selected". A non-empty list that matches zero live calendars (e.g. an account was re-added and identifiers rotated) is treated as stale, not deliberate: MeetAlert falls back to every calendar and shows a ⚠︎ warning instead of going silently dead. |