stella-coder 3.9.1 → 4.0.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.
Files changed (235) hide show
  1. package/.mcp.json +11 -0
  2. package/.opencode/skills/banner-design/SKILL.md +196 -0
  3. package/.opencode/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
  4. package/.opencode/skills/brand/SKILL.md +97 -0
  5. package/.opencode/skills/brand/references/approval-checklist.md +169 -0
  6. package/.opencode/skills/brand/references/asset-organization.md +157 -0
  7. package/.opencode/skills/brand/references/brand-guideline-template.md +140 -0
  8. package/.opencode/skills/brand/references/color-palette-management.md +186 -0
  9. package/.opencode/skills/brand/references/consistency-checklist.md +94 -0
  10. package/.opencode/skills/brand/references/logo-usage-rules.md +185 -0
  11. package/.opencode/skills/brand/references/messaging-framework.md +85 -0
  12. package/.opencode/skills/brand/references/typography-specifications.md +214 -0
  13. package/.opencode/skills/brand/references/update.md +118 -0
  14. package/.opencode/skills/brand/references/visual-identity.md +96 -0
  15. package/.opencode/skills/brand/references/voice-framework.md +88 -0
  16. package/.opencode/skills/brand/scripts/extract-colors.cjs +341 -0
  17. package/.opencode/skills/brand/scripts/inject-brand-context.cjs +349 -0
  18. package/.opencode/skills/brand/scripts/sync-brand-to-tokens.cjs +248 -0
  19. package/.opencode/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +52 -0
  20. package/.opencode/skills/brand/scripts/validate-asset.cjs +387 -0
  21. package/.opencode/skills/brand/templates/brand-guidelines-starter.md +275 -0
  22. package/.opencode/skills/design/SKILL.md +313 -0
  23. package/.opencode/skills/design/data/cip/deliverables.csv +51 -0
  24. package/.opencode/skills/design/data/cip/industries.csv +21 -0
  25. package/.opencode/skills/design/data/cip/mockup-contexts.csv +21 -0
  26. package/.opencode/skills/design/data/cip/styles.csv +21 -0
  27. package/.opencode/skills/design/data/icon/styles.csv +16 -0
  28. package/.opencode/skills/design/data/logo/colors.csv +56 -0
  29. package/.opencode/skills/design/data/logo/industries.csv +56 -0
  30. package/.opencode/skills/design/data/logo/styles.csv +56 -0
  31. package/.opencode/skills/design/references/banner-sizes-and-styles.md +118 -0
  32. package/.opencode/skills/design/references/cip-deliverable-guide.md +95 -0
  33. package/.opencode/skills/design/references/cip-design.md +121 -0
  34. package/.opencode/skills/design/references/cip-prompt-engineering.md +84 -0
  35. package/.opencode/skills/design/references/cip-style-guide.md +68 -0
  36. package/.opencode/skills/design/references/design-routing.md +207 -0
  37. package/.opencode/skills/design/references/icon-design.md +122 -0
  38. package/.opencode/skills/design/references/logo-color-psychology.md +101 -0
  39. package/.opencode/skills/design/references/logo-design.md +92 -0
  40. package/.opencode/skills/design/references/logo-prompt-engineering.md +158 -0
  41. package/.opencode/skills/design/references/logo-style-guide.md +109 -0
  42. package/.opencode/skills/design/references/slides-copywriting-formulas.md +84 -0
  43. package/.opencode/skills/design/references/slides-create.md +4 -0
  44. package/.opencode/skills/design/references/slides-html-template.md +295 -0
  45. package/.opencode/skills/design/references/slides-layout-patterns.md +137 -0
  46. package/.opencode/skills/design/references/slides-strategies.md +94 -0
  47. package/.opencode/skills/design/references/slides.md +42 -0
  48. package/.opencode/skills/design/references/social-photos-design.md +329 -0
  49. package/.opencode/skills/design/scripts/cip/core.py +215 -0
  50. package/.opencode/skills/design/scripts/cip/generate.py +484 -0
  51. package/.opencode/skills/design/scripts/cip/render-html.py +424 -0
  52. package/.opencode/skills/design/scripts/cip/search.py +127 -0
  53. package/.opencode/skills/design/scripts/icon/generate.py +487 -0
  54. package/.opencode/skills/design/scripts/logo/core.py +175 -0
  55. package/.opencode/skills/design/scripts/logo/generate.py +362 -0
  56. package/.opencode/skills/design/scripts/logo/search.py +114 -0
  57. package/.opencode/skills/design-system/SKILL.md +244 -0
  58. package/.opencode/skills/design-system/data/slide-backgrounds.csv +11 -0
  59. package/.opencode/skills/design-system/data/slide-charts.csv +26 -0
  60. package/.opencode/skills/design-system/data/slide-color-logic.csv +14 -0
  61. package/.opencode/skills/design-system/data/slide-copy.csv +26 -0
  62. package/.opencode/skills/design-system/data/slide-layout-logic.csv +16 -0
  63. package/.opencode/skills/design-system/data/slide-layouts.csv +26 -0
  64. package/.opencode/skills/design-system/data/slide-strategies.csv +16 -0
  65. package/.opencode/skills/design-system/data/slide-typography.csv +15 -0
  66. package/.opencode/skills/design-system/references/component-specs.md +236 -0
  67. package/.opencode/skills/design-system/references/component-tokens.md +214 -0
  68. package/.opencode/skills/design-system/references/primitive-tokens.md +203 -0
  69. package/.opencode/skills/design-system/references/semantic-tokens.md +215 -0
  70. package/.opencode/skills/design-system/references/states-and-variants.md +241 -0
  71. package/.opencode/skills/design-system/references/tailwind-integration.md +251 -0
  72. package/.opencode/skills/design-system/references/token-architecture.md +224 -0
  73. package/.opencode/skills/design-system/scripts/embed-tokens.cjs +99 -0
  74. package/.opencode/skills/design-system/scripts/fetch-background.py +317 -0
  75. package/.opencode/skills/design-system/scripts/generate-slide.py +770 -0
  76. package/.opencode/skills/design-system/scripts/generate-tokens.cjs +205 -0
  77. package/.opencode/skills/design-system/scripts/html-token-validator.py +327 -0
  78. package/.opencode/skills/design-system/scripts/search-slides.py +218 -0
  79. package/.opencode/skills/design-system/scripts/slide-token-validator.py +35 -0
  80. package/.opencode/skills/design-system/scripts/slide_search_core.py +453 -0
  81. package/.opencode/skills/design-system/scripts/tests/test_validate_tokens.py +48 -0
  82. package/.opencode/skills/design-system/scripts/validate-tokens.cjs +246 -0
  83. package/.opencode/skills/design-system/templates/design-tokens-starter.json +143 -0
  84. package/.opencode/skills/slides/SKILL.md +40 -0
  85. package/.opencode/skills/slides/references/copywriting-formulas.md +84 -0
  86. package/.opencode/skills/slides/references/create.md +4 -0
  87. package/.opencode/skills/slides/references/html-template.md +295 -0
  88. package/.opencode/skills/slides/references/layout-patterns.md +137 -0
  89. package/.opencode/skills/slides/references/slide-strategies.md +94 -0
  90. package/.opencode/skills/ui-styling/LICENSE.txt +202 -0
  91. package/.opencode/skills/ui-styling/SKILL.md +324 -0
  92. package/.opencode/skills/ui-styling/references/canvas-design-system.md +320 -0
  93. package/.opencode/skills/ui-styling/references/shadcn-accessibility.md +471 -0
  94. package/.opencode/skills/ui-styling/references/shadcn-components.md +424 -0
  95. package/.opencode/skills/ui-styling/references/shadcn-theming.md +373 -0
  96. package/.opencode/skills/ui-styling/references/tailwind-customization.md +483 -0
  97. package/.opencode/skills/ui-styling/references/tailwind-responsive.md +382 -0
  98. package/.opencode/skills/ui-styling/references/tailwind-utilities.md +455 -0
  99. package/.opencode/skills/ui-styling/scripts/requirements.txt +17 -0
  100. package/.opencode/skills/ui-styling/scripts/shadcn_add.py +308 -0
  101. package/.opencode/skills/ui-styling/scripts/tailwind_config_gen.py +473 -0
  102. package/.opencode/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
  103. package/.opencode/skills/ui-styling/scripts/tests/requirements.txt +3 -0
  104. package/.opencode/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  105. package/.opencode/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +394 -0
  106. package/.opencode/skills/ui-ux-pro-max/SKILL.md +396 -0
  107. package/.opencode/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
  108. package/.opencode/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
  109. package/.opencode/skills/ui-ux-pro-max/data/charts.csv +26 -0
  110. package/.opencode/skills/ui-ux-pro-max/data/colors.csv +193 -0
  111. package/.opencode/skills/ui-ux-pro-max/data/design.csv +1776 -0
  112. package/.opencode/skills/ui-ux-pro-max/data/draft.csv +1779 -0
  113. package/.opencode/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
  114. package/.opencode/skills/ui-ux-pro-max/data/icons.csv +106 -0
  115. package/.opencode/skills/ui-ux-pro-max/data/landing.csv +35 -0
  116. package/.opencode/skills/ui-ux-pro-max/data/motion.csv +17 -0
  117. package/.opencode/skills/ui-ux-pro-max/data/products.csv +193 -0
  118. package/.opencode/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  119. package/.opencode/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
  120. package/.opencode/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  121. package/.opencode/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
  122. package/.opencode/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  123. package/.opencode/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  124. package/.opencode/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
  125. package/.opencode/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  126. package/.opencode/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
  127. package/.opencode/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  128. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +71 -0
  129. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  130. package/.opencode/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  131. package/.opencode/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  132. package/.opencode/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  133. package/.opencode/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  134. package/.opencode/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  135. package/.opencode/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
  136. package/.opencode/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
  137. package/.opencode/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
  138. package/.opencode/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  139. package/.opencode/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
  140. package/.opencode/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
  141. package/.opencode/skills/ui-ux-pro-max/data/styles.csv +85 -0
  142. package/.opencode/skills/ui-ux-pro-max/data/typography.csv +75 -0
  143. package/.opencode/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
  144. package/.opencode/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  145. package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
  146. package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
  147. package/.opencode/skills/ui-ux-pro-max/scripts/core.py +274 -0
  148. package/.opencode/skills/ui-ux-pro-max/scripts/design_system.py +1329 -0
  149. package/.opencode/skills/ui-ux-pro-max/scripts/search.py +127 -0
  150. package/README.md +5 -0
  151. package/STELLA.md +1 -1
  152. package/dist/antimalware/database.mjs +871 -0
  153. package/dist/antimalware/index.mjs +8 -0
  154. package/dist/antimalware/scanner.mjs +591 -0
  155. package/dist/antimalware/ui.mjs +570 -0
  156. package/dist/banner.mjs +46 -0
  157. package/dist/index.mjs +3073 -0
  158. package/dist/markdown.mjs +100 -0
  159. package/dist/sea-config.json +4 -0
  160. package/dist/security.mjs +237 -0
  161. package/dist/theme.mjs +89 -0
  162. package/dist/tools.mjs +3145 -0
  163. package/install-stella.bat +44 -0
  164. package/package.json +12 -3
  165. package/presentation-advanced-features-demo/README.md +27 -0
  166. package/presentation-advanced-features-demo/index.html +371 -0
  167. package/presentation-advanced-features-demo/print.html +81 -0
  168. package/presentation-advanced-features-demo/slide-01.html +98 -0
  169. package/presentation-advanced-features-demo/slide-02.html +101 -0
  170. package/presentation-advanced-features-demo/slide-03.html +100 -0
  171. package/presentation-advanced-features-demo/slide-04.html +100 -0
  172. package/presentation-advanced-features-demo/slide-05.html +100 -0
  173. package/presentation-advanced-features-demo/slide-06.html +64 -0
  174. package/presentation-advanced-features-demo/slide-07.html +101 -0
  175. package/presentation-advanced-features-demo/slide-08.html +91 -0
  176. package/presentation-stella-coder---ai-coding-agent/README.md +27 -0
  177. package/presentation-stella-coder---ai-coding-agent/index.html +168 -0
  178. package/presentation-stella-coder---ai-coding-agent/slide-01.html +98 -0
  179. package/presentation-stella-coder---ai-coding-agent/slide-02.html +101 -0
  180. package/presentation-stella-coder---ai-coding-agent/slide-03.html +100 -0
  181. package/presentation-stella-coder---ai-coding-agent/slide-04.html +100 -0
  182. package/presentation-stella-coder---ai-coding-agent/slide-05.html +100 -0
  183. package/presentation-stella-coder---ai-coding-agent/slide-06.html +100 -0
  184. package/presentation-stella-coder---ai-coding-agent/slide-07.html +100 -0
  185. package/presentation-stella-coder---ai-coding-agent/slide-08.html +64 -0
  186. package/presentation-stella-coder---ai-coding-agent/slide-09.html +101 -0
  187. package/presentation-stella-coder---ai-coding-agent/slide-10.html +91 -0
  188. package/presentation-stella-coder-demo/README.md +27 -0
  189. package/presentation-stella-coder-demo/index.html +371 -0
  190. package/presentation-stella-coder-demo/slide-01.html +98 -0
  191. package/presentation-stella-coder-demo/slide-02.html +101 -0
  192. package/presentation-stella-coder-demo/slide-03.html +100 -0
  193. package/presentation-stella-coder-demo/slide-04.html +100 -0
  194. package/presentation-stella-coder-demo/slide-05.html +100 -0
  195. package/presentation-stella-coder-demo/slide-06.html +64 -0
  196. package/presentation-stella-coder-demo/slide-07.html +101 -0
  197. package/presentation-stella-coder-demo/slide-08.html +91 -0
  198. package/presentation-stella-coder-features/README.md +27 -0
  199. package/presentation-stella-coder-features/index.html +364 -0
  200. package/presentation-stella-coder-features/print.html +77 -0
  201. package/presentation-stella-coder-features/slide-01.html +98 -0
  202. package/presentation-stella-coder-features/slide-02.html +101 -0
  203. package/presentation-stella-coder-features/slide-03.html +100 -0
  204. package/presentation-stella-coder-features/slide-04.html +100 -0
  205. package/presentation-stella-coder-features/slide-05.html +64 -0
  206. package/presentation-stella-coder-features/slide-06.html +101 -0
  207. package/presentation-stella-coder-features/slide-07.html +91 -0
  208. package/presentation-test-presentation/README.md +27 -0
  209. package/presentation-test-presentation/index.html +350 -0
  210. package/presentation-test-presentation/print.html +69 -0
  211. package/presentation-test-presentation/slide-01.html +98 -0
  212. package/presentation-test-presentation/slide-02.html +101 -0
  213. package/presentation-test-presentation/slide-03.html +64 -0
  214. package/presentation-test-presentation/slide-04.html +101 -0
  215. package/presentation-test-presentation/slide-05.html +91 -0
  216. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/README.md +27 -0
  217. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/index.html +159 -0
  218. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/slide-01.html +98 -0
  219. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/slide-02.html +101 -0
  220. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/slide-03.html +100 -0
  221. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/slide-04.html +100 -0
  222. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/slide-05.html +64 -0
  223. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/slide-06.html +101 -0
  224. package/presentation-/321/202/320/265/321/201/321/202/320/276/320/262/320/260/321/217-/320/277/321/200/320/265/320/267/320/265/320/275/321/202/320/260/321/206/320/270/321/217/slide-07.html +91 -0
  225. package/stella-cli/coding-brain.mjs +753 -0
  226. package/stella-cli/index.mjs +931 -15
  227. package/stella-cli/mcp.mjs +296 -0
  228. package/stella-cli/presentations.mjs +1106 -0
  229. package/stella-cli/subagents.mjs +142 -0
  230. package/stella-cli/telegram-bot.mjs +824 -0
  231. package/stella-cli/tg-bg.bat +13 -0
  232. package/stella-cli/tg-server.bat +15 -0
  233. package/stella-cli/tg-server.mjs +116 -0
  234. package/stella-cli/theme.mjs +6 -4
  235. package/stella-coder-3.9.2.tgz +0 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,3073 @@
