- Update architecture section to reflect actual file structure - Document entity system (sensor, binary_sensor, switch, light, button) - Add MQTT topic documentation - Add NixOS/home-manager integration section - Update commands section - Replace metrics-specialist with entity-specialist - Replace events-specialist with nix-specialist - Update mqtt-specialist context for current topic structure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
4.8 KiB
4.8 KiB
Systant
A system monitoring agent that collects metrics, monitors services, and reports to MQTT/Home Assistant; and responds to events over MQTT to trigger commands or other behavior.
Project Overview
Systant is a lightweight CLI tool written in Bun/TypeScript that:
- Collects system metrics (CPU, memory, disk, network)
- Monitors service health
- Publishes data to MQTT brokers
- Supports Home Assistant auto-discovery
- Listens for MQTT commands to trigger actions (run scripts, restart services, etc.)
- Responds to events with configurable handlers
- Runs as a daemon or one-shot command
Architecture
index.ts # CLI entry point
src/
config.ts # TOML configuration loading
mqtt.ts # MQTT client, publishing, and HA discovery
entities.ts # Entity management (state polling, command handling)
Entity System
Systant uses a unified "entity" concept that combines state monitoring and command handling. Entity types:
- sensor: Read-only numeric/string values (CPU usage, temperature, etc.)
- binary_sensor: Read-only on/off states (service running, etc.)
- switch: Controllable on/off with
on_commandandoff_command - light: Same as switch, for display/monitor control
- button: Press-only actions with
press_command
Each entity is defined in TOML with shell commands:
[entities.cpu_usage]
type = "sensor"
state_command = "awk '/^cpu / {print int(($2+$4)*100/($2+$4+$5))}' /proc/stat"
unit = "%"
icon = "mdi:chip"
name = "CPU Usage"
[entities.headphones]
type = "switch"
state_command = "pactl get-default-sink | grep -q usb && echo ON || echo OFF"
on_command = "pactl set-default-sink alsa_output.usb-..."
off_command = "pactl set-default-sink alsa_output.pci-..."
MQTT Topics
systant/{hostname}/{entity_id}/state # State updates
systant/{hostname}/{entity_id}/set # Commands (for switch/light)
systant/{hostname}/{entity_id}/press # Button presses
systant/{hostname}/availability # Online/offline status
homeassistant/{type}/{hostname}_{id}/config # HA auto-discovery
Key Design Decisions
- Single binary: Compiles to standalone executable via
bun build --compile - No external services: Uses Bun built-ins (sqlite, file, etc.)
- Config-driven: TOML configuration for flexibility
- Typed throughout: Full TypeScript with strict mode
Tech Stack
- Runtime: Bun (not Node.js)
- Config: TOML (smol-toml)
- MQTT: mqtt.js
- Packaging: Nix flake with home-manager module
NixOS/Home Manager Integration
# flake.nix inputs
inputs.systant.url = "git+ssh://...";
# Add overlay for pkgs.systant
nixpkgs.overlays = [ inputs.systant.overlays.default ];
# Import home-manager module
home-manager.sharedModules = [ inputs.systant.homeManagerModules.default ];
# In user config
services.systant = {
enable = true;
settings = { /* TOML as Nix attrset */ };
# or: configFile = ./systant.toml;
};
Bun Conventions
Default to using Bun instead of Node.js.
- Use
bun <file>instead ofnode <file>orts-node <file> - Use
bun testinstead ofjestorvitest - Use
bun build <file.html|file.ts|file.css>instead ofwebpackoresbuild - Use
bun installinstead ofnpm installoryarn installorpnpm install - Use
bun run <script>instead ofnpm run <script> - Use
bunx <package> <command>instead ofnpx <package> <command> - Bun automatically loads .env, so don't use dotenv.
Bun APIs
Bun.serve()for HTTP/WebSocket serversbun:sqlitefor SQLite (not better-sqlite3)Bun.file()for file I/O (not node:fs readFile/writeFile)Bun.$\cmd`` for shell commands (not execa)- Native
WebSocket(not ws)
Testing
import { test, expect, describe, beforeEach } from "bun:test";
describe("MetricCollector", () => {
test("collects CPU metrics", async () => {
const metrics = await collectCPU();
expect(metrics.usage).toBeGreaterThanOrEqual(0);
});
});
Run tests: bun test
Run specific: bun test src/metrics
Watch mode: bun test --watch
Code Style
- Prefer
async/awaitover callbacks - Use explicit return types on public functions
- Prefer
interfaceovertypefor object shapes - Use
constby default,letonly when reassignment needed - No classes unless state encapsulation is genuinely needed
- Prefer pure functions and composition
Commands
# Development
bun run index.ts run --config systant.toml
# Build standalone binary
bun build index.ts --compile --outfile systant
# Nix build
nix build .#systant
# Run tests
bun test
Planning Protocol
When implementing features:
- Discuss the approach before writing code
- Start with types/interfaces
- Write tests alongside implementation
- Keep PRs focused and small