Zero-dependency VPS monitor that reports as an image, not a wall of text
Reads /proc directly, renders hacker-green on black, ships it to your chat.
A fast, zero-dependency VPS system monitor written in Zig. Reads metrics directly from /proc/ and renders a hacker-style ASCII dashboard.
The main purpose of this tool is to send a visual image of your system status instead of a plain text message. It generates a PNG image with green-on-black terminal aesthetics — perfect for Telegram, WhatsApp, or Discord bot integrations.
- CPU usage with visual bar (sampled over a 500 ms window)
- RAM usage (used / total)
- Disk usage (used / total)
- Network traffic (total RX/TX since boot, physical interfaces only)
- Load average (1/5/15 min)
- Uptime (days, hours, minutes)
- Top 5 processes by memory usage (user-space only, no kernel threads)
- AI token usage — last-24h OpenClaw token consumption (input / output / cache, model calls)
- PNG rendering built in — hacker green (
#00FF41) on black, no external tool - Telegram integration — one-command render + send via Bot API
- Zig >= 0.16.0
That is the whole list. The PNG encoder and the bitmap font are part of the
program, so rendering needs no image library, no font installed on the system,
and no convert on the box you deploy to.
git clone https://github.com/burnshall-ui/vpsmon.git
cd vpsmon
# Debug build
zig build
# Optimized release build
zig build -Doptimize=ReleaseFast
# Run directly
zig build runThe binary is at zig-out/bin/vpsmon (statically linked; ~0.5 MB after strip).
strip zig-out/bin/vpsmon
cp zig-out/bin/vpsmon ~/.local/bin/zig build test --summary all # unit tests
zig fmt --check . # formattingThe tests cover the hand-rolled parsers — /proc/meminfo fields, the civil-date
conversion, ISO timestamp parsing, the minimal JSON scraper, the token
formatter's unit thresholds and the command line — plus the disk arithmetic,
the top-process selection, the PNG encoder and the renderer's buffer sizing.
Everything else reads /proc, which CI checks by
running the binary and asserting the dashboard still contains every section,
then rendering a PNG and checking it with Python's zlib rather than with the
encoder that wrote it.
vpsmon+--------------------[ SYSTEM STATUS ]---------------------+
| |
| [####============] CPU 25.3% |
| [######==========] RAM 38.1% 8.8/23 GB |
| [######==========] DISK 39.8% 46/115 GB |
| |
| NET ^ 2.9 GB v 14.3 GB (total) |
| LOAD 0.76 0.63 0.51 |
| UPTIME 7d 9h 6m |
| |
+--------------------[ AI / OPENCLAW ]---------------------+
| |
| TOKENS 5.1M / 24h (20 calls) |
| in 1.9M out 58.3k cache 3.0M |
| |
+--------------------[ TOP PROCESSES ]---------------------+
| |
| openclaw-gateway............ 394 MB |
| next-server (v16.1.6)....... 174 MB |
| node........................ 83 MB |
| python3..................... 42 MB |
| redis-server................ 12 MB |
| |
+----------------------------------------------------------+
Pass an output path and the dashboard is written as a PNG instead of printed:
vpsmon /tmp/vpsmon.png./render.sh [path] does the same and defaults the path to /tmp/vpsmon.png.
Anything starting with - is read as an option, not a path: vpsmon --help
prints the usage, anything else is refused. For a file whose name starts with a
dash, write ./-name.png.
# 1. Copy and configure
cp send_status.sh.example send_status.sh
# Edit send_status.sh — set BOT_TOKEN and CHAT_ID
# 2. Run
./send_status.shvpsmon ships with an OpenClaw skill definition. To use it:
-
Copy
SKILL.mdto your OpenClaw workspace skills directory:mkdir -p ~/.openclaw/workspace/skills/vpsmon cp SKILL.md ~/.openclaw/workspace/skills/vpsmon/SKILL.md
-
Add exec approval for your agent:
openclaw approvals allowlist add --agent main "/path/to/vpsmon/send_status.sh" -
Add to your agent's
SOUL.md:## VPS-Monitoring (vpsmon) When the user asks about system status, run `/path/to/vpsmon/send_status.sh`. Respond with "SYSTEM STATUS" only — the script sends the image via Telegram.
-
Restart the gateway and ask your agent: "How's the server doing?"
vpsmon/
├── src/
│ ├── main.zig # Core monitor — reads /proc/, outputs ASCII + tests
│ ├── renderer.zig # Draws the dashboard into an RGB pixel buffer
│ ├── font.zig # Embedded VGA 8x16 bitmap font
│ └── png.zig # PNG encoder built on std + tests
├── render.sh # Convenience wrapper around `vpsmon <path>`
├── send_status.sh.example # Template: render + send via Telegram
├── SKILL.md # OpenClaw agent skill definition
├── assets/
│ └── screenshot.png # Example output
├── .github/
│ ├── workflows/
│ │ └── ci.yml # fmt check, build, tests, smoke tests
│ └── check_png.py # Independent PNG check for CI
├── build.zig # Zig build configuration
├── build.zig.zon # Zig package manifest
├── LICENSE # MIT
└── README.md
vpsmon reads Linux system metrics directly from the /proc filesystem:
| Metric | Source |
|---|---|
| CPU | /proc/stat |
| Memory | /proc/meminfo |
| Disk | statfs("/") |
| Network | /proc/net/dev, filtered to interfaces with /sys/class/net/<if>/device |
| Load | /proc/loadavg |
| Uptime | /proc/uptime |
| Processes | /proc/[pid]/stat + /proc/[pid]/cmdline |
| AI tokens | ~/.openclaw/agents/*/sessions/*.trajectory.jsonl |
CPU usage is computed from the delta of two /proc/stat snapshots taken 500 ms apart (the raw counters are cumulative since boot).
The AI section sums the model.completed events of the last 24 hours from OpenClaw's trajectory logs. If no OpenClaw installation is present, the section shows "no model calls in the last 24h".
The same ASCII the terminal gets is drawn glyph by glyph into an RGB buffer
using an embedded VGA 8x16 bitmap font, then encoded as a PNG by src/png.zig.
That encoder leans entirely on the standard library. std.compress.flate
handles deflate and, with its zlib container, writes the two-byte header and the
trailing Adler-32 checksum — the genuinely hard part of the format.
std.hash.crc supplies the per-chunk CRC. What is left is the container itself:
signature, IHDR, IDAT, IEND, and a filter byte in front of each scanline.
The output is 8-bit truecolour, no interlacing, filter type 0 on every row. Choosing no filter is conformant — PNG lets an encoder pick per row — and costs nothing here, because deflate already collapses the repeated border rows: a dashboard compresses to under 1% of its raw size.
Since the dashboard is pure ASCII, only bytes 0x00–0xFF of the font are ever needed; there is no UTF-8 shaping, and no font has to be installed.
MIT
