No description
Find a file
Vincent S. 501839ac29
Some checks failed
CI / fmt (push) Failing after 16s
CI / build (push) Successful in 3m8s
fix(llama): distinguish idle router from a loaded-but-quiet model
The waybar text read "WAIT" both when the router had no model loaded and
when a model was loaded but not generating. "WAIT" suggests something is
blocked, when in fact the server is available in both cases.

Split the two states: no model loaded now renders "IDLE" (new `idle`
class, tooltip explains a model loads on the first request), and the
loaded-but-quiet state renders "READY". The latter keeps its `wait` CSS
class so existing waybar styling is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 20:21:55 +02:00
.forgejo/workflows ci: add Forgejo Actions build/test/fmt workflow 2026-06-28 00:32:48 +02:00
.githooks chore: add pre-commit hook running clippy and tests 2026-06-03 21:09:43 +02:00
alfred fix(llama): distinguish idle router from a loaded-but-quiet model 2026-07-18 20:21:55 +02:00
alfred-core feat(ytmd): migrate YouTube Music backend from YTMDesktop to th-ch/Pear 2026-07-07 00:02:20 +02:00
alfred-matrix fix(matrix): background the bootstrap sync so the command bot arms on startup 2026-07-07 18:26:14 +02:00
alfred-ytm [UPDATE] Runned fmt 2026-06-28 00:30:04 +02:00
.gitignore chore: initial scaffold 2026-05-25 17:45:34 +02:00
Cargo.lock feat(ytmd): migrate YouTube Music backend from YTMDesktop to th-ch/Pear 2026-07-07 00:02:20 +02:00
Cargo.toml feat(notify): Matrix notifications + notification router 2026-07-01 18:50:52 +02:00
CLAUDE.md fix(ytmd): queue-clear keeps current track + ordered enqueue-next + batch verb 2026-07-07 19:18:58 +02:00
README.md feat(matrix): inbound command bot (queries, control, music, capture) 2026-07-01 23:59:56 +02:00

alfred

A personal system telemetry daemon for the Linux desktop.

Architecture

alfred serve                      # persistent daemon, managed by systemd --user
alfred get <module>               # one-shot query for scripts/waybar/debugging
alfred stream <module>            # optional long-lived NDJSON client
alfred status                     # check if daemon is running
alfred notify "message"           # send an outbound Matrix notification
alfred completions zsh > _alfred  # generate shell completion

Modules

Module Source Interval
system /proc/stat, /proc/meminfo, sysfs hwmon 2s
gpu NVML (libnvidia-ml) 5s
audio wpctl / pactl subprocesses 2s
media D-Bus MPRIS via zbus 5s
network /sys/class/net 5s
storage statvfs + sysfs hwmon 30s
llama HTTP localhost:8080/metrics 2s
ytmd YTMD companion HTTP + Socket.IO 5s
hyprland Hyprland IPC 1s

API

GET  /:module            → latest cached JSON (one-shot)
GET  /stream/:module     → newline-delimited JSON stream (push on update)
POST /control/:module/:action → trigger an action, forces immediate refresh
POST /notify             → dispatch a notification through the router (channels/backends)

Config

~/.config/alfred/config.toml

Matrix notifications

alfred can post m.room.message events to a Matrix room — from the CLI or from any internal module (now-playing, "job done", alerts). Send-only, into encrypted or unencrypted rooms.

alfred notify "hello from alfred"
alfred notify --channel alerts --level warn "disk almost full"
alfred notify --room "#alfred:server" --level error --title "Backup" --html "<b>failed</b>"

