Compare commits
No commits in common. "main" and "bun" have entirely different histories.
31
AGENTS.md
31
AGENTS.md
@ -27,17 +27,17 @@ Use this agent to review code changes before committing.
|
|||||||
|
|
||||||
**Trigger:** Before creating commits or PRs.
|
**Trigger:** Before creating commits or PRs.
|
||||||
|
|
||||||
## entity-specialist
|
## metrics-specialist
|
||||||
|
|
||||||
Use this agent when working on entity configuration or the entity system.
|
Use this agent when working on system metric collection.
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
- Understand entity types (sensor, binary_sensor, switch, light, button)
|
- Understand Linux /proc and /sys interfaces
|
||||||
- Design shell commands for state polling and actions
|
- Know cross-platform metric collection strategies
|
||||||
- Ensure proper Home Assistant discovery payloads
|
- Ensure metrics are properly typed and documented
|
||||||
- Validate entity configuration options
|
- Validate metric units and normalization
|
||||||
|
|
||||||
**Context:** Systant uses a unified "entity" system where all metrics and controls are defined as entities in TOML config. Each entity has a `state_command` and optionally `on_command`/`off_command`/`press_command` depending on type.
|
**Context:** Systant collects CPU, memory, disk, and network metrics. Metrics should be normalized (percentages 0-100, bytes for sizes) and include metadata for Home Assistant discovery.
|
||||||
|
|
||||||
## mqtt-specialist
|
## mqtt-specialist
|
||||||
|
|
||||||
@ -50,19 +50,20 @@ Use this agent when working on MQTT publishing or Home Assistant integration.
|
|||||||
- Handle connection lifecycle (connect, reconnect, disconnect)
|
- Handle connection lifecycle (connect, reconnect, disconnect)
|
||||||
- Design topic hierarchies for commands and events
|
- Design topic hierarchies for commands and events
|
||||||
|
|
||||||
**Context:** Systant publishes to MQTT with Home Assistant auto-discovery. Topics follow the pattern `systant/{hostname}/{entity_id}/state` for state updates, `systant/{hostname}/{entity_id}/set` for switch/light commands, and `homeassistant/{type}/{hostname}_{entity_id}/config` for discovery.
|
**Context:** Systant publishes to MQTT with Home Assistant auto-discovery. Topics follow the pattern `systant/{hostname}/{metric_type}`. Command topics use `systant/{hostname}/command/{action}`.
|
||||||
|
|
||||||
## nix-specialist
|
## events-specialist
|
||||||
|
|
||||||
Use this agent when working on Nix packaging or the home-manager module.
|
Use this agent when working on the event/command system.
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
- Maintain the Nix flake and package definition
|
- Design secure command execution with allowlists
|
||||||
- Update the home-manager module options
|
- Implement event handlers and action dispatching
|
||||||
- Handle fixed-output derivations for npm dependencies
|
- Ensure proper input validation and sanitization
|
||||||
- Ensure cross-system compatibility
|
- Handle timeouts and error reporting
|
||||||
|
- Consider security implications of remote command execution
|
||||||
|
|
||||||
**Context:** Systant is packaged as a Nix flake with a home-manager module. The package uses a two-phase build: FOD for `bun install`, then `bun build --compile` for the binary. The home-manager module creates a systemd user service.
|
**Context:** Systant listens for MQTT commands and executes configured actions. Security is paramount - all commands must be validated against an allowlist, inputs sanitized, and execution sandboxed where possible.
|
||||||
|
|
||||||
## debug-investigator
|
## debug-investigator
|
||||||
|
|
||||||
|
|||||||
95
CLAUDE.md
95
CLAUDE.md
@ -16,50 +16,31 @@ Systant is a lightweight CLI tool written in Bun/TypeScript that:
|
|||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
index.ts # CLI entry point
|
index.ts # CLI entry point (yargs)
|
||||||
src/
|
src/
|
||||||
config.ts # TOML configuration loading
|
commands/ # CLI command handlers
|
||||||
mqtt.ts # MQTT client, publishing, and HA discovery
|
metrics/ # System metric collectors
|
||||||
entities.ts # Entity management (state polling, command handling)
|
mqtt/ # MQTT client and publishing
|
||||||
|
events/ # MQTT event listeners and handlers
|
||||||
|
actions/ # Executable actions (shell, service, notify)
|
||||||
|
ha/ # Home Assistant discovery
|
||||||
|
config/ # Configuration loading
|
||||||
```
|
```
|
||||||
|
|
||||||
### Entity System
|
### Event/Command System
|
||||||
|
|
||||||
Systant uses a unified "entity" concept that combines state monitoring and command handling. Entity types:
|
Systant subscribes to MQTT topics and executes configured actions:
|
||||||
|
|
||||||
- **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
|
Topic: systant/{hostname}/command/{action}
|
||||||
systant/{hostname}/{entity_id}/set # Commands (for switch/light)
|
Payload: { "args": [...], "timeout": 30 }
|
||||||
systant/{hostname}/{entity_id}/press # Button presses
|
|
||||||
systant/{hostname}/availability # Online/offline status
|
Topic: systant/{hostname}/event/{event_name}
|
||||||
homeassistant/{type}/{hostname}_{id}/config # HA auto-discovery
|
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
|
### Key Design Decisions
|
||||||
|
|
||||||
- **Single binary**: Compiles to standalone executable via `bun build --compile`
|
- **Single binary**: Compiles to standalone executable via `bun build --compile`
|
||||||
@ -70,29 +51,10 @@ homeassistant/{type}/{hostname}_{id}/config # HA auto-discovery
|
|||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Runtime**: Bun (not Node.js)
|
- **Runtime**: Bun (not Node.js)
|
||||||
- **Config**: TOML (smol-toml)
|
- **CLI**: yargs
|
||||||
- **MQTT**: mqtt.js
|
- **Config**: TOML
|
||||||
- **Packaging**: Nix flake with home-manager module
|
- **MQTT**: mqtt.js or Bun-native when available
|
||||||
|
- **Package**: Nix flake for reproducible builds
|
||||||
### 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
|
## Bun Conventions
|
||||||
|
|
||||||
@ -143,17 +105,10 @@ Watch mode: `bun test --watch`
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development
|
bun run start # Run in development
|
||||||
bun run index.ts run --config systant.toml
|
bun run dist # Build standalone binary
|
||||||
|
bun test # Run tests
|
||||||
# Build standalone binary
|
bun test --watch # Watch mode
|
||||||
bun build index.ts --compile --outfile systant
|
|
||||||
|
|
||||||
# Nix build
|
|
||||||
nix build .#systant
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
bun test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Planning Protocol
|
## Planning Protocol
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
# Home Manager module (system-independent)
|
# NixOS module (system-independent)
|
||||||
homeManagerModules.default = import ./nix/nixos-module.nix;
|
nixosModules.default = import ./nix/nixos-module.nix;
|
||||||
|
|
||||||
# Overlay to add systant to pkgs
|
# Overlay to add systant to pkgs
|
||||||
overlays.default = final: prev: {
|
overlays.default = final: prev: {
|
||||||
|
|||||||
@ -1,17 +1,11 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.services.systant;
|
cfg = config.systant;
|
||||||
settingsFormat = pkgs.formats.toml { };
|
settingsFormat = pkgs.formats.toml { };
|
||||||
configFile =
|
|
||||||
if cfg.configFile != null
|
|
||||||
then cfg.configFile
|
|
||||||
else if cfg.settings != { }
|
|
||||||
then settingsFormat.generate "systant-config.toml" cfg.settings
|
|
||||||
else null;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.systant = {
|
options.systant = {
|
||||||
enable = lib.mkEnableOption "systant system monitoring agent";
|
enable = lib.mkEnableOption "systant system monitoring agent";
|
||||||
|
|
||||||
package = lib.mkOption {
|
package = lib.mkOption {
|
||||||
@ -59,30 +53,70 @@ in
|
|||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "systant";
|
||||||
|
description = "User account under which systant runs.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "systant";
|
||||||
|
description = "Group under which systant runs.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home.packages = [ cfg.package ];
|
# Create systant user/group if using defaults
|
||||||
|
users.users.${cfg.user} = lib.mkIf (cfg.user == "systant") {
|
||||||
systemd.user.services.systant = {
|
isSystemUser = true;
|
||||||
Unit = {
|
group = cfg.group;
|
||||||
Description = "Systant system monitoring agent";
|
description = "Systant service user";
|
||||||
After = [ "network-online.target" ];
|
|
||||||
Wants = [ "network-online.target" ];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
users.groups.${cfg.group} = lib.mkIf (cfg.group == "systant") { };
|
||||||
|
|
||||||
|
# Generate config file from settings if configFile not provided
|
||||||
|
environment.etc."systant/config.toml" = lib.mkIf (cfg.configFile == null && cfg.settings != { }) {
|
||||||
|
source = settingsFormat.generate "systant-config.toml" cfg.settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.systant = {
|
||||||
|
description = "Systant system monitoring agent";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network-online.target" ];
|
||||||
|
wants = [ "network-online.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
ExecStart =
|
ExecStart =
|
||||||
if configFile != null
|
let
|
||||||
then "${cfg.package}/bin/systant run --config ${configFile}"
|
configPath =
|
||||||
else "${cfg.package}/bin/systant run";
|
if cfg.configFile != null
|
||||||
|
then cfg.configFile
|
||||||
|
else "/etc/systant/config.toml";
|
||||||
|
in
|
||||||
|
"${cfg.package}/bin/systant run --config ${configPath}";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
};
|
|
||||||
|
|
||||||
Install = {
|
# Hardening
|
||||||
WantedBy = [ "default.target" ];
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
|
||||||
|
# Allow reading system metrics
|
||||||
|
ReadOnlyPaths = [
|
||||||
|
"/proc"
|
||||||
|
"/sys"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user