handycode 2.1.0__tar.gz
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.
- handycode-2.1.0/LICENSE +21 -0
- handycode-2.1.0/PKG-INFO +20 -0
- handycode-2.1.0/README.md +68 -0
- handycode-2.1.0/handycode/__init__.py +13 -0
- handycode-2.1.0/handycode/__main__.py +5 -0
- handycode-2.1.0/handycode/assistant.py +533 -0
- handycode-2.1.0/handycode/cli.py +130 -0
- handycode-2.1.0/handycode/config.py +78 -0
- handycode-2.1.0/handycode/file_manager.py +238 -0
- handycode-2.1.0/handycode/logo.py +90 -0
- handycode-2.1.0/handycode/main.py +26 -0
- handycode-2.1.0/handycode/models.py +77 -0
- handycode-2.1.0/handycode/project_templates.py +55 -0
- handycode-2.1.0/handycode/security.py +103 -0
- handycode-2.1.0/handycode/utils.py +92 -0
- handycode-2.1.0/handycode.egg-info/PKG-INFO +20 -0
- handycode-2.1.0/handycode.egg-info/SOURCES.txt +21 -0
- handycode-2.1.0/handycode.egg-info/dependency_links.txt +1 -0
- handycode-2.1.0/handycode.egg-info/entry_points.txt +3 -0
- handycode-2.1.0/handycode.egg-info/requires.txt +1 -0
- handycode-2.1.0/handycode.egg-info/top_level.txt +1 -0
- handycode-2.1.0/setup.cfg +4 -0
- handycode-2.1.0/setup.py +20 -0
handycode-2.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HandyCode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
handycode-2.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: handycode
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: AI Code Assistant for DeepSeek
|
|
5
|
+
Home-page: https://github.com/AuraTechno/HandyCode
|
|
6
|
+
Author: AuraTechno
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: requests>=2.28.0
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: description
|
|
13
|
+
Dynamic: description-content-type
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
Dynamic: summary
|
|
19
|
+
|
|
20
|
+
HandyCode - AI Code Assistant
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# HandyCode
|
|
2
|
+
|
|
3
|
+
AI-ассистент для командной строки, аналог Claude Code. Создавайте проекты, пишите код и управляйте файлами с помощью AI.
|
|
4
|
+
|
|
5
|
+
## Быстрая установка одной командой
|
|
6
|
+
|
|
7
|
+
### Linux/MacOS
|
|
8
|
+
```bash
|
|
9
|
+
curl -sSL https://raw.githubusercontent.com/yourusername/handycode/main/install.sh | bash
|
|
10
|
+
```
|
|
11
|
+
### Windows
|
|
12
|
+
```bash
|
|
13
|
+
irm https://raw.githubusercontent.com/yourusername/handycode/main/install.bat | iex
|
|
14
|
+
```
|
|
15
|
+
### Через pip
|
|
16
|
+
```bash
|
|
17
|
+
pip install handycode
|
|
18
|
+
```
|
|
19
|
+
### Использование
|
|
20
|
+
```bash
|
|
21
|
+
hc # Запуск в текущей директории
|
|
22
|
+
hc -p /путь/к/проекту # Запуск в конкретной папке
|
|
23
|
+
hc -c "Создай React приложение" # Быстрое создание проекта
|
|
24
|
+
hc -m gpt4 # Выбор модели
|
|
25
|
+
hc -y -c "Создай Python API" # Авто-подтверждение
|
|
26
|
+
handycode --help # Справка
|
|
27
|
+
```
|
|
28
|
+
### Команды внутри программы
|
|
29
|
+
```bash
|
|
30
|
+
/help, /помощь - Справка
|
|
31
|
+
|
|
32
|
+
/scan, /сканировать - Показать структуру проекта
|
|
33
|
+
|
|
34
|
+
/models, /модели - Список моделей
|
|
35
|
+
|
|
36
|
+
/model, /модель - Переключить модель
|
|
37
|
+
|
|
38
|
+
/clear, /очистить - Очистить историю
|
|
39
|
+
|
|
40
|
+
/save, /сохранить - Сохранить сессию
|
|
41
|
+
|
|
42
|
+
/stats, /статистика - Статистика
|
|
43
|
+
|
|
44
|
+
/exit, /выход - Выйти
|
|
45
|
+
```
|
|
46
|
+
### Настройка API ключа
|
|
47
|
+
|
|
48
|
+
Получите ключ на https://openrouter.ai/keys
|
|
49
|
+
|
|
50
|
+
Создайте файл ~/.handycode/.env:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
OPENROUTER_API_KEY=ваш_ключ
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Безопасность
|
|
57
|
+
Все операции подтверждаются пользователем
|
|
58
|
+
|
|
59
|
+
Создаются резервные копии перед изменениями
|
|
60
|
+
|
|
61
|
+
Проверка путей на безопасность
|
|
62
|
+
|
|
63
|
+
Запрет выхода за пределы проекта
|
|
64
|
+
|
|
65
|
+
Блокировка опасных команд
|
|
66
|
+
|
|
67
|
+
### Лицензия
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HandyCode - AI Ассистент для разработки
|
|
3
|
+
Аналог Claude Code для командной строки
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "1.0.0"
|
|
7
|
+
__author__ = "AURA Tec."
|
|
8
|
+
__license__ = "MIT"
|
|
9
|
+
|
|
10
|
+
from .main import main
|
|
11
|
+
from .assistant import HandyCode
|
|
12
|
+
|
|
13
|
+
__all__ = ["main", "HandyCode"]
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Основной класс ассистента HandyCode
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import atexit
|
|
10
|
+
import signal
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List, Dict, Optional
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import readline
|
|
18
|
+
HAS_READLINE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
HAS_READLINE = False
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import requests
|
|
24
|
+
HAS_REQUESTS = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
HAS_REQUESTS = False
|
|
27
|
+
import urllib.request
|
|
28
|
+
import urllib.error
|
|
29
|
+
import ssl
|
|
30
|
+
|
|
31
|
+
from handycode.config import Config
|
|
32
|
+
from handycode.models import MODELS, get_model_settings
|
|
33
|
+
from handycode.file_manager import FileManager
|
|
34
|
+
from handycode.security import SecurityChecker
|
|
35
|
+
from handycode.utils import (
|
|
36
|
+
print_colored, print_header, print_success,
|
|
37
|
+
print_error, print_warning, print_info, print_logo
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HandyCode:
|
|
42
|
+
def __init__(self, project_path, model="deepseek", auto_approve=False, config=None):
|
|
43
|
+
self.project_path = project_path
|
|
44
|
+
self.auto_approve = auto_approve
|
|
45
|
+
self.config = config or Config()
|
|
46
|
+
|
|
47
|
+
self.api_key = self.config.get_api_key()
|
|
48
|
+
if not self.api_key:
|
|
49
|
+
raise ValueError("API key not found")
|
|
50
|
+
|
|
51
|
+
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
52
|
+
self.current_model = MODELS.get(model, MODELS["deepseek"])
|
|
53
|
+
self.model_settings = get_model_settings(self.current_model)
|
|
54
|
+
|
|
55
|
+
self.file_manager = FileManager(self.project_path)
|
|
56
|
+
self.security = SecurityChecker(self.project_path)
|
|
57
|
+
|
|
58
|
+
project_context = self._build_project_context()
|
|
59
|
+
|
|
60
|
+
self.conversation_history = [
|
|
61
|
+
{"role": "system", "content": self._get_system_prompt() + project_context}
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
self.stats = {
|
|
65
|
+
"messages_sent": 0,
|
|
66
|
+
"files_created": [],
|
|
67
|
+
"files_modified": [],
|
|
68
|
+
"files_deleted": [],
|
|
69
|
+
"commands_executed": [],
|
|
70
|
+
"start_time": datetime.now()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
self._setup_readline()
|
|
74
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
|
75
|
+
self._interrupt_count = 0
|
|
76
|
+
|
|
77
|
+
def _build_project_context(self):
|
|
78
|
+
"""Собирает контекст проекта"""
|
|
79
|
+
context = f"\n\n=== PROJECT CONTEXT ===\n"
|
|
80
|
+
context += f"Working directory: {self.project_path}\n"
|
|
81
|
+
context += f"OS: {sys.platform}\n"
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
all_files = []
|
|
85
|
+
for ext in self.file_manager.allowed_extensions:
|
|
86
|
+
all_files.extend(self.project_path.rglob(f"*{ext}"))
|
|
87
|
+
|
|
88
|
+
all_files.extend(self.project_path.rglob("*"))
|
|
89
|
+
all_files = list(set(all_files))
|
|
90
|
+
|
|
91
|
+
files = [f for f in all_files if f.is_file()
|
|
92
|
+
and not any(ex in f.parts for ex in self.file_manager.excluded_dirs)]
|
|
93
|
+
|
|
94
|
+
context += f"\nFiles in project ({len(files)} total):\n"
|
|
95
|
+
|
|
96
|
+
for file in sorted(files):
|
|
97
|
+
try:
|
|
98
|
+
rel_path = file.relative_to(self.project_path)
|
|
99
|
+
size = file.stat().st_size
|
|
100
|
+
context += f" - {rel_path} ({self._format_size(size)})\n"
|
|
101
|
+
except:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
context += f"\nFile contents (for context):\n"
|
|
105
|
+
total_size = 0
|
|
106
|
+
max_total = 30000
|
|
107
|
+
|
|
108
|
+
for file in sorted(files):
|
|
109
|
+
if total_size >= max_total:
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
content = file.read_text(encoding='utf-8', errors='ignore')
|
|
114
|
+
if len(content) > 3000:
|
|
115
|
+
content = content[:3000] + "\n... (truncated)"
|
|
116
|
+
|
|
117
|
+
rel_path = file.relative_to(self.project_path)
|
|
118
|
+
context += f"\n=== {rel_path} ===\n{content}\n"
|
|
119
|
+
total_size += len(content)
|
|
120
|
+
except:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
context += f"\nError: {e}\n"
|
|
125
|
+
|
|
126
|
+
return context
|
|
127
|
+
|
|
128
|
+
def _format_size(self, size):
|
|
129
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
130
|
+
if size < 1024:
|
|
131
|
+
return f"{size:.0f}{unit}"
|
|
132
|
+
size /= 1024
|
|
133
|
+
return f"{size:.0f}GB"
|
|
134
|
+
|
|
135
|
+
def _get_system_prompt(self):
|
|
136
|
+
return """You are HandyCode, a powerful AI code assistant with FULL file system access.
|
|
137
|
+
|
|
138
|
+
YOU CAN:
|
|
139
|
+
- CREATE files: [[CREATE:path/to/file.ext]] content
|
|
140
|
+
- MODIFY files: [[MODIFY:path/to/file.ext]] new content
|
|
141
|
+
- DELETE files: [[DELETE:path/to/file.ext]]
|
|
142
|
+
- READ files: [[READ:path/to/file.ext]]
|
|
143
|
+
- LIST directory: [[LIST:path/to/dir]]
|
|
144
|
+
- EXECUTE commands: [[EXEC:command]]
|
|
145
|
+
- CREATE folders: [[MKDIR:path/to/dir]]
|
|
146
|
+
- COPY files: [[COPY:source]] -> [[CREATE:destination]] (use CREATE with content)
|
|
147
|
+
- MOVE files: [[MOVE:source]] [[CREATE:destination]]
|
|
148
|
+
|
|
149
|
+
FILES ARE AUTO-CREATED without asking. Only COMMANDS need confirmation.
|
|
150
|
+
You see ALL project files and their contents.
|
|
151
|
+
|
|
152
|
+
RULES:
|
|
153
|
+
1. CREATE/MODIFY/DELETE/MKDIR happen automatically
|
|
154
|
+
2. EXEC commands need user confirmation
|
|
155
|
+
3. Show COMPLETE file contents
|
|
156
|
+
4. Create ALL needed files
|
|
157
|
+
5. Use the project context
|
|
158
|
+
|
|
159
|
+
Speak Russian. Write code in English."""
|
|
160
|
+
|
|
161
|
+
def _setup_readline(self):
|
|
162
|
+
if not HAS_READLINE:
|
|
163
|
+
return
|
|
164
|
+
try:
|
|
165
|
+
histfile = os.path.join(os.path.expanduser("~"), ".handycode", "history")
|
|
166
|
+
os.makedirs(os.path.dirname(histfile), exist_ok=True)
|
|
167
|
+
readline.read_history_file(histfile)
|
|
168
|
+
readline.set_history_length(1000)
|
|
169
|
+
atexit.register(readline.write_history_file, histfile)
|
|
170
|
+
except:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def _signal_handler(self, sig, frame):
|
|
174
|
+
self._interrupt_count += 1
|
|
175
|
+
if self._interrupt_count == 1:
|
|
176
|
+
print("\n\nPress Ctrl+C again to exit")
|
|
177
|
+
else:
|
|
178
|
+
os._exit(0)
|
|
179
|
+
|
|
180
|
+
def reset_interrupt(self):
|
|
181
|
+
self._interrupt_count = 0
|
|
182
|
+
|
|
183
|
+
def _make_request_stream(self, data):
|
|
184
|
+
"""Отправляет запрос и получает ответ в реальном времени"""
|
|
185
|
+
if not HAS_REQUESTS:
|
|
186
|
+
return self._make_request(data)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
response = requests.post(
|
|
190
|
+
self.api_url,
|
|
191
|
+
headers={
|
|
192
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
193
|
+
"Content-Type": "application/json"
|
|
194
|
+
},
|
|
195
|
+
json={**data, "stream": True},
|
|
196
|
+
timeout=120,
|
|
197
|
+
stream=True
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
full_response = ""
|
|
201
|
+
|
|
202
|
+
for line in response.iter_lines():
|
|
203
|
+
if line:
|
|
204
|
+
line = line.decode('utf-8')
|
|
205
|
+
if line.startswith('data: '):
|
|
206
|
+
data_str = line[6:]
|
|
207
|
+
if data_str.strip() == '[DONE]':
|
|
208
|
+
break
|
|
209
|
+
try:
|
|
210
|
+
chunk = json.loads(data_str)
|
|
211
|
+
if 'choices' in chunk and chunk['choices']:
|
|
212
|
+
delta = chunk['choices'][0].get('delta', {})
|
|
213
|
+
content = delta.get('content', '')
|
|
214
|
+
if content:
|
|
215
|
+
print(content, end="", flush=True)
|
|
216
|
+
full_response += content
|
|
217
|
+
except:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
print()
|
|
221
|
+
return full_response
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print_error(f"API Error: {e}")
|
|
225
|
+
return ""
|
|
226
|
+
|
|
227
|
+
def _make_request(self, data):
|
|
228
|
+
"""Обычный запрос без стриминга"""
|
|
229
|
+
if HAS_REQUESTS:
|
|
230
|
+
try:
|
|
231
|
+
response = requests.post(
|
|
232
|
+
self.api_url,
|
|
233
|
+
headers={
|
|
234
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
235
|
+
"Content-Type": "application/json"
|
|
236
|
+
},
|
|
237
|
+
json=data,
|
|
238
|
+
timeout=120
|
|
239
|
+
)
|
|
240
|
+
result = response.json()
|
|
241
|
+
if 'choices' in result and result['choices']:
|
|
242
|
+
return result['choices'][0]['message']['content']
|
|
243
|
+
except Exception as e:
|
|
244
|
+
print_error(f"API Error: {e}")
|
|
245
|
+
return ""
|
|
246
|
+
|
|
247
|
+
def send_message(self, user_input):
|
|
248
|
+
if user_input.startswith('/'):
|
|
249
|
+
return self._handle_command(user_input)
|
|
250
|
+
|
|
251
|
+
self.conversation_history.append({"role": "user", "content": user_input})
|
|
252
|
+
|
|
253
|
+
payload = {
|
|
254
|
+
"model": self.current_model,
|
|
255
|
+
"messages": self.conversation_history,
|
|
256
|
+
"temperature": self.model_settings.get("temperature", 0.3),
|
|
257
|
+
"max_tokens": self.model_settings.get("max_tokens", 8000),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
print_info(f"\nDEEPSEEK:")
|
|
262
|
+
response = self._make_request_stream(payload)
|
|
263
|
+
|
|
264
|
+
if response:
|
|
265
|
+
self.conversation_history.append({"role": "assistant", "content": response})
|
|
266
|
+
|
|
267
|
+
actions = self._parse_actions(response)
|
|
268
|
+
if actions:
|
|
269
|
+
self._execute_actions_smart(actions)
|
|
270
|
+
|
|
271
|
+
self.stats["messages_sent"] += 1
|
|
272
|
+
return response
|
|
273
|
+
except Exception as e:
|
|
274
|
+
return print_error(f"Error: {e}")
|
|
275
|
+
|
|
276
|
+
def _parse_actions(self, response):
|
|
277
|
+
"""Парсит все типы действий"""
|
|
278
|
+
actions = []
|
|
279
|
+
|
|
280
|
+
# CREATE
|
|
281
|
+
for match in re.finditer(r'\[\[CREATE:(.+?)\]\](.*?)(?=\[\[|$)', response, re.DOTALL):
|
|
282
|
+
path = match.group(1).strip()
|
|
283
|
+
content = match.group(2).strip()
|
|
284
|
+
content = re.sub(r'^```[\w]*\n?', '', content)
|
|
285
|
+
content = re.sub(r'\n?```$', '', content)
|
|
286
|
+
actions.append({'type': 'create', 'path': path, 'content': content})
|
|
287
|
+
|
|
288
|
+
# MODIFY
|
|
289
|
+
for match in re.finditer(r'\[\[MODIFY:(.+?)\]\](.*?)(?=\[\[|$)', response, re.DOTALL):
|
|
290
|
+
path = match.group(1).strip()
|
|
291
|
+
content = match.group(2).strip()
|
|
292
|
+
content = re.sub(r'^```[\w]*\n?', '', content)
|
|
293
|
+
content = re.sub(r'\n?```$', '', content)
|
|
294
|
+
actions.append({'type': 'modify', 'path': path, 'content': content})
|
|
295
|
+
|
|
296
|
+
# DELETE
|
|
297
|
+
for match in re.finditer(r'\[\[DELETE:(.+?)\]\]', response):
|
|
298
|
+
actions.append({'type': 'delete', 'path': match.group(1).strip()})
|
|
299
|
+
|
|
300
|
+
# READ
|
|
301
|
+
for match in re.finditer(r'\[\[READ:(.+?)\]\]', response):
|
|
302
|
+
actions.append({'type': 'read', 'path': match.group(1).strip()})
|
|
303
|
+
|
|
304
|
+
# LIST
|
|
305
|
+
for match in re.finditer(r'\[\[LIST:(.+?)\]\]', response):
|
|
306
|
+
actions.append({'type': 'list', 'path': match.group(1).strip()})
|
|
307
|
+
|
|
308
|
+
# MKDIR
|
|
309
|
+
for match in re.finditer(r'\[\[MKDIR:(.+?)\]\]', response):
|
|
310
|
+
actions.append({'type': 'mkdir', 'path': match.group(1).strip()})
|
|
311
|
+
|
|
312
|
+
# COPY
|
|
313
|
+
for match in re.finditer(r'\[\[COPY:(.+?)\]\]', response):
|
|
314
|
+
actions.append({'type': 'copy', 'path': match.group(1).strip()})
|
|
315
|
+
|
|
316
|
+
# MOVE
|
|
317
|
+
for match in re.finditer(r'\[\[MOVE:(.+?)\]\]', response):
|
|
318
|
+
actions.append({'type': 'move', 'path': match.group(1).strip()})
|
|
319
|
+
|
|
320
|
+
# EXEC
|
|
321
|
+
for match in re.finditer(r'\[\[EXEC:(.+?)\]\]', response):
|
|
322
|
+
actions.append({'type': 'exec', 'command': match.group(1).strip()})
|
|
323
|
+
|
|
324
|
+
return actions
|
|
325
|
+
|
|
326
|
+
def _execute_actions_smart(self, actions):
|
|
327
|
+
"""Умное выполнение: файлы авто, команды с подтверждением"""
|
|
328
|
+
if not actions:
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
file_actions = [a for a in actions if a['type'] in ['create', 'modify', 'delete', 'mkdir', 'read', 'list', 'copy', 'move']]
|
|
332
|
+
exec_actions = [a for a in actions if a['type'] == 'exec']
|
|
333
|
+
|
|
334
|
+
# Файловые операции выполняем автоматически
|
|
335
|
+
if file_actions:
|
|
336
|
+
print_header("\nAUTO FILE OPERATIONS:")
|
|
337
|
+
for action in file_actions:
|
|
338
|
+
self._execute_action(action)
|
|
339
|
+
|
|
340
|
+
# Команды требуют подтверждения
|
|
341
|
+
if exec_actions:
|
|
342
|
+
print_header("\nCOMMANDS TO EXECUTE:")
|
|
343
|
+
for i, action in enumerate(exec_actions, 1):
|
|
344
|
+
print(f" {i}. {action['command']}")
|
|
345
|
+
|
|
346
|
+
print("\n[A] Execute all [S] Skip all [1-N] Select [C] Cancel")
|
|
347
|
+
choice = input("> ").strip().upper()
|
|
348
|
+
|
|
349
|
+
if choice == 'A':
|
|
350
|
+
for action in exec_actions:
|
|
351
|
+
self._execute_action(action)
|
|
352
|
+
elif choice == 'S':
|
|
353
|
+
print_warning("Skipped")
|
|
354
|
+
elif choice == 'C':
|
|
355
|
+
print_warning("Cancelled")
|
|
356
|
+
elif choice.isdigit():
|
|
357
|
+
idx = int(choice) - 1
|
|
358
|
+
if 0 <= idx < len(exec_actions):
|
|
359
|
+
self._execute_action(exec_actions[idx])
|
|
360
|
+
|
|
361
|
+
def _execute_action(self, action):
|
|
362
|
+
"""Выполняет одно действие"""
|
|
363
|
+
try:
|
|
364
|
+
if action['type'] == 'create':
|
|
365
|
+
self.file_manager.create_file(action['path'], action['content'])
|
|
366
|
+
self.stats["files_created"].append(action['path'])
|
|
367
|
+
|
|
368
|
+
elif action['type'] == 'modify':
|
|
369
|
+
self.file_manager.modify_file(action['path'], action['content'])
|
|
370
|
+
self.stats["files_modified"].append(action['path'])
|
|
371
|
+
|
|
372
|
+
elif action['type'] == 'delete':
|
|
373
|
+
self._delete_file(action['path'])
|
|
374
|
+
self.stats["files_deleted"].append(action['path'])
|
|
375
|
+
|
|
376
|
+
elif action['type'] == 'read':
|
|
377
|
+
self._read_file(action['path'])
|
|
378
|
+
|
|
379
|
+
elif action['type'] == 'list':
|
|
380
|
+
self._list_directory(action['path'])
|
|
381
|
+
|
|
382
|
+
elif action['type'] == 'mkdir':
|
|
383
|
+
self._make_directory(action['path'])
|
|
384
|
+
|
|
385
|
+
elif action['type'] == 'exec':
|
|
386
|
+
self.file_manager.execute_command(action['command'])
|
|
387
|
+
self.stats["commands_executed"].append(action['command'])
|
|
388
|
+
|
|
389
|
+
elif action['type'] == 'copy':
|
|
390
|
+
self._copy_file(action['path'])
|
|
391
|
+
|
|
392
|
+
elif action['type'] == 'move':
|
|
393
|
+
self._move_file(action['path'])
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print_error(f"Failed: {e}")
|
|
397
|
+
|
|
398
|
+
def _delete_file(self, path):
|
|
399
|
+
"""Удаляет файл"""
|
|
400
|
+
full_path = self.project_path / path
|
|
401
|
+
if not self.security.is_safe_path(str(path)):
|
|
402
|
+
print_error(f"Unsafe: {path}")
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
if full_path.exists():
|
|
406
|
+
# Бэкап перед удалением
|
|
407
|
+
backup = full_path.with_suffix(full_path.suffix + '.bak')
|
|
408
|
+
shutil.copy2(full_path, backup)
|
|
409
|
+
full_path.unlink()
|
|
410
|
+
print_success(f"Deleted: {path} (backup: {backup.name})")
|
|
411
|
+
else:
|
|
412
|
+
print_warning(f"Not found: {path}")
|
|
413
|
+
|
|
414
|
+
def _read_file(self, path):
|
|
415
|
+
"""Читает и показывает файл"""
|
|
416
|
+
full_path = self.project_path / path
|
|
417
|
+
if full_path.exists():
|
|
418
|
+
content = full_path.read_text(encoding='utf-8', errors='ignore')
|
|
419
|
+
print_header(f"\n=== {path} ===")
|
|
420
|
+
print(content)
|
|
421
|
+
print_header("=" * (len(path) + 8))
|
|
422
|
+
else:
|
|
423
|
+
print_warning(f"Not found: {path}")
|
|
424
|
+
|
|
425
|
+
def _list_directory(self, path):
|
|
426
|
+
"""Показывает содержимое директории"""
|
|
427
|
+
full_path = self.project_path / path
|
|
428
|
+
if full_path.exists() and full_path.is_dir():
|
|
429
|
+
items = sorted(full_path.iterdir())
|
|
430
|
+
print_header(f"\n=== {path}/ ({len(items)} items) ===")
|
|
431
|
+
for item in items:
|
|
432
|
+
if item.is_dir():
|
|
433
|
+
print(f" [DIR] {item.name}/")
|
|
434
|
+
else:
|
|
435
|
+
size = item.stat().st_size
|
|
436
|
+
print(f" [FILE] {item.name} ({self._format_size(size)})")
|
|
437
|
+
else:
|
|
438
|
+
print_warning(f"Not found: {path}")
|
|
439
|
+
|
|
440
|
+
def _make_directory(self, path):
|
|
441
|
+
"""Создаёт директорию"""
|
|
442
|
+
full_path = self.project_path / path
|
|
443
|
+
full_path.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
print_success(f"Created dir: {path}")
|
|
445
|
+
|
|
446
|
+
def _copy_file(self, source):
|
|
447
|
+
"""Копирует файл (ожидает, что следом будет CREATE)"""
|
|
448
|
+
print_info(f"Copy source noted: {source}")
|
|
449
|
+
|
|
450
|
+
def _move_file(self, source):
|
|
451
|
+
"""Перемещает файл (ожидает, что следом будет CREATE)"""
|
|
452
|
+
full_path = self.project_path / source
|
|
453
|
+
if full_path.exists():
|
|
454
|
+
print_info(f"Move source noted: {source}")
|
|
455
|
+
else:
|
|
456
|
+
print_warning(f"Source not found: {source}")
|
|
457
|
+
|
|
458
|
+
def _handle_command(self, user_input):
|
|
459
|
+
cmd = user_input.lower().split()[0]
|
|
460
|
+
if cmd in ['/help', '/h']:
|
|
461
|
+
print("""
|
|
462
|
+
COMMANDS:
|
|
463
|
+
/help Show help
|
|
464
|
+
/scan Scan project
|
|
465
|
+
/models List models
|
|
466
|
+
/model N Switch model
|
|
467
|
+
/clear Clear history
|
|
468
|
+
/save Save session
|
|
469
|
+
/stats Statistics
|
|
470
|
+
/exit Exit
|
|
471
|
+
""")
|
|
472
|
+
elif cmd in ['/scan', '/s']:
|
|
473
|
+
print(self.file_manager.scan_project())
|
|
474
|
+
elif cmd in ['/models', '/m']:
|
|
475
|
+
for name in MODELS:
|
|
476
|
+
print(f" {name}")
|
|
477
|
+
elif cmd in ['/clear', '/c']:
|
|
478
|
+
self.conversation_history = [self.conversation_history[0]]
|
|
479
|
+
print_success("Cleared")
|
|
480
|
+
elif cmd in ['/save']:
|
|
481
|
+
self.file_manager.save_session(self.conversation_history, self.current_model, self.stats)
|
|
482
|
+
elif cmd in ['/stats']:
|
|
483
|
+
duration = datetime.now() - self.stats["start_time"]
|
|
484
|
+
print(f"Messages: {self.stats['messages_sent']}")
|
|
485
|
+
print(f"Created: {len(self.stats['files_created'])} files")
|
|
486
|
+
print(f"Modified: {len(self.stats['files_modified'])} files")
|
|
487
|
+
print(f"Deleted: {len(self.stats['files_deleted'])} files")
|
|
488
|
+
print(f"Commands: {len(self.stats['commands_executed'])}")
|
|
489
|
+
print(f"Duration: {duration}")
|
|
490
|
+
elif cmd in ['/exit', '/q']:
|
|
491
|
+
print_success("Goodbye!")
|
|
492
|
+
os._exit(0)
|
|
493
|
+
elif cmd in ['/model'] and len(user_input.split()) > 1:
|
|
494
|
+
model_name = user_input.split()[1]
|
|
495
|
+
if model_name in MODELS:
|
|
496
|
+
self.current_model = MODELS[model_name]
|
|
497
|
+
self.model_settings = get_model_settings(self.current_model)
|
|
498
|
+
print_success(f"Switched to: {model_name}")
|
|
499
|
+
else:
|
|
500
|
+
print_error(f"Unknown model: {model_name}")
|
|
501
|
+
return ""
|
|
502
|
+
|
|
503
|
+
def execute_command(self, command):
|
|
504
|
+
self.send_message(command)
|
|
505
|
+
|
|
506
|
+
def run(self):
|
|
507
|
+
print_logo()
|
|
508
|
+
print()
|
|
509
|
+
print_info(f"Project: {self.project_path}")
|
|
510
|
+
print_info(f"Model: {self.current_model}")
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
files = list(self.project_path.rglob("*"))
|
|
514
|
+
files = [f for f in files if f.is_file()
|
|
515
|
+
and not any(ex in f.parts for ex in self.file_manager.excluded_dirs)]
|
|
516
|
+
visible = [f for f in files if not f.name.startswith('.')]
|
|
517
|
+
if visible:
|
|
518
|
+
print_info(f"\nFound {len(visible)} files (AI sees all)")
|
|
519
|
+
except:
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
print_info("/help for commands\n")
|
|
523
|
+
|
|
524
|
+
while True:
|
|
525
|
+
try:
|
|
526
|
+
self.reset_interrupt()
|
|
527
|
+
user_input = input("> ").strip()
|
|
528
|
+
if user_input:
|
|
529
|
+
self.send_message(user_input)
|
|
530
|
+
except KeyboardInterrupt:
|
|
531
|
+
continue
|
|
532
|
+
except EOFError:
|
|
533
|
+
break
|