astraagent 2.25.6

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 (63) hide show
  1. package/.env.template +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +333 -0
  4. package/astra/__init__.py +15 -0
  5. package/astra/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/astra/__pycache__/chat.cpython-314.pyc +0 -0
  7. package/astra/__pycache__/cli.cpython-314.pyc +0 -0
  8. package/astra/__pycache__/prompts.cpython-314.pyc +0 -0
  9. package/astra/__pycache__/updater.cpython-314.pyc +0 -0
  10. package/astra/chat.py +763 -0
  11. package/astra/cli.py +913 -0
  12. package/astra/core/__init__.py +8 -0
  13. package/astra/core/__pycache__/__init__.cpython-314.pyc +0 -0
  14. package/astra/core/__pycache__/agent.cpython-314.pyc +0 -0
  15. package/astra/core/__pycache__/config.cpython-314.pyc +0 -0
  16. package/astra/core/__pycache__/memory.cpython-314.pyc +0 -0
  17. package/astra/core/__pycache__/reasoning.cpython-314.pyc +0 -0
  18. package/astra/core/__pycache__/state.cpython-314.pyc +0 -0
  19. package/astra/core/agent.py +515 -0
  20. package/astra/core/config.py +247 -0
  21. package/astra/core/memory.py +782 -0
  22. package/astra/core/reasoning.py +423 -0
  23. package/astra/core/state.py +366 -0
  24. package/astra/core/voice.py +144 -0
  25. package/astra/llm/__init__.py +32 -0
  26. package/astra/llm/__pycache__/__init__.cpython-314.pyc +0 -0
  27. package/astra/llm/__pycache__/providers.cpython-314.pyc +0 -0
  28. package/astra/llm/providers.py +530 -0
  29. package/astra/planning/__init__.py +117 -0
  30. package/astra/prompts.py +289 -0
  31. package/astra/reflection/__init__.py +181 -0
  32. package/astra/search.py +469 -0
  33. package/astra/tasks.py +466 -0
  34. package/astra/tools/__init__.py +17 -0
  35. package/astra/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  36. package/astra/tools/__pycache__/advanced.cpython-314.pyc +0 -0
  37. package/astra/tools/__pycache__/base.cpython-314.pyc +0 -0
  38. package/astra/tools/__pycache__/browser.cpython-314.pyc +0 -0
  39. package/astra/tools/__pycache__/file.cpython-314.pyc +0 -0
  40. package/astra/tools/__pycache__/git.cpython-314.pyc +0 -0
  41. package/astra/tools/__pycache__/memory_tool.cpython-314.pyc +0 -0
  42. package/astra/tools/__pycache__/python.cpython-314.pyc +0 -0
  43. package/astra/tools/__pycache__/shell.cpython-314.pyc +0 -0
  44. package/astra/tools/__pycache__/web.cpython-314.pyc +0 -0
  45. package/astra/tools/__pycache__/windows.cpython-314.pyc +0 -0
  46. package/astra/tools/advanced.py +251 -0
  47. package/astra/tools/base.py +344 -0
  48. package/astra/tools/browser.py +93 -0
  49. package/astra/tools/file.py +476 -0
  50. package/astra/tools/git.py +74 -0
  51. package/astra/tools/memory_tool.py +89 -0
  52. package/astra/tools/python.py +238 -0
  53. package/astra/tools/shell.py +183 -0
  54. package/astra/tools/web.py +804 -0
  55. package/astra/tools/windows.py +542 -0
  56. package/astra/updater.py +450 -0
  57. package/astra/utils/__init__.py +230 -0
  58. package/bin/astraagent.js +73 -0
  59. package/bin/postinstall.js +25 -0
  60. package/config.json.template +52 -0
  61. package/main.py +16 -0
  62. package/package.json +51 -0
  63. package/pyproject.toml +72 -0
