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.
@@ -22,6 +22,10 @@ import { runSubagent, listSubagents, parseAgentCommand, SUBAGENTS } from "./suba
22
22
  import { mcp, MCP_COMMANDS } from "./mcp.mjs"
23
23
  import { generatePresentation, createPresentationFromTopic, AVAILABLE_THEMES, exportToPDF } from "./presentations.mjs"
24
24
  import { AutonomousAgent } from "./autonomous-agent.mjs"
25
+ import { ScreenMonitor } from "./screen-monitor.mjs"
26
+ import { ChromeBrowser } from "./browser-control.mjs"
27
+ import { GitAPI } from "./git-api.mjs"
28
+ import { WebParser } from "./web-parser.mjs"
25
29
  import {
26
30
  buildRepoMap, buildProjectContext, compressContext,
27
31
  loadSpec, generateSpecTemplate,
@@ -41,7 +45,7 @@ import {
41
45
  generateAdminCode, listAuthorizedUsers,
42
46
  } from "./telegram-bot.mjs"
43
47
 
44
- const VERSION = "5.1.2"
48
+ const VERSION = "5.2.0"
45
49
  const CONFIG_DIR = path.join(os.homedir(), ".stella")
46
50
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json")
47
51
  const HISTORY_PATH = path.join(CONFIG_DIR, "history.json")
@@ -650,6 +654,44 @@ const COMMANDS = [
650
654
  ["/auto-dashboard", "создать dashboard с результатами"],
651
655
  ["/auto-history", "история выполненных задач"],
652
656
 
657
+ // Computer Vision Monitor
658
+ ["/monitor", "запустить мониторинг экрана (AI vision)"],
659
+ ["/monitor-stop", "остановить мониторинг"],
660
+ ["/monitor-status", "статус мониторинга"],
661
+ ["/monitor-alerts", "показать обнаруженные проблемы"],
662
+ ["/monitor-screenshot", "сделать скриншот и проанализировать"],
663
+
664
+ // Chrome DevTools Protocol
665
+ ["/browser", "открыть браузер и перейти на URL"],
666
+ ["/browser-click", "кликнуть по элементу (CSS selector)"],
667
+ ["/browser-type", "набрать текст в поле"],
668
+ ["/browser-scroll", "прокрутить страницу"],
669
+ ["/browser-screenshot", "скриншот текущей страницы"],
670
+ ["/browser-links", "показать все ссылки"],
671
+ ["/browser-forms", "показать формы на странице"],
672
+ ["/browser-close", "закрыть браузер"],
673
+
674
+ // Git API (GitHub/GitLab)
675
+ ["/git-issues", "список issues"],
676
+ ["/git-issue", "создать issue"],
677
+ ["/git-prs", "список Pull Requests"],
678
+ ["/git-pr-create", "создать Pull Request"],
679
+ ["/git-pr-review", "оставить ревью на PR"],
680
+ ["/git-pr-merge", "слить PR"],
681
+ ["/git-branches", "список веток"],
682
+ ["/git-releases", "список релизов"],
683
+ ["/git-release", "создать релиз"],
684
+
685
+ // Web Parser
686
+ ["/fetch", "загрузить и показать страницу"],
687
+ ["/fetch-text", "извлечь текст со страницы"],
688
+ ["/fetch-links", "извлечь все ссылки"],
689
+ ["/fetch-images", "извлечь все картинки"],
690
+ ["/fetch-forms", "извлечь формы"],
691
+ ["/fetch-seo", "SEO анализ страницы"],
692
+ ["/web-search", "поиск в интернете"],
693
+ ["/web-batch", "загрузить несколько страниц"],
694
+
653
695
  // Управление компьютером
654
696
  ["/open", "открыть приложение/файл/URL"],
655
697
  ["/app", "запустить приложение"],
@@ -775,6 +817,10 @@ async function handleCommand(line) {
775
817
  ["📱 Telegram", ["/tg", "/tg-stop", "/tg-notify", "/tg-sessions", "/tg-verify", "/tg-code", "/tg-users"]],
776
818
  ["🔀 Git Экосистема", ["/git-eco", "/git-pr", "/git-merge-auto"]],
777
819
  ["🤖 Autonomous Agent", ["/auto", "/auto-stop", "/auto-status", "/auto-dashboard", "/auto-history"]],
820
+ ["👁️ Computer Vision", ["/monitor", "/monitor-stop", "/monitor-status", "/monitor-alerts", "/monitor-screenshot"]],
821
+ ["🌐 Browser Control", ["/browser", "/browser-click", "/browser-type", "/browser-scroll", "/browser-screenshot", "/browser-links", "/browser-forms", "/browser-close"]],
822
+ ["🔗 Git API", ["/git-issues", "/git-issue", "/git-prs", "/git-pr-create", "/git-pr-review", "/git-pr-merge", "/git-branches", "/git-releases", "/git-release"]],
823
+ ["🔍 Web Parser", ["/fetch", "/fetch-text", "/fetch-links", "/fetch-images", "/fetch-forms", "/fetch-seo", "/web-search", "/web-batch"]],
778
824
  ["⚙️ Настройки", ["/help", "/model", "/clear", "/compact", "/cost", "/context", "/config", "/version", "/login", "/newkey", "/doctor", "/sessions", "/color", "/lang", "/shortcut"]],
779
825
  ]
780
826
  for (const [cat, cmds] of categories) {
@@ -3599,6 +3645,646 @@ async function handleCommand(line) {
3599
3645
  return
3600
3646
  }
3601
3647
 
3648
+ // ═══════════════════════════════════════════════════
3649
+ // COMPUTER VISION MONITOR
3650
+ // ═══════════════════════════════════════════════════
3651
+ case "/monitor": {
3652
+ console.log()
3653
+ const monitor = new ScreenMonitor()
3654
+
3655
+ if (monitor.state.running) {
3656
+ console.log(yellow(" Мониторинг уже запущен! Используй /monitor-stop для остановки.\n"))
3657
+ return
3658
+ }
3659
+
3660
+ const intervalSec = arg ? parseInt(arg) || 2 : 2
3661
+ const intervalMs = intervalSec * 1000
3662
+
3663
+ console.log(box([
3664
+ violet("👁️ COMPUTER VISION MONITOR"),
3665
+ "",
3666
+ white("Интервал: ") + cyan(`${intervalSec} секунды`),
3667
+ white("Режим: ") + cyan("автоматический скриншот + AI анализ"),
3668
+ "",
3669
+ dim("Агент будет делать скриншот каждые") + white(`${intervalSec}с`),
3670
+ dim("и анализировать через AI на наличие ошибок, крашей, алертов."),
3671
+ dim("Все обнаруженные проблемы будут сохранены в лог."),
3672
+ ], { color: violet, padding: 2 }))
3673
+ console.log()
3674
+
3675
+ const confirmMonitor = await question(" " + green("Запустить мониторинг? (y/n) › "))
3676
+ if (confirmMonitor.trim().toLowerCase() !== "y" && confirmMonitor.trim().toLowerCase() !== "д") {
3677
+ console.log(dim(" Отменено\n"))
3678
+ return
3679
+ }
3680
+
3681
+ const visionFn = async (imageBase64, recentContext) => {
3682
+ const contextStr = recentContext?.length > 0
3683
+ ? `\nRecent history:\n${recentContext.map(r => `- ${r.time}: ${r.summary} (${r.alerts} alerts)`).join("\n")}`
3684
+ : ""
3685
+
3686
+ const prompt = `Analyze this screenshot. Detect:
3687
+ 1. Error messages, crash dialogs, exception windows
3688
+ 2. Alert popups, warning notifications
3689
+ 3. Application crashes or "not responding" dialogs
3690
+ 4. System errors, BSOD, kernel panics
3691
+ 5. Any other issues or anomalies
3692
+
3693
+ ${contextStr}
3694
+
3695
+ Return JSON:
3696
+ {
3697
+ "summary": "one-line description of what's on screen",
3698
+ "text": "detailed analysis of what you see",
3699
+ "issues": ["list of issues found"],
3700
+ "severity": "none|info|warning|critical",
3701
+ "recommendations": ["what to do about issues"]
3702
+ }
3703
+
3704
+ If everything looks normal, set severity to "none" and issues to [].`
3705
+
3706
+ const model = getModel(state.model)
3707
+ const { text } = await generateText({
3708
+ model,
3709
+ messages: [{
3710
+ role: "user",
3711
+ content: [
3712
+ { type: "text", text: prompt },
3713
+ { type: "image", image: `data:image/png;base64,${imageBase64}` }
3714
+ ]
3715
+ }],
3716
+ maxTokens: 2000,
3717
+ })
3718
+
3719
+ try {
3720
+ let clean = text.trim()
3721
+ if (clean.startsWith("```")) clean = clean.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "")
3722
+ return JSON.parse(clean)
3723
+ } catch {
3724
+ return { summary: text.slice(0, 200), text, issues: [], severity: "none", recommendations: [] }
3725
+ }
3726
+ }
3727
+
3728
+ console.log()
3729
+ startSpinner("Запускаю мониторинг")
3730
+ const result = monitor.start(intervalMs, visionFn)
3731
+ stopSpinner()
3732
+
3733
+ if (result.success) {
3734
+ console.log(green(` ✓ Мониторинг запущен! Интервал: ${intervalSec}с`))
3735
+ console.log(dim(" Скриншоты сохраняются в ~/.stella/monitor/screenshots/"))
3736
+ console.log(dim(" Используй /monitor-stop для остановки"))
3737
+ console.log(dim(" Используй /monitor-status для проверки"))
3738
+ console.log(dim(" Используй /monitor-alerts для просмотра проблем\n"))
3739
+
3740
+ // Show first capture
3741
+ console.log(dim(" Делаю первый скриншот..."))
3742
+ const firstCapture = await monitor.captureAndAnalyze(visionFn)
3743
+ if (firstCapture.success) {
3744
+ console.log(green(` ✓ Скриншот: ${firstCapture.capture.sizeKB}KB`))
3745
+ if (firstCapture.capture.analysis?.summary) {
3746
+ console.log(dim(" Анализ: ") + white(firstCapture.capture.analysis.summary))
3747
+ }
3748
+ if (firstCapture.capture.alerts?.length > 0) {
3749
+ console.log(yellow(` ⚠️ Обнаружено проблем: ${firstCapture.capture.alerts.length}`))
3750
+ }
3751
+ }
3752
+ console.log()
3753
+ } else {
3754
+ console.log(red(` ✗ ${result.error}\n`))
3755
+ }
3756
+ return
3757
+ }
3758
+
3759
+ case "/monitor-stop": {
3760
+ console.log()
3761
+ const monitor = new ScreenMonitor()
3762
+ if (!monitor.state.running) {
3763
+ console.log(dim(" Мониторинг не запущен\n"))
3764
+ return
3765
+ }
3766
+ monitor.stop()
3767
+ console.log(green(" ✓ Мониторинг остановлен"))
3768
+ console.log(dim(` Всего скриншотов: ${monitor.state.totalCaptures}`))
3769
+ console.log(dim(` Всего алертов: ${monitor.state.totalAlerts}`))
3770
+ console.log()
3771
+ return
3772
+ }
3773
+
3774
+ case "/monitor-status": {
3775
+ console.log()
3776
+ const monitor = new ScreenMonitor()
3777
+ const status = monitor.getStatus()
3778
+
3779
+ if (!status.running && status.totalCaptures === 0) {
3780
+ console.log(dim(" Мониторинг не запущен. Используй /monitor для запуска.\n"))
3781
+ return
3782
+ }
3783
+
3784
+ const items = [
3785
+ dim("Статус: ") + (status.running ? green("● RUNNING") : red("○ STOPPED")),
3786
+ dim("Интервал: ") + cyan(`every ${status.interval / 1000}s`),
3787
+ dim("Начато: ") + white(status.startedAt ? new Date(status.startedAt).toLocaleString() : "—"),
3788
+ dim("Скриншотов: ") + cyan(String(status.totalCaptures)),
3789
+ dim("Алертов: ") + (status.totalAlerts > 0 ? red(String(status.totalAlerts)) : green("0")),
3790
+ dim("PID: ") + white(String(status.pid || "—")),
3791
+ ]
3792
+
3793
+ if (status.recentAlerts.length > 0) {
3794
+ items.push("", violet("Последние алерты:"))
3795
+ for (const a of status.recentAlerts.slice(-5)) {
3796
+ const icon = a.severity === "critical" ? red("🔴") : yellow("🟡")
3797
+ items.push(` ${icon} [${new Date(a.timestamp).toLocaleTimeString()}] ${a.type}`)
3798
+ if (a.description) items.push(dim(` ${a.description.slice(0, 100)}`))
3799
+ }
3800
+ }
3801
+
3802
+ if (status.recentCaptures.length > 0) {
3803
+ items.push("", dim("Последние скриншоты:"))
3804
+ for (const c of status.recentCaptures) {
3805
+ const icon = c.alertCount > 0 ? yellow("⚠️") : green("✅")
3806
+ items.push(` ${icon} [${new Date(c.time).toLocaleTimeString()}] ${c.summary}`)
3807
+ }
3808
+ }
3809
+
3810
+ console.log(box(items, { title: "👁️ Monitor Status", color: violet, padding: 2 }))
3811
+ console.log()
3812
+ return
3813
+ }
3814
+
3815
+ case "/monitor-alerts": {
3816
+ console.log()
3817
+ const monitor = new ScreenMonitor()
3818
+ const alerts = monitor.getAlerts()
3819
+
3820
+ if (alerts.length === 0) {
3821
+ console.log(dim(" Нет обнаруженных проблем. Всё чисто! ✅\n"))
3822
+ return
3823
+ }
3824
+
3825
+ console.log(box([
3826
+ violet(`Всего алертов: ${alerts.length}`),
3827
+ "",
3828
+ ...alerts.slice(-15).map(a => {
3829
+ const icon = a.severity === "critical" ? red("🔴") : yellow("🟡")
3830
+ return `${icon} [${new Date(a.timestamp).toLocaleTimeString()}] ${a.type}`
3831
+ }),
3832
+ ], { title: "⚠️ Monitor Alerts", color: yellow, padding: 2 }))
3833
+ console.log()
3834
+ return
3835
+ }
3836
+
3837
+ case "/monitor-screenshot": {
3838
+ console.log()
3839
+ startSpinner("Делаю скриншот")
3840
+ const monitor = new ScreenMonitor()
3841
+
3842
+ const visionFn = async (imageBase64) => {
3843
+ const prompt = `Analyze this screenshot in detail. What's on screen? Any errors, issues, or anomalies? Return JSON with "summary", "text", "issues", "severity".`
3844
+ const model = getModel(state.model)
3845
+ const { text } = await generateText({
3846
+ model,
3847
+ messages: [{
3848
+ role: "user",
3849
+ content: [
3850
+ { type: "text", text: prompt },
3851
+ { type: "image", image: `data:image/png;base64,${imageBase64}` }
3852
+ ]
3853
+ }],
3854
+ maxTokens: 2000,
3855
+ })
3856
+ try { return JSON.parse(text) } catch { return { summary: text.slice(0, 200), text } }
3857
+ }
3858
+
3859
+ const capture = await monitor.captureAndAnalyze(visionFn)
3860
+ stopSpinner()
3861
+
3862
+ if (capture.success) {
3863
+ console.log(box([
3864
+ green("✓ Скриншот сделан!"),
3865
+ "",
3866
+ dim("Файл: ") + cyan(capture.capture.path),
3867
+ dim("Размер: ") + white(`${capture.capture.sizeKB}KB`),
3868
+ "",
3869
+ violet("Анализ:"),
3870
+ white(capture.capture.analysis?.summary || "analyzing..."),
3871
+ capture.capture.analysis?.text ? dim(capture.capture.analysis.text.slice(0, 500)) : "",
3872
+ "",
3873
+ capture.capture.alerts?.length > 0
3874
+ ? yellow(`⚠️ Обнаружено проблем: ${capture.capture.alerts.length}`)
3875
+ : green("✅ Проблем не обнаружено"),
3876
+ ], { title: "📸 Screenshot Analysis", color: cyan, padding: 2 }))
3877
+ } else {
3878
+ console.log(red(` ✗ ${capture.error}\n`))
3879
+ }
3880
+ console.log()
3881
+ return
3882
+ }
3883
+
3884
+ // ═══════════════════════════════════════════════════
3885
+ // CHROME DEVTOOLS PROTOCOL (BROWSER CONTROL)
3886
+ // ═══════════════════════════════════════════════════
3887
+ case "/browser": {
3888
+ console.log()
3889
+ const browser = new ChromeBrowser()
3890
+ const url = arg || "about:blank"
3891
+ startSpinner("Launching browser")
3892
+ const launch = await browser.launch(false)
3893
+ stopSpinner()
3894
+ if (!launch.success) { console.log(red(` ✗ ${launch.error}\n`)); return }
3895
+ startSpinner("Connecting")
3896
+ const conn = await browser.connect()
3897
+ stopSpinner()
3898
+ if (!conn.success) { console.log(red(` ✗ ${conn.error}\n`)); return }
3899
+ startSpinner(`Navigating to ${url}`)
3900
+ await browser.navigate(url)
3901
+ stopSpinner()
3902
+ const title = await browser.getTitle()
3903
+ console.log(box([
3904
+ green("✓ Browser launched!"), "",
3905
+ dim("URL: ") + cyan(url),
3906
+ dim("Title: ") + white(title.title || "—"), "",
3907
+ dim("Commands: /browser-click, /browser-type, /browser-scroll"),
3908
+ dim(" /browser-screenshot, /browser-links, /browser-close"),
3909
+ ], { title: "🌐 Browser", color: cyan, padding: 2 }))
3910
+ console.log()
3911
+ return
3912
+ }
3913
+
3914
+ case "/browser-click": {
3915
+ if (!arg) { console.log(dim("\n Usage: /browser-click <css-selector>\n")); return }
3916
+ const browser = new ChromeBrowser()
3917
+ const result = await browser.click(arg.trim())
3918
+ if (result.success) console.log(green(` ✓ Clicked at (${result.x}, ${result.y})\n`))
3919
+ else console.log(red(` ✗ ${result.error}\n`))
3920
+ return
3921
+ }
3922
+
3923
+ case "/browser-type": {
3924
+ if (!arg) { console.log(dim("\n Usage: /browser-type <selector> <text>\n")); return }
3925
+ const [sel, ...textParts] = arg.split(" ")
3926
+ const text = textParts.join(" ")
3927
+ const browser = new ChromeBrowser()
3928
+ const result = await browser.type(sel, text)
3929
+ if (result.success) console.log(green(` ✓ Typed "${text}" in ${sel}\n`))
3930
+ else console.log(red(` ✗ ${result.error}\n`))
3931
+ return
3932
+ }
3933
+
3934
+ case "/browser-scroll": {
3935
+ const browser = new ChromeBrowser()
3936
+ const delta = arg ? parseInt(arg) || 300 : 300
3937
+ await browser.scroll(0, delta)
3938
+ console.log(green(` ✓ Scrolled ${delta}px\n`))
3939
+ return
3940
+ }
3941
+
3942
+ case "/browser-screenshot": {
3943
+ const browser = new ChromeBrowser()
3944
+ const ss = await browser.screenshot()
3945
+ if (ss.success) {
3946
+ console.log(green(` ✓ Screenshot saved: ${ss.path}\n`))
3947
+ try { execSync(`start "" "${ss.path}"`, { shell: "cmd.exe" }) } catch {}
3948
+ } else {
3949
+ console.log(red(` ✗ ${ss.error}\n`))
3950
+ }
3951
+ return
3952
+ }
3953
+
3954
+ case "/browser-links": {
3955
+ const browser = new ChromeBrowser()
3956
+ const links = await browser.getLinks()
3957
+ if (links.success && links.links.length > 0) {
3958
+ console.log(box([
3959
+ ...links.links.slice(0, 20).map(l => cyan(l.text.slice(0, 50)) + dim(` → ${l.href.slice(0, 60)}`)),
3960
+ links.links.length > 20 ? dim(`... and ${links.links.length - 20} more`) : "",
3961
+ ].filter(Boolean), { title: `🔗 Links (${links.links.length})`, color: cyan, padding: 2 }))
3962
+ } else {
3963
+ console.log(dim(" No links found\n"))
3964
+ }
3965
+ console.log()
3966
+ return
3967
+ }
3968
+
3969
+ case "/browser-forms": {
3970
+ const browser = new ChromeBrowser()
3971
+ const forms = await browser.getForms()
3972
+ if (forms.success && forms.forms.length > 0) {
3973
+ console.log(box([
3974
+ ...forms.forms.map((f, i) => [
3975
+ violet(`Form ${i + 1}: ${f.method} ${f.action?.slice(0, 50)}`),
3976
+ ...f.inputs.map(inp => dim(` ${inp.name || inp.type}: ${inp.placeholder || ""}`)),
3977
+ ].join("\n")),
3978
+ ], { title: `📝 Forms (${forms.forms.length})`, color: violet, padding: 2 }))
3979
+ } else {
3980
+ console.log(dim(" No forms found\n"))
3981
+ }
3982
+ console.log()
3983
+ return
3984
+ }
3985
+
3986
+ case "/browser-close": {
3987
+ const browser = new ChromeBrowser()
3988
+ browser.close()
3989
+ console.log(green(" ✓ Browser closed\n"))
3990
+ return
3991
+ }
3992
+
3993
+ // ═══════════════════════════════════════════════════
3994
+ // GIT API (GitHub/GitLab)
3995
+ // ═══════════════════════════════════════════════════
3996
+ case "/git-issues": {
3997
+ console.log()
3998
+ const api = new GitAPI()
3999
+ if (!api.isConfigured()) { console.log(red(" ✗ No GitHub/GitLab remote found\n")); return }
4000
+ if (!api.hasToken()) { console.log(yellow(" ⚠ No token. Set GITHUB_TOKEN or GITLAB_TOKEN\n")); return }
4001
+ startSpinner("Fetching issues")
4002
+ const result = await api.listIssues()
4003
+ stopSpinner()
4004
+ if (result.success && result.issues.length > 0) {
4005
+ console.log(box([
4006
+ ...result.issues.slice(0, 15).map(i => {
4007
+ const num = i.number || i.iid
4008
+ const state = i.state === "open" ? green("●") : red("●")
4009
+ return `${state} ${violet(`#${num}`)} ${white(i.title?.slice(0, 60))}`
4010
+ }),
4011
+ ], { title: `📋 Issues (${result.issues.length})`, color: cyan, padding: 2 }))
4012
+ } else {
4013
+ console.log(dim(" No issues found\n"))
4014
+ }
4015
+ console.log()
4016
+ return
4017
+ }
4018
+
4019
+ case "/git-issue": {
4020
+ if (!arg) { console.log(dim("\n Usage: /git-issue <title>\n")); return }
4021
+ const api = new GitAPI()
4022
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4023
+ startSpinner("Creating issue")
4024
+ const result = await api.createIssue(arg.trim(), `Created by Stella Coder\n\n_${new Date().toISOString()}_`)
4025
+ stopSpinner()
4026
+ if (result.success) {
4027
+ console.log(green(` ✓ Issue created: ${result.url}\n`))
4028
+ } else {
4029
+ console.log(red(` ✗ Failed to create issue\n`))
4030
+ }
4031
+ return
4032
+ }
4033
+
4034
+ case "/git-prs": {
4035
+ console.log()
4036
+ const api = new GitAPI()
4037
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4038
+ startSpinner("Fetching PRs")
4039
+ const result = await api.listPRs()
4040
+ stopSpinner()
4041
+ if (result.success && result.prs.length > 0) {
4042
+ console.log(box([
4043
+ ...result.prs.slice(0, 15).map(pr => {
4044
+ const num = pr.number || pr.iid
4045
+ const state = pr.merged_at ? violet("merged") : pr.state === "open" ? green("open") : red("closed")
4046
+ return `${state} ${violet(`#${num}`)} ${white(pr.title?.slice(0, 60))}`
4047
+ }),
4048
+ ], { title: `🔀 Pull Requests (${result.prs.length})`, color: violet, padding: 2 }))
4049
+ } else {
4050
+ console.log(dim(" No PRs found\n"))
4051
+ }
4052
+ console.log()
4053
+ return
4054
+ }
4055
+
4056
+ case "/git-pr-create": {
4057
+ if (!arg) { console.log(dim("\n Usage: /git-pr-create <title>\n")); return }
4058
+ const api = new GitAPI()
4059
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4060
+ startSpinner("Creating PR")
4061
+ const result = await api.createPR(arg.trim(), `Created by Stella Coder\n\n_${new Date().toISOString()}_`)
4062
+ stopSpinner()
4063
+ if (result.success) {
4064
+ console.log(green(` ✓ PR created: ${result.url}\n`))
4065
+ } else {
4066
+ console.log(red(` ✗ Failed to create PR\n`))
4067
+ }
4068
+ return
4069
+ }
4070
+
4071
+ case "/git-pr-review": {
4072
+ if (!arg) { console.log(dim("\n Usage: /git-pr-review <number> <APPROVE|REQUEST_CHANGES|COMMENT>\n")); return }
4073
+ const [prNum, ...reviewParts] = arg.split(" ")
4074
+ const reviewEvent = reviewParts.join(" ") || "COMMENT"
4075
+ const api = new GitAPI()
4076
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4077
+ startSpinner("Submitting review")
4078
+ const result = await api.reviewPR(parseInt(prNum), reviewEvent, `Review by Stella Coder`)
4079
+ stopSpinner()
4080
+ if (result.success) console.log(green(` ✓ Review submitted on #${prNum}\n`))
4081
+ else console.log(red(` ✗ Failed\n`))
4082
+ return
4083
+ }
4084
+
4085
+ case "/git-pr-merge": {
4086
+ if (!arg) { console.log(dim("\n Usage: /git-pr-merge <number>\n")); return }
4087
+ const api = new GitAPI()
4088
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4089
+ startSpinner("Merging PR")
4090
+ const result = await api.mergePR(parseInt(arg))
4091
+ stopSpinner()
4092
+ if (result.success) console.log(green(` ✓ PR #${arg} merged\n`))
4093
+ else console.log(red(` ✗ Failed\n`))
4094
+ return
4095
+ }
4096
+
4097
+ case "/git-branches": {
4098
+ console.log()
4099
+ const api = new GitAPI()
4100
+ if (!api.isConfigured()) { console.log(red(" ✗ Not configured\n")); return }
4101
+ startSpinner("Fetching branches")
4102
+ const result = await api.listBranches()
4103
+ stopSpinner()
4104
+ if (result.success) {
4105
+ console.log(box(result.branches.map(b => cyan(b)), { title: "🌿 Branches", color: cyan, padding: 2 }))
4106
+ }
4107
+ console.log()
4108
+ return
4109
+ }
4110
+
4111
+ case "/git-releases": {
4112
+ console.log()
4113
+ const api = new GitAPI()
4114
+ if (!api.isConfigured()) { console.log(red(" ✗ Not configured\n")); return }
4115
+ startSpinner("Fetching releases")
4116
+ const result = await api.listReleases()
4117
+ stopSpinner()
4118
+ if (result.success && result.releases.length > 0) {
4119
+ console.log(box([
4120
+ ...result.releases.slice(0, 10).map(r => green(r.tag_name) + dim(` — ${r.name || ""}`)),
4121
+ ], { title: `📦 Releases (${result.releases.length})`, color: green, padding: 2 }))
4122
+ } else {
4123
+ console.log(dim(" No releases found\n"))
4124
+ }
4125
+ console.log()
4126
+ return
4127
+ }
4128
+
4129
+ case "/git-release": {
4130
+ if (!arg) { console.log(dim("\n Usage: /git-release <tag> [name]\n")); return }
4131
+ const [tag, ...nameParts] = arg.split(" ")
4132
+ const name = nameParts.join(" ") || tag
4133
+ const api = new GitAPI()
4134
+ if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
4135
+ startSpinner("Creating release")
4136
+ const result = await api.createRelease(tag, name, `Release ${tag} created by Stella Coder`)
4137
+ stopSpinner()
4138
+ if (result.success) {
4139
+ console.log(green(` ✓ Release created: ${result.url}\n`))
4140
+ } else {
4141
+ console.log(red(` ✗ Failed\n`))
4142
+ }
4143
+ return
4144
+ }
4145
+
4146
+ // ═══════════════════════════════════════════════════
4147
+ // WEB PARSER
4148
+ // ═══════════════════════════════════════════════════
4149
+ case "/fetch": {
4150
+ if (!arg) { console.log(dim("\n Usage: /fetch <url>\n")); return }
4151
+ console.log()
4152
+ const parser = new WebParser()
4153
+ startSpinner("Fetching page")
4154
+ const result = await parser.fetchAndParse(arg.trim())
4155
+ stopSpinner()
4156
+ if (result.success) {
4157
+ console.log(box([
4158
+ green(`✓ ${result.page.title || "No title"}`),
4159
+ dim(`URL: ${result.page.url}`),
4160
+ dim(`Length: ${result.page.text?.length || 0} chars`),
4161
+ "",
4162
+ result.page.text?.slice(0, 500) || "No text content",
4163
+ result.page.text?.length > 500 ? dim("\n... (truncated)") : "",
4164
+ ], { title: "🌐 Fetched Page", color: cyan, padding: 2 }))
4165
+ } else {
4166
+ console.log(red(` ✗ ${result.error}\n`))
4167
+ }
4168
+ console.log()
4169
+ return
4170
+ }
4171
+
4172
+ case "/fetch-text": {
4173
+ if (!arg) { console.log(dim("\n Usage: /fetch-text <url>\n")); return }
4174
+ const parser = new WebParser()
4175
+ const result = await parser.fetchAndParse(arg.trim())
4176
+ if (result.success) {
4177
+ console.log(result.page.text || "No text")
4178
+ } else {
4179
+ console.log(red(` ✗ ${result.error}`))
4180
+ }
4181
+ console.log()
4182
+ return
4183
+ }
4184
+
4185
+ case "/fetch-links": {
4186
+ if (!arg) { console.log(dim("\n Usage: /fetch-links <url>\n")); return }
4187
+ const parser = new WebParser()
4188
+ const result = await parser.fetchAndParse(arg.trim())
4189
+ if (result.success && result.page.links?.length > 0) {
4190
+ 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 }))
4191
+ } else {
4192
+ console.log(dim(" No links found\n"))
4193
+ }
4194
+ console.log()
4195
+ return
4196
+ }
4197
+
4198
+ case "/fetch-images": {
4199
+ if (!arg) { console.log(dim("\n Usage: /fetch-images <url>\n")); return }
4200
+ const parser = new WebParser()
4201
+ const result = await parser.fetchAndParse(arg.trim())
4202
+ if (result.success && result.page.images?.length > 0) {
4203
+ console.log(box(result.page.images.slice(0, 20).map(i => {
4204
+ const src = i.src?.length > 80 ? i.src.slice(0, 80) + "..." : i.src
4205
+ return violet(src) + dim(` alt="${i.alt || ""}"`)
4206
+ }), { title: `🖼️ Images (${result.page.images.length})`, color: violet, padding: 2 }))
4207
+ } else {
4208
+ console.log(dim(" No images found\n"))
4209
+ }
4210
+ console.log()
4211
+ return
4212
+ }
4213
+
4214
+ case "/fetch-forms": {
4215
+ if (!arg) { console.log(dim("\n Usage: /fetch-forms <url>\n")); return }
4216
+ const parser = new WebParser()
4217
+ const result = await parser.fetchAndParse(arg.trim())
4218
+ if (result.success && result.page.forms?.length > 0) {
4219
+ console.log(box(result.page.forms.map((f, i) => [
4220
+ violet(`Form ${i + 1}: ${f.method} ${f.action}`),
4221
+ ...f.fields.map(field => dim(` ${field.name || field.type}: ${field.placeholder || ""}`)),
4222
+ ].join("\n")), { title: `📝 Forms (${result.page.forms.length})`, color: violet, padding: 2 }))
4223
+ } else {
4224
+ console.log(dim(" No forms found\n"))
4225
+ }
4226
+ console.log()
4227
+ return
4228
+ }
4229
+
4230
+ case "/fetch-seo": {
4231
+ if (!arg) { console.log(dim("\n Usage: /fetch-seo <url>\n")); return }
4232
+ console.log()
4233
+ const parser = new WebParser()
4234
+ const result = await parser.fetchAndParse(arg.trim())
4235
+ if (!result.success) { console.log(red(` ✗ ${result.error}\n`)); return }
4236
+ const seo = parser.analyzeSEO(result.page)
4237
+ console.log(box([
4238
+ `Score: ${seo.score}/100`,
4239
+ "",
4240
+ ...seo.checks.map(c => `${c.pass ? green("✓") : red("✗")} ${c.label}: ${dim(c.detail)}`),
4241
+ "",
4242
+ ...seo.recommendations.map(r => dim(`• ${r}`)),
4243
+ ], { title: `🔍 SEO Analysis`, color: seo.score >= 70 ? green : yellow, padding: 2 }))
4244
+ console.log()
4245
+ return
4246
+ }
4247
+
4248
+ case "/web-search": {
4249
+ if (!arg) { console.log(dim("\n Usage: /web-search <query>\n")); return }
4250
+ console.log()
4251
+ const parser = new WebParser()
4252
+ startSpinner("Searching")
4253
+ const results = await parser.search(arg.trim())
4254
+ stopSpinner()
4255
+ if (results.length > 0) {
4256
+ console.log(box(results.slice(0, 8).map((r, i) => [
4257
+ violet(`${i + 1}. `) + white(r.title),
4258
+ dim(` ${r.url}`),
4259
+ dim(` ${r.snippet?.slice(0, 100) || ""}`),
4260
+ ].join("\n")), { title: `🔍 Search Results (${results.length})`, color: violet, padding: 2 }))
4261
+ } else {
4262
+ console.log(dim(" No results found\n"))
4263
+ }
4264
+ console.log()
4265
+ return
4266
+ }
4267
+
4268
+ case "/web-batch": {
4269
+ if (!arg) { console.log(dim("\n Usage: /web-batch <url1> <url2> ...\n")); return }
4270
+ const urls = arg.split(/\s+/).filter(Boolean)
4271
+ console.log()
4272
+ const parser = new WebParser()
4273
+ startSpinner(`Fetching ${urls.length} pages`)
4274
+ const results = await parser.batchFetch(urls)
4275
+ stopSpinner()
4276
+ const ok = results.filter(r => r.success)
4277
+ const fail = results.filter(r => !r.success)
4278
+ console.log(box([
4279
+ green(`✓ Fetched: ${ok.length}`),
4280
+ fail.length > 0 ? red(`✗ Failed: ${fail.length}`) : "",
4281
+ "",
4282
+ ...ok.map(r => dim(`• ${r.page?.title || r.page?.url}`)),
4283
+ ].filter(Boolean), { title: "📦 Batch Fetch", color: cyan, padding: 2 }))
4284
+ console.log()
4285
+ return
4286
+ }
4287
+
3602
4288
  default:
3603
4289
  console.log(dim(`\n Неизвестная команда: ${cmd}. Смотри /help\n`))
3604
4290
  }