handycode 2.2.1__tar.gz → 2.3.1__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.2.1 → handycode-2.3.1}/PKG-INFO +1 -1
- {handycode-2.2.1 → handycode-2.3.1}/handycode/__init__.py +1 -1
- {handycode-2.2.1 → handycode-2.3.1}/handycode/assistant.py +185 -75
- handycode-2.3.1/handycode/config.py +163 -0
- handycode-2.3.1/handycode/file_manager.py +228 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/utils.py +53 -28
- {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/PKG-INFO +1 -1
- {handycode-2.2.1 → handycode-2.3.1}/setup.py +1 -1
- handycode-2.2.1/handycode/config.py +0 -78
- handycode-2.2.1/handycode/file_manager.py +0 -238
- {handycode-2.2.1 → handycode-2.3.1}/LICENSE +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/README.md +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/__main__.py +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/cli.py +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/logo.py +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/main.py +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/models.py +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/project_templates.py +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode/security.py +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/SOURCES.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/dependency_links.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/entry_points.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/requires.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/top_level.txt +0 -0
- {handycode-2.2.1 → handycode-2.3.1}/setup.cfg +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Основной класс ассистента HandyCode
|
|
2
|
+
Основной класс ассистента HandyCode
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
@@ -34,43 +34,33 @@ from handycode.security import SecurityChecker
|
|
|
34
34
|
from handycode.utils import (
|
|
35
35
|
Colors, Theme, colorize, print_colored, print_header, print_success,
|
|
36
36
|
print_error, print_warning, print_info, print_logo,
|
|
37
|
-
print_divider, print_file_action, print_status, print_section, print_box
|
|
37
|
+
print_divider, print_file_action, print_status, print_section, print_box,
|
|
38
|
+
Spinner, print_package_status
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def interactive_confirm(commands):
|
|
42
|
-
"""
|
|
43
|
-
Интерактивное меню выбора команд.
|
|
44
|
-
Управление: ↑/↓ для навигации, ПРОБЕЛ для выбора, ENTER для подтверждения.
|
|
45
|
-
Возвращает список выбранных команд.
|
|
46
|
-
"""
|
|
43
|
+
"""Интерактивное меню выбора команд"""
|
|
47
44
|
if not commands:
|
|
48
45
|
return []
|
|
49
46
|
|
|
50
|
-
# Настройка для Windows
|
|
51
47
|
if os.name == 'nt':
|
|
52
48
|
import msvcrt
|
|
53
|
-
|
|
54
49
|
def get_key():
|
|
55
50
|
key = msvcrt.getch()
|
|
56
|
-
if key == b'\xe0':
|
|
51
|
+
if key == b'\xe0':
|
|
57
52
|
key = msvcrt.getch()
|
|
58
53
|
if key == b'H': return 'up'
|
|
59
54
|
if key == b'P': return 'down'
|
|
60
55
|
if key == b'\r': return 'enter'
|
|
61
56
|
if key == b' ': return 'space'
|
|
62
|
-
if key
|
|
63
|
-
if key
|
|
64
|
-
if key
|
|
65
|
-
if key == b'S': return 'S'
|
|
66
|
-
if key == b'c': return 'c'
|
|
67
|
-
if key == b'C': return 'C'
|
|
57
|
+
if key in [b'a', b'A']: return 'a'
|
|
58
|
+
if key in [b's', b'S']: return 's'
|
|
59
|
+
if key in [b'c', b'C']: return 'c'
|
|
68
60
|
if key == b'\x1b': return 'escape'
|
|
69
61
|
return key.decode('utf-8', errors='ignore')
|
|
70
62
|
else:
|
|
71
|
-
import tty
|
|
72
|
-
import termios
|
|
73
|
-
|
|
63
|
+
import tty, termios
|
|
74
64
|
def get_key():
|
|
75
65
|
fd = sys.stdin.fileno()
|
|
76
66
|
old = termios.tcgetattr(fd)
|
|
@@ -88,51 +78,28 @@ def interactive_confirm(commands):
|
|
|
88
78
|
finally:
|
|
89
79
|
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
90
80
|
|
|
91
|
-
selected = [True] * len(commands)
|
|
81
|
+
selected = [True] * len(commands)
|
|
92
82
|
current = 0
|
|
93
83
|
|
|
94
84
|
def render():
|
|
95
|
-
# Очищаем предыдущий вывод
|
|
96
85
|
print(f"\033[{len(commands) + 4}A\033[J", end="")
|
|
97
|
-
|
|
98
86
|
print()
|
|
99
87
|
print(colorize(" ⚡ Команды для выполнения:", Theme.HIGHLIGHT + Colors.BOLD))
|
|
100
88
|
print(colorize(" ─────────────────────────────────────────────────", Theme.MUTED))
|
|
101
|
-
|
|
102
89
|
for i, cmd in enumerate(commands):
|
|
103
|
-
if i == current
|
|
104
|
-
|
|
105
|
-
else
|
|
106
|
-
prefix = " "
|
|
107
|
-
|
|
108
|
-
if selected[i]:
|
|
109
|
-
checkbox = colorize("◉", Theme.SUCCESS)
|
|
110
|
-
cmd_color = Theme.SUCCESS
|
|
111
|
-
else:
|
|
112
|
-
checkbox = colorize("○", Theme.MUTED)
|
|
113
|
-
cmd_color = Theme.MUTED
|
|
114
|
-
|
|
90
|
+
prefix = colorize(" ›", Theme.PRIMARY + Colors.BOLD) if i == current else " "
|
|
91
|
+
checkbox = colorize("◉", Theme.SUCCESS) if selected[i] else colorize("○", Theme.MUTED)
|
|
92
|
+
cmd_color = Theme.SUCCESS if selected[i] else Theme.MUTED
|
|
115
93
|
print(f"{prefix} {checkbox} {colorize(cmd, cmd_color)}")
|
|
116
|
-
|
|
117
|
-
print()
|
|
118
|
-
print(colorize(" Управление:", Theme.MUTED))
|
|
119
|
-
print(colorize(" ↑↓ Навигация ПРОБЕЛ Выбрать A Все S Пропустить ENTER Подтвердить", Theme.MUTED))
|
|
120
|
-
|
|
121
|
-
# Рендерим первый раз
|
|
122
|
-
print()
|
|
123
|
-
print()
|
|
124
|
-
print()
|
|
125
|
-
print()
|
|
126
|
-
print()
|
|
127
|
-
print()
|
|
128
|
-
for _ in range(len(commands) + 4):
|
|
129
94
|
print()
|
|
95
|
+
print(colorize(" ↑↓ Навигация ПРОБЕЛ Выбрать A Все S Пропустить ENTER Подтвердить C Отмена", Theme.MUTED))
|
|
130
96
|
|
|
97
|
+
for _ in range(len(commands) + 5):
|
|
98
|
+
print()
|
|
131
99
|
render()
|
|
132
100
|
|
|
133
101
|
while True:
|
|
134
102
|
key = get_key()
|
|
135
|
-
|
|
136
103
|
if key == 'up':
|
|
137
104
|
current = (current - 1) % len(commands)
|
|
138
105
|
render()
|
|
@@ -160,15 +127,30 @@ class HandyCode:
|
|
|
160
127
|
self.project_path = project_path
|
|
161
128
|
self.auto_approve = auto_approve
|
|
162
129
|
self.config = config or Config()
|
|
130
|
+
|
|
131
|
+
# Этот вызов запросит ключ если его нет
|
|
163
132
|
self.api_key = self.config.get_api_key()
|
|
164
|
-
|
|
165
|
-
|
|
133
|
+
|
|
134
|
+
# Проверяем что ключ не пустой
|
|
135
|
+
if not self.api_key or len(self.api_key) < 10:
|
|
136
|
+
print()
|
|
137
|
+
print(" ❌ API ключ не настроен. HandyCode не может работать без ключа.")
|
|
138
|
+
print(" Получите ключ на https://openrouter.ai/keys")
|
|
139
|
+
print(" и добавьте в ~/.handycode/.env:")
|
|
140
|
+
print(" OPENROUTER_API_KEY=ваш_ключ")
|
|
141
|
+
print()
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
166
144
|
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
|
|
167
145
|
self.current_model = MODELS.get(model, MODELS["deepseek"])
|
|
168
146
|
self.model_settings = get_model_settings(self.current_model)
|
|
169
147
|
self.file_manager = FileManager(self.project_path)
|
|
170
148
|
self.security = SecurityChecker(self.project_path)
|
|
149
|
+
|
|
150
|
+
self.installed_packages = self.file_manager.get_installed_packages()
|
|
151
|
+
|
|
171
152
|
project_context = self._build_project_context()
|
|
153
|
+
|
|
172
154
|
self.conversation_history = [
|
|
173
155
|
{"role": "system", "content": self._get_system_prompt() + project_context}
|
|
174
156
|
]
|
|
@@ -179,12 +161,21 @@ class HandyCode:
|
|
|
179
161
|
}
|
|
180
162
|
self.stream_buffer = ""
|
|
181
163
|
self.pending_commands = []
|
|
164
|
+
self.command_results = []
|
|
182
165
|
self._setup_readline()
|
|
183
166
|
signal.signal(signal.SIGINT, self._signal_handler)
|
|
184
167
|
self._interrupt_count = 0
|
|
185
168
|
|
|
186
169
|
def _build_project_context(self):
|
|
187
170
|
context = f"\n\n=== CURRENT PROJECT ===\nDirectory: {self.project_path}\n"
|
|
171
|
+
context += f"\n=== INSTALLED PACKAGES ===\n"
|
|
172
|
+
if self.installed_packages:
|
|
173
|
+
context += ", ".join(self.installed_packages[:50])
|
|
174
|
+
if len(self.installed_packages) > 50:
|
|
175
|
+
context += f"\n... and {len(self.installed_packages) - 50} more"
|
|
176
|
+
else:
|
|
177
|
+
context += "No packages detected"
|
|
178
|
+
|
|
188
179
|
try:
|
|
189
180
|
all_files = []
|
|
190
181
|
for ext in self.file_manager.allowed_extensions:
|
|
@@ -199,13 +190,26 @@ class HandyCode:
|
|
|
199
190
|
if not any(rel.startswith(ex) for ex in self.file_manager.excluded_dirs):
|
|
200
191
|
files.append(f)
|
|
201
192
|
seen.add(f)
|
|
202
|
-
context += f"\
|
|
193
|
+
context += f"\n\n=== PROJECT FILES ({len(files)}) ===\n"
|
|
203
194
|
for file in files:
|
|
204
195
|
try:
|
|
205
196
|
rel_path = file.relative_to(self.project_path)
|
|
206
197
|
size = file.stat().st_size
|
|
207
198
|
context += f" {rel_path} ({self._format_size(size)})\n"
|
|
208
199
|
except: pass
|
|
200
|
+
|
|
201
|
+
context += f"\n=== FILE CONTENTS ===\n"
|
|
202
|
+
total = 0
|
|
203
|
+
for file in files:
|
|
204
|
+
if total > 50000: break
|
|
205
|
+
try:
|
|
206
|
+
content = file.read_text(encoding='utf-8', errors='ignore')
|
|
207
|
+
if len(content) > 3000:
|
|
208
|
+
content = content[:3000] + "\n... (truncated)"
|
|
209
|
+
rel_path = file.relative_to(self.project_path)
|
|
210
|
+
context += f"\n=== {rel_path} ===\n{content}\n"
|
|
211
|
+
total += len(content)
|
|
212
|
+
except: pass
|
|
209
213
|
except: pass
|
|
210
214
|
return context
|
|
211
215
|
|
|
@@ -216,22 +220,50 @@ class HandyCode:
|
|
|
216
220
|
return f"{size:.1f}TB"
|
|
217
221
|
|
|
218
222
|
def _get_system_prompt(self):
|
|
219
|
-
return """You are HandyCode - AI coding assistant.
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
return """You are HandyCode - AI coding assistant.
|
|
224
|
+
|
|
225
|
+
CAPABILITIES:
|
|
226
|
+
- Create, modify, delete files
|
|
227
|
+
- Run shell commands
|
|
228
|
+
- Install Python packages via pip
|
|
229
|
+
- Analyze code and errors
|
|
230
|
+
- See installed packages and project files
|
|
231
|
+
|
|
232
|
+
PACKAGE MANAGEMENT:
|
|
233
|
+
- Check INSTALLED PACKAGES section before suggesting imports
|
|
234
|
+
- If a package is needed but not installed, use [[INSTALL:package_name]]
|
|
235
|
+
- Example: [[INSTALL:fastapi]] [[INSTALL:uvicorn]]
|
|
236
|
+
|
|
237
|
+
FILE FORMAT:
|
|
238
|
+
[[CREATE:path/file.py]]
|
|
222
239
|
code here
|
|
223
240
|
[[END]]
|
|
224
|
-
|
|
241
|
+
|
|
242
|
+
[[MODIFY:path/file.py]]
|
|
225
243
|
new code here
|
|
226
244
|
[[END]]
|
|
245
|
+
|
|
246
|
+
[[DELETE:path/file.py]]
|
|
247
|
+
[[READ:path/file.py]]
|
|
248
|
+
[[LIST:directory/]]
|
|
227
249
|
[[EXEC:command]]
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
250
|
+
[[INSTALL:package_name]]
|
|
251
|
+
|
|
252
|
+
CRITICAL RULES:
|
|
253
|
+
1. ALWAYS check if required packages are installed before using them
|
|
254
|
+
2. Install missing packages with [[INSTALL:...]]
|
|
255
|
+
3. CREATE files + INSTALL packages + EXEC commands in ONE response
|
|
256
|
+
4. Use [[END]] to close file blocks
|
|
257
|
+
5. NO comments or explanations inside [[CREATE]]...[[END]] - ONLY CODE
|
|
258
|
+
6. Put ALL explanations BEFORE [[CREATE]] blocks
|
|
259
|
+
7. After EXEC, I will show you any errors
|
|
260
|
+
|
|
261
|
+
ERROR HANDLING:
|
|
262
|
+
- After running commands, I'll show you the output
|
|
263
|
+
- If there are errors, I'll show them to you
|
|
264
|
+
- You can then fix the files and re-run
|
|
265
|
+
|
|
266
|
+
Speak Russian. Write code in English. Code ONLY inside [[CREATE]]...[[END]]."""
|
|
235
267
|
|
|
236
268
|
def _setup_readline(self):
|
|
237
269
|
if not HAS_READLINE: return
|
|
@@ -254,6 +286,8 @@ Speak Russian. Write code in English."""
|
|
|
254
286
|
|
|
255
287
|
def _process_stream_chunk(self, chunk):
|
|
256
288
|
self.stream_buffer += chunk
|
|
289
|
+
|
|
290
|
+
# CREATE с анимацией
|
|
257
291
|
while True:
|
|
258
292
|
match = re.search(r'\[\[CREATE:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
259
293
|
if match:
|
|
@@ -262,11 +296,15 @@ Speak Russian. Write code in English."""
|
|
|
262
296
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
263
297
|
content = re.sub(r'\n```$', '', content)
|
|
264
298
|
if content and self.security.is_safe_path(path):
|
|
299
|
+
spinner = Spinner(f"Создание {path}")
|
|
300
|
+
spinner.start()
|
|
265
301
|
self.file_manager.create_file(path, content)
|
|
302
|
+
spinner.stop(f" ✔ {path} ({content.count(chr(10))+1} строк)")
|
|
266
303
|
self.stats["files_created"].append(path)
|
|
267
|
-
print_file_action('create', path, f"({content.count(chr(10))+1} lines)")
|
|
268
304
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
269
305
|
else: break
|
|
306
|
+
|
|
307
|
+
# MODIFY
|
|
270
308
|
while True:
|
|
271
309
|
match = re.search(r'\[\[MODIFY:(.+?)\]\](.*?)\[\[END\]\]', self.stream_buffer, re.DOTALL)
|
|
272
310
|
if match:
|
|
@@ -275,11 +313,25 @@ Speak Russian. Write code in English."""
|
|
|
275
313
|
content = re.sub(r'^```[\w]*\n', '', content)
|
|
276
314
|
content = re.sub(r'\n```$', '', content)
|
|
277
315
|
if content and self.security.is_safe_path(path):
|
|
316
|
+
spinner = Spinner(f"Изменение {path}")
|
|
317
|
+
spinner.start()
|
|
278
318
|
self.file_manager.modify_file(path, content)
|
|
319
|
+
spinner.stop(f" ✎ {path} ({content.count(chr(10))+1} строк)")
|
|
279
320
|
self.stats["files_modified"].append(path)
|
|
280
|
-
print_file_action('modify', path, f"({content.count(chr(10))+1} lines)")
|
|
281
321
|
self.stream_buffer = self.stream_buffer[match.end():]
|
|
282
322
|
else: break
|
|
323
|
+
|
|
324
|
+
# INSTALL
|
|
325
|
+
while True:
|
|
326
|
+
match = re.search(r'\[\[INSTALL:(.+?)\]\]', self.stream_buffer)
|
|
327
|
+
if match:
|
|
328
|
+
package = match.group(1).strip()
|
|
329
|
+
print_status(f"Установка пакета: {package}")
|
|
330
|
+
self.file_manager.install_package(package)
|
|
331
|
+
self.stream_buffer = self.stream_buffer[match.end():]
|
|
332
|
+
else: break
|
|
333
|
+
|
|
334
|
+
# EXEC
|
|
283
335
|
while True:
|
|
284
336
|
match = re.search(r'\[\[EXEC:(.+?)\]\]', self.stream_buffer)
|
|
285
337
|
if match:
|
|
@@ -317,14 +369,29 @@ Speak Russian. Write code in English."""
|
|
|
317
369
|
content = delta.get('content', '')
|
|
318
370
|
if content:
|
|
319
371
|
full_response += content
|
|
372
|
+
|
|
373
|
+
# Вход в кодовый блок - скрываем всё
|
|
320
374
|
if '[[CREATE:' in content or '[[MODIFY:' in content:
|
|
321
375
|
in_code = True
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
# Выход из кодового блока
|
|
322
379
|
if '[[END]]' in content:
|
|
323
380
|
in_code = False
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# Если внутри кода - НИЧЕГО не выводим
|
|
384
|
+
if in_code:
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
# Очищаем маркеры
|
|
388
|
+
clean = content
|
|
389
|
+
for marker in ['[[CREATE:', '[[MODIFY:', '[[END]]', '[[EXEC:', '[[INSTALL:', '[[DELETE:', '[[READ:', '[[LIST:', ']]']:
|
|
390
|
+
clean = clean.replace(marker, '')
|
|
391
|
+
|
|
392
|
+
if clean.strip():
|
|
393
|
+
print(clean, end="", flush=True)
|
|
394
|
+
|
|
328
395
|
self._process_stream_chunk(content)
|
|
329
396
|
except: continue
|
|
330
397
|
print()
|
|
@@ -355,14 +422,29 @@ Speak Russian. Write code in English."""
|
|
|
355
422
|
content = delta.get('content', '')
|
|
356
423
|
if content:
|
|
357
424
|
full_response += content
|
|
425
|
+
|
|
426
|
+
# Вход в кодовый блок - скрываем всё
|
|
358
427
|
if '[[CREATE:' in content or '[[MODIFY:' in content:
|
|
359
428
|
in_code = True
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
# Выход из кодового блока
|
|
360
432
|
if '[[END]]' in content:
|
|
361
433
|
in_code = False
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
# Если внутри кода - НИЧЕГО не выводим
|
|
437
|
+
if in_code:
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
# Очищаем маркеры
|
|
441
|
+
clean = content
|
|
442
|
+
for marker in ['[[CREATE:', '[[MODIFY:', '[[END]]', '[[EXEC:', '[[INSTALL:', '[[DELETE:', '[[READ:', '[[LIST:', ']]']:
|
|
443
|
+
clean = clean.replace(marker, '')
|
|
444
|
+
|
|
445
|
+
if clean.strip():
|
|
446
|
+
print(clean, end="", flush=True)
|
|
447
|
+
|
|
366
448
|
self._process_stream_chunk(content)
|
|
367
449
|
except: continue
|
|
368
450
|
print()
|
|
@@ -409,8 +491,25 @@ Speak Russian. Write code in English."""
|
|
|
409
491
|
for cmd in selected_commands:
|
|
410
492
|
if self.security.is_safe_command(cmd):
|
|
411
493
|
print_status(f"Выполняется: {cmd}")
|
|
412
|
-
self.file_manager.execute_command(cmd)
|
|
494
|
+
success, output = self.file_manager.execute_command(cmd)
|
|
413
495
|
self.stats["commands_executed"].append(cmd)
|
|
496
|
+
self.command_results.append({
|
|
497
|
+
"command": cmd,
|
|
498
|
+
"success": success,
|
|
499
|
+
"output": output
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
errors = [r for r in self.command_results if not r['success']]
|
|
503
|
+
if errors:
|
|
504
|
+
print()
|
|
505
|
+
print_section("❌ Обнаружены ошибки", [])
|
|
506
|
+
for err in errors:
|
|
507
|
+
print(colorize(f" Команда: {err['command']}", Theme.ERROR))
|
|
508
|
+
if err['output']:
|
|
509
|
+
for line in err['output'].strip().split('\n')[:5]:
|
|
510
|
+
print(colorize(f" {line}", Theme.MUTED))
|
|
511
|
+
print()
|
|
512
|
+
print_info("Вы можете попросить меня исправить ошибки")
|
|
414
513
|
else:
|
|
415
514
|
print_warning("Команды пропущены")
|
|
416
515
|
|
|
@@ -426,6 +525,7 @@ Speak Russian. Write code in English."""
|
|
|
426
525
|
print_box([
|
|
427
526
|
"/help Справка",
|
|
428
527
|
"/scan Показать проект",
|
|
528
|
+
"/packages Показать установленные пакеты",
|
|
429
529
|
"/models Модели",
|
|
430
530
|
"/model N Сменить модель",
|
|
431
531
|
"/clear Очистить историю",
|
|
@@ -435,6 +535,11 @@ Speak Russian. Write code in English."""
|
|
|
435
535
|
], Theme.PRIMARY)
|
|
436
536
|
elif cmd in ['/scan', '/s']:
|
|
437
537
|
print(self.file_manager.scan_project())
|
|
538
|
+
elif cmd in ['/packages', '/pkg']:
|
|
539
|
+
packages = self.file_manager.get_installed_packages()
|
|
540
|
+
print_section("📦 Установленные пакеты", packages[:20])
|
|
541
|
+
if len(packages) > 20:
|
|
542
|
+
print(colorize(f" ... и ещё {len(packages) - 20}", Theme.MUTED))
|
|
438
543
|
elif cmd in ['/models', '/m']:
|
|
439
544
|
lines = []
|
|
440
545
|
for name, mid in MODELS.items():
|
|
@@ -449,14 +554,18 @@ Speak Russian. Write code in English."""
|
|
|
449
554
|
print_success(f"Модель изменена на {name}")
|
|
450
555
|
elif cmd in ['/clear', '/c']:
|
|
451
556
|
self.conversation_history = [self.conversation_history[0]]
|
|
557
|
+
self.command_results = []
|
|
452
558
|
print_success("История очищена")
|
|
559
|
+
elif cmd in ['/save']:
|
|
560
|
+
self.file_manager.save_session(self.conversation_history, self.current_model, self.stats)
|
|
453
561
|
elif cmd in ['/stats']:
|
|
454
562
|
print_box([
|
|
455
563
|
f"Сообщений: {self.stats['messages_sent']}",
|
|
456
564
|
f"Создано файлов: {len(self.stats['files_created'])}",
|
|
457
565
|
f"Изменено: {len(self.stats['files_modified'])}",
|
|
458
566
|
f"Удалено: {len(self.stats['files_deleted'])}",
|
|
459
|
-
f"Команд выполнено: {len(self.stats['commands_executed'])}"
|
|
567
|
+
f"Команд выполнено: {len(self.stats['commands_executed'])}",
|
|
568
|
+
f"Пакетов установлено: {len(self.installed_packages)}"
|
|
460
569
|
], Theme.SECONDARY)
|
|
461
570
|
elif cmd in ['/exit', '/q']:
|
|
462
571
|
print_success("До свидания!")
|
|
@@ -471,6 +580,7 @@ Speak Russian. Write code in English."""
|
|
|
471
580
|
print_divider("─", 60, Theme.MUTED)
|
|
472
581
|
print(colorize(f" 📁 Проект: {self.project_path}", Theme.TEXT))
|
|
473
582
|
print(colorize(f" 🤖 Модель: {self.current_model}", Theme.TEXT))
|
|
583
|
+
print(colorize(f" 📦 Пакетов: {len(self.installed_packages)}", Theme.TEXT))
|
|
474
584
|
print(colorize(f" {Theme.MUTED}/help для команд{Colors.RESET}", Theme.MUTED))
|
|
475
585
|
print_divider("─", 60, Theme.MUTED)
|
|
476
586
|
print()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Управление конфигурацией HandyCode
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Config:
|
|
11
|
+
"""Управляет конфигурацией HandyCode"""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.config_dir = Path.home() / '.handycode'
|
|
15
|
+
self.config_dir.mkdir(exist_ok=True)
|
|
16
|
+
|
|
17
|
+
self.env_file = self.config_dir / '.env'
|
|
18
|
+
self.config_file = self.config_dir / 'config.json'
|
|
19
|
+
|
|
20
|
+
self.config = self._load_config()
|
|
21
|
+
|
|
22
|
+
def _load_config(self) -> dict:
|
|
23
|
+
default_config = {
|
|
24
|
+
"default_model": "deepseek",
|
|
25
|
+
"auto_approve": False,
|
|
26
|
+
"language": "ru",
|
|
27
|
+
"installed_version": "2.3.0",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if self.config_file.exists():
|
|
31
|
+
try:
|
|
32
|
+
with open(self.config_file, encoding='utf-8') as f:
|
|
33
|
+
loaded = json.load(f)
|
|
34
|
+
default_config.update(loaded)
|
|
35
|
+
except:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
return default_config
|
|
39
|
+
|
|
40
|
+
def save_config(self):
|
|
41
|
+
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
42
|
+
json.dump(self.config, f, indent=2, ensure_ascii=False)
|
|
43
|
+
|
|
44
|
+
def get_api_key(self) -> str:
|
|
45
|
+
"""Получает API ключ из разных источников, запрашивает если нет"""
|
|
46
|
+
api_key = None
|
|
47
|
+
|
|
48
|
+
# 1. Переменная окружения
|
|
49
|
+
api_key = os.getenv('OPENROUTER_API_KEY')
|
|
50
|
+
if api_key:
|
|
51
|
+
return api_key
|
|
52
|
+
|
|
53
|
+
# 2. .env файл
|
|
54
|
+
if self.env_file.exists():
|
|
55
|
+
try:
|
|
56
|
+
with open(self.env_file, encoding='utf-8') as f:
|
|
57
|
+
for line in f:
|
|
58
|
+
line = line.strip()
|
|
59
|
+
if line.startswith('OPENROUTER_API_KEY='):
|
|
60
|
+
key = line.split('=', 1)[1].strip().strip('"').strip("'")
|
|
61
|
+
if key and not key.startswith('#'):
|
|
62
|
+
api_key = key
|
|
63
|
+
break
|
|
64
|
+
except:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if api_key:
|
|
68
|
+
return api_key
|
|
69
|
+
|
|
70
|
+
# 3. Файл конфигурации
|
|
71
|
+
if 'api_key' in self.config and self.config['api_key']:
|
|
72
|
+
return self.config['api_key']
|
|
73
|
+
|
|
74
|
+
# 4. Запрашиваем у пользователя
|
|
75
|
+
api_key = self._request_api_key()
|
|
76
|
+
|
|
77
|
+
return api_key
|
|
78
|
+
|
|
79
|
+
def _request_api_key(self) -> str:
|
|
80
|
+
"""Запрашивает API ключ у пользователя с красивым оформлением"""
|
|
81
|
+
print()
|
|
82
|
+
print("╔══════════════════════════════════════════════════════════════╗")
|
|
83
|
+
print("║ ║")
|
|
84
|
+
print("║ 🔑 API КЛЮЧ НЕ НАЙДЕН ║")
|
|
85
|
+
print("║ ║")
|
|
86
|
+
print("╚══════════════════════════════════════════════════════════════╝")
|
|
87
|
+
print()
|
|
88
|
+
print(" Для работы HandyCode требуется API ключ OpenRouter.")
|
|
89
|
+
print(" Получите его бесплатно на сайте:")
|
|
90
|
+
print()
|
|
91
|
+
print(" https://openrouter.ai/keys")
|
|
92
|
+
print()
|
|
93
|
+
print(" Инструкция:")
|
|
94
|
+
print(" 1. Зарегистрируйтесь на openrouter.ai")
|
|
95
|
+
print(" 2. Перейдите в раздел Keys")
|
|
96
|
+
print(" 3. Создайте новый ключ")
|
|
97
|
+
print(" 4. Скопируйте ключ и вставьте его ниже")
|
|
98
|
+
print()
|
|
99
|
+
|
|
100
|
+
while True:
|
|
101
|
+
api_key = input(" API ключ: ").strip()
|
|
102
|
+
|
|
103
|
+
if not api_key:
|
|
104
|
+
print()
|
|
105
|
+
print(" ⚠ Ключ не может быть пустым. Попробуйте снова.")
|
|
106
|
+
print()
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if len(api_key) < 20:
|
|
110
|
+
print()
|
|
111
|
+
print(" ⚠ Ключ слишком короткий. Проверьте ключ.")
|
|
112
|
+
print()
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
# Сохраняем ключ
|
|
118
|
+
try:
|
|
119
|
+
# В .env файл
|
|
120
|
+
env_content = ""
|
|
121
|
+
if self.env_file.exists():
|
|
122
|
+
with open(self.env_file, encoding='utf-8') as f:
|
|
123
|
+
env_content = f.read()
|
|
124
|
+
|
|
125
|
+
if 'OPENROUTER_API_KEY=' in env_content:
|
|
126
|
+
lines = env_content.split('\n')
|
|
127
|
+
new_lines = []
|
|
128
|
+
for line in lines:
|
|
129
|
+
if line.startswith('OPENROUTER_API_KEY='):
|
|
130
|
+
new_lines.append(f'OPENROUTER_API_KEY={api_key}')
|
|
131
|
+
else:
|
|
132
|
+
new_lines.append(line)
|
|
133
|
+
env_content = '\n'.join(new_lines)
|
|
134
|
+
else:
|
|
135
|
+
if env_content and not env_content.endswith('\n'):
|
|
136
|
+
env_content += '\n'
|
|
137
|
+
env_content += f'OPENROUTER_API_KEY={api_key}\n'
|
|
138
|
+
|
|
139
|
+
with open(self.env_file, 'w', encoding='utf-8') as f:
|
|
140
|
+
f.write(env_content)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
os.chmod(self.env_file, 0o600)
|
|
144
|
+
except:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
print()
|
|
148
|
+
print(" ✅ Ключ сохранён в ~/.handycode/.env")
|
|
149
|
+
print()
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f" ⚠ Не удалось сохранить ключ: {e}")
|
|
153
|
+
print(f" Добавьте вручную в {self.env_file}:")
|
|
154
|
+
print(f" OPENROUTER_API_KEY={api_key}")
|
|
155
|
+
|
|
156
|
+
return api_key
|
|
157
|
+
|
|
158
|
+
def get(self, key: str, default=None):
|
|
159
|
+
return self.config.get(key, default)
|
|
160
|
+
|
|
161
|
+
def set(self, key: str, value):
|
|
162
|
+
self.config[key] = value
|
|
163
|
+
self.save_config()
|