1
+ #!/usr/bin/env node
2
+ // Stella Coder 3.9 — терминальный AI-агент
3
+ import readline from "node:readline"
4
+ import fs from "node:fs"
5
+ import path from "node:path"
6
+ import os from "node:os"
7
+ import { execSync } from "node:child_process"
8
+ import { streamText, stepCountIs, generateText } from "ai"
9
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
10
+ import { createTools } from "./tools.mjs"
11
+ import { printBanner } from "./banner.mjs"
12
+ import { createStreamRenderer, renderMarkdown } from "./markdown.mjs"
13
+ import {
14
+ bold, dim, violet, purple, indigo, blue, cyan, green, red, yellow, gray, darkGray, white,
15
+ gradientLine, box, SPINNER_FRAMES, SPINNER_WORDS,
16
+ } from "./theme.mjs"
17
+ import {
18
+ verifyCodeIntegrity, getApiKey, saveApiKey, deleteApiKey,
19
+ getHardwareInfo, saveIntegrityHash,
20
+ } from "./security.mjs"
21
+
22
+ const VERSION = "3.9.2"
23
+ const CONFIG_DIR = path.join(os.homedir(), ".stella")
24
+ const CONFIG_PATH = path.join(CONFIG_DIR, "config.json")
25
+ const HISTORY_PATH = path.join(CONFIG_DIR, "history.json")
26
+
27
+ const ZEN_BASE_URL = "https://opencode.ai/zen/v1"
28
+
29
+ const MODELS = [
30
+ { id: "mimo-v2.5-free", label: "MiMo V2.5 (бесплатная)" },
31
+ { id: "deepseek-v4-flash-free", label: "DeepSeek V4 Flash (бесплатная)" },
32
+ { id: "gpt-5.4", label: "GPT 5.4 (OpenAI)" },
33
+ { id: "gpt-5.4-mini", label: "GPT 5.4 Mini (OpenAI)" },
34
+ { id: "gpt-5.2", label: "GPT 5.2 (OpenAI)" },
35
+ { id: "gpt-5.2-codex", label: "GPT 5.2 Codex (OpenAI)" },
36
+ { id: "gpt-5", label: "GPT 5 (OpenAI)" },
37
+ { id: "gpt-5-codex", label: "GPT 5 Codex (OpenAI)" },
38
+ { id: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" },
39
+ { id: "claude-opus-4.5", label: "Claude Opus 4.5" },
40
+ { id: "claude-haiku-4.5", label: "Claude Haiku 4.5" },
41
+ { id: "claude-sonnet-5", label: "Claude Sonnet 5" },
42
+ { id: "gemini-3-flash", label: "Gemini 3 Flash (Google)" },
43
+ { id: "gemini-3.1-pro", label: "Gemini 3.1 Pro (Google)" },
44
+ { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
45
+ { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash" },
46
+ { id: "glm-5.2", label: "GLM 5.2 (Z.AI)" },
47
+ { id: "glm-5", label: "GLM 5 (Z.AI)" },
48
+ { id: "kimi-k2.6", label: "Kimi K2.6 (Moonshot)" },
49
+ { id: "minimax-m3", label: "MiniMax M3" },
50
+ ]
51
+
52
+ // ---------- config ----------
53
+ function loadConfig() {
54
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")) } catch { return {} }
55
+ }
56
+ function saveConfig(cfg) {
57
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
58
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2))
59
+ }
60
+
61
+ const config = loadConfig()
62
+
63
+ // Load API key from secure vault (hardware-bound)
64
+ const secureKey = getApiKey()
65
+ let apiKey = ""
66
+ if (secureKey && secureKey.error) {
67
+ console.log(red("\n ✗ " + secureKey.error))
68
+ process.exit(1)
69
+ } else if (secureKey && secureKey.apiKey) {
70
+ apiKey = secureKey.apiKey
71
+ } else if (config.apiKey) {
72
+ apiKey = config.apiKey
73
+ saveApiKey(apiKey)
74
+ }
75
+
76
+ // OpenCode Zen provider
77
+ const zen = createOpenAICompatible({
78
+ name: "zen",
79
+ baseURL: ZEN_BASE_URL,
80
+ apiKey,
81
+ })
82
+
83
+ function getModel(modelId) {
84
+ return zen.chatModel(modelId)
85
+ }
86
+
87
+ // auto-load .env files from cwd (like Next.js)
88
+ for (const envFile of [".env.local", ".env.development.local", ".env"]) {
89
+ try {
90
+ const p = path.join(process.cwd(), envFile)
91
+ if (!fs.existsSync(p)) continue
92
+ for (const line of fs.readFileSync(p, "utf8").split("\n")) {
93
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/)
94
+ if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, "")
95
+ }
96
+ } catch {}
97
+ }
98
+
99
+ // ---------- state ----------
100
+ const state = {
101
+ model: config.model || MODELS[0].id,
102
+ messages: [],
103
+ totalTokens: { input: 0, output: 0 },
104
+ totalCost: 0,
105
+ turns: 0,
106
+ alwaysAllow: new Set(config.alwaysAllow || []),
107
+ interrupted: false,
108
+ startedAt: Date.now(),
109
+ }
110
+
111
+ // approx pricing per 1M tokens (USD) for cost estimation
112
+ const PRICING = { input: 0.6, output: 2.2 }
113
+
114
+ // ---------- args ----------
115
+ const args = process.argv.slice(2)
116
+ const printMode = args.includes("-p") || args.includes("--print")
117
+ const printPrompt = printMode ? args[args.indexOf(args.includes("-p") ? "-p" : "--print") + 1] : null
118
+ if (args.includes("--version") || args.includes("-v")) {
119
+ console.log(`stella-coder ${VERSION}`)
120
+ process.exit(0)
121
+ }
122
+ if (args.includes("--model")) state.model = args[args.indexOf("--model") + 1] || state.model
123
+
124
+ // ---------- system prompt ----------
125
+ function systemPrompt() {
126
+ let projectContext = ""
127
+ try {
128
+ if (fs.existsSync("STELLA.md")) projectContext = "\n\n# Контекст проекта (STELLA.md)\n" + fs.readFileSync("STELLA.md", "utf8")
129
+ } catch {}
130
+ return `Ты — Stella, ИИ-агент терминального агента Stella Coder 3.9 (аналог Claude Code).
131
+ Ты — эксперт-программист, работающий в директории: ${process.cwd()} (ОС: ${os.platform()}).
132
+
133
+ Правила:
134
+ - Используй инструменты (read_file, edit_file, write_file, bash, grep, glob, list_dir), чтобы РЕАЛЬНО выполнять задачи, а не просто описывать их.
135
+ - Перед изменением файла всегда читай его.
136
+ - Для многошаговых задач используй todo_write, чтобы показать план.
137
+ - Отвечай кратко и по делу, в формате markdown. Код — в блоках с указанием языка.
138
+ - Отвечай на языке пользователя.${projectContext}`
139
+ }
140
+
141
+ // ---------- spinner ----------
142
+ let spinnerTimer = null
143
+ function startSpinner(label) {
144
+ if (printMode) return
145
+ let i = 0
146
+ const word = label || SPINNER_WORDS[Math.floor(Math.random() * SPINNER_WORDS.length)]
147
+ const start = Date.now()
148
+ spinnerTimer = setInterval(() => {
149
+ const f = SPINNER_FRAMES[i++ % SPINNER_FRAMES.length]
150
+ const secs = Math.floor((Date.now() - start) / 1000)
151
+ process.stdout.write(`\r${violet(f)} ${purple(word + "…")} ${dim(`(${secs}s · esc/ctrl+c — прервать)`)} `)
152
+ }, 80)
153
+ }
154
+ function stopSpinner() {
155
+ if (spinnerTimer) {
156
+ clearInterval(spinnerTimer)
157
+ spinnerTimer = null
158
+ process.stdout.write("\r" + " ".repeat(70) + "\r")
159
+ }
160
+ }
161
+
162
+ // ---------- permissions ----------
163
+ let rl // set later
164
+ async function askPermission(kind, summary) {
165
+ if (printMode) return true
166
+ if (state.alwaysAllow.has(kind)) return true
167
+ stopSpinner()
168
+ console.log()
169
+ console.log(box([
170
+ yellow("⚠ Требуется разрешение"),
171
+ "",
172
+ white(summary),
173
+ ], { color: indigo, padding: 2 }))
174
+ const ans = await question(
175
+ " " + bold(violet("Разрешить?")) + dim(" [y] да · [n] нет · [a] всегда для " + kind + " › "),
176
+ )
177
+ const a = ans.trim().toLowerCase()
178
+ if (a === "a" || a === "always" || a === "в") {
179
+ state.alwaysAllow.add(kind)
180
+ saveConfig({ ...loadConfig(), alwaysAllow: [...state.alwaysAllow] })
181
+ return true
182
+ }
183
+ return a === "y" || a === "yes" || a === "д" || a === ""
184
+ }
185
+
186
+ function question(q) {
187
+ return new Promise((res) => rl.question(q, res))
188
+ }
189
+
190
+ // ---------- todos display ----------
191
+ function renderTodos(todos) {
192
+ stopSpinner()
193
+ console.log()
194
+ const lines = todos.map((t) => {
195
+ if (t.status === "completed") return green("☒ ") + strikeDim(t.content)
196
+ if (t.status === "in_progress") return blue("◐ ") + bold(white(t.content))
197
+ return darkGray("☐ ") + gray(t.content)
198
+ })
199
+ console.log(box(lines, { title: "План", color: purple, padding: 2 }))
200
+ console.log()
201
+ }
202
+ function strikeDim(s) {
203
+ return dim("\x1b[9m" + s + "\x1b[29m")
204
+ }
205
+
206
+ // ---------- tools ----------
207
+ const tools = createTools({ ask: askPermission, onTodos: renderTodos })
208
+
209
+ // ---------- tool display ----------
210
+ function toolLabel(name, input) {
211
+ const short = (s, n = 60) => (s && s.length > n ? s.slice(0, n) + "…" : s || "")
212
+ switch (name) {
213
+ case "read_file": return `Read(${input.path})`
214
+ case "write_file": return `Write(${input.path})`
215
+ case "edit_file": return `Edit(${input.path})`
216
+ case "bash": return `Bash(${short(input.command)})`
217
+ case "grep": return `Grep("${short(input.pattern, 40)}")`
218
+ case "glob": return `Glob(${input.pattern})`
219
+ case "list_dir": return `List(${input.path || "."})`
220
+ case "todo_write": return `Todo(${input.todos?.length ?? 0} задач)`
221
+ default: return name
222
+ }
223
+ }
224
+ function toolResultSummary(name, output) {
225
+ if (!output) return ""
226
+ if (output.error) return red("✗ " + String(output.error).split("\n")[0].slice(0, 80))
227
+ switch (name) {
228
+ case "read_file": return dim(`прочитано ${output.totalLines ?? "?"} строк`)
229
+ case "write_file": return dim(`записано ${output.lines ?? "?"} строк`)
230
+ case "edit_file": return dim(`замен: ${output.replacements ?? 1}`)
231
+ case "bash": return dim((output.output || "").split("\n")[0].slice(0, 80) || "готово")
232
+ case "grep": return dim(`${(output.matches || "").split("\n").filter(Boolean).length} совпадений`)
233
+ case "glob": return dim(`${output.total ?? 0} файлов`)
234
+ case "list_dir": return dim(`${output.entries?.length ?? 0} элементов`)
235
+ default: return dim("готово")
236
+ }
237
+ }
238
+
239
+ // ---------- agent turn ----------
240
+ async function runTurn(userText) {
241
+ state.messages.push({ role: "user", content: userText })
242
+ state.turns++
243
+ state.interrupted = false
244
+
245
+ const isOllama = state.model.startsWith("ollama:")
246
+ const controller = new AbortController()
247
+ const onSigint = () => {
248
+ state.interrupted = true
249
+ controller.abort()
250
+ }
251
+ process.once("SIGINT", onSigint)
252
+
253
+ startSpinner()
254
+ const t0 = Date.now()
255
+ let firstText = true
256
+
257
+ try {
258
+ let result
259
+
260
+ if (isOllama) {
261
+ const ollamaModel = state.model.replace("ollama:", "")
262
+ const ollamaMessages = state.messages.map(m => ({
263
+ role: m.role,
264
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
265
+ }))
266
+
267
+ const res = await fetch(`${OLLAMA_BASE}/api/chat`, {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/json" },
270
+ body: JSON.stringify({
271
+ model: ollamaModel,
272
+ messages: [{ role: "system", content: systemPrompt() }, ...ollamaMessages],
273
+ stream: true,
274
+ }),
275
+ signal: controller.signal,
276
+ })
277
+
278
+ stopSpinner()
279
+ const reader = res.body.getReader()
280
+ const decoder = new TextDecoder()
281
+ let buffer = ""
282
+
283
+ while (true) {
284
+ const { done, value } = await reader.read()
285
+ if (done) break
286
+ buffer += decoder.decode(value, { stream: true })
287
+ const lines = buffer.split("\n")
288
+ buffer = lines.pop()
289
+
290
+ for (const line of lines) {
291
+ if (!line.trim()) continue
292
+ try {
293
+ const json = JSON.parse(line)
294
+ if (json.message?.content) {
295
+ process.stdout.write(json.message.content)
296
+ }
297
+ } catch {}
298
+ }
299
+ }
300
+ console.log("\n")
301
+ } else {
302
+ result = streamText({
303
+ model: getModel(state.model),
304
+ system: systemPrompt(),
305
+ messages: state.messages,
306
+ tools,
307
+ stopWhen: stepCountIs(30),
308
+ abortSignal: controller.signal,
309
+ onError: () => {},
310
+ })
311
+
312
+ const renderer = createStreamRenderer((s) => process.stdout.write(s))
313
+
314
+ for await (const part of result.fullStream) {
315
+ switch (part.type) {
316
+ case "text-delta": {
317
+ stopSpinner()
318
+ if (firstText) {
319
+ process.stdout.write("\n" + violet("⏺ ") )
320
+ firstText = false
321
+ }
322
+ renderer.push(part.text)
323
+ break
324
+ }
325
+ case "text-end": {
326
+ renderer.flush()
327
+ firstText = true
328
+ break
329
+ }
330
+ case "tool-call": {
331
+ stopSpinner()
332
+ renderer.flush()
333
+ console.log("\n" + purple("⏺ ") + bold(white(toolLabel(part.toolName, part.input))))
334
+ startSpinner("Выполняю")
335
+ break
336
+ }
337
+ case "tool-result": {
338
+ stopSpinner()
339
+ console.log(darkGray(" ⎿ ") + toolResultSummary(part.toolName, part.output))
340
+ startSpinner()
341
+ break
342
+ }
343
+ case "tool-error": {
344
+ stopSpinner()
345
+ console.log(darkGray(" ⎿ ") + red("ошибка инструмента"))
346
+ startSpinner()
347
+ break
348
+ }
349
+ case "error": {
350
+ stopSpinner()
351
+ renderer.flush()
352
+ console.log("\n" + red("✗ Ошибка: ") + String(part.error?.message || part.error).slice(0, 300))
353
+ break
354
+ }
355
+ case "finish": {
356
+ stopSpinner()
357
+ renderer.flush()
358
+ break
359
+ }
360
+ }
361
+ }
362
+
363
+ const response = await result.response
364
+ state.messages.push(...response.messages)
365
+
366
+ const usage = await result.usage
367
+ const inTok = usage.inputTokens ?? 0
368
+ const outTok = usage.outputTokens ?? 0
369
+ state.totalTokens.input += inTok
370
+ state.totalTokens.output += outTok
371
+ const cost = (inTok * PRICING.input + outTok * PRICING.output) / 1e6
372
+ state.totalCost += cost
373
+
374
+ const dur = ((Date.now() - t0) / 1000).toFixed(1)
375
+ console.log()
376
+ console.log(
377
+ dim(" ") + darkGray(`⏱ ${dur}s · ↑${inTok} ↓${outTok} ток · ~$${cost.toFixed(4)} · ${blue(state.model)}`),
378
+ )
379
+ }
380
+ } catch (e) {
381
+ stopSpinner()
382
+ if (state.interrupted || e?.name === "AbortError") {
383
+ console.log("\n" + yellow("⏸ Прервано пользователем"))
384
+ } else {
385
+ console.log("\n" + red("✗ Ошибка: ") + String(e?.message || e).slice(0, 500))
386
+ if (String(e?.message || "").match(/api key|unauthorized|401|credential/i)) {
387
+ console.log(dim(" Задай ключ: ") + purple("/login") + dim(" или сохрани в ~/.stella/config.json"))
388
+ }
389
+ }
390
+ } finally {
391
+ process.removeListener("SIGINT", onSigint)
392
+ }
393
+ console.log()
394
+ }
395
+
396
+ // ---------- slash commands ----------
397
+ const COMMANDS = [
398
+ // Основные
399
+ ["/help", "все команды"],
400
+ ["/model", "выбрать модель"],
401
+ ["/clear", "очистить контекст"],
402
+ ["/compact", "сжать историю (суммаризация)"],
403
+ ["/cost", "статистика токенов и стоимости"],
404
+ ["/context", "размер текущего контекста"],
405
+ ["/config", "показать/изменить конфигурацию"],
406
+ ["/version", "версия"],
407
+ ["/exit", "выход (также /quit, Ctrl+D)"],
408
+
409
+ // Работа с файлами
410
+ ["/read", "прочитать файл"],
411
+ ["/write", "записать файл"],
412
+ ["/edit", "редактировать файл"],
413
+ ["/find", "найти файлы по паттерну"],
414
+ ["/grep", "поиск по содержимому"],
415
+ ["/tree", "дерево файлов"],
416
+ ["/head", "первые N строк файла"],
417
+ ["/tail", "последние N строк файла"],
418
+ ["/wc", "подсчёт строк/слов"],
419
+ ["/diff", "разница между файлами"],
420
+ ["/patch", "применить патч"],
421
+ ["/chmod", "изменить права доступа"],
422
+ ["/mkdir", "создать директорию"],
423
+ ["/rm", "удалить файл"],
424
+ ["/cp", "копировать файл"],
425
+ ["/mv", "переместить файл"],
426
+ ["/touch", "создать пустой файл"],
427
+ ["/ln", "создать ссылку"],
428
+ ["/stat", "информация о файле"],
429
+ ["/type", "показать тип файла"],
430
+ ["/open", "открыть файл в редакторе"],
431
+
432
+ // Git
433
+ ["/git", "git operations (status, diff, log, commit, push, pull)"],
434
+ ["/commit", "AI-коммит (сгенерировать сообщение + git commit)"],
435
+ ["/diff", "git diff"],
436
+ ["/log", "git log"],
437
+ ["/status", "git status"],
438
+ ["/branch", "git branch"],
439
+ ["/checkout", "git checkout"],
440
+ ["/merge", "git merge"],
441
+ ["/rebase", "git rebase"],
442
+ ["/stash", "git stash"],
443
+ ["/pop", "git stash pop"],
444
+ ["/tag", "git tag"],
445
+ ["/remote", "git remote"],
446
+ ["/fetch", "git fetch"],
447
+ ["/pull", "git pull"],
448
+ ["/push", "git push"],
449
+ ["/clone", "git clone"],
450
+ ["/init", "git init"],
451
+
452
+ // AI и агенты
453
+ ["/plan", "автономный план выполнения задачи"],
454
+ ["/todo", "управление задачами (создать/показать/выполнитb)"],
455
+ ["/agent", "запустить субагент для сложной задачи"],
456
+ ["/skill", "загрузить/показать навыки"],
457
+ ["/mcp", "управление MCP серверами"],
458
+ ["/memory", "показать/обновить память проекта (STELLA.md)"],
459
+ ["/remember", "сохранить информацию в память"],
460
+ ["/forget", "удалить информацию из памяти"],
461
+
462
+ // Веб и внешние ресурсы
463
+ ["/web", "поиск в интернете"],
464
+ ["/fetch", "загрузить URL"],
465
+ ["/screenshot", "сделать скриншот страницы"],
466
+
467
+ // Работа с контентом
468
+ ["/read-image", "прочитать изображение"],
469
+ ["/read-pdf", "прочитать PDF"],
470
+ ["/read-doc", "прочитать документ"],
471
+ ["/ocr", "распознать текст на изображении"],
472
+ ["/translate", "перевести текст"],
473
+ ["/summarize", "суммаризировать текст"],
474
+ ["/explain", "объяснить код"],
475
+ ["/review", "ревью кода"],
476
+ ["/refactor", "рефакторинг кода"],
477
+ ["/test", "написать/запустить тесты"],
478
+ ["/lint", "проверить код линтером"],
479
+ ["/format", "отформатировать код"],
480
+ ["/typecheck", "проверить типы"],
481
+ ["/build", "собрать проект"],
482
+ ["/run", "запустить скрипт"],
483
+ ["/debug", "отладка"],
484
+ ["/profile", "профилирование"],
485
+ ["/benchmark", "бенчмарки"],
486
+
487
+ // Безопасность
488
+ ["/av", "запустить Stellar Antivirus"],
489
+ ["/vt", "сканировать файл через VirusTotal"],
490
+ ["/ai-scan", "AI-анализ подозрительного кода"],
491
+ ["/audit", "аудит безопасности"],
492
+ ["/vuln", "поиск уязвимостей"],
493
+ ["/secrets", "поиск секретов в коде"],
494
+ ["/hash", "вычислить хеш файла"],
495
+
496
+ // Управление пакетами
497
+ ["/install", "установить пакет"],
498
+ ["/uninstall", "удалить пакет"],
499
+ ["/update", "обновить пакеты"],
500
+ ["/outdated", "проверить устаревшие пакеты"],
501
+ ["/deps", "показать зависимости"],
502
+ ["/audit-deps", "аудит зависимостей"],
503
+
504
+ // Окружение
505
+ ["/env", "показать переменные окружения"],
506
+ ["/set-env", "установить переменную окружения"],
507
+ ["/unset-env", "удалить переменную окружения"],
508
+ ["/path", "показать PATH"],
509
+ ["/which", "найти расположение команды"],
510
+ ["/whoami", "текущий пользователь"],
511
+ ["/hostname", "имя компьютера"],
512
+ ["/uname", "информация о системе"],
513
+ ["/uptime", "время работы"],
514
+ ["/disk", "использование диска"],
515
+ ["/mem", "использование памяти"],
516
+ ["/cpu", "информация о CPU"],
517
+ ["/gpu", "информация о GPU"],
518
+ ["/net", "сетевые интерфейсы"],
519
+ ["/ping", "проверить соединение"],
520
+ ["/dns", "DNS запрос"],
521
+ ["/curl", "HTTP запрос"],
522
+ ["/wget", "загрузить файл"],
523
+
524
+ // Мониторинг
525
+ ["/top", "процессы по использованию CPU"],
526
+ ["/ps", "список процессов"],
527
+ ["/kill", "завершить процесс"],
528
+ ["/watch", "мониторинг команды"],
529
+ ["/tail-log", "мониторинг лог-файла"],
530
+ ["/notify", "отправить уведомление"],
531
+
532
+ // Логирование
533
+ ["/log", "показать логи"],
534
+ ["/error", "показать ошибки"],
535
+ ["/warn", "показать предупреждения"],
536
+ ["/info", "показать информацию"],
537
+ ["/debug-log", "включить отладочный лог"],
538
+
539
+ // Работа с базами данных
540
+ ["/db", "подключение к БД"],
541
+ ["/query", "выполнить SQL запрос"],
542
+ ["/migrate", "миграция базы данных"],
543
+ ["/seed", "заполнить БД тестовыми данными"],
544
+
545
+ // Документация
546
+ ["/docs", "сгенерировать документацию"],
547
+ ["/readme", "сгенерировать README"],
548
+ ["/changelog", "сгенерировать changelog"],
549
+ ["/examples", "показать примеры"],
550
+ ["/tutorial", "показать обучение"],
551
+ ["/wizard", "мастер настройки"],
552
+
553
+ // Управление сессией
554
+ ["/login", "ввести API ключ"],
555
+ ["/newkey", "заменить API ключ"],
556
+ ["/delapikey", "удалить API ключ"],
557
+ ["/permissions", "сбросить выданные разрешения"],
558
+ ["/doctor", "диагностика окружения"],
559
+ ["/history", "история сообщений сессии"],
560
+ ["/export", "экспортировать диалог в файл"],
561
+ ["/import", "импортировать диалог из файла"],
562
+ ["/save", "сохранить сессию"],
563
+ ["/load", "загрузить сессию"],
564
+ ["/sessions", "список сохранённых сессий"],
565
+
566
+ // Ollama
567
+ ["/ollama", "подключить локальную LLM (Ollama)"],
568
+
569
+ // Управление компьютером
570
+ ["/open", "открыть приложение/файл/URL"],
571
+ ["/app", "запустить приложение"],
572
+ ["/url", "открыть ссылку в браузере"],
573
+ ["/kinopoisk", "открыть Кинопоиск"],
574
+ ["/youtube", "открыть YouTube"],
575
+ ["/google", "открыть Google"],
576
+ ["/folder", "открыть папку в проводнике"],
577
+ ["/screenshot", "сделать скриншот"],
578
+ ["/clipboard", "буфер обмена (получить/вставить)"],
579
+ ["/type", "набрать текст на клавиатуре"],
580
+ ["/key", "нажать клавишу/комбинацию"],
581
+ ["/volume", "управление громкостью"],
582
+ ["/brightness", "управление яркостью"],
583
+ ["/notify", "показать уведомление Windows"],
584
+ ["/windows", "список открытых окон"],
585
+ ["/focus", "переключиться на окно"],
586
+ ["/wifi", "Wi-Fi сети"],
587
+ ["/lock", "заблокировать экран"],
588
+ ["/shutdown", "выключить/перезагрузить"],
589
+ ["/search", "быстрый поиск файлов"],
590
+
591
+ // Умный дом
592
+ ["/tv", "управление Sony TV (вкл/выкл/канал/громкость)"],
593
+ ["tv-off", "выключить TV"],
594
+ ["tv-on", "включить TV"],
595
+ ["/tv-info", "информация о TV"],
596
+ ["/cec", "HDMI-CEC управление"],
597
+ ["/smart", "сканирование умных устройств"],
598
+ ["/light", "управление умными лампами"],
599
+ ["/cast", "трансляция на Chromecast"],
600
+ ["/wol", "включить устройство по MAC"],
601
+
602
+ // Сервер и терминал
603
+ ["/exec", "выполнить команду в терминале"],
604
+ ["/shell", "выполнить shell-команду"],
605
+ ["/powershell", "выполнить PowerShell команду"],
606
+ ["/cmd", "выполнить cmd команду"],
607
+ ["/ssh", "подключение по SSH"],
608
+ ["/scp", "копирование файлов по SCP"],
609
+ ["/server", "статус сервера"],
610
+ ["/ports", "открытые порты"],
611
+ ["/connections", "активные соединения"],
612
+ ["/kill-port", "завершить процесс на порту"],
613
+ ["/network", "сетевые интерфейсы"],
614
+ ["/dns", "DNS настройки"],
615
+ ["/ping", "пинг хоста"],
616
+ ["/traceroute", "трассировка маршрута"],
617
+ ["/download", "скачать файл"],
618
+ ["/upload", "загрузить файл"],
619
+ ["/http-server", "запустить HTTP сервер"],
620
+ ["/firewall", "файрвол Windows"],
621
+ ["/service", "службы Windows"],
622
+ ["/registry", "реестр Windows"],
623
+ ["/script", "запустить скрипт"],
624
+ ["/schedule", "запланировать задачу"],
625
+ ["/docker", "управление Docker"],
626
+ ["/pm2", "управление PM2"],
627
+ ["/npm", "npm/yarn/pnpm операции"],
628
+ ["/monitor", "мониторинг ресурсов"],
629
+ ["/sys", "полный доступ к системе"],
630
+ ["/sudo", "команда от администратора"],
631
+
632
+ // Дополнительно
633
+ ["/paste", "вставить из буфера обмена"],
634
+ ["/clipboard", "копировать в буфер обмена"],
635
+ ["/color", "изменить цветовую тему"],
636
+ ["/theme", "выбрать тему оформления"],
637
+ ["/lang", "сменить язык интерфейса"],
638
+ ["/voice", "голосовой ввод"],
639
+ ["/macro", "выполнить макрос"],
640
+ ["/alias", "создать/показать алиасы"],
641
+ ["/shortcut", "показать горячие клавиши"],
642
+ ["/feedback", "отправить отзыв"],
643
+ ["/report", "сообщить об ошибке"],
644
+ ["/support", "получить поддержку"],
645
+ ]
646
+
647
+ async function handleCommand(line) {
648
+ const [cmd, ...rest] = line.trim().split(/\s+/)
649
+ const arg = rest.join(" ")
650
+ switch (cmd) {
651
+ case "/help": {
652
+ console.log()
653
+ const categories = [
654
+ ["📁 Файлы", ["/read", "/write", "/edit", "/find", "/grep", "/tree", "/head", "/tail", "/wc", "/diff", "/stat"]],
655
+ ["🔧 Git", ["/git", "/commit", "/log", "/status", "/branch", "/checkout", "/merge", "/stash", "/pop", "/tag", "/remote", "/pull", "/push", "/clone"]],
656
+ ["🤖 AI", ["/plan", "/todo", "/agent", "/skill", "/mcp", "/memory", "/remember", "/forget"]],
657
+ ["🌐 Веб", ["/web", "/read-image", "/read-pdf", "/explain", "/review", "/refactor", "/translate", "/summarize"]],
658
+ ["💻 Компьютер", ["/open", "/url", "/kinopoisk", "/youtube", "/google", "/folder", "/screenshot", "/clipboard", "/type", "/key", "/volume", "/brightness", "/notify", "/windows", "/focus", "/wifi", "/lock", "/shutdown", "/search"]],
659
+ ["🏠 Умный дом", ["/tv", "/tv-on", "/tv-off", "/tv-info", "/cec", "/smart", "/light", "/cast", "/wol"]],
660
+ ["🖥 Сервер", ["/exec", "/powershell", "/cmd", "/ssh", "/scp", "/server", "/ports", "/connections", "/kill-port", "/network", "/dns", "/ping", "/traceroute", "/download", "/upload", "/http-server", "/firewall", "/service", "/registry", "/script", "/schedule", "/docker", "/pm2", "/npm", "/monitor", "/sys", "/sudo"]],
661
+ ["🧪 Тесты", ["/test", "/lint", "/format", "/typecheck", "/build", "/run", "/debug"]],
662
+ ["🔒 Безопасность", ["/av", "/vt", "/ai-scan", "/audit", "/secrets", "/hash"]],
663
+ ["📦 Пакеты", ["/install", "/uninstall", "/outdated", "/deps"]],
664
+ ["💻 Система", ["/env", "/path", "/which", "/whoami", "/hostname", "/uname", "/uptime", "/disk", "/mem", "/cpu", "/net", "/ps", "/kill"]],
665
+ ["📚 Документация", ["/docs", "/readme", "/changelog", "/examples", "/tutorial", "/wizard"]],
666
+ ["⚙️ Настройки", ["/help", "/model", "/clear", "/compact", "/cost", "/context", "/config", "/version", "/login", "/newkey", "/doctor", "/sessions", "/color", "/lang", "/shortcut"]],
667
+ ]
668
+ for (const [cat, cmds] of categories) {
669
+ console.log(` ${bold(cat)}`)
670
+ for (const c of cmds) {
671
+ const desc = COMMANDS.find(([cmd]) => cmd === c)?.[1] || ""
672
+ console.log(` ${violet(c.padEnd(14))}${dim(desc)}`)
673
+ }
674
+ console.log()
675
+ }
676
+ console.log(dim(" ") + purple("!команда".padEnd(14)) + dim("выполнить shell-команду напрямую"))
677
+ console.log(dim(" ") + purple("Ctrl+C".padEnd(14)) + dim("прервать (2x — выход)"))
678
+ console.log(dim(" ") + purple("Tab".padEnd(14)) + dim("автодополнение"))
679
+ console.log()
680
+ return
681
+ }
682
+ case "/model": {
683
+ console.log()
684
+ MODELS.forEach((m, i) => {
685
+ const active = m.id === state.model
686
+ console.log(
687
+ " " + (active ? violet("● ") : darkGray("○ ")) + bold(white(String(i + 1))) + ". " +
688
+ (active ? violet(m.label) : gray(m.label)) + dim(` (${m.id})`),
689
+ )
690
+ })
691
+ const ans = await question("\n " + violet("Номер модели › "))
692
+ const idx = Number.parseInt(ans) - 1
693
+ if (MODELS[idx]) {
694
+ state.model = MODELS[idx].id
695
+ saveConfig({ ...loadConfig(), model: state.model })
696
+ console.log(" " + green("✓ Модель: ") + blue(state.model) + "\n")
697
+ }
698
+ return
699
+ }
700
+ case "/clear": {
701
+ state.messages = []
702
+ console.clear()
703
+ printBanner({ model: state.model, cwd: process.cwd(), version: VERSION })
704
+ return
705
+ }
706
+ case "/compact": {
707
+ if (state.messages.length < 2) { console.log(dim(" Нечего сжимать\n")); return }
708
+ startSpinner("Сжимаю контекст")
709
+ try {
710
+ const { text } = await generateText({
711
+ model: getModel(state.model),
712
+ system: "Сожми диалог в краткое резюме: цели, сделанные изменения, важные файлы и решения. Пиши по-русски, кратко.",
713
+ messages: [...state.messages, { role: "user", content: "Сожми весь диалог выше в резюме." }],
714
+ })
715
+ state.messages = [{ role: "user", content: `Резюме предыдущего диалога:\n${text}` }, { role: "assistant", content: "Понял, продолжаем с этим контекстом." }]
716
+ stopSpinner()
717
+ console.log(green(" ✓ Контекст сжат\n"))
718
+ } catch (e) {
719
+ stopSpinner()
720
+ console.log(red(" ✗ " + String(e?.message || e).slice(0, 200)) + "\n")
721
+ }
722
+ return
723
+ }
724
+ case "/cost": {
725
+ const mins = ((Date.now() - state.startedAt) / 60000).toFixed(1)
726
+ console.log()
727
+ console.log(box([
728
+ dim("Сессия: ") + white(`${mins} мин · ${state.turns} запросов`),
729
+ dim("Токены (вход): ") + blue(String(state.totalTokens.input)),
730
+ dim("Токены (выход):") + blue(" " + String(state.totalTokens.output)),
731
+ dim("Стоимость: ") + green(`~$${state.totalCost.toFixed(4)}`),
732
+ dim("Модель: ") + violet(state.model),
733
+ ], { title: "Статистика", padding: 2 }))
734
+ console.log()
735
+ return
736
+ }
737
+ case "/context": {
738
+ const chars = JSON.stringify(state.messages).length
739
+ console.log(dim(`\n Сообщений: ${state.messages.length} · ~${Math.round(chars / 4)} токенов в контексте\n`))
740
+ return
741
+ }
742
+ case "/init": {
743
+ await runTurn(
744
+ "Изучи этот проект (структуру, package.json, ключевые файлы) и создай файл STELLA.md с кратким описанием: что за проект, стек, структура, как запускать, соглашения. Файл будет использоваться как твоя память о проекте.",
745
+ )
746
+ return
747
+ }
748
+ case "/memory": {
749
+ if (fs.existsSync("STELLA.md")) console.log("\n" + renderMarkdown(fs.readFileSync("STELLA.md", "utf8")) + "\n")
750
+ else console.log(dim("\n STELLA.md не найден. Создай его командой /init\n"))
751
+ return
752
+ }
753
+ case "/history": {
754
+ console.log()
755
+ for (const m of state.messages) {
756
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content).slice(0, 100)
757
+ console.log(" " + (m.role === "user" ? blue("вы ") : violet("ai ")) + dim(String(content).slice(0, 100)))
758
+ }
759
+ console.log()
760
+ return
761
+ }
762
+ case "/export": {
763
+ const file = arg || `stella-session-${Date.now()}.md`
764
+ const md = state.messages
765
+ .map((m) => `## ${m.role}\n\n${typeof m.content === "string" ? m.content : JSON.stringify(m.content, null, 2)}`)
766
+ .join("\n\n")
767
+ fs.writeFileSync(file, `# Stella Coder — сессия\n\n${md}`)
768
+ console.log(green(`\n ✓ Экспортировано в ${file}\n`))
769
+ return
770
+ }
771
+ case "/config": {
772
+ console.log()
773
+ console.log(box([
774
+ dim("Конфиг: ") + gray(CONFIG_PATH),
775
+ dim("Модель: ") + violet(state.model),
776
+ dim("API-ключ: ") + (apiKey ? green("задан") : red("не задан")),
777
+ dim("Endpoint: ") + gray(ZEN_BASE_URL),
778
+ dim("Всегда: ") + gray([...state.alwaysAllow].join(", ") || "—"),
779
+ ], { title: "Конфигурация", padding: 2 }))
780
+ console.log()
781
+ return
782
+ }
783
+ case "/login":
784
+ case "/newkey": {
785
+ const key = arg || (await question(" " + violet("API ключ › ")))
786
+ if (key.trim()) {
787
+ const result = saveApiKey(key.trim())
788
+ if (result.ok) {
789
+ apiKey = key.trim()
790
+ const hw = getHardwareInfo()
791
+ console.log(green(" ✓ Ключ сохранён и привязан к железу"))
792
+ console.log(dim(" ingerprint: ") + gray(hw.fingerprint))
793
+ console.log(dim(" Платформа: ") + gray(hw.platform + " / " + hw.hostname))
794
+ console.log()
795
+ } else {
796
+ console.log(red(" ✗ " + result.error + "\n"))
797
+ }
798
+ }
799
+ return
800
+ }
801
+ case "/delapikey": {
802
+ const confirm = await question(" " + red("Удалить API ключ? (yes/no) › "))
803
+ if (confirm.trim().toLowerCase() === "yes") {
804
+ const result = deleteApiKey()
805
+ if (result.ok) {
806
+ apiKey = ""
807
+ console.log(green(" ✓ API ключ удалён\n"))
808
+ } else {
809
+ console.log(red(" ✗ " + result.error + "\n"))
810
+ }
811
+ } else {
812
+ console.log(dim(" Отмена\n"))
813
+ }
814
+ return
815
+ }
816
+ case "/permissions": {
817
+ state.alwaysAllow.clear()
818
+ saveConfig({ ...loadConfig(), alwaysAllow: [] })
819
+ console.log(green(" ✓ Разрешения сброшены\n"))
820
+ return
821
+ }
822
+ case "/plan": {
823
+ if (!arg) { console.log(dim("\n Использование: /plan <описание задачи>\n")); return }
824
+ await runPlan(arg)
825
+ return
826
+ }
827
+ case "/commit": {
828
+ await runCommit(arg)
829
+ return
830
+ }
831
+ case "/ollama": {
832
+ await handleOllama(arg)
833
+ return
834
+ }
835
+ case "/vt": {
836
+ await handleVirusTotal(arg)
837
+ return
838
+ }
839
+ case "/ai-scan": {
840
+ await handleAIScan(arg)
841
+ return
842
+ }
843
+ case "/doctor": {
844
+ console.log()
845
+ const checks = [
846
+ ["Node.js", process.version, true],
847
+ ["Рабочая папка", process.cwd(), true],
848
+ ["API ключ", apiKey ? "задан" : "НЕ задан", !!apiKey],
849
+ ["git", safeExec("git --version"), null],
850
+ ["Интернет", "проверяю…", null],
851
+ ]
852
+ for (const [name, val, ok] of checks.slice(0, 4)) {
853
+ console.log(" " + (ok === false ? red("✗") : green("✓")) + " " + dim(name.padEnd(22)) + white(String(val)))
854
+ }
855
+ try {
856
+ const res = await fetch(ZEN_BASE_URL + "/v1/models", {
857
+ headers: { Authorization: `Bearer ${apiKey}` },
858
+ signal: AbortSignal.timeout(5000),
859
+ })
860
+ console.log(" " + green("✓") + " " + dim("Интернет".padEnd(22)) + white(`HTTP ${res.status}`))
861
+ } catch {
862
+ console.log(" " + red("✗") + " " + dim("Интернет".padEnd(22)) + red("недоступен"))
863
+ }
864
+ console.log()
865
+ return
866
+ }
867
+ case "/version": {
868
+ console.log("\n " + gradientLine(`✦ Stella Coder v${VERSION}`) + dim(" · AI engine · ai-sdk") + "\n")
869
+ return
870
+ }
871
+ case "/av": {
872
+ const { showUI } = await import("../antimalware/ui.mjs")
873
+ rl.close()
874
+ await showUI()
875
+ // After AV exits, restart REPL
876
+ rl = readline.createInterface({
877
+ input: process.stdin,
878
+ output: process.stdout,
879
+ completer: (line) => {
880
+ if (!line.startsWith("/")) return [[], line]
881
+ const hits = COMMANDS.map(([c]) => c).filter((c) => c.startsWith(line))
882
+ return [hits, line]
883
+ },
884
+ })
885
+ rl.on("SIGINT", () => {
886
+ const now = Date.now()
887
+ if (now - lastSigint < 1500) goodbye()
888
+ lastSigint = now
889
+ console.log(dim("\n (нажми Ctrl+C ещё раз для выхода)"))
890
+ promptUser()
891
+ })
892
+ rl.on("close", goodbye)
893
+ console.log()
894
+ return
895
+ }
896
+
897
+ // ═══════════════════════════════════════════════════
898
+ // ФАЙЛЫ
899
+ // ═══════════════════════════════════════════════════
900
+ case "/read": {
901
+ if (!arg) { console.log(dim("\n Использование: /read <файл>\n")); return }
902
+ try {
903
+ const content = fs.readFileSync(arg.trim(), "utf8")
904
+ console.log("\n" + content + "\n")
905
+ } catch (e) {
906
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
907
+ }
908
+ return
909
+ }
910
+ case "/write": {
911
+ if (!arg) { console.log(dim("\n Использование: /write <файл> <содержимое>\n")); return }
912
+ const [writeFile, ...writeContent] = arg.split(/\s+/)
913
+ const writeData = writeContent.join(" ")
914
+ if (!writeData) {
915
+ console.log(dim("\n Введите содержимое (Ctrl+D для завершения):\n"))
916
+ const lines = []
917
+ const line = await question("")
918
+ lines.push(line)
919
+ fs.writeFileSync(writeFile, lines.join("\n"))
920
+ } else {
921
+ fs.writeFileSync(writeFile, writeData)
922
+ }
923
+ console.log(green(`\n ✓ Записано в ${writeFile}\n`))
924
+ return
925
+ }
926
+ case "/edit": {
927
+ if (!arg) { console.log(dim("\n Использование: /edit <файл> <строка> <текст>\n")); return }
928
+ const editParts = arg.split(/\s+/)
929
+ const editFile = editParts[0]
930
+ const editLine = parseInt(editParts[1])
931
+ const editText = editParts.slice(2).join(" ")
932
+ if (!editFile || isNaN(editLine) || !editText) {
933
+ console.log(dim("\n Формат: /edit файл номер_строки новый_текст\n"))
934
+ return
935
+ }
936
+ try {
937
+ const lines = fs.readFileSync(editFile, "utf8").split("\n")
938
+ if (editLine < 1 || editLine > lines.length) {
939
+ console.log(red(`\n ✗ Строка ${editLine} не существует (всего ${lines.length})\n`))
940
+ return
941
+ }
942
+ lines[editLine - 1] = editText
943
+ fs.writeFileSync(editFile, lines.join("\n"))
944
+ console.log(green(`\n ✓ Строка ${editLine} обновлена в ${editFile}\n`))
945
+ } catch (e) {
946
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
947
+ }
948
+ return
949
+ }
950
+ case "/find": {
951
+ if (!arg) { console.log(dim("\n Использование: /find <паттерн>\n")); return }
952
+ try {
953
+ const { execSync } = await import("node:child_process")
954
+ const result = execSync(`dir /s /b "${arg}"`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
955
+ console.log("\n" + result + "\n")
956
+ } catch (e) {
957
+ console.log(red(`\n ✗ Файлы не найдены\n`))
958
+ }
959
+ return
960
+ }
961
+ case "/grep": {
962
+ if (!arg) { console.log(dim("\n Использование: /grep <паттерн> [файл]\n")); return }
963
+ const [grepPattern, grepFile] = arg.split(/\s+/)
964
+ try {
965
+ const { execSync } = await import("node:child_process")
966
+ const cmd = grepFile ? `findstr /N /I "${grepPattern}" "${grepFile}"` : `findstr /N /I /S "${grepPattern}" *.*`
967
+ const result = execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
968
+ console.log("\n" + result + "\n")
969
+ } catch (e) {
970
+ console.log(red(`\n ✗ Не найдено\n`))
971
+ }
972
+ return
973
+ }
974
+ case "/tree": {
975
+ const treePath = arg || "."
976
+ try {
977
+ const { execSync } = await import("node:child_process")
978
+ const result = execSync(`tree "${treePath}" /F /A`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
979
+ console.log("\n" + result + "\n")
980
+ } catch {
981
+ console.log(red(`\n ✗ Не удалось построить дерево\n`))
982
+ }
983
+ return
984
+ }
985
+ case "/head": {
986
+ if (!arg) { console.log(dim("\n Использование: /head [-n N] <файл>\n")); return }
987
+ const headLines = arg.startsWith("-n ") ? parseInt(arg.split(/\s+/)[1]) : 10
988
+ const headFile = arg.startsWith("-n ") ? arg.split(/\s+/).slice(2).join(" ") : arg
989
+ try {
990
+ const lines = fs.readFileSync(headFile, "utf8").split("\n").slice(0, headLines)
991
+ console.log("\n" + lines.join("\n") + "\n")
992
+ } catch (e) {
993
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
994
+ }
995
+ return
996
+ }
997
+ case "/tail": {
998
+ if (!arg) { console.log(dim("\n Использование: /tail [-n N] <файл>\n")); return }
999
+ const tailLines = arg.startsWith("-n ") ? parseInt(arg.split(/\s+/)[1]) : 10
1000
+ const tailFile = arg.startsWith("-n ") ? arg.split(/\s+/).slice(2).join(" ") : arg
1001
+ try {
1002
+ const lines = fs.readFileSync(tailFile, "utf8").split("\n")
1003
+ console.log("\n" + lines.slice(-tailLines).join("\n") + "\n")
1004
+ } catch (e) {
1005
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
1006
+ }
1007
+ return
1008
+ }
1009
+ case "/wc": {
1010
+ if (!arg) { console.log(dim("\n Использование: /wc <файл>\n")); return }
1011
+ try {
1012
+ const content = fs.readFileSync(arg.trim(), "utf8")
1013
+ const lines = content.split("\n").length
1014
+ const words = content.split(/\s+/).length
1015
+ const chars = content.length
1016
+ console.log(`\n ${lines} строк · ${words} слов · ${chars} символов\n`)
1017
+ } catch (e) {
1018
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
1019
+ }
1020
+ return
1021
+ }
1022
+ case "/stat": {
1023
+ if (!arg) { console.log(dim("\n Использование: /stat <файл>\n")); return }
1024
+ try {
1025
+ const stat = fs.statSync(arg.trim())
1026
+ console.log(box([
1027
+ dim("Имя: ") + white(path.basename(arg)),
1028
+ dim("Тип: ") + white(stat.isDirectory() ? "Папка" : "Файл"),
1029
+ dim("Размер: ") + white(`${(stat.size / 1024).toFixed(1)} KB`),
1030
+ dim("Создан: ") + white(stat.birthtime.toLocaleString()),
1031
+ dim("Изменён: ") + white(stat.mtime.toLocaleString()),
1032
+ dim("Права: ") + white(stat.mode.toString(8)),
1033
+ ], { title: "Статистика файла", padding: 2 }))
1034
+ console.log()
1035
+ } catch (e) {
1036
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
1037
+ }
1038
+ return
1039
+ }
1040
+
1041
+ // ═══════════════════════════════════════════════════
1042
+ // GIT
1043
+ // ═══════════════════════════════════════════════════
1044
+ case "/git": {
1045
+ if (!arg) {
1046
+ console.log(box([
1047
+ violet("/git status") + dim(" — статус"),
1048
+ violet("/git diff") + dim(" — различия"),
1049
+ violet("/git log") + dim(" — история коммитов"),
1050
+ violet("/git commit") + dim(" — AI-коммит"),
1051
+ violet("/git push") + dim(" — отправить"),
1052
+ violet("/git pull") + dim(" — получить"),
1053
+ violet("/git branch") + dim(" — ветки"),
1054
+ violet("/git checkout <ветка>") + dim(" — переключить"),
1055
+ violet("/git stash") + dim(" — сохранить изменения"),
1056
+ violet("/git pop") + dim(" — восстановить"),
1057
+ violet("/git tag <имя>") + dim(" — создать тег"),
1058
+ violet("/git clone <url>") + dim(" — клонировать"),
1059
+ violet("/git init") + dim(" — инициализировать"),
1060
+ ], { title: "Git операции", padding: 2 }))
1061
+ console.log()
1062
+ return
1063
+ }
1064
+ const [gitCmd, ...gitArgs] = arg.split(/\s+/)
1065
+ const gitFull = `git ${gitCmd} ${gitArgs.join(" ")}`.trim()
1066
+ try {
1067
+ const { execSync } = await import("node:child_process")
1068
+ const result = execSync(gitFull, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1069
+ console.log("\n" + result + "\n")
1070
+ } catch (e) {
1071
+ console.log(red(`\n ✗ git ${gitCmd}: ${e.stderr || e.message}\n`))
1072
+ }
1073
+ return
1074
+ }
1075
+ case "/log": {
1076
+ try {
1077
+ const { execSync } = await import("node:child_process")
1078
+ const result = execSync("git log --oneline -20", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1079
+ console.log("\n" + result + "\n")
1080
+ } catch {
1081
+ console.log(red("\n ✗ Не удалось получить git log\n"))
1082
+ }
1083
+ return
1084
+ }
1085
+ case "/status": {
1086
+ try {
1087
+ const { execSync } = await import("node:child_process")
1088
+ const result = execSync("git status", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1089
+ console.log("\n" + result + "\n")
1090
+ } catch {
1091
+ console.log(red("\n ✗ Не удалось получить git status\n"))
1092
+ }
1093
+ return
1094
+ }
1095
+ case "/branch": {
1096
+ try {
1097
+ const { execSync } = await import("node:child_process")
1098
+ const result = execSync("git branch -a", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1099
+ console.log("\n" + result + "\n")
1100
+ } catch {
1101
+ console.log(red("\n ✗ Не удалось получить git branch\n"))
1102
+ }
1103
+ return
1104
+ }
1105
+ case "/checkout": {
1106
+ if (!arg) { console.log(dim("\n Использование: /checkout <ветка>\n")); return }
1107
+ try {
1108
+ const { execSync } = await import("node:child_process")
1109
+ execSync(`git checkout ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1110
+ console.log(green(`\n ✓ Переключено на ${arg}\n`))
1111
+ } catch (e) {
1112
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1113
+ }
1114
+ return
1115
+ }
1116
+ case "/merge": {
1117
+ if (!arg) { console.log(dim("\n Использование: /merge <ветка>\n")); return }
1118
+ try {
1119
+ const { execSync } = await import("node:child_process")
1120
+ execSync(`git merge ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1121
+ console.log(green(`\n ✓ Слияние с ${arg} выполнено\n`))
1122
+ } catch (e) {
1123
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1124
+ }
1125
+ return
1126
+ }
1127
+ case "/stash": {
1128
+ try {
1129
+ const { execSync } = await import("node:child_process")
1130
+ execSync("git stash", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1131
+ console.log(green("\n ✓ Изменения сохранены в stash\n"))
1132
+ } catch (e) {
1133
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1134
+ }
1135
+ return
1136
+ }
1137
+ case "/pop": {
1138
+ try {
1139
+ const { execSync } = await import("node:child_process")
1140
+ execSync("git stash pop", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1141
+ console.log(green("\n ✓ Изменения восстановлены из stash\n"))
1142
+ } catch (e) {
1143
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1144
+ }
1145
+ return
1146
+ }
1147
+ case "/tag": {
1148
+ if (!arg) { console.log(dim("\n Использование: /tag <имя_тега>\n")); return }
1149
+ try {
1150
+ const { execSync } = await import("node:child_process")
1151
+ execSync(`git tag ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1152
+ console.log(green(`\n ✓ Тег ${arg} создан\n`))
1153
+ } catch (e) {
1154
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1155
+ }
1156
+ return
1157
+ }
1158
+ case "/remote": {
1159
+ try {
1160
+ const { execSync } = await import("node:child_process")
1161
+ const result = execSync("git remote -v", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1162
+ console.log("\n" + (result || "Нет remote репозиториев") + "\n")
1163
+ } catch {
1164
+ console.log(red("\n ✗ Не удалось получить git remote\n"))
1165
+ }
1166
+ return
1167
+ }
1168
+ case "/pull": {
1169
+ try {
1170
+ const { execSync } = await import("node:child_process")
1171
+ execSync("git pull", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1172
+ console.log(green("\n ✓ Pull выполнен\n"))
1173
+ } catch (e) {
1174
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1175
+ }
1176
+ return
1177
+ }
1178
+ case "/push": {
1179
+ try {
1180
+ const { execSync } = await import("node:child_process")
1181
+ execSync("git push", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1182
+ console.log(green("\n ✓ Push выполнен\n"))
1183
+ } catch (e) {
1184
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1185
+ }
1186
+ return
1187
+ }
1188
+ case "/clone": {
1189
+ if (!arg) { console.log(dim("\n Использование: /clone <url>\n")); return }
1190
+ try {
1191
+ const { execSync } = await import("node:child_process")
1192
+ execSync(`git clone ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1193
+ console.log(green(`\n ✓ Клонировано из ${arg}\n`))
1194
+ } catch (e) {
1195
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1196
+ }
1197
+ return
1198
+ }
1199
+
1200
+ // ═══════════════════════════════════════════════════
1201
+ // AI И АГЕНТЫ
1202
+ // ═══════════════════════════════════════════════════
1203
+ case "/todo": {
1204
+ const todoPath = path.join(process.cwd(), ".stella-todos.json")
1205
+ let todos = []
1206
+ try { todos = JSON.parse(fs.readFileSync(todoPath, "utf8")) } catch {}
1207
+
1208
+ if (!arg || arg === "list" || arg === "показать") {
1209
+ if (todos.length === 0) {
1210
+ console.log(dim("\n Нет задач. Добавь: /todo add <текст>\n"))
1211
+ } else {
1212
+ console.log()
1213
+ todos.forEach((t, i) => {
1214
+ const status = t.done ? green("✓") : yellow("○")
1215
+ console.log(` ${status} ${white(String(i + 1))}. ${t.done ? dim(t.text) : t.text}`)
1216
+ })
1217
+ console.log()
1218
+ }
1219
+ return
1220
+ }
1221
+ if (arg.startsWith("add ") || arg.startsWith("добавить ")) {
1222
+ const text = arg.replace(/^(add|добавить)\s+/, "")
1223
+ todos.push({ text, done: false, created: new Date().toISOString() })
1224
+ fs.writeFileSync(todoPath, JSON.stringify(todos, null, 2))
1225
+ console.log(green(`\n ✓ Задача добавлена: ${text}\n`))
1226
+ return
1227
+ }
1228
+ if (arg.startsWith("done ") || arg.startsWith("выполнитb ")) {
1229
+ const idx = parseInt(arg.replace(/^(done|выполнитb)\s+/, "")) - 1
1230
+ if (todos[idx]) {
1231
+ todos[idx].done = true
1232
+ fs.writeFileSync(todoPath, JSON.stringify(todos, null, 2))
1233
+ console.log(green(`\n ✓ Задача ${idx + 1} выполнена\n`))
1234
+ } else {
1235
+ console.log(red("\n ✗ Неверный номер задачи\n"))
1236
+ }
1237
+ return
1238
+ }
1239
+ if (arg.startsWith("rm ") || arg.startsWith("удалить ")) {
1240
+ const idx = parseInt(arg.replace(/^(rm|удалить)\s+/, "")) - 1
1241
+ if (todos[idx]) {
1242
+ todos.splice(idx, 1)
1243
+ fs.writeFileSync(todoPath, JSON.stringify(todos, null, 2))
1244
+ console.log(green(`\n ✓ Задача удалена\n`))
1245
+ } else {
1246
+ console.log(red("\n ✗ Неверный номер задачи\n"))
1247
+ }
1248
+ return
1249
+ }
1250
+ if (arg === "clear" || arg === "очистить") {
1251
+ fs.writeFileSync(todoPath, "[]")
1252
+ console.log(green("\n ✓ Все задачи удалены\n"))
1253
+ return
1254
+ }
1255
+ console.log(dim("\n Использование: /todo [add|done|rm|list|clear] [аргумент]\n"))
1256
+ return
1257
+ }
1258
+ case "/remember": {
1259
+ if (!arg) { console.log(dim("\n Использование: /remember <информация для запоминания>\n")); return }
1260
+ const memoryPath = path.join(process.cwd(), "STELLA.md")
1261
+ const entry = `\n\n## ${new Date().toLocaleString()}\n${arg}`
1262
+ if (fs.existsSync(memoryPath)) {
1263
+ fs.appendFileSync(memoryPath, entry)
1264
+ } else {
1265
+ fs.writeFileSync(memoryPath, `# Память проекта Stella\n${entry}`)
1266
+ }
1267
+ console.log(green("\n ✓ Информация сохранена в STELLA.md\n"))
1268
+ return
1269
+ }
1270
+ case "/forget": {
1271
+ if (!arg) { console.log(dim("\n Использование: /forget <что удалить>\n")); return }
1272
+ const forgetPath = path.join(process.cwd(), "STELLA.md")
1273
+ if (fs.existsSync(forgetPath)) {
1274
+ let content = fs.readFileSync(forgetPath, "utf8")
1275
+ content = content.replace(arg, "")
1276
+ fs.writeFileSync(forgetPath, content)
1277
+ console.log(green("\n ✓ Информация удалена из памяти\n"))
1278
+ } else {
1279
+ console.log(dim("\n STELLA.md не найден\n"))
1280
+ }
1281
+ return
1282
+ }
1283
+ case "/skill": {
1284
+ console.log(box([
1285
+ violet("/skill list") + dim(" — показать доступные навыки"),
1286
+ violet("/skill load <название>") + dim(" — загрузить навык"),
1287
+ violet("/skill search <запрос>") + dim(" — найти навык"),
1288
+ "",
1289
+ dim("Навыки расширяют возможности Stella."),
1290
+ dim("Смотри: ") + cyan("https://opencode.ai/skills"),
1291
+ ], { title: "Навыки (Skills)", padding: 2 }))
1292
+ console.log()
1293
+ return
1294
+ }
1295
+ case "/mcp": {
1296
+ console.log(box([
1297
+ violet("/mcp list") + dim(" — список MCP серверов"),
1298
+ violet("/mcp add <url>") + dim(" — добавить сервер"),
1299
+ violet("/mcp remove <url>") + dim(" — удалить сервер"),
1300
+ violet("/mcp status") + dim(" — статус подключений"),
1301
+ "",
1302
+ dim("MCP (Model Context Protocol) расширяет инструменты AI."),
1303
+ ], { title: "MCP Серверы", padding: 2 }))
1304
+ console.log()
1305
+ return
1306
+ }
1307
+
1308
+ // ═══════════════════════════════════════════════════
1309
+ // ВЕБ И КОНТЕНТ
1310
+ // ═══════════════════════════════════════════════════
1311
+ case "/web": {
1312
+ if (!arg) { console.log(dim("\n Использование: /web <поисковый запрос>\n")); return }
1313
+ await runTurn(`Найди в интернете: ${arg}. Верни краткую выжимку с источниками.`)
1314
+ return
1315
+ }
1316
+ case "/read-image": {
1317
+ if (!arg) { console.log(dim("\n Использование: /read-image <путь к изображению>\n")); return }
1318
+ console.log(dim("\n Анализирую изображение...\n"))
1319
+ await runTurn(`Проанализируй изображение ${arg}. Опиши что на нём изображено.`)
1320
+ return
1321
+ }
1322
+ case "/read-pdf": {
1323
+ if (!arg) { console.log(dim("\n Использование: /read-pdf <путь к PDF>\n")); return }
1324
+ console.log(dim("\n Извлекаю текст из PDF...\n"))
1325
+ await runTurn(`Прочитай PDF файл ${arg}. Извлеки и верни текст.`)
1326
+ return
1327
+ }
1328
+ case "/explain": {
1329
+ if (!arg) { console.log(dim("\n Использование: /explain <файл или код>\n")); return }
1330
+ await runTurn(`Объясни что делает этот код/файл: ${arg}. Будь подробным и понятным.`)
1331
+ return
1332
+ }
1333
+ case "/review": {
1334
+ if (!arg) { console.log(dim("\n Использование: /review <файл>\n")); return }
1335
+ await runTurn(`Сделай код-ревью файла ${arg}. Найди баги, улучшения, проблемы безопасности.`)
1336
+ return
1337
+ }
1338
+ case "/refactor": {
1339
+ if (!arg) { console.log(dim("\n Использование: /refactor <файл>\n")); return }
1340
+ await runTurn(`Проведи рефакторинг файла ${arg}. Улучши читаемость, производительность, архитектуру.`)
1341
+ return
1342
+ }
1343
+ case "/translate": {
1344
+ if (!arg) { console.log(dim("\n Использование: /translate <текст> [язык]\n")); return }
1345
+ await runTurn(`Переведи текст: ${arg}`)
1346
+ return
1347
+ }
1348
+ case "/summarize": {
1349
+ if (!arg) { console.log(dim("\n Использование: /summarize <текст или файл>\n")); return }
1350
+ await runTurn(`Суммаризируй: ${arg}`)
1351
+ return
1352
+ }
1353
+
1354
+ // ═══════════════════════════════════════════════════
1355
+ // ТЕСТИРОВАНИЕ И СБОРКА
1356
+ // ═══════════════════════════════════════════════════
1357
+ case "/test": {
1358
+ const testCmd = arg || "npm test"
1359
+ console.log(dim(`\n Запускаю: ${testCmd}\n`))
1360
+ try {
1361
+ const { execSync } = await import("node:child_process")
1362
+ const result = execSync(testCmd, { encoding: "utf8", stdio: "inherit" })
1363
+ } catch (e) {
1364
+ console.log(red(`\n ✗ Тесты завершились с ошибкой\n`))
1365
+ }
1366
+ return
1367
+ }
1368
+ case "/lint": {
1369
+ const lintCmd = arg || "npm run lint"
1370
+ console.log(dim(`\n Запускаю: ${lintCmd}\n`))
1371
+ try {
1372
+ const { execSync } = await import("node:child_process")
1373
+ execSync(lintCmd, { encoding: "utf8", stdio: "inherit" })
1374
+ } catch {
1375
+ console.log(red(`\n ✗ Линтер завершился с ошибкой\n`))
1376
+ }
1377
+ return
1378
+ }
1379
+ case "/format": {
1380
+ const formatCmd = arg || "npx prettier --write ."
1381
+ console.log(dim(`\n Форматирую: ${formatCmd}\n`))
1382
+ try {
1383
+ const { execSync } = await import("node:child_process")
1384
+ execSync(formatCmd, { encoding: "utf8", stdio: "inherit" })
1385
+ console.log(green("\n ✓ Код отформатирован\n"))
1386
+ } catch {
1387
+ console.log(red(`\n ✗ Ошибка форматирования\n`))
1388
+ }
1389
+ return
1390
+ }
1391
+ case "/typecheck": {
1392
+ const typeCmd = arg || "npx tsc --noEmit"
1393
+ console.log(dim(`\n Проверяю типы: ${typeCmd}\n`))
1394
+ try {
1395
+ const { execSync } = await import("node:child_process")
1396
+ execSync(typeCmd, { encoding: "utf8", stdio: "inherit" })
1397
+ console.log(green("\n ✓ Типы корректны\n"))
1398
+ } catch {
1399
+ console.log(red(`\n ✗ Ошибка проверки типов\n`))
1400
+ }
1401
+ return
1402
+ }
1403
+ case "/build": {
1404
+ const buildCmd = arg || "npm run build"
1405
+ console.log(dim(`\n Собираю: ${buildCmd}\n`))
1406
+ try {
1407
+ const { execSync } = await import("node:child_process")
1408
+ execSync(buildCmd, { encoding: "utf8", stdio: "inherit" })
1409
+ console.log(green("\n ✓ Сборка завершена\n"))
1410
+ } catch {
1411
+ console.log(red(`\n ✗ Ошибка сборки\n`))
1412
+ }
1413
+ return
1414
+ }
1415
+ case "/run": {
1416
+ if (!arg) { console.log(dim("\n Использование: /run <команда>\n")); return }
1417
+ console.log(dim(`\n Запускаю: ${arg}\n`))
1418
+ try {
1419
+ const { execSync } = await import("node:child_process")
1420
+ execSync(arg, { encoding: "utf8", stdio: "inherit" })
1421
+ } catch {
1422
+ console.log(red(`\n ✗ Команда завершилась с ошибкой\n`))
1423
+ }
1424
+ return
1425
+ }
1426
+ case "/debug": {
1427
+ if (!arg) { console.log(dim("\n Использование: /debug <команда>\n")); return }
1428
+ console.log(dim(`\n Отладка: ${arg}\n`))
1429
+ try {
1430
+ const { execSync } = await import("node:child_process")
1431
+ execSync(`node --inspect ${arg}`, { encoding: "utf8", stdio: "inherit" })
1432
+ } catch {
1433
+ console.log(red(`\n ✗ Ошибка отладки\n`))
1434
+ }
1435
+ return
1436
+ }
1437
+
1438
+ // ═══════════════════════════════════════════════════
1439
+ // БЕЗОПАСНОСТЬ
1440
+ // ═══════════════════════════════════════════════════
1441
+ case "/audit": {
1442
+ console.log(dim("\n Аудит безопасности...\n"))
1443
+ await runTurn(`Проведи аудит безопасности этого проекта. Проверь зависимости, конфиги, секреты.`)
1444
+ return
1445
+ }
1446
+ case "/secrets": {
1447
+ console.log(dim("\n Поиск секретов в коде...\n"))
1448
+ try {
1449
+ const { execSync } = await import("node:child_process")
1450
+ const result = execSync('findstr /S /I /N "password api_key secret token private_key" *.*', { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1451
+ if (result.trim()) {
1452
+ console.log(yellow("\n ⚠ Найдены потенциальные секреты:\n"))
1453
+ console.log(result)
1454
+ } else {
1455
+ console.log(green("\n ✓ Секреты не найдены\n"))
1456
+ }
1457
+ } catch {
1458
+ console.log(green("\n ✓ Секреты не найдены\n"))
1459
+ }
1460
+ return
1461
+ }
1462
+ case "/hash": {
1463
+ if (!arg) { console.log(dim("\n Использование: /hash <файл>\n")); return }
1464
+ try {
1465
+ const crypto = await import("node:crypto")
1466
+ const content = fs.readFileSync(arg.trim())
1467
+ const sha256 = crypto.createHash("sha256").update(content).digest("hex")
1468
+ const md5 = crypto.createHash("md5").update(content).digest("hex")
1469
+ console.log(box([
1470
+ dim("Файл: ") + white(path.basename(arg)),
1471
+ dim("SHA256:") + cyan(sha256),
1472
+ dim("MD5: ") + cyan(md5),
1473
+ dim("Размер:") + white(`${(content.length / 1024).toFixed(1)} KB`),
1474
+ ], { title: "Хеш файла", padding: 2 }))
1475
+ console.log()
1476
+ } catch (e) {
1477
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
1478
+ }
1479
+ return
1480
+ }
1481
+
1482
+ // ═══════════════════════════════════════════════════
1483
+ // ПАКЕТЫ
1484
+ // ═══════════════════════════════════════════════════
1485
+ case "/install": {
1486
+ if (!arg) { console.log(dim("\n Использование: /install <пакет>\n")); return }
1487
+ const pkgManager = fs.existsSync("pnpm-lock.yaml") ? "pnpm" : fs.existsSync("yarn.lock") ? "yarn" : "npm"
1488
+ console.log(dim(`\n Устанавливаю ${arg} через ${pkgManager}...\n`))
1489
+ try {
1490
+ const { execSync } = await import("node:child_process")
1491
+ execSync(`${pkgManager} install ${arg}`, { encoding: "utf8", stdio: "inherit" })
1492
+ console.log(green(`\n ✓ ${arg} установлен\n`))
1493
+ } catch {
1494
+ console.log(red(`\n ✗ Ошибка установки\n`))
1495
+ }
1496
+ return
1497
+ }
1498
+ case "/uninstall": {
1499
+ if (!arg) { console.log(dim("\n Использование: /uninstall <пакет>\n")); return }
1500
+ const pkgManager2 = fs.existsSync("pnpm-lock.yaml") ? "pnpm" : fs.existsSync("yarn.lock") ? "yarn" : "npm"
1501
+ console.log(dim(`\n Удаляю ${arg}...\n`))
1502
+ try {
1503
+ const { execSync } = await import("node:child_process")
1504
+ execSync(`${pkgManager2} uninstall ${arg}`, { encoding: "utf8", stdio: "inherit" })
1505
+ console.log(green(`\n ✓ ${arg} удалён\n`))
1506
+ } catch {
1507
+ console.log(red(`\n ✗ Ошибка удаления\n`))
1508
+ }
1509
+ return
1510
+ }
1511
+ case "/outdated": {
1512
+ const pkgManager3 = fs.existsSync("pnpm-lock.yaml") ? "pnpm" : fs.existsSync("yarn.lock") ? "yarn" : "npm"
1513
+ console.log(dim(`\n Проверяю устаревшие пакеты...\n`))
1514
+ try {
1515
+ const { execSync } = await import("node:child_process")
1516
+ execSync(`${pkgManager3} outdated`, { encoding: "utf8", stdio: "inherit" })
1517
+ } catch {
1518
+ console.log(dim("\n Все пакеты актуальны\n"))
1519
+ }
1520
+ return
1521
+ }
1522
+ case "/deps": {
1523
+ if (fs.existsSync("package.json")) {
1524
+ const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
1525
+ console.log()
1526
+ if (pkg.dependencies) {
1527
+ console.log(dim(" Зависимости:"))
1528
+ for (const [name, ver] of Object.entries(pkg.dependencies)) {
1529
+ console.log(` ${white(name)} ${dim(ver)}`)
1530
+ }
1531
+ }
1532
+ if (pkg.devDependencies) {
1533
+ console.log(dim("\n Dev-зависимости:"))
1534
+ for (const [name, ver] of Object.entries(pkg.devDependencies)) {
1535
+ console.log(` ${white(name)} ${dim(ver)}`)
1536
+ }
1537
+ }
1538
+ console.log()
1539
+ } else {
1540
+ console.log(dim("\n package.json не найден\n"))
1541
+ }
1542
+ return
1543
+ }
1544
+
1545
+ // ═══════════════════════════════════════════════════
1546
+ // ОКРУЖЕНИЕ
1547
+ // ═══════════════════════════════════════════════════
1548
+ case "/env": {
1549
+ console.log()
1550
+ if (arg) {
1551
+ console.log(` ${dim(arg)}=${white(process.env[arg] || "не задана")}`)
1552
+ } else {
1553
+ for (const [key, val] of Object.entries(process.env).sort()) {
1554
+ console.log(` ${dim(key)}=${white(String(val).slice(0, 80))}`)
1555
+ }
1556
+ }
1557
+ console.log()
1558
+ return
1559
+ }
1560
+ case "/set-env": {
1561
+ if (!arg) { console.log(dim("\n Использование: /set-env КЛЮЧ=ЗНАЧЕНИЕ\n")); return }
1562
+ const [envKey, ...envVal] = arg.split("=")
1563
+ process.env[envKey] = envVal.join("=")
1564
+ console.log(green(`\n ✓ ${envKey}=${process.env[envKey]}\n`))
1565
+ return
1566
+ }
1567
+ case "/path": {
1568
+ console.log("\n" + (process.env.PATH || "").split(path.delimiter).join("\n") + "\n")
1569
+ return
1570
+ }
1571
+ case "/which": {
1572
+ if (!arg) { console.log(dim("\n Использование: /which <команда>\n")); return }
1573
+ try {
1574
+ const { execSync } = await import("node:child_process")
1575
+ const result = execSync(`where ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1576
+ console.log("\n" + result + "\n")
1577
+ } catch {
1578
+ console.log(red(`\n ✗ ${arg} не найден\n`))
1579
+ }
1580
+ return
1581
+ }
1582
+ case "/whoami": {
1583
+ console.log("\n" + os.userInfo().username + "\n")
1584
+ return
1585
+ }
1586
+ case "/hostname": {
1587
+ console.log("\n" + os.hostname() + "\n")
1588
+ return
1589
+ }
1590
+ case "/uname": {
1591
+ console.log(`\n${os.platform()} ${os.release()} (${os.arch()})\n`)
1592
+ return
1593
+ }
1594
+ case "/uptime": {
1595
+ const uptime = os.uptime()
1596
+ const hours = Math.floor(uptime / 3600)
1597
+ const mins = Math.floor((uptime % 3600) / 60)
1598
+ console.log(`\n Время работы: ${hours}ч ${mins}м\n`)
1599
+ return
1600
+ }
1601
+ case "/disk": {
1602
+ try {
1603
+ const { execSync } = await import("node:child_process")
1604
+ const result = execSync("wmic logicaldisk get size,freespace,caption", { encoding: "utf8" })
1605
+ console.log("\n" + result + "\n")
1606
+ } catch {
1607
+ console.log(red("\n ✗ Не удалось получить информацию о дисках\n"))
1608
+ }
1609
+ return
1610
+ }
1611
+ case "/mem": {
1612
+ const total = os.totalmem()
1613
+ const free = os.freemem()
1614
+ const used = total - free
1615
+ console.log(box([
1616
+ dim("Всего: ") + white(`${(total / 1e9).toFixed(1)} GB`),
1617
+ dim("Используется:") + yellow(` ${(used / 1e9).toFixed(1)} GB (${((used / total) * 100).toFixed(1)}%)`),
1618
+ dim("Свободно: ") + green(`${(free / 1e9).toFixed(1)} GB`),
1619
+ ], { title: "Память", padding: 2 }))
1620
+ console.log()
1621
+ return
1622
+ }
1623
+ case "/cpu": {
1624
+ console.log(box([
1625
+ dim("Модель: ") + white(os.cpus()[0]?.model || "N/A"),
1626
+ dim("Ядер: ") + white(String(os.cpus().length)),
1627
+ dim("Архитектура:") + white(os.arch()),
1628
+ ], { title: "CPU", padding: 2 }))
1629
+ console.log()
1630
+ return
1631
+ }
1632
+ case "/net": {
1633
+ try {
1634
+ const { execSync } = await import("node:child_process")
1635
+ const result = execSync("ipconfig", { encoding: "utf8" })
1636
+ console.log("\n" + result + "\n")
1637
+ } catch {
1638
+ console.log(red("\n ✗ Не удалось получить сетевую информацию\n"))
1639
+ }
1640
+ return
1641
+ }
1642
+ case "/ping": {
1643
+ if (!arg) { console.log(dim("\n Использование: /ping <хост>\n")); return }
1644
+ try {
1645
+ const { execSync } = await import("node:child_process")
1646
+ const result = execSync(`ping -n 4 ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1647
+ console.log("\n" + result + "\n")
1648
+ } catch (e) {
1649
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1650
+ }
1651
+ return
1652
+ }
1653
+ case "/curl": {
1654
+ if (!arg) { console.log(dim("\n Использование: /curl <url>\n")); return }
1655
+ try {
1656
+ const { execSync } = await import("node:child_process")
1657
+ const result = execSync(`curl -s ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1658
+ console.log("\n" + result.slice(0, 2000) + "\n")
1659
+ } catch (e) {
1660
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1661
+ }
1662
+ return
1663
+ }
1664
+ case "/wget": {
1665
+ if (!arg) { console.log(dim("\n Использование: /wget <url>\n")); return }
1666
+ try {
1667
+ const { execSync } = await import("node:child_process")
1668
+ execSync(`curl -O ${arg}`, { encoding: "utf8", stdio: "inherit" })
1669
+ console.log(green(`\n ✓ Загружено из ${arg}\n`))
1670
+ } catch (e) {
1671
+ console.log(red(`\n ✗ Ошибка загрузки\n`))
1672
+ }
1673
+ return
1674
+ }
1675
+
1676
+ // ═══════════════════════════════════════════════════
1677
+ // МОНИТОРИНГ
1678
+ // ═══════════════════════════════════════════════════
1679
+ case "/ps": {
1680
+ try {
1681
+ const { execSync } = await import("node:child_process")
1682
+ const result = execSync("tasklist /FO TABLE", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1683
+ console.log("\n" + result + "\n")
1684
+ } catch {
1685
+ console.log(red("\n ✗ Не удалось получить список процессов\n"))
1686
+ }
1687
+ return
1688
+ }
1689
+ case "/kill": {
1690
+ if (!arg) { console.log(dim("\n Использование: /kill <PID или имя>\n")); return }
1691
+ try {
1692
+ const { execSync } = await import("node:child_process")
1693
+ execSync(`taskkill /F /IM ${arg}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
1694
+ console.log(green(`\n ✓ Процесс ${arg} завершён\n`))
1695
+ } catch (e) {
1696
+ console.log(red(`\n ✗ ${e.stderr || e.message}\n`))
1697
+ }
1698
+ return
1699
+ }
1700
+
1701
+ // ═══════════════════════════════════════════════════
1702
+ // ДОКУМЕНТАЦИЯ
1703
+ // ═══════════════════════════════════════════════════
1704
+ case "/docs": {
1705
+ console.log(dim("\n Генерирую документацию...\n"))
1706
+ await runTurn(`Сгенерируй документацию для этого проекта. Включи: описание, установку, использование, API.`)
1707
+ return
1708
+ }
1709
+ case "/readme": {
1710
+ console.log(dim("\n Генерирую README...\n"))
1711
+ await runTurn(`Сгенерируй README.md для этого проекта. Включи: описание, особенности, установку, использование, лицензию.`)
1712
+ return
1713
+ }
1714
+ case "/changelog": {
1715
+ console.log(dim("\n Генерирую changelog...\n"))
1716
+ await runTurn(`Сгенерируй CHANGELOG.md на основе git log. Используй формат Keep a Changelog.`)
1717
+ return
1718
+ }
1719
+ case "/examples": {
1720
+ console.log(dim("\n Генерирую примеры...\n"))
1721
+ await runTurn(`Сгенерируй примеры использования этого проекта. Покажи базовые и продвинутые сценарии.`)
1722
+ return
1723
+ }
1724
+ case "/tutorial": {
1725
+ console.log(dim("\n Создаю обучение...\n"))
1726
+ await runTurn(`Создай пошаговое обучение для этого проекта. Начни с основ, дойди до продвинутых тем.`)
1727
+ return
1728
+ }
1729
+ case "/wizard": {
1730
+ console.log(dim("\n Запускаю мастер настройки...\n"))
1731
+ await runTurn(`Проведи через мастер настройки этого проекта. Определи стек, настрой окружение, проверь зависимости.`)
1732
+ return
1733
+ }
1734
+
1735
+ // ═══════════════════════════════════════════════════
1736
+ // СЕССИИ
1737
+ // ═══════════════════════════════════════════════════
1738
+ case "/save": {
1739
+ const savePath = arg || path.join(CONFIG_DIR, `session-${Date.now()}.json`)
1740
+ const session = { model: state.model, messages: state.messages, savedAt: new Date().toISOString() }
1741
+ fs.writeFileSync(savePath, JSON.stringify(session, null, 2))
1742
+ console.log(green(`\n ✓ Сессия сохранена в ${savePath}\n`))
1743
+ return
1744
+ }
1745
+ case "/load": {
1746
+ if (!arg) { console.log(dim("\n Использование: /load <путь к сессии>\n")); return }
1747
+ try {
1748
+ const session = JSON.parse(fs.readFileSync(arg.trim(), "utf8"))
1749
+ state.model = session.model || state.model
1750
+ state.messages = session.messages || []
1751
+ console.log(green(`\n ✓ Сессия загружена (${state.messages.length} сообщений)\n`))
1752
+ } catch (e) {
1753
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
1754
+ }
1755
+ return
1756
+ }
1757
+ case "/sessions": {
1758
+ try {
1759
+ const files = fs.readdirSync(CONFIG_DIR).filter(f => f.startsWith("session-") && f.endsWith(".json"))
1760
+ if (files.length === 0) {
1761
+ console.log(dim("\n Нет сохранённых сессий\n"))
1762
+ } else {
1763
+ console.log()
1764
+ files.forEach((f, i) => {
1765
+ const stat = fs.statSync(path.join(CONFIG_DIR, f))
1766
+ console.log(` ${white(String(i + 1))}. ${dim(f)} ${gray(stat.mtime.toLocaleString())}`)
1767
+ })
1768
+ console.log()
1769
+ }
1770
+ } catch {
1771
+ console.log(dim("\n Нет сохранённых сессий\n"))
1772
+ }
1773
+ return
1774
+ }
1775
+
1776
+ // ═══════════════════════════════════════════════════
1777
+ // ДОПОЛНИТЕЛЬНО
1778
+ // ═══════════════════════════════════════════════════
1779
+ case "/color":
1780
+ case "/theme": {
1781
+ console.log(box([
1782
+ violet("1") + dim(" — Violet (по умолчанию)"),
1783
+ violet("2") + dim(" — Ocean"),
1784
+ violet("3") + dim(" — Forest"),
1785
+ violet("4") + dim(" — Sunset"),
1786
+ violet("5") + dim(" — Monokai"),
1787
+ ], { title: "Темы оформления", padding: 2 }))
1788
+ console.log()
1789
+ return
1790
+ }
1791
+ case "/lang": {
1792
+ console.log(box([
1793
+ violet("1") + dim(" — Русский (по умолчанию)"),
1794
+ violet("2") + dim(" — English"),
1795
+ violet("3") + dim(" — 한국어"),
1796
+ violet("4") + dim(" — 中文"),
1797
+ violet("5") + dim(" — 日本語"),
1798
+ ], { title: "Язык интерфейса", padding: 2 }))
1799
+ console.log()
1800
+ return
1801
+ }
1802
+ case "/alias": {
1803
+ if (!arg) {
1804
+ console.log(dim("\n Использование: /alias <имя> <команда>\n"))
1805
+ return
1806
+ }
1807
+ const [aliasName, ...aliasCmd] = arg.split(/\s+/)
1808
+ const aliasFile = path.join(CONFIG_DIR, "aliases.json")
1809
+ let aliases = {}
1810
+ try { aliases = JSON.parse(fs.readFileSync(aliasFile, "utf8")) } catch {}
1811
+ aliases[aliasName] = aliasCmd.join(" ")
1812
+ fs.writeFileSync(aliasFile, JSON.stringify(aliases, null, 2))
1813
+ console.log(green(`\n ✓ Алиас ${aliasName} создан\n`))
1814
+ return
1815
+ }
1816
+ case "/shortcut": {
1817
+ console.log(box([
1818
+ dim("Ctrl+C") + white(" — прервать (2x — выход)"),
1819
+ dim("Ctrl+D") + white(" — выход"),
1820
+ dim("Tab") + white(" — автодополнение команд"),
1821
+ dim("!команда") + white(" — shell-команда напрямую"),
1822
+ dim("/help") + white(" — все команды"),
1823
+ ], { title: "Горячие клавиши", padding: 2 }))
1824
+ console.log()
1825
+ return
1826
+ }
1827
+ case "/feedback": {
1828
+ console.log(dim("\n Отправь отзыв: ") + cyan("https://github.com/anomalyco/opencode/issues\n"))
1829
+ return
1830
+ }
1831
+ case "/report": {
1832
+ console.log(dim("\n Сообщи об ошибке: ") + cyan("https://github.com/anomalyco/opencode/issues\n"))
1833
+ return
1834
+ }
1835
+ case "/support": {
1836
+ console.log(box([
1837
+ dim("Документация: ") + cyan("https://opencode.ai/docs"),
1838
+ dim("GitHub: ") + cyan("https://github.com/anomalyco/opencode"),
1839
+ dim("Issues: ") + cyan("https://github.com/anomalyco/opencode/issues"),
1840
+ dim("Discord: ") + cyan("https://discord.gg/opencode"),
1841
+ ], { title: "Поддержка", padding: 2 }))
1842
+ console.log()
1843
+ return
1844
+ }
1845
+
1846
+ // ═══════════════════════════════════════════════════
1847
+ // УПРАВЛЕНИЕ КОМПЬЮТЕРОМ
1848
+ // ═══════════════════════════════════════════════════
1849
+ case "/open":
1850
+ case "/app": {
1851
+ if (!arg) { console.log(dim("\n Использование: /open <приложение или URL>\n")); return }
1852
+ const appMap = {
1853
+ "chrome": "chrome.exe", "google chrome": "chrome.exe",
1854
+ "firefox": "firefox.exe", "edge": "msedge.exe", "browser": "msedge.exe",
1855
+ "notepad": "notepad.exe", "explorer": "explorer.exe", "files": "explorer.exe",
1856
+ "calc": "calc.exe", "калькулятор": "calc.exe",
1857
+ "paint": "mspaint.exe", "word": "winword.exe", "excel": "excel.exe",
1858
+ "powerpoint": "powerpnt.exe", "code": "code.cmd", "vscode": "code.cmd",
1859
+ "spotify": "Spotify.exe", "steam": "steam.exe", "discord": "Discord.exe",
1860
+ "telegram": "Telegram.exe", "teams": "Teams.exe", "zoom": "Zoom.exe",
1861
+ "terminal": "wt.exe", "powershell": "pwsh.exe", "cmd": "cmd.exe",
1862
+ "task manager": "taskmgr.exe", "диспетчер": "taskmgr.exe",
1863
+ "settings": "ms-settings:", "настройки": "ms-settings:",
1864
+ "registry": "regedit.exe", "реестр": "regedit.exe",
1865
+ }
1866
+ const appName = appMap[arg.toLowerCase()] || arg
1867
+ if (arg.startsWith("http://") || arg.startsWith("https://")) {
1868
+ try { execSync(`start "" "${arg}"`, { shell: "cmd.exe", stdio: "pipe" }); console.log(green(`\n ✓ Открыто: ${arg}\n`)) }
1869
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1870
+ } else {
1871
+ try { execSync(`start "" "${appName}"`, { shell: "cmd.exe", stdio: "pipe" }); console.log(green(`\n ✓ Запущено: ${appName}\n`)) }
1872
+ catch (e) { console.log(red(`\n ✗ Не удалось запустить: ${appName}\n`)) }
1873
+ }
1874
+ return
1875
+ }
1876
+ case "/url": {
1877
+ if (!arg) { console.log(dim("\n Использование: /url <URL>\n")); return }
1878
+ try { execSync(`start "" "${arg}"`, { shell: "cmd.exe", stdio: "pipe" }); console.log(green(`\n ✓ Открыто: ${arg}\n`)) }
1879
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1880
+ return
1881
+ }
1882
+ case "/kinopoisk": {
1883
+ const kpUrl = arg ? `https://www.kinopoisk.ru/index.php?what=${encodeURIComponent(arg)}` : "https://www.kinopoisk.ru"
1884
+ try { execSync(`start "" "${kpUrl}"`, { shell: "cmd.exe", stdio: "pipe" }); console.log(green(`\n ✓ Кинопоиск открыт${arg ? `: ${arg}` : ""}\n`)) }
1885
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1886
+ return
1887
+ }
1888
+ case "/youtube": {
1889
+ const ytUrl = arg ? `https://www.youtube.com/results?search_query=${encodeURIComponent(arg)}` : "https://www.youtube.com"
1890
+ try { execSync(`start "" "${ytUrl}"`, { shell: "cmd.exe", stdio: "pipe" }); console.log(green(`\n ✓ YouTube открыт${arg ? `: ${arg}` : ""}\n`)) }
1891
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1892
+ return
1893
+ }
1894
+ case "/google": {
1895
+ const gUrl = arg ? `https://www.google.com/search?q=${encodeURIComponent(arg)}` : "https://www.google.com"
1896
+ try { execSync(`start "" "${gUrl}"`, { shell: "cmd.exe", stdio: "pipe" }); console.log(green(`\n ✓ Google открыт${arg ? `: ${arg}` : ""}\n`)) }
1897
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1898
+ return
1899
+ }
1900
+ case "/folder": {
1901
+ if (!arg) { console.log(dim("\n Использование: /folder <путь>\n")); return }
1902
+ try { execSync(`explorer "${arg}"`, { stdio: "pipe" }); console.log(green(`\n ✓ Папка открыта: ${arg}\n`)) }
1903
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1904
+ return
1905
+ }
1906
+ case "/screenshot": {
1907
+ const ssPath = arg || "screenshot.png"
1908
+ try {
1909
+ execSync(`powershell -command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen | ForEach-Object { $b = $_.Bounds; Add-Type -TypeDefinition 'using System;using System.Drawing;using System.Drawing.Imaging;public class SC{public static void Cap(string f){var bmp=new Bitmap($b.Width,$b.Height);Graphics.FromImage(bmp).CopyFromScreen($b.Location,Point.Empty,$b.Size);bmp.Save(f,ImageFormat.Png);}}'; [SC]::Cap('${ssPath.replace(/\\/g, "\\\\")}')"`, { encoding: "utf8", stdio: "pipe" })
1910
+ console.log(green(`\n ✓ Скриншот сохранён: ${ssPath}\n`))
1911
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1912
+ return
1913
+ }
1914
+ case "/clipboard": {
1915
+ if (!arg) {
1916
+ try {
1917
+ const text = execSync("powershell -command \"Get-Clipboard\"", { encoding: "utf8", stdio: "pipe" })
1918
+ console.log(`\n Буфер обмена:\n ${dim(text.trim())}\n`)
1919
+ } catch { console.log(dim("\n Буфер обмена пуст\n")) }
1920
+ } else {
1921
+ try { execSync(`powershell -command "Set-Clipboard -Value '${arg.replace(/'/g, "''")}'"`, { encoding: "utf8", stdio: "pipe" }); console.log(green(`\n ✓ Скопировано в буфер\n`)) }
1922
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1923
+ }
1924
+ return
1925
+ }
1926
+ case "/type": {
1927
+ if (!arg) { console.log(dim("\n Использование: /type <текст>\n")); return }
1928
+ try { execSync(`powershell -command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${arg.replace(/'/g, "''").replace(/"/g, '""')}')"`, { encoding: "utf8", stdio: "pipe" }); console.log(green(`\n ✓ Текст набран\n`)) }
1929
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1930
+ return
1931
+ }
1932
+ case "/key": {
1933
+ if (!arg) { console.log(dim("\n Использование: /key <клавиша>\n Примеры: enter, tab, esc, {DELETE}, ^c, ^v, ^s, ^a, ^z, {F5}\n")); return }
1934
+ try { execSync(`powershell -command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${arg}')"`, { encoding: "utf8", stdio: "pipe" }); console.log(green(`\n ✓ Нажато: ${arg}\n`)) }
1935
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1936
+ return
1937
+ }
1938
+ case "/volume": {
1939
+ if (!arg) { console.log(dim("\n Использование: /volume <0-100>\n")); return }
1940
+ const vol = Math.max(0, Math.min(100, parseInt(arg)))
1941
+ try {
1942
+ execSync(`powershell -command "$wsh = New-Object -ComObject WScript.Shell; 1..50 | ForEach-Object { $wsh.SendKeys([char]174) }; 1..${Math.round(vol / 2)} | ForEach-Object { $wsh.SendKeys([char]175) }"`, { encoding: "utf8", stdio: "pipe" })
1943
+ console.log(green(`\n ✓ Громкость: ${vol}%\n`))
1944
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1945
+ return
1946
+ }
1947
+ case "/brightness": {
1948
+ if (!arg) { console.log(dim("\n Использование: /brightness <0-100>\n")); return }
1949
+ try {
1950
+ execSync(`powershell -command "Get-WmiObject -Namespace root\\wmi -Class WmiMonitorBrightnessMethods | ForEach-Object { $_.WmiSetBrightness(10, ${parseInt(arg)}) }"`, { encoding: "utf8", stdio: "pipe" })
1951
+ console.log(green(`\n ✓ Яркость: ${arg}%\n`))
1952
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1953
+ return
1954
+ }
1955
+ case "/notify": {
1956
+ if (!arg) { console.log(dim("\n Использование: /notify <текст уведомления>\n")); return }
1957
+ try {
1958
+ execSync(`powershell -command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.BalloonTipTitle = 'Stella'; $n.BalloonTipText = '${arg.replace(/'/g, "''")}'; $n.Visible = $true; $n.ShowBalloonTip(5000)"`, { encoding: "utf8", stdio: "pipe" })
1959
+ console.log(green(`\n ✓ Уведомление: ${arg}\n`))
1960
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1961
+ return
1962
+ }
1963
+ case "/windows": {
1964
+ try {
1965
+ const result = execSync(`powershell -command "Get-Process | Where-Object {$_.MainWindowTitle -ne ''} | Select-Object ProcessName, MainWindowTitle | Format-Table -AutoSize"`, { encoding: "utf8", stdio: "pipe" })
1966
+ console.log("\n" + result + "\n")
1967
+ } catch { console.log(dim("\n Не удалось получить список окон\n")) }
1968
+ return
1969
+ }
1970
+ case "/focus": {
1971
+ if (!arg) { console.log(dim("\n Использование: /focus <имя приложения>\n")); return }
1972
+ try {
1973
+ execSync(`powershell -command "Get-Process | Where-Object {$_.ProcessName -like '*${arg}*'} | Select-Object -First 1 | ForEach-Object { $_.MainWindowHandle } | ForEach-Object { Add-Type -Name Win -Namespace User32 -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr hWnd);'; [User32.Win]::SetForegroundWindow($_) }"`, { encoding: "utf8", stdio: "pipe" })
1974
+ console.log(green(`\n ✓ Фокус на: ${arg}\n`))
1975
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1976
+ return
1977
+ }
1978
+ case "/wifi": {
1979
+ try {
1980
+ const result = execSync("netsh wlan show networks mode=bssid", { encoding: "utf8", stdio: "pipe" })
1981
+ console.log("\n" + result + "\n")
1982
+ } catch { console.log(dim("\n Не удалось получить список Wi-Fi сетей\n")) }
1983
+ return
1984
+ }
1985
+ case "/lock": {
1986
+ try { execSync("rundll32.exe user32.dll,LockWorkStation", { stdio: "pipe" }); console.log(green("\n ✓ Экран заблокирован\n")) }
1987
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1988
+ return
1989
+ }
1990
+ case "/shutdown": {
1991
+ if (!arg) { console.log(dim("\n Использование: /shutdown <shutdown|restart|sleep>\n")); return }
1992
+ const shutCmds = { shutdown: "shutdown /s /t 0", restart: "shutdown /r /t 0", sleep: "rundll32.exe powrprof.dll,SetSuspendState 0,1,0" }
1993
+ try { execSync(shutCmds[arg] || "shutdown /s /t 0", { stdio: "pipe" }); console.log(green(`\n ✓ ${arg}\n`)) }
1994
+ catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
1995
+ return
1996
+ }
1997
+ case "/search": {
1998
+ if (!arg) { console.log(dim("\n Использование: /search <имя файла>\n")); return }
1999
+ try {
2000
+ const result = execSync(`dir /s /b "C:\\*${arg}*"` , { encoding: "utf8", stdio: "pipe", timeout: 10000 })
2001
+ const files = result.split("\n").filter(f => f.trim()).slice(0, 20)
2002
+ console.log(`\n Найдено файлов: ${files.length}`)
2003
+ files.forEach(f => console.log(` ${dim(f.trim())}`))
2004
+ console.log()
2005
+ } catch { console.log(dim("\n Файлы не найдены\n")) }
2006
+ return
2007
+ }
2008
+
2009
+ // ═══════════════════════════════════════════════════
2010
+ // СЕРВЕР И ТЕРМИНАЛ
2011
+ // ═══════════════════════════════════════════════════
2012
+ case "/exec":
2013
+ case "/shell": {
2014
+ if (!arg) { console.log(dim("\n Использование: /exec <команда>\n")); return }
2015
+ try {
2016
+ const result = execSync(arg, { encoding: "utf8", timeout: 120000, maxBuffer: 50 * 1024 * 1024, stdio: "pipe" })
2017
+ console.log("\n" + result + "\n")
2018
+ } catch (e) {
2019
+ console.log(red(`\n ✗ Ошибка (код ${e.status}):\n`))
2020
+ if (e.stderr) console.log(e.stderr)
2021
+ if (e.stdout) console.log(e.stdout)
2022
+ console.log()
2023
+ }
2024
+ return
2025
+ }
2026
+ case "/powershell": {
2027
+ if (!arg) { console.log(dim("\n Использование: /powershell <команда>\n")); return }
2028
+ try {
2029
+ const result = execSync(`powershell -ExecutionPolicy Bypass -Command "${arg.replace(/"/g, '\\"')}"`, { encoding: "utf8", timeout: 120000, stdio: "pipe" })
2030
+ console.log("\n" + result + "\n")
2031
+ } catch (e) {
2032
+ console.log(red(`\n ✗ PowerShell ошибка:\n`))
2033
+ if (e.stderr) console.log(e.stderr)
2034
+ if (e.stdout) console.log(e.stdout)
2035
+ console.log()
2036
+ }
2037
+ return
2038
+ }
2039
+ case "/cmd": {
2040
+ if (!arg) { console.log(dim("\n Использование: /cmd <команда>\n")); return }
2041
+ try {
2042
+ const result = execSync(`cmd /c "${arg}"`, { encoding: "utf8", timeout: 120000, stdio: "pipe" })
2043
+ console.log("\n" + result + "\n")
2044
+ } catch (e) {
2045
+ console.log(red(`\n ✗ cmd ошибка:\n`))
2046
+ if (e.stderr) console.log(e.stderr)
2047
+ if (e.stdout) console.log(e.stdout)
2048
+ console.log()
2049
+ }
2050
+ return
2051
+ }
2052
+ case "/ssh": {
2053
+ if (!arg) { console.log(dim("\n Использование: /ssh <user@host> [команда]\n")); return }
2054
+ const sshParts = arg.split(/\s+/)
2055
+ const sshTarget = sshParts[0]
2056
+ const sshCmd = sshParts.slice(1).join(" ")
2057
+ try {
2058
+ const cmd = sshCmd ? `ssh ${sshTarget} "${sshCmd}"` : `ssh ${sshTarget}`
2059
+ const result = execSync(cmd, { encoding: "utf8", timeout: 120000, stdio: "pipe" })
2060
+ console.log("\n" + result + "\n")
2061
+ } catch (e) {
2062
+ console.log(red(`\n ✗ SSH ошибка: ${e.message}\n`))
2063
+ }
2064
+ return
2065
+ }
2066
+ case "/scp": {
2067
+ if (!arg) { console.log(dim("\n Использование: /scp <источник> <назначение>\n")); return }
2068
+ const scpParts = arg.split(/\s+/)
2069
+ if (scpParts.length < 2) { console.log(dim("\n Нужно указать источник и назначение\n")); return }
2070
+ try {
2071
+ execSync(`scp ${scpParts[0]} ${scpParts[1]}`, { encoding: "utf8", timeout: 300000, stdio: "pipe" })
2072
+ console.log(green(`\n ✓ Файл скопирован\n`))
2073
+ } catch (e) {
2074
+ console.log(red(`\n ✗ SCP ошибка: ${e.message}\n`))
2075
+ }
2076
+ return
2077
+ }
2078
+ case "/server": {
2079
+ try {
2080
+ const cpu = execSync("wmic cpu get loadpercentage /value", { encoding: "utf8", stdio: "pipe" })
2081
+ const cpuMatch = cpu.match(/LoadPercentage=(\d+)/)
2082
+ const cpuVal = cpuMatch ? parseInt(cpuMatch[1]) : 0
2083
+
2084
+ const mem = execSync("wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /value", { encoding: "utf8", stdio: "pipe" })
2085
+ const totalMatch = mem.match(/TotalVisibleMemorySize=(\d+)/)
2086
+ const freeMatch = mem.match(/FreePhysicalMemory=(\d+)/)
2087
+ const total = totalMatch ? parseInt(totalMatch[1]) / 1024 / 1024 : 0
2088
+ const free = freeMatch ? parseInt(freeMatch[1]) / 1024 / 1024 : 0
2089
+
2090
+ const ports = execSync("netstat -ano | findstr LISTENING", { encoding: "utf8", timeout: 5000, stdio: "pipe" })
2091
+ const listening = ports.split("\n").filter(l => l.trim()).length
2092
+
2093
+ console.log(box([
2094
+ dim("CPU: ") + (cpuVal > 80 ? red(cpuVal + "%") : cpuVal > 50 ? yellow(cpuVal + "%") : green(cpuVal + "%")),
2095
+ dim("Память: ") + white(`${(total - free).toFixed(1)} / ${total.toFixed(1)} GB (${((total - free) / total * 100).toFixed(0)}%)`),
2096
+ dim("Порты: ") + cyan(String(listening)),
2097
+ dim("Uptime: ") + dim(safeExec("net stats workstation | findstr Started")),
2098
+ ], { title: "Сервер", color: violet, padding: 2 }))
2099
+ console.log()
2100
+ } catch (e) {
2101
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
2102
+ }
2103
+ return
2104
+ }
2105
+ case "/ports": {
2106
+ try {
2107
+ const result = execSync("netstat -ano | findstr LISTENING", { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2108
+ const lines = result.split("\n").filter(l => l.trim()).slice(0, 30)
2109
+ console.log(dim("\n Открытые порты:\n"))
2110
+ lines.forEach(l => console.log(" " + l.trim()))
2111
+ console.log()
2112
+ } catch { console.log(dim("\n Не удалось получить порты\n")) }
2113
+ return
2114
+ }
2115
+ case "/connections": {
2116
+ try {
2117
+ const result = execSync("netstat -ano | findstr ESTABLISHED", { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2118
+ const lines = result.split("\n").filter(l => l.trim()).slice(0, 30)
2119
+ console.log(dim("\n Активные соединения:\n"))
2120
+ lines.forEach(l => console.log(" " + l.trim()))
2121
+ console.log()
2122
+ } catch { console.log(dim("\n Нет активных соединений\n")) }
2123
+ return
2124
+ }
2125
+ case "/kill-port": {
2126
+ if (!arg) { console.log(dim("\n Использование: /kill-port <порт>\n")); return }
2127
+ try {
2128
+ const result = execSync(`netstat -ano | findstr :${arg}`, { encoding: "utf8", stdio: "pipe" })
2129
+ const line = result.split("\n").find(l => l.includes("LISTENING"))
2130
+ if (!line) { console.log(dim(`\n Порт ${arg} не используется\n`)); return }
2131
+ const pidMatch = line.match(/(\d+)\s*$/)
2132
+ if (pidMatch) {
2133
+ execSync(`taskkill /F /PID ${pidMatch[1]}`, { encoding: "utf8", stdio: "pipe" })
2134
+ console.log(green(`\n ✓ Процесс на порту ${arg} (PID: ${pidMatch[1]}) завершён\n`))
2135
+ }
2136
+ } catch (e) {
2137
+ console.log(red(`\n ✗ Ошибка: ${e.message}\n`))
2138
+ }
2139
+ return
2140
+ }
2141
+ case "/network": {
2142
+ try {
2143
+ const result = execSync("ipconfig /all", { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2144
+ console.log("\n" + result + "\n")
2145
+ } catch { console.log(dim("\n Не удалось получить информацию\n")) }
2146
+ return
2147
+ }
2148
+ case "/dns": {
2149
+ if (!arg || arg === "show") {
2150
+ try {
2151
+ const result = execSync("ipconfig /displaydns", { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2152
+ console.log("\n" + result.slice(0, 3000) + "\n")
2153
+ } catch { console.log(dim("\n Не удалось получить DNS\n")) }
2154
+ } else if (arg === "flush") {
2155
+ try {
2156
+ execSync("ipconfig /flushdns", { encoding: "utf8", stdio: "pipe" })
2157
+ console.log(green("\n ✓ DNS кэш очищен\n"))
2158
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2159
+ }
2160
+ return
2161
+ }
2162
+ case "/ping": {
2163
+ if (!arg) { console.log(dim("\n Использование: /ping <хост>\n")); return }
2164
+ try {
2165
+ const result = execSync(`ping -n 4 ${arg}`, { encoding: "utf8", timeout: 30000, stdio: "pipe" })
2166
+ console.log("\n" + result + "\n")
2167
+ } catch (e) { console.log(red(`\n ✗ Пинг не удался\n`)) }
2168
+ return
2169
+ }
2170
+ case "/traceroute": {
2171
+ if (!arg) { console.log(dim("\n Использование: /traceroute <хост>\n")); return }
2172
+ try {
2173
+ const result = execSync(`tracert -d ${arg}`, { encoding: "utf8", timeout: 120000, stdio: "pipe" })
2174
+ console.log("\n" + result + "\n")
2175
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2176
+ return
2177
+ }
2178
+ case "/download": {
2179
+ if (!arg) { console.log(dim("\n Использование: /download <URL>\n")); return }
2180
+ try {
2181
+ execSync(`curl -L -O "${arg}"`, { encoding: "utf8", timeout: 300000, stdio: "pipe" })
2182
+ console.log(green(`\n ✓ Скачано из ${arg}\n`))
2183
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2184
+ return
2185
+ }
2186
+ case "/upload": {
2187
+ if (!arg) { console.log(dim("\n Использование: /upload <файл> <URL>\n")); return }
2188
+ const uploadParts = arg.split(/\s+/)
2189
+ if (uploadParts.length < 2) { console.log(dim("\n Нужно указать файл и URL\n")); return }
2190
+ try {
2191
+ execSync(`curl -T "${uploadParts[0]}" "${uploadParts[1]}"`, { encoding: "utf8", timeout: 300000, stdio: "pipe" })
2192
+ console.log(green(`\n ✓ Загружено: ${uploadParts[0]} → ${uploadParts[1]}\n`))
2193
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2194
+ return
2195
+ }
2196
+ case "/http-server": {
2197
+ const port = parseInt(arg) || 8080
2198
+ try {
2199
+ const { spawn } = await import("node:child_process")
2200
+ const server = spawn("python", ["-m", "http.server", String(port)], { detached: true, stdio: "ignore" })
2201
+ server.unref()
2202
+ console.log(green(`\n ✓ HTTP сервер запущен на порту ${port} (PID: ${server.pid})\n`))
2203
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2204
+ return
2205
+ }
2206
+ case "/firewall": {
2207
+ try {
2208
+ const result = execSync("netsh advfirewall show allprofiles state", { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2209
+ console.log("\n" + result + "\n")
2210
+ } catch { console.log(dim("\n Не удалось получить статус файрвола\n")) }
2211
+ return
2212
+ }
2213
+ case "/service": {
2214
+ if (!arg) {
2215
+ try {
2216
+ const result = execSync("net start", { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2217
+ const services = result.split("\n").filter(l => l.trim()).slice(0, 30)
2218
+ console.log(dim("\n Запущенные службы:\n"))
2219
+ services.forEach(s => console.log(" " + s.trim()))
2220
+ console.log()
2221
+ } catch { console.log(dim("\n Не удалось получить службы\n")) }
2222
+ } else {
2223
+ try {
2224
+ execSync(`net start "${arg}"`, { encoding: "utf8", timeout: 30000, stdio: "pipe" })
2225
+ console.log(green(`\n ✓ Служба ${arg} запущена\n`))
2226
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2227
+ }
2228
+ return
2229
+ }
2230
+ case "/registry": {
2231
+ if (!arg) { console.log(dim("\n Использование: /registry <путь к ключу>\n")); return }
2232
+ try {
2233
+ const result = execSync(`reg query "${arg}"`, { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2234
+ console.log("\n" + result + "\n")
2235
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2236
+ return
2237
+ }
2238
+ case "/script": {
2239
+ if (!arg) { console.log(dim("\n Использование: /script <путь к скрипту>\n")); return }
2240
+ const ext = arg.split(".").pop().toLowerCase()
2241
+ const runners = { ps1: "powershell -ExecutionPolicy Bypass -File", py: "python", js: "node", mjs: "node" }
2242
+ const runner = runners[ext] || ""
2243
+ try {
2244
+ const cmd = runner ? `${runner} "${arg}"` : `"${arg}"`
2245
+ const result = execSync(cmd, { encoding: "utf8", timeout: 300000, stdio: "pipe" })
2246
+ console.log("\n" + result + "\n")
2247
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2248
+ return
2249
+ }
2250
+ case "/schedule": {
2251
+ if (!arg) { console.log(dim("\n Использование: /schedule <скрипт> [daily|weekly|once] [время]\n")); return }
2252
+ const schedParts = arg.split(/\s+/)
2253
+ const schedScript = schedParts[0]
2254
+ const schedType = schedParts[1] || "daily"
2255
+ const schedTime = schedParts[2] || "09:00"
2256
+ const taskName = `Stella_${Date.now()}`
2257
+ try {
2258
+ execSync(`schtasks /create /tn "${taskName}" /tr "${schedScript}" /sc ${schedType} /st ${schedTime} /f`, { encoding: "utf8", timeout: 10000, stdio: "pipe" })
2259
+ console.log(green(`\n ✓ Задача ${taskName} создана (${schedType} в ${schedTime})\n`))
2260
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2261
+ return
2262
+ }
2263
+ case "/docker": {
2264
+ const dockerCmd = arg || "ps"
2265
+ try {
2266
+ let cmd = `docker ${dockerCmd}`
2267
+ if (dockerCmd === "ps") cmd = "docker ps"
2268
+ if (dockerCmd === "list") cmd = "docker ps -a"
2269
+ const result = execSync(cmd, { encoding: "utf8", timeout: 30000, stdio: "pipe" })
2270
+ console.log("\n" + result + "\n")
2271
+ } catch (e) { console.log(red(`\n ✗ Docker: ${e.message}\n`)) }
2272
+ return
2273
+ }
2274
+ case "/pm2": {
2275
+ const pm2Cmd = arg || "list"
2276
+ try {
2277
+ const result = execSync(`pm2 ${pm2Cmd}`, { encoding: "utf8", timeout: 30000, stdio: "pipe" })
2278
+ console.log("\n" + result + "\n")
2279
+ } catch (e) { console.log(red(`\n ✗ PM2: ${e.message}\n`)) }
2280
+ return
2281
+ }
2282
+ case "/npm": {
2283
+ const npmCmd = arg || "list"
2284
+ try {
2285
+ const pkgManager = fs.existsSync("pnpm-lock.yaml") ? "pnpm" : fs.existsSync("yarn.lock") ? "yarn" : "npm"
2286
+ const result = execSync(`${pkgManager} ${npmCmd}`, { encoding: "utf8", timeout: 120000, stdio: "pipe" })
2287
+ console.log("\n" + result + "\n")
2288
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2289
+ return
2290
+ }
2291
+ case "/monitor": {
2292
+ const monType = arg || "cpu"
2293
+ try {
2294
+ let result = ""
2295
+ if (monType === "cpu") {
2296
+ result = execSync("wmic cpu get loadpercentage /value", { encoding: "utf8", stdio: "pipe" })
2297
+ const match = result.match(/LoadPercentage=(\d+)/)
2298
+ console.log(`\n CPU: ${match ? match[1] : "?"}%\n`)
2299
+ } else if (monType === "mem") {
2300
+ result = execSync("wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /value", { encoding: "utf8", stdio: "pipe" })
2301
+ console.log("\n" + result + "\n")
2302
+ } else if (monType === "disk") {
2303
+ result = execSync("wmic diskdrive get model,size,status", { encoding: "utf8", stdio: "pipe" })
2304
+ console.log("\n" + result + "\n")
2305
+ } else if (monType === "net") {
2306
+ result = execSync("netstat -e", { encoding: "utf8", stdio: "pipe" })
2307
+ console.log("\n" + result + "\n")
2308
+ } else {
2309
+ console.log(dim("\n /monitor <cpu|mem|disk|net>\n"))
2310
+ }
2311
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2312
+ return
2313
+ }
2314
+ case "/sys": {
2315
+ if (!arg) { console.log(dim("\n Использование: /sys <любая команда>\n")); return }
2316
+ try {
2317
+ const result = execSync(arg, { encoding: "utf8", timeout: 300000, maxBuffer: 100 * 1024 * 1024, stdio: "pipe" })
2318
+ console.log("\n" + result + "\n")
2319
+ } catch (e) {
2320
+ console.log(red(`\n ✗ Ошибка (код ${e.status}):\n`))
2321
+ if (e.stderr) console.log(e.stderr)
2322
+ if (e.stdout) console.log(e.stdout)
2323
+ console.log()
2324
+ }
2325
+ return
2326
+ }
2327
+ case "/sudo": {
2328
+ if (!arg) { console.log(dim("\n Использование: /sudo <команда>\n")); return }
2329
+ try {
2330
+ execSync(`powershell -Command "Start-Process cmd -ArgumentList '/c ${arg.replace(/"/g, '\\"')}' -Verb RunAs -Wait"`, { encoding: "utf8", timeout: 300000, stdio: "pipe" })
2331
+ console.log(green(`\n ✓ Команда выполнена от администратора\n`))
2332
+ } catch (e) { console.log(red(`\n ✗ Ошибка: ${e.message}\n`)) }
2333
+ return
2334
+ }
2335
+
2336
+ // ═══════════════════════════════════════════════════
2337
+ // УМНЫЙ ДОМ
2338
+ // ═══════════════════════════════════════════════════
2339
+ case "/tv": {
2340
+ if (!arg) {
2341
+ console.log(box([
2342
+ violet("/tv discover") + dim(" — найти Sony TV в сети"),
2343
+ violet("/tv off IP") + dim(" — выключить TV"),
2344
+ violet("/tv on IP") + dim(" — включить TV"),
2345
+ violet("/tv info IP") + dim(" — информация о TV"),
2346
+ violet("/tv vol IP ±N") + dim(" — громкость"),
2347
+ violet("/tv ch IP N") + dim(" — канал"),
2348
+ violet("/tv hdmi IP N") + dim(" — переключить HDMI"),
2349
+ violet("/tv remote IP команда") + dim(" — пульт (up/down/left/right/ok/home/back)"),
2350
+ "",
2351
+ dim("Sony TV используют REST API. Включи в настройках TV:"),
2352
+ dim("Сеть → Другое → Режим.getSimpleName() → Включить"),
2353
+ ], { title: "Sony TV", color: cyan, padding: 2 }))
2354
+ console.log()
2355
+ return
2356
+ }
2357
+ const tvParts = arg.split(/\s+/)
2358
+ const tvAction = tvParts[0]
2359
+
2360
+ if (tvAction === "discover") {
2361
+ console.log(cyan("\n Сканирую сеть на Sony TV...\n"))
2362
+ await runTurn("Найди Sony BRAVIA TV в локальной сети используя SSDP M-SEARCH. Верни IP адреса найденных телевизоров.")
2363
+ return
2364
+ }
2365
+
2366
+ if (tvParts.length < 2) { console.log(dim("\n Укажи IP TV. Пример: /tv off 192.168.1.100\n")); return }
2367
+ const tvIp = tvParts[1]
2368
+
2369
+ if (tvAction === "off" || tvAction === "poweroff") {
2370
+ console.log(cyan(`\n Выключаю TV ${tvIp}...\n`))
2371
+ await runTurn(`Выключи Sony BRAVIA TV по IP ${tvIp}. Используй REST API метод setPowerStatus с параметром status:false на http://${tvIp}/sony/videoControl. Если нужен PSK, попроси пользователя.`)
2372
+ return
2373
+ }
2374
+
2375
+ if (tvAction === "on" || tvAction === "poweron") {
2376
+ console.log(cyan(`\n Включаю TV ${tvIp}...\n`))
2377
+ await runTurn(`Включи Sony BRAVIA TV по IP ${tvIp}. Используй REST API метод setPowerStatus с параметром status:true на http://${tvIp}/sony/videoControl. Если нужен PSK, попроси пользователя.`)
2378
+ return
2379
+ }
2380
+
2381
+ if (tvAction === "info") {
2382
+ console.log(cyan(`\n Получаю информацию о TV ${tvIp}...\n`))
2383
+ await runTurn(`Получи информацию о Sony BRAVIA TV по IP ${tvIp}. Используй REST API на http://${tvIp}/sony/videoControl: getPowerStatus, getCurrentExternalInputsStatus, getVolumeInformation.`)
2384
+ return
2385
+ }
2386
+
2387
+ if (tvAction === "vol") {
2388
+ const volArg = tvParts[2] || "+1"
2389
+ console.log(cyan(`\n Регулирую громкость ${tvIp}: ${volArg}\n`))
2390
+ await runTurn(`Установи громкость Sony TV по IP ${tvIp} на ${volArg}. Используй REST API метод setAudioVolume на http://${tvIp}/sony/videoControl.`)
2391
+ return
2392
+ }
2393
+
2394
+ if (tvAction === "ch") {
2395
+ const ch = tvParts[2] || "1"
2396
+ console.log(cyan(`\n Переключаю канал на ${tvIp}: ${ch}\n`))
2397
+ await runTurn(`Переключи канал на Sony TV по IP ${tvIp} на канал ${ch}. Используй REST API метод setChannelPosition на http://${tvIp}/sony/videoControl.`)
2398
+ return
2399
+ }
2400
+
2401
+ if (tvAction === "hdmi") {
2402
+ const hdmi = tvParts[2] || "1"
2403
+ console.log(cyan(`\n Переключаю HDMI ${tvIp}: порт ${hdmi}\n`))
2404
+ await runTurn(`Переключи Sony TV по IP ${tvIp} на HDMI ${hdmi}. Используй REST API метод setPlayContent с uri extInput:hdmi?port=${hdmi} на http://${tvIp}/sony/videoControl.`)
2405
+ return
2406
+ }
2407
+
2408
+ // Remote control
2409
+ console.log(cyan(`\n Команда пульта: ${tvAction} на ${tvIp}\n`))
2410
+ await runTurn(`Отправь команду пульта "${tvAction}" на Sony TV по IP ${tvIp}. Используй REST API на http://${tvIp}/sony/videoControl.`)
2411
+ return
2412
+ }
2413
+ case "/tv-off": {
2414
+ const ip = arg || ""
2415
+ if (!ip) { console.log(dim("\n Использование: /tv-off <IP TV>\n")); return }
2416
+ console.log(cyan(`\n Выключаю TV ${ip}...\n`))
2417
+ await runTurn(`Выключи Sony BRAVIA TV по IP ${ip}. Используй REST API метод setPowerStatus с параметром status:false на http://${ip}/sony/videoControl.`)
2418
+ return
2419
+ }
2420
+ case "/tv-on": {
2421
+ const ip = arg || ""
2422
+ if (!ip) { console.log(dim("\n Использование: /tv-on <IP TV>\n")); return }
2423
+ console.log(cyan(`\n Включаю TV ${ip}...\n`))
2424
+ await runTurn(`Включи Sony BRAVIA TV по IP ${ip}. Используй REST API метод setPowerStatus с параметром status:true на http://${ip}/sony/videoControl.`)
2425
+ return
2426
+ }
2427
+ case "/tv-info": {
2428
+ const ip = arg || ""
2429
+ if (!ip) { console.log(dim("\n Использование: /tv-info <IP TV>\n")); return }
2430
+ console.log(cyan(`\n Получаю информацию о TV ${ip}...\n`))
2431
+ await runTurn(`Получи информацию о Sony BRAVIA TV по IP ${ip}. Используй REST API: getPowerStatus, getCurrentExternalInputsStatus, getVolumeInformation на http://${ip}/sony/videoControl.`)
2432
+ return
2433
+ }
2434
+ case "/cec": {
2435
+ if (!arg) { console.log(dim("\n Использование: /cec <on|off|standby> [устройство]\n")); return }
2436
+ const cecParts = arg.split(/\s+/)
2437
+ const cecCmd = cecParts[0]
2438
+ const cecDevice = parseInt(cecParts[1]) || 0
2439
+ console.log(cyan(`\n HDMI-CEC: ${cecCmd} → устройство ${cecDevice}\n`))
2440
+ await runTurn(`Отправь HDMI-CEC команду "${cecCmd}" на устройство с адресом ${cecDevice} (0=TV, 1-14=устройства).`)
2441
+ return
2442
+ }
2443
+ case "/smart": {
2444
+ console.log(cyan("\n Сканирую сеть на умные устройства...\n"))
2445
+ await runTurn("Просканируй локальную сеть на умные устройства (UPnP/SSDP). Найди все устройства и покажи их типы и IP.")
2446
+ return
2447
+ }
2448
+ case "/light": {
2449
+ if (!arg) { console.log(dim("\n Использование: /light <hue|yeelight|tplink> IP <on|off|dim|color> [значение]\n")); return }
2450
+ const lightParts = arg.split(/\s+/)
2451
+ console.log(cyan(`\n Управление лампой: ${lightParts.join(" ")}\n`))
2452
+ await runTurn(`Управление умной лампой: ${arg}. Используй соответствующий API.`)
2453
+ return
2454
+ }
2455
+ case "/cast": {
2456
+ if (!arg) { console.log(dim("\n Использование: /cast <URL> [IP Chromecast]\n")); return }
2457
+ const castParts = arg.split(/\s+/)
2458
+ console.log(cyan(`\n Трансляция на Chromecast: ${castParts[0]}\n`))
2459
+ await runTurn(`Транслируй ${castParts[0]} на Chromecast${castParts[1] ? ` с IP ${castParts[1]}` : ""}. Используй catt или аналог.`)
2460
+ return
2461
+ }
2462
+ case "/wol": {
2463
+ if (!arg) { console.log(dim("\n Использование: /wol <MAC адрес>\n")); return }
2464
+ console.log(cyan(`\n Wake-on-LAN: ${arg}\n`))
2465
+ await runTurn(`Включи устройство с MAC адресом ${arg} через Wake-on-LAN. Отправь magic packet на широковещательный адрес порт 9.`)
2466
+ return
2467
+ }
2468
+
2469
+ case "/exit":
2470
+ case "/quit": {
2471
+ goodbye()
2472
+ return
2473
+ }
2474
+ default:
2475
+ console.log(dim(`\n Неизвестная команда: ${cmd}. Смотри /help\n`))
2476
+ }
2477
+ }
2478
+
2479
+ function safeExec(cmd) {
2480
+ try {
2481
+ return execSync(cmd, { encoding: "utf8" }).trim()
2482
+ } catch {
2483
+ return "не найден"
2484
+ }
2485
+ }
2486
+
2487
+ // ---------- /plan: Autonomous multi-step execution ----------
2488
+ async function runPlan(taskDescription) {
2489
+ console.log()
2490
+ console.log(box([
2491
+ violet("Автономный план выполнения"),
2492
+ "",
2493
+ white("Задача: ") + dim(taskDescription),
2494
+ "",
2495
+ dim("ИИ составит план, вы подтвердите, затем автономное выполнение."),
2496
+ ], { color: purple, padding: 2 }))
2497
+ console.log()
2498
+
2499
+ startSpinner("Составляю план")
2500
+ const planPrompt = `Составь пошаговый план для следующей задачи. Верни ТОЛЬКО JSON массив объектов { "id": N, "action": "описание действия", "files": ["список файлов"] } без markdown и пояснений.
2501
+
2502
+ Задача: ${taskDescription}
2503
+
2504
+ Рабочая директория: ${process.cwd()}`
2505
+
2506
+ let plan
2507
+ try {
2508
+ const { text } = await generateText({
2509
+ model: getModel(state.model),
2510
+ messages: [{ role: "user", content: planPrompt }],
2511
+ })
2512
+ const jsonMatch = text.match(/\[[\s\S]*\]/)
2513
+ if (!jsonMatch) { stopSpinner(); console.log(red(" ✗ Не удалось распознать план\n")); return }
2514
+ plan = JSON.parse(jsonMatch[0])
2515
+ stopSpinner()
2516
+ } catch (e) {
2517
+ stopSpinner()
2518
+ console.log(red(" ✗ Ошибка генерации плана: ") + String(e?.message || e).slice(0, 200) + "\n")
2519
+ return
2520
+ }
2521
+
2522
+ console.log(box([
2523
+ ...plan.map((s, i) => violet(`[${i + 1}]`) + " " + white(s.action)),
2524
+ "",
2525
+ dim("Подтвердите выполнение плана"),
2526
+ ], { title: "План", color: purple, padding: 2 }))
2527
+
2528
+ const confirm = await question("\n " + violet("Выполнить план? (y/n) › "))
2529
+ if (confirm.trim().toLowerCase() !== "y" && confirm.trim().toLowerCase() !== "д") {
2530
+ console.log(dim(" Отмена\n"))
2531
+ return
2532
+ }
2533
+
2534
+ for (let i = 0; i < plan.length; i++) {
2535
+ const step = plan[i]
2536
+ console.log()
2537
+ console.log(purple(` ▶ Шаг ${i + 1}/${plan.length}: `) + bold(white(step.action)))
2538
+
2539
+ await runTurn(step.action)
2540
+
2541
+ console.log(dim(` ✓ Шаг ${i + 1} выполнен`))
2542
+ }
2543
+
2544
+ console.log()
2545
+ console.log(green(" ✓ Все шаги плана выполнены!\n"))
2546
+ }
2547
+
2548
+ // ---------- /commit: AI-generated git commits ----------
2549
+ async function runCommit(customMessage) {
2550
+ console.log()
2551
+ const status = safeExec("git status --porcelain")
2552
+ if (!status) { console.log(dim(" Нет изменений для коммита\n")); return }
2553
+
2554
+ const diff = safeExec("git diff --stat")
2555
+ console.log(box([
2556
+ dim("Изменения:"),
2557
+ dim(diff || status),
2558
+ ], { title: "Git Status", color: purple, padding: 2 }))
2559
+
2560
+ let commitMessage = customMessage
2561
+ if (!commitMessage) {
2562
+ startSpinner("Генерирую сообщение коммита")
2563
+ const commitPrompt = `Проанализируй эти изменения в git и создай краткое сообщение коммита (50-72 символа). Верни ТОЛЬКО текст сообщения без кавычек и markdown.
2564
+
2565
+ Изменения:
2566
+ ${status}
2567
+
2568
+ Diff:
2569
+ ${diff}`
2570
+
2571
+ try {
2572
+ const { text } = await generateText({
2573
+ model: getModel(state.model),
2574
+ messages: [{ role: "user", content: commitPrompt }],
2575
+ })
2576
+ commitMessage = text.trim().replace(/^["']|["']$/g, "").split("\n")[0].slice(0, 72)
2577
+ stopSpinner()
2578
+ } catch (e) {
2579
+ stopSpinner()
2580
+ console.log(red(" ✗ Ошибка: ") + String(e?.message || e).slice(0, 200) + "\n")
2581
+ return
2582
+ }
2583
+ }
2584
+
2585
+ console.log()
2586
+ console.log(dim(" Сообщение: ") + white(commitMessage))
2587
+ const confirm = await question("\n " + violet("Сделать коммит? (y/n) › "))
2588
+ if (confirm.trim().toLowerCase() !== "y" && confirm.trim().toLowerCase() !== "д") {
2589
+ console.log(dim(" Отмена\n"))
2590
+ return
2591
+ }
2592
+
2593
+ const addResult = safeExec("git add -A")
2594
+ const commitResult = safeExec(`git commit -m "${commitMessage}"`)
2595
+ console.log(green(" ✓ ") + dim(commitResult.split("\n")[0]))
2596
+ console.log()
2597
+ }
2598
+
2599
+ // ---------- /ollama: Local LLM integration ----------
2600
+ const OLLAMA_BASE = "http://localhost:11434"
2601
+ let ollamaModels = []
2602
+
2603
+ async function handleOllama(arg) {
2604
+ console.log()
2605
+
2606
+ if (arg === "list" || arg === "ls" || arg === "список") {
2607
+ startSpinner("Подключаюсь к Ollama")
2608
+ try {
2609
+ const res = await fetch(`${OLLAMA_BASE}/api/tags`, { signal: AbortSignal.timeout(3000) })
2610
+ stopSpinner()
2611
+ if (!res.ok) { console.log(red(" ✗ Ollama недоступен. Запустите: ollama serve\n")); return }
2612
+ const data = await res.json()
2613
+ ollamaModels = data.models || []
2614
+ if (ollamaModels.length === 0) {
2615
+ console.log(dim(" Модели не найдены. Установите: ollama pull llama3.2\n"))
2616
+ return
2617
+ }
2618
+ console.log(box(ollamaModels.map((m, i) =>
2619
+ violet(`[${i + 1}]`) + " " + white(m.name) + dim(` (${(m.size / 1e9).toFixed(1)}GB)`)
2620
+ ), { title: "Локальные модели (Ollama)", color: purple, padding: 2 }))
2621
+ console.log()
2622
+ console.log(dim(" Используйте: /ollama run <номер> для запуска модели"))
2623
+ console.log()
2624
+ } catch {
2625
+ stopSpinner()
2626
+ console.log(red(" ✗ Ollama не запущен. Запустите: ollama serve\n"))
2627
+ }
2628
+ return
2629
+ }
2630
+
2631
+ if (arg === "run" || arg === "запуск") {
2632
+ const numArg = await question(" " + violet("Номер модели › "))
2633
+ const idx = Number.parseInt(numArg) - 1
2634
+ if (!ollamaModels[idx]) { console.log(dim(" Неверный номер\n")); return }
2635
+
2636
+ const modelName = ollamaModels[idx].name
2637
+ console.log(green(` ✓ Модель: ${modelName} (Ollama local)`))
2638
+ console.log(dim(" Теперь все запросы будут идти через локальную модель"))
2639
+ console.log(dim(" Для возврата к облачной: /model\n"))
2640
+
2641
+ state.model = `ollama:${modelName}`
2642
+ return
2643
+ }
2644
+
2645
+ console.log(box([
2646
+ violet("/ollama list") + dim(" — список локальных моделей"),
2647
+ violet("/ollama run") + dim(" — выбрать модель для работы"),
2648
+ "",
2649
+ dim("Ollama должен быть запущен: ") + white("ollama serve"),
2650
+ dim("Установка модели: ") + white("ollama pull llama3.2"),
2651
+ ], { title: "Локальные LLM (Ollama)", color: purple, padding: 2 }))
2652
+ console.log()
2653
+ }
2654
+
2655
+ // ---------- /vt: VirusTotal integration ----------
2656
+ async function handleVirusTotal(arg) {
2657
+ console.log()
2658
+ if (!arg) {
2659
+ console.log(dim(" Использование: /vt <путь к файлу>\n"))
2660
+ return
2661
+ }
2662
+
2663
+ const filePath = arg.trim()
2664
+ if (!fs.existsSync(filePath)) {
2665
+ console.log(red(` ✗ Файл не найден: ${filePath}\n`))
2666
+ return
2667
+ }
2668
+
2669
+ startSpinner("Вычисляю хеш файла")
2670
+ const fileContent = fs.readFileSync(filePath)
2671
+ const hash = crypto.createHash("sha256").update(fileContent).digest("hex")
2672
+
2673
+ console.log(dim(" SHA-256: ") + gray(hash))
2674
+
2675
+ startSpinner("Запрашиваю VirusTotal")
2676
+ try {
2677
+ const vtKey = process.env.VIRUSTOTAL_API_KEY || ""
2678
+ if (!vtKey) {
2679
+ stopSpinner()
2680
+ console.log(yellow(" ⚠ VIRUSTOTAL_API_KEY не задан"))
2681
+ console.log(dim(" Получите ключ: ") + cyan("https://www.virustotal.com/gui/my-apikey"))
2682
+ console.log(dim(" Задайте: ") + violet("export VIRUSTOTAL_API_KEY=ваш_ключ\n"))
2683
+ return
2684
+ }
2685
+
2686
+ const res = await fetch(`https://www.virustotal.com/api/v3/files/${hash}`, {
2687
+ headers: { "x-apikey": vtKey },
2688
+ signal: AbortSignal.timeout(10000),
2689
+ })
2690
+ stopSpinner()
2691
+
2692
+ if (res.status === 404) {
2693
+ console.log(yellow(" ⚠ Файл не найден в базе VirusTotal"))
2694
+ console.log(dim(" Загрузите файл на: ") + cyan("https://www.virustotal.com/gui/upload\n"))
2695
+ return
2696
+ }
2697
+
2698
+ if (!res.ok) {
2699
+ console.log(red(` ✗ Ошибка API: HTTP ${res.status}\n`))
2700
+ return
2701
+ }
2702
+
2703
+ const data = await res.json()
2704
+ const stats = data.data?.attributes?.last_analysis_stats || {}
2705
+ const malicious = stats.malicious || 0
2706
+ const suspicious = stats.suspicious || 0
2707
+ const harmless = stats.harmless || 0
2708
+ const total = malicious + suspicious + harmless
2709
+
2710
+ const severity = malicious > 0 ? "danger" : suspicious > 0 ? "warning" : "safe"
2711
+
2712
+ console.log(box([
2713
+ dim("Файл: ") + white(path.basename(filePath)),
2714
+ dim("Хеш: ") + gray(hash.slice(0, 16) + "..."),
2715
+ dim("Размер: ") + white(`${(fileContent.length / 1024).toFixed(1)} KB`),
2716
+ "",
2717
+ malicious > 0
2718
+ ? red(`⚠ ОБНАРУЖЕНО: ${malicious} антивирусов`) : green("✓ Не обнаружено"),
2719
+ suspicious > 0 ? yellow(` Подозрительных: ${suspicious}`) : "",
2720
+ dim(` Проверено: ${total} антивирусов`),
2721
+ ], { title: "VirusTotal", color: severity === "danger" ? [255, 80, 80] : severity === "warning" ? [255, 200, 0] : [0, 200, 100], padding: 2 }))
2722
+ console.log()
2723
+ } catch (e) {
2724
+ stopSpinner()
2725
+ console.log(red(" ✗ Ошибка: ") + String(e?.message || e).slice(0, 200) + "\n")
2726
+ }
2727
+ }
2728
+
2729
+ // ---------- /ai-scan: AI-powered code analysis ----------
2730
+ async function handleAIScan(arg) {
2731
+ console.log()
2732
+ if (!arg) {
2733
+ console.log(dim(" Использование: /ai-scan <путь к файлу>\n"))
2734
+ return
2735
+ }
2736
+
2737
+ const filePath = arg.trim()
2738
+ if (!fs.existsSync(filePath)) {
2739
+ console.log(red(` ✗ Файл не найден: ${filePath}\n`))
2740
+ return
2741
+ }
2742
+
2743
+ const content = fs.readFileSync(filePath, "utf8")
2744
+ if (content.length > 50000) {
2745
+ console.log(red(" ✗ Файл слишком большой для AI-анализа (>50KB)\n"))
2746
+ return
2747
+ }
2748
+
2749
+ startSpinner("AI анализирует код")
2750
+
2751
+ const analysisPrompt = `Ты — эксперт по информационной безопасности. Проанализируй этот файл на наличие:
2752
+
2753
+ 1. Backdoors / бэкдоров
2754
+ 2. Обфусцированного кода
2755
+ 3. Утечки данных
2756
+ 4. Подозрительных API-вызовов
2757
+ 5. Скрытых сетевых соединений
2758
+ 6. Кейлоггеров
2759
+ 7. Руткитов
2760
+
2761
+ Файл: ${path.basename(filePath)}
2762
+ Размер: ${content.length} символов
2763
+
2764
+ Содержимое:
2765
+ \`\`\`
2766
+ ${content.slice(0, 15000)}
2767
+ \`\`\`
2768
+
2769
+ Ответь в формате JSON:
2770
+ {
2771
+ "verdict": "safe|suspicious|malicious",
2772
+ "confidence": 0-100,
2773
+ "threats": [{"type": "...", "severity": "low|medium|high|critical", "description": "..."}],
2774
+ "summary": "краткое резюме на русском"
2775
+ }`
2776
+
2777
+ try {
2778
+ const { text } = await generateText({
2779
+ model: getModel(state.model),
2780
+ messages: [{ role: "user", content: analysisPrompt }],
2781
+ })
2782
+ stopSpinner()
2783
+
2784
+ let result
2785
+ try {
2786
+ const jsonMatch = text.match(/\{[\s\S]*\}/)
2787
+ result = jsonMatch ? JSON.parse(jsonMatch[0]) : { verdict: "unknown", summary: text }
2788
+ } catch {
2789
+ result = { verdict: "unknown", summary: text }
2790
+ }
2791
+
2792
+ const verdictColors = { safe: [0, 200, 100], suspicious: [255, 200, 0], malicious: [255, 80, 80], unknown: [150, 150, 150] }
2793
+ const verdictLabels = { safe: "БЕЗОПАСЕН", suspicious: "ПОДОЗРИТЕЛЕН", malicious: "ОПАСЕН", unknown: "НЕОПРЕДЕЛЁН" }
2794
+ const vc = verdictColors[result.verdict] || verdictColors.unknown
2795
+
2796
+ const lines = [
2797
+ dim("Файл: ") + white(path.basename(filePath)),
2798
+ dim("Вердикт: ") + `\x1b[38;2;${vc[0]};${vc[1]};${vc[2]}m${bold(verdictLabels[result.verdict] || result.verdict)}\x1b[0m`,
2799
+ dim("Уверенность: ") + white(`${result.confidence || "?"}%`),
2800
+ "",
2801
+ ]
2802
+
2803
+ if (result.threats && result.threats.length > 0) {
2804
+ lines.push(red("Угрозы:"))
2805
+ for (const t of result.threats) {
2806
+ const sev = t.severity === "critical" ? red : t.severity === "high" ? yellow : t.severity === "medium" ? cyan : dim
2807
+ lines.push(` ${sev("▸")} [${t.severity}] ${t.type}: ${t.description}`)
2808
+ }
2809
+ } else {
2810
+ lines.push(green(" Угроз не обнаружено"))
2811
+ }
2812
+
2813
+ if (result.summary) {
2814
+ lines.push("")
2815
+ lines.push(dim("Резюме: ") + white(result.summary))
2816
+ }
2817
+
2818
+ console.log(box(lines, { title: "AI Security Analysis", color: vc, padding: 2 }))
2819
+ console.log()
2820
+ } catch (e) {
2821
+ stopSpinner()
2822
+ console.log(red(" ✗ Ошибка: ") + String(e?.message || e).slice(0, 200) + "\n")
2823
+ }
2824
+ }
2825
+
2826
+ // ---------- /ollama: Local LLM integration ----------
2827
+
2828
+ function goodbye() {
2829
+ console.log()
2830
+ console.log(gradientLine(" ✦ До встречи! Stella Coder завершает работу."))
2831
+ console.log(dim(` Сессия: ${state.turns} запросов · ~$${state.totalCost.toFixed(4)}`))
2832
+ console.log()
2833
+ process.exit(0)
2834
+ }
2835
+
2836
+ // ---------- REPL ----------
2837
+ async function welcomeAndSetup() {
2838
+ console.clear()
2839
+ console.log()
2840
+ console.log(gradientLine(" ✦ Добро пожаловать в Stella!"))
2841
+ console.log()
2842
+
2843
+ // Step 1: Try Ollama (free, no API key needed)
2844
+ let ollamaAvailable = false
2845
+ try {
2846
+ const res = await fetch(`${OLLAMA_BASE}/api/tags`, { signal: AbortSignal.timeout(2000) })
2847
+ if (res.ok) {
2848
+ const data = await res.json()
2849
+ ollamaModels = data.models || []
2850
+ ollamaAvailable = ollamaModels.length > 0
2851
+ }
2852
+ } catch {}
2853
+
2854
+ if (ollamaAvailable) {
2855
+ console.log(box([
2856
+ green("✓ Ollama найден!") + dim(` (${ollamaModels.length} моделей)`),
2857
+ "",
2858
+ white("Локальные модели работают бесплатно без API ключа."),
2859
+ "",
2860
+ ...ollamaModels.slice(0, 5).map((m, i) =>
2861
+ violet(` [${i + 1}]`) + " " + white(m.name) + dim(` (${(m.size / 1e9).toFixed(1)}GB)`)
2862
+ ),
2863
+ ollamaModels.length > 5 ? dim(` ... и ещё ${ollamaModels.length - 5}`) : "",
2864
+ ], { title: "✦ Бесплатные модели (Ollama)", color: [0, 200, 100], padding: 2 }))
2865
+ console.log()
2866
+
2867
+ const useOllama = await question(" " + green("Использовать Ollama? (Y/n) › "))
2868
+ if (!useOllama || useOllama.toLowerCase() !== "n") {
2869
+ if (ollamaModels.length === 1) {
2870
+ state.model = `ollama:${ollamaModels[0].name}`
2871
+ console.log(green(` ✓ Модель: ${ollamaModels[0].name}\n`))
2872
+ } else {
2873
+ const numStr = await question(" " + violet("Выберите номер модели › "))
2874
+ const idx = Number.parseInt(numStr) - 1
2875
+ if (ollamaModels[idx]) {
2876
+ state.model = `ollama:${ollamaModels[idx].name}`
2877
+ console.log(green(` ✓ Модель: ${ollamaModels[idx].name}\n`))
2878
+ } else {
2879
+ state.model = `ollama:${ollamaModels[0].name}`
2880
+ console.log(green(` ✓ Модель: ${ollamaModels[0].name}\n`))
2881
+ }
2882
+ }
2883
+ return "ollama"
2884
+ }
2885
+ }
2886
+
2887
+ // Step 2: No Ollama — offer API key or install Ollama
2888
+ console.log(box([
2889
+ white("Stella работает с двумя типами моделей:"),
2890
+ "",
2891
+ green("1. Ollama (бесплатно, без интернета)") + dim(" — локальные модели"),
2892
+ violet("2. Stella AI (облачные модели)") + dim(" — нужен API ключ"),
2893
+ "",
2894
+ ollamaAvailable ? "" : yellow("⚠ Ollama не найден."),
2895
+ ollamaAvailable ? "" : dim("Установите: ") + white("https://ollama.com/download"),
2896
+ ], { title: "✦ Выберите способ", color: violet, padding: 2 }))
2897
+ console.log()
2898
+
2899
+ if (!ollamaAvailable) {
2900
+ console.log(dim(" Для бесплатного использования установите Ollama:"))
2901
+ console.log(dim(" 1. Скачайте: ") + cyan("https://ollama.com/download"))
2902
+ console.log(dim(" 2. Установите модель: ") + white("ollama pull llama3.2"))
2903
+ console.log(dim(" 3. Запустите: ") + white("ollama serve"))
2904
+ console.log()
2905
+
2906
+ const installOllama = await question(" " + violet("Установлено? (y/N) › "))
2907
+ if (installOllama && installOllama.toLowerCase() === "y") {
2908
+ // Try again to detect Ollama
2909
+ try {
2910
+ const res = await fetch(`${OLLAMA_BASE}/api/tags`, { signal: AbortSignal.timeout(2000) })
2911
+ if (res.ok) {
2912
+ const data = await res.json()
2913
+ ollamaModels = data.models || []
2914
+ if (ollamaModels.length > 0) {
2915
+ if (ollamaModels.length === 1) {
2916
+ state.model = `ollama:${ollamaModels[0].name}`
2917
+ console.log(green(` ✓ Модель: ${ollamaModels[0].name}\n`))
2918
+ } else {
2919
+ console.log(box(ollamaModels.map((m, i) =>
2920
+ violet(`[${i + 1}]`) + " " + white(m.name) + dim(` (${(m.size / 1e9).toFixed(1)}GB)`)
2921
+ ), { title: "Модели Ollama", color: purple, padding: 2 }))
2922
+ const numStr = await question(" " + violet("Номер модели › "))
2923
+ const idx = Number.parseInt(numStr) - 1
2924
+ state.model = `ollama:${ollamaModels[idx]?.name || ollamaModels[0].name}`
2925
+ console.log(green(` ✓ Модель: ${ollamaModels[idx]?.name || ollamaModels[0].name}\n`))
2926
+ }
2927
+ return "ollama"
2928
+ }
2929
+ }
2930
+ } catch {}
2931
+ console.log(yellow(" Ollama всё ещё не обнаружен. Проверьте что ollama serve запущен.\n"))
2932
+ }
2933
+ }
2934
+
2935
+ // Step 3: API key for cloud models
2936
+ console.log(box([
2937
+ violet("Для облачных моделей нужен API ключ:"),
2938
+ "",
2939
+ white("1. Перейдите на сайт:"),
2940
+ cyan(" https://opencode.ai/workspace/wrk_01KWPREB55NNG22C1D2MFPT84B"),
2941
+ "",
2942
+ white("2. Скопируйте ваш API ключ"),
2943
+ white("3. Отправьте его мне"),
2944
+ ], { title: "✦ Stella AI API ключ", color: violet, padding: 2 }))
2945
+ console.log()
2946
+
2947
+ let attempts = 0
2948
+ while (attempts < 5) {
2949
+ const key = await question(" " + violet("Введите API ключ › "))
2950
+ const trimmed = key.trim()
2951
+
2952
+ if (!trimmed) {
2953
+ console.log(red(" Ключ не может быть пустым\n"))
2954
+ attempts++
2955
+ continue
2956
+ }
2957
+
2958
+ if (trimmed.length < 10) {
2959
+ console.log(red(" Ключ слишком короткий. Попробуйте ещё раз.\n"))
2960
+ attempts++
2961
+ continue
2962
+ }
2963
+
2964
+ const result = saveApiKey(trimmed)
2965
+ if (result.ok) {
2966
+ const hw = getHardwareInfo()
2967
+ console.log()
2968
+ console.log(green(" ✓ Ключ сохранён!"))
2969
+ console.log(dim(" Платформа: ") + gray(hw.platform + " / " + hw.hostname))
2970
+ console.log()
2971
+ return trimmed
2972
+ } else {
2973
+ console.log(red(" ✗ " + result.error + "\n"))
2974
+ attempts++
2975
+ }
2976
+ }
2977
+
2978
+ console.log(red(" Превышено количество попыток. Выход.\n"))
2979
+ process.exit(1)
2980
+ }
2981
+
2982
+ async function main() {
2983
+ // Code integrity check
2984
+ const integrity = verifyCodeIntegrity()
2985
+ if (integrity.warning) {
2986
+ console.log(yellow(" ⚠ " + integrity.warning))
2987
+ }
2988
+
2989
+ if (printMode) {
2990
+ // In print mode, try to use piped input as prompt
2991
+ // API key should already be loaded from secure vault
2992
+ const prompt = printPrompt || (await readStdin())
2993
+ if (!prompt) {
2994
+ if (!apiKey && !state.model.startsWith("ollama:")) {
2995
+ console.error(red(" API ключ не найден. Запустите stella без -p для настройки."))
2996
+ process.exit(1)
2997
+ }
2998
+ console.error("Нет запроса. Использование: stella -p \"вопрос\"")
2999
+ process.exit(1)
3000
+ }
3001
+ if (!apiKey && !state.model.startsWith("ollama:")) {
3002
+ console.error(red(" API ключ не найден. Запустите stella без -p для настройки."))
3003
+ process.exit(1)
3004
+ }
3005
+ await runTurn(prompt)
3006
+ process.exit(0)
3007
+ }
3008
+
3009
+ // Create readline interface FIRST (needed for welcomeAndSetup question prompts)
3010
+ rl = readline.createInterface({
3011
+ input: process.stdin,
3012
+ output: process.stdout,
3013
+ completer: (line) => {
3014
+ if (!line.startsWith("/")) return [[], line]
3015
+ const hits = COMMANDS.map(([c]) => c).filter((c) => c.startsWith(line))
3016
+ return [hits, line]
3017
+ },
3018
+ })
3019
+
3020
+ // Check for API key — show welcome if not found
3021
+ if (!apiKey && !state.model.startsWith("ollama:")) {
3022
+ apiKey = await welcomeAndSetup()
3023
+ if (!state.model.startsWith("ollama:")) {
3024
+ state.model = MODELS[0].id
3025
+ }
3026
+ }
3027
+
3028
+ console.clear()
3029
+ printBanner({ model: state.model, cwd: process.cwd(), version: VERSION })
3030
+
3031
+ let lastSigint = 0
3032
+ rl.on("SIGINT", () => {
3033
+ const now = Date.now()
3034
+ if (now - lastSigint < 1500) goodbye()
3035
+ lastSigint = now
3036
+ console.log(dim("\n (нажми Ctrl+C ещё раз для выхода)"))
3037
+ promptUser()
3038
+ })
3039
+ rl.on("close", goodbye)
3040
+
3041
+ const promptUser = () => {
3042
+ rl.question(gradientLine("❯ "), async (line) => {
3043
+ const input = line.trim()
3044
+ if (!input) return promptUser()
3045
+ if (input.startsWith("/")) {
3046
+ await handleCommand(input)
3047
+ } else if (input.startsWith("!")) {
3048
+ try {
3049
+ const { execSync } = await import("node:child_process")
3050
+ const out = execSync(input.slice(1), { encoding: "utf8", stdio: ["inherit", "pipe", "pipe"] })
3051
+ console.log(dim(out))
3052
+ } catch (e) {
3053
+ console.log(red(String(e.stderr || e.message).slice(0, 500)))
3054
+ }
3055
+ } else {
3056
+ await runTurn(input)
3057
+ }
3058
+ promptUser()
3059
+ })
3060
+ }
3061
+ promptUser()
3062
+ }
3063
+
3064
+ function readStdin() {
3065
+ return new Promise((res) => {
3066
+ if (process.stdin.isTTY) return res("")
3067
+ let data = ""
3068
+ process.stdin.on("data", (c) => (data += c))
3069
+ process.stdin.on("end", () => res(data.trim()))
3070
+ })
3071
+ }
3072
+
3073
+ main()