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 CHANGED
@@ -1,5 +1,6 @@
1
1
  # 🧟 OpenCode Zombie Monitor
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/opencode-zombie-monitor)](https://www.npmjs.com/package/opencode-zombie-monitor)
3
4
  [![GitHub](https://img.shields.io/github/license/Matroskin86/opencode-zombie-monitor)](https://github.com/Matroskin86/opencode-zombie-monitor/blob/main/LICENSE)
4
5
  [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blue)]()
5
6
  [![OpenCode](https://img.shields.io/badge/opencode-plugin-purple)]()
@@ -39,7 +40,7 @@ Add to your `opencode.json`:
39
40
 
40
41
  ```json
41
42
  {
42
- "plugin": ["github:Matroskin86/opencode-zombie-monitor"]
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 opencode processes | Freed ~300MB RAM
57
+ 🧟 Killed 3 zombie processes | Freed 300MB RAM | Headshot! 💥
57
58
  ```
58
59
 
59
- ### Manual Mode
60
+ ### Commands
60
61
 
61
- Want to check the situation yourself?
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
- Response when trouble brewing:
79
+ **Manual kill:**
80
+ ```
81
+ /kill-zombies
82
+ ```
73
83
  ```
74
- 🧟 5 zombies of 7 processes | ~500MB RAM | Fix: oc-kill-zombies
84
+ 💥 Headshot! Killed 5 zombies | Freed 500MB RAM
75
85
  ```
76
86
 
77
87
  ## ⚙️ Configuration
78
88
 
79
- Edit `index.mjs` to adjust aggression level:
89
+ Configure in your `opencode.json`:
80
90
 
81
- ```javascript
82
- // RAMBO MODE: Kill on sight (default)
83
- const AUTO_KILL_THRESHOLD = 1
91
+ ```json
92
+ {
93
+ "plugin": [
94
+ ["opencode-zombie-monitor", {
95
+ "autoKill": true,
96
+ "threshold": 1
97
+ }]
98
+ ]
99
+ }
100
+ ```
84
101
 
85
- // PATIENT MODE: Wait until horde forms
86
- const AUTO_KILL_THRESHOLD = 5
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
- // PACIFIST MODE: Only notify, never kill
89
- const AUTO_KILL_THRESHOLD = 999
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 opencode process${n > 1 ? "es" : ""} | Freed ~${n * 100}MB RAM`,
25
- found: (n) => `🧟 ${n} zombie opencode process${n > 1 ? "es" : ""} | ~${n * 100}MB RAM | Fix: oc-kill-zombies`,
26
- status: (zombies, total) => zombies > 0
27
- ? `🧟 ${zombies} zombie${zombies > 1 ? "s" : ""} of ${total} process${total > 1 ? "es" : ""} | ~${zombies * 100}MB RAM | Fix: oc-kill-zombies`
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} зомби-процессов opencode | Освобождено ~${n * 100}MB RAM`,
33
- found: (n) => `🧟 ${n} зомби-процессов opencode | ~${n * 100}MB RAM | Fix: oc-kill-zombies`,
34
- status: (zombies, total) => zombies > 0
35
- ? `🧟 ${zombies} зомби из ${total} процессов | ~${zombies * 100}MB RAM | Fix: oc-kill-zombies`
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} 个僵尸 opencode 进程 | 释放 ~${n * 100}MB 内存`,
41
- found: (n) => `🧟 发现 ${n} 个僵尸 opencode 进程 | ~${n * 100}MB 内存 | 修复: oc-kill-zombies`,
42
- status: (zombies, total) => zombies > 0
43
- ? `🧟 ${total} 个进程中有 ${zombies} 个僵尸 | ~${zombies * 100}MB 内存 | 修复: oc-kill-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: "检查僵尸 opencode 进程"
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
- // Count only processes WITHOUT terminal (real zombies)
52
- const getZombieCount = async () => {
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
- const { stdout } = await execAsync(`ps aux | grep "[o]pencode" | grep -v "opencode/" | awk '$7 == "${ttyPattern}" {count++} END {print count+0}'`)
57
- return parseInt(stdout.trim()) || 0
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
- // Auto-kill threshold (1 = kill immediately)
85
- const AUTO_KILL_THRESHOLD = 1
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 getZombieCount()
144
+ const { count: zombies, mb } = await getZombieStats()
123
145
 
124
- // Auto-kill if zombies >= threshold
125
- if (zombies >= AUTO_KILL_THRESHOLD) {
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 (but below threshold)
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
- if (input.command !== "zombies") return
145
-
146
- const zombies = await getZombieCount()
147
- const total = await getTotalCount()
148
-
149
- await sendNotification(client, input.sessionID, t.status(zombies, total))
150
- throw new Error("__ZOMBIES_HANDLED__")
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.1",
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/jenshen86/opencode-zombie-monitor"
12
+ "url": "https://github.com/Matroskin86/opencode-zombie-monitor"
13
13
  },
14
14
  "peerDependencies": {
15
15
  "@opencode-ai/plugin": ">=1.0.0"
@@ -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)