stella-coder 3.9.2 → 5.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.
- package/.mcp.json +11 -0
- package/.opencode/skills/banner-design/SKILL.md +196 -0
- package/.opencode/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
- package/.opencode/skills/brand/SKILL.md +97 -0
- package/.opencode/skills/brand/references/approval-checklist.md +169 -0
- package/.opencode/skills/brand/references/asset-organization.md +157 -0
- package/.opencode/skills/brand/references/brand-guideline-template.md +140 -0
- package/.opencode/skills/brand/references/color-palette-management.md +186 -0
- package/.opencode/skills/brand/references/consistency-checklist.md +94 -0
- package/.opencode/skills/brand/references/logo-usage-rules.md +185 -0
- package/.opencode/skills/brand/references/messaging-framework.md +85 -0
- package/.opencode/skills/brand/references/typography-specifications.md +214 -0
- package/.opencode/skills/brand/references/update.md +118 -0
- package/.opencode/skills/brand/references/visual-identity.md +96 -0
- package/.opencode/skills/brand/references/voice-framework.md +88 -0
- package/.opencode/skills/brand/scripts/extract-colors.cjs +341 -0
- package/.opencode/skills/brand/scripts/inject-brand-context.cjs +349 -0
- package/.opencode/skills/brand/scripts/sync-brand-to-tokens.cjs +248 -0
- package/.opencode/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +52 -0
- package/.opencode/skills/brand/scripts/validate-asset.cjs +387 -0
- package/.opencode/skills/brand/templates/brand-guidelines-starter.md +275 -0
- package/.opencode/skills/design/SKILL.md +313 -0
- package/.opencode/skills/design/data/cip/deliverables.csv +51 -0
- package/.opencode/skills/design/data/cip/industries.csv +21 -0
- package/.opencode/skills/design/data/cip/mockup-contexts.csv +21 -0
- package/.opencode/skills/design/data/cip/styles.csv +21 -0
- package/.opencode/skills/design/data/icon/styles.csv +16 -0
- package/.opencode/skills/design/data/logo/colors.csv +56 -0
- package/.opencode/skills/design/data/logo/industries.csv +56 -0
- package/.opencode/skills/design/data/logo/styles.csv +56 -0
- package/.opencode/skills/design/references/banner-sizes-and-styles.md +118 -0
- package/.opencode/skills/design/references/cip-deliverable-guide.md +95 -0
- package/.opencode/skills/design/references/cip-design.md +121 -0
- package/.opencode/skills/design/references/cip-prompt-engineering.md +84 -0
- package/.opencode/skills/design/references/cip-style-guide.md +68 -0
- package/.opencode/skills/design/references/design-routing.md +207 -0
- package/.opencode/skills/design/references/icon-design.md +122 -0
- package/.opencode/skills/design/references/logo-color-psychology.md +101 -0
- package/.opencode/skills/design/references/logo-design.md +92 -0
- package/.opencode/skills/design/references/logo-prompt-engineering.md +158 -0
- package/.opencode/skills/design/references/logo-style-guide.md +109 -0
- package/.opencode/skills/design/references/slides-copywriting-formulas.md +84 -0
- package/.opencode/skills/design/references/slides-create.md +4 -0
- package/.opencode/skills/design/references/slides-html-template.md +295 -0
- package/.opencode/skills/design/references/slides-layout-patterns.md +137 -0
- package/.opencode/skills/design/references/slides-strategies.md +94 -0
- package/.opencode/skills/design/references/slides.md +42 -0
- package/.opencode/skills/design/references/social-photos-design.md +329 -0
- package/.opencode/skills/design/scripts/cip/core.py +215 -0
- package/.opencode/skills/design/scripts/cip/generate.py +484 -0
- package/.opencode/skills/design/scripts/cip/render-html.py +424 -0
- package/.opencode/skills/design/scripts/cip/search.py +127 -0
- package/.opencode/skills/design/scripts/icon/generate.py +487 -0
- package/.opencode/skills/design/scripts/logo/core.py +175 -0
- package/.opencode/skills/design/scripts/logo/generate.py +362 -0
- package/.opencode/skills/design/scripts/logo/search.py +114 -0
- package/.opencode/skills/design-system/SKILL.md +244 -0
- package/.opencode/skills/design-system/data/slide-backgrounds.csv +11 -0
- package/.opencode/skills/design-system/data/slide-charts.csv +26 -0
- package/.opencode/skills/design-system/data/slide-color-logic.csv +14 -0
- package/.opencode/skills/design-system/data/slide-copy.csv +26 -0
- package/.opencode/skills/design-system/data/slide-layout-logic.csv +16 -0
- package/.opencode/skills/design-system/data/slide-layouts.csv +26 -0
- package/.opencode/skills/design-system/data/slide-strategies.csv +16 -0
- package/.opencode/skills/design-system/data/slide-typography.csv +15 -0
- package/.opencode/skills/design-system/references/component-specs.md +236 -0
- package/.opencode/skills/design-system/references/component-tokens.md +214 -0
- package/.opencode/skills/design-system/references/primitive-tokens.md +203 -0
- package/.opencode/skills/design-system/references/semantic-tokens.md +215 -0
- package/.opencode/skills/design-system/references/states-and-variants.md +241 -0
- package/.opencode/skills/design-system/references/tailwind-integration.md +251 -0
- package/.opencode/skills/design-system/references/token-architecture.md +224 -0
- package/.opencode/skills/design-system/scripts/embed-tokens.cjs +99 -0
- package/.opencode/skills/design-system/scripts/fetch-background.py +317 -0
- package/.opencode/skills/design-system/scripts/generate-slide.py +770 -0
- package/.opencode/skills/design-system/scripts/generate-tokens.cjs +205 -0
- package/.opencode/skills/design-system/scripts/html-token-validator.py +327 -0
- package/.opencode/skills/design-system/scripts/search-slides.py +218 -0
- package/.opencode/skills/design-system/scripts/slide-token-validator.py +35 -0
- package/.opencode/skills/design-system/scripts/slide_search_core.py +453 -0
- package/.opencode/skills/design-system/scripts/tests/test_validate_tokens.py +48 -0
- package/.opencode/skills/design-system/scripts/validate-tokens.cjs +246 -0
- package/.opencode/skills/design-system/templates/design-tokens-starter.json +143 -0
- package/.opencode/skills/slides/SKILL.md +40 -0
- package/.opencode/skills/slides/references/copywriting-formulas.md +84 -0
- package/.opencode/skills/slides/references/create.md +4 -0
- package/.opencode/skills/slides/references/html-template.md +295 -0
- package/.opencode/skills/slides/references/layout-patterns.md +137 -0
- package/.opencode/skills/slides/references/slide-strategies.md +94 -0
- package/.opencode/skills/ui-styling/LICENSE.txt +202 -0
- package/.opencode/skills/ui-styling/SKILL.md +324 -0
- package/.opencode/skills/ui-styling/references/canvas-design-system.md +320 -0
- package/.opencode/skills/ui-styling/references/shadcn-accessibility.md +471 -0
- package/.opencode/skills/ui-styling/references/shadcn-components.md +424 -0
- package/.opencode/skills/ui-styling/references/shadcn-theming.md +373 -0
- package/.opencode/skills/ui-styling/references/tailwind-customization.md +483 -0
- package/.opencode/skills/ui-styling/references/tailwind-responsive.md +382 -0
- package/.opencode/skills/ui-styling/references/tailwind-utilities.md +455 -0
- package/.opencode/skills/ui-styling/scripts/requirements.txt +17 -0
- package/.opencode/skills/ui-styling/scripts/shadcn_add.py +308 -0
- package/.opencode/skills/ui-styling/scripts/tailwind_config_gen.py +473 -0
- package/.opencode/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
- package/.opencode/skills/ui-styling/scripts/tests/requirements.txt +3 -0
- package/.opencode/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
- package/.opencode/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +394 -0
- package/.opencode/skills/ui-ux-pro-max/SKILL.md +396 -0
- package/.opencode/skills/ui-ux-pro-max/data/_sync_all.py +414 -0
- package/.opencode/skills/ui-ux-pro-max/data/app-interface.csv +31 -0
- package/.opencode/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.opencode/skills/ui-ux-pro-max/data/colors.csv +193 -0
- package/.opencode/skills/ui-ux-pro-max/data/design.csv +1776 -0
- package/.opencode/skills/ui-ux-pro-max/data/draft.csv +1779 -0
- package/.opencode/skills/ui-ux-pro-max/data/google-fonts.csv +1924 -0
- package/.opencode/skills/ui-ux-pro-max/data/icons.csv +106 -0
- package/.opencode/skills/ui-ux-pro-max/data/landing.csv +35 -0
- package/.opencode/skills/ui-ux-pro-max/data/motion.csv +17 -0
- package/.opencode/skills/ui-ux-pro-max/data/products.csv +193 -0
- package/.opencode/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/angular.csv +51 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/avalonia.csv +57 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/javafx.csv +76 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/laravel.csv +51 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +71 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/threejs.csv +54 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/uno.csv +60 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/uwp.csv +56 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/winui.csv +60 -0
- package/.opencode/skills/ui-ux-pro-max/data/stacks/wpf.csv +57 -0
- package/.opencode/skills/ui-ux-pro-max/data/styles.csv +85 -0
- package/.opencode/skills/ui-ux-pro-max/data/typography.csv +75 -0
- package/.opencode/skills/ui-ux-pro-max/data/ui-reasoning.csv +162 -0
- package/.opencode/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
- package/.opencode/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc +0 -0
- package/.opencode/skills/ui-ux-pro-max/scripts/core.py +274 -0
- package/.opencode/skills/ui-ux-pro-max/scripts/design_system.py +1329 -0
- package/.opencode/skills/ui-ux-pro-max/scripts/search.py +127 -0
- package/README.md +25 -30
- package/STELLA.md +1 -1
- package/dist/antimalware/database.mjs +871 -0
- package/dist/antimalware/index.mjs +8 -0
- package/dist/antimalware/scanner.mjs +591 -0
- package/dist/antimalware/ui.mjs +570 -0
- package/dist/banner.mjs +46 -0
- package/dist/index.mjs +3073 -0
- package/dist/markdown.mjs +100 -0
- package/dist/sea-config.json +4 -0
- package/dist/security.mjs +237 -0
- package/dist/theme.mjs +89 -0
- package/dist/tools.mjs +3145 -0
- package/install-stella.bat +44 -0
- package/package.json +5 -4
- package/stella-cli/coding-brain.mjs +753 -0
- package/stella-cli/index.mjs +931 -15
- package/stella-cli/mcp.mjs +296 -0
- package/stella-cli/presentations.mjs +1106 -0
- package/stella-cli/subagents.mjs +142 -0
- package/stella-cli/telegram-bot.mjs +824 -0
- package/stella-cli/tg-bg.bat +13 -0
- package/stella-cli/tg-server.bat +15 -0
- package/stella-cli/tg-server.mjs +116 -0
- package/stella-cli/theme.mjs +6 -4
- package/create_cats_pptx.py +0 -121
- package/hello.py +0 -2
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()
|