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.
@@ -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.
@@ -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,5 @@
1
+ """Точка входа для python -m handycode"""
2
+ from handycode.main import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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