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.
Files changed (25) hide show
  1. {handycode-2.2.1 → handycode-2.3.1}/PKG-INFO +1 -1
  2. {handycode-2.2.1 → handycode-2.3.1}/handycode/__init__.py +1 -1
  3. {handycode-2.2.1 → handycode-2.3.1}/handycode/assistant.py +185 -75
  4. handycode-2.3.1/handycode/config.py +163 -0
  5. handycode-2.3.1/handycode/file_manager.py +228 -0
  6. {handycode-2.2.1 → handycode-2.3.1}/handycode/utils.py +53 -28
  7. {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/PKG-INFO +1 -1
  8. {handycode-2.2.1 → handycode-2.3.1}/setup.py +1 -1
  9. handycode-2.2.1/handycode/config.py +0 -78
  10. handycode-2.2.1/handycode/file_manager.py +0 -238
  11. {handycode-2.2.1 → handycode-2.3.1}/LICENSE +0 -0
  12. {handycode-2.2.1 → handycode-2.3.1}/README.md +0 -0
  13. {handycode-2.2.1 → handycode-2.3.1}/handycode/__main__.py +0 -0
  14. {handycode-2.2.1 → handycode-2.3.1}/handycode/cli.py +0 -0
  15. {handycode-2.2.1 → handycode-2.3.1}/handycode/logo.py +0 -0
  16. {handycode-2.2.1 → handycode-2.3.1}/handycode/main.py +0 -0
  17. {handycode-2.2.1 → handycode-2.3.1}/handycode/models.py +0 -0
  18. {handycode-2.2.1 → handycode-2.3.1}/handycode/project_templates.py +0 -0
  19. {handycode-2.2.1 → handycode-2.3.1}/handycode/security.py +0 -0
  20. {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/SOURCES.txt +0 -0
  21. {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/dependency_links.txt +0 -0
  22. {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/entry_points.txt +0 -0
  23. {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/requires.txt +0 -0
  24. {handycode-2.2.1 → handycode-2.3.1}/handycode.egg-info/top_level.txt +0 -0
  25. {handycode-2.2.1 → handycode-2.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: handycode
3
- Version: 2.2.1
3
+ Version: 2.3.1
4
4
  Summary: AI Code Assistant for DeepSeek
5
5
  Home-page: https://github.com/AuraTechno/HandyCode
6
6
  Author: AuraTechno
@@ -3,7 +3,7 @@ HandyCode - AI Ассистент для разработки
3
3
  Аналог Claude Code для командной строки
4
4
  """
5
5
 
6
- __version__ = "2.2.1"
6
+ __version__ = "2.3.1"
7
7
  __author__ = "AURA Tec."
8
8
  __license__ = "MIT"
9
9
 
@@ -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 == b'a': return 'a'
63
- if key == b'A': return 'A'
64
- if key == b's': return 's'
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
- prefix = colorize("", Theme.PRIMARY + Colors.BOLD)
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
- if not self.api_key:
165
- raise ValueError("API key not found")
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"\nFiles ({len(files)}):\n"
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. Create/modify/delete files and run commands.
220
- FORMAT:
221
- [[CREATE:path/file]]
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
- [[MODIFY:path/file]]
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
- RULES:
229
- 1. CREATE + EXEC in ONE response
230
- 2. Use [[END]] after file content
231
- 3. NO comments inside [[CREATE]]...[[END]]
232
- 4. Explanations BEFORE [[CREATE]] blocks
233
- 5. Files create automatically, commands need confirmation
234
- Speak Russian. Write code in English."""
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
- if not in_code:
325
- clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
326
- if clean.strip():
327
- print(clean, end="", flush=True)
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
- if not in_code:
363
- clean = content.replace('[[CREATE:', '').replace('[[MODIFY:', '').replace('[[END]]', '').replace('[[EXEC:', '').replace(']]', '')
364
- if clean.strip():
365
- print(clean, end="", flush=True)
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()