systant/CLAUDE.md
ryan 904bef4aa7 docs: update CLAUDE.md and AGENTS.md for entity-based architecture
- 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>
2026-01-19 21:33:51 -08:00

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_command and off_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 of node <file> or ts-node <file>
  • Use bun test instead of jest or vitest
  • Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild
  • Use bun install instead of npm install or yarn install or pnpm install
  • Use bun run <script> instead of npm run <script>
  • Use bunx <package> <command> instead of npx <package> <command>
  • Bun automatically loads .env, so don't use dotenv.

Bun APIs

  • Bun.serve() for HTTP/WebSocket servers
  • bun:sqlite for 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/await over callbacks
  • Use explicit return types on public functions
  • Prefer interface over type for object shapes
  • Use const by default, let only 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:

  1. Discuss the approach before writing code
  2. Start with types/interfaces
  3. Write tests alongside implementation
  4. Keep PRs focused and small