Skip to content
open tomato/ ned
GitHub
ned pages
  1. ned
  2. How it will work

How it will work

ned's planned design, from a message's kind to its routes and providers, with the config written from empty up.

NOTE

This page is a placeholder: it describes ned's design, and none of it is built. Every command and config block below is copied from the design and its specs, and none has run.

Three parts

ned has three parts, and each changes without touching the others. If you know log shippers such as Vector, the shape is the same: sources, routes and sinks become kinds, routes and providers.

  • A kind is a type of message with its own fields. ned plans two: alert, for something that needs a person, and notice, for something that finished. A program can register its own kinds, such as log.
  • A route says which providers a kind goes to. Every route that matches a message applies, so one alert can reach a journal file and a phone at once.
  • A provider delivers to one place. The first version plans telegram, ntfy, file, claude-feed (a file a Claude session watches, which then sends a push to your phone) and tray (a small desktop app).

Every message carries a few common fields: an id, its kind, a source naming who sent it, and a time. alert and notice add a title, and an optional priority from 1 to 5. A route can set minPriority so that only the urgent messages reach it.

The command

These are the planned forms of ned send and ned check:

ned send alert --title "loop halted: checkout moved" --source rafa --severity error
ned send notice --title "PR opened" --link https://example.com/pr/1
ned send --json '{"kind":"alert","source":"cron","title":"backup failed"}'
ned check

[verify: not run; ned has no package or command yet. The forms come from ned#7, the CLI spec.]

ned check is planned to print where each kind goes, so you can see a route before anything is sent. The planned exit codes: 0 when every target delivered or skipped, 1 for an invalid message or config, 2 when any target failed.

Config, from empty up

The config is a TypeScript file built on define-config, so later layers change earlier ones by key: the user's file in ~/.config/ned/, then the project's ned.config.ts.

With no routes, every message is reported as not routed, and nothing is delivered:

export default defineConfig([defaults])

One Telegram chat for alerts. The config holds the name of a secret, never the token itself:

export default defineConfig([defaults, {
  providers: { phone: { use: 'telegram', tokenSecret: 'ned.telegram.token', chatId: '123' } },
  routes: { alerts: { kind: 'alert', to: ['phone'] } },
}])

A project turning off a route the user's file set:

export default defineConfig([{ routes: { notices: false } }])

[verify: not run; the three blocks are the design's own examples, and defineConfig and defaults would come from @open-tomato/ned/config, which is not published.]

Providers are named instances: a route points at phone, never at telegram, so you can keep two bots, or change a bot without touching a route.

Secrets

ned is designed to look a secret up by its name: first in your system's secret store, then in an environment variable NED_SECRET_<NAME>, the name upper-cased with dots as underscores. A Linux server with no unlocked keyring would use the variable.

An ntfy topic is a secret too. Anyone who knows the topic can read and post to it, so it is a password in all but name.

What it is not

ned is not a background service you must start first. The command sends and exits; the only long-running part is the optional tray app.