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.
@@ -23,6 +23,16 @@ 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
25
  import { ScreenMonitor } from "./screen-monitor.mjs"
26
+ import { ChromeBrowser } from "./browser-control.mjs"
27
+ import { GitAPI } from "./git-api.mjs"
28
+ import { WebParser } from "./web-parser.mjs"
29
+ import { GmailClient } from "./gmail.mjs"
30
+ import { ChartGenerator } from "./charts.mjs"
31
+ import { YandexMaps } from "./yandex-maps.mjs"
32
+ import { GameEngine } from "./game-engine.mjs"
33
+ import { GDriveBackup } from "./gdrive-backup.mjs"
34
+ import { ADB } from "./adb.mjs"
35
+ import { HomeAssistant } from "./home-assistant.mjs"
26
36
  import {
27
37
  buildRepoMap, buildProjectContext, compressContext,
28
38
  loadSpec, generateSpecTemplate,
@@ -658,6 +668,109 @@ const COMMANDS = [
658
668
  ["/monitor-alerts", "показать обнаруженные проблемы"],
659
669
  ["/monitor-screenshot", "сделать скриншот и проанализировать"],
660
670
 
671
+ // Chrome DevTools Protocol
672
+ ["/browser", "открыть браузер и перейти на URL"],
673
+ ["/browser-click", "кликнуть по элементу (CSS selector)"],
674
+ ["/browser-type", "набрать текст в поле"],
675
+ ["/browser-scroll", "прокрутить страницу"],
676
+ ["/browser-screenshot", "скриншот текущей страницы"],
677
+ ["/browser-links", "показать все ссылки"],
678
+ ["/browser-forms", "показать формы на странице"],
679
+ ["/browser-close", "закрыть браузер"],
680
+
681
+ // Git API (GitHub/GitLab)
682
+ ["/git-issues", "список issues"],
683
+ ["/git-issue", "создать issue"],
684
+ ["/git-prs", "список Pull Requests"],
685
+ ["/git-pr-create", "создать Pull Request"],
686
+ ["/git-pr-review", "оставить ревью на PR"],
687
+ ["/git-pr-merge", "слить PR"],
688
+ ["/git-branches", "список веток"],
689
+ ["/git-releases", "список релизов"],
690
+ ["/git-release", "создать релиз"],
691
+
692
+ // Web Parser
693
+ ["/fetch", "загрузить и показать страницу"],
694
+ ["/fetch-text", "извлечь текст со страницы"],
695
+ ["/fetch-links", "извлечь все ссылки"],
696
+ ["/fetch-images", "извлечь все картинки"],
697
+ ["/fetch-forms", "извлечь формы"],
698
+ ["/fetch-seo", "SEO анализ страницы"],
699
+ ["/web-search", "поиск в интернете"],
700
+ ["/web-batch", "загрузить несколько страниц"],
701
+
702
+ // Gmail
703
+ ["/gmail-setup", "настроить Gmail API (OAuth2)"],
704
+ ["/gmail-auth", "завершить OAuth2 авторизацию"],
705
+ ["/gmail-send", "отправить email"],
706
+ ["/gmail-inbox", "последние письма"],
707
+ ["/gmail-search", "поиск писем"],
708
+ ["/gmail-read", "прочитать письмо"],
709
+ ["/gmail-labels", "список меток"],
710
+ ["/gmail-read-all", "пометить все прочитанным"],
711
+
712
+ // Charts
713
+ ["/chart-bar", "столбчатая диаграмма"],
714
+ ["/chart-line", "линейный график"],
715
+ ["/chart-pie", "круговая диаграмма"],
716
+ ["/chart-doughnut", "кольцевая диаграмма"],
717
+ ["/chart-radar", "лепестковая диаграмма"],
718
+ ["/chart-scatter", "точечный график"],
719
+ ["/chart-csv", "график из CSV файла"],
720
+ ["/chart-list", "список сохранённых графиков"],
721
+
722
+ // Yandex Maps
723
+ ["/ymaps-key", "установить API ключ Яндекс.Карт"],
724
+ ["/ymaps-geo", "геокодирование (адрес → координаты)"],
725
+ ["/ymaps-show", "показать место на карте"],
726
+ ["/ymaps-route", "маршрут от A до B"],
727
+ ["/ymaps-places", "показать несколько мест"],
728
+ ["/ymaps-static", "скачать статическую карту"],
729
+
730
+ // Games
731
+ ["/snake", "игра Змейка"],
732
+ ["/tetris", "игра Тетрис"],
733
+ ["/minesweeper", "игра Сапёр"],
734
+ ["/2048", "игра 2048"],
735
+ ["/flappy", "игра Flappy Bird"],
736
+ ["/tictactoe", "Крестики-нолики"],
737
+ ["/sudoku", "игра Судоку"],
738
+
739
+ // Google Drive Backup
740
+ ["/gdrive-setup", "настроить Google Drive API"],
741
+ ["/gdrive-auth", "завершить OAuth2 авторизацию"],
742
+ ["/gdrive-backup", "создать бэкап проекта"],
743
+ ["/gdrive-list", "список файлов на Drive"],
744
+ ["/gdrive-quota", "использовано места"],
745
+ ["/gdrive-search", "поиск файлов"],
746
+ ["/gdrive-history", "история бэкапов"],
747
+
748
+ // ADB
749
+ ["/adb-devices", "список устройств"],
750
+ ["/adb-info", "информация об устройстве"],
751
+ ["/adb-screenshot", "скриншот телефона"],
752
+ ["/adb-shell", "выполнить shell команду"],
753
+ ["/adb-tap", "тап по координатам"],
754
+ ["/adb-swipe", "свайп"],
755
+ ["/adb-type", "набрать текст"],
756
+ ["/adb-battery", "информация о батарее"],
757
+ ["/adb-install", "установить APK"],
758
+ ["/adb-packages", "список пакетов"],
759
+
760
+ // Home Assistant
761
+ ["/ha-setup", "настроить Home Assistant"],
762
+ ["/ha-status", "статус HA"],
763
+ ["/ha-states", "все состояния"],
764
+ ["/ha-lights", "спискок лампочек"],
765
+ ["/ha-sensors", "список сенсоров"],
766
+ ["/ha-toggle", "переключить устройство"],
767
+ ["/ha-on", "включить"],
768
+ ["/ha-off", "выключить"],
769
+ ["/ha-climate", "информация о климате"],
770
+ ["/ha-media", "список медиа-плееров"],
771
+ ["/ha-volume", "громкость"],
772
+ ["/ha-cover", "управление шторами/воротами"],
773
+
661
774
  // Управление компьютером
662
775
  ["/open", "открыть приложение/файл/URL"],
663
776
  ["/app", "запустить приложение"],
@@ -784,6 +897,16 @@ async function handleCommand(line) {
784
897
  ["🔀 Git Экосистема", ["/git-eco", "/git-pr", "/git-merge-auto"]],
785
898
  ["🤖 Autonomous Agent", ["/auto", "/auto-stop", "/auto-status", "/auto-dashboard", "/auto-history"]],
786
899
  ["👁️ Computer Vision", ["/monitor", "/monitor-stop", "/monitor-status", "/monitor-alerts", "/monitor-screenshot"]],
900
+ ["🌐 Browser Control", ["/browser", "/browser-click", "/browser-type", "/browser-scroll", "/browser-screenshot", "/browser-links", "/browser-forms", "/browser-close"]],
901
+ ["🔗 Git API", ["/git-issues", "/git-issue", "/git-prs", "/git-pr-create", "/git-pr-review", "/git-pr-merge", "/git-branches", "/git-releases", "/git-release"]],
902
+ ["🔍 Web Parser", ["/fetch", "/fetch-text", "/fetch-links", "/fetch-images", "/fetch-forms", "/fetch-seo", "/web-search", "/web-batch"]],
903
+ ["📧 Gmail", ["/gmail-setup", "/gmail-auth", "/gmail-send", "/gmail-inbox", "/gmail-search", "/gmail-read", "/gmail-labels", "/gmail-read-all"]],
904
+ ["📊 Charts", ["/chart-bar", "/chart-line", "/chart-pie", "/chart-doughnut", "/chart-radar", "/chart-scatter", "/chart-csv", "/chart-list"]],
905
+ ["🗺️ Яндекс.Карты", ["/ymaps-key", "/ymaps-geo", "/ymaps-show", "/ymaps-route", "/ymaps-places", "/ymaps-static"]],
906
+ ["🎮 Игры", ["/snake", "/tetris", "/minesweeper", "/2048", "/flappy", "/tictactoe", "/sudoku"]],
907
+ ["💾 Google Drive", ["/gdrive-setup", "/gdrive-auth", "/gdrive-backup", "/gdrive-list", "/gdrive-quota", "/gdrive-search", "/gdrive-history"]],
908
+ ["📱 ADB", ["/adb-devices", "/adb-info", "/adb-screenshot", "/adb-shell", "/adb-tap", "/adb-swipe", "/adb-type", "/adb-battery", "/adb-install", "/adb-packages"]],
909
+ ["🏠 Home Assistant", ["/ha-setup", "/ha-status", "/ha-states", "/ha-lights", "/ha-sensors", "/ha-toggle", "/ha-on", "/ha-off", "/ha-climate", "/ha-media", "/ha-volume", "/ha-cover"]],
787
910
  ["⚙️ Настройки", ["/help", "/model", "/clear", "/compact", "/cost", "/context", "/config", "/version", "/login", "/newkey", "/doctor", "/sessions", "/color", "/lang", "/shortcut"]],
788
911
  ]
789
912
  for (const [cat, cmds] of categories) {
@@ -3844,6 +3967,1247 @@ If everything looks normal, set severity to "none" and issues to [].`
3844
3967
  return
3845
3968
  }
3846
3969
 
3970
+ // ═══════════════════════════════════════════════════
3971
+ // CHROME DEVTOOLS PROTOCOL (BROWSER CONTROL)
3972
+ // ═══════════════════════════════════════════════════
3973
+ case "/browser": {
3974
+ console.log()
3975
+ const browser = new ChromeBrowser()
3976
+ const url = arg || "about:blank"
3977
+ startSpinner("Launching browser")
3978
+ const launch = await browser.launch(false)
3979
+ stopSpinner()
3980
+ if (!launch.success) { console.log(red(` ✗ ${launch.error}\n`)); return }
3981
+ startSpinner("Connecting")
3982
+ const conn = await browser.connect()
3983
+ stopSpinner()
3984
+ if (!conn.success) { console.log(red(` ✗ ${conn.error}\n`)); return }
3985
+ startSpinner(`Navigating to ${url}`)
3986
+ await browser.navigate(url)
3987
+ stopSpinner()
3988
+ const title = await browser.getTitle()
3989
+ console.log(box([
3990
+ green("✓ Browser launched!"), "",
3991
+ dim("URL: ") + cyan(url),
3992
+ dim("Title: ") + white(title.title || "—"), "",
3993
+ dim("Commands: /browser-click, /browser-type, /browser-scroll"),
3994
+ dim(" /browser-screenshot, /browser-links, /browser-close"),
3995
+ ], { title: "🌐 Browser", color: cyan, padding: 2 }))
3996
+ console.log()
3997
+ return
3998
+ }
3999
+
4000
+ case "/browser-click": {
4001
+ if (!arg) { console.log(dim("\n Usage: /browser-click <css-selector>\n")); return }
4002
+ const browser = new ChromeBrowser()
4003
+ const result = await browser.click(arg.trim())
4004
+ if (result.success) console.log(green(` ✓ Clicked at (${result.x}, ${result.y})\n`))
4005
+ else console.log(red(` ✗ ${result.error}\n`))
4006
+ return
4007
+ }
4008
+
4009
+ case "/browser-type": {
4010
+ if (!arg) { console.log(dim("\n Usage: /browser-type <selector> <text>\n")); return }
4011
+ const [sel, ...textParts] = arg.split(" ")
4012
+ const text = textParts.join(" ")
4013
+ const browser = new ChromeBrowser()
4014
+ const result = await browser.type(sel, text)
4015
+ if (result.success) console.log(green(` ✓ Typed "${text}" in ${sel}\n`))
4016
+ else console.log(red(` ✗ ${result.error}\n`))
4017
+ return
4018
+ }
4019
+
4020
+ case "/browser-scroll": {
4021
+ const browser = new ChromeBrowser()
4022
+ const delta = arg ? parseInt(arg) || 300 : 300
4023
+ await browser.scroll(0, delta)
4024
+ console.log(green(` ✓ Scrolled ${delta}px\n`))
4025
+ return
4026
+ }
4027
+
4028
+ case "/browser-screenshot": {
4029
+ const browser = new ChromeBrowser()
4030
+ const ss = await browser.screenshot()
4031
+ if (ss.success) {
4032
+ console.log(green(` ✓ Screenshot saved: ${ss.path}\n`))
4033
+ try { execSync(`start "" "${ss.path}"`, { shell: "cmd.exe" }) } catch {}
4034
+ } else {
4035
+ console.log(red(` ✗ ${ss.error}\n`))
4036
+ }
4037
+ return
4038
+ }
4039
+
4040
+ case "/browser-links": {
4041
+ const browser = new ChromeBrowser()
4042
+ const links = await browser.getLinks()
4043
+ if (links.success && links.links.length > 0) {
4044
+ console.log(box([
4045
+ ...links.links.slice(0, 20).map(l => cyan(l.text.slice(0, 50)) + dim(` → ${l.href.slice(0, 60)}`)),
4046
+ links.links.length > 20 ? dim(`... and ${links.links.length - 20} more`) : "",
4047
+ ].filter(Boolean), { title: `🔗 Links (${links.links.length})`, color: cyan, padding: 2 }))
4048
+ } else {
4049
+ console.log(dim(" No links found\n"))
4050
+ }
4051
+ console.log()
4052
+ return
4053
+ }
4054
+
4055
+ case "/browser-forms": {
4056
+ const browser = new ChromeBrowser()
4057
+ const forms = await browser.getForms()
4058
+ if (forms.success && forms.forms.length > 0) {
4059
+ console.log(box([
4060
+ ...forms.forms.map((f, i) => [
4061
+ violet(`Form ${i + 1}: ${f.method} ${f.action?.slice(0, 50)}`),
4062
+ ...f.inputs.map(inp => dim(` ${inp.name || inp.type}: ${inp.placeholder || ""}`)),
4063
+ ].join("\n")),
4064
+ ], { title: `📝 Forms (${forms.forms.length})`, color: violet, padding: 2 }))
4065
+ } else {
4066
+ console.log(dim(" No forms found\n"))
4067
+ }
4068
+ console.log()
4069
+ return
4070
+ }
4071
+
4072
+ case "/browser-close": {
4073
+ const browser = new ChromeBrowser()
4074
+ browser.close()
4075
+ console.log(green(" ✓ Browser closed\n"))
4076
+ return
4077
+ }
4078
+
4079
+ // ═══════════════════════════════════════════════════
4080
+ // GIT API (GitHub/GitLab)
4081
+ // ═══════════════════════════════════════════════════
4082
+ case "/git-issues": {
4083
+ console.log()
4084
+ const api = new GitAPI()
4085
+ if (!api.isConfigured()) { console.log(red(" ✗ No GitHub/GitLab remote found\n")); return }
4086
+ if (!api.hasToken()) { console.log(yellow(" ⚠ No token. Set GITHUB_TOKEN or GITLAB_TOKEN\n")); return }
4087
+ startSpinner("Fetching issues")
4088
+ const result = await api.listIssues()
4089
+ stopSpinner()
4090
+ if (result.success && result.issues.length > 0) {
4091
+ console.log(box([
4092
+ ...result.issues.slice(0, 15).map(i => {
4093
+ const num = i.number || i.iid
4094
+ const state = i.state === "open" ? green("●") : red("●")
4095
+ return `${state} ${violet(`#${num}`)} ${white(i.title?.slice(0, 60))}`
4096
+ }),
4097
+ ], { title: `📋 Issues (${result.issues.length})`, color: cyan, padding: 2 }))
4098
+ } else {
4099
+ console.log(dim(" No issues found\n"))
4100
+ }
4101
+ console.log()
4102
+ return
4103
+ }
4104
+
4105
+ case "/git-issue": {
4106
+ if (!arg) { console.log(dim("\n Usage: /git-issue <title>\n")); return }
4107
+ const api = new GitAPI()
4108
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4109
+ startSpinner("Creating issue")
4110
+ const result = await api.createIssue(arg.trim(), `Created by Stella Coder\n\n_${new Date().toISOString()}_`)
4111
+ stopSpinner()
4112
+ if (result.success) {
4113
+ console.log(green(` ✓ Issue created: ${result.url}\n`))
4114
+ } else {
4115
+ console.log(red(` ✗ Failed to create issue\n`))
4116
+ }
4117
+ return
4118
+ }
4119
+
4120
+ case "/git-prs": {
4121
+ console.log()
4122
+ const api = new GitAPI()
4123
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4124
+ startSpinner("Fetching PRs")
4125
+ const result = await api.listPRs()
4126
+ stopSpinner()
4127
+ if (result.success && result.prs.length > 0) {
4128
+ console.log(box([
4129
+ ...result.prs.slice(0, 15).map(pr => {
4130
+ const num = pr.number || pr.iid
4131
+ const state = pr.merged_at ? violet("merged") : pr.state === "open" ? green("open") : red("closed")
4132
+ return `${state} ${violet(`#${num}`)} ${white(pr.title?.slice(0, 60))}`
4133
+ }),
4134
+ ], { title: `🔀 Pull Requests (${result.prs.length})`, color: violet, padding: 2 }))
4135
+ } else {
4136
+ console.log(dim(" No PRs found\n"))
4137
+ }
4138
+ console.log()
4139
+ return
4140
+ }
4141
+
4142
+ case "/git-pr-create": {
4143
+ if (!arg) { console.log(dim("\n Usage: /git-pr-create <title>\n")); return }
4144
+ const api = new GitAPI()
4145
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4146
+ startSpinner("Creating PR")
4147
+ const result = await api.createPR(arg.trim(), `Created by Stella Coder\n\n_${new Date().toISOString()}_`)
4148
+ stopSpinner()
4149
+ if (result.success) {
4150
+ console.log(green(` ✓ PR created: ${result.url}\n`))
4151
+ } else {
4152
+ console.log(red(` ✗ Failed to create PR\n`))
4153
+ }
4154
+ return
4155
+ }
4156
+
4157
+ case "/git-pr-review": {
4158
+ if (!arg) { console.log(dim("\n Usage: /git-pr-review <number> <APPROVE|REQUEST_CHANGES|COMMENT>\n")); return }
4159
+ const [prNum, ...reviewParts] = arg.split(" ")
4160
+ const reviewEvent = reviewParts.join(" ") || "COMMENT"
4161
+ const api = new GitAPI()
4162
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4163
+ startSpinner("Submitting review")
4164
+ const result = await api.reviewPR(parseInt(prNum), reviewEvent, `Review by Stella Coder`)
4165
+ stopSpinner()
4166
+ if (result.success) console.log(green(` ✓ Review submitted on #${prNum}\n`))
4167
+ else console.log(red(` ✗ Failed\n`))
4168
+ return
4169
+ }
4170
+
4171
+ case "/git-pr-merge": {
4172
+ if (!arg) { console.log(dim("\n Usage: /git-pr-merge <number>\n")); return }
4173
+ const api = new GitAPI()
4174
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4175
+ startSpinner("Merging PR")
4176
+ const result = await api.mergePR(parseInt(arg))
4177
+ stopSpinner()
4178
+ if (result.success) console.log(green(` ✓ PR #${arg} merged\n`))
4179
+ else console.log(red(` ✗ Failed\n`))
4180
+ return
4181
+ }
4182
+
4183
+ case "/git-branches": {
4184
+ console.log()
4185
+ const api = new GitAPI()
4186
+ if (!api.isConfigured()) { console.log(red(" ✗ Not configured\n")); return }
4187
+ startSpinner("Fetching branches")
4188
+ const result = await api.listBranches()
4189
+ stopSpinner()
4190
+ if (result.success) {
4191
+ console.log(box(result.branches.map(b => cyan(b)), { title: "🌿 Branches", color: cyan, padding: 2 }))
4192
+ }
4193
+ console.log()
4194
+ return
4195
+ }
4196
+
4197
+ case "/git-releases": {
4198
+ console.log()
4199
+ const api = new GitAPI()
4200
+ if (!api.isConfigured()) { console.log(red(" ✗ Not configured\n")); return }
4201
+ startSpinner("Fetching releases")
4202
+ const result = await api.listReleases()
4203
+ stopSpinner()
4204
+ if (result.success && result.releases.length > 0) {
4205
+ console.log(box([
4206
+ ...result.releases.slice(0, 10).map(r => green(r.tag_name) + dim(` — ${r.name || ""}`)),
4207
+ ], { title: `📦 Releases (${result.releases.length})`, color: green, padding: 2 }))
4208
+ } else {
4209
+ console.log(dim(" No releases found\n"))
4210
+ }
4211
+ console.log()
4212
+ return
4213
+ }
4214
+
4215
+ case "/git-release": {
4216
+ if (!arg) { console.log(dim("\n Usage: /git-release <tag> [name]\n")); return }
4217
+ const [tag, ...nameParts] = arg.split(" ")
4218
+ const name = nameParts.join(" ") || tag
4219
+ const api = new GitAPI()
4220
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4221
+ startSpinner("Creating release")
4222
+ const result = await api.createRelease(tag, name, `Release ${tag} created by Stella Coder`)
4223
+ stopSpinner()
4224
+ if (result.success) {
4225
+ console.log(green(` ✓ Release created: ${result.url}\n`))
4226
+ } else {
4227
+ console.log(red(` ✗ Failed\n`))
4228
+ }
4229
+ return
4230
+ }
4231
+
4232
+ // ═══════════════════════════════════════════════════
4233
+ // WEB PARSER
4234
+ // ═══════════════════════════════════════════════════
4235
+ case "/fetch": {
4236
+ if (!arg) { console.log(dim("\n Usage: /fetch <url>\n")); return }
4237
+ console.log()
4238
+ const parser = new WebParser()
4239
+ startSpinner("Fetching page")
4240
+ const result = await parser.fetchAndParse(arg.trim())
4241
+ stopSpinner()
4242
+ if (result.success) {
4243
+ console.log(box([
4244
+ green(`✓ ${result.page.title || "No title"}`),
4245
+ dim(`URL: ${result.page.url}`),
4246
+ dim(`Length: ${result.page.text?.length || 0} chars`),
4247
+ "",
4248
+ result.page.text?.slice(0, 500) || "No text content",
4249
+ result.page.text?.length > 500 ? dim("\n... (truncated)") : "",
4250
+ ], { title: "🌐 Fetched Page", color: cyan, padding: 2 }))
4251
+ } else {
4252
+ console.log(red(` ✗ ${result.error}\n`))
4253
+ }
4254
+ console.log()
4255
+ return
4256
+ }
4257
+
4258
+ case "/fetch-text": {
4259
+ if (!arg) { console.log(dim("\n Usage: /fetch-text <url>\n")); return }
4260
+ const parser = new WebParser()
4261
+ const result = await parser.fetchAndParse(arg.trim())
4262
+ if (result.success) {
4263
+ console.log(result.page.text || "No text")
4264
+ } else {
4265
+ console.log(red(` ✗ ${result.error}`))
4266
+ }
4267
+ console.log()
4268
+ return
4269
+ }
4270
+
4271
+ case "/fetch-links": {
4272
+ if (!arg) { console.log(dim("\n Usage: /fetch-links <url>\n")); return }
4273
+ const parser = new WebParser()
4274
+ const result = await parser.fetchAndParse(arg.trim())
4275
+ if (result.success && result.page.links?.length > 0) {
4276
+ console.log(box(result.page.links.slice(0, 30).map(l => cyan(l.text.slice(0, 50)) + dim(` → ${l.href}`)), { title: `🔗 Links (${result.page.links.length})`, color: cyan, padding: 2 }))
4277
+ } else {
4278
+ console.log(dim(" No links found\n"))
4279
+ }
4280
+ console.log()
4281
+ return
4282
+ }
4283
+
4284
+ case "/fetch-images": {
4285
+ if (!arg) { console.log(dim("\n Usage: /fetch-images <url>\n")); return }
4286
+ const parser = new WebParser()
4287
+ const result = await parser.fetchAndParse(arg.trim())
4288
+ if (result.success && result.page.images?.length > 0) {
4289
+ console.log(box(result.page.images.slice(0, 20).map(i => {
4290
+ const src = i.src?.length > 80 ? i.src.slice(0, 80) + "..." : i.src
4291
+ return violet(src) + dim(` alt="${i.alt || ""}"`)
4292
+ }), { title: `🖼️ Images (${result.page.images.length})`, color: violet, padding: 2 }))
4293
+ } else {
4294
+ console.log(dim(" No images found\n"))
4295
+ }
4296
+ console.log()
4297
+ return
4298
+ }
4299
+
4300
+ case "/fetch-forms": {
4301
+ if (!arg) { console.log(dim("\n Usage: /fetch-forms <url>\n")); return }
4302
+ const parser = new WebParser()
4303
+ const result = await parser.fetchAndParse(arg.trim())
4304
+ if (result.success && result.page.forms?.length > 0) {
4305
+ console.log(box(result.page.forms.map((f, i) => [
4306
+ violet(`Form ${i + 1}: ${f.method} ${f.action}`),
4307
+ ...f.fields.map(field => dim(` ${field.name || field.type}: ${field.placeholder || ""}`)),
4308
+ ].join("\n")), { title: `📝 Forms (${result.page.forms.length})`, color: violet, padding: 2 }))
4309
+ } else {
4310
+ console.log(dim(" No forms found\n"))
4311
+ }
4312
+ console.log()
4313
+ return
4314
+ }
4315
+
4316
+ case "/fetch-seo": {
4317
+ if (!arg) { console.log(dim("\n Usage: /fetch-seo <url>\n")); return }
4318
+ console.log()
4319
+ const parser = new WebParser()
4320
+ const result = await parser.fetchAndParse(arg.trim())
4321
+ if (!result.success) { console.log(red(` ✗ ${result.error}\n`)); return }
4322
+ const seo = parser.analyzeSEO(result.page)
4323
+ console.log(box([
4324
+ `Score: ${seo.score}/100`,
4325
+ "",
4326
+ ...seo.checks.map(c => `${c.pass ? green("✓") : red("✗")} ${c.label}: ${dim(c.detail)}`),
4327
+ "",
4328
+ ...seo.recommendations.map(r => dim(`• ${r}`)),
4329
+ ], { title: `🔍 SEO Analysis`, color: seo.score >= 70 ? green : yellow, padding: 2 }))
4330
+ console.log()
4331
+ return
4332
+ }
4333
+
4334
+ case "/web-search": {
4335
+ if (!arg) { console.log(dim("\n Usage: /web-search <query>\n")); return }
4336
+ console.log()
4337
+ const parser = new WebParser()
4338
+ startSpinner("Searching")
4339
+ const results = await parser.search(arg.trim())
4340
+ stopSpinner()
4341
+ if (results.length > 0) {
4342
+ console.log(box(results.slice(0, 8).map((r, i) => [
4343
+ violet(`${i + 1}. `) + white(r.title),
4344
+ dim(` ${r.url}`),
4345
+ dim(` ${r.snippet?.slice(0, 100) || ""}`),
4346
+ ].join("\n")), { title: `🔍 Search Results (${results.length})`, color: violet, padding: 2 }))
4347
+ } else {
4348
+ console.log(dim(" No results found\n"))
4349
+ }
4350
+ console.log()
4351
+ return
4352
+ }
4353
+
4354
+ case "/web-batch": {
4355
+ if (!arg) { console.log(dim("\n Usage: /web-batch <url1> <url2> ...\n")); return }
4356
+ const urls = arg.split(/\s+/).filter(Boolean)
4357
+ console.log()
4358
+ const parser = new WebParser()
4359
+ startSpinner(`Fetching ${urls.length} pages`)
4360
+ const results = await parser.batchFetch(urls)
4361
+ stopSpinner()
4362
+ const ok = results.filter(r => r.success)
4363
+ const fail = results.filter(r => !r.success)
4364
+ console.log(box([
4365
+ green(`✓ Fetched: ${ok.length}`),
4366
+ fail.length > 0 ? red(`✗ Failed: ${fail.length}`) : "",
4367
+ "",
4368
+ ...ok.map(r => dim(`• ${r.page?.title || r.page?.url}`)),
4369
+ ].filter(Boolean), { title: "📦 Batch Fetch", color: cyan, padding: 2 }))
4370
+ console.log()
4371
+ return
4372
+ }
4373
+
4374
+
4375
+ // ═══════════════════════════════════════════════════
4376
+ // GMAIL
4377
+ // ═══════════════════════════════════════════════════
4378
+ case "/gmail-setup": {
4379
+ console.log()
4380
+ const gmail = new GmailClient()
4381
+ const url = gmail.getSetupURL()
4382
+ if (!url) {
4383
+ console.log(box([
4384
+ yellow("Требуется файл credentials.json"),
4385
+ "",
4386
+ dim("1. Создай проект в https://console.cloud.google.com"),
4387
+ dim("2. Включи Gmail API"),
4388
+ dim("3. Скачай credentials.json (OAuth 2.0 Client ID)"),
4389
+ dim("4. Сохрани в ~/.stella/gmail/credentials.json"),
4390
+ ], { title: "📧 Gmail Setup", color: yellow, padding: 2 }))
4391
+ } else {
4392
+ console.log(box([
4393
+ green("Перейди по ссылке и авторизуйся:"),
4394
+ "",
4395
+ cyan(url),
4396
+ "",
4397
+ dim("После авторизации введи код:"),
4398
+ dim(" /gmail-auth <code>"),
4399
+ ], { title: "📧 Gmail OAuth2", color: cyan, padding: 2 }))
4400
+ }
4401
+ console.log()
4402
+ return
4403
+ }
4404
+
4405
+ case "/gmail-auth": {
4406
+ if (!arg) { console.log(dim("\n Usage: /gmail-auth <code>\n")); return }
4407
+ const gmail = new GmailClient()
4408
+ const result = await gmail.completeAuth(arg.trim())
4409
+ if (result.success) console.log(green(" ✓ Gmail авторизован!\n"))
4410
+ else console.log(red(` ✗ ${result.error}\n`))
4411
+ return
4412
+ }
4413
+
4414
+ case "/gmail-send": {
4415
+ if (!arg) { console.log(dim("\n Usage: /gmail-send <to> | <subject> | <body>\n")); return }
4416
+ const parts = arg.split("|").map(s => s.trim())
4417
+ const [to, subject = "(no subject)", ...bodyParts] = parts
4418
+ const body = bodyParts.join("|")
4419
+ const gmail = new GmailClient()
4420
+ startSpinner("Sending email")
4421
+ const result = await gmail.sendEmail({ to, subject, body, isHtml: body.includes("<") })
4422
+ stopSpinner()
4423
+ if (result.success) console.log(green(` ✓ Email sent: ${result.messageId}\n`))
4424
+ else console.log(red(` ✗ ${result.error}\n`))
4425
+ return
4426
+ }
4427
+
4428
+ case "/gmail-inbox": {
4429
+ console.log()
4430
+ const gmail = new GmailClient()
4431
+ startSpinner("Fetching inbox")
4432
+ const result = await gmail.listMessages({ labelIds: ["INBOX"] })
4433
+ stopSpinner()
4434
+ if (result.success && result.messages.length > 0) {
4435
+ console.log(box(result.messages.slice(0, 10).map(m =>
4436
+ `${m.from?.match(/<?(\S+@\S+)/)?.[1] || m.from} ${violet(m.subject?.slice(0, 40))} ${dim(m.date?.slice(0, 16) || "")}`
4437
+ ), { title: `📧 Inbox (${result.messages.length})`, color: cyan, padding: 2 }))
4438
+ } else {
4439
+ console.log(dim(" No messages\n"))
4440
+ }
4441
+ console.log()
4442
+ return
4443
+ }
4444
+
4445
+ case "/gmail-search": {
4446
+ if (!arg) { console.log(dim("\n Usage: /gmail-search <query>\n")); return }
4447
+ console.log()
4448
+ const gmail = new GmailClient()
4449
+ startSpinner("Searching")
4450
+ const result = await gmail.searchMessages(arg.trim())
4451
+ stopSpinner()
4452
+ if (result.success && result.messages.length > 0) {
4453
+ console.log(box(result.messages.slice(0, 10).map(m =>
4454
+ `${m.from?.match(/<?(\S+@\S+)/)?.[1] || m.from} ${violet(m.subject?.slice(0, 40))} ${dim(m.date?.slice(0, 16) || "")}`
4455
+ ), { title: `🔍 Search (${result.messages.length})`, color: violet, padding: 2 }))
4456
+ } else {
4457
+ console.log(dim(" No results\n"))
4458
+ }
4459
+ console.log()
4460
+ return
4461
+ }
4462
+
4463
+ case "/gmail-read": {
4464
+ if (!arg) { console.log(dim("\n Usage: /gmail-read <messageId>\n")); return }
4465
+ const gmail = new GmailClient()
4466
+ startSpinner("Reading message")
4467
+ const result = await gmail.getMessage(arg.trim())
4468
+ stopSpinner()
4469
+ if (result.success) {
4470
+ console.log(box([
4471
+ `${violet("From:")} ${result.message.from}`,
4472
+ `${violet("Subject:")} ${result.message.subject}`,
4473
+ `${violet("Date:")} ${result.message.date}`,
4474
+ "",
4475
+ result.message.textBody?.slice(0, 1000) || result.message.snippet || "(no text)",
4476
+ ], { title: "📧 Message", color: cyan, padding: 2 }))
4477
+ } else {
4478
+ console.log(red(` ✗ ${result.error}\n`))
4479
+ }
4480
+ console.log()
4481
+ return
4482
+ }
4483
+
4484
+ case "/gmail-labels": {
4485
+ console.log()
4486
+ const gmail = new GmailClient()
4487
+ const result = await gmail.listLabels()
4488
+ if (result.success) {
4489
+ console.log(box(result.labels.map(l => `${l.name}`), { title: "🏷️ Labels", color: violet, padding: 2 }))
4490
+ } else {
4491
+ console.log(dim(" No labels\n"))
4492
+ }
4493
+ console.log()
4494
+ return
4495
+ }
4496
+
4497
+ case "/gmail-read-all": {
4498
+ const gmail = new GmailClient()
4499
+ const result = await gmail.getUnreadCount()
4500
+ if (result === 0) { console.log(green(" ✓ All read\n")); return }
4501
+ console.log(dim(` Found ${result} unread\n`))
4502
+ return
4503
+ }
4504
+
4505
+ // ═══════════════════════════════════════════════════
4506
+ // CHARTS
4507
+ // ═══════════════════════════════════════════════════
4508
+ case "/chart-bar": {
4509
+ if (!arg) { console.log(dim("\n Usage: /chart-bar <title> | <label1:val1,label2:val2,...>\n")); return }
4510
+ const parts = arg.split("|").map(s => s.trim())
4511
+ const title = parts[0]
4512
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4513
+ const labels = dataParts.map(p => p.split(":")[0])
4514
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4515
+ const chart = new ChartGenerator()
4516
+ const result = await chart.bar(title, labels, data)
4517
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4518
+ return
4519
+ }
4520
+
4521
+ case "/chart-line": {
4522
+ if (!arg) { console.log(dim("\n Usage: /chart-line <title> | <label1:val1,label2:val2,...>\n")); return }
4523
+ const parts = arg.split("|").map(s => s.trim())
4524
+ const title = parts[0]
4525
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4526
+ const labels = dataParts.map(p => p.split(":")[0])
4527
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4528
+ const chart = new ChartGenerator()
4529
+ const result = await chart.line(title, labels, [{ label: title, data }])
4530
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4531
+ return
4532
+ }
4533
+
4534
+ case "/chart-pie": {
4535
+ if (!arg) { console.log(dim("\n Usage: /chart-pie <title> | <label1:val1,label2:val2,...>\n")); return }
4536
+ const parts = arg.split("|").map(s => s.trim())
4537
+ const title = parts[0]
4538
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4539
+ const labels = dataParts.map(p => p.split(":")[0])
4540
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4541
+ const chart = new ChartGenerator()
4542
+ const result = await chart.pie(title, labels, data)
4543
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4544
+ return
4545
+ }
4546
+
4547
+ case "/chart-doughnut": {
4548
+ if (!arg) { console.log(dim("\n Usage: /chart-doughnut <title> | <label1:val1,...>\n")); return }
4549
+ const parts = arg.split("|").map(s => s.trim())
4550
+ const title = parts[0]
4551
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4552
+ const labels = dataParts.map(p => p.split(":")[0])
4553
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4554
+ const chart = new ChartGenerator()
4555
+ const result = await chart.doughnut(title, labels, data)
4556
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4557
+ return
4558
+ }
4559
+
4560
+ case "/chart-radar": {
4561
+ if (!arg) { console.log(dim("\n Usage: /chart-radar <title> | <label1:val1,...>\n")); return }
4562
+ const parts = arg.split("|").map(s => s.trim())
4563
+ const title = parts[0]
4564
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4565
+ const labels = dataParts.map(p => p.split(":")[0])
4566
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4567
+ const chart = new ChartGenerator()
4568
+ const result = await chart.radar(title, labels, [{ label: title, data }])
4569
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4570
+ return
4571
+ }
4572
+
4573
+ case "/chart-scatter": {
4574
+ if (!arg) { console.log(dim("\n Usage: /chart-scatter <title> | <x1,y1,x2,y2,...>\n")); return }
4575
+ const parts = arg.split("|").map(s => s.trim())
4576
+ const title = parts[0]
4577
+ const nums = (parts[1] || "").split(",").map(parseFloat).filter(n => !isNaN(n))
4578
+ const points = []
4579
+ for (let i = 0; i < nums.length - 1; i += 2) { points.push({ x: nums[i], y: nums[i + 1] }) }
4580
+ const chart = new ChartGenerator()
4581
+ const result = await chart.scatter(title, points)
4582
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4583
+ return
4584
+ }
4585
+
4586
+ case "/chart-csv": {
4587
+ if (!arg) { console.log(dim("\n Usage: /chart-csv <path.csv>\n")); return }
4588
+ const chart = new ChartGenerator()
4589
+ const result = await chart.fromCSV(arg.trim())
4590
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4591
+ else console.log(red(` ✗ ${result.error}\n`))
4592
+ return
4593
+ }
4594
+
4595
+ case "/chart-list": {
4596
+ console.log()
4597
+ const chart = new ChartGenerator()
4598
+ const list = chart.listCharts()
4599
+ if (list.length > 0) {
4600
+ console.log(box(list.map(c => `${dim(c.created?.toLocaleString() || "")} ${violet(c.name)}`), { title: "📊 Charts", color: cyan, padding: 2 }))
4601
+ } else {
4602
+ console.log(dim(" No charts yet\n"))
4603
+ }
4604
+ console.log()
4605
+ return
4606
+ }
4607
+
4608
+ // ═══════════════════════════════════════════════════
4609
+ // YANDEX MAPS
4610
+ // ═══════════════════════════════════════════════════
4611
+ case "/ymaps-key": {
4612
+ if (!arg) { console.log(dim("\n Usage: /ymaps-key <api_key>\n")); return }
4613
+ state.config.yandexMapsKey = arg.trim()
4614
+ console.log(green(" ✓ API key set\n"))
4615
+ return
4616
+ }
4617
+
4618
+ case "/ymaps-geo": {
4619
+ if (!arg) { console.log(dim("\n Usage: /ymaps-geo <address>\n")); return }
4620
+ console.log()
4621
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4622
+ const ymaps = new YandexMaps(key)
4623
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set. Use /ymaps-key <key>\n")); return }
4624
+ startSpinner("Geocoding")
4625
+ const result = await ymaps.geocode(arg.trim())
4626
+ stopSpinner()
4627
+ if (result.success) {
4628
+ console.log(box([
4629
+ `${violet("Name:")} ${result.name}`,
4630
+ `${violet("Address:")} ${result.address || result.description}`,
4631
+ `${violet("Coordinates:")} ${result.coordinates.lat}, ${result.coordinates.lng}`,
4632
+ ], { title: "🗺️ Geocode", color: cyan, padding: 2 }))
4633
+ } else {
4634
+ console.log(red(` ✗ ${result.error}\n`))
4635
+ }
4636
+ console.log()
4637
+ return
4638
+ }
4639
+
4640
+ case "/ymaps-show": {
4641
+ if (!arg) { console.log(dim("\n Usage: /ymaps-show <address>\n")); return }
4642
+ console.log()
4643
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4644
+ const ymaps = new YandexMaps(key)
4645
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4646
+ startSpinner("Loading map")
4647
+ const result = await ymaps.showLocation(arg.trim())
4648
+ stopSpinner()
4649
+ if (result.success) console.log(green(` ✓ Map: ${result.path}\n`))
4650
+ else console.log(red(` ✗ ${result.error}\n`))
4651
+ return
4652
+ }
4653
+
4654
+ case "/ymaps-route": {
4655
+ if (!arg) { console.log(dim("\n Usage: /ymaps-route <from> | <to>\n")); return }
4656
+ const [from, to] = arg.split("|").map(s => s.trim())
4657
+ if (!from || !to) { console.log(dim("\n Usage: /ymaps-route <from> | <to>\n")); return }
4658
+ console.log()
4659
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4660
+ const ymaps = new YandexMaps(key)
4661
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4662
+ startSpinner("Building route")
4663
+ const result = await ymaps.showRoute(from, to)
4664
+ stopSpinner()
4665
+ if (result.success) console.log(green(` ✓ Route: ${result.path}\n`))
4666
+ else console.log(red(` ✗ ${result.error}\n`))
4667
+ return
4668
+ }
4669
+
4670
+ case "/ymaps-places": {
4671
+ if (!arg) { console.log(dim("\n Usage: /ymaps-places <addr1> | <addr2> | ...\n")); return }
4672
+ const places = arg.split("|").map(s => s.trim()).filter(Boolean)
4673
+ console.log()
4674
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4675
+ const ymaps = new YandexMaps(key)
4676
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4677
+ startSpinner("Loading places")
4678
+ const result = await ymaps.showMultiplePlaces(places)
4679
+ stopSpinner()
4680
+ if (result.success) console.log(green(` ✓ Map with ${places.length} places: ${result.path}\n`))
4681
+ else console.log(red(` ✗ ${result.error}\n`))
4682
+ return
4683
+ }
4684
+
4685
+ case "/ymaps-static": {
4686
+ if (!arg) { console.log(dim("\n Usage: /ymaps-static <address>\n")); return }
4687
+ console.log()
4688
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4689
+ const ymaps = new YandexMaps(key)
4690
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4691
+ startSpinner("Geocoding")
4692
+ const geo = await ymaps.geocode(arg.trim())
4693
+ stopSpinner()
4694
+ if (!geo.success) { console.log(red(` ✗ ${geo.error}\n`)); return }
4695
+ startSpinner("Downloading map")
4696
+ const result = await ymaps.getStaticMap({ center: [geo.coordinates.lat, geo.coordinates.lng], zoom: 15, markers: [{ lat: geo.coordinates.lat, lng: geo.coordinates.lng }] })
4697
+ stopSpinner()
4698
+ if (result.success) console.log(green(` ✓ Map saved: ${result.path} (${result.size} bytes)\n`))
4699
+ else console.log(red(` ✗ ${result.error}\n`))
4700
+ return
4701
+ }
4702
+
4703
+ // ═══════════════════════════════════════════════════
4704
+ // GAMES
4705
+ // ═══════════════════════════════════════════════════
4706
+ case "/snake": {
4707
+ const game = new GameEngine()
4708
+ const result = await game.playSnake()
4709
+ if (result.success) console.log(green(` ✓ Snake: ${result.path}\n`))
4710
+ return
4711
+ }
4712
+
4713
+ case "/tetris": {
4714
+ const game = new GameEngine()
4715
+ const result = await game.playTetris()
4716
+ if (result.success) console.log(green(` ✓ Tetris: ${result.path}\n`))
4717
+ return
4718
+ }
4719
+
4720
+ case "/minesweeper": {
4721
+ const game = new GameEngine()
4722
+ const result = await game.playMinesweeper()
4723
+ if (result.success) console.log(green(` ✓ Minesweeper: ${result.path}\n`))
4724
+ return
4725
+ }
4726
+
4727
+ case "/2048": {
4728
+ const game = new GameEngine()
4729
+ const result = await game.play2048()
4730
+ if (result.success) console.log(green(` ✓ 2048: ${result.path}\n`))
4731
+ return
4732
+ }
4733
+
4734
+ case "/flappy": {
4735
+ const game = new GameEngine()
4736
+ const result = await game.playFlappyBird()
4737
+ if (result.success) console.log(green(` ✓ Flappy Bird: ${result.path}\n`))
4738
+ return
4739
+ }
4740
+
4741
+ case "/tictactoe": {
4742
+ const game = new GameEngine()
4743
+ const result = await game.playTicTacToe()
4744
+ if (result.success) console.log(green(` ✓ Tic Tac Toe: ${result.path}\n`))
4745
+ return
4746
+ }
4747
+
4748
+ case "/sudoku": {
4749
+ const game = new GameEngine()
4750
+ const result = await game.playSudoku()
4751
+ if (result.success) console.log(green(` ✓ Sudoku: ${result.path}\n`))
4752
+ return
4753
+ }
4754
+
4755
+ // ═══════════════════════════════════════════════════
4756
+ // GOOGLE DRIVE BACKUP
4757
+ // ═══════════════════════════════════════════════════
4758
+ case "/gdrive-setup": {
4759
+ console.log()
4760
+ const gdrive = new GDriveBackup()
4761
+ const url = gdrive.getSetupURL()
4762
+ if (!url) {
4763
+ console.log(box([
4764
+ yellow("Требуется credentials.json"),
4765
+ "",
4766
+ dim("1. https://console.cloud.google.com"),
4767
+ dim("2. Включи Google Drive API"),
4768
+ dim("3. Скачай credentials.json (OAuth 2.0)"),
4769
+ dim("4. Сохрани в ~/.stella/gdrive/credentials.json"),
4770
+ ], { title: "💾 Google Drive Setup", color: yellow, padding: 2 }))
4771
+ } else {
4772
+ console.log(box([
4773
+ green("Перейди по ссылке и авторизуйся:"), "",
4774
+ cyan(url), "",
4775
+ dim("После авторизации введи код:"),
4776
+ dim(" /gdrive-auth <code>"),
4777
+ ], { title: "💾 GDrive OAuth2", color: cyan, padding: 2 }))
4778
+ }
4779
+ console.log()
4780
+ return
4781
+ }
4782
+
4783
+ case "/gdrive-auth": {
4784
+ if (!arg) { console.log(dim("\n Usage: /gdrive-auth <code>\n")); return }
4785
+ const gdrive = new GDriveBackup()
4786
+ const result = await gdrive.completeAuth(arg.trim())
4787
+ if (result.success) console.log(green(" ✓ Google Drive авторизован!\n"))
4788
+ else console.log(red(` ✗ ${result.error}\n`))
4789
+ return
4790
+ }
4791
+
4792
+ case "/gdrive-backup": {
4793
+ if (!arg) { console.log(dim("\n Usage: /gdrive-backup <path> [name]\n")); return }
4794
+ const parts = arg.split("|").map(s => s.trim())
4795
+ const targetPath = parts[0]
4796
+ const name = parts[1] || `backup_${path.basename(targetPath)}`
4797
+ if (!fs.existsSync(targetPath)) { console.log(red(` ✗ Path not found: ${targetPath}\n`)); return }
4798
+ const gdrive = new GDriveBackup()
4799
+ startSpinner("Backing up")
4800
+ const result = await gdrive.backupProject(targetPath, name)
4801
+ stopSpinner()
4802
+ if (result.success) {
4803
+ const size = result.size ? ` (${(result.size / 1024 / 1024).toFixed(2)} MB)` : ""
4804
+ console.log(green(` ✓ Backup "${name}" uploaded${size}\n`))
4805
+ } else {
4806
+ console.log(red(` ✗ ${result.error}\n`))
4807
+ }
4808
+ return
4809
+ }
4810
+
4811
+ case "/gdrive-list": {
4812
+ console.log()
4813
+ const gdrive = new GDriveBackup()
4814
+ startSpinner("Fetching files")
4815
+ const result = await gdrive.listBackups()
4816
+ stopSpinner()
4817
+ if (result.success && result.files.length > 0) {
4818
+ console.log(box(result.files.slice(0, 20).map(f =>
4819
+ `${violet(f.name)} ${dim(new Date(f.created).toLocaleDateString())} ${dim((f.size / 1024 / 1024).toFixed(1) + " MB")}`
4820
+ ), { title: "💾 Google Drive Files", color: cyan, padding: 2 }))
4821
+ } else {
4822
+ console.log(dim(" No files found\n"))
4823
+ }
4824
+ console.log()
4825
+ return
4826
+ }
4827
+
4828
+ case "/gdrive-quota": {
4829
+ console.log()
4830
+ const gdrive = new GDriveBackup()
4831
+ const result = await gdrive.getQuota()
4832
+ if (result.success) {
4833
+ const pct = result.limit > 0 ? ((result.used / result.limit) * 100).toFixed(1) : "?"
4834
+ console.log(box([
4835
+ `${green("Used:")} ${result.usedFormatted}`,
4836
+ `${yellow("Limit:")} ${result.limitFormatted}`,
4837
+ `${violet("Usage:")} ${pct}%`,
4838
+ ], { title: "💾 Storage Quota", color: cyan, padding: 2 }))
4839
+ }
4840
+ console.log()
4841
+ return
4842
+ }
4843
+
4844
+ case "/gdrive-search": {
4845
+ if (!arg) { console.log(dim("\n Usage: /gdrive-search <query>\n")); return }
4846
+ console.log()
4847
+ const gdrive = new GDriveBackup()
4848
+ startSpinner("Searching")
4849
+ const result = await gdrive.searchFiles(arg.trim())
4850
+ stopSpinner()
4851
+ if (result.success && result.files.length > 0) {
4852
+ console.log(box(result.files.map(f => `${violet(f.name)} (${f.id})`), { title: "🔍 Search", color: violet, padding: 2 }))
4853
+ } else {
4854
+ console.log(dim(" No results\n"))
4855
+ }
4856
+ console.log()
4857
+ return
4858
+ }
4859
+
4860
+ case "/gdrive-history": {
4861
+ console.log()
4862
+ const gdrive = new GDriveBackup()
4863
+ const history = gdrive.getHistory()
4864
+ if (history.length > 0) {
4865
+ console.log(box(history.slice(-10).reverse().map(h =>
4866
+ `${dim(new Date(h.date).toLocaleString())} ${violet(h.name)} ${dim((h.size / 1024 / 1024).toFixed(1) + " MB")}`
4867
+ ), { title: "📋 Backup History", color: cyan, padding: 2 }))
4868
+ } else {
4869
+ console.log(dim(" No backup history\n"))
4870
+ }
4871
+ console.log()
4872
+ return
4873
+ }
4874
+
4875
+ // ═══════════════════════════════════════════════════
4876
+ // ADB (ANDROID DEBUG BRIDGE)
4877
+ // ═══════════════════════════════════════════════════
4878
+ case "/adb-devices": {
4879
+ console.log()
4880
+ const adb = new ADB()
4881
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found. Install Android SDK platform-tools\n")); return }
4882
+ const result = adb.getDevices()
4883
+ if (result.success && result.devices.length > 0) {
4884
+ console.log(box(result.devices.map(d => `${d.status === "device" ? green("✓") : yellow("?")} ${d.id} (${d.status})`), { title: "📱 ADB Devices", color: cyan, padding: 2 }))
4885
+ } else {
4886
+ console.log(dim(" No devices connected\n"))
4887
+ }
4888
+ console.log()
4889
+ return
4890
+ }
4891
+
4892
+ case "/adb-info": {
4893
+ console.log()
4894
+ const adb = new ADB()
4895
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4896
+ const devices = adb.getDevices()
4897
+ const serial = devices.success && devices.devices[0]?.id
4898
+ if (!serial) { console.log(dim(" No device connected\n")); return }
4899
+ const info = adb.getInfo(serial)
4900
+ if (info.success) {
4901
+ console.log(box([
4902
+ `${violet("Model:")} ${info.model}`,
4903
+ `${violet("Brand:")} ${info.brand}`,
4904
+ `${violet("Android:")} ${info.android} (SDK ${info.sdk})`,
4905
+ `${violet("Resolution:")} ${info.resolution}`,
4906
+ `${violet("Density:")} ${info.density}`,
4907
+ ], { title: `📱 ${serial}`, color: cyan, padding: 2 }))
4908
+ }
4909
+ console.log()
4910
+ return
4911
+ }
4912
+
4913
+ case "/adb-screenshot": {
4914
+ const adb = new ADB()
4915
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4916
+ const devices = adb.getDevices()
4917
+ const serial = devices.success && devices.devices[0]?.id
4918
+ startSpinner("Taking screenshot")
4919
+ const result = adb.screenshot(serial)
4920
+ stopSpinner()
4921
+ if (result.success) console.log(green(` ✓ Screenshot: ${result.path}\n`))
4922
+ else console.log(red(` ✗ ${result.error}\n`))
4923
+ return
4924
+ }
4925
+
4926
+ case "/adb-shell": {
4927
+ if (!arg) { console.log(dim("\n Usage: /adb-shell <command>\n")); return }
4928
+ const adb = new ADB()
4929
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4930
+ const devices = adb.getDevices()
4931
+ const serial = devices.success && devices.devices[0]?.id
4932
+ const result = adb.shell(serial, arg.trim())
4933
+ if (result.success) console.log(result.output + "\n")
4934
+ else console.log(red(` ✗ ${result.error}\n`))
4935
+ return
4936
+ }
4937
+
4938
+ case "/adb-tap": {
4939
+ if (!arg) { console.log(dim("\n Usage: /adb-tap <x> <y>\n")); return }
4940
+ const [x, y] = arg.split(" ").map(parseFloat)
4941
+ if (isNaN(x) || isNaN(y)) { console.log(dim("\n Usage: /adb-tap <x> <y>\n")); return }
4942
+ const adb = new ADB()
4943
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4944
+ const devices = adb.getDevices()
4945
+ const serial = devices.success && devices.devices[0]?.id
4946
+ const result = adb.tap(serial, x, y)
4947
+ if (result.success) console.log(green(` ✓ Tapped at (${x}, ${y})\n`))
4948
+ else console.log(red(` ✗ ${result.error}\n`))
4949
+ return
4950
+ }
4951
+
4952
+ case "/adb-swipe": {
4953
+ if (!arg) { console.log(dim("\n Usage: /adb-swipe <x1> <y1> <x2> <y2>\n")); return }
4954
+ const [x1, y1, x2, y2] = arg.split(" ").map(parseFloat)
4955
+ if ([x1, y1, x2, y2].some(n => isNaN(n))) { console.log(dim("\n Usage: /adb-swipe <x1> <y1> <x2> <y2>\n")); return }
4956
+ const adb = new ADB()
4957
+ const devices = adb.getDevices()
4958
+ const serial = devices.success && devices.devices[0]?.id
4959
+ const result = adb.swipe(serial, x1, y1, x2, y2)
4960
+ if (result.success) console.log(green(" ✓ Swiped\n"))
4961
+ else console.log(red(` ✗ ${result.error}\n`))
4962
+ return
4963
+ }
4964
+
4965
+ case "/adb-type": {
4966
+ if (!arg) { console.log(dim("\n Usage: /adb-type <text>\n")); return }
4967
+ const adb = new ADB()
4968
+ const devices = adb.getDevices()
4969
+ const serial = devices.success && devices.devices[0]?.id
4970
+ const result = adb.text(serial, arg)
4971
+ if (result.success) console.log(green(` ✓ Typed "${arg.slice(0, 30)}"\n`))
4972
+ else console.log(red(` ✗ ${result.error}\n`))
4973
+ return
4974
+ }
4975
+
4976
+ case "/adb-battery": {
4977
+ console.log()
4978
+ const adb = new ADB()
4979
+ const devices = adb.getDevices()
4980
+ const serial = devices.success && devices.devices[0]?.id
4981
+ if (!serial) { console.log(dim(" No device\n")); return }
4982
+ const result = adb.getBattery(serial)
4983
+ if (result.success) {
4984
+ console.log(box([
4985
+ `${green("Level:")} ${result.level}%`,
4986
+ `${yellow("Temperature:")} ${result.temperature}°C`,
4987
+ `${violet("Status:")} ${result.status}`,
4988
+ ], { title: "🔋 Battery", color: cyan, padding: 2 }))
4989
+ }
4990
+ console.log()
4991
+ return
4992
+ }
4993
+
4994
+ case "/adb-install": {
4995
+ if (!arg) { console.log(dim("\n Usage: /adb-install <path.apk>\n")); return }
4996
+ if (!fs.existsSync(arg.trim())) { console.log(red(` ✗ File not found: ${arg.trim()}\n`)); return }
4997
+ const adb = new ADB()
4998
+ const devices = adb.getDevices()
4999
+ const serial = devices.success && devices.devices[0]?.id
5000
+ startSpinner("Installing")
5001
+ const result = adb.installApp(serial, arg.trim())
5002
+ stopSpinner()
5003
+ if (result.success) console.log(green(" ✓ Installed\n"))
5004
+ else console.log(red(` ✗ ${result.error}\n`))
5005
+ return
5006
+ }
5007
+
5008
+ case "/adb-packages": {
5009
+ console.log()
5010
+ const adb = new ADB()
5011
+ const devices = adb.getDevices()
5012
+ const serial = devices.success && devices.devices[0]?.id
5013
+ if (!serial) { console.log(dim(" No device\n")); return }
5014
+ const filter = arg || ""
5015
+ const result = adb.listPackages(serial, filter)
5016
+ if (result.success && result.packages.length > 0) {
5017
+ console.log(box(result.packages.slice(0, 30).map(p => dim(p)), { title: `📦 Packages (${result.packages.length})`, color: violet, padding: 2 }))
5018
+ } else {
5019
+ console.log(dim(" No packages\n"))
5020
+ }
5021
+ console.log()
5022
+ return
5023
+ }
5024
+
5025
+ // ═══════════════════════════════════════════════════
5026
+ // HOME ASSISTANT
5027
+ // ═══════════════════════════════════════════════════
5028
+ case "/ha-setup": {
5029
+ if (!arg) { console.log(dim("\n Usage: /ha-setup <url> | <token>\n")); return }
5030
+ const [url, token] = arg.split("|").map(s => s.trim())
5031
+ if (!url || !token) { console.log(dim("\n Usage: /ha-setup <url> | <token>\n")); return }
5032
+ const ha = new HomeAssistant(url, token)
5033
+ startSpinner("Connecting")
5034
+ const ping = await ha.ping()
5035
+ stopSpinner()
5036
+ if (ping.success) {
5037
+ ha.configure(url, token)
5038
+ console.log(green(" ✓ Home Assistant configured!\n"))
5039
+ } else {
5040
+ console.log(red(` ✗ Connection failed\n`))
5041
+ }
5042
+ return
5043
+ }
5044
+
5045
+ case "/ha-status": {
5046
+ console.log()
5047
+ const ha = new HomeAssistant()
5048
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured. Use /ha-setup\n")); return }
5049
+ startSpinner("Getting info")
5050
+ const info = await ha.getAutoInfo()
5051
+ stopSpinner()
5052
+ if (info.success) {
5053
+ console.log(box([
5054
+ `${violet("HA Version:")} ${info.haVersion || "?"}`,
5055
+ `${violet("Location:")} ${info.locationName || "?"}`,
5056
+ `${violet("Timezone:")} ${info.timezone || "?"}`,
5057
+ ], { title: "🏠 Home Assistant", color: cyan, padding: 2 }))
5058
+ } else {
5059
+ console.log(red(` ✗ ${info.error}\n`))
5060
+ }
5061
+ console.log()
5062
+ return
5063
+ }
5064
+
5065
+ case "/ha-states": {
5066
+ console.log()
5067
+ const ha = new HomeAssistant()
5068
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5069
+ startSpinner("Fetching states")
5070
+ const result = await ha.getStates()
5071
+ stopSpinner()
5072
+ if (result.success) {
5073
+ const entries = result.data.map(s => `${s.state === "on" ? green("●") : s.state === "off" ? red("●") : dim("○")} ${dim(s.entity_id)}: ${white(s.state)}`)
5074
+ console.log(box(entries.slice(0, 25), { title: `🏠 States (${result.data.length})`, color: cyan, padding: 2 }))
5075
+ if (result.data.length > 25) console.log(dim(` ... and ${result.data.length - 25} more\n`))
5076
+ }
5077
+ console.log()
5078
+ return
5079
+ }
5080
+
5081
+ case "/ha-lights": {
5082
+ console.log()
5083
+ const ha = new HomeAssistant()
5084
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5085
+ const result = await ha.getLights()
5086
+ if (result.success && result.lights.length > 0) {
5087
+ console.log(box(result.lights.map(l =>
5088
+ `${l.state === "on" ? green("●") : red("●")} ${l.friendlyName || l.entity} ${dim(l.brightness ? `(${Math.round(l.brightness / 2.55)}%)` : "")}`
5089
+ ), { title: `💡 Lights (${result.lights.length})`, color: yellow, padding: 2 }))
5090
+ } else {
5091
+ console.log(dim(" No lights\n"))
5092
+ }
5093
+ console.log()
5094
+ return
5095
+ }
5096
+
5097
+ case "/ha-sensors": {
5098
+ console.log()
5099
+ const ha = new HomeAssistant()
5100
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5101
+ const result = await ha.getSensors()
5102
+ if (result.success && result.sensors.length > 0) {
5103
+ console.log(box(result.sensors.slice(0, 20).map(s =>
5104
+ `${s.friendlyName || s.entity}: ${white(s.state)} ${dim(s.unit || "")}`
5105
+ ), { title: `📡 Sensors (${result.sensors.length})`, color: cyan, padding: 2 }))
5106
+ } else {
5107
+ console.log(dim(" No sensors\n"))
5108
+ }
5109
+ console.log()
5110
+ return
5111
+ }
5112
+
5113
+ case "/ha-toggle": {
5114
+ if (!arg) { console.log(dim("\n Usage: /ha-toggle <entity_id>\n")); return }
5115
+ const ha = new HomeAssistant()
5116
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5117
+ const result = await ha.toggle(arg.trim())
5118
+ if (result.success) console.log(green(` ✓ Toggled ${arg.trim()}\n`))
5119
+ else console.log(red(` ✗ ${result.error}\n`))
5120
+ return
5121
+ }
5122
+
5123
+ case "/ha-on": {
5124
+ if (!arg) { console.log(dim("\n Usage: /ha-on <entity_id>\n")); return }
5125
+ const ha = new HomeAssistant()
5126
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5127
+ const result = await ha.turnOn(arg.trim())
5128
+ if (result.success) console.log(green(` ✓ Turned on ${arg.trim()}\n`))
5129
+ else console.log(red(` ✗ ${result.error}\n`))
5130
+ return
5131
+ }
5132
+
5133
+ case "/ha-off": {
5134
+ if (!arg) { console.log(dim("\n Usage: /ha-off <entity_id>\n")); return }
5135
+ const ha = new HomeAssistant()
5136
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5137
+ const result = await ha.turnOff(arg.trim())
5138
+ if (result.success) console.log(green(` ✓ Turned off ${arg.trim()}\n`))
5139
+ else console.log(red(` ✗ ${result.error}\n`))
5140
+ return
5141
+ }
5142
+
5143
+ case "/ha-climate": {
5144
+ console.log()
5145
+ const ha = new HomeAssistant()
5146
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5147
+ const result = await ha.getClimate()
5148
+ if (result.success && result.devices.length > 0) {
5149
+ console.log(box(result.devices.map(c =>
5150
+ `${c.friendlyName || c.entity}: ${c.state} ${dim(c.currentTemp ? `(${c.currentTemp}°C → ${c.temp}°C)` : "")}`
5151
+ ), { title: "🌡️ Climate", color: cyan, padding: 2 }))
5152
+ } else {
5153
+ console.log(dim(" No climate devices\n"))
5154
+ }
5155
+ console.log()
5156
+ return
5157
+ }
5158
+
5159
+ case "/ha-media": {
5160
+ console.log()
5161
+ const ha = new HomeAssistant()
5162
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5163
+ const result = await ha.getMediaPlayers()
5164
+ if (result.success && result.players.length > 0) {
5165
+ console.log(box(result.players.map(p =>
5166
+ `${p.state === "playing" ? green("▶") : p.state === "paused" ? yellow("⏸") : dim("⏹")} ${p.friendlyName || p.entity} ${dim(p.volume ? `(${Math.round(p.volume * 100)}%)` : "")}`
5167
+ ), { title: "📺 Media Players", color: violet, padding: 2 }))
5168
+ } else {
5169
+ console.log(dim(" No media players\n"))
5170
+ }
5171
+ console.log()
5172
+ return
5173
+ }
5174
+
5175
+ case "/ha-volume": {
5176
+ if (!arg) { console.log(dim("\n Usage: /ha-volume <entity_id> | <level 0-100>\n")); return }
5177
+ const [entity, levelStr] = arg.split("|").map(s => s.trim())
5178
+ const level = parseFloat(levelStr) / 100
5179
+ if (isNaN(level)) { console.log(dim("\n Usage: /ha-volume <entity_id> | <level 0-100>\n")); return }
5180
+ const ha = new HomeAssistant()
5181
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5182
+ const result = await ha.setVolume(entity, level)
5183
+ if (result.success) console.log(green(` ✓ Volume set to ${levelStr}%\n`))
5184
+ else console.log(red(` ✗ ${result.error}\n`))
5185
+ return
5186
+ }
5187
+
5188
+ case "/ha-cover": {
5189
+ if (!arg) { console.log(dim("\n Usage: /ha-cover <entity_id> | <open|close|stop|position>\n")); return }
5190
+ const [entityId, action] = arg.split("|").map(s => s.trim())
5191
+ const ha = new HomeAssistant()
5192
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5193
+ const actions = { open: () => ha.openCover(entityId), close: () => ha.closeCover(entityId), stop: () => ha.stopCover(entityId) }
5194
+ const fn = actions[action]
5195
+ if (!fn) {
5196
+ const pos = parseInt(action)
5197
+ if (!isNaN(pos)) {
5198
+ const result = await ha.setCoverPosition(entityId, pos)
5199
+ if (result.success) console.log(green(` ✓ Cover position set to ${pos}%\n`))
5200
+ else console.log(red(` ✗ ${result.error}\n`))
5201
+ return
5202
+ }
5203
+ console.log(dim("\n Usage: /ha-cover <entity_id> | <open|close|stop|0-100>\n")); return
5204
+ }
5205
+ const result = await fn()
5206
+ if (result.success) console.log(green(` ✓ Cover ${action}: ${entityId}\n`))
5207
+ else console.log(red(` ✗ ${result.error}\n`))
5208
+ return
5209
+ }
5210
+
3847
5211
  default:
3848
5212
  console.log(dim(`\n Неизвестная команда: ${cmd}. Смотри /help\n`))
3849
5213
  }