@irsyadulibad/servermon 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # Server Monitoring Daemon — Telegram Bot Config
2
+ # Copy to .env: cp .env.example .env
3
+ #
4
+ # 1. Bikin bot via @BotFather → dapet token
5
+ # 2. Dapetin chat ID: DM @userinfobot, atau cek getUpdates API
6
+ # - DM/private: 123456789
7
+ # - Supergroup: -1001234567890
8
+
9
+ TELEGRAM_BOT_TOKEN=your_bot_token_here
10
+ TELEGRAM_CHAT_ID=your_chat_id_here
11
+
12
+ # Interval monitoring dalam detik (default: 300 = 5 menit)
13
+ # Minimal 30 detik
14
+ MONITOR_INTERVAL=300
@@ -0,0 +1,5 @@
1
+ *.log
2
+ node_modules/
3
+ server-monitor
4
+ .env
5
+ dist/
package/.prettierrc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "trailingComma": "es5",
5
+ "tabWidth": 2,
6
+ "printWidth": 100,
7
+ "bracketSpacing": true,
8
+ "arrowParens": "always",
9
+ "endOfLine": "lf"
10
+ }
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # 🖥 Server Monitor
2
+
3
+ > **Lightweight server watchdog** — collects real-time system metrics and sends structured reports to Telegram.
4
+ > Global CLI tool. Install once, run anywhere. Built with Bun + TypeScript.
5
+
6
+ <p align="center">
7
+ <img src="https://img.shields.io/badge/runtime-Bun-000?style=flat&logo=bun" alt="bun">
8
+ <img src="https://img.shields.io/badge/language-TypeScript-3178C6?style=flat&logo=typescript" alt="ts">
9
+ <img src="https://img.shields.io/badge/target-Telegram-26A5E4?style=flat&logo=telegram" alt="telegram">
10
+ <img src="https://img.shields.io/badge/license-MIT-green?style=flat" alt="license">
11
+ </p>
12
+
13
+ ---
14
+
15
+ ## ✨ Features
16
+
17
+ | Category | What it tracks |
18
+ | -------------------- | -------------------------------------------------------------- |
19
+ | 💻 **CPU** | Model, core count, usage %, load average (1/5/15 min) |
20
+ | 🧠 **RAM** | Used / total, usage %, swap usage |
21
+ | 💾 **Disk** | Per-mount used / total, usage % — deduplicated |
22
+ | 🌐 **Network** | RX / TX rate (bytes/sec, sampled over 1s) |
23
+ | 🌡 **Temperature** | CPU package temp via thermal zones (`/sys/class/thermal`) |
24
+ | 📊 **Top Processes** | Top 5 by CPU % — PID, name, CPU %, MEM % |
25
+ | 🚨 **Alerts** | Auto-flag when CPU > 85%, RAM > 90%, disk > 90%, or swap > 50% |
26
+
27
+ Each report is color-coded with 🟢🟡🔴 health indicators and visual bar charts.
28
+
29
+ ---
30
+
31
+ ## 🚀 Quick Start
32
+
33
+ ### Install globally
34
+
35
+ ```bash
36
+ bun i -g github:irsyadulibad/servermon
37
+ ```
38
+
39
+ ### First run — interactive setup
40
+
41
+ ```bash
42
+ servermon
43
+ ```
44
+
45
+ You'll be prompted for:
46
+
47
+ 1. **Telegram Bot Token** — get one from [@BotFather](https://t.me/BotFather)
48
+ 2. **Report interval** — how often to send reports (default: 300s = 5 min)
49
+
50
+ Config is saved to `~/.irsyadulibad/servermon/config.json`.
51
+
52
+ ### Send a message to your bot
53
+
54
+ DM your bot **once** (any message). The daemon auto-detects your chat ID.
55
+
56
+ ### Start monitoring
57
+
58
+ ```bash
59
+ servermon
60
+ ```
61
+
62
+ That's it. The daemon auto-detects your chat ID, sends an initial report, then loops.
63
+
64
+ ---
65
+
66
+ ## 🛠 Development
67
+
68
+ ```bash
69
+ git clone https://github.com/irsyadlab/server-monitoring.git
70
+ cd server-monitoring
71
+ bun install
72
+
73
+ # Run in dev mode
74
+ bun start
75
+
76
+ # Global link (for testing)
77
+ bun link
78
+ servermon
79
+
80
+ # Unlink
81
+ bun unlink
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 📷 Example Report
87
+
88
+ ```
89
+ ✅ HEALTHY
90
+ 📅 7 Jun 2026, 22:11 │ ⏱ 3d 21h
91
+ 🐧 linux x86_64 │ Intel Xeon E5-2680 v4 @ 2.40GHz (2c)
92
+
93
+ 💻 CPU 10.5% 🟢 ▰▱▱▱▱▱▱▱▱▱
94
+ Load: 0.59 / 1.40 / 1.51
95
+
96
+ 🧠 RAM 27.3% 🟢 ▰▰▰▱▱▱▱▱▱▱
97
+ 1.0 GiB / 3.8 GiB
98
+
99
+ 💾 DISK
100
+ / 26% 🟢 ▰▰▰▱▱▱▱▱▱▱
101
+ └ 7957 GiB / 33092 GiB
102
+
103
+ 🌐 NET
104
+ ↓ 2.6 KB/s ↑ 808 B/s
105
+
106
+ 📊 TOP PROCESSES
107
+ 57318 ps CPU 200.0% MEM 0.1%
108
+ 57296 bun CPU 13.6% MEM 1.0%
109
+ 56149 hermes CPU 8.7% MEM 5.3%
110
+
111
+ ✨ All systems normal
112
+ ```
113
+
114
+ When thresholds are crossed, alerts appear inline.
115
+
116
+ ---
117
+
118
+ ## 🔧 Scripts
119
+
120
+ | Command | Description |
121
+ | ---------------- | ---------------------------------------------- |
122
+ | `bun start` | Run the daemon in dev mode |
123
+ | `bun run build` | Compile standalone binary → `./server-monitor` |
124
+ | `bun run lint` | Run ESLint |
125
+ | `bun run format` | Format with Prettier |
126
+ | `bun run check` | Format check + lint (CI-ready) |
127
+
128
+ ---
129
+
130
+ ## 📁 Project Structure
131
+
132
+ ```
133
+ server-monitoring/
134
+ ├── cli.ts # Global binary entry (interactive setup + launcher)
135
+ ├── index.ts # Daemon core (start/loop/report)
136
+ ├── src/
137
+ │ ├── config.ts # Config manager (~/.irsyadulibad/servermon/)
138
+ │ ├── monitor.ts # Metrics collector (CPU, RAM, disk, net, temp, procs)
139
+ │ └── reporter.ts # HTML formatter & Telegram sender
140
+ ├── eslint.config.js
141
+ ├── .prettierrc
142
+ ├── package.json
143
+ └── tsconfig.json
144
+ ```
145
+
146
+ ---
147
+
148
+ ## ⚙️ Config
149
+
150
+ Stored at `~/.irsyadulibad/servermon/config.json`:
151
+
152
+ ```json
153
+ {
154
+ "token": "883280...",
155
+ "interval": 300,
156
+ "chatId": "1216431846"
157
+ }
158
+ ```
159
+
160
+ - `token` — Telegram bot token (required)
161
+ - `interval` — seconds between reports, min: 30 (default: 300)
162
+ - `chatId` — auto-detected on first run, persisted for subsequent runs
163
+
164
+ ---
165
+
166
+ ## 📦 Deploy as systemd service
167
+
168
+ ```bash
169
+ # After installing globally
170
+ sudo tee /etc/systemd/system/servermon.service << 'EOF'
171
+ [Unit]
172
+ Description=Server Monitor Daemon
173
+ After=network.target
174
+
175
+ [Service]
176
+ Type=simple
177
+ ExecStart=/root/.bun/bin/servermon
178
+ Restart=always
179
+ RestartSec=10
180
+
181
+ [Install]
182
+ WantedBy=multi-user.target
183
+ EOF
184
+
185
+ sudo systemctl daemon-reload
186
+ sudo systemctl enable --now servermon
187
+ ```
188
+
189
+ ---
190
+
191
+ ## 🛠 Built With
192
+
193
+ - [Bun](https://bun.com) — fast all-in-one JavaScript runtime
194
+ - [TypeScript](https://www.typescriptlang.org/) — type safety
195
+ - [Telegram Bot API](https://core.telegram.org/bots/api) — message delivery
196
+ - [ESLint](https://eslint.org/) + [Prettier](https://prettier.io/) — code quality
197
+
198
+ ---
199
+
200
+ ## 📝 License
201
+
202
+ MIT — do whatever you want.
package/bun.lock ADDED
@@ -0,0 +1,209 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "server-monitoring",
7
+ "devDependencies": {
8
+ "@eslint/js": "^10.0.1",
9
+ "@types/bun": "latest",
10
+ "eslint": "^10.4.1",
11
+ "eslint-config-prettier": "^10.1.8",
12
+ "prettier": "^3.8.3",
13
+ "typescript-eslint": "^8.60.1",
14
+ },
15
+ "peerDependencies": {
16
+ "typescript": "^5",
17
+ },
18
+ },
19
+ },
20
+ "packages": {
21
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
22
+
23
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
24
+
25
+ "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
26
+
27
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
28
+
29
+ "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
30
+
31
+ "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
32
+
33
+ "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
34
+
35
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
36
+
37
+ "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
38
+
39
+ "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
40
+
41
+ "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
42
+
43
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
44
+
45
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
46
+
47
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
48
+
49
+ "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
50
+
51
+ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
52
+
53
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
54
+
55
+ "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
56
+
57
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/type-utils": "8.60.1", "@typescript-eslint/utils": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg=="],
58
+
59
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA=="],
60
+
61
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.1", "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw=="],
62
+
63
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1" } }, "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w=="],
64
+
65
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA=="],
66
+
67
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A=="],
68
+
69
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.60.1", "", {}, "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w=="],
70
+
71
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.1", "@typescript-eslint/tsconfig-utils": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew=="],
72
+
73
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.1", "@typescript-eslint/types": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg=="],
74
+
75
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.1", "", { "dependencies": { "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag=="],
76
+
77
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
78
+
79
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
80
+
81
+ "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
82
+
83
+ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
84
+
85
+ "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
86
+
87
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
88
+
89
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
90
+
91
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
92
+
93
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
94
+
95
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
96
+
97
+ "eslint": ["eslint@10.4.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw=="],
98
+
99
+ "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
100
+
101
+ "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
102
+
103
+ "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
104
+
105
+ "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
106
+
107
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
108
+
109
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
110
+
111
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
112
+
113
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
114
+
115
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
116
+
117
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
118
+
119
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
120
+
121
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
122
+
123
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
124
+
125
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
126
+
127
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
128
+
129
+ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
130
+
131
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
132
+
133
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
134
+
135
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
136
+
137
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
138
+
139
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
140
+
141
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
142
+
143
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
144
+
145
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
146
+
147
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
148
+
149
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
150
+
151
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
152
+
153
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
154
+
155
+ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
156
+
157
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
158
+
159
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
160
+
161
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
162
+
163
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
164
+
165
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
166
+
167
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
168
+
169
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
170
+
171
+ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
172
+
173
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
174
+
175
+ "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
176
+
177
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
178
+
179
+ "semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="],
180
+
181
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
182
+
183
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
184
+
185
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
186
+
187
+ "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
188
+
189
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
190
+
191
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
192
+
193
+ "typescript-eslint": ["typescript-eslint@8.60.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.1", "@typescript-eslint/parser": "8.60.1", "@typescript-eslint/typescript-estree": "8.60.1", "@typescript-eslint/utils": "8.60.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA=="],
194
+
195
+ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
196
+
197
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
198
+
199
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
200
+
201
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
202
+
203
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
204
+
205
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
206
+
207
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
208
+ }
209
+ }
package/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ // Shim — npm requires .js for bin entries, Bun handles .ts imports
3
+ import "./cli.ts";
package/cli.ts ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Server Monitor — Global CLI entry point.
4
+ * Handles first-time interactive setup and starts the monitoring daemon.
5
+ */
6
+
7
+ import { loadConfig, saveConfig, configPath, configDir } from "./src/config";
8
+
9
+ function banner() {
10
+ console.log("╔══════════════════════════════════════╗");
11
+ console.log("║ 🖥 SERVER MONITOR DAEMON 🖥 ║");
12
+ console.log("║ Telegram • Bun • TypeScript ║");
13
+ console.log("╚══════════════════════════════════════╝");
14
+ console.log();
15
+ }
16
+
17
+ async function interactiveSetup(): Promise<void> {
18
+ console.log("🖥 Server Monitor — First Time Setup");
19
+ console.log(` Config will be saved to: ${configDir()}\n`);
20
+
21
+ const token = prompt("🔑 Telegram Bot Token: ")?.trim();
22
+ if (!token) {
23
+ console.error("❌ Bot token is required. Get one from @BotFather on Telegram.");
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!token.includes(":")) {
28
+ console.error("❌ Invalid bot token format. Should look like: 123456:ABC-DEF1234gh...");
29
+ process.exit(1);
30
+ }
31
+
32
+ const intervalRaw =
33
+ prompt("⏱ Report interval in seconds (default 300 = 5 min): ")?.trim() || "300";
34
+ const interval = Math.max(30, parseInt(intervalRaw) || 300);
35
+
36
+ await saveConfig({ token, interval });
37
+ console.log(`\n✅ Config saved!`);
38
+ console.log(` 📁 ${configPath()}`);
39
+ console.log(` ⏱ Interval: ${interval}s (${(interval / 60).toFixed(0)} min)`);
40
+ console.log(`\n📡 Next step: DM your bot once on Telegram, then re-run \`servermon\`.`);
41
+ }
42
+
43
+ async function main() {
44
+ banner();
45
+
46
+ const config = await loadConfig();
47
+
48
+ if (!config) {
49
+ await interactiveSetup();
50
+ process.exit(0);
51
+ }
52
+
53
+ // Push config into env for the daemon
54
+ process.env["TELEGRAM_BOT_TOKEN"] = config.token;
55
+ process.env["MONITOR_INTERVAL"] = String(config.interval);
56
+ if (config.chatId) process.env["TELEGRAM_CHAT_ID"] = config.chatId;
57
+
58
+ console.log(`📁 Config: ${configPath()}`);
59
+ console.log(`📡 Bot: ...${config.token.slice(-8)}`);
60
+ if (config.chatId) console.log(`💬 Chat: ${config.chatId}`);
61
+ console.log();
62
+
63
+ // Start the daemon
64
+ const { start } = await import("./index.ts");
65
+ await start();
66
+ }
67
+
68
+ main().catch((err) => {
69
+ console.error("❌ Fatal error:", err?.message ?? err);
70
+ process.exit(1);
71
+ });
@@ -0,0 +1,19 @@
1
+ import js from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+ import prettier from "eslint-config-prettier";
4
+
5
+ export default tseslint.config(
6
+ js.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ prettier,
9
+ {
10
+ ignores: ["node_modules/", "server-monitor", "*.js"],
11
+ },
12
+ {
13
+ rules: {
14
+ "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
15
+ "@typescript-eslint/no-explicit-any": "warn",
16
+ "no-console": "off",
17
+ },
18
+ }
19
+ );
package/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { sendReport } from "./src/reporter";
2
+ import { saveConfig } from "./src/config";
3
+
4
+ const botToken = process.env["TELEGRAM_BOT_TOKEN"] ?? "";
5
+ let chatId = process.env["TELEGRAM_CHAT_ID"] ?? "";
6
+ const rawInterval = process.env["MONITOR_INTERVAL"] ?? "300";
7
+ const intervalSec = Math.max(30, parseInt(rawInterval) || 300);
8
+
9
+ // --- Auto-detect chat ID ---
10
+ async function autoDetectChatId(token: string): Promise<string | null> {
11
+ try {
12
+ const resp = await fetch(`https://api.telegram.org/bot${token}/getUpdates?limit=5`);
13
+ const data = (await resp.json()) as {
14
+ ok: boolean;
15
+ result?: Array<{
16
+ message?: { chat?: { id: number; title?: string; first_name?: string } };
17
+ channel_post?: { chat?: { id: number; title?: string } };
18
+ }>;
19
+ };
20
+ if (!data.ok || !data.result?.length) return null;
21
+
22
+ const chatIds = new Set<string>();
23
+ for (const update of data.result.reverse()) {
24
+ const chat = update.message?.chat || update.channel_post?.chat;
25
+ if (chat?.id) chatIds.add(String(chat.id));
26
+ }
27
+ if (chatIds.size === 0) return null;
28
+
29
+ const id = [...chatIds][0]!;
30
+ const chatInfo = data.result.find(
31
+ (u) => String(u.message?.chat?.id || u.channel_post?.chat?.id) === id
32
+ );
33
+ const chatName =
34
+ chatInfo?.message?.chat?.title ||
35
+ chatInfo?.message?.chat?.first_name ||
36
+ chatInfo?.channel_post?.chat?.title ||
37
+ "Unknown";
38
+ console.log(`🔍 Auto-detected chat: ${chatName} (ID: ${id})`);
39
+ return id;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ // --- Daemon ---
46
+ export async function start() {
47
+ if (!botToken) {
48
+ console.error("❌ TELEGRAM_BOT_TOKEN not set. Run `servermon` first to configure.");
49
+ process.exit(1);
50
+ }
51
+
52
+ if (!chatId) {
53
+ console.log("🔍 TELEGRAM_CHAT_ID not set — auto-detecting...");
54
+ const detected = await autoDetectChatId(botToken);
55
+ if (!detected) {
56
+ console.error("❌ No recent chats found. DM your bot first, then re-run.");
57
+ console.error(" Or set TELEGRAM_CHAT_ID in ~/.irsyadulibad/servermon/config.json");
58
+ process.exit(1);
59
+ }
60
+ chatId = detected;
61
+ // Persist to config
62
+ try {
63
+ const { loadConfig } = await import("./src/config");
64
+ const cfg = await loadConfig();
65
+ if (cfg) {
66
+ cfg.chatId = chatId;
67
+ await saveConfig(cfg);
68
+ console.log("💾 Chat ID saved to config");
69
+ }
70
+ } catch {
71
+ // ok
72
+ }
73
+ }
74
+
75
+ console.log(`⏱ Interval: ${intervalSec}s (${(intervalSec / 60).toFixed(0)} menit)`);
76
+ console.log(`📡 Bot: ...${botToken.slice(-8)}`);
77
+ console.log(`💬 Chat: ${chatId}`);
78
+ console.log();
79
+
80
+ async function tick() {
81
+ const start2 = Date.now();
82
+ const ok = await sendReport(botToken, chatId);
83
+ const elapsed = Date.now() - start2;
84
+ const ts = new Date().toLocaleString("id-ID", { timeZone: "Asia/Jakarta" });
85
+ console.log(`[${ts}] ${ok ? "✅" : "❌"} ${elapsed}ms`);
86
+ }
87
+
88
+ // Run once at startup
89
+ await tick();
90
+
91
+ // Loop
92
+ setInterval(tick, intervalSec * 1000);
93
+
94
+ process.on("SIGINT", () => {
95
+ console.log("\n👋 Shutting down...");
96
+ process.exit(0);
97
+ });
98
+ process.on("SIGTERM", () => {
99
+ console.log("\n👋 Shutting down...");
100
+ process.exit(0);
101
+ });
102
+ }
103
+
104
+ // Direct run support (for dev / bun index.ts)
105
+ // When imported by cli.ts, cli.ts calls start() explicitly
106
+ const isDirectlyRun = import.meta.url.endsWith(process.argv[1]?.replace(/^.*\//, "") ?? "");
107
+ if (isDirectlyRun || process.argv[1]?.endsWith("index.ts")) {
108
+ start();
109
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@irsyadulibad/servermon",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight server monitoring daemon — collects system metrics and sends structured reports to Telegram. Built with Bun + TypeScript.",
5
+ "module": "index.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "servermon": "./cli.js"
9
+ },
10
+ "private": false,
11
+ "scripts": {
12
+ "start": "bun index.ts",
13
+ "build": "bun build --compile index.ts --outfile server-monitor",
14
+ "lint": "eslint .",
15
+ "lint:fix": "eslint . --fix",
16
+ "format": "prettier --write .",
17
+ "format:check": "prettier --check .",
18
+ "check": "bun run format:check && bun run lint"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^10",
22
+ "@types/bun": "latest",
23
+ "eslint": "^10",
24
+ "eslint-config-prettier": "^10",
25
+ "prettier": "^3",
26
+ "typescript-eslint": "^8"
27
+ },
28
+ "peerDependencies": {
29
+ "typescript": "^5"
30
+ }
31
+ }
package/src/config.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { mkdir } from "fs/promises";
4
+
5
+ const CONFIG_DIR = join(homedir(), ".irsyadulibad", "servermon");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+
8
+ export interface ServerMonConfig {
9
+ token: string;
10
+ interval: number;
11
+ chatId?: string;
12
+ }
13
+
14
+ export async function ensureConfigDir(): Promise<void> {
15
+ await mkdir(CONFIG_DIR, { recursive: true });
16
+ }
17
+
18
+ export async function loadConfig(): Promise<ServerMonConfig | null> {
19
+ try {
20
+ const file = Bun.file(CONFIG_FILE);
21
+ if (!(await file.exists())) return null;
22
+ const data = await file.json();
23
+ // Minimal validation
24
+ if (!data?.token) return null;
25
+ return {
26
+ token: String(data.token),
27
+ interval: Math.max(30, parseInt(String(data.interval)) || 300),
28
+ chatId: data.chatId ? String(data.chatId) : undefined,
29
+ };
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export async function saveConfig(config: ServerMonConfig): Promise<void> {
36
+ await ensureConfigDir();
37
+ await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
38
+ }
39
+
40
+ export function configPath(): string {
41
+ return CONFIG_FILE;
42
+ }
43
+
44
+ export function configDir(): string {
45
+ return CONFIG_DIR;
46
+ }
package/src/monitor.ts ADDED
@@ -0,0 +1,236 @@
1
+ import * as os from "os";
2
+
3
+ export interface CPUMetrics {
4
+ model: string;
5
+ cores: number;
6
+ loadAvg: { "1min": number; "5min": number; "15min": number };
7
+ usagePercent: number;
8
+ }
9
+
10
+ export interface MemoryMetrics {
11
+ total: number;
12
+ used: number;
13
+ free: number;
14
+ usagePercent: number;
15
+ swapTotal: number;
16
+ swapUsed: number;
17
+ swapUsagePercent: number;
18
+ }
19
+
20
+ export interface DiskInfo {
21
+ mount: string;
22
+ total: number;
23
+ used: number;
24
+ available: number;
25
+ usagePercent: number;
26
+ }
27
+
28
+ export interface NetworkMetrics {
29
+ rxRate: number; // bytes/sec
30
+ txRate: number;
31
+ rxTotal: number;
32
+ txTotal: number;
33
+ }
34
+
35
+ export interface ProcInfo {
36
+ pid: number;
37
+ name: string;
38
+ cpuPercent: number;
39
+ memPercent: number;
40
+ }
41
+
42
+ export interface SystemMetrics {
43
+ hostname: string;
44
+ platform: string;
45
+ arch: string;
46
+ uptime: number;
47
+ cpu: CPUMetrics;
48
+ memory: MemoryMetrics;
49
+ disks: DiskInfo[];
50
+ network: NetworkMetrics;
51
+ topProcs: ProcInfo[];
52
+ temperature: number | null; // celsius
53
+ }
54
+
55
+ async function exec(cmd: string): Promise<string> {
56
+ const proc = Bun.spawn(["bash", "-c", cmd], { stdout: "pipe" });
57
+ const text = await new Response(proc.stdout).text();
58
+ await proc.exited;
59
+ return text.trim();
60
+ }
61
+
62
+ function parseDfLine(line: string): DiskInfo | null {
63
+ const parts = line.trim().split(/\s+/);
64
+ if (parts.length < 6) return null;
65
+ const total = parseInt(parts[1]!) * 1024;
66
+ const used = parseInt(parts[2]!) * 1024;
67
+ const available = parseInt(parts[3]!) * 1024;
68
+ const usagePercent = parseInt(parts[4]!);
69
+ const mount = parts[5]!;
70
+ return { mount, total, used, available, usagePercent };
71
+ }
72
+
73
+ async function readNetDev(): Promise<{ rx: number; tx: number }> {
74
+ const data = await exec("cat /proc/net/dev 2>/dev/null");
75
+ let rx = 0,
76
+ tx = 0;
77
+ for (const line of data.split("\n")) {
78
+ if (!line.includes(":")) continue;
79
+ const ifname = line.split(":")[0]!.trim();
80
+ // Skip loopback
81
+ if (ifname === "lo") continue;
82
+ // Skip veth, docker
83
+ if (ifname.startsWith("veth") || ifname.startsWith("docker") || ifname.startsWith("br-"))
84
+ continue;
85
+ const parts = line.split(":")[1]!.trim().split(/\s+/);
86
+ rx += parseInt(parts[0]!) || 0;
87
+ tx += parseInt(parts[8]!) || 0;
88
+ }
89
+ return { rx, tx };
90
+ }
91
+
92
+ async function readTemperature(): Promise<number | null> {
93
+ try {
94
+ const zones = await exec(
95
+ `for z in /sys/class/thermal/thermal_zone*/temp; do [ -r "$z" ] && echo "$z=$(cat "$z")"; done 2>/dev/null`
96
+ );
97
+ if (!zones) return null;
98
+ let best = Number.MAX_VALUE;
99
+ for (const line of zones.split("\n")) {
100
+ const val = parseInt(line.split("=")[1]!);
101
+ // thermal_zone0 often the CPU package; pick the highest non-zero temp
102
+ if (val > 0 && val < best) best = val;
103
+ // Actually we want the HIGHEST, not lowest
104
+ }
105
+ // Reread — pick highest
106
+ let highest = -Infinity;
107
+ for (const line of zones.split("\n")) {
108
+ const val = parseInt(line.split("=")[1]!);
109
+ if (val > 0 && val > highest) highest = val;
110
+ }
111
+ return highest > 0 ? highest / 1000 : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ export async function collectMetrics(): Promise<SystemMetrics> {
118
+ // --- disk ---
119
+ const dfOutput = await exec("df -P -B1 / /home 2>/dev/null");
120
+ const disks: DiskInfo[] = dfOutput
121
+ .split("\n")
122
+ .slice(1)
123
+ .map(parseDfLine)
124
+ .filter(Boolean)
125
+ .filter((d, i, arr) => arr.findIndex((x) => x.mount === d.mount) === i) as DiskInfo[];
126
+
127
+ // --- memory ---
128
+ const memOutput = await exec("free -b 2>/dev/null");
129
+ const memLines = memOutput.split("\n");
130
+ const memParts = memLines[1]?.trim().split(/\s+/);
131
+ const swapParts = memLines[2]?.trim().split(/\s+/);
132
+ const memTotal = memParts ? parseInt(memParts[1]!) : os.totalmem();
133
+ const memUsed = memParts ? parseInt(memParts[2]!) : os.totalmem() - os.freemem();
134
+ const memFree = memParts ? parseInt(memParts[3]!) : os.freemem();
135
+ const swapTotal = swapParts ? parseInt(swapParts[1]!) : 0;
136
+ const swapUsed = swapParts ? parseInt(swapParts[2]!) : 0;
137
+
138
+ // --- CPU usage (poll /proc/stat with 500ms delay) ---
139
+ const loadAvg = os.loadavg();
140
+ const stat1 = await exec("cat /proc/stat | grep '^cpu '");
141
+ await new Promise((r) => setTimeout(r, 500));
142
+ const stat2 = await exec("cat /proc/stat | grep '^cpu '");
143
+
144
+ function cpuTicks(stat: string): number[] {
145
+ return stat.split(/\s+/).slice(1).map(Number);
146
+ }
147
+ const t1 = cpuTicks(stat1);
148
+ const t2 = cpuTicks(stat2);
149
+ let usagePercent = 0;
150
+ if (t1.length >= 4 && t2.length >= 4) {
151
+ const idle1 = t1[3]! + (t1[4] ?? 0);
152
+ const idle2 = t2[3]! + (t2[4] ?? 0);
153
+ const total1 = t1.reduce((a, b) => a + b, 0);
154
+ const total2 = t2.reduce((a, b) => a + b, 0);
155
+ const totalDelta = total2 - total1;
156
+ const idleDelta = idle2 - idle1;
157
+ if (totalDelta > 0) usagePercent = ((totalDelta - idleDelta) / totalDelta) * 100;
158
+ }
159
+
160
+ // --- network rate (poll 1s delta) ---
161
+ const net1 = await readNetDev();
162
+ await new Promise((r) => setTimeout(r, 1000));
163
+ const net2 = await readNetDev();
164
+ const rxRate = Math.max(0, net2.rx - net1.rx); // bytes/sec
165
+ const txRate = Math.max(0, net2.tx - net1.tx);
166
+
167
+ // --- top processes ---
168
+ const topOutput = await exec(
169
+ "ps -eo pid,comm,pcpu,pmem --sort=-pcpu --no-headers 2>/dev/null | head -5"
170
+ );
171
+ const topProcs: ProcInfo[] = topOutput
172
+ .split("\n")
173
+ .filter(Boolean)
174
+ .map((line) => {
175
+ const p = line.trim().split(/\s+/);
176
+ return {
177
+ pid: parseInt(p[0]!) || 0,
178
+ name: p[1]?.slice(0, 15) ?? "?",
179
+ cpuPercent: parseFloat(p[2]!) || 0,
180
+ memPercent: parseFloat(p[3]!) || 0,
181
+ };
182
+ });
183
+
184
+ // --- temperature ---
185
+ const temperature = await readTemperature();
186
+
187
+ return {
188
+ hostname: os.hostname(),
189
+ platform: os.platform(),
190
+ arch: os.machine(),
191
+ uptime: os.uptime(),
192
+ cpu: {
193
+ model: (os.cpus()[0]?.model ?? "unknown").trim(),
194
+ cores: os.cpus().length,
195
+ loadAvg: { "1min": loadAvg[0]!, "5min": loadAvg[1]!, "15min": loadAvg[2]! },
196
+ usagePercent: Math.round(usagePercent * 10) / 10,
197
+ },
198
+ memory: {
199
+ total: memTotal,
200
+ used: memUsed,
201
+ free: memFree,
202
+ usagePercent: Math.round((memUsed / memTotal) * 1000) / 10,
203
+ swapTotal,
204
+ swapUsed,
205
+ swapUsagePercent: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 1000) / 10 : 0,
206
+ },
207
+ disks,
208
+ network: { rxRate, txRate, rxTotal: net2.rx, txTotal: net2.tx },
209
+ topProcs,
210
+ temperature,
211
+ };
212
+ }
213
+
214
+ export function formatBytes(bytes: number): string {
215
+ if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GiB`;
216
+ if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MiB`;
217
+ if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KiB`;
218
+ return `${bytes} B`;
219
+ }
220
+
221
+ export function formatRate(bytesPerSec: number): string {
222
+ if (bytesPerSec >= 1_048_576) return `${(bytesPerSec / 1_048_576).toFixed(2)} MB/s`;
223
+ if (bytesPerSec >= 1_024) return `${(bytesPerSec / 1_024).toFixed(1)} KB/s`;
224
+ return `${bytesPerSec} B/s`;
225
+ }
226
+
227
+ export function formatUptime(seconds: number): string {
228
+ const d = Math.floor(seconds / 86400);
229
+ const h = Math.floor((seconds % 86400) / 3600);
230
+ const m = Math.floor((seconds % 3600) / 60);
231
+ const parts: string[] = [];
232
+ if (d > 0) parts.push(`${d}d`);
233
+ if (h > 0) parts.push(`${h}h`);
234
+ parts.push(`${m}m`);
235
+ return parts.join(" ");
236
+ }
@@ -0,0 +1,181 @@
1
+ import { formatBytes, formatRate, formatUptime, type SystemMetrics } from "./monitor";
2
+
3
+ // HTML escape
4
+ function esc(s: string): string {
5
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6
+ }
7
+
8
+ // Bar chart with inline colored blocks
9
+ function bar(percent: number, w = 10): string {
10
+ const filled = Math.min(w, Math.max(0, Math.round((percent / 100) * w)));
11
+ const empty = w - filled;
12
+ const color = percent > 80 ? "🔴" : percent > 50 ? "🟡" : "🟢";
13
+ return color + " " + "▰".repeat(filled) + "▱".repeat(empty);
14
+ }
15
+
16
+ function pad(n: number, dp = 1): string {
17
+ return n.toFixed(dp).padStart(5);
18
+ }
19
+
20
+ // Overall health
21
+ function healthTag(m: SystemMetrics): string {
22
+ const d = m.disks.length ? Math.max(...m.disks.map((x) => x.usagePercent)) : 0;
23
+ if (m.cpu.usagePercent > 85 || m.memory.usagePercent > 95 || d > 95) return "🚨 CRITICAL";
24
+ if (m.cpu.usagePercent > 70 || m.memory.usagePercent > 85 || d > 85) return "⚠️ WARNING";
25
+ return "✅ HEALTHY";
26
+ }
27
+
28
+ export function formatReportHTML(m: SystemMetrics): string {
29
+ const now = new Date().toLocaleString("id-ID", {
30
+ timeZone: "Asia/Jakarta",
31
+ day: "numeric",
32
+ month: "short",
33
+ year: "numeric",
34
+ hour: "2-digit",
35
+ minute: "2-digit",
36
+ });
37
+
38
+ const tempStr = m.temperature !== null ? ` 🌡 ${m.temperature.toFixed(0)}°C` : "";
39
+
40
+ // ── Header ──
41
+ const header = [
42
+ `<b>🖥 ${esc(m.hostname)}</b> — ${healthTag(m)}`,
43
+ `📅 ${esc(now)} │ ⏱ ${esc(formatUptime(m.uptime))}${tempStr}`,
44
+ `🐧 ${esc(m.platform)} ${esc(m.arch)} │ ${esc(m.cpu.model)} (${m.cpu.cores}c)`,
45
+ ];
46
+
47
+ // ── CPU card ──
48
+ const cpu = [
49
+ `<b>💻 CPU</b> <code>${pad(m.cpu.usagePercent)}%</code> ${bar(m.cpu.usagePercent)}`,
50
+ ` Load: <code>${m.cpu.loadAvg["1min"].toFixed(2)}</code> / <code>${m.cpu.loadAvg["5min"].toFixed(2)}</code> / <code>${m.cpu.loadAvg["15min"].toFixed(2)}</code>`,
51
+ ];
52
+
53
+ // ── Memory card ──
54
+ const mem = [
55
+ `<b>🧠 RAM</b> <code>${pad(m.memory.usagePercent)}%</code> ${bar(m.memory.usagePercent)}`,
56
+ ` <code>${esc(formatBytes(m.memory.used))}</code> / <code>${esc(formatBytes(m.memory.total))}</code>`,
57
+ m.memory.swapTotal > 0
58
+ ? ` Swap: <code>${esc(formatBytes(m.memory.swapUsed))}</code> / <code>${esc(formatBytes(m.memory.swapTotal))}</code> (${m.memory.swapUsagePercent.toFixed(1)}%)`
59
+ : "",
60
+ ].filter(Boolean);
61
+
62
+ // ── Disk card ──
63
+ const disks = [`<b>💾 DISK</b>`];
64
+ for (const d of m.disks) {
65
+ disks.push(
66
+ ` <code>${esc(d.mount)}</code> <code>${pad(d.usagePercent, 0)}%</code> ${bar(d.usagePercent)}`
67
+ );
68
+ disks.push(
69
+ ` └ <code>${esc(formatBytes(d.used))}</code> / <code>${esc(formatBytes(d.total))}</code>`
70
+ );
71
+ }
72
+
73
+ // ── Network card ──
74
+ const net = [
75
+ `<b>🌐 NET</b>`,
76
+ ` ↓ <code>${esc(formatRate(m.network.rxRate))}</code> ↑ <code>${esc(formatRate(m.network.txRate))}</code>`,
77
+ ];
78
+
79
+ // ── Top processes ──
80
+ const procLines: string[] = [];
81
+ if (m.topProcs.length > 0) {
82
+ procLines.push(`<b>📊 TOP PROCESSES</b>`);
83
+ for (const p of m.topProcs) {
84
+ procLines.push(
85
+ ` <code>${String(p.pid).padStart(6)}</code> ${esc(p.name.slice(0, 15).padEnd(15))} CPU <code>${p.cpuPercent.toFixed(1).padStart(5)}%</code> MEM <code>${p.memPercent.toFixed(1).padStart(5)}%</code>`
86
+ );
87
+ }
88
+ }
89
+
90
+ // ── Alerts ──
91
+ const alerts: string[] = [];
92
+ if (m.cpu.usagePercent > 85) alerts.push(`🔴 CPU tinggi: ${m.cpu.usagePercent.toFixed(1)}%`);
93
+ if (m.memory.usagePercent > 90)
94
+ alerts.push(`🔴 RAM hampir penuh: ${m.memory.usagePercent.toFixed(1)}%`);
95
+ if (m.memory.swapUsagePercent > 50)
96
+ alerts.push(`🟡 Swap tinggi: ${m.memory.swapUsagePercent.toFixed(1)}%`);
97
+ for (const d of m.disks) {
98
+ if (d.usagePercent > 90)
99
+ alerts.push(`🔴 Disk <code>${esc(d.mount)}</code>: ${d.usagePercent}%`);
100
+ }
101
+
102
+ if (alerts.length > 0) {
103
+ alerts.unshift(`<b>⚠️ ALERTS</b>`);
104
+ }
105
+
106
+ return [
107
+ ...header,
108
+ "",
109
+ ...cpu,
110
+ "",
111
+ ...mem,
112
+ "",
113
+ ...disks,
114
+ "",
115
+ ...net,
116
+ ...(procLines.length ? ["", ...procLines] : []),
117
+ ...(alerts.length ? ["", ...alerts] : []),
118
+ "",
119
+ alerts.length === 0 ? "✨ All systems normal" : "",
120
+ ]
121
+ .filter(Boolean)
122
+ .join("\n");
123
+ }
124
+
125
+ async function sendMessage(botToken: string, chatId: string, text: string): Promise<Response> {
126
+ return fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({
130
+ chat_id: chatId,
131
+ text,
132
+ parse_mode: "HTML",
133
+ disable_web_page_preview: true,
134
+ }),
135
+ });
136
+ }
137
+
138
+ export async function sendReport(botToken: string, chatId: string): Promise<boolean> {
139
+ if (!botToken || !chatId) {
140
+ console.error("❌ TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are required.");
141
+ return false;
142
+ }
143
+
144
+ const m = await import("./monitor").then((x) => x.collectMetrics());
145
+ const report = formatReportHTML(m);
146
+
147
+ // Telegram 4096 char limit — split on double newlines
148
+ if (report.length > 4000) {
149
+ const chunks = report.split("\n\n");
150
+ let current = "";
151
+ for (const chunk of chunks) {
152
+ if (current.length + chunk.length + 2 > 4000) {
153
+ const r = await sendMessage(botToken, chatId, current);
154
+ if (!r.ok) {
155
+ console.error(`❌ Telegram: ${r.status} ${await r.text()}`);
156
+ return false;
157
+ }
158
+ current = chunk;
159
+ } else {
160
+ current += (current ? "\n\n" : "") + chunk;
161
+ }
162
+ }
163
+ if (current) {
164
+ const r = await sendMessage(botToken, chatId, current);
165
+ if (!r.ok) {
166
+ console.error(`❌ Telegram: ${r.status} ${await r.text()}`);
167
+ return false;
168
+ }
169
+ }
170
+ console.log("📤 Report sent (chunked)");
171
+ return true;
172
+ }
173
+
174
+ const resp = await sendMessage(botToken, chatId, report);
175
+ if (!resp.ok) {
176
+ console.error(`❌ Telegram: ${resp.status} ${await resp.text()}`);
177
+ return false;
178
+ }
179
+ console.log("📤 Report sent to Telegram");
180
+ return true;
181
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+ "types": ["bun"],
11
+
12
+ // Bundler mode
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "verbatimModuleSyntax": true,
16
+ "noEmit": true,
17
+
18
+ // Best practices
19
+ "strict": true,
20
+ "skipLibCheck": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+
25
+ // Some stricter flags (disabled by default)
26
+ "noUnusedLocals": false,
27
+ "noUnusedParameters": false,
28
+ "noPropertyAccessFromIndexSignature": false
29
+ }
30
+ }