stella-coder 5.1.2 → 5.2.1

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.
@@ -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
@@ -0,0 +1,229 @@
1
+ import http from "http"
2
+ import https from "https"
3
+ import { JSDOM } from "jsdom"
4
+
5
+ function fetchUrl(url, options = {}) {
6
+ return new Promise((resolve, reject) => {
7
+ const mod = url.startsWith("https") ? https : http
8
+ const headers = {
9
+ "User-Agent": "StellaBot/5.2 (compatible; like Googlebot)",
10
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
11
+ "Accept-Language": "en-US,en;q=0.5",
12
+ ...options.headers,
13
+ }
14
+
15
+ const req = mod.get(url, { headers, timeout: 15000 }, (res) => {
16
+ // Follow redirects
17
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
18
+ const redirectUrl = new URL(res.headers.location, url).href
19
+ return fetchUrl(redirectUrl, options).then(resolve).catch(reject)
20
+ }
21
+
22
+ let data = ""
23
+ res.on("data", (chunk) => data += chunk)
24
+ res.on("end", () => resolve({
25
+ status: res.statusCode,
26
+ headers: res.headers,
27
+ html: data,
28
+ url,
29
+ }))
30
+ })
31
+ req.on("error", reject)
32
+ req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")) })
33
+ })
34
+ }
35
+
36
+ export class WebParser {
37
+ constructor() {
38
+ this.cache = new Map()
39
+ }
40
+
41
+ async fetchPage(url) {
42
+ if (this.cache.has(url)) {
43
+ return this.cache.get(url)
44
+ }
45
+
46
+ const result = await fetchUrl(url)
47
+ const dom = new JSDOM(result.html)
48
+ const doc = dom.window.document
49
+
50
+ const parsed = {
51
+ url,
52
+ status: result.status,
53
+ title: doc.querySelector("title")?.textContent?.trim() || "",
54
+ meta: {
55
+ description: doc.querySelector('meta[name="description"]')?.content || "",
56
+ keywords: doc.querySelector('meta[name="keywords"]')?.content || "",
57
+ ogTitle: doc.querySelector('meta[property="og:title"]')?.content || "",
58
+ ogDescription: doc.querySelector('meta[property="og:description"]')?.content || "",
59
+ ogImage: doc.querySelector('meta[property="og:image"]')?.content || "",
60
+ canonical: doc.querySelector('link[rel="canonical"]')?.href || "",
61
+ },
62
+ headings: Array.from(doc.querySelectorAll("h1,h2,h3,h4")).map(h => ({
63
+ level: parseInt(h.tagName[1]),
64
+ text: h.textContent.trim(),
65
+ })),
66
+ links: Array.from(doc.querySelectorAll("a[href]")).map(a => ({
67
+ text: a.textContent.trim().slice(0, 100),
68
+ href: a.href,
69
+ })).filter(l => l.text).slice(0, 100),
70
+ images: Array.from(doc.querySelectorAll("img[src]")).map(img => ({
71
+ src: img.src,
72
+ alt: img.alt || "",
73
+ })).slice(0, 50),
74
+ text: doc.body?.innerText?.trim()?.slice(0, 50000) || "",
75
+ html: result.html,
76
+ linksCount: doc.querySelectorAll("a[href]").length,
77
+ imagesCount: doc.querySelectorAll("img[src]").length,
78
+ formsCount: doc.querySelectorAll("form").length,
79
+ scriptsCount: doc.querySelectorAll("script").length,
80
+ }
81
+
82
+ this.cache.set(url, parsed)
83
+ return parsed
84
+ }
85
+
86
+ async extractText(url) {
87
+ const page = await this.fetchPage(url)
88
+ return { success: true, title: page.title, text: page.text }
89
+ }
90
+
91
+ async extractLinks(url) {
92
+ const page = await this.fetchPage(url)
93
+ return { success: true, links: page.links }
94
+ }
95
+
96
+ async extractImages(url) {
97
+ const page = await this.fetchPage(url)
98
+ return { success: true, images: page.images }
99
+ }
100
+
101
+ async extractForms(url) {
102
+ const page = await this.fetchPage(url)
103
+ const dom = new JSDOM(page.html)
104
+ const doc = dom.window.document
105
+
106
+ const forms = Array.from(doc.querySelectorAll("form")).map(f => ({
107
+ action: f.action,
108
+ method: f.method?.toUpperCase() || "GET",
109
+ fields: Array.from(f.querySelectorAll("input,textarea,select,button")).map(el => ({
110
+ tag: el.tagName.toLowerCase(),
111
+ type: el.type || "",
112
+ name: el.name || "",
113
+ placeholder: el.placeholder || "",
114
+ value: el.value || "",
115
+ required: el.required,
116
+ options: el.tagName === "SELECT"
117
+ ? Array.from(el.querySelectorAll("option")).map(o => ({ value: o.value, text: o.textContent.trim() }))
118
+ : undefined,
119
+ })),
120
+ }))
121
+
122
+ return { success: true, forms }
123
+ }
124
+
125
+ async search(query, engine = "duckduckgo") {
126
+ if (engine === "duckduckgo") {
127
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`
128
+ const page = await this.fetchPage(url)
129
+ const dom = new JSDOM(page.html)
130
+ const doc = dom.window.document
131
+
132
+ const results = Array.from(doc.querySelectorAll(".result")).map(r => ({
133
+ title: r.querySelector(".result__title")?.textContent?.trim() || "",
134
+ url: r.querySelector(".result__url")?.textContent?.trim() || "",
135
+ snippet: r.querySelector(".result__snippet")?.textContent?.trim() || "",
136
+ })).slice(0, 10)
137
+
138
+ return { success: true, results }
139
+ }
140
+
141
+ if (engine === "google") {
142
+ const url = `https://www.google.com/search?q=${encodeURIComponent(query)}`
143
+ const page = await this.fetchPage(url)
144
+ const dom = new JSDOM(page.html)
145
+ const doc = dom.window.document
146
+
147
+ const results = Array.from(doc.querySelectorAll("div.g,div[data-sokoban-container]")).map(r => ({
148
+ title: r.querySelector("h3")?.textContent?.trim() || "",
149
+ url: r.querySelector("a")?.href || "",
150
+ snippet: r.querySelector(".VwiC3b,.st")?.textContent?.trim() || "",
151
+ })).filter(r => r.title).slice(0, 10)
152
+
153
+ return { success: true, results }
154
+ }
155
+
156
+ return { success: false, error: `Unknown engine: ${engine}` }
157
+ }
158
+
159
+ async getSEO(url) {
160
+ const page = await this.fetchPage(url)
161
+ const dom = new JSDOM(page.html)
162
+ const doc = dom.window.document
163
+
164
+ const seo = {
165
+ title: page.title,
166
+ titleLength: page.title.length,
167
+ metaDescription: page.meta.description,
168
+ metaDescLength: page.meta.description.length,
169
+ h1Count: doc.querySelectorAll("h1").length,
170
+ hasCanonical: !!page.meta.canonical,
171
+ hasOgTitle: !!page.meta.ogTitle,
172
+ hasOgDescription: !!page.meta.ogDescription,
173
+ hasOgImage: !!page.meta.ogImage,
174
+ imageAlts: Array.from(doc.querySelectorAll("img")).filter(img => !img.alt).length === 0,
175
+ linksInternal: page.links.filter(l => new URL(l.href, url).hostname === new URL(url).hostname).length,
176
+ linksExternal: page.links.filter(l => new URL(l.href, url).hostname !== new URL(url).hostname).length,
177
+ score: 0,
178
+ issues: [],
179
+ }
180
+
181
+ // Score
182
+ if (seo.titleLength >= 30 && seo.titleLength <= 60) seo.score += 20
183
+ else seo.issues.push(`Title length ${seo.titleLength} (optimal: 30-60)`)
184
+
185
+ if (seo.metaDescLength >= 120 && seo.metaDescLength <= 160) seo.score += 20
186
+ else seo.issues.push(`Meta description length ${seo.metaDescLength} (optimal: 120-160)`)
187
+
188
+ if (seo.h1Count === 1) seo.score += 15
189
+ else seo.issues.push(`H1 count: ${seo.h1Count} (should be 1)`)
190
+
191
+ if (seo.hasCanonical) seo.score += 10
192
+ else seo.issues.push("Missing canonical tag")
193
+
194
+ if (seo.hasOgTitle) seo.score += 10
195
+ else seo.issues.push("Missing og:title")
196
+
197
+ if (seo.hasOgDescription) seo.score += 10
198
+ else seo.issues.push("Missing og:description")
199
+
200
+ if (seo.hasOgImage) seo.score += 5
201
+ else seo.issues.push("Missing og:image")
202
+
203
+ if (seo.imageAlts) seo.score += 10
204
+ else seo.issues.push("Images without alt text")
205
+
206
+ return { success: true, seo }
207
+ }
208
+
209
+ async batchFetch(urls) {
210
+ const results = await Promise.allSettled(
211
+ urls.map(url => this.fetchPage(url).then(p => ({
212
+ url,
213
+ title: p.title,
214
+ status: p.status,
215
+ textLength: p.text.length,
216
+ })))
217
+ )
218
+
219
+ return {
220
+ success: true,
221
+ results: results.map((r, i) => ({
222
+ url: urls[i],
223
+ ...(r.status === "fulfilled" ? r.value : { error: r.reason?.message }),
224
+ })),
225
+ }
226
+ }
227
+ }
228
+
229
+ export default WebParser