stella-coder 5.2.0 → 5.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stella-coder",
3
- "version": "5.2.0",
3
+ "version": "5.3.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",
@@ -36,6 +36,7 @@
36
36
  "class-variance-authority": "^0.7.1",
37
37
  "clsx": "^2.1.1",
38
38
  "gsap": "^3.15.0",
39
+ "jsdom": "^29.1.1",
39
40
  "lucide-react": "^1.16.0",
40
41
  "next": "16.2.6",
41
42
  "react": "^19",
@@ -0,0 +1,200 @@
1
+ import { execSync } from "node:child_process"
2
+ import fs from "node:fs"
3
+ import path from "node:path"
4
+ import os from "node:os"
5
+
6
+ const ADB_DIR = path.join(os.homedir(), ".stella", "adb")
7
+
8
+ function ensureDir() { if (!fs.existsSync(ADB_DIR)) fs.mkdirSync(ADB_DIR, { recursive: true }) }
9
+
10
+ function adb(args) {
11
+ try {
12
+ return execSync(`adb ${args}`, { encoding: "utf8", timeout: 10000 }).trim()
13
+ } catch (e) {
14
+ return { error: e.stderr?.trim() || e.message }
15
+ }
16
+ }
17
+
18
+ export class ADB {
19
+ constructor() { ensureDir() }
20
+
21
+ isAvailable() {
22
+ try {
23
+ execSync("adb --version", { encoding: "utf8", stdio: "ignore" })
24
+ return true
25
+ } catch { return false }
26
+ }
27
+
28
+ getDevices() {
29
+ const out = adb("devices")
30
+ if (out.error) return { success: false, error: out.error }
31
+ const lines = out.split("\n").filter(l => l.trim() && !l.includes("List of devices"))
32
+ const devices = lines.map(l => { const [id, status] = l.trim().split(/\s+/); return { id, status } })
33
+ return { success: true, devices }
34
+ }
35
+
36
+ getState(serial) {
37
+ const s = serial ? `-s ${serial}` : ""
38
+ const out = adb(`${s} get-state`)
39
+ return out.error ? { success: false, error: out.error } : { success: true, state: out }
40
+ }
41
+
42
+ shell(serial, cmd) {
43
+ const s = serial ? `-s ${serial}` : ""
44
+ const out = adb(`${s} shell ${cmd}`)
45
+ return out.error ? { success: false, error: out.error } : { success: true, output: out }
46
+ }
47
+
48
+ screenshot(serial) {
49
+ ensureDir()
50
+ const s = serial ? `-s ${serial}` : ""
51
+ const filename = `screenshot_${Date.now()}.png`
52
+ const localPath = path.join(ADB_DIR, filename)
53
+ adb(`${s} exec-out screencap -p > "${localPath}"`)
54
+ if (fs.existsSync(localPath) && fs.statSync(localPath).size > 0) {
55
+ return { success: true, path: localPath }
56
+ }
57
+ try {
58
+ adb(`${s} shell screencap -p /sdcard/screen.png`)
59
+ adb(`${s} pull /sdcard/screen.png "${localPath}"`)
60
+ adb(`${s} shell rm /sdcard/screen.png`)
61
+ if (fs.existsSync(localPath) && fs.statSync(localPath).size > 0) return { success: true, path: localPath }
62
+ } catch {}
63
+ return { success: false, error: "Screenshot failed" }
64
+ }
65
+
66
+ tap(serial, x, y) {
67
+ const s = serial ? `-s ${serial}` : ""
68
+ const out = adb(`${s} shell input tap ${x} ${y}`)
69
+ return out.error ? { success: false, error: out.error } : { success: true }
70
+ }
71
+
72
+ swipe(serial, x1, y1, x2, y2, duration = 300) {
73
+ const s = serial ? `-s ${serial}` : ""
74
+ const out = adb(`${s} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`)
75
+ return out.error ? { success: false, error: out.error } : { success: true }
76
+ }
77
+
78
+ text(serial, text) {
79
+ const s = serial ? `-s ${serial}` : ""
80
+ const escaped = text.replace(/ /g, "%s").replace(/"/g, '\\"')
81
+ const out = adb(`${s} shell input text "${escaped}"`)
82
+ return out.error ? { success: false, error: out.error } : { success: true }
83
+ }
84
+
85
+ key(serial, keycode) {
86
+ const s = serial ? `-s ${serial}` : ""
87
+ const out = adb(`${s} shell input keyevent ${keycode}`)
88
+ return out.error ? { success: false, error: out.error } : { success: true }
89
+ }
90
+
91
+ pressHome(serial) { return this.key(serial, 3) }
92
+ pressBack(serial) { return this.key(serial, 4) }
93
+ pressMenu(serial) { return this.key(serial, 82) }
94
+ pressPower(serial) { return this.key(serial, 26) }
95
+ pressVolumeUp(serial) { return this.key(serial, 24) }
96
+ pressVolumeDown(serial) { return this.key(serial, 25) }
97
+
98
+ installApp(serial, apkPath) {
99
+ if (!fs.existsSync(apkPath)) return { success: false, error: "APK not found" }
100
+ const s = serial ? `-s ${serial}` : ""
101
+ const out = adb(`${s} install "${apkPath}"`)
102
+ if (out.error) return { success: false, error: out.error }
103
+ return { success: true, output: out }
104
+ }
105
+
106
+ uninstallApp(serial, packageName) {
107
+ const s = serial ? `-s ${serial}` : ""
108
+ const out = adb(`${s} uninstall ${packageName}`)
109
+ if (out.error) return { success: false, error: out.error }
110
+ return { success: true, output: out }
111
+ }
112
+
113
+ listPackages(serial, filter = "") {
114
+ const s = serial ? `-s ${serial}` : ""
115
+ const out = adb(`${s} shell pm list packages ${filter}`)
116
+ if (out.error) return { success: false, error: out.error }
117
+ const packages = out.split("\n").filter(l => l.trim()).map(l => l.replace("package:", "").trim())
118
+ return { success: true, packages }
119
+ }
120
+
121
+ getBattery(serial) {
122
+ const s = serial ? `-s ${serial}` : ""
123
+ const out = adb(`${s} shell dumpsys battery`)
124
+ if (out.error) return { success: false, error: out.error }
125
+ const level = out.match(/level:\s*(\d+)/)?.[1]
126
+ const temp = out.match(/temperature:\s*(\d+)/)?.[1]
127
+ const status = out.match(/status:\s*(\d+)/)?.[1]
128
+ const statusMap = { 1: "unknown", 2: "charging", 3: "discharging", 4: "not charging", 5: "full" }
129
+ return { success: true, level: parseInt(level), temperature: temp ? parseInt(temp) / 10 : null, status: statusMap[status] || "unknown" }
130
+ }
131
+
132
+ getInfo(serial) {
133
+ const s = serial ? `-s ${serial}` : ""
134
+ const model = adb(`${s} shell getprop ro.product.model`)
135
+ const brand = adb(`${s} shell getprop ro.product.brand`)
136
+ const android = adb(`${s} shell getprop ro.build.version.release`)
137
+ const sdk = adb(`${s} shell getprop ro.build.version.sdk`)
138
+ const resolution = adb(`${s} shell wm size`)
139
+ const density = adb(`${s} shell wm density`)
140
+ return {
141
+ success: true,
142
+ model: model.error ? "unknown" : model,
143
+ brand: brand.error ? "unknown" : brand,
144
+ android: android.error ? "unknown" : android,
145
+ sdk: sdk.error ? "unknown" : sdk,
146
+ resolution: resolution.error ? "unknown" : resolution.replace("Physical size: ", ""),
147
+ density: density.error ? "unknown" : density.replace("Physical density: ", ""),
148
+ }
149
+ }
150
+
151
+ startApp(serial, packageName) {
152
+ return this.shell(serial, `monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`)
153
+ }
154
+
155
+ killApp(serial, packageName) {
156
+ return this.shell(serial, `am force-stop ${packageName}`)
157
+ }
158
+
159
+ pullFile(serial, remotePath, localPath) {
160
+ const s = serial ? `-s ${serial}` : ""
161
+ ensureDir()
162
+ const dest = localPath || path.join(ADB_DIR, path.basename(remotePath))
163
+ const out = adb(`${s} pull "${remotePath}" "${dest}"`)
164
+ if (out.error) return { success: false, error: out.error }
165
+ return { success: true, path: dest }
166
+ }
167
+
168
+ pushFile(serial, localPath, remotePath) {
169
+ const s = serial ? `-s ${serial}` : ""
170
+ const out = adb(`${s} push "${localPath}" "${remotePath}"`)
171
+ if (out.error) return { success: false, error: out.error }
172
+ return { success: true }
173
+ }
174
+
175
+ reboot(serial) {
176
+ const s = serial ? `-s ${serial}` : ""
177
+ const out = adb(`${s} reboot`)
178
+ if (out.error) return { success: false, error: out.error }
179
+ return { success: true }
180
+ }
181
+
182
+ logcat(serial, filter = "", lines = 50) {
183
+ const s = serial ? `-s ${serial}` : ""
184
+ const out = adb(`${s} logcat -d -t ${lines} ${filter}`)
185
+ if (out.error) return { success: false, error: out.error }
186
+ return { success: true, log: out }
187
+ }
188
+
189
+ wifiConnect(ip, port = 5555) {
190
+ const out = adb(`connect ${ip}:${port}`)
191
+ if (out.error) return { success: false, error: out.error }
192
+ return { success: true, output: out }
193
+ }
194
+
195
+ wifiDisconnect(ip) {
196
+ const out = adb(`disconnect ${ip}`)
197
+ if (out.error) return { success: false, error: out.error }
198
+ return { success: true, output: out }
199
+ }
200
+ }
@@ -0,0 +1,274 @@
1
+ import { execSync, spawn } from "child_process"
2
+ import { writeFileSync, existsSync, mkdirSync } from "fs"
3
+ import { join } from "path"
4
+ import { homedir } from "os"
5
+ import http from "http"
6
+
7
+ const STELLA_DIR = join(homedir(), ".stella")
8
+ const BROWSER_DIR = join(STELLA_DIR, "browser")
9
+ const PROFILES_DIR = join(BROWSER_DIR, "profiles")
10
+
11
+ function ensureDir() {
12
+ if (!existsSync(BROWSER_DIR)) mkdirSync(BROWSER_DIR, { recursive: true })
13
+ if (!existsSync(PROFILES_DIR)) mkdirSync(PROFILES_DIR, { recursive: true })
14
+ }
15
+
16
+ function httpGet(url) {
17
+ return new Promise((resolve, reject) => {
18
+ http.get(url, (res) => {
19
+ let data = ""
20
+ res.on("data", (chunk) => data += chunk)
21
+ res.on("end", () => {
22
+ try { resolve(JSON.parse(data)) } catch { resolve(data) }
23
+ })
24
+ }).on("error", reject)
25
+ })
26
+ }
27
+
28
+ export class ChromeBrowser {
29
+ constructor() {
30
+ ensureDir()
31
+ this.ws = null
32
+ this.pageId = null
33
+ this.debugPort = 9222
34
+ this.proc = null
35
+ this.msgId = 0
36
+ this.callbacks = new Map()
37
+ }
38
+
39
+ async launch(headless = false) {
40
+ const chromePaths = [
41
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
42
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
43
+ join(homedir(), "AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"),
44
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
45
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
46
+ ]
47
+
48
+ let chromePath = null
49
+ for (const p of chromePaths) {
50
+ if (existsSync(p)) { chromePath = p; break }
51
+ }
52
+
53
+ if (!chromePath) {
54
+ return { success: false, error: "Chrome/Edge not found" }
55
+ }
56
+
57
+ const args = [
58
+ `--remote-debugging-port=${this.debugPort}`,
59
+ "--no-first-run",
60
+ "--no-default-browser-check",
61
+ headless ? "--headless=new" : "",
62
+ `--user-data-dir=${join(PROFILES_DIR, "default")}`,
63
+ "about:blank",
64
+ ].filter(Boolean)
65
+
66
+ this.proc = spawn(chromePath, args, { detached: true, stdio: "ignore" })
67
+ this.proc.unref()
68
+
69
+ await new Promise(r => setTimeout(r, 2000))
70
+
71
+ try {
72
+ const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
73
+ if (tabs && tabs.length > 0) {
74
+ this.pageId = tabs[0].id
75
+ return { success: true, port: this.debugPort, pid: this.proc.pid }
76
+ }
77
+ } catch (e) {
78
+ return { success: false, error: `Chrome launched but DevTools not ready: ${e.message}` }
79
+ }
80
+
81
+ return { success: true, port: this.debugPort }
82
+ }
83
+
84
+ async connect() {
85
+ try {
86
+ const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
87
+ if (!tabs || tabs.length === 0) {
88
+ return { success: false, error: "No tabs found. Launch Chrome first." }
89
+ }
90
+
91
+ const wsUrl = tabs[0].webSocketDebuggerUrl
92
+ if (!wsUrl) {
93
+ return { success: false, error: "WebSocket URL not available" }
94
+ }
95
+
96
+ return new Promise((resolve) => {
97
+ this.ws = new WebSocket(wsUrl)
98
+ this.ws.onopen = () => {
99
+ this.pageId = tabs[0].id
100
+ resolve({ success: true, title: tabs[0].title })
101
+ }
102
+ this.ws.onmessage = (event) => {
103
+ const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString())
104
+ if (msg.id && this.callbacks.has(msg.id)) {
105
+ this.callbacks.get(msg.id)(msg)
106
+ this.callbacks.delete(msg.id)
107
+ }
108
+ }
109
+ this.ws.onerror = (err) => {
110
+ resolve({ success: false, error: err.message || "WebSocket error" })
111
+ }
112
+ })
113
+ } catch (e) {
114
+ return { success: false, error: e.message }
115
+ }
116
+ }
117
+
118
+ async sendCommand(method, params = {}) {
119
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
120
+ const conn = await this.connect()
121
+ if (!conn.success) return { success: false, error: conn.error }
122
+ }
123
+
124
+ const id = ++this.msgId
125
+ return new Promise((resolve) => {
126
+ this.callbacks.set(id, (msg) => {
127
+ if (msg.error) {
128
+ resolve({ success: false, error: msg.error.message })
129
+ } else {
130
+ resolve({ success: true, result: msg.result })
131
+ }
132
+ })
133
+ this.ws.send(JSON.stringify({ id, method, params }))
134
+ setTimeout(() => {
135
+ if (this.callbacks.has(id)) {
136
+ this.callbacks.delete(id)
137
+ resolve({ success: false, error: "Command timeout" })
138
+ }
139
+ }, 30000)
140
+ })
141
+ }
142
+
143
+ async navigate(url) {
144
+ const result = await this.sendCommand("Page.navigate", { url })
145
+ if (result.success) await new Promise(r => setTimeout(r, 2000))
146
+ return result
147
+ }
148
+
149
+ async getPageContent() {
150
+ const result = await this.sendCommand("Runtime.evaluate", {
151
+ expression: "document.documentElement.outerHTML",
152
+ returnByValue: true,
153
+ })
154
+ return result.success ? { success: true, html: result.result?.value || "" } : result
155
+ }
156
+
157
+ async getText() {
158
+ const result = await this.sendCommand("Runtime.evaluate", {
159
+ expression: "document.body.innerText",
160
+ returnByValue: true,
161
+ })
162
+ return result.success ? { success: true, text: result.result?.value || "" } : result
163
+ }
164
+
165
+ async getTitle() {
166
+ const result = await this.sendCommand("Runtime.evaluate", {
167
+ expression: "document.title",
168
+ returnByValue: true,
169
+ })
170
+ return result.success ? { success: true, title: result.result?.value || "" } : result
171
+ }
172
+
173
+ async screenshot(path) {
174
+ const result = await this.sendCommand("Page.captureScreenshot", { format: "png" })
175
+ if (result.success && result.result?.data) {
176
+ const imgPath = path || join(BROWSER_DIR, `screenshot_${Date.now()}.png`)
177
+ writeFileSync(imgPath, Buffer.from(result.result.data, "base64"))
178
+ return { success: true, path: imgPath }
179
+ }
180
+ return result
181
+ }
182
+
183
+ async evaluate(expression) {
184
+ return await this.sendCommand("Runtime.evaluate", {
185
+ expression,
186
+ returnByValue: true,
187
+ awaitPromise: true,
188
+ })
189
+ }
190
+
191
+ async click(selector) {
192
+ const posResult = await this.sendCommand("Runtime.evaluate", {
193
+ expression: `(() => { const el = document.querySelector('${selector.replace(/'/g, "\\'")}'); if (!el) return null; const r = el.getBoundingClientRect(); return { x: r.x + r.width/2, y: r.y + r.height/2 }; })()`,
194
+ returnByValue: true,
195
+ })
196
+
197
+ if (!posResult.success || !posResult.result?.value) {
198
+ return { success: false, error: `Element not found: ${selector}` }
199
+ }
200
+
201
+ const { x, y } = posResult.result.value
202
+ await this.sendCommand("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 })
203
+ await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 })
204
+ return { success: true, x, y }
205
+ }
206
+
207
+ async type(selector, text) {
208
+ await this.sendCommand("Runtime.evaluate", {
209
+ expression: `document.querySelector('${selector.replace(/'/g, "\\'")}')?.focus()`,
210
+ })
211
+ for (const char of text) {
212
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", text: char, key: char })
213
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", key: char })
214
+ }
215
+ return { success: true }
216
+ }
217
+
218
+ async scroll(deltaX = 0, deltaY = 300) {
219
+ await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseWheel", x: 400, y: 400, deltaX, deltaY })
220
+ return { success: true }
221
+ }
222
+
223
+ async pressKey(key) {
224
+ const keys = {
225
+ enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
226
+ tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
227
+ escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
228
+ backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
229
+ space: { key: " ", code: "Space", windowsVirtualKeyCode: 32 },
230
+ arrowup: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
231
+ arrowdown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
232
+ }
233
+ const k = keys[key.toLowerCase()] || { key, code: `Key${key.toUpperCase()}` }
234
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", ...k })
235
+ await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", ...k })
236
+ return { success: true }
237
+ }
238
+
239
+ async fillForm(fields) {
240
+ const results = []
241
+ for (const { selector, value } of fields) {
242
+ results.push({ selector, ...(await this.type(selector, value)) })
243
+ }
244
+ return { success: true, results }
245
+ }
246
+
247
+ async getLinks() {
248
+ const result = await this.sendCommand("Runtime.evaluate", {
249
+ expression: `Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim().slice(0, 100), href: a.href })).filter(a => a.text).slice(0, 50)`,
250
+ returnByValue: true,
251
+ })
252
+ return result.success ? { success: true, links: result.result?.value || [] } : result
253
+ }
254
+
255
+ async getForms() {
256
+ const result = await this.sendCommand("Runtime.evaluate", {
257
+ expression: `Array.from(document.querySelectorAll('form')).map(f => ({ action: f.action, method: f.method, inputs: Array.from(f.querySelectorAll('input,textarea,select')).map(i => ({ name: i.name, type: i.type, placeholder: i.placeholder })) }))`,
258
+ returnByValue: true,
259
+ })
260
+ return result.success ? { success: true, forms: result.result?.value || [] } : result
261
+ }
262
+
263
+ close() {
264
+ if (this.ws) this.ws.close()
265
+ if (this.proc) this.proc.kill()
266
+ return { success: true }
267
+ }
268
+
269
+ static async listTabs(port = 9222) {
270
+ try { return await httpGet(`http://127.0.0.1:${port}/json`) } catch { return [] }
271
+ }
272
+ }
273
+
274
+ export default ChromeBrowser