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.
- package/.env.template +22 -0
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/astra/__init__.py +15 -0
- package/astra/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/__pycache__/chat.cpython-314.pyc +0 -0
- package/astra/__pycache__/cli.cpython-314.pyc +0 -0
- package/astra/__pycache__/prompts.cpython-314.pyc +0 -0
- package/astra/__pycache__/updater.cpython-314.pyc +0 -0
- package/astra/chat.py +763 -0
- package/astra/cli.py +913 -0
- package/astra/core/__init__.py +8 -0
- package/astra/core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/agent.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/config.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/memory.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/reasoning.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/state.cpython-314.pyc +0 -0
- package/astra/core/agent.py +515 -0
- package/astra/core/config.py +247 -0
- package/astra/core/memory.py +782 -0
- package/astra/core/reasoning.py +423 -0
- package/astra/core/state.py +366 -0
- package/astra/core/voice.py +144 -0
- package/astra/llm/__init__.py +32 -0
- package/astra/llm/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/llm/__pycache__/providers.cpython-314.pyc +0 -0
- package/astra/llm/providers.py +530 -0
- package/astra/planning/__init__.py +117 -0
- package/astra/prompts.py +289 -0
- package/astra/reflection/__init__.py +181 -0
- package/astra/search.py +469 -0
- package/astra/tasks.py +466 -0
- package/astra/tools/__init__.py +17 -0
- package/astra/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/advanced.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/base.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/browser.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/file.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/git.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/memory_tool.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/python.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/shell.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/web.cpython-314.pyc +0 -0
- package/astra/tools/__pycache__/windows.cpython-314.pyc +0 -0
- package/astra/tools/advanced.py +251 -0
- package/astra/tools/base.py +344 -0
- package/astra/tools/browser.py +93 -0
- package/astra/tools/file.py +476 -0
- package/astra/tools/git.py +74 -0
- package/astra/tools/memory_tool.py +89 -0
- package/astra/tools/python.py +238 -0
- package/astra/tools/shell.py +183 -0
- package/astra/tools/web.py +804 -0
- package/astra/tools/windows.py +542 -0
- package/astra/updater.py +450 -0
- package/astra/utils/__init__.py +230 -0
- package/bin/astraagent.js +73 -0
- package/bin/postinstall.js +25 -0
- package/config.json.template +52 -0
- package/main.py +16 -0
- package/package.json +51 -0
- 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)}")
|