@hasna/notifications
v0.1.0
Published
User notifications from AI agents and apps: send, list and revive them, shown as native-looking macOS banners with a menu-bar history. Server on SQLite or PostgreSQL.
Maintainers
Readme
@hasna/notifications
Notifications for you, sent by your AI agents, apps and scripts. They pop up on your Mac as native-looking banners, stack at the top right, and stay in a menu-bar history where you can bring any of them back.
- CLI
notifications— send, list, read, dismiss, revive, follow live - MCP
notifications-mcp— lets coding agents notify you - Server
notifications-serve— HTTP API on PostgreSQL or SQLite - SDK
@hasna/notifications/sdk— for other apps - macOS app — menu-bar bell, banners, history (built from source on your Mac)
notifications send "Deploy finished" -b "emails-prod is live on 1.4.2" -l success \
-a "Open dashboard=https://example.com/deploys/42"Two ways to run it
Hosted (Hasna)
Clients use https://api.hasna.com/notifications/v1 by default. Put your API
key in the credential chain every Hasna client reads — the macOS Keychain item
hasna.credentials.notifications.api-key, the file
~/.hasna/notifications/config/credentials (HASNA_NOTIFICATIONS_API_KEY=…,
mode 0600), or the HASNA_NOTIFICATIONS_API_KEY environment variable — and
everything (CLI, MCP, SDK, macOS app) connects there. With no key, clients
fail closed; they never fall back to a local database.
On your own server
One machine, one command:
bun add -g @hasna/notifications
notifications setup # server + key + credentials; on macOS also a LaunchAgent
notifications app install # macOS: build and start the menu-bar appsetup writes ~/.hasna/notifications/config/server.env (signing key, port)
and ~/.hasna/notifications/config/credentials (URL + API key), both 0600, and
checks the server answers. The server uses SQLite at
~/.hasna/notifications/notifications.db. For PostgreSQL, run setup with
HASNA_NOTIFICATIONS_DATABASE_URL set in the environment.
Docker: docker compose up (see docker-compose.yml; SQLite on a volume, or
set HASNA_NOTIFICATIONS_DATABASE_URL).
Storage switch (server only): PostgreSQL when
HASNA_NOTIFICATIONS_DATABASE_URL is set; otherwise SQLite at
HASNA_NOTIFICATIONS_SQLITE_PATH (default above). A server listening on a
non-loopback interface refuses to start unless one of the two is set
explicitly, and refuses to start without a signing key. PostgreSQL uses TLS
when the URL asks for it (sslmode=require, verify-ca or verify-full),
verifying the server against HASNA_NOTIFICATIONS_PG_CA_FILE or
PGSSLROOTCERT when set (the Docker image ships the Amazon RDS bundle).
setup refuses to replace a credentials file that points at another server
(for example the hosted service); pass --force to replace it, and the old
file is kept as a backup.
CLI
notifications send <title> send a notification
-b, --body <text|-> body ("-" reads stdin)
-l, --level <level> info | success | warning | error
-p, --priority <priority> low (history only) | normal | high (stays) | urgent (stays + sound)
-u, --url <url> what clicking it opens
-a, --action <label=url> link button, repeatable (max 4)
-c, --category <name> e.g. email, build, deploy
-t, --tag <tag> repeatable
-g, --group <key> [--replace] stack related ones; --replace updates the newest in place
--dedupe <key> a repeat send returns the first notification
--expires <30m|2h|1d> stop showing it after a while
--silent history only
--source <label> who it is from (default: detected agent, or "cli")
notifications list [--unread] [-s state] [-n N] [-q text] [--source s] [--before id]
notifications show <id>
notifications read <ids…> | --all mark read
notifications unread <ids…>
notifications dismiss <ids…> | --all
notifications revive <ids…> show again as a banner
notifications delete <ids…>
notifications tail follow changes live
notifications stats | status
notifications setup [--port 7766] [--rotate-key] [--no-service]
notifications server start|stop|status local server LaunchAgent (macOS)
notifications app install|start|stop|status
notifications app ctl <command> script the running app (state, snapshot, …)Every command takes --json.
MCP (agents)
{ "mcpServers": { "notifications": { "command": "notifications-mcp" } } }Tools: send_notification, list_notifications, get_notification,
revive_notification. Agents send and look; marking read and dismissing is
left to you.
SDK
import { createNotificationsClient, notify } from "@hasna/notifications/sdk";
await notify("Build finished", { level: "success", url: "https://ci.example.com/run/991" });
const client = createNotificationsClient();
for await (const event of client.stream()) console.log(event.type, event.notification?.title);macOS app
notifications app install builds the app from the Swift sources in this
package (swift build, Xcode command line tools, macOS 14+), ad-hoc signs it,
installs ~/Applications/Notifications.app and starts it at login. It needs no
macOS permissions.
- Menu bar: a bell with the unread count. Click it for the history: All or Unread, search, grouped by day. Click any entry to show it again as a banner.
- Banners: top right, newest on top, up to five. Normal ones hide after a few seconds (paused while you hover); high and urgent ones stay. Drag any banner to move the stack; "Reset Banner Position" puts it back.
- Buttons: link actions open their URL. A click on the banner opens its URL, or the history when it has none. The ✕ dismisses it everywhere.
HTTP API
/v1/notifications (POST send, GET list), /v1/notifications/{id} (GET,
DELETE), /v1/notifications/{id}/{read|unread|dismiss|revive},
/v1/notifications/{read-all|dismiss-all}, /v1/stats, /v1/events?since=,
/v1/stream (Server-Sent Events; resumes from Last-Event-ID). Public:
/health, /ready, /version, /v1/openapi.json. Keys:
hasna_notifications_* tokens in x-api-key or Authorization: Bearer;
reads need notifications:read, everything else notifications:write.
Safety
Buttons only open web and mail links (http, https, mailto) — nothing
on your Mac is ever executed from a notification. Every other scheme
(file:, javascript:, smb:, ssh:, app-registered schemes, …) is refused
by the server and again by the macOS app.
Secrets never go on a command line or into a LaunchAgent plist.
License
Apache-2.0