--level (info|warn|error) adds a leading emoji and, for warn/error, an HTML colour (and, on desktop backends, a notify-send urgency). --channel targets a [notify.channels] group (which may fan out to several backends); --room sends directly to one Matrix room (friendly name from [matrix.rooms], or a literal !id / #alias). With neither, it uses [notify].default_channel, falling back to [matrix].default_room.

Notification router (channels & backends)

Notifications flow through a small router so producers (the CLI, alerts, internal modules) don't care how a message is delivered. A channel is a named group of backends; a backend is either matrix (E2EE-capable, below) or exec (a shell command template).

[notify]
default_channel = "alerts"               # used when nothing else is specified

[notify.backends.desktop]
kind    = "exec"
command = "notify-send -u {{urgency}} '{{title}}' '{{body}}'"
# exec template vars: {{title}} {{body}} {{level}} {{urgency}} {{icon}}

[notify.backends.matrix-music]
kind = "matrix"
room = "now-playing"                     # a [matrix.rooms] name, or an id/alias

[notify.channels.alerts]
backends = ["matrix", "desktop"]         # "matrix" is built-in → [matrix].default_room

[notify.channels.music]
backends = ["matrix-music"]

alfred notify --channel alerts … (or an alert's notify = "alerts") then goes to both Matrix and desktop. The CLI prints a per-backend result and exits non-zero only if every backend failed.

Alerts can drive it directly instead of shelling out — an alert may set exec, notify, or both:

[[alerts]]
name      = "cpu-hot"
condition = "cpu_temp > 85"
notify    = "alerts"                     # channel name
level     = "error"
title     = "CPU hot"
message   = "Temperature: {{cpu_temp}}°C"
recover_message = "CPU back to normal"   # optional; sent as info on recover
# exec = "..."                           # still supported, and can coexist

How it works (E2EE)

The lean daemon does not link matrix-rust-sdk. It resolves the bot token, looks up the bot's user/device via whoami, builds the message, and forwards the send to a small alfred-matrix worker subprocess (same pattern as the alfred-ytm music helper). That worker links matrix-rust-sdk with Olm/Megolm and owns a persistent crypto store, so it encrypts transparently when the target room has encryption enabled and sends plaintext otherwise.

Notes:

  • The bot token must be minted via /login (it needs a device_id); a /register-only token has no device and E2EE can't work — alfred rejects it with a clear error.
  • The crypto store lives at ${XDG_STATE_HOME:-~/.local/state}/alfred-matrix/<device_id>/ (dir mode 700). Keep it: deleting it or rotating the token (→ new device) forces new keys, and other clients may show the bot's device as unverified (they still decrypt — messages are shared with all member devices by default).
  • The bot must be a member of the room; if it has been invited it will join on first send.
  • First send after (re)start does an initial sync to bootstrap keys, so it can be slower; subsequent sends reuse the warm client.

The daemon never logs the token or the auth header. If the homeserver is unreachable it returns an error — the daemon stays up and the CLI reports a clear message.

Config

[matrix]
enabled    = true
homeserver = "https://matrix.mydomain"      # base URL, no trailing path
# Provide the bot access token via ONE of these. Prefer a file or command so the
# secret never lives in a committed config; a plaintext access_token warns.
# access_token         = "syt_…"            # discouraged
access_token_file    = "/home/me/.secrets/alfred-matrix.token"  # must be chmod 600
# access_token_command = "gpg -q -d ~/.secrets/alfred-matrix.gpg"
default_room = "!roomid:mydomain"            # id or #alias, or a name below

[matrix.rooms]                               # friendly name → room id/alias
alerts      = "!abcdef:mydomain"
now-playing = "#music:mydomain"

Commands from Matrix (inbound)

The bot can also answer you: message !alfred <command> in a watched room and alfred replies there. This is opt-in and locked down — only allowlisted users are ever acted on, the bot ignores its own messages, and messages sent before the listener started (e.g. after a restart) are dropped so history never re-triggers.

[matrix.commands]
enabled      = true
prefix       = "!alfred"                     # message prefix that marks a command
allow_from   = ["@me:mydomain"]              # MXIDs allowed to issue commands
rooms        = ["!abcdef:mydomain"]          # watched rooms; defaults to default_room
allow_control = false                        # true → allow mute/skip/etc. over Matrix

Commands:

!alfred help                       # list commands
!alfred status                     # one line per module
!alfred get <module>               # a module's text + tooltip
!alfred modules                    # modules and their control actions
!alfred events [n]                 # recent journal events (max 25)
!alfred control <module> <action> [args…]   # requires allow_control = true

With allow_control = false (the default) the bot is a read-only dashboard; control verbs are refused. Delivery is E2EE when the room is encrypted, same as outbound. Internally: the alfred-matrix worker watches its sync stream and forwards prefix-matched messages to the daemon, which runs the query/action and replies in the same room.

Minting a bot token against conduwuit

Register a dedicated bot user, then log in once to obtain a long-lived access token (do this from a trusted shell; the token grants the bot's full access):

# 1. Register the bot user (conduwuit: needs a registration token if configured,
#    or use the admin room / your homeserver's registration flow).
curl -s -XPOST 'https://matrix.mydomain/_matrix/client/v3/register' \
  -H 'Content-Type: application/json' \
  -d '{
        "username": "alfred",
        "password": "CHOOSE_A_STRONG_PASSWORD",
        "auth": { "type": "m.login.dummy" }
      }'

# 2. Log in to mint an access token.
curl -s -XPOST 'https://matrix.mydomain/_matrix/client/v3/login' \
  -H 'Content-Type: application/json' \
  -d '{
        "type": "m.login.password",
        "identifier": { "type": "m.id.user", "user": "alfred" },
        "password": "CHOOSE_A_STRONG_PASSWORD",
        "initial_device_display_name": "alfred-daemon"
      }'
# → { "access_token": "syt_…", "user_id": "@alfred:mydomain", ... }

Store that access_token where access_token_file/access_token_command points, invite @alfred:mydomain to the room (encrypted or not — the bot joins on first send), and put the room id in default_room / [matrix.rooms]. Get a room id from your client's room settings, or:

curl -s 'https://matrix.mydomain/_matrix/client/v3/directory/room/%23music%3Amydomain' \
  -H 'Authorization: Bearer syt_…'   # resolves #music:mydomain → { "room_id": "!…" }

Waybar integration

Waybar 0.15.0 has been more reliable here with normal polling than with long-lived interval: 0 stream consumers, so the recommended setup is:

"custom/system": {
    "interval": 2,
    "exec": "alfred get system",
    "return-type": "json",
    "format": "{}"
}

alfred stream <module> is still available for other long-lived consumers.

Systemd

~/.config/systemd/user/alfred.service

Shell completions

Generate a zsh completion file:

alfred completions zsh > ~/.local/share/zsh/site-functions/_alfred

Then make sure that directory is in fpath before compinit, e.g. in ~/.zshrc:

fpath=(~/.local/share/zsh/site-functions $fpath)
autoload -U compinit
compinit