notifs
by Rottenwooood
README.md
# notifs
A tiny local notification relay for **agents, scripts and humans**.
Anything can POST one event; notifs deduplicates it, matches rules, and fans it out
to your channels. It also hosts your long-running monitor programs, and exposes an
**MCP server** so an agent can notify you without you pasting anything.
```
agent / script / service ──POST──▶ notifs ──▶ desktop / Feishu / ...
│
dedup · rules · retry · outbox
```
One process, one SQLite file, binds `127.0.0.1` only.
## Install
```sh
npm i -g notifs
notifs init # 建数据目录 + 一个 agent token(~/.config/notifs/token)
notifs channel add desktop --kind local --config '{}'
notifs rule add --name all --when '{}' --actions '[{"channel":"desktop"}]'
notifs notify "hello"
notifs install-service # systemd 用户服务 + 开机自启
```
## The whole contract
```http
POST http://127.0.0.1:7710/api/v1/events
Authorization: Bearer <token> # ~/.config/notifs/token
Content-Type: application/json
{"type":"experiment.finished","text":"loss 2.10→0.83 · 3h12m · ~/models/x.pt"}
```
Only `type` and `text` are required. Unknown fields are rejected (strict).
Returns `202 {accepted,deduplicated,dispatches,event_ids}` — `deduplicated:true` is
also success, don't retry.
`type` is dotted and hierarchical (`disk.low`, `github.pr.reply`); rules match it with
`*` globs. Give an explicit `dedup_key` whenever you can decide "this is the same
thing" — e.g. `train:<runId>:epoch:100` (include the *run* id, or a re-run will never
notify again).
Full contract: `notifs contract`.
## Ask — send, then wait for a reply
```http
POST /api/v1/ask
Authorization: Bearer <token>
Content-Type: application/json
{"text":"Run the full sweep now?","timeout_minutes":10}
```
```json
202 {"ask_id":"01M...","event_id":"01M...","expires_at":"...","dispatches":1}
```
The notification goes out through the normal rules. When you **quote-reply** that
message in Feishu, the ask gets answered — and the caller gets your words back:
```http
GET /api/v1/ask/01M...?wait=300 # long-poll, server caps `wait` at 600s
```
```json
{"status":"answered","reply":"go ahead, small run first","replied_at":"...","expires_at":"..."}
```
`status` is `pending` → `answered` | `expired`. Default timeout **5 minutes**, max **30**.
Two things to know:
- **You must quote-reply** the notification. A plain message carries no `parent_id`,
so there is nothing to match it against.
- The reply has to come back over a channel that can *receive*: `feishu-app` plus the
inbound long-connection producer. A group bot webhook (`feishu`) is send-only.
The MCP tool `ask` does all of this in one call and returns the reply text.
## Three ways to write a producer
| | when | how |
|---|---|---|
| **Script** | you already have cron/systemd, or it's 20 lines | just POST. No SDK, no registration beyond a token |
| **Managed** | you want notifs to own the process | `notifs watcher init <slug>` → fill `producer.json` + `index.mjs` → `watcher check/try/enable` |
| **Poller** | plain HTTP + "new item / field changed / threshold" | `notifs watcher init <slug> --kind poller`, fill the `poller` field, no code |
Managed producers get: auto-start, 30s heartbeat, crash backoff-restart, encrypted
credential injection, log capture under the data dir.
> **The honest trade-off:** a script nobody supervises can die silently — you just
> stop getting notifications, and "no notification" is indistinguishable from
> "nothing happened". That's why managed mode exists.
## Example producers
Two runnable examples live in the repo — **not in this package**, because a producer is a
program, not library code:
| Directory | What it does | Deps |
|---|---|---|
| [`producers/routine`](https://github.com/Rottenwooood/notifs/tree/main/producers/routine) | time-based reminders | none |
| [`producers/feishu-events`](https://github.com/Rottenwooood/notifs/tree/main/producers/feishu-events) | **receives** Feishu messages (long connection) → acks / `ask` | `@larksuiteoapi/node-sdk` |
```sh
git clone https://github.com/Rottenwooood/notifs.git
cp -r notifs/producers/routine ~/.local/share/notifs/producers/
cd ~/.local/share/notifs/producers/routine && npm install # only if it declares deps
notifs watcher sync && notifs watcher enable routine
```
> Sending **to** Feishu is not a producer — that's the built-in `feishu` / `feishu-app`
> channel. `feishu-events` only brings messages back (you need it for `ask` and acks).
## MCP
```jsonc
{
"mcp": {
"notifs": {
"type": "local",
"command": ["notifs", "mcp"],
"environment": { "NOTIFY_BASE_URL": "http://127.0.0.1:7710" }
}
}
}
```
Exposes 4 tools — `send`, `events`, `deliveries`, `status` (the last answers
"did it actually reach my phone?") — 3 prompts (`write-script`, `write-monitor`,
`poller`) and one resource (`notifs://contract`).
Tools are deliberately **not** able to delete rules or rotate tokens: the blast
radius of a confused agent is "one extra notification".
Full setup (opencode + any MCP client, all five tools, prompts, resources):
[`docs/mcp.md`](docs/mcp.md).
## Channels
| kind | status |
|---|---|
| `local` | **maintained** — Linux desktop notifications |
| `feishu` | **maintained** — Feishu group bot webhook |
| `feishu-app` | **maintained** — Feishu app bot (DM or group) |
| `ntfy` `bark` `wecom` `webhook` | in the code, **untested** — no promises |
## Known limits
- **At-least-once delivery.** Crash recovery can resend; messages carry `event_id` for downstream dedup.
- **No public ingress.** Binds `127.0.0.1`; remote senders need a proxy or a different bind.
- **`at` is a timestamp label, not a scheduler.** There is no built-in cron. Time-based reminders are just a producer that watches the clock.
- **No throttling / quiet hours yet.** Rate-limit on the producer side.
- **No feedback loop.** Notifications are one-way; there's no ack/button round-trip.
## 中文文档
- [`docs/mcp.md`](docs/mcp.md) — MCP 配置(opencode / 通用客户端)、5 个工具、prompts 与 resource
- [`docs/remote.md`](docs/remote.md) — 把中继放到服务器(SSH 隧道,不开公网端口)
- [`docs/使用手册.md`](docs/使用手册.md) — CLI 全量用法、托管/不托管/轮询三种写法、可直接交给 agent 的提示词
- [`docs/producer-protocol.md`](docs/producer-protocol.md) — 监控程序接入规范(完整版)
- [`docs/channels.md`](docs/channels.md) — 各出口怎么拿凭据
## Development
```sh
pnpm install
pnpm typecheck && pnpm test
pnpm build # → dist/cli.mjs, dist/server.mjs(esbuild 单文件,只外置 better-sqlite3)
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues