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/cli.py
ADDED
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AstraAgent CLI - Command Line Interface for the Autonomous Agent.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
import fnmatch
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from typing import Optional, List, Dict, Any, Tuple
|
|
15
|
+
|
|
16
|
+
from astra.core.agent import AstraAgent
|
|
17
|
+
from astra.core.config import AgentConfig, ExecutionMode, LogLevel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
BANNER = r"""
|
|
21
|
+
___ __ ___ __
|
|
22
|
+
/ | _____ / /__________ _ / | ____ ____ ____ / /_
|
|
23
|
+
/ /| | / ___/ / __/ ___/ __ `// /| | / __ `/ _ \/ __ \/ __/
|
|
24
|
+
/ ___ |(__ ) / /_/ / / /_/ // ___ |/ /_/ / __/ / / / /_
|
|
25
|
+
/_/ |_/____/ \__/_/ \__,_//_/ |_|\__, /\___/_/ /_/\__/
|
|
26
|
+
/____/
|
|
27
|
+
|
|
28
|
+
[*] Autonomous AI Agent v2.25.6
|
|
29
|
+
============================================
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def print_banner():
|
|
34
|
+
"""Print the ASCII banner."""
|
|
35
|
+
print(BANNER)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
39
|
+
"""Create argument parser."""
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
prog="astra",
|
|
42
|
+
description="AstraAgent - Autonomous AI Agent",
|
|
43
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Main commands
|
|
47
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
48
|
+
|
|
49
|
+
# Run command
|
|
50
|
+
run_parser = subparsers.add_parser("run", help="Run agent with a goal")
|
|
51
|
+
run_parser.add_argument("goal", nargs="?", help="Goal to accomplish")
|
|
52
|
+
run_parser.add_argument("-i", "--interactive", action="store_true", help="Interactive mode")
|
|
53
|
+
run_parser.add_argument("-m", "--mode", choices=["autonomous", "supervised", "dry_run"],
|
|
54
|
+
default="autonomous", help="Execution mode")
|
|
55
|
+
run_parser.add_argument("--model", default=None, help="LLM model to use")
|
|
56
|
+
run_parser.add_argument("--provider", default="openai", help="LLM provider")
|
|
57
|
+
run_parser.add_argument("--max-iterations", type=int, default=50, help="Max iterations")
|
|
58
|
+
run_parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
59
|
+
run_parser.add_argument("-w", "--workspace", default="./", help="Workspace directory")
|
|
60
|
+
|
|
61
|
+
# Start command (friendly alias for run)
|
|
62
|
+
start_parser = subparsers.add_parser("start", help="Start the agent quickly")
|
|
63
|
+
start_parser.add_argument("goal", nargs="?", help="Optional goal to run")
|
|
64
|
+
start_parser.add_argument("-i", "--interactive", action="store_true", help="Interactive mode")
|
|
65
|
+
start_parser.add_argument("-m", "--mode", choices=["autonomous", "supervised", "dry_run"],
|
|
66
|
+
default="autonomous", help="Execution mode")
|
|
67
|
+
start_parser.add_argument("--model", default=None, help="LLM model to use")
|
|
68
|
+
start_parser.add_argument("--provider", default="openai", help="LLM provider")
|
|
69
|
+
start_parser.add_argument("--max-iterations", type=int, default=50, help="Max iterations")
|
|
70
|
+
start_parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
71
|
+
start_parser.add_argument("-w", "--workspace", default="./", help="Workspace directory")
|
|
72
|
+
|
|
73
|
+
# Config command
|
|
74
|
+
config_parser = subparsers.add_parser("config", help="Manage configuration")
|
|
75
|
+
config_parser.add_argument("--show", action="store_true", help="Show current config")
|
|
76
|
+
config_parser.add_argument("--save", help="Save config to file")
|
|
77
|
+
config_parser.add_argument("--load", help="Load config from file")
|
|
78
|
+
config_parser.add_argument("--set-key", help="Set API key")
|
|
79
|
+
|
|
80
|
+
# Tools command
|
|
81
|
+
tools_parser = subparsers.add_parser("tools", help="List available tools")
|
|
82
|
+
tools_parser.add_argument("--info", help="Show info about a specific tool")
|
|
83
|
+
|
|
84
|
+
# Memory command
|
|
85
|
+
memory_parser = subparsers.add_parser("memory", help="Manage agent memory")
|
|
86
|
+
memory_parser.add_argument("--list", action="store_true", help="List recent memories")
|
|
87
|
+
memory_parser.add_argument("--search", help="Search memories")
|
|
88
|
+
memory_parser.add_argument("--clear", action="store_true", help="Clear all memories")
|
|
89
|
+
|
|
90
|
+
# Chat command (multi-provider interactive chat)
|
|
91
|
+
chat_parser = subparsers.add_parser("chat", help="Interactive chat with LLM providers")
|
|
92
|
+
chat_parser.add_argument("-p", "--provider", help="LLM provider (openai, gemini, anthropic, groq, openrouter, local)")
|
|
93
|
+
chat_parser.add_argument("-m", "--model", help="Model to use")
|
|
94
|
+
chat_parser.add_argument("-k", "--api-key", help="API key for provider")
|
|
95
|
+
chat_parser.add_argument("--list-providers", action="store_true", help="List available providers")
|
|
96
|
+
|
|
97
|
+
# Settings command
|
|
98
|
+
settings_parser = subparsers.add_parser("settings", help="Manage chat settings and API keys")
|
|
99
|
+
settings_parser.add_argument("--list-keys", action="store_true", help="View all API keys status")
|
|
100
|
+
settings_parser.add_argument("--set-key", metavar="PROVIDER", help="Set API key for provider")
|
|
101
|
+
settings_parser.add_argument("--view-env", action="store_true", help="View .env file contents")
|
|
102
|
+
settings_parser.add_argument("--reset-all", action="store_true", help="Reset all API keys")
|
|
103
|
+
|
|
104
|
+
# Keys command (shortcut to view keys)
|
|
105
|
+
keys_parser = subparsers.add_parser("keys", help="View and manage API keys")
|
|
106
|
+
keys_parser.add_argument("--list", action="store_true", help="List all API keys")
|
|
107
|
+
keys_parser.add_argument("--set", metavar="PROVIDER", help="Set API key for provider")
|
|
108
|
+
keys_parser.add_argument("--get", metavar="PROVIDER", help="Get masked status of API key")
|
|
109
|
+
|
|
110
|
+
# Providers command
|
|
111
|
+
providers_parser = subparsers.add_parser("providers", help="List available LLM providers")
|
|
112
|
+
providers_parser.add_argument("--info", metavar="PROVIDER", help="Show info about specific provider")
|
|
113
|
+
|
|
114
|
+
# List models for a provider
|
|
115
|
+
models_parser = subparsers.add_parser("models", help="List models for a provider")
|
|
116
|
+
models_parser.add_argument("provider", help="Provider name (openai, gemini, etc.)")
|
|
117
|
+
models_parser.add_argument("--info", action="store_true", help="Show provider info")
|
|
118
|
+
|
|
119
|
+
# Search command - device-wide search
|
|
120
|
+
search_parser = subparsers.add_parser("search", help="Search files across the device")
|
|
121
|
+
search_parser.add_argument("query", nargs="?", help="Search query or filename (supports wildcards)")
|
|
122
|
+
search_parser.add_argument("--root", help="Root directory to search (default: all device roots)")
|
|
123
|
+
search_parser.add_argument("--ext", help="Search by extension (e.g. .py)")
|
|
124
|
+
search_parser.add_argument("--type", help="Search by type (code, image, document)")
|
|
125
|
+
search_parser.add_argument("--recent", action="store_true", help="Search recently modified files")
|
|
126
|
+
search_parser.add_argument("--days", type=int, default=7, help="Recent window in days (with --recent)")
|
|
127
|
+
search_parser.add_argument("--content", help="Search file contents for text")
|
|
128
|
+
search_parser.add_argument("--limit", type=int, default=100, help="Maximum results to show")
|
|
129
|
+
search_parser.add_argument("--max-depth", type=int, default=-1, help="Maximum directory depth (-1 for unlimited)")
|
|
130
|
+
search_parser.add_argument("--show-hidden", action="store_true", help="Include hidden files/directories")
|
|
131
|
+
|
|
132
|
+
# Tasks command - perform file operations
|
|
133
|
+
tasks_parser = subparsers.add_parser("tasks", help="Perform file/folder tasks from CLI (single or batch)")
|
|
134
|
+
tasks_parser.add_argument("action", nargs="?", help="Action (copy, move, delete, info, mkdir, list, rename)")
|
|
135
|
+
tasks_parser.add_argument("src", nargs="?", help="Source path")
|
|
136
|
+
tasks_parser.add_argument("dest", nargs="?", help="Destination path")
|
|
137
|
+
tasks_parser.add_argument("--overwrite", action="store_true", help="Overwrite destination when supported")
|
|
138
|
+
tasks_parser.add_argument("--recursive", action="store_true", help="Recursive operation for delete/list")
|
|
139
|
+
tasks_parser.add_argument("--yes", action="store_true", help="Skip confirmation prompts")
|
|
140
|
+
tasks_parser.add_argument("--batch", help="JSON array of tasks, e.g. '[{\"action\":\"mkdir\",\"src\":\"C:/tmp\"}]'")
|
|
141
|
+
tasks_parser.add_argument("--batch-file", help="Path to JSON file containing task array")
|
|
142
|
+
|
|
143
|
+
# Updater command
|
|
144
|
+
updater_parser = subparsers.add_parser("update", help="Check and apply updates")
|
|
145
|
+
updater_parser.add_argument("--check", action="store_true", help="Check for updates")
|
|
146
|
+
updater_parser.add_argument("--npm", action="store_true", help="Update via npm")
|
|
147
|
+
updater_parser.add_argument("--pip", action="store_true", help="Update via pip")
|
|
148
|
+
updater_parser.add_argument("--github", action="store_true", help="Download from GitHub releases")
|
|
149
|
+
updater_parser.add_argument("--auto", action="store_true", help="Check and automatically install latest (npm preferred)")
|
|
150
|
+
|
|
151
|
+
# Version
|
|
152
|
+
parser.add_argument("--version", action="version", version="AstraAgent 1.0.0")
|
|
153
|
+
|
|
154
|
+
return parser
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def build_config(args) -> AgentConfig:
|
|
158
|
+
"""Build configuration from arguments and config file."""
|
|
159
|
+
# First try to load from config.json if it exists
|
|
160
|
+
config_file = Path("config.json")
|
|
161
|
+
if config_file.exists():
|
|
162
|
+
try:
|
|
163
|
+
config = AgentConfig.load(str(config_file))
|
|
164
|
+
except Exception:
|
|
165
|
+
config = AgentConfig()
|
|
166
|
+
else:
|
|
167
|
+
config = AgentConfig()
|
|
168
|
+
|
|
169
|
+
# Then apply environment variables (they override config file)
|
|
170
|
+
env_config = AgentConfig.from_env()
|
|
171
|
+
if env_config.llm.api_key:
|
|
172
|
+
config.llm.api_key = env_config.llm.api_key
|
|
173
|
+
config.llm.provider = env_config.llm.provider
|
|
174
|
+
config.llm.model = env_config.llm.model
|
|
175
|
+
|
|
176
|
+
# Apply CLI arguments (only if explicitly provided)
|
|
177
|
+
if hasattr(args, 'workspace') and args.workspace:
|
|
178
|
+
config.workspace_path = args.workspace
|
|
179
|
+
|
|
180
|
+
if hasattr(args, 'mode') and args.mode:
|
|
181
|
+
mode_map = {
|
|
182
|
+
"autonomous": ExecutionMode.AUTONOMOUS,
|
|
183
|
+
"supervised": ExecutionMode.SUPERVISED,
|
|
184
|
+
"dry_run": ExecutionMode.DRY_RUN
|
|
185
|
+
}
|
|
186
|
+
config.mode = mode_map.get(args.mode, ExecutionMode.AUTONOMOUS)
|
|
187
|
+
|
|
188
|
+
# Only override provider if explicitly passed (not default)
|
|
189
|
+
if hasattr(args, 'provider') and args.provider and args.provider != "openai":
|
|
190
|
+
config.llm.provider = args.provider
|
|
191
|
+
|
|
192
|
+
if hasattr(args, 'model') and args.model:
|
|
193
|
+
config.llm.model = args.model
|
|
194
|
+
|
|
195
|
+
if hasattr(args, 'max_iterations'):
|
|
196
|
+
config.max_iterations = args.max_iterations
|
|
197
|
+
|
|
198
|
+
# Default to ERROR level for clean output, DEBUG if verbose
|
|
199
|
+
if hasattr(args, 'verbose') and args.verbose:
|
|
200
|
+
config.logging.level = LogLevel.DEBUG
|
|
201
|
+
elif hasattr(args, 'command') and args.command in ('run', 'start'):
|
|
202
|
+
# For run command, only show errors by default
|
|
203
|
+
config.logging.level = LogLevel.ERROR
|
|
204
|
+
|
|
205
|
+
return config
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def cli_interactive_session(config: AgentConfig):
|
|
209
|
+
"""Interactive CLI session - text-based."""
|
|
210
|
+
import asyncio as aio
|
|
211
|
+
|
|
212
|
+
print("\n[*] AstraAgent Interactive Mode")
|
|
213
|
+
print("Type your goals/commands. Type 'exit' to quit.\n")
|
|
214
|
+
|
|
215
|
+
agent = AstraAgent(config)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
while True:
|
|
219
|
+
try:
|
|
220
|
+
user_input = input("You > ").strip()
|
|
221
|
+
except EOFError:
|
|
222
|
+
break
|
|
223
|
+
except KeyboardInterrupt:
|
|
224
|
+
print("\n[*] Exiting...")
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
if not user_input:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if user_input.lower() in ['exit', 'quit', 'q']:
|
|
231
|
+
print("Goodbye!")
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
if user_input.lower() == 'status':
|
|
235
|
+
try:
|
|
236
|
+
print(agent.state.get_summary())
|
|
237
|
+
except:
|
|
238
|
+
print("No status available")
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if user_input.lower() == 'tools':
|
|
242
|
+
try:
|
|
243
|
+
tools = agent.tools.list_enabled()
|
|
244
|
+
print(f"Available tools: {', '.join(tools)}")
|
|
245
|
+
except:
|
|
246
|
+
print("Could not list tools")
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
print("\n[...] Working on your request...\n")
|
|
250
|
+
try:
|
|
251
|
+
result = aio.run(agent.run(user_input))
|
|
252
|
+
print(f"\n{result}\n")
|
|
253
|
+
except ValueError as e:
|
|
254
|
+
print(f"ā Configuration Error: {e}\n")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print(f"ā Error: {e}\n")
|
|
257
|
+
|
|
258
|
+
# Reset for next goal
|
|
259
|
+
agent.state = agent.state.__class__()
|
|
260
|
+
agent.messages = []
|
|
261
|
+
|
|
262
|
+
finally:
|
|
263
|
+
agent.shutdown()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def run_agent(goal: str, config: AgentConfig) -> str:
|
|
267
|
+
"""Run the agent with a goal."""
|
|
268
|
+
agent = AstraAgent(config)
|
|
269
|
+
try:
|
|
270
|
+
result = await agent.run(goal)
|
|
271
|
+
|
|
272
|
+
# Check if agent encountered an error
|
|
273
|
+
if agent.state.last_error:
|
|
274
|
+
error_msg = agent.state.last_error
|
|
275
|
+
if "Cannot connect" in error_msg or "timeout" in error_msg.lower():
|
|
276
|
+
return f"ā Connection Error: Cannot reach LLM server.\n\nRun: ollama serve (or use LM Studio)\nThen try again or see SETUP.md"
|
|
277
|
+
else:
|
|
278
|
+
return f"ā Error: {error_msg}"
|
|
279
|
+
|
|
280
|
+
# Success - return summary only if non-empty
|
|
281
|
+
if result and not result.startswith("==="):
|
|
282
|
+
return result
|
|
283
|
+
elif result.startswith("==="):
|
|
284
|
+
# Extract key info from summary
|
|
285
|
+
return f"ā
Goal '{goal}' processed.\nCheck logs for details."
|
|
286
|
+
|
|
287
|
+
return "ā
Done"
|
|
288
|
+
|
|
289
|
+
except ValueError as e:
|
|
290
|
+
# Configuration errors
|
|
291
|
+
return f"ā Configuration Error: {str(e)}\n\nSee SETUP.md for instructions."
|
|
292
|
+
except RuntimeError as e:
|
|
293
|
+
# Runtime errors (connection, API errors)
|
|
294
|
+
error_msg = str(e)
|
|
295
|
+
if "Cannot connect" in error_msg or "timeout" in error_msg.lower():
|
|
296
|
+
return f"ā Connection Error: Cannot reach LLM server.\n\nRun: ollama serve (or use LM Studio)\nThen try again or see SETUP.md"
|
|
297
|
+
return f"ā Error: {error_msg}"
|
|
298
|
+
except Exception as e:
|
|
299
|
+
return f"ā Unexpected Error: {str(e)}"
|
|
300
|
+
finally:
|
|
301
|
+
agent.shutdown()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def show_tools(tool_name: Optional[str] = None):
|
|
305
|
+
"""Show available tools."""
|
|
306
|
+
from astra.tools.base import create_default_registry
|
|
307
|
+
|
|
308
|
+
registry = create_default_registry()
|
|
309
|
+
|
|
310
|
+
if tool_name:
|
|
311
|
+
tool = registry.get(tool_name)
|
|
312
|
+
if tool:
|
|
313
|
+
info = tool.get_info()
|
|
314
|
+
print(f"\nTool: {info['name']}")
|
|
315
|
+
print(f"Description: {info['description']}")
|
|
316
|
+
print(f"Category: {info['category']}")
|
|
317
|
+
print(f"Risk Level: {info['risk_level']}")
|
|
318
|
+
print("\nParameters:")
|
|
319
|
+
for param in tool.parameters:
|
|
320
|
+
req = "required" if param.required else "optional"
|
|
321
|
+
print(f" - {param.name} ({param.type}, {req}): {param.description}")
|
|
322
|
+
else:
|
|
323
|
+
print(f"Tool not found: {tool_name}")
|
|
324
|
+
else:
|
|
325
|
+
print("\nAvailable Tools:")
|
|
326
|
+
for name in registry.list_enabled():
|
|
327
|
+
tool = registry.get(name)
|
|
328
|
+
print(f" - {name}: {tool.description[:60]}...")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
FILE_TYPE_EXTENSIONS: Dict[str, set] = {
|
|
332
|
+
"code": {".py", ".js", ".ts", ".java", ".cpp", ".c", ".cs", ".go", ".rs", ".php", ".rb", ".swift", ".kt"},
|
|
333
|
+
"image": {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg", ".ico"},
|
|
334
|
+
"document": {".txt", ".md", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".rtf", ".csv"},
|
|
335
|
+
"archive": {".zip", ".rar", ".7z", ".tar", ".gz"},
|
|
336
|
+
"media": {".mp3", ".wav", ".mp4", ".mkv", ".avi", ".mov"},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _format_size(size_bytes: int) -> str:
|
|
341
|
+
"""Format file size in a human-readable form."""
|
|
342
|
+
size = float(size_bytes)
|
|
343
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
344
|
+
if size < 1024:
|
|
345
|
+
return f"{size:.1f}{unit}"
|
|
346
|
+
size /= 1024
|
|
347
|
+
return f"{size:.1f}PB"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _resolve_search_roots(root: Optional[str]) -> List[Path]:
|
|
351
|
+
"""Resolve search roots. Defaults to full device roots."""
|
|
352
|
+
if root:
|
|
353
|
+
root_path = Path(root).resolve()
|
|
354
|
+
return [root_path] if root_path.exists() else []
|
|
355
|
+
|
|
356
|
+
if os.name == "nt":
|
|
357
|
+
roots: List[Path] = []
|
|
358
|
+
for drive_letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
|
359
|
+
drive = Path(f"{drive_letter}:\\")
|
|
360
|
+
if drive.exists():
|
|
361
|
+
roots.append(drive)
|
|
362
|
+
return roots
|
|
363
|
+
|
|
364
|
+
return [Path("/")]
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _is_hidden(path: Path) -> bool:
|
|
368
|
+
"""Best-effort hidden file detection."""
|
|
369
|
+
if path.name.startswith("."):
|
|
370
|
+
return True
|
|
371
|
+
if os.name != "nt":
|
|
372
|
+
return False
|
|
373
|
+
try:
|
|
374
|
+
import stat
|
|
375
|
+
attrs = path.stat().st_file_attributes
|
|
376
|
+
return bool(attrs & stat.FILE_ATTRIBUTE_HIDDEN)
|
|
377
|
+
except Exception:
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _iter_files(root: Path, max_depth: int = -1, show_hidden: bool = False):
|
|
382
|
+
"""Iterate files with optional depth limit."""
|
|
383
|
+
stack: List[Tuple[Path, int]] = [(root, 0)]
|
|
384
|
+
|
|
385
|
+
while stack:
|
|
386
|
+
current, depth = stack.pop()
|
|
387
|
+
if not current.exists():
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
entries = list(current.iterdir())
|
|
392
|
+
except (PermissionError, OSError):
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
for entry in entries:
|
|
396
|
+
if not show_hidden and _is_hidden(entry):
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
if entry.is_dir():
|
|
400
|
+
if max_depth < 0 or depth < max_depth:
|
|
401
|
+
stack.append((entry, depth + 1))
|
|
402
|
+
elif entry.is_file():
|
|
403
|
+
yield entry
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _query_matches(path: Path, query: Optional[str]) -> bool:
|
|
407
|
+
"""Match filename against substring or wildcard query."""
|
|
408
|
+
if not query:
|
|
409
|
+
return True
|
|
410
|
+
name = path.name
|
|
411
|
+
if any(ch in query for ch in "*?[]"):
|
|
412
|
+
return fnmatch.fnmatch(name.lower(), query.lower())
|
|
413
|
+
return query.lower() in name.lower()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _content_matches(path: Path, content_query: Optional[str]) -> bool:
|
|
417
|
+
"""Search content in text-like files with safe limits."""
|
|
418
|
+
if not content_query:
|
|
419
|
+
return True
|
|
420
|
+
try:
|
|
421
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
422
|
+
chunk = f.read(1024 * 1024)
|
|
423
|
+
return content_query.lower() in chunk.lower()
|
|
424
|
+
except Exception:
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def run_device_search(args) -> int:
|
|
429
|
+
"""Run filesystem-wide search from CLI command arguments."""
|
|
430
|
+
if not args.query and not args.ext and not args.type and not args.content and not args.recent:
|
|
431
|
+
print("Provide at least one filter: query, --ext, --type, --content, or --recent")
|
|
432
|
+
return 1
|
|
433
|
+
|
|
434
|
+
roots = _resolve_search_roots(args.root)
|
|
435
|
+
if not roots:
|
|
436
|
+
print(f"No valid search root found for: {args.root}")
|
|
437
|
+
return 1
|
|
438
|
+
|
|
439
|
+
ext_filter = args.ext.lower() if args.ext else None
|
|
440
|
+
if ext_filter and not ext_filter.startswith("."):
|
|
441
|
+
ext_filter = f".{ext_filter}"
|
|
442
|
+
|
|
443
|
+
type_filter = args.type.lower() if args.type else None
|
|
444
|
+
if type_filter and type_filter not in FILE_TYPE_EXTENSIONS:
|
|
445
|
+
valid = ", ".join(sorted(FILE_TYPE_EXTENSIONS.keys()))
|
|
446
|
+
print(f"Invalid --type '{args.type}'. Valid types: {valid}")
|
|
447
|
+
return 1
|
|
448
|
+
|
|
449
|
+
recent_cutoff = None
|
|
450
|
+
if args.recent:
|
|
451
|
+
recent_cutoff = datetime.now() - timedelta(days=max(1, args.days))
|
|
452
|
+
|
|
453
|
+
results: List[Path] = []
|
|
454
|
+
scanned_files = 0
|
|
455
|
+
|
|
456
|
+
for root in roots:
|
|
457
|
+
for file_path in _iter_files(root, max_depth=args.max_depth, show_hidden=args.show_hidden):
|
|
458
|
+
scanned_files += 1
|
|
459
|
+
|
|
460
|
+
if not _query_matches(file_path, args.query):
|
|
461
|
+
continue
|
|
462
|
+
if ext_filter and file_path.suffix.lower() != ext_filter:
|
|
463
|
+
continue
|
|
464
|
+
if type_filter and file_path.suffix.lower() not in FILE_TYPE_EXTENSIONS[type_filter]:
|
|
465
|
+
continue
|
|
466
|
+
if recent_cutoff:
|
|
467
|
+
try:
|
|
468
|
+
if datetime.fromtimestamp(file_path.stat().st_mtime) < recent_cutoff:
|
|
469
|
+
continue
|
|
470
|
+
except (PermissionError, OSError):
|
|
471
|
+
continue
|
|
472
|
+
if not _content_matches(file_path, args.content):
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
results.append(file_path)
|
|
476
|
+
if len(results) >= args.limit:
|
|
477
|
+
break
|
|
478
|
+
|
|
479
|
+
if len(results) >= args.limit:
|
|
480
|
+
break
|
|
481
|
+
|
|
482
|
+
if not results:
|
|
483
|
+
print(f"No matching files found. Scanned {scanned_files} files.")
|
|
484
|
+
return 0
|
|
485
|
+
|
|
486
|
+
print(f"\nFound {len(results)} file(s) (scanned {scanned_files} files):\n")
|
|
487
|
+
for idx, file_path in enumerate(results, start=1):
|
|
488
|
+
try:
|
|
489
|
+
stat = file_path.stat()
|
|
490
|
+
modified = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
491
|
+
size = _format_size(stat.st_size)
|
|
492
|
+
print(f"{idx:3}. {file_path} [{size}, {modified}]")
|
|
493
|
+
except (PermissionError, OSError):
|
|
494
|
+
print(f"{idx:3}. {file_path}")
|
|
495
|
+
|
|
496
|
+
if len(results) == args.limit:
|
|
497
|
+
print(f"\nResult limit reached ({args.limit}). Increase with --limit.")
|
|
498
|
+
|
|
499
|
+
return 0
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _execute_task(action: str, src: Optional[str], dest: Optional[str], overwrite: bool,
|
|
503
|
+
recursive: bool, assume_yes: bool) -> Tuple[bool, str]:
|
|
504
|
+
"""Execute one filesystem task."""
|
|
505
|
+
action = action.lower().strip()
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
if action == "mkdir":
|
|
509
|
+
if not src:
|
|
510
|
+
return False, "mkdir requires src path"
|
|
511
|
+
Path(src).mkdir(parents=True, exist_ok=True)
|
|
512
|
+
return True, f"Created directory: {Path(src).resolve()}"
|
|
513
|
+
|
|
514
|
+
if action == "info":
|
|
515
|
+
if not src:
|
|
516
|
+
return False, "info requires src path"
|
|
517
|
+
target = Path(src).resolve()
|
|
518
|
+
if not target.exists():
|
|
519
|
+
return False, f"Path not found: {target}"
|
|
520
|
+
stat = target.stat()
|
|
521
|
+
kind = "directory" if target.is_dir() else "file"
|
|
522
|
+
details = (
|
|
523
|
+
f"Path: {target}\n"
|
|
524
|
+
f"Type: {kind}\n"
|
|
525
|
+
f"Size: {_format_size(stat.st_size)}\n"
|
|
526
|
+
f"Modified: {datetime.fromtimestamp(stat.st_mtime)}"
|
|
527
|
+
)
|
|
528
|
+
return True, details
|
|
529
|
+
|
|
530
|
+
if action == "list":
|
|
531
|
+
if not src:
|
|
532
|
+
return False, "list requires src directory"
|
|
533
|
+
target = Path(src).resolve()
|
|
534
|
+
if not target.is_dir():
|
|
535
|
+
return False, f"Not a directory: {target}"
|
|
536
|
+
entries = sorted(target.rglob("*") if recursive else target.iterdir(), key=lambda p: p.name.lower())
|
|
537
|
+
entries = [p for p in entries if p != target]
|
|
538
|
+
lines = [f"Listing: {target}", f"Items: {len(entries)}"]
|
|
539
|
+
for p in entries[:500]:
|
|
540
|
+
suffix = "/" if p.is_dir() else ""
|
|
541
|
+
lines.append(f"- {p}{suffix}")
|
|
542
|
+
if len(entries) > 500:
|
|
543
|
+
lines.append(f"... and {len(entries) - 500} more")
|
|
544
|
+
return True, "\n".join(lines)
|
|
545
|
+
|
|
546
|
+
if action == "copy":
|
|
547
|
+
if not src or not dest:
|
|
548
|
+
return False, "copy requires src and dest"
|
|
549
|
+
src_path = Path(src).resolve()
|
|
550
|
+
dest_path = Path(dest).resolve()
|
|
551
|
+
if not src_path.exists():
|
|
552
|
+
return False, f"Source not found: {src_path}"
|
|
553
|
+
if dest_path.exists() and not overwrite:
|
|
554
|
+
return False, f"Destination exists: {dest_path} (use --overwrite)"
|
|
555
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
556
|
+
if src_path.is_dir():
|
|
557
|
+
if dest_path.exists():
|
|
558
|
+
shutil.rmtree(dest_path)
|
|
559
|
+
shutil.copytree(src_path, dest_path)
|
|
560
|
+
else:
|
|
561
|
+
shutil.copy2(src_path, dest_path)
|
|
562
|
+
return True, f"Copied {src_path} -> {dest_path}"
|
|
563
|
+
|
|
564
|
+
if action == "move":
|
|
565
|
+
if not src or not dest:
|
|
566
|
+
return False, "move requires src and dest"
|
|
567
|
+
src_path = Path(src).resolve()
|
|
568
|
+
dest_path = Path(dest).resolve()
|
|
569
|
+
if not src_path.exists():
|
|
570
|
+
return False, f"Source not found: {src_path}"
|
|
571
|
+
if dest_path.exists() and not overwrite:
|
|
572
|
+
return False, f"Destination exists: {dest_path} (use --overwrite)"
|
|
573
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
574
|
+
shutil.move(str(src_path), str(dest_path))
|
|
575
|
+
return True, f"Moved {src_path} -> {dest_path}"
|
|
576
|
+
|
|
577
|
+
if action == "rename":
|
|
578
|
+
if not src or not dest:
|
|
579
|
+
return False, "rename requires src and dest"
|
|
580
|
+
src_path = Path(src).resolve()
|
|
581
|
+
dest_path = src_path.with_name(dest)
|
|
582
|
+
if not src_path.exists():
|
|
583
|
+
return False, f"Source not found: {src_path}"
|
|
584
|
+
if dest_path.exists() and not overwrite:
|
|
585
|
+
return False, f"Destination exists: {dest_path} (use --overwrite)"
|
|
586
|
+
src_path.rename(dest_path)
|
|
587
|
+
return True, f"Renamed {src_path.name} -> {dest_path.name}"
|
|
588
|
+
|
|
589
|
+
if action == "delete":
|
|
590
|
+
if not src:
|
|
591
|
+
return False, "delete requires src path"
|
|
592
|
+
target = Path(src).resolve()
|
|
593
|
+
if not target.exists():
|
|
594
|
+
return False, f"Path not found: {target}"
|
|
595
|
+
if not assume_yes:
|
|
596
|
+
confirm = input(f"Delete '{target}'? (y/N): ").strip().lower()
|
|
597
|
+
if confirm != "y":
|
|
598
|
+
return False, "Delete cancelled"
|
|
599
|
+
if target.is_dir():
|
|
600
|
+
if recursive:
|
|
601
|
+
shutil.rmtree(target)
|
|
602
|
+
else:
|
|
603
|
+
target.rmdir()
|
|
604
|
+
else:
|
|
605
|
+
target.unlink()
|
|
606
|
+
return True, f"Deleted: {target}"
|
|
607
|
+
|
|
608
|
+
return False, f"Unknown action: {action}"
|
|
609
|
+
|
|
610
|
+
except Exception as e:
|
|
611
|
+
return False, str(e)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _load_batch_tasks(batch_json: Optional[str], batch_file: Optional[str]) -> List[Dict[str, Any]]:
|
|
615
|
+
"""Load task list from JSON string or file."""
|
|
616
|
+
if batch_json:
|
|
617
|
+
payload = json.loads(batch_json)
|
|
618
|
+
elif batch_file:
|
|
619
|
+
payload = json.loads(Path(batch_file).read_text(encoding="utf-8-sig"))
|
|
620
|
+
else:
|
|
621
|
+
return []
|
|
622
|
+
|
|
623
|
+
if not isinstance(payload, list):
|
|
624
|
+
raise ValueError("Batch payload must be a JSON array")
|
|
625
|
+
return payload
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def run_tasks_command(args) -> int:
|
|
629
|
+
"""Run single or batch file tasks."""
|
|
630
|
+
tasks: List[Dict[str, Any]] = []
|
|
631
|
+
|
|
632
|
+
if args.batch or args.batch_file:
|
|
633
|
+
try:
|
|
634
|
+
tasks = _load_batch_tasks(args.batch, args.batch_file)
|
|
635
|
+
except Exception as e:
|
|
636
|
+
print(f"Failed to load batch tasks: {e}")
|
|
637
|
+
return 1
|
|
638
|
+
else:
|
|
639
|
+
if not args.action:
|
|
640
|
+
print("tasks requires action, or use --batch/--batch-file")
|
|
641
|
+
return 1
|
|
642
|
+
tasks = [{
|
|
643
|
+
"action": args.action,
|
|
644
|
+
"src": args.src,
|
|
645
|
+
"dest": args.dest,
|
|
646
|
+
"overwrite": args.overwrite,
|
|
647
|
+
"recursive": args.recursive,
|
|
648
|
+
"yes": args.yes,
|
|
649
|
+
}]
|
|
650
|
+
|
|
651
|
+
success_count = 0
|
|
652
|
+
for idx, task in enumerate(tasks, start=1):
|
|
653
|
+
action = str(task.get("action", "")).strip()
|
|
654
|
+
src = task.get("src")
|
|
655
|
+
dest = task.get("dest")
|
|
656
|
+
overwrite = bool(task.get("overwrite", args.overwrite))
|
|
657
|
+
recursive = bool(task.get("recursive", args.recursive))
|
|
658
|
+
assume_yes = bool(task.get("yes", args.yes))
|
|
659
|
+
|
|
660
|
+
ok, message = _execute_task(action, src, dest, overwrite, recursive, assume_yes)
|
|
661
|
+
status = "OK" if ok else "FAIL"
|
|
662
|
+
print(f"[{idx}/{len(tasks)}] {status} {action}: {message}")
|
|
663
|
+
if ok:
|
|
664
|
+
success_count += 1
|
|
665
|
+
|
|
666
|
+
print(f"\nTask summary: {success_count}/{len(tasks)} succeeded")
|
|
667
|
+
return 0 if success_count == len(tasks) else 1
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def run_update_command(args) -> int:
|
|
671
|
+
"""Check and/or install updates using updater module."""
|
|
672
|
+
def _safe_print(text: str):
|
|
673
|
+
try:
|
|
674
|
+
print(text)
|
|
675
|
+
except UnicodeEncodeError:
|
|
676
|
+
print(text.encode("ascii", "replace").decode("ascii"))
|
|
677
|
+
|
|
678
|
+
try:
|
|
679
|
+
from astra.updater import UpdateManager, format_update_info
|
|
680
|
+
except Exception as e:
|
|
681
|
+
print(f"Update module unavailable: {e}")
|
|
682
|
+
return 1
|
|
683
|
+
|
|
684
|
+
manager = UpdateManager()
|
|
685
|
+
|
|
686
|
+
# Default behavior: check for updates if no install option chosen.
|
|
687
|
+
if args.auto or args.check or not any([args.npm, args.pip, args.github, args.auto]):
|
|
688
|
+
info = manager.check_for_updates(force=True)
|
|
689
|
+
_safe_print(format_update_info(info))
|
|
690
|
+
|
|
691
|
+
if not args.auto and not any([args.npm, args.pip, args.github]):
|
|
692
|
+
return 0
|
|
693
|
+
|
|
694
|
+
# Auto mode only proceeds when update exists
|
|
695
|
+
if args.auto and not info.get("update_available"):
|
|
696
|
+
return 0
|
|
697
|
+
|
|
698
|
+
source = None
|
|
699
|
+
if args.npm:
|
|
700
|
+
source = "npm"
|
|
701
|
+
elif args.pip:
|
|
702
|
+
source = "pip"
|
|
703
|
+
elif args.github:
|
|
704
|
+
source = "github"
|
|
705
|
+
elif args.auto:
|
|
706
|
+
# Prefer npm when available, fallback to pip.
|
|
707
|
+
source = "npm" if manager.get_version_info().get("npm_available") else "pip"
|
|
708
|
+
|
|
709
|
+
if not source:
|
|
710
|
+
return 0
|
|
711
|
+
|
|
712
|
+
result = manager.download_update(source=source)
|
|
713
|
+
if result.get("success"):
|
|
714
|
+
_safe_print(result.get("message", "Update completed"))
|
|
715
|
+
return 0
|
|
716
|
+
|
|
717
|
+
_safe_print(result.get("message", "Update failed"))
|
|
718
|
+
if result.get("error"):
|
|
719
|
+
_safe_print(result["error"])
|
|
720
|
+
return 1
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def main():
|
|
724
|
+
"""Main entry point."""
|
|
725
|
+
parser = create_parser()
|
|
726
|
+
args = parser.parse_args()
|
|
727
|
+
|
|
728
|
+
if args.command is None:
|
|
729
|
+
print_banner()
|
|
730
|
+
parser.print_help()
|
|
731
|
+
return
|
|
732
|
+
|
|
733
|
+
if args.command in ("run", "start"):
|
|
734
|
+
print_banner()
|
|
735
|
+
|
|
736
|
+
if args.interactive or not getattr(args, "goal", None):
|
|
737
|
+
config = build_config(args)
|
|
738
|
+
cli_interactive_session(config)
|
|
739
|
+
else:
|
|
740
|
+
config = build_config(args)
|
|
741
|
+
result = asyncio.run(run_agent(args.goal, config))
|
|
742
|
+
# Only show result if it's not empty and not a verbose success message
|
|
743
|
+
if result and not result.startswith("==="):
|
|
744
|
+
print(result)
|
|
745
|
+
|
|
746
|
+
elif args.command == "tools":
|
|
747
|
+
show_tools(args.info if hasattr(args, 'info') else None)
|
|
748
|
+
|
|
749
|
+
elif args.command == "config":
|
|
750
|
+
config = AgentConfig.from_env()
|
|
751
|
+
if args.show:
|
|
752
|
+
import json
|
|
753
|
+
print(json.dumps(config.to_dict(), indent=2, default=str))
|
|
754
|
+
elif args.save:
|
|
755
|
+
config.save(args.save)
|
|
756
|
+
print(f"Config saved to {args.save}")
|
|
757
|
+
elif args.load:
|
|
758
|
+
config = AgentConfig.load(args.load)
|
|
759
|
+
print(f"Config loaded from {args.load}")
|
|
760
|
+
|
|
761
|
+
elif args.command == "memory":
|
|
762
|
+
from astra.core.memory import MemoryManager
|
|
763
|
+
memory = MemoryManager(persistence_path="./.astra_memory")
|
|
764
|
+
|
|
765
|
+
if args.list:
|
|
766
|
+
if hasattr(memory, "get_recent"):
|
|
767
|
+
recent = memory.get_recent(10)
|
|
768
|
+
else:
|
|
769
|
+
recent = memory.get_recent_conversations(10)
|
|
770
|
+
if recent:
|
|
771
|
+
for item in recent:
|
|
772
|
+
print(f"[{item.memory_type}] {item.content[:100]}...")
|
|
773
|
+
else:
|
|
774
|
+
print("No memories found")
|
|
775
|
+
elif args.search:
|
|
776
|
+
results = memory.search(query=args.search)
|
|
777
|
+
for item in results:
|
|
778
|
+
print(f"[{item.id}] {item.content[:100]}...")
|
|
779
|
+
|
|
780
|
+
elif args.command == "search":
|
|
781
|
+
exit_code = run_device_search(args)
|
|
782
|
+
if exit_code != 0:
|
|
783
|
+
sys.exit(exit_code)
|
|
784
|
+
|
|
785
|
+
elif args.command == "tasks":
|
|
786
|
+
exit_code = run_tasks_command(args)
|
|
787
|
+
if exit_code != 0:
|
|
788
|
+
sys.exit(exit_code)
|
|
789
|
+
|
|
790
|
+
elif args.command == "update":
|
|
791
|
+
exit_code = run_update_command(args)
|
|
792
|
+
if exit_code != 0:
|
|
793
|
+
sys.exit(exit_code)
|
|
794
|
+
|
|
795
|
+
elif args.command == "chat":
|
|
796
|
+
# Interactive chat mode with multiple providers
|
|
797
|
+
from astra.chat import start_chat_mode
|
|
798
|
+
from astra.llm import PROVIDERS
|
|
799
|
+
|
|
800
|
+
# Show available providers if requested
|
|
801
|
+
if args.list_providers:
|
|
802
|
+
print("\nš¦ Available LLM Providers:\n")
|
|
803
|
+
for key, info in PROVIDERS.items():
|
|
804
|
+
free_tag = "š° Free" if info.get("free") else "š³ Paid"
|
|
805
|
+
print(f" ⢠{key:12} - {info['name']:20} [{free_tag}]")
|
|
806
|
+
print("\nRun: python main.py chat")
|
|
807
|
+
return
|
|
808
|
+
|
|
809
|
+
# Start interactive chat
|
|
810
|
+
asyncio.run(start_chat_mode())
|
|
811
|
+
|
|
812
|
+
elif args.command == "settings":
|
|
813
|
+
# Settings management
|
|
814
|
+
from astra.chat import APIKeyManager
|
|
815
|
+
|
|
816
|
+
if args.list_keys:
|
|
817
|
+
APIKeyManager.view_keys()
|
|
818
|
+
elif args.set_key:
|
|
819
|
+
print(f"\nSetting API key for: {args.set_key}")
|
|
820
|
+
api_key = input("Enter API key: ").strip()
|
|
821
|
+
if api_key:
|
|
822
|
+
APIKeyManager.set_key(args.set_key, api_key)
|
|
823
|
+
elif args.view_env:
|
|
824
|
+
env_file = Path(".env")
|
|
825
|
+
if env_file.exists():
|
|
826
|
+
print("\nš .env file contents:\n")
|
|
827
|
+
print(env_file.read_text())
|
|
828
|
+
else:
|
|
829
|
+
print("ā .env file not found")
|
|
830
|
+
elif args.reset_all:
|
|
831
|
+
confirm = input("ā ļø Clear ALL API keys from .env? (y/N): ").strip().lower()
|
|
832
|
+
if confirm == 'y':
|
|
833
|
+
env_file = Path(".env")
|
|
834
|
+
if env_file.exists():
|
|
835
|
+
env_file.unlink()
|
|
836
|
+
print("ā
All keys cleared")
|
|
837
|
+
else:
|
|
838
|
+
# Default: show keys
|
|
839
|
+
APIKeyManager.view_keys()
|
|
840
|
+
|
|
841
|
+
elif args.command == "keys":
|
|
842
|
+
# API key management shortcut
|
|
843
|
+
from astra.chat import APIKeyManager
|
|
844
|
+
|
|
845
|
+
if args.list or not any([args.set, args.get]):
|
|
846
|
+
APIKeyManager.view_keys()
|
|
847
|
+
elif args.set:
|
|
848
|
+
print(f"\nSetting API key for: {args.set}")
|
|
849
|
+
api_key = input("Enter API key: ").strip()
|
|
850
|
+
if api_key:
|
|
851
|
+
APIKeyManager.set_key(args.set, api_key)
|
|
852
|
+
elif args.get:
|
|
853
|
+
from astra.llm import PROVIDERS
|
|
854
|
+
if args.get not in PROVIDERS:
|
|
855
|
+
print(f"ā Unknown provider: {args.get}")
|
|
856
|
+
return
|
|
857
|
+
keys = APIKeyManager.get_all_keys()
|
|
858
|
+
status = keys.get(args.get, "NOT SET")
|
|
859
|
+
print(f"\n{args.get}: {status}")
|
|
860
|
+
|
|
861
|
+
elif args.command == "providers":
|
|
862
|
+
# Show provider information
|
|
863
|
+
from astra.llm import PROVIDERS
|
|
864
|
+
|
|
865
|
+
if args.info:
|
|
866
|
+
provider = PROVIDERS.get(args.info)
|
|
867
|
+
if not provider:
|
|
868
|
+
print(f"ā Unknown provider: {args.info}")
|
|
869
|
+
return
|
|
870
|
+
|
|
871
|
+
print(f"\nš¦ Provider: {provider['name']}")
|
|
872
|
+
print(f" Key: {args.info}")
|
|
873
|
+
print(f" Type: {'š° Free' if provider.get('free') else 'š³ Paid'}")
|
|
874
|
+
print(f" Website: {provider.get('website', 'N/A')}")
|
|
875
|
+
print(f"\n Models: {len(provider['models'])} available")
|
|
876
|
+
for model in provider['models'][:10]:
|
|
877
|
+
print(f" ⢠{model}")
|
|
878
|
+
if len(provider['models']) > 10:
|
|
879
|
+
print(f" ... and {len(provider['models']) - 10} more")
|
|
880
|
+
else:
|
|
881
|
+
# List all providers
|
|
882
|
+
print("\nš¦ Available LLM Providers:\n")
|
|
883
|
+
for key, info in PROVIDERS.items():
|
|
884
|
+
free_tag = "š° Free" if info.get("free") else "š³ Paid"
|
|
885
|
+
print(f" {key:12} - {info['name']:20} [{free_tag}] ({len(info['models'])} models)")
|
|
886
|
+
print("\nRun: python main.py providers --info <provider>")
|
|
887
|
+
|
|
888
|
+
elif args.command == "models":
|
|
889
|
+
# Show models for a provider
|
|
890
|
+
from astra.llm import PROVIDERS
|
|
891
|
+
|
|
892
|
+
provider = PROVIDERS.get(args.provider)
|
|
893
|
+
if not provider:
|
|
894
|
+
print(f"ā Unknown provider: {args.provider}")
|
|
895
|
+
return
|
|
896
|
+
|
|
897
|
+
if args.info:
|
|
898
|
+
print(f"\nš¦ Provider: {provider['name']}")
|
|
899
|
+
print(f" Website: {provider.get('website', 'N/A')}")
|
|
900
|
+
print(f" Type: {'š° Free' if provider.get('free') else 'š³ Paid'}")
|
|
901
|
+
|
|
902
|
+
print(f"\nš¤ Available models for {provider['name']}:\n")
|
|
903
|
+
for i, model in enumerate(provider['models'], 1):
|
|
904
|
+
print(f" {i:2}. {model}")
|
|
905
|
+
|
|
906
|
+
print(f"\nTotal: {len(provider['models'])} models")
|
|
907
|
+
|
|
908
|
+
else:
|
|
909
|
+
parser.print_help()
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
if __name__ == "__main__":
|
|
913
|
+
main()
|