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

166 lines
4.8 KiB
Markdown

# 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:
```toml
[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
```nix
# 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
```ts
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
```bash
# 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