A lightweight system monitoring agent that: - Collects metrics via configurable shell commands - Publishes to MQTT with Home Assistant auto-discovery - Supports entity types: sensor, binary_sensor, light, switch, button - Responds to commands over MQTT for controllable entities Architecture: - src/config.ts: TOML config loading and validation - src/mqtt.ts: MQTT client with HA discovery - src/entities.ts: Entity state polling and command handling - index.ts: CLI entry point (run, check, once commands) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3.7 KiB
3.7 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 (yargs)
src/
commands/ # CLI command handlers
metrics/ # System metric collectors
mqtt/ # MQTT client and publishing
events/ # MQTT event listeners and handlers
actions/ # Executable actions (shell, service, notify)
ha/ # Home Assistant discovery
config/ # Configuration loading
Event/Command System
Systant subscribes to MQTT topics and executes configured actions:
Topic: systant/{hostname}/command/{action}
Payload: { "args": [...], "timeout": 30 }
Topic: systant/{hostname}/event/{event_name}
Payload: { ... event data ... }
Actions are sandboxed and configurable via allowlists in the config file. Security is critical - never execute arbitrary commands without validation.
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)
- CLI: yargs
- Config: TOML
- MQTT: mqtt.js or Bun-native when available
- Package: Nix flake for reproducible builds
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
bun run start # Run in development
bun run dist # Build standalone binary
bun test # Run tests
bun test --watch # Watch mode
Planning Protocol
When implementing features:
- Discuss the approach before writing code
- Start with types/interfaces
- Write tests alongside implementation
- Keep PRs focused and small