stella-coder 5.1.1 → 5.2.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/capture.ps1 ADDED
@@ -0,0 +1,14 @@
1
+ Add-Type -AssemblyName System.Windows.Forms
2
+ Add-Type -AssemblyName System.Drawing
3
+ try {
4
+ $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
5
+ $bitmap = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
6
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
7
+ $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
8
+ $bitmap.Save("C:\Users\user\Downloads\stella-coder-3-9\test_screen.png")
9
+ $graphics.Dispose()
10
+ $bitmap.Dispose()
11
+ Write-Output "OK"
12
+ } catch {
13
+ Write-Output "FAIL:$($_.Exception.Message)"
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stella-coder",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "private": false,
5
5
  "description": "Stella Coder 5.0 — AI coding agent with Telegram bot, huge context, TDD, Git ecosystem, presentations, computer control, smart home, Office automation, and antivirus",
6
6
  "main": "stella-cli/index.mjs",
@@ -22,6 +22,7 @@ import { runSubagent, listSubagents, parseAgentCommand, SUBAGENTS } from "./suba
22
22
  import { mcp, MCP_COMMANDS } from "./mcp.mjs"
23
23
  import { generatePresentation, createPresentationFromTopic, AVAILABLE_THEMES, exportToPDF } from "./presentations.mjs"
24
24
  import { AutonomousAgent } from "./autonomous-agent.mjs"
25
+ import { ScreenMonitor } from "./screen-monitor.mjs"
25
26
  import {
26
27
  buildRepoMap, buildProjectContext, compressContext,
27
28
  loadSpec, generateSpecTemplate,
@@ -41,7 +42,7 @@ import {
41
42
  generateAdminCode, listAuthorizedUsers,
42
43
  } from "./telegram-bot.mjs"
43
44
 
44
- const VERSION = "5.1.1"
45
+ const VERSION = "5.2.0"
45
46
  const CONFIG_DIR = path.join(os.homedir(), ".stella")
46
47
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json")
47
48
  const HISTORY_PATH = path.join(CONFIG_DIR, "history.json")
@@ -650,6 +651,13 @@ const COMMANDS = [
650
651
  ["/auto-dashboard", "создать dashboard с результатами"],
651
652
  ["/auto-history", "история выполненных задач"],
652
653
 
654
+ // Computer Vision Monitor
655
+ ["/monitor", "запустить мониторинг экрана (AI vision)"],
656
+ ["/monitor-stop", "остановить мониторинг"],
657
+ ["/monitor-status", "статус мониторинга"],
658
+ ["/monitor-alerts", "показать обнаруженные проблемы"],
659
+ ["/monitor-screenshot", "сделать скриншот и проанализировать"],
660
+
653
661
  // Управление компьютером
654
662
  ["/open", "открыть приложение/файл/URL"],
655
663
  ["/app", "запустить приложение"],
@@ -775,6 +783,7 @@ async function handleCommand(line) {
775
783
  ["📱 Telegram", ["/tg", "/tg-stop", "/tg-notify", "/tg-sessions", "/tg-verify", "/tg-code", "/tg-users"]],
776
784
  ["🔀 Git Экосистема", ["/git-eco", "/git-pr", "/git-merge-auto"]],
777
785
  ["🤖 Autonomous Agent", ["/auto", "/auto-stop", "/auto-status", "/auto-dashboard", "/auto-history"]],
786
+ ["👁️ Computer Vision", ["/monitor", "/monitor-stop", "/monitor-status", "/monitor-alerts", "/monitor-screenshot"]],
778
787
  ["⚙️ Настройки", ["/help", "/model", "/clear", "/compact", "/cost", "/context", "/config", "/version", "/login", "/newkey", "/doctor", "/sessions", "/color", "/lang", "/shortcut"]],
779
788
  ]
780
789
  for (const [cat, cmds] of categories) {
@@ -3444,8 +3453,13 @@ async function handleCommand(line) {
3444
3453
  console.log()
3445
3454
 
3446
3455
  const apiCall = async (prompt) => {
3447
- const response = await queryAI(prompt, apiKey, state.model)
3448
- return response
3456
+ const model = getModel(state.model)
3457
+ const { text } = await generateText({
3458
+ model,
3459
+ messages: [{ role: "user", content: prompt }],
3460
+ maxTokens: 16000,
3461
+ })
3462
+ return text
3449
3463
  }
3450
3464
 
3451
3465
  console.log(dim(" Запускаю автономный режим...\n"))
@@ -3594,6 +3608,242 @@ async function handleCommand(line) {
3594
3608
  return
3595
3609
  }
3596
3610
 
3611
+ // ═══════════════════════════════════════════════════
3612
+ // COMPUTER VISION MONITOR
3613
+ // ═══════════════════════════════════════════════════
3614
+ case "/monitor": {
3615
+ console.log()
3616
+ const monitor = new ScreenMonitor()
3617
+
3618
+ if (monitor.state.running) {
3619
+ console.log(yellow(" Мониторинг уже запущен! Используй /monitor-stop для остановки.\n"))
3620
+ return
3621
+ }
3622
+
3623
+ const intervalSec = arg ? parseInt(arg) || 2 : 2
3624
+ const intervalMs = intervalSec * 1000
3625
+
3626
+ console.log(box([
3627
+ violet("👁️ COMPUTER VISION MONITOR"),
3628
+ "",
3629
+ white("Интервал: ") + cyan(`${intervalSec} секунды`),
3630
+ white("Режим: ") + cyan("автоматический скриншот + AI анализ"),
3631
+ "",
3632
+ dim("Агент будет делать скриншот каждые") + white(`${intervalSec}с`),
3633
+ dim("и анализировать через AI на наличие ошибок, крашей, алертов."),
3634
+ dim("Все обнаруженные проблемы будут сохранены в лог."),
3635
+ ], { color: violet, padding: 2 }))
3636
+ console.log()
3637
+
3638
+ const confirmMonitor = await question(" " + green("Запустить мониторинг? (y/n) › "))
3639
+ if (confirmMonitor.trim().toLowerCase() !== "y" && confirmMonitor.trim().toLowerCase() !== "д") {
3640
+ console.log(dim(" Отменено\n"))
3641
+ return
3642
+ }
3643
+
3644
+ const visionFn = async (imageBase64, recentContext) => {
3645
+ const contextStr = recentContext?.length > 0
3646
+ ? `\nRecent history:\n${recentContext.map(r => `- ${r.time}: ${r.summary} (${r.alerts} alerts)`).join("\n")}`
3647
+ : ""
3648
+
3649
+ const prompt = `Analyze this screenshot. Detect:
3650
+ 1. Error messages, crash dialogs, exception windows
3651
+ 2. Alert popups, warning notifications
3652
+ 3. Application crashes or "not responding" dialogs
3653
+ 4. System errors, BSOD, kernel panics
3654
+ 5. Any other issues or anomalies
3655
+
3656
+ ${contextStr}
3657
+
3658
+ Return JSON:
3659
+ {
3660
+ "summary": "one-line description of what's on screen",
3661
+ "text": "detailed analysis of what you see",
3662
+ "issues": ["list of issues found"],
3663
+ "severity": "none|info|warning|critical",
3664
+ "recommendations": ["what to do about issues"]
3665
+ }
3666
+
3667
+ If everything looks normal, set severity to "none" and issues to [].`
3668
+
3669
+ const model = getModel(state.model)
3670
+ const { text } = await generateText({
3671
+ model,
3672
+ messages: [{
3673
+ role: "user",
3674
+ content: [
3675
+ { type: "text", text: prompt },
3676
+ { type: "image", image: `data:image/png;base64,${imageBase64}` }
3677
+ ]
3678
+ }],
3679
+ maxTokens: 2000,
3680
+ })
3681
+
3682
+ try {
3683
+ let clean = text.trim()
3684
+ if (clean.startsWith("```")) clean = clean.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "")
3685
+ return JSON.parse(clean)
3686
+ } catch {
3687
+ return { summary: text.slice(0, 200), text, issues: [], severity: "none", recommendations: [] }
3688
+ }
3689
+ }
3690
+
3691
+ console.log()
3692
+ startSpinner("Запускаю мониторинг")
3693
+ const result = monitor.start(intervalMs, visionFn)
3694
+ stopSpinner()
3695
+
3696
+ if (result.success) {
3697
+ console.log(green(` ✓ Мониторинг запущен! Интервал: ${intervalSec}с`))
3698
+ console.log(dim(" Скриншоты сохраняются в ~/.stella/monitor/screenshots/"))
3699
+ console.log(dim(" Используй /monitor-stop для остановки"))
3700
+ console.log(dim(" Используй /monitor-status для проверки"))
3701
+ console.log(dim(" Используй /monitor-alerts для просмотра проблем\n"))
3702
+
3703
+ // Show first capture
3704
+ console.log(dim(" Делаю первый скриншот..."))
3705
+ const firstCapture = await monitor.captureAndAnalyze(visionFn)
3706
+ if (firstCapture.success) {
3707
+ console.log(green(` ✓ Скриншот: ${firstCapture.capture.sizeKB}KB`))
3708
+ if (firstCapture.capture.analysis?.summary) {
3709
+ console.log(dim(" Анализ: ") + white(firstCapture.capture.analysis.summary))
3710
+ }
3711
+ if (firstCapture.capture.alerts?.length > 0) {
3712
+ console.log(yellow(` ⚠️ Обнаружено проблем: ${firstCapture.capture.alerts.length}`))
3713
+ }
3714
+ }
3715
+ console.log()
3716
+ } else {
3717
+ console.log(red(` ✗ ${result.error}\n`))
3718
+ }
3719
+ return
3720
+ }
3721
+
3722
+ case "/monitor-stop": {
3723
+ console.log()
3724
+ const monitor = new ScreenMonitor()
3725
+ if (!monitor.state.running) {
3726
+ console.log(dim(" Мониторинг не запущен\n"))
3727
+ return
3728
+ }
3729
+ monitor.stop()
3730
+ console.log(green(" ✓ Мониторинг остановлен"))
3731
+ console.log(dim(` Всего скриншотов: ${monitor.state.totalCaptures}`))
3732
+ console.log(dim(` Всего алертов: ${monitor.state.totalAlerts}`))
3733
+ console.log()
3734
+ return
3735
+ }
3736
+
3737
+ case "/monitor-status": {
3738
+ console.log()
3739
+ const monitor = new ScreenMonitor()
3740
+ const status = monitor.getStatus()
3741
+
3742
+ if (!status.running && status.totalCaptures === 0) {
3743
+ console.log(dim(" Мониторинг не запущен. Используй /monitor для запуска.\n"))
3744
+ return
3745
+ }
3746
+
3747
+ const items = [
3748
+ dim("Статус: ") + (status.running ? green("● RUNNING") : red("○ STOPPED")),
3749
+ dim("Интервал: ") + cyan(`every ${status.interval / 1000}s`),
3750
+ dim("Начато: ") + white(status.startedAt ? new Date(status.startedAt).toLocaleString() : "—"),
3751
+ dim("Скриншотов: ") + cyan(String(status.totalCaptures)),
3752
+ dim("Алертов: ") + (status.totalAlerts > 0 ? red(String(status.totalAlerts)) : green("0")),
3753
+ dim("PID: ") + white(String(status.pid || "—")),
3754
+ ]
3755
+
3756
+ if (status.recentAlerts.length > 0) {
3757
+ items.push("", violet("Последние алерты:"))
3758
+ for (const a of status.recentAlerts.slice(-5)) {
3759
+ const icon = a.severity === "critical" ? red("🔴") : yellow("🟡")
3760
+ items.push(` ${icon} [${new Date(a.timestamp).toLocaleTimeString()}] ${a.type}`)
3761
+ if (a.description) items.push(dim(` ${a.description.slice(0, 100)}`))
3762
+ }
3763
+ }
3764
+
3765
+ if (status.recentCaptures.length > 0) {
3766
+ items.push("", dim("Последние скриншоты:"))
3767
+ for (const c of status.recentCaptures) {
3768
+ const icon = c.alertCount > 0 ? yellow("⚠️") : green("✅")
3769
+ items.push(` ${icon} [${new Date(c.time).toLocaleTimeString()}] ${c.summary}`)
3770
+ }
3771
+ }
3772
+
3773
+ console.log(box(items, { title: "👁️ Monitor Status", color: violet, padding: 2 }))
3774
+ console.log()
3775
+ return
3776
+ }
3777
+
3778
+ case "/monitor-alerts": {
3779
+ console.log()
3780
+ const monitor = new ScreenMonitor()
3781
+ const alerts = monitor.getAlerts()
3782
+
3783
+ if (alerts.length === 0) {
3784
+ console.log(dim(" Нет обнаруженных проблем. Всё чисто! ✅\n"))
3785
+ return
3786
+ }
3787
+
3788
+ console.log(box([
3789
+ violet(`Всего алертов: ${alerts.length}`),
3790
+ "",
3791
+ ...alerts.slice(-15).map(a => {
3792
+ const icon = a.severity === "critical" ? red("🔴") : yellow("🟡")
3793
+ return `${icon} [${new Date(a.timestamp).toLocaleTimeString()}] ${a.type}`
3794
+ }),
3795
+ ], { title: "⚠️ Monitor Alerts", color: yellow, padding: 2 }))
3796
+ console.log()
3797
+ return
3798
+ }
3799
+
3800
+ case "/monitor-screenshot": {
3801
+ console.log()
3802
+ startSpinner("Делаю скриншот")
3803
+ const monitor = new ScreenMonitor()
3804
+
3805
+ const visionFn = async (imageBase64) => {
3806
+ const prompt = `Analyze this screenshot in detail. What's on screen? Any errors, issues, or anomalies? Return JSON with "summary", "text", "issues", "severity".`
3807
+ const model = getModel(state.model)
3808
+ const { text } = await generateText({
3809
+ model,
3810
+ messages: [{
3811
+ role: "user",
3812
+ content: [
3813
+ { type: "text", text: prompt },
3814
+ { type: "image", image: `data:image/png;base64,${imageBase64}` }
3815
+ ]
3816
+ }],
3817
+ maxTokens: 2000,
3818
+ })
3819
+ try { return JSON.parse(text) } catch { return { summary: text.slice(0, 200), text } }
3820
+ }
3821
+
3822
+ const capture = await monitor.captureAndAnalyze(visionFn)
3823
+ stopSpinner()
3824
+
3825
+ if (capture.success) {
3826
+ console.log(box([
3827
+ green("✓ Скриншот сделан!"),
3828
+ "",
3829
+ dim("Файл: ") + cyan(capture.capture.path),
3830
+ dim("Размер: ") + white(`${capture.capture.sizeKB}KB`),
3831
+ "",
3832
+ violet("Анализ:"),
3833
+ white(capture.capture.analysis?.summary || "analyzing..."),
3834
+ capture.capture.analysis?.text ? dim(capture.capture.analysis.text.slice(0, 500)) : "",
3835
+ "",
3836
+ capture.capture.alerts?.length > 0
3837
+ ? yellow(`⚠️ Обнаружено проблем: ${capture.capture.alerts.length}`)
3838
+ : green("✅ Проблем не обнаружено"),
3839
+ ], { title: "📸 Screenshot Analysis", color: cyan, padding: 2 }))
3840
+ } else {
3841
+ console.log(red(` ✗ ${capture.error}\n`))
3842
+ }
3843
+ console.log()
3844
+ return
3845
+ }
3846
+
3597
3847
  default:
3598
3848
  console.log(dim(`\n Неизвестная команда: ${cmd}. Смотри /help\n`))
3599
3849
  }
@@ -0,0 +1,334 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, statSync } from "fs"
2
+ import { join } from "path"
3
+ import { homedir } from "os"
4
+ import { execSync } from "child_process"
5
+
6
+ const STELLA_DIR = join(homedir(), ".stella")
7
+ const MONITOR_DIR = join(STELLA_DIR, "monitor")
8
+ const HISTORY_FILE = join(MONITOR_DIR, "history.json")
9
+ const ALERTS_FILE = join(MONITOR_DIR, "alerts.json")
10
+ const SCREENSHOTS_DIR = join(MONITOR_DIR, "screenshots")
11
+ const MONITOR_STATE = join(MONITOR_DIR, "state.json")
12
+
13
+ function ensureDir() {
14
+ if (!existsSync(MONITOR_DIR)) mkdirSync(MONITOR_DIR, { recursive: true })
15
+ if (!existsSync(SCREENSHOTS_DIR)) mkdirSync(SCREENSHOTS_DIR, { recursive: true })
16
+ }
17
+
18
+ function loadJSON(file, fallback) {
19
+ try { return JSON.parse(readFileSync(file, "utf-8")) } catch { return fallback }
20
+ }
21
+
22
+ function saveJSON(file, data) {
23
+ ensureDir()
24
+ writeFileSync(file, JSON.stringify(data, null, 2))
25
+ }
26
+
27
+ function captureScreenSync() {
28
+ const ts = Date.now()
29
+ const outPath = join(SCREENSHOTS_DIR, `screen_${ts}.png`)
30
+ const tempPath = join(SCREENSHOTS_DIR, `_temp.png`)
31
+ const psScriptPath = join(SCREENSHOTS_DIR, `_capture.ps1`)
32
+
33
+ // Write PowerShell script to file (avoids escaping issues)
34
+ const psScript = `
35
+ Add-Type -AssemblyName System.Windows.Forms
36
+ Add-Type -AssemblyName System.Drawing
37
+ try {
38
+ $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
39
+ $bitmap = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
40
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
41
+ $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
42
+ $bitmap.Save("${tempPath.replace(/\\/g, "\\\\")}")
43
+ $graphics.Dispose()
44
+ $bitmap.Dispose()
45
+ Write-Output "OK"
46
+ } catch {
47
+ Write-Output "FAIL:$($_.Exception.Message)"
48
+ }
49
+ `.trim()
50
+
51
+ try {
52
+ writeFileSync(psScriptPath, psScript, "utf-8")
53
+ const result = execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${psScriptPath}"`, {
54
+ encoding: "utf-8",
55
+ timeout: 10000,
56
+ stdio: ["pipe", "pipe", "pipe"],
57
+ }).trim()
58
+
59
+ if (result.startsWith("OK") && existsSync(tempPath)) {
60
+ const data = readFileSync(tempPath)
61
+ writeFileSync(outPath, data)
62
+ try { unlinkSync(tempPath) } catch {}
63
+ try { unlinkSync(psScriptPath) } catch {}
64
+ return outPath
65
+ }
66
+ try { unlinkSync(psScriptPath) } catch {}
67
+ return null
68
+ } catch {
69
+ return null
70
+ }
71
+ }
72
+
73
+ function compressImage(imagePath, maxWidth = 800) {
74
+ const compressedPath = imagePath.replace(".png", "_small.png")
75
+ const psScriptPath = join(SCREENSHOTS_DIR, `_compress.ps1`)
76
+
77
+ const psScript = `
78
+ Add-Type -AssemblyName System.Drawing
79
+ try {
80
+ $img = [System.Drawing.Image]::FromFile("${imagePath.replace(/\\/g, "\\\\")}")
81
+ $ratio = ${maxWidth} / $img.Width
82
+ if ($ratio -ge 1) { $ratio = 1 }
83
+ $newW = [int]($img.Width * $ratio)
84
+ $newH = [int]($img.Height * $ratio)
85
+ $bmp = New-Object System.Drawing.Bitmap($newW, $newH)
86
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
87
+ $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
88
+ $g.DrawImage($img, 0, 0, $newW, $newH)
89
+ $bmp.Save("${compressedPath.replace(/\\/g, "\\\\")}")
90
+ $g.Dispose()
91
+ $bmp.Dispose()
92
+ $img.Dispose()
93
+ Write-Output "OK"
94
+ } catch {
95
+ Write-Output "FAIL"
96
+ }
97
+ `.trim()
98
+
99
+ try {
100
+ writeFileSync(psScriptPath, psScript, "utf-8")
101
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${psScriptPath}"`, {
102
+ encoding: "utf-8",
103
+ timeout: 10000,
104
+ stdio: ["pipe", "pipe", "pipe"],
105
+ })
106
+ try { unlinkSync(psScriptPath) } catch {}
107
+ if (existsSync(compressedPath)) {
108
+ return compressedPath
109
+ }
110
+ } catch {}
111
+ return imagePath
112
+ }
113
+
114
+ export class ScreenMonitor {
115
+ constructor() {
116
+ ensureDir()
117
+ this.state = loadJSON(MONITOR_STATE, {
118
+ running: false,
119
+ interval: 2000,
120
+ startedAt: null,
121
+ totalCaptures: 0,
122
+ totalAlerts: 0,
123
+ pid: null,
124
+ })
125
+ this.history = loadJSON(HISTORY_FILE, [])
126
+ this.alerts = loadJSON(ALERTS_FILE, [])
127
+ this.intervalId = null
128
+ }
129
+
130
+ save() {
131
+ saveJSON(MONITOR_STATE, this.state)
132
+ saveJSON(HISTORY_FILE, this.history.slice(-500))
133
+ saveJSON(ALERTS_FILE, this.alerts.slice(-200))
134
+ }
135
+
136
+ async captureAndAnalyze(visionFn) {
137
+ const imagePath = captureScreenSync()
138
+ if (!imagePath) {
139
+ return { success: false, error: "Failed to capture screen" }
140
+ }
141
+
142
+ const stats = statSync(imagePath)
143
+ const sizeKB = Math.round(stats.size / 1024)
144
+
145
+ // Compress for AI analysis
146
+ const compressedPath = compressImage(imagePath, 600)
147
+ let imageBase64
148
+ try {
149
+ imageBase64 = readFileSync(compressedPath).toString("base64")
150
+ } catch {
151
+ imageBase64 = readFileSync(imagePath).toString("base64")
152
+ }
153
+
154
+ const ts = new Date().toISOString()
155
+ const capture = {
156
+ id: Date.now().toString(36),
157
+ timestamp: ts,
158
+ path: imagePath,
159
+ sizeKB,
160
+ analysis: null,
161
+ alerts: [],
162
+ }
163
+
164
+ // Analyze with vision AI
165
+ if (visionFn) {
166
+ try {
167
+ const analysis = await visionFn(imageBase64, this.getRecentContext())
168
+ capture.analysis = analysis
169
+
170
+ // Detect alerts
171
+ const alertKeywords = [
172
+ "error", "crash", "exception", "fatal", "alert", "warning", "failed",
173
+ "ошибка", "краш", "исключение", "фатальная", "предупреждение", "сбой",
174
+ "not responding", "has stopped", "dead", "kill", "terminate",
175
+ "не отвечает", "остановлен", "завершить",
176
+ ]
177
+
178
+ const textLower = (analysis.text || "").toLowerCase()
179
+ const detected = alertKeywords.filter(k => textLower.includes(k))
180
+
181
+ if (detected.length > 0) {
182
+ const alert = {
183
+ id: capture.id,
184
+ timestamp: ts,
185
+ type: detected.join(", "),
186
+ severity: detected.some(k => ["fatal", "crash", "exception", "фатальная", "краш", "исключение"].includes(k)) ? "critical" : "warning",
187
+ description: analysis.text?.slice(0, 500),
188
+ screenshot: imagePath,
189
+ }
190
+ capture.alerts.push(alert)
191
+ this.alerts.push(alert)
192
+ this.state.totalAlerts++
193
+ }
194
+ } catch (err) {
195
+ capture.analysis = { error: err.message }
196
+ }
197
+ }
198
+
199
+ this.history.push(capture)
200
+ this.state.totalCaptures++
201
+ this.save()
202
+
203
+ // Clean old screenshots (keep last 50)
204
+ this.cleanOldScreenshots(50)
205
+
206
+ return { success: true, capture }
207
+ }
208
+
209
+ getRecentContext() {
210
+ const recent = this.history.slice(-5)
211
+ return recent.map(h => ({
212
+ time: h.timestamp,
213
+ analysis: h.analysis?.summary || h.analysis?.text?.slice(0, 200) || "no analysis",
214
+ alerts: h.alerts?.length || 0,
215
+ }))
216
+ }
217
+
218
+ cleanOldScreenshots(keep = 50) {
219
+ try {
220
+ const files = readdirSync(SCREENSHOTS_DIR)
221
+ .filter(f => f.startsWith("screen_") && f.endsWith(".png"))
222
+ .sort()
223
+
224
+ if (files.length > keep) {
225
+ const toDelete = files.slice(0, files.length - keep)
226
+ for (const f of toDelete) {
227
+ try { unlinkSync(join(SCREENSHOTS_DIR, f)) } catch {}
228
+ }
229
+ }
230
+ } catch {}
231
+ }
232
+
233
+ start(intervalMs, visionFn) {
234
+ if (this.state.running) {
235
+ return { success: false, error: "Monitor already running" }
236
+ }
237
+
238
+ this.state.running = true
239
+ this.state.startedAt = new Date().toISOString()
240
+ this.state.interval = intervalMs || 2000
241
+ this.state.pid = process.pid
242
+ this.save()
243
+
244
+ this.intervalId = setInterval(async () => {
245
+ if (!this.state.running) {
246
+ this.stop()
247
+ return
248
+ }
249
+ await this.captureAndAnalyze(visionFn)
250
+ }, this.state.interval)
251
+
252
+ return { success: true, interval: this.state.interval }
253
+ }
254
+
255
+ stop() {
256
+ if (this.intervalId) {
257
+ clearInterval(this.intervalId)
258
+ this.intervalId = null
259
+ }
260
+ this.state.running = false
261
+ this.state.pid = null
262
+ this.save()
263
+ return { success: true }
264
+ }
265
+
266
+ getStatus() {
267
+ const recentAlerts = this.alerts.slice(-10)
268
+ const recentCaptures = this.history.slice(-5)
269
+
270
+ return {
271
+ running: this.state.running,
272
+ startedAt: this.state.startedAt,
273
+ interval: this.state.interval,
274
+ totalCaptures: this.state.totalCaptures,
275
+ totalAlerts: this.state.totalAlerts,
276
+ pid: this.state.pid,
277
+ recentAlerts,
278
+ recentCaptures: recentCaptures.map(c => ({
279
+ time: c.timestamp,
280
+ size: c.sizeKB + "KB",
281
+ summary: c.analysis?.summary || c.analysis?.text?.slice(0, 100) || "analyzing...",
282
+ alertCount: c.alerts?.length || 0,
283
+ })),
284
+ }
285
+ }
286
+
287
+ getAlerts(since) {
288
+ if (since) {
289
+ return this.alerts.filter(a => new Date(a.timestamp) > new Date(since))
290
+ }
291
+ return this.alerts
292
+ }
293
+
294
+ getScreenshotPath() {
295
+ const files = readdirSync(SCREENSHOTS_DIR)
296
+ .filter(f => f.startsWith("screen_") && f.endsWith(".png"))
297
+ .sort()
298
+ return files.length > 0 ? join(SCREENSHOTS_DIR, files[files.length - 1]) : null
299
+ }
300
+
301
+ generateReport() {
302
+ const status = this.getStatus()
303
+ const alertsByType = {}
304
+ for (const a of this.alerts) {
305
+ alertsByType[a.type] = (alertsByType[a.type] || 0) + 1
306
+ }
307
+
308
+ const report = `
309
+ ═══════════════════════════════════════════
310
+ 👁️ SCREEN MONITOR REPORT
311
+ ═══════════════════════════════════════════
312
+
313
+ Status: ${status.running ? "🟢 RUNNING" : "🔴 STOPPED"}
314
+ Started: ${status.startedAt ? new Date(status.startedAt).toLocaleString() : "Never"}
315
+ Interval: every ${status.interval / 1000}s
316
+ Total captures: ${status.totalCaptures}
317
+ Total alerts: ${status.totalAlerts}
318
+
319
+ ${Object.keys(alertsByType).length > 0 ? `
320
+ Alert breakdown:
321
+ ${Object.entries(alertsByType).map(([type, count]) => ` ⚠️ ${type}: ${count}`).join("\n")}
322
+ ` : "No alerts detected."}
323
+
324
+ Recent captures:
325
+ ${status.recentCaptures.map(c => ` [${new Date(c.time).toLocaleTimeString()}] ${c.summary} ${c.alertCount > 0 ? `⚠️ ${c.alertCount} alerts` : "✅"}`).join("\n")}
326
+
327
+ ═══════════════════════════════════════════
328
+ `.trim()
329
+
330
+ return report
331
+ }
332
+ }
333
+
334
+ export default ScreenMonitor
Binary file