package/astra/chat.py ADDED
@@ -0,0 +1,763 @@
1
+ """Chat Mode - Enhanced with keyboard shortcuts, file access, and settings."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from datetime import datetime
8
+ from typing import List, Optional, Dict, Any
9
+
10
+ from astra.llm.providers import (
11
+ Message, PROVIDERS, create_provider, LLMProvider
12
+ )
13
+ from astra.search import DeviceSearcher, format_search_results
14
+ from astra.tasks import TaskExecutor, format_task_result
15
+ from astra.updater import UpdateManager, format_update_info
16
+
17
+
18
+ class ChatHistory:
19
+ """Manages chat history and logging."""
20
+
21
+ def __init__(self, logs_dir: str = "./logs/chats"):
22
+ self.logs_dir = Path(logs_dir)
23
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
24
+ self.messages: List[dict] = []
25
+ self.provider_name: str = ""
26
+ self.model_name: str = ""
27
+ self.session_start = datetime.now()
28
+
29
+ def add_message(self, role: str, content: str):
30
+ """Add message to history."""
31
+ self.messages.append({
32
+ "timestamp": datetime.now().isoformat(),
33
+ "role": role,
34
+ "content": content
35
+ })
36
+
37
+ def save(self):
38
+ """Save chat history to file."""
39
+ timestamp = self.session_start.strftime("%Y%m%d_%H%M%S")
40
+ filename = f"chat_{self.provider_name}_{self.model_name}_{timestamp}.json"
41
+ filepath = self.logs_dir / filename
42
+
43
+ data = {
44
+ "provider": self.provider_name,
45
+ "model": self.model_name,
46
+ "session_start": self.session_start.isoformat(),
47
+ "session_end": datetime.now().isoformat(),
48
+ "total_messages": len(self.messages),
49
+ "messages": self.messages
50
+ }
51
+
52
+ with open(filepath, "w", encoding="utf-8") as f:
53
+ json.dump(data, f, indent=2, ensure_ascii=False)
54
+
55
+ return filepath
56
+
57
+
58
+ class APIKeyManager:
59
+ """Manage API keys across providers."""
60
+
61
+ ENV_FILE = Path(".env")
62
+
63
+ @staticmethod
64
+ def get_all_keys() -> Dict[str, Optional[str]]:
65
+ """Get all API keys from environment."""
66
+ keys = {}
67
+ for provider_key, provider_info in PROVIDERS.items():
68
+ env_keys = provider_info.get("env_keys", [])
69
+ if env_keys:
70
+ key = os.getenv(env_keys[0])
71
+ if key:
72
+ # Mask most of the key
73
+ masked = key[:4] + "*" * (len(key) - 8) + key[-4:]
74
+ keys[provider_key] = masked
75
+ else:
76
+ keys[provider_key] = "NOT SET"
77
+ return keys
78
+
79
+ @staticmethod
80
+ def set_key(provider_key: str, api_key: str):
81
+ """Set API key for provider."""
82
+ provider_info = PROVIDERS.get(provider_key)
83
+ if not provider_info:
84
+ print(f"❌ Unknown provider: {provider_key}")
85
+ return False
86
+
87
+ env_key = provider_info["env_keys"][0]
88
+
89
+ # Update .env file
90
+ content = APIKeyManager.ENV_FILE.read_text() if APIKeyManager.ENV_FILE.exists() else ""
91
+
92
+ if f"{env_key}=" in content:
93
+ # Update existing
94
+ lines = content.split("\n")
95
+ content = "\n".join(f"{env_key}={api_key}" if line.startswith(f"{env_key}=") else line for line in lines)
96
+ else:
97
+ # Add new
98
+ if content and not content.endswith("\n"):
99
+ content += "\n"
100
+ content += f"{env_key}={api_key}\n"
101
+
102
+ APIKeyManager.ENV_FILE.write_text(content)
103
+ os.environ[env_key] = api_key
104
+ print(f"✅ API key saved for {provider_key}")
105
+ return True
106
+
107
+ @staticmethod
108
+ def view_keys():
109
+ """View all API keys (masked)."""
110
+ keys = APIKeyManager.get_all_keys()
111
+ print("\n🔑 API Keys Status:\n")
112
+ for provider, status in keys.items():
113
+ check = "✅" if status != "NOT SET" else "❌"
114
+ print(f" {check} {provider:15} {status}")
115
+
116
+
117
+ def show_help():
118
+ """Show help and keyboard shortcuts."""
119
+ print("\n" + "="*70)
120
+ print("⌨️ KEYBOARD SHORTCUTS & COMMANDS")
121
+ print("="*70)
122
+ print("""
123
+ CHAT COMMANDS:
124
+ /help or ? Show this help
125
+ /exit or /quit or /q Close chat and save
126
+ /close Close chat (without saving)
127
+ /clear Clear conversation history
128
+ /history or /h Show chat history
129
+ /settings or /s Open settings menu
130
+ /file or /f Read and send file contents
131
+ /files or /ls List files in current directory
132
+ /docs List documentation files
133
+ /images or /img List image files
134
+ /view <filename> View file contents
135
+ /keys or /k View API keys status
136
+ /models or /m Show available models for current provider
137
+ /provider or /p Show current provider info
138
+ /tokens or /t Show token estimate
139
+
140
+ SEARCH COMMANDS (Device Search):
141
+ /search <name> Search for file/folder by name
142
+ /find <name> Alias for search
143
+ /search-ext <ext> Search files by extension
144
+ /search-type <type> Search by file type (code, image, doc, etc)
145
+ /search-recent Find recently modified files
146
+ /search-size <kb> Find files by size
147
+ /search-content <text> Search file contents
148
+ /browse <path> Browse directory
149
+
150
+ TASK COMMANDS (File Operations):
151
+ /open <path> Open file with default app
152
+ /folder <path> Open folder in file explorer
153
+ /copy <src> <dest> Copy file
154
+ /move <src> <dest> Move file
155
+ /delete <path> Delete file
156
+ /rename <path> <name> Rename file
157
+ /mkdir <path> Create folder
158
+ /info <path> Get file/folder info
159
+ /ls <path> List directory contents
160
+
161
+ UPDATE COMMANDS:
162
+ /updates Check for available updates
163
+ /update-check Force update check
164
+ /update npm Update via npm
165
+ /update pip Update via pip
166
+ /update github Update via github
167
+ /version Show current version
168
+ /rollback <version> Rollback to previous version
169
+
170
+ SETTINGS MENU (/settings):
171
+ - Change provider
172
+ - Change model
173
+ - Update API key
174
+ - Adjust temperature
175
+ - Set max tokens
176
+ - View current settings
177
+
178
+ FILE ACCESS (/file):
179
+ - Load .txt, .md, .py, .json, .html files
180
+ - Files automatically included in AI context
181
+ - Max 100KB per file
182
+
183
+ IMAGE SUPPORT (Paste):
184
+ - Paste images from clipboard (Ctrl+V)
185
+ - Send image URLs directly
186
+ - Supported: PNG, JPG, WebP, GIF
187
+ - Provider must support vision
188
+
189
+ TIPS:
190
+ • Type normally to send messages
191
+ • Use /help anytime for this reference
192
+ • Search works across entire device
193
+ • Tasks handle file operations safely
194
+ • Updates check npm and PyPI simultaneously
195
+ """)
196
+ print("="*70 + "\n")
197
+ • Use Ctrl+C to interrupt response
198
+ • Use Tab for auto-completion (if available)
199
+ • Multi-line input: Use Shift+Enter
200
+ • Type /help anytime for quick reference
201
+ """)
202
+ print("="*70 + "\n")
203
+
204
+
205
+ def read_file_content(filepath: str, max_size: int = 100000) -> Optional[str]:
206
+ """Safely read file content."""
207
+ try:
208
+ path = Path(filepath)
209
+ if not path.exists():
210
+ print(f"❌ File not found: {filepath}")
211
+ return None
212
+
213
+ if path.stat().st_size > max_size:
214
+ print(f"❌ File too large ({path.stat().st_size} bytes, max {max_size})")
215
+ return None
216
+
217
+ # Allow safe file types
218
+ safe_extensions = {'.txt', '.md', '.py', '.js', '.json', '.html', '.xml', '.csv', '.log'}
219
+ if path.suffix not in safe_extensions:
220
+ print(f"❌ File type not supported. Safe types: {', '.join(safe_extensions)}")
221
+ return None
222
+
223
+ with open(path, 'r', encoding='utf-8', errors='ignore') as f:
224
+ content = f.read()
225
+
226
+ return content
227
+ except Exception as e:
228
+ print(f"❌ Error reading file: {e}")
229
+ return None
230
+
231
+
232
+ def list_files(pattern: str = "*", directory: str = ".") -> List[str]:
233
+ """List files matching pattern."""
234
+ try:
235
+ path = Path(directory)
236
+ files = sorted([f.name for f in path.glob(pattern) if f.is_file()])
237
+ return files[:20] # Max 20 files
238
+ except Exception:
239
+ return []
240
+
241
+
242
+ def list_docs() -> List[str]:
243
+ """List documentation files."""
244
+ docs = list_files("*.md", ".")
245
+ return docs
246
+
247
+
248
+ def list_images() -> List[str]:
249
+ """List image files."""
250
+ img_extensions = ['*.png', '*.jpg', '*.jpeg', '*.webp', '*.gif', '*.bmp']
251
+ images = []
252
+ for ext in img_extensions:
253
+ images.extend(list_files(ext, "."))
254
+ return sorted(list(set(images)))
255
+
256
+
257
+ def select_provider() -> tuple[str, str]:
258
+ """Interactive provider selection."""
259
+ print("\n📦 Available LLM Providers:\n")
260
+
261
+ providers_list = list(PROVIDERS.keys())
262
+ for i, provider_key in enumerate(providers_list, 1):
263
+ info = PROVIDERS[provider_key]
264
+ free_tag = "💰 Free" if info.get("free") else "💳 Paid"
265
+ print(f" {i}. {info['name']} [{free_tag}]")
266
+
267
+ print(f" 0. View API key status")
268
+
269
+ while True:
270
+ try:
271
+ choice = input(f"\nSelect provider (0-{len(providers_list)}): ").strip()
272
+ idx = int(choice)
273
+
274
+ if idx == 0:
275
+ APIKeyManager.view_keys()
276
+ continue
277
+
278
+ if 1 <= idx <= len(providers_list):
279
+ selected = providers_list[idx - 1]
280
+ return selected, PROVIDERS[selected]["name"]
281
+
282
+ print("❌ Invalid choice")
283
+ except (ValueError, IndexError):
284
+ print("❌ Invalid input")
285
+
286
+
287
+ def select_model(provider_key: str) -> str:
288
+ """Interactive model selection."""
289
+ provider_info = PROVIDERS[provider_key]
290
+ models = provider_info["models"]
291
+
292
+ print(f"\n🤖 Available models for {provider_info['name']}:\n")
293
+
294
+ for i, model in enumerate(models, 1):
295
+ print(f" {i}. {model}")
296
+
297
+ print(f" {len(models) + 1}. Custom model (enter name)")
298
+
299
+ while True:
300
+ try:
301
+ choice = input(f"\nSelect model (1-{len(models) + 1}): ").strip()
302
+ idx = int(choice) - 1
303
+
304
+ if 0 <= idx < len(models):
305
+ return models[idx]
306
+ elif idx == len(models):
307
+ custom = input("Enter custom model name: ").strip()
308
+ if custom:
309
+ return custom
310
+ print("❌ Model name cannot be empty")
311
+ else:
312
+ print("❌ Invalid choice")
313
+ except ValueError:
314
+ print("❌ Invalid input")
315
+
316
+
317
+ def get_api_key(provider_key: str) -> str:
318
+ """Get API key for provider."""
319
+ provider_info = PROVIDERS[provider_key]
320
+ env_keys = provider_info["env_keys"]
321
+
322
+ print(f"\n🔑 API Key for {provider_info['name']}")
323
+
324
+ # Check if already set
325
+ for env_key in env_keys:
326
+ existing = os.getenv(env_key)
327
+ if existing:
328
+ use_existing = input(f"Found {env_key} in environment. Use it? (y/n): ").lower()
329
+ if use_existing == 'y':
330
+ return existing
331
+
332
+ # Get from user
333
+ print(f"Enter your API key (or paste it):")
334
+ api_key = input("> ").strip()
335
+
336
+ if not api_key:
337
+ raise ValueError("API key cannot be empty")
338
+
339
+ # Offer to save
340
+ save_to_env = input("\nSave to environment variable? (y/n): ").lower()
341
+ if save_to_env == 'y':
342
+ APIKeyManager.set_key(provider_key, api_key)
343
+
344
+ return api_key
345
+
346
+
347
+ async def settings_menu(provider_key: str, model_name: str, provider_info: dict) -> tuple[str, str]:
348
+ """Interactive settings menu."""
349
+ while True:
350
+ print("\n" + "="*50)
351
+ print("⚙️ SETTINGS MENU")
352
+ print("="*50)
353
+ print(f"\nCurrent Settings:")
354
+ print(f" Provider: {provider_key}")
355
+ print(f" Model: {model_name}")
356
+ print(f" Temperature: 0.7 (default)")
357
+ print(f" Max Tokens: 4096 (default)")
358
+ print(f"\nOptions:")
359
+ print(f" 1. Change provider")
360
+ print(f" 2. Change model")
361
+ print(f" 3. Update/set API key")
362
+ print(f" 4. View all API keys")
363
+ print(f" 5. Back to chat")
364
+
365
+ choice = input(f"\nSelect option (1-5): ").strip()
366
+
367
+ if choice == '1':
368
+ provider_key, _ = select_provider()
369
+ print(f"✅ Provider changed to: {provider_key}")
370
+ model_name = select_model(provider_key)
371
+ print(f"✅ Model changed to: {model_name}")
372
+
373
+ elif choice == '2':
374
+ model_name = select_model(provider_key)
375
+ print(f"✅ Model changed to: {model_name}")
376
+
377
+ elif choice == '3':
378
+ new_key = input("Enter new API key: ").strip()
379
+ if new_key:
380
+ APIKeyManager.set_key(provider_key, new_key)
381
+
382
+ elif choice == '4':
383
+ APIKeyManager.view_keys()
384
+ input("\nPress Enter to continue...")
385
+
386
+ elif choice == '5':
387
+ return provider_key, model_name
388
+
389
+ else:
390
+ print("❌ Invalid option")
391
+
392
+
393
+ async def chat_with_provider(provider: LLMProvider, history: ChatHistory, provider_key: str):
394
+ """Interactive chat loop with enhanced features."""
395
+ print(f"\n💬 Chat Mode")
396
+ print(f"Provider: {history.provider_name} | Model: {history.model_name}")
397
+ print(f"Type '/help' for commands or just start typing\n")
398
+
399
+ conversation = []
400
+ should_close = False
401
+
402
+ while True:
403
+ try:
404
+ user_input = input("You: ").strip()
405
+ except (EOFError, KeyboardInterrupt):
406
+ print("\n[*] Chat interrupted")
407
+ break
408
+
409
+ if not user_input:
410
+ continue
411
+
412
+ # Handle shortcuts
413
+ lower_input = user_input.lower()
414
+
415
+ if lower_input in ['/exit', '/quit', '/q', 'exit', 'quit']:
416
+ should_close = True
417
+ break
418
+
419
+ if lower_input == '/close':
420
+ should_close = False
421
+ break
422
+
423
+ if lower_input in ['/help', '?', '/h']:
424
+ show_help()
425
+ continue
426
+
427
+ if lower_input == '/clear':
428
+ conversation.clear()
429
+ history.messages.clear()
430
+ print("✅ Conversation cleared\n")
431
+ continue
432
+
433
+ if lower_input in ['/history', '/h']:
434
+ if not conversation:
435
+ print("No messages yet.\n")
436
+ else:
437
+ print("\n📜 Chat History:\n")
438
+ for i, msg in enumerate(conversation, 1):
439
+ role = msg["role"].upper()
440
+ content = msg["content"][:80]
441
+ print(f" {i}. [{role}] {content}...")
442
+ print()
443
+ continue
444
+
445
+ if lower_input in ['/keys', '/k']:
446
+ APIKeyManager.view_keys()
447
+ print()
448
+ continue
449
+
450
+ if lower_input in ['/models', '/m']:
451
+ provider_info = PROVIDERS[provider_key]
452
+ print(f"\n🤖 Available models for {provider_info['name']}:")
453
+ for model in provider_info["models"]:
454
+ print(f" • {model}")
455
+ print()
456
+ continue
457
+
458
+ if lower_input in ['/provider', '/p']:
459
+ provider_info = PROVIDERS[provider_key]
460
+ print(f"\n📦 Current Provider: {provider_info['name']}")
461
+ print(f" Key: {provider_key}")
462
+ print(f" Models: {len(provider_info['models'])} available")
463
+ print()
464
+ continue
465
+
466
+ if lower_input in ['/files', '/ls']:
467
+ files = list_files()
468
+ print(f"\n📁 Files in current directory:\n")
469
+ for f in files:
470
+ print(f" • {f}")
471
+ print()
472
+ continue
473
+
474
+ if lower_input == '/docs':
475
+ docs = list_docs()
476
+ print(f"\n📖 Documentation files:\n")
477
+ for doc in docs:
478
+ print(f" • {doc}")
479
+ print()
480
+ continue
481
+
482
+ if lower_input in ['/images', '/img']:
483
+ images = list_images()
484
+ if not images:
485
+ print("❌ No image files found\n")
486
+ else:
487
+ print(f"\n🖼️ Image files:\n")
488
+ for img in images:
489
+ print(f" • {img}")
490
+ print()
491
+ continue
492
+
493
+ if lower_input.startswith('/view '):
494
+ filename = user_input[6:].strip()
495
+ content = read_file_content(filename)
496
+ if content:
497
+ print(f"\n📄 {filename}:\n{content}\n")
498
+ continue
499
+
500
+ if lower_input.startswith('/file ') or lower_input.startswith('/f '):
501
+ filename = user_input.split(' ', 1)[1].strip() if ' ' in user_input else ""
502
+ if not filename:
503
+ print("Usage: /file <filename>\n")
504
+ continue
505
+
506
+ content = read_file_content(filename)
507
+ if content:
508
+ user_input = f"Please review this file and provide insights:\n\n```\n{content}\n```"
509
+ print(f"✅ File loaded ({len(content)} bytes)\n")
510
+ else:
511
+ continue
512
+
513
+ if lower_input in ['/settings', '/s']:
514
+ # Need to update provider and model through settings
515
+ print("Settings menu - returning to main chat\n")
516
+ continue
517
+
518
+ # SEARCH COMMANDS
519
+ if lower_input.startswith('/search ') or lower_input.startswith('/find '):
520
+ search_term = user_input.split(' ', 1)[1].strip()
521
+ searcher = DeviceSearcher(max_results=20)
522
+ results = searcher.search_by_name(search_term)
523
+ print(format_search_results(results, f"Search for '{search_term}'"))
524
+ continue
525
+
526
+ if lower_input.startswith('/search-ext '):
527
+ ext = user_input.split(' ', 1)[1].strip()
528
+ searcher = DeviceSearcher(max_results=30)
529
+ results = searcher.search_by_extension(ext, limit=30)
530
+ print(format_search_results(results, f"Files with extension '.{ext}'"))
531
+ continue
532
+
533
+ if lower_input.startswith('/search-type '):
534
+ file_type = user_input.split(' ', 1)[1].strip().lower()
535
+ searcher = DeviceSearcher(max_results=30)
536
+ results = searcher.search_by_category(file_type, limit=30)
537
+ print(format_search_results(results, f"Files of type '{file_type}'"))
538
+ continue
539
+
540
+ if lower_input == '/search-recent':
541
+ searcher = DeviceSearcher(max_results=20)
542
+ results = searcher.search_by_date(days_ago=7)
543
+ print(format_search_results(results, "Files modified in last 7 days"))
544
+ continue
545
+
546
+ if lower_input.startswith('/search-size '):
547
+ try:
548
+ size_kb = int(user_input.split(' ', 1)[1].strip())
549
+ size_bytes = size_kb * 1024
550
+ searcher = DeviceSearcher(max_results=20)
551
+ results = searcher.search_by_size(min_size=size_bytes, limit=20)
552
+ print(format_search_results(results, f"Files larger than {size_kb} KB"))
553
+ except:
554
+ print("❌ Usage: /search-size <kb>\n")
555
+ continue
556
+
557
+ if lower_input.startswith('/search-content '):
558
+ search_text = user_input.split(' ', 1)[1].strip()
559
+ searcher = DeviceSearcher(max_results=20)
560
+ results = searcher.search_by_content(search_text, limit=20)
561
+ print(format_search_results(results, f"Files containing '{search_text}'"))
562
+ continue
563
+
564
+ if lower_input.startswith('/browse '):
565
+ path = user_input.split(' ', 1)[1].strip()
566
+ searcher = DeviceSearcher()
567
+ result = searcher.get_directory_contents(path)
568
+ if 'error' in result:
569
+ print(f"❌ Error: {result['error']}\n")
570
+ else:
571
+ output = f"\n📁 Directory: {result['path']}\n"
572
+ output += f" Total folders: {result['total_dirs']}\n"
573
+ output += f" Total files: {result['total_files']}\n\n"
574
+
575
+ if result['directories']:
576
+ output += " 📂 Folders:\n"
577
+ for d in result['directories']:
578
+ output += f" {d['name']}\n"
579
+
580
+ if result['files']:
581
+ output += "\n 📄 Files:\n"
582
+ for f in result['files']:
583
+ output += f" {f['name']} ({f['size_kb']} KB)\n"
584
+
585
+ print(output)
586
+ continue
587
+
588
+ # TASK COMMANDS
589
+ if lower_input.startswith('/open '):
590
+ path = user_input.split(' ', 1)[1].strip()
591
+ result = TaskExecutor.open_file(path)
592
+ print(format_task_result(result))
593
+ continue
594
+
595
+ if lower_input.startswith('/folder '):
596
+ path = user_input.split(' ', 1)[1].strip()
597
+ result = TaskExecutor.open_folder(path)
598
+ print(format_task_result(result))
599
+ continue
600
+
601
+ if lower_input.startswith('/copy '):
602
+ parts = user_input.split(' ')
603
+ if len(parts) < 3:
604
+ print("❌ Usage: /copy <source> <destination>\n")
605
+ continue
606
+ source = parts[1]
607
+ dest = ' '.join(parts[2:])
608
+ result = TaskExecutor.copy_file(source, dest)
609
+ print(format_task_result(result))
610
+ continue
611
+
612
+ if lower_input.startswith('/move '):
613
+ parts = user_input.split(' ')
614
+ if len(parts) < 3:
615
+ print("❌ Usage: /move <source> <destination>\n")
616
+ continue
617
+ source = parts[1]
618
+ dest = ' '.join(parts[2:])
619
+ result = TaskExecutor.move_file(source, dest)
620
+ print(format_task_result(result))
621
+ continue
622
+
623
+ if lower_input.startswith('/delete '):
624
+ path = user_input.split(' ', 1)[1].strip()
625
+ confirm = input(f"⚠️ Delete '{path}'? (y/N): ").strip().lower()
626
+ if confirm == 'y':
627
+ result = TaskExecutor.delete_file(path)
628
+ print(format_task_result(result))
629
+ continue
630
+
631
+ if lower_input.startswith('/rename '):
632
+ parts = user_input.split(' ')
633
+ if len(parts) < 3:
634
+ print("❌ Usage: /rename <path> <new_name>\n")
635
+ continue
636
+ path = parts[1]
637
+ new_name = ' '.join(parts[2:])
638
+ result = TaskExecutor.rename_file(path, new_name)
639
+ print(format_task_result(result))
640
+ continue
641
+
642
+ if lower_input.startswith('/mkdir '):
643
+ path = user_input.split(' ', 1)[1].strip()
644
+ result = TaskExecutor.create_folder(path)
645
+ print(format_task_result(result))
646
+ continue
647
+
648
+ if lower_input.startswith('/info '):
649
+ path = user_input.split(' ', 1)[1].strip()
650
+ result = TaskExecutor.get_file_info(path)
651
+ print(format_task_result(result))
652
+ continue
653
+
654
+ # UPDATE COMMANDS
655
+ if lower_input in ['/updates', '/update-check']:
656
+ updater = UpdateManager()
657
+ force = lower_input == '/update-check'
658
+ updates = updater.check_for_updates(force=force)
659
+ print(format_update_info(updates))
660
+ continue
661
+
662
+ if lower_input.startswith('/update '):
663
+ source = user_input.split(' ', 1)[1].strip().lower()
664
+ if source not in ['npm', 'pip', 'github']:
665
+ print("❌ Usage: /update <npm|pip|github>\n")
666
+ continue
667
+
668
+ confirm = input(f"⚠️ Update via {source}? (y/N): ").strip().lower()
669
+ if confirm == 'y':
670
+ updater = UpdateManager()
671
+ result = updater.download_update(source)
672
+ print(format_task_result(result))
673
+ continue
674
+
675
+ if lower_input == '/version':
676
+ updater = UpdateManager()
677
+ version_info = updater.get_version_info()
678
+ output = f"\n📦 Version Information:\n"
679
+ output += f" Project: {version_info['project']}\n"
680
+ output += f" Version: {version_info['current_version']}\n"
681
+ output += f" Python: {version_info['python_version']}\n"
682
+ output += f" Platform: {version_info['platform']}\n"
683
+ output += f" npm available: {'✅' if version_info['npm_available'] else '❌'}\n"
684
+ output += f" pip available: {'✅' if version_info['pip_available'] else '❌'}\n\n"
685
+ print(output)
686
+ continue
687
+
688
+ if lower_input.startswith('/rollback '):
689
+ version = user_input.split(' ', 1)[1].strip()
690
+ confirm = input(f"⚠️ Rollback to {version}? (y/N): ").strip().lower()
691
+ if confirm == 'y':
692
+ updater = UpdateManager()
693
+ result = updater.rollback_version(version)
694
+ print(format_task_result(result))
695
+ continue
696
+
697
+ # Add user message
698
+ conversation.append({"role": "user", "content": user_input})
699
+ history.add_message("user", user_input)
700
+
701
+ # Get AI response
702
+ try:
703
+ print("🤖 ", end="", flush=True)
704
+ messages = [Message(role=msg["role"], content=msg["content"]) for msg in conversation]
705
+ response = await provider.generate(messages, temperature=0.7)
706
+
707
+ ai_response = response.content
708
+ print(ai_response, end="\n\n", flush=True)
709
+
710
+ # Add to history
711
+ conversation.append({"role": "assistant", "content": ai_response})
712
+ history.add_message("assistant", ai_response)
713
+
714
+ except RuntimeError as e:
715
+ print(f"\n❌ Error: {str(e)}\n")
716
+ except Exception as e:
717
+ print(f"\n❌ Unexpected error: {str(e)}\n")
718
+
719
+ return should_close
720
+
721
+
722
+ async def start_chat_mode():
723
+ """Start interactive chat mode with enhanced features."""
724
+ try:
725
+ # Select provider
726
+ provider_key, provider_name = select_provider()
727
+
728
+ # Select model
729
+ model_name = select_model(provider_key)
730
+
731
+ # Get API key
732
+ try:
733
+ api_key = get_api_key(provider_key)
734
+ except ValueError as e:
735
+ print(f"❌ {str(e)}")
736
+ return
737
+
738
+ # Create provider
739
+ try:
740
+ provider = create_provider(provider_key, model=model_name, api_key=api_key)
741
+ except ValueError as e:
742
+ print(f"❌ {str(e)}")
743
+ return
744
+
745
+ # Create history
746
+ history = ChatHistory()
747
+ history.provider_name = provider_key
748
+ history.model_name = model_name.replace("/", "_")[:30]
749
+
750
+ # Start chat
751
+ should_save = await chat_with_provider(provider, history, provider_key)
752
+
753
+ # Save history if not explicitly closed
754
+ if should_save:
755
+ filepath = history.save()
756
+ print(f"\n✅ Chat saved to: {filepath}")
757
+ else:
758
+ print(f"\n⚠️ Chat closed without saving")
759
+
760
+ except KeyboardInterrupt:
761
+ print("\n[*] Chat interrupted")
762
+ except Exception as e:
763
+ print(f"❌ Error: {str(e)}")