opencode-zombie-monitor 1.0.1 → 1.1.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/README.md +48 -16
- package/index.mjs +71 -37
- package/package.json +2 -2
- package/download-emoji.mjs +0 -115
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# 🧟 OpenCode Zombie Monitor
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-zombie-monitor)
|
|
3
4
|
[](https://github.com/Matroskin86/opencode-zombie-monitor/blob/main/LICENSE)
|
|
4
5
|
[]()
|
|
5
6
|
[]()
|
|
@@ -39,7 +40,7 @@ Add to your `opencode.json`:
|
|
|
39
40
|
|
|
40
41
|
```json
|
|
41
42
|
{
|
|
42
|
-
"plugin": ["
|
|
43
|
+
"plugin": ["opencode-zombie-monitor"]
|
|
43
44
|
}
|
|
44
45
|
```
|
|
45
46
|
|
|
@@ -53,40 +54,71 @@ Just chat normally. The plugin silently patrols your system and eliminates zombi
|
|
|
53
54
|
|
|
54
55
|
When zombies are neutralized, you'll see:
|
|
55
56
|
```
|
|
56
|
-
🧟 Killed 3 zombie
|
|
57
|
+
🧟 Killed 3 zombie processes | Freed 300MB RAM | Headshot! 💥
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
###
|
|
60
|
+
### Commands
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| `/zombies` | Check zombie status |
|
|
65
|
+
| `/kill-zombies` | Manually kill all zombies |
|
|
62
66
|
|
|
67
|
+
**Check status:**
|
|
63
68
|
```
|
|
64
69
|
/zombies
|
|
65
70
|
```
|
|
66
|
-
|
|
67
|
-
Response when all clear:
|
|
68
71
|
```
|
|
69
72
|
✅ 2 processes, no zombies
|
|
70
73
|
```
|
|
74
|
+
or
|
|
75
|
+
```
|
|
76
|
+
🧟 5 zombies of 7 processes | 500MB RAM | /kill-zombies
|
|
77
|
+
```
|
|
71
78
|
|
|
72
|
-
|
|
79
|
+
**Manual kill:**
|
|
80
|
+
```
|
|
81
|
+
/kill-zombies
|
|
82
|
+
```
|
|
73
83
|
```
|
|
74
|
-
|
|
84
|
+
💥 Headshot! Killed 5 zombies | Freed 500MB RAM
|
|
75
85
|
```
|
|
76
86
|
|
|
77
87
|
## ⚙️ Configuration
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
Configure in your `opencode.json`:
|
|
80
90
|
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"plugin": [
|
|
94
|
+
["opencode-zombie-monitor", {
|
|
95
|
+
"autoKill": true,
|
|
96
|
+
"threshold": 1
|
|
97
|
+
}]
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
84
101
|
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
| Option | Default | Description |
|
|
103
|
+
|--------|---------|-------------|
|
|
104
|
+
| `autoKill` | `true` | Auto-kill zombies on every message |
|
|
105
|
+
| `threshold` | `1` | Minimum zombies to trigger action |
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
### Modes
|
|
108
|
+
|
|
109
|
+
**RAMBO MODE** (default) — Kill on sight:
|
|
110
|
+
```json
|
|
111
|
+
{ "autoKill": true, "threshold": 1 }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**PATIENT MODE** — Wait until horde forms:
|
|
115
|
+
```json
|
|
116
|
+
{ "autoKill": true, "threshold": 5 }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**MANUAL MODE** — Only notify, kill with `/kill-zombies`:
|
|
120
|
+
```json
|
|
121
|
+
{ "autoKill": false }
|
|
90
122
|
```
|
|
91
123
|
|
|
92
124
|
## 🖥️ Supported Platforms
|
package/index.mjs
CHANGED
|
@@ -18,45 +18,55 @@ const getLang = () => {
|
|
|
18
18
|
return "en"
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Format memory size
|
|
22
|
+
const formatMB = (mb) => mb >= 1024 ? `${(mb / 1024).toFixed(1)}GB` : `${Math.round(mb)}MB`
|
|
23
|
+
|
|
21
24
|
// Localized messages
|
|
22
25
|
const i18n = {
|
|
23
26
|
en: {
|
|
24
|
-
killed: (n) => `🧟 Killed ${n} zombie
|
|
25
|
-
found: (n) => `🧟 ${n} zombie
|
|
26
|
-
status: (zombies, total) => zombies > 0
|
|
27
|
-
? `🧟 ${zombies} zombie${zombies > 1 ? "s" : ""} of ${total} process${total > 1 ? "es" : ""} |
|
|
27
|
+
killed: (n, mb) => `🧟 Killed ${n} zombie process${n > 1 ? "es" : ""} | Freed ${formatMB(mb)} RAM | Headshot! 💥`,
|
|
28
|
+
found: (n, mb) => `🧟 ${n} zombie process${n > 1 ? "es" : ""} | ${formatMB(mb)} RAM | /kill-zombies`,
|
|
29
|
+
status: (zombies, total, mb) => zombies > 0
|
|
30
|
+
? `🧟 ${zombies} zombie${zombies > 1 ? "s" : ""} of ${total} process${total > 1 ? "es" : ""} | ${formatMB(mb)} RAM | /kill-zombies`
|
|
28
31
|
: `✅ ${total} process${total > 1 ? "es" : ""}, no zombies`,
|
|
29
|
-
commandDesc: "Check zombie opencode processes"
|
|
32
|
+
commandDesc: "Check zombie opencode processes",
|
|
33
|
+
killDesc: "Kill zombie opencode processes",
|
|
34
|
+
manualKilled: (n, mb) => `💥 Headshot! Killed ${n} zombie${n > 1 ? "s" : ""} | Freed ${formatMB(mb)} RAM`
|
|
30
35
|
},
|
|
31
36
|
ru: {
|
|
32
|
-
killed: (n) => `🧟 Убито ${n}
|
|
33
|
-
found: (n) => `🧟 ${n}
|
|
34
|
-
status: (zombies, total) => zombies > 0
|
|
35
|
-
? `🧟 ${zombies} зомби из ${total} процессов |
|
|
37
|
+
killed: (n, mb) => `🧟 Убито ${n} зомби-процесс${n > 1 ? "ов" : ""} | Освобождено ${formatMB(mb)} RAM | Headshot! 💥`,
|
|
38
|
+
found: (n, mb) => `🧟 ${n} зомби-процесс${n > 1 ? "ов" : ""} | ${formatMB(mb)} RAM | /kill-zombies`,
|
|
39
|
+
status: (zombies, total, mb) => zombies > 0
|
|
40
|
+
? `🧟 ${zombies} зомби из ${total} процессов | ${formatMB(mb)} RAM | /kill-zombies`
|
|
36
41
|
: `✅ ${total} процессов, зомби нет`,
|
|
37
|
-
commandDesc: "Проверить зомби-процессы opencode"
|
|
42
|
+
commandDesc: "Проверить зомби-процессы opencode",
|
|
43
|
+
killDesc: "Убить зомби-процессы opencode",
|
|
44
|
+
manualKilled: (n, mb) => `💥 Headshot! Убито ${n} зомби | Освобождено ${formatMB(mb)} RAM`
|
|
38
45
|
},
|
|
39
46
|
zh: {
|
|
40
|
-
killed: (n) => `🧟 已击杀 ${n}
|
|
41
|
-
found: (n) => `🧟 发现 ${n}
|
|
42
|
-
status: (zombies, total) => zombies > 0
|
|
43
|
-
? `🧟 ${total} 个进程中有 ${zombies} 个僵尸 |
|
|
47
|
+
killed: (n, mb) => `🧟 已击杀 ${n} 个僵尸进程 | 释放 ${formatMB(mb)} 内存 | Headshot! 💥`,
|
|
48
|
+
found: (n, mb) => `🧟 发现 ${n} 个僵尸进程 | ${formatMB(mb)} 内存 | /kill-zombies`,
|
|
49
|
+
status: (zombies, total, mb) => zombies > 0
|
|
50
|
+
? `🧟 ${total} 个进程中有 ${zombies} 个僵尸 | ${formatMB(mb)} 内存 | /kill-zombies`
|
|
44
51
|
: `✅ ${total} 个进程,没有僵尸`,
|
|
45
|
-
commandDesc: "
|
|
52
|
+
commandDesc: "检查僵尸进程",
|
|
53
|
+
killDesc: "击杀僵尸进程",
|
|
54
|
+
manualKilled: (n, mb) => `💥 Headshot! 已击杀 ${n} 个僵尸 | 释放 ${formatMB(mb)} 内存`
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
const t = i18n[getLang()]
|
|
50
59
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
60
|
+
// Get zombie count and their total memory (RSS in MB)
|
|
61
|
+
const getZombieStats = async () => {
|
|
53
62
|
try {
|
|
54
|
-
// macOS: TTY = "??" | Linux: TTY = "?"
|
|
55
63
|
const ttyPattern = isMac ? '??' : '?'
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
// $6 = RSS in KB on both macOS and Linux
|
|
65
|
+
const { stdout } = await execAsync(`ps aux | grep "[o]pencode" | grep -v "opencode/" | awk '$7 == "${ttyPattern}" {count++; mem+=$6} END {print count+0, mem/1024}'`)
|
|
66
|
+
const [count, mb] = stdout.trim().split(/\s+/)
|
|
67
|
+
return { count: parseInt(count) || 0, mb: parseFloat(mb) || 0 }
|
|
58
68
|
} catch {
|
|
59
|
-
return 0
|
|
69
|
+
return { count: 0, mb: 0 }
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
72
|
|
|
@@ -81,8 +91,13 @@ const killZombies = async () => {
|
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
//
|
|
85
|
-
|
|
94
|
+
// Configuration defaults
|
|
95
|
+
// autoKill: true = kill automatically, false = notify only (manual mode)
|
|
96
|
+
// threshold: minimum zombies to trigger action (1 = immediate)
|
|
97
|
+
const DEFAULT_CONFIG = {
|
|
98
|
+
autoKill: true,
|
|
99
|
+
threshold: 1
|
|
100
|
+
}
|
|
86
101
|
|
|
87
102
|
const sendNotification = async (client, sessionId, text) => {
|
|
88
103
|
await client.session.prompt({
|
|
@@ -103,8 +118,11 @@ const getSessionIdFromMessages = (messages) => {
|
|
|
103
118
|
return null
|
|
104
119
|
}
|
|
105
120
|
|
|
106
|
-
export const ZombieMonitor = async ({ client }) => {
|
|
121
|
+
export const ZombieMonitor = async ({ client, config: pluginConfig }) => {
|
|
107
122
|
let lastNotifiedCount = 0
|
|
123
|
+
|
|
124
|
+
// Merge user config with defaults
|
|
125
|
+
const cfg = { ...DEFAULT_CONFIG, ...pluginConfig }
|
|
108
126
|
|
|
109
127
|
return {
|
|
110
128
|
config: async (opencodeConfig) => {
|
|
@@ -113,27 +131,31 @@ export const ZombieMonitor = async ({ client }) => {
|
|
|
113
131
|
template: "",
|
|
114
132
|
description: t.commandDesc
|
|
115
133
|
}
|
|
134
|
+
opencodeConfig.command["kill-zombies"] = {
|
|
135
|
+
template: "",
|
|
136
|
+
description: t.killDesc
|
|
137
|
+
}
|
|
116
138
|
},
|
|
117
139
|
|
|
118
140
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
119
141
|
const sessionId = getSessionIdFromMessages(output.messages)
|
|
120
142
|
if (!sessionId) return
|
|
121
143
|
|
|
122
|
-
const zombies = await
|
|
144
|
+
const { count: zombies, mb } = await getZombieStats()
|
|
123
145
|
|
|
124
|
-
// Auto-kill if zombies >= threshold
|
|
125
|
-
if (zombies >=
|
|
146
|
+
// Auto-kill if enabled and zombies >= threshold
|
|
147
|
+
if (cfg.autoKill && zombies >= cfg.threshold) {
|
|
126
148
|
await killZombies()
|
|
127
149
|
try {
|
|
128
|
-
await sendNotification(client, sessionId, t.killed(zombies))
|
|
150
|
+
await sendNotification(client, sessionId, t.killed(zombies, mb))
|
|
129
151
|
} catch (e) {}
|
|
130
152
|
lastNotifiedCount = 0
|
|
131
153
|
}
|
|
132
|
-
// Notify if zombies appeared (
|
|
154
|
+
// Notify if zombies appeared (manual mode or below threshold)
|
|
133
155
|
else if (zombies > 0 && zombies > lastNotifiedCount) {
|
|
134
156
|
lastNotifiedCount = zombies
|
|
135
157
|
try {
|
|
136
|
-
await sendNotification(client, sessionId, t.found(zombies))
|
|
158
|
+
await sendNotification(client, sessionId, t.found(zombies, mb))
|
|
137
159
|
} catch (e) {}
|
|
138
160
|
} else if (zombies === 0) {
|
|
139
161
|
lastNotifiedCount = 0
|
|
@@ -141,13 +163,25 @@ export const ZombieMonitor = async ({ client }) => {
|
|
|
141
163
|
},
|
|
142
164
|
|
|
143
165
|
"command.execute.before": async (input) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
// /zombies - check status
|
|
167
|
+
if (input.command === "zombies") {
|
|
168
|
+
const { count: zombies, mb } = await getZombieStats()
|
|
169
|
+
const total = await getTotalCount()
|
|
170
|
+
await sendNotification(client, input.sessionID, t.status(zombies, total, mb))
|
|
171
|
+
throw new Error("__ZOMBIES_HANDLED__")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// /kill-zombies - manual kill
|
|
175
|
+
if (input.command === "kill-zombies") {
|
|
176
|
+
const { count: zombies, mb } = await getZombieStats()
|
|
177
|
+
if (zombies > 0) {
|
|
178
|
+
await killZombies()
|
|
179
|
+
await sendNotification(client, input.sessionID, t.manualKilled(zombies, mb))
|
|
180
|
+
} else {
|
|
181
|
+
await sendNotification(client, input.sessionID, t.status(0, await getTotalCount(), 0))
|
|
182
|
+
}
|
|
183
|
+
throw new Error("__ZOMBIES_HANDLED__")
|
|
184
|
+
}
|
|
151
185
|
}
|
|
152
186
|
}
|
|
153
187
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-zombie-monitor",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Auto-kill zombie opencode processes. Supports macOS and Linux with auto language detection (EN/RU).",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/
|
|
12
|
+
"url": "https://github.com/Matroskin86/opencode-zombie-monitor"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"@opencode-ai/plugin": ">=1.0.0"
|
package/download-emoji.mjs
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Скрипт для скачивания Telegram custom emoji
|
|
3
|
-
*
|
|
4
|
-
* 1. Отправь эмодзи боту @your_bot
|
|
5
|
-
* 2. Запусти: node download-emoji.mjs
|
|
6
|
-
* 3. Эмодзи сохранится в ./assets/emoji.webp
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import https from 'https'
|
|
10
|
-
import fs from 'fs'
|
|
11
|
-
import path from 'path'
|
|
12
|
-
|
|
13
|
-
const BOT_TOKEN = '8061734285:AAHeLB0YHXomdjPPyTg18GM0_RrdvTbp4aA'
|
|
14
|
-
const API_URL = `https://api.telegram.org/bot${BOT_TOKEN}`
|
|
15
|
-
|
|
16
|
-
async function fetchJson(url) {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
https.get(url, (res) => {
|
|
19
|
-
let data = ''
|
|
20
|
-
res.on('data', chunk => data += chunk)
|
|
21
|
-
res.on('end', () => resolve(JSON.parse(data)))
|
|
22
|
-
}).on('error', reject)
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function downloadFile(filePath, destPath) {
|
|
27
|
-
const fileUrl = `https://api.telegram.org/file/bot${BOT_TOKEN}/${filePath}`
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
const file = fs.createWriteStream(destPath)
|
|
30
|
-
https.get(fileUrl, (res) => {
|
|
31
|
-
res.pipe(file)
|
|
32
|
-
file.on('finish', () => {
|
|
33
|
-
file.close()
|
|
34
|
-
resolve(destPath)
|
|
35
|
-
})
|
|
36
|
-
}).on('error', reject)
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function main() {
|
|
41
|
-
console.log('🔍 Получаю последние сообщения...')
|
|
42
|
-
|
|
43
|
-
// Получить updates
|
|
44
|
-
const updates = await fetchJson(`${API_URL}/getUpdates?limit=10`)
|
|
45
|
-
|
|
46
|
-
if (!updates.ok || !updates.result.length) {
|
|
47
|
-
console.log('❌ Нет сообщений. Отправь эмодзи боту и попробуй снова.')
|
|
48
|
-
return
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Найти сообщение с custom emoji
|
|
52
|
-
let customEmojiId = null
|
|
53
|
-
let messageText = null
|
|
54
|
-
|
|
55
|
-
for (const update of updates.result.reverse()) {
|
|
56
|
-
const msg = update.message
|
|
57
|
-
if (msg?.entities) {
|
|
58
|
-
for (const entity of msg.entities) {
|
|
59
|
-
if (entity.type === 'custom_emoji') {
|
|
60
|
-
customEmojiId = entity.custom_emoji_id
|
|
61
|
-
messageText = msg.text
|
|
62
|
-
break
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (customEmojiId) break
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!customEmojiId) {
|
|
70
|
-
console.log('❌ Custom emoji не найден. Отправь эмодзи из платного/премиум пака.')
|
|
71
|
-
console.log('📋 Последние сообщения:', updates.result.map(u => u.message?.text).filter(Boolean))
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
console.log(`✅ Найден emoji: "${messageText}" (ID: ${customEmojiId})`)
|
|
76
|
-
|
|
77
|
-
// Получить стикер по emoji ID
|
|
78
|
-
console.log('📥 Получаю файл стикера...')
|
|
79
|
-
const stickers = await fetchJson(`${API_URL}/getCustomEmojiStickers?custom_emoji_ids=["${customEmojiId}"]`)
|
|
80
|
-
|
|
81
|
-
if (!stickers.ok || !stickers.result.length) {
|
|
82
|
-
console.log('❌ Не удалось получить стикер:', stickers)
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const sticker = stickers.result[0]
|
|
87
|
-
console.log(`📦 Стикер: ${sticker.file_id}`)
|
|
88
|
-
|
|
89
|
-
// Получить путь к файлу
|
|
90
|
-
const fileInfo = await fetchJson(`${API_URL}/getFile?file_id=${sticker.file_id}`)
|
|
91
|
-
|
|
92
|
-
if (!fileInfo.ok) {
|
|
93
|
-
console.log('❌ Не удалось получить файл:', fileInfo)
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Создать папку assets
|
|
98
|
-
const assetsDir = './assets'
|
|
99
|
-
if (!fs.existsSync(assetsDir)) {
|
|
100
|
-
fs.mkdirSync(assetsDir)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Скачать файл
|
|
104
|
-
const ext = path.extname(fileInfo.result.file_path) || '.webp'
|
|
105
|
-
const destPath = `${assetsDir}/emoji${ext}`
|
|
106
|
-
|
|
107
|
-
console.log(`💾 Скачиваю в ${destPath}...`)
|
|
108
|
-
await downloadFile(fileInfo.result.file_path, destPath)
|
|
109
|
-
|
|
110
|
-
console.log(`✅ Готово! Файл сохранён: ${destPath}`)
|
|
111
|
-
console.log(`\n📝 Для README используй:`)
|
|
112
|
-
console.log(`<img src="${destPath}" width="20" height="20">`)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
main().catch(console.error)
|