astraagent 2.25.6 → 2.26.0
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/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__/search.cpython-314.pyc +0 -0
- package/astra/__pycache__/tasks.cpython-314.pyc +0 -0
- package/astra/chat.py +82 -21
- package/astra/cli.py +785 -514
- package/astra/core/__pycache__/agent.cpython-314.pyc +0 -0
- package/astra/core/__pycache__/config.cpython-314.pyc +0 -0
- package/astra/core/agent.py +162 -115
- package/astra/core/config.py +25 -9
- package/astra/core/memory.py +87 -82
- package/astra/llm/__pycache__/providers.cpython-314.pyc +0 -0
- package/astra/llm/providers.py +134 -20
- package/astra/prompts.py +79 -54
- package/package.json +2 -2
package/astra/cli.py
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
AstraAgent CLI - Command Line Interface for the Autonomous Agent.
|
|
3
3
|
"""
|
|
4
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
|
|
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
15
|
|
|
16
16
|
from astra.core.agent import AstraAgent
|
|
17
17
|
from astra.core.config import AgentConfig, ExecutionMode, LogLevel
|
|
@@ -54,21 +54,21 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
54
54
|
default="autonomous", help="Execution mode")
|
|
55
55
|
run_parser.add_argument("--model", default=None, help="LLM model to use")
|
|
56
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")
|
|
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
72
|
|
|
73
73
|
# Config command
|
|
74
74
|
config_parser = subparsers.add_parser("config", help="Manage configuration")
|
|
@@ -116,37 +116,37 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
116
116
|
models_parser.add_argument("provider", help="Provider name (openai, gemini, etc.)")
|
|
117
117
|
models_parser.add_argument("--info", action="store_true", help="Show provider info")
|
|
118
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")
|
|
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
142
|
|
|
143
143
|
# Updater command
|
|
144
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)")
|
|
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
150
|
|
|
151
151
|
# Version
|
|
152
152
|
parser.add_argument("--version", action="version", version="AstraAgent 1.0.0")
|
|
@@ -196,21 +196,182 @@ def build_config(args) -> AgentConfig:
|
|
|
196
196
|
config.max_iterations = args.max_iterations
|
|
197
197
|
|
|
198
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
|
|
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
204
|
|
|
205
205
|
return config
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
def show_interactive_help():
|
|
209
|
+
"""Show help for interactive CLI mode."""
|
|
210
|
+
print("\n" + "="*50)
|
|
211
|
+
print("⌨️ ASTRA INTERACTIVE COMMANDS")
|
|
212
|
+
print("="*50)
|
|
213
|
+
print("""
|
|
214
|
+
GENERAL COMMANDS:
|
|
215
|
+
help or /h Show this help
|
|
216
|
+
settings or /s Open interactive settings
|
|
217
|
+
status Show current agent status
|
|
218
|
+
tools List enabled tools
|
|
219
|
+
exit or q Exit the agent
|
|
220
|
+
|
|
221
|
+
SLASH COMMANDS (Direct execution):
|
|
222
|
+
/open <path> Open file/app
|
|
223
|
+
/folder <path> Open folder in explorer
|
|
224
|
+
/ls [path] List directory contents
|
|
225
|
+
/delete <path> Delete file
|
|
226
|
+
/mkdir <path> Create folder
|
|
227
|
+
/info <path> Get file/folder info
|
|
228
|
+
|
|
229
|
+
Any other text will be handled by the AI agent.
|
|
230
|
+
""")
|
|
231
|
+
print("="*50 + "\n")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def handle_interactive_settings(agent, config):
|
|
236
|
+
"""Handle interactive settings menu."""
|
|
237
|
+
from astra.chat import APIKeyManager
|
|
238
|
+
from astra.llm.providers import PROVIDERS
|
|
239
|
+
|
|
240
|
+
while True:
|
|
241
|
+
print("\n[⚙️] AstraAgent Settings & Configuration")
|
|
242
|
+
print("-" * 60)
|
|
243
|
+
print(f"Current Provider: {config.llm.provider}")
|
|
244
|
+
print(f"Current Model: {config.llm.model}")
|
|
245
|
+
print("-" * 60)
|
|
246
|
+
|
|
247
|
+
# List all providers and their status
|
|
248
|
+
print(f"{' Provider':<20} {'Status':<15} {'API KEY'}")
|
|
249
|
+
all_keys = APIKeyManager.get_all_keys()
|
|
250
|
+
|
|
251
|
+
for p_key, p_data in PROVIDERS.items():
|
|
252
|
+
is_current = "--> " if p_key == config.llm.provider else " "
|
|
253
|
+
key_status = all_keys.get(p_key, "NOT SET")
|
|
254
|
+
status_icon = "✅ Ready" if key_status != "NOT SET" or p_data.get("free") else "❌ No Key"
|
|
255
|
+
|
|
256
|
+
print(f"{is_current}{p_key:<16} {status_icon:<15} {key_status}")
|
|
257
|
+
|
|
258
|
+
print("-" * 60)
|
|
259
|
+
print("Commands:")
|
|
260
|
+
print(" use <provider> Switch provider (e.g., 'use openai', 'use gemini')")
|
|
261
|
+
print(" model <name> Set model for current provider")
|
|
262
|
+
print(" key <provider> <key> Set API key (e.g., 'key openai sk-...')")
|
|
263
|
+
print(" view-env View .env file")
|
|
264
|
+
print(" back Return to main menu")
|
|
265
|
+
print("-" * 60)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
cmd_input = input("Settings > ").strip()
|
|
269
|
+
except (EOFError, KeyboardInterrupt):
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
if not cmd_input or cmd_input.lower() in ['back', 'exit', 'q']:
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
parts = cmd_input.split(' ')
|
|
276
|
+
cmd = parts[0].lower()
|
|
277
|
+
args = parts[1:]
|
|
278
|
+
|
|
279
|
+
if cmd == 'use':
|
|
280
|
+
if not args:
|
|
281
|
+
print("❌ Usage: use <provider>")
|
|
282
|
+
continue
|
|
283
|
+
new_provider = args[0].lower()
|
|
284
|
+
if new_provider == 'google':
|
|
285
|
+
new_provider = 'gemini'
|
|
286
|
+
|
|
287
|
+
if new_provider not in PROVIDERS:
|
|
288
|
+
print(f"❌ Unknown provider: {new_provider}")
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
config.llm.provider = new_provider
|
|
292
|
+
# Set default model for this provider if available
|
|
293
|
+
defaults = PROVIDERS[new_provider].get("models", [])
|
|
294
|
+
if defaults:
|
|
295
|
+
config.llm.model = defaults[0]
|
|
296
|
+
|
|
297
|
+
if agent:
|
|
298
|
+
agent.config.llm.provider = new_provider
|
|
299
|
+
agent.config.llm.model = config.llm.model
|
|
300
|
+
|
|
301
|
+
# Update API Key for the new provider
|
|
302
|
+
env_keys = PROVIDERS[new_provider].get("env_keys", [])
|
|
303
|
+
if env_keys:
|
|
304
|
+
env_var = env_keys[0]
|
|
305
|
+
new_key = os.getenv(env_var)
|
|
306
|
+
if new_key:
|
|
307
|
+
agent.config.llm.api_key = new_key
|
|
308
|
+
config.llm.api_key = new_key
|
|
309
|
+
else:
|
|
310
|
+
print(f"⚠️ No API key found for {new_provider} (Expected {env_var})")
|
|
311
|
+
agent.config.llm.api_key = ""
|
|
312
|
+
config.llm.api_key = ""
|
|
313
|
+
|
|
314
|
+
agent.llm = None # Force re-init with new config
|
|
315
|
+
|
|
316
|
+
print(f"✅ Switched to {new_provider} (Model: {config.llm.model})")
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
if cmd == 'key':
|
|
320
|
+
if len(args) < 2:
|
|
321
|
+
print("❌ Usage: key <provider> <your-api-key>")
|
|
322
|
+
continue
|
|
323
|
+
p_key = args[0].lower()
|
|
324
|
+
if p_key == 'google':
|
|
325
|
+
p_key = 'gemini'
|
|
326
|
+
|
|
327
|
+
if p_key not in PROVIDERS:
|
|
328
|
+
print(f"❌ Unknown provider: {p_key}")
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
if APIKeyManager.set_key(p_key, api_key):
|
|
333
|
+
# If we just set the key for the current or new provider
|
|
334
|
+
if p_key == config.llm.provider:
|
|
335
|
+
config.llm.api_key = api_key
|
|
336
|
+
if agent:
|
|
337
|
+
agent.config.llm.api_key = api_key
|
|
338
|
+
agent.llm = None
|
|
339
|
+
print(f"✅ Key updated for {p_key}")
|
|
340
|
+
except Exception as e:
|
|
341
|
+
print(f"❌ Error setting key: {e}")
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
if cmd == 'model':
|
|
345
|
+
if not args:
|
|
346
|
+
print("❌ Usage: model <model_name>")
|
|
347
|
+
continue
|
|
348
|
+
new_model = args[0]
|
|
349
|
+
config.llm.model = new_model
|
|
350
|
+
if agent:
|
|
351
|
+
agent.config.llm.model = new_model
|
|
352
|
+
agent.llm = None
|
|
353
|
+
print(f"✅ Model set to {new_model}")
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
if cmd == 'view-env':
|
|
357
|
+
env_file = os.path.join(os.getcwd(), ".env")
|
|
358
|
+
if os.path.exists(env_file):
|
|
359
|
+
print("\n📄 .env file contents:\n")
|
|
360
|
+
with open(env_file, 'r') as ef:
|
|
361
|
+
print(ef.read())
|
|
362
|
+
else:
|
|
363
|
+
print("❌ .env file not found")
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
print("Unknown settings command. Type 'back' to exit settings.")
|
|
367
|
+
|
|
368
|
+
|
|
208
369
|
def cli_interactive_session(config: AgentConfig):
|
|
209
370
|
"""Interactive CLI session - text-based."""
|
|
210
371
|
import asyncio as aio
|
|
211
372
|
|
|
212
373
|
print("\n[*] AstraAgent Interactive Mode")
|
|
213
|
-
print("Type your goals/commands. Type 'exit' to quit.\n")
|
|
374
|
+
print("Type your goals/commands. Type 'exit' to quit. Type 'help' for more.\n")
|
|
214
375
|
|
|
215
376
|
agent = AstraAgent(config)
|
|
216
377
|
|
|
@@ -227,18 +388,40 @@ def cli_interactive_session(config: AgentConfig):
|
|
|
227
388
|
if not user_input:
|
|
228
389
|
continue
|
|
229
390
|
|
|
230
|
-
|
|
391
|
+
# Handle shortcuts and commands
|
|
392
|
+
lowered = user_input.lower()
|
|
393
|
+
|
|
394
|
+
if lowered in ['exit', 'quit', 'q']:
|
|
231
395
|
print("Goodbye!")
|
|
232
396
|
break
|
|
233
397
|
|
|
234
|
-
if
|
|
398
|
+
if lowered in ['help', 'h', '?', '/help']:
|
|
399
|
+
show_interactive_help()
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if lowered in ['settings', 's', '/settings', '/s', 'config']:
|
|
403
|
+
handle_interactive_settings(agent, config)
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
if lowered == 'status':
|
|
235
407
|
try:
|
|
236
408
|
print(agent.state.get_summary())
|
|
237
409
|
except:
|
|
238
410
|
print("No status available")
|
|
239
411
|
continue
|
|
240
412
|
|
|
241
|
-
if
|
|
413
|
+
if lowered in ['/engineer', 'engineer', 'dev', 'continuous']:
|
|
414
|
+
config.max_iterations = 1000
|
|
415
|
+
config.mode = ExecutionMode.AUTONOMOUS
|
|
416
|
+
config.prompt_mode = "engineer"
|
|
417
|
+
# Modifying agent's config in place:
|
|
418
|
+
agent.config.max_iterations = 1000
|
|
419
|
+
agent.config.prompt_mode = "engineer"
|
|
420
|
+
print("\n👷 Engineer Mode ACTIVATED (24/7 autonomous loop, max 1000 steps)")
|
|
421
|
+
print("Type your high-level goal (e.g., 'Refactor the entire backend').")
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
if lowered == 'tools':
|
|
242
425
|
try:
|
|
243
426
|
tools = agent.tools.list_enabled()
|
|
244
427
|
print(f"Available tools: {', '.join(tools)}")
|
|
@@ -246,14 +429,102 @@ def cli_interactive_session(config: AgentConfig):
|
|
|
246
429
|
print("Could not list tools")
|
|
247
430
|
continue
|
|
248
431
|
|
|
249
|
-
|
|
432
|
+
# Handle slash commands (direct filesystem/task operations)
|
|
433
|
+
if user_input.startswith('/'):
|
|
434
|
+
try:
|
|
435
|
+
from astra.tasks import TaskExecutor, format_task_result
|
|
436
|
+
parts = user_input[1:].split(' ', 1)
|
|
437
|
+
cmd = parts[0].lower()
|
|
438
|
+
arg = parts[1].strip() if len(parts) > 1 else ""
|
|
439
|
+
|
|
440
|
+
if cmd == 'open':
|
|
441
|
+
print(format_task_result(TaskExecutor.open_file(arg)))
|
|
442
|
+
continue
|
|
443
|
+
elif cmd == 'folder':
|
|
444
|
+
print(format_task_result(TaskExecutor.open_folder(arg)))
|
|
445
|
+
continue
|
|
446
|
+
elif cmd in ['delete', 'rm']:
|
|
447
|
+
if not arg:
|
|
448
|
+
print("❌ Usage: /delete <path>")
|
|
449
|
+
else:
|
|
450
|
+
confirm = input(f"⚠️ Delete '{arg}'? (y/N): ").strip().lower()
|
|
451
|
+
if confirm == 'y':
|
|
452
|
+
print(format_task_result(TaskExecutor.delete_file(arg)))
|
|
453
|
+
continue
|
|
454
|
+
elif cmd == 'mkdir':
|
|
455
|
+
print(format_task_result(TaskExecutor.create_folder(arg)))
|
|
456
|
+
continue
|
|
457
|
+
elif cmd in ['ls', 'dir', 'list']:
|
|
458
|
+
print(format_task_result(TaskExecutor.list_directory(arg or ".")))
|
|
459
|
+
continue
|
|
460
|
+
elif cmd == 'info':
|
|
461
|
+
print(format_task_result(TaskExecutor.get_file_info(arg)))
|
|
462
|
+
continue
|
|
463
|
+
except Exception as e:
|
|
464
|
+
print(f"❌ Error executing slash command: {e}")
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
# Warn about missing API key for non-slash commands
|
|
468
|
+
if not config.llm.api_key:
|
|
469
|
+
print("\n⚠️ LOCAL_API_KEY is not set. Autonomous mode will fail.")
|
|
470
|
+
print("Type 'settings' to configure your API key or use slash commands (e.g., /open).\n")
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
# Enhanced Thinking Animation
|
|
474
|
+
from astra.chat import ThinkingIndicator
|
|
475
|
+
|
|
476
|
+
# Run the agent with animation
|
|
250
477
|
try:
|
|
251
|
-
|
|
478
|
+
# We need to run the async generator or function in a loop
|
|
479
|
+
# Since agent.run returns a string summary, we wrap it
|
|
480
|
+
async def run_with_animation():
|
|
481
|
+
async with ThinkingIndicator(f"Working on '{user_input[:20]}...'"):
|
|
482
|
+
return await agent.run(user_input)
|
|
483
|
+
|
|
484
|
+
result = aio.run(run_with_animation())
|
|
485
|
+
|
|
252
486
|
print(f"\n{result}\n")
|
|
487
|
+
|
|
488
|
+
# Save session to user logs
|
|
489
|
+
from pathlib import Path
|
|
490
|
+
from datetime import datetime
|
|
491
|
+
import json
|
|
492
|
+
|
|
493
|
+
log_dir = Path.home() / ".astra" / "logs" / "agent_sessions"
|
|
494
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
495
|
+
|
|
496
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
497
|
+
log_file = log_dir / f"session_{timestamp}.json"
|
|
498
|
+
|
|
499
|
+
session_data = {
|
|
500
|
+
"goal": user_input,
|
|
501
|
+
"result": result,
|
|
502
|
+
"provider": config.llm.provider,
|
|
503
|
+
"model": config.llm.model,
|
|
504
|
+
"summary": agent.state.get_summary()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
with open(log_file, "w", encoding="utf-8") as f:
|
|
508
|
+
json.dump(session_data, f, indent=2)
|
|
509
|
+
|
|
510
|
+
except KeyboardInterrupt:
|
|
511
|
+
print("\n🛑 Action stopped by user.")
|
|
512
|
+
# Reset state on manual stop
|
|
513
|
+
agent.state = agent.state.__class__()
|
|
514
|
+
agent.messages = []
|
|
515
|
+
continue
|
|
516
|
+
|
|
253
517
|
except ValueError as e:
|
|
254
518
|
print(f"❌ Configuration Error: {e}\n")
|
|
519
|
+
except RuntimeError as e:
|
|
520
|
+
# Handle rate limits specifically if propagated
|
|
521
|
+
err_msg = str(e)
|
|
522
|
+
if "429" in err_msg or "rate limit" in err_msg.lower():
|
|
523
|
+
print(f"\n⚠️ Rate limit hit. Please try again in a moment.\n")
|
|
524
|
+
else:
|
|
525
|
+
print(f"\n❌ Runtime Error: {e}\n")
|
|
255
526
|
except Exception as e:
|
|
256
|
-
print(f"❌ Error: {e}\n")
|
|
527
|
+
print(f"\n❌ Error: {e}\n")
|
|
257
528
|
|
|
258
529
|
# Reset for next goal
|
|
259
530
|
agent.state = agent.state.__class__()
|
|
@@ -301,7 +572,7 @@ async def run_agent(goal: str, config: AgentConfig) -> str:
|
|
|
301
572
|
agent.shutdown()
|
|
302
573
|
|
|
303
574
|
|
|
304
|
-
def show_tools(tool_name: Optional[str] = None):
|
|
575
|
+
def show_tools(tool_name: Optional[str] = None):
|
|
305
576
|
"""Show available tools."""
|
|
306
577
|
from astra.tools.base import create_default_registry
|
|
307
578
|
|
|
@@ -323,404 +594,404 @@ def show_tools(tool_name: Optional[str] = None):
|
|
|
323
594
|
print(f"Tool not found: {tool_name}")
|
|
324
595
|
else:
|
|
325
596
|
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():
|
|
597
|
+
for name in registry.list_enabled():
|
|
598
|
+
tool = registry.get(name)
|
|
599
|
+
print(f" - {name}: {tool.description[:60]}...")
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
FILE_TYPE_EXTENSIONS: Dict[str, set] = {
|
|
603
|
+
"code": {".py", ".js", ".ts", ".java", ".cpp", ".c", ".cs", ".go", ".rs", ".php", ".rb", ".swift", ".kt"},
|
|
604
|
+
"image": {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg", ".ico"},
|
|
605
|
+
"document": {".txt", ".md", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".rtf", ".csv"},
|
|
606
|
+
"archive": {".zip", ".rar", ".7z", ".tar", ".gz"},
|
|
607
|
+
"media": {".mp3", ".wav", ".mp4", ".mkv", ".avi", ".mov"},
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _format_size(size_bytes: int) -> str:
|
|
612
|
+
"""Format file size in a human-readable form."""
|
|
613
|
+
size = float(size_bytes)
|
|
614
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
615
|
+
if size < 1024:
|
|
616
|
+
return f"{size:.1f}{unit}"
|
|
617
|
+
size /= 1024
|
|
618
|
+
return f"{size:.1f}PB"
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _resolve_search_roots(root: Optional[str]) -> List[Path]:
|
|
622
|
+
"""Resolve search roots. Defaults to full device roots."""
|
|
623
|
+
if root:
|
|
624
|
+
root_path = Path(root).resolve()
|
|
625
|
+
return [root_path] if root_path.exists() else []
|
|
626
|
+
|
|
627
|
+
if os.name == "nt":
|
|
628
|
+
roots: List[Path] = []
|
|
629
|
+
for drive_letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
|
630
|
+
drive = Path(f"{drive_letter}:\\")
|
|
631
|
+
if drive.exists():
|
|
632
|
+
roots.append(drive)
|
|
633
|
+
return roots
|
|
634
|
+
|
|
635
|
+
return [Path("/")]
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _is_hidden(path: Path) -> bool:
|
|
639
|
+
"""Best-effort hidden file detection."""
|
|
640
|
+
if path.name.startswith("."):
|
|
641
|
+
return True
|
|
642
|
+
if os.name != "nt":
|
|
643
|
+
return False
|
|
644
|
+
try:
|
|
645
|
+
import stat
|
|
646
|
+
attrs = path.stat().st_file_attributes
|
|
647
|
+
return bool(attrs & stat.FILE_ATTRIBUTE_HIDDEN)
|
|
648
|
+
except Exception:
|
|
649
|
+
return False
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _iter_files(root: Path, max_depth: int = -1, show_hidden: bool = False):
|
|
653
|
+
"""Iterate files with optional depth limit."""
|
|
654
|
+
stack: List[Tuple[Path, int]] = [(root, 0)]
|
|
655
|
+
|
|
656
|
+
while stack:
|
|
657
|
+
current, depth = stack.pop()
|
|
658
|
+
if not current.exists():
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
try:
|
|
662
|
+
entries = list(current.iterdir())
|
|
663
|
+
except (PermissionError, OSError):
|
|
664
|
+
continue
|
|
665
|
+
|
|
666
|
+
for entry in entries:
|
|
667
|
+
if not show_hidden and _is_hidden(entry):
|
|
668
|
+
continue
|
|
669
|
+
|
|
670
|
+
if entry.is_dir():
|
|
671
|
+
if max_depth < 0 or depth < max_depth:
|
|
672
|
+
stack.append((entry, depth + 1))
|
|
673
|
+
elif entry.is_file():
|
|
674
|
+
yield entry
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _query_matches(path: Path, query: Optional[str]) -> bool:
|
|
678
|
+
"""Match filename against substring or wildcard query."""
|
|
679
|
+
if not query:
|
|
680
|
+
return True
|
|
681
|
+
name = path.name
|
|
682
|
+
if any(ch in query for ch in "*?[]"):
|
|
683
|
+
return fnmatch.fnmatch(name.lower(), query.lower())
|
|
684
|
+
return query.lower() in name.lower()
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _content_matches(path: Path, content_query: Optional[str]) -> bool:
|
|
688
|
+
"""Search content in text-like files with safe limits."""
|
|
689
|
+
if not content_query:
|
|
690
|
+
return True
|
|
691
|
+
try:
|
|
692
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
693
|
+
chunk = f.read(1024 * 1024)
|
|
694
|
+
return content_query.lower() in chunk.lower()
|
|
695
|
+
except Exception:
|
|
696
|
+
return False
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def run_device_search(args) -> int:
|
|
700
|
+
"""Run filesystem-wide search from CLI command arguments."""
|
|
701
|
+
if not args.query and not args.ext and not args.type and not args.content and not args.recent:
|
|
702
|
+
print("Provide at least one filter: query, --ext, --type, --content, or --recent")
|
|
703
|
+
return 1
|
|
704
|
+
|
|
705
|
+
roots = _resolve_search_roots(args.root)
|
|
706
|
+
if not roots:
|
|
707
|
+
print(f"No valid search root found for: {args.root}")
|
|
708
|
+
return 1
|
|
709
|
+
|
|
710
|
+
ext_filter = args.ext.lower() if args.ext else None
|
|
711
|
+
if ext_filter and not ext_filter.startswith("."):
|
|
712
|
+
ext_filter = f".{ext_filter}"
|
|
713
|
+
|
|
714
|
+
type_filter = args.type.lower() if args.type else None
|
|
715
|
+
if type_filter and type_filter not in FILE_TYPE_EXTENSIONS:
|
|
716
|
+
valid = ", ".join(sorted(FILE_TYPE_EXTENSIONS.keys()))
|
|
717
|
+
print(f"Invalid --type '{args.type}'. Valid types: {valid}")
|
|
718
|
+
return 1
|
|
719
|
+
|
|
720
|
+
recent_cutoff = None
|
|
721
|
+
if args.recent:
|
|
722
|
+
recent_cutoff = datetime.now() - timedelta(days=max(1, args.days))
|
|
723
|
+
|
|
724
|
+
results: List[Path] = []
|
|
725
|
+
scanned_files = 0
|
|
726
|
+
|
|
727
|
+
for root in roots:
|
|
728
|
+
for file_path in _iter_files(root, max_depth=args.max_depth, show_hidden=args.show_hidden):
|
|
729
|
+
scanned_files += 1
|
|
730
|
+
|
|
731
|
+
if not _query_matches(file_path, args.query):
|
|
732
|
+
continue
|
|
733
|
+
if ext_filter and file_path.suffix.lower() != ext_filter:
|
|
734
|
+
continue
|
|
735
|
+
if type_filter and file_path.suffix.lower() not in FILE_TYPE_EXTENSIONS[type_filter]:
|
|
736
|
+
continue
|
|
737
|
+
if recent_cutoff:
|
|
738
|
+
try:
|
|
739
|
+
if datetime.fromtimestamp(file_path.stat().st_mtime) < recent_cutoff:
|
|
740
|
+
continue
|
|
741
|
+
except (PermissionError, OSError):
|
|
742
|
+
continue
|
|
743
|
+
if not _content_matches(file_path, args.content):
|
|
744
|
+
continue
|
|
745
|
+
|
|
746
|
+
results.append(file_path)
|
|
747
|
+
if len(results) >= args.limit:
|
|
748
|
+
break
|
|
749
|
+
|
|
750
|
+
if len(results) >= args.limit:
|
|
751
|
+
break
|
|
752
|
+
|
|
753
|
+
if not results:
|
|
754
|
+
print(f"No matching files found. Scanned {scanned_files} files.")
|
|
755
|
+
return 0
|
|
756
|
+
|
|
757
|
+
print(f"\nFound {len(results)} file(s) (scanned {scanned_files} files):\n")
|
|
758
|
+
for idx, file_path in enumerate(results, start=1):
|
|
759
|
+
try:
|
|
760
|
+
stat = file_path.stat()
|
|
761
|
+
modified = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
762
|
+
size = _format_size(stat.st_size)
|
|
763
|
+
print(f"{idx:3}. {file_path} [{size}, {modified}]")
|
|
764
|
+
except (PermissionError, OSError):
|
|
765
|
+
print(f"{idx:3}. {file_path}")
|
|
766
|
+
|
|
767
|
+
if len(results) == args.limit:
|
|
768
|
+
print(f"\nResult limit reached ({args.limit}). Increase with --limit.")
|
|
769
|
+
|
|
770
|
+
return 0
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _execute_task(action: str, src: Optional[str], dest: Optional[str], overwrite: bool,
|
|
774
|
+
recursive: bool, assume_yes: bool) -> Tuple[bool, str]:
|
|
775
|
+
"""Execute one filesystem task."""
|
|
776
|
+
action = action.lower().strip()
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
if action == "mkdir":
|
|
780
|
+
if not src:
|
|
781
|
+
return False, "mkdir requires src path"
|
|
782
|
+
Path(src).mkdir(parents=True, exist_ok=True)
|
|
783
|
+
return True, f"Created directory: {Path(src).resolve()}"
|
|
784
|
+
|
|
785
|
+
if action == "info":
|
|
786
|
+
if not src:
|
|
787
|
+
return False, "info requires src path"
|
|
788
|
+
target = Path(src).resolve()
|
|
789
|
+
if not target.exists():
|
|
790
|
+
return False, f"Path not found: {target}"
|
|
791
|
+
stat = target.stat()
|
|
792
|
+
kind = "directory" if target.is_dir() else "file"
|
|
793
|
+
details = (
|
|
794
|
+
f"Path: {target}\n"
|
|
795
|
+
f"Type: {kind}\n"
|
|
796
|
+
f"Size: {_format_size(stat.st_size)}\n"
|
|
797
|
+
f"Modified: {datetime.fromtimestamp(stat.st_mtime)}"
|
|
798
|
+
)
|
|
799
|
+
return True, details
|
|
800
|
+
|
|
801
|
+
if action == "list":
|
|
802
|
+
if not src:
|
|
803
|
+
return False, "list requires src directory"
|
|
804
|
+
target = Path(src).resolve()
|
|
805
|
+
if not target.is_dir():
|
|
806
|
+
return False, f"Not a directory: {target}"
|
|
807
|
+
entries = sorted(target.rglob("*") if recursive else target.iterdir(), key=lambda p: p.name.lower())
|
|
808
|
+
entries = [p for p in entries if p != target]
|
|
809
|
+
lines = [f"Listing: {target}", f"Items: {len(entries)}"]
|
|
810
|
+
for p in entries[:500]:
|
|
811
|
+
suffix = "/" if p.is_dir() else ""
|
|
812
|
+
lines.append(f"- {p}{suffix}")
|
|
813
|
+
if len(entries) > 500:
|
|
814
|
+
lines.append(f"... and {len(entries) - 500} more")
|
|
815
|
+
return True, "\n".join(lines)
|
|
816
|
+
|
|
817
|
+
if action == "copy":
|
|
818
|
+
if not src or not dest:
|
|
819
|
+
return False, "copy requires src and dest"
|
|
820
|
+
src_path = Path(src).resolve()
|
|
821
|
+
dest_path = Path(dest).resolve()
|
|
822
|
+
if not src_path.exists():
|
|
823
|
+
return False, f"Source not found: {src_path}"
|
|
824
|
+
if dest_path.exists() and not overwrite:
|
|
825
|
+
return False, f"Destination exists: {dest_path} (use --overwrite)"
|
|
826
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
827
|
+
if src_path.is_dir():
|
|
828
|
+
if dest_path.exists():
|
|
829
|
+
shutil.rmtree(dest_path)
|
|
830
|
+
shutil.copytree(src_path, dest_path)
|
|
831
|
+
else:
|
|
832
|
+
shutil.copy2(src_path, dest_path)
|
|
833
|
+
return True, f"Copied {src_path} -> {dest_path}"
|
|
834
|
+
|
|
835
|
+
if action == "move":
|
|
836
|
+
if not src or not dest:
|
|
837
|
+
return False, "move requires src and dest"
|
|
838
|
+
src_path = Path(src).resolve()
|
|
839
|
+
dest_path = Path(dest).resolve()
|
|
840
|
+
if not src_path.exists():
|
|
841
|
+
return False, f"Source not found: {src_path}"
|
|
842
|
+
if dest_path.exists() and not overwrite:
|
|
843
|
+
return False, f"Destination exists: {dest_path} (use --overwrite)"
|
|
844
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
845
|
+
shutil.move(str(src_path), str(dest_path))
|
|
846
|
+
return True, f"Moved {src_path} -> {dest_path}"
|
|
847
|
+
|
|
848
|
+
if action == "rename":
|
|
849
|
+
if not src or not dest:
|
|
850
|
+
return False, "rename requires src and dest"
|
|
851
|
+
src_path = Path(src).resolve()
|
|
852
|
+
dest_path = src_path.with_name(dest)
|
|
853
|
+
if not src_path.exists():
|
|
854
|
+
return False, f"Source not found: {src_path}"
|
|
855
|
+
if dest_path.exists() and not overwrite:
|
|
856
|
+
return False, f"Destination exists: {dest_path} (use --overwrite)"
|
|
857
|
+
src_path.rename(dest_path)
|
|
858
|
+
return True, f"Renamed {src_path.name} -> {dest_path.name}"
|
|
859
|
+
|
|
860
|
+
if action == "delete":
|
|
861
|
+
if not src:
|
|
862
|
+
return False, "delete requires src path"
|
|
863
|
+
target = Path(src).resolve()
|
|
864
|
+
if not target.exists():
|
|
865
|
+
return False, f"Path not found: {target}"
|
|
866
|
+
if not assume_yes:
|
|
867
|
+
confirm = input(f"Delete '{target}'? (y/N): ").strip().lower()
|
|
868
|
+
if confirm != "y":
|
|
869
|
+
return False, "Delete cancelled"
|
|
870
|
+
if target.is_dir():
|
|
871
|
+
if recursive:
|
|
872
|
+
shutil.rmtree(target)
|
|
873
|
+
else:
|
|
874
|
+
target.rmdir()
|
|
875
|
+
else:
|
|
876
|
+
target.unlink()
|
|
877
|
+
return True, f"Deleted: {target}"
|
|
878
|
+
|
|
879
|
+
return False, f"Unknown action: {action}"
|
|
880
|
+
|
|
881
|
+
except Exception as e:
|
|
882
|
+
return False, str(e)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _load_batch_tasks(batch_json: Optional[str], batch_file: Optional[str]) -> List[Dict[str, Any]]:
|
|
886
|
+
"""Load task list from JSON string or file."""
|
|
887
|
+
if batch_json:
|
|
888
|
+
payload = json.loads(batch_json)
|
|
889
|
+
elif batch_file:
|
|
890
|
+
payload = json.loads(Path(batch_file).read_text(encoding="utf-8-sig"))
|
|
891
|
+
else:
|
|
892
|
+
return []
|
|
893
|
+
|
|
894
|
+
if not isinstance(payload, list):
|
|
895
|
+
raise ValueError("Batch payload must be a JSON array")
|
|
896
|
+
return payload
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def run_tasks_command(args) -> int:
|
|
900
|
+
"""Run single or batch file tasks."""
|
|
901
|
+
tasks: List[Dict[str, Any]] = []
|
|
902
|
+
|
|
903
|
+
if args.batch or args.batch_file:
|
|
904
|
+
try:
|
|
905
|
+
tasks = _load_batch_tasks(args.batch, args.batch_file)
|
|
906
|
+
except Exception as e:
|
|
907
|
+
print(f"Failed to load batch tasks: {e}")
|
|
908
|
+
return 1
|
|
909
|
+
else:
|
|
910
|
+
if not args.action:
|
|
911
|
+
print("tasks requires action, or use --batch/--batch-file")
|
|
912
|
+
return 1
|
|
913
|
+
tasks = [{
|
|
914
|
+
"action": args.action,
|
|
915
|
+
"src": args.src,
|
|
916
|
+
"dest": args.dest,
|
|
917
|
+
"overwrite": args.overwrite,
|
|
918
|
+
"recursive": args.recursive,
|
|
919
|
+
"yes": args.yes,
|
|
920
|
+
}]
|
|
921
|
+
|
|
922
|
+
success_count = 0
|
|
923
|
+
for idx, task in enumerate(tasks, start=1):
|
|
924
|
+
action = str(task.get("action", "")).strip()
|
|
925
|
+
src = task.get("src")
|
|
926
|
+
dest = task.get("dest")
|
|
927
|
+
overwrite = bool(task.get("overwrite", args.overwrite))
|
|
928
|
+
recursive = bool(task.get("recursive", args.recursive))
|
|
929
|
+
assume_yes = bool(task.get("yes", args.yes))
|
|
930
|
+
|
|
931
|
+
ok, message = _execute_task(action, src, dest, overwrite, recursive, assume_yes)
|
|
932
|
+
status = "OK" if ok else "FAIL"
|
|
933
|
+
print(f"[{idx}/{len(tasks)}] {status} {action}: {message}")
|
|
934
|
+
if ok:
|
|
935
|
+
success_count += 1
|
|
936
|
+
|
|
937
|
+
print(f"\nTask summary: {success_count}/{len(tasks)} succeeded")
|
|
938
|
+
return 0 if success_count == len(tasks) else 1
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def run_update_command(args) -> int:
|
|
942
|
+
"""Check and/or install updates using updater module."""
|
|
943
|
+
def _safe_print(text: str):
|
|
944
|
+
try:
|
|
945
|
+
print(text)
|
|
946
|
+
except UnicodeEncodeError:
|
|
947
|
+
print(text.encode("ascii", "replace").decode("ascii"))
|
|
948
|
+
|
|
949
|
+
try:
|
|
950
|
+
from astra.updater import UpdateManager, format_update_info
|
|
951
|
+
except Exception as e:
|
|
952
|
+
print(f"Update module unavailable: {e}")
|
|
953
|
+
return 1
|
|
954
|
+
|
|
955
|
+
manager = UpdateManager()
|
|
956
|
+
|
|
957
|
+
# Default behavior: check for updates if no install option chosen.
|
|
958
|
+
if args.auto or args.check or not any([args.npm, args.pip, args.github, args.auto]):
|
|
959
|
+
info = manager.check_for_updates(force=True)
|
|
960
|
+
_safe_print(format_update_info(info))
|
|
961
|
+
|
|
962
|
+
if not args.auto and not any([args.npm, args.pip, args.github]):
|
|
963
|
+
return 0
|
|
964
|
+
|
|
965
|
+
# Auto mode only proceeds when update exists
|
|
966
|
+
if args.auto and not info.get("update_available"):
|
|
967
|
+
return 0
|
|
968
|
+
|
|
969
|
+
source = None
|
|
970
|
+
if args.npm:
|
|
971
|
+
source = "npm"
|
|
972
|
+
elif args.pip:
|
|
973
|
+
source = "pip"
|
|
974
|
+
elif args.github:
|
|
975
|
+
source = "github"
|
|
976
|
+
elif args.auto:
|
|
977
|
+
# Prefer npm when available, fallback to pip.
|
|
978
|
+
source = "npm" if manager.get_version_info().get("npm_available") else "pip"
|
|
979
|
+
|
|
980
|
+
if not source:
|
|
981
|
+
return 0
|
|
982
|
+
|
|
983
|
+
result = manager.download_update(source=source)
|
|
984
|
+
if result.get("success"):
|
|
985
|
+
_safe_print(result.get("message", "Update completed"))
|
|
986
|
+
return 0
|
|
987
|
+
|
|
988
|
+
_safe_print(result.get("message", "Update failed"))
|
|
989
|
+
if result.get("error"):
|
|
990
|
+
_safe_print(result["error"])
|
|
991
|
+
return 1
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def main():
|
|
724
995
|
"""Main entry point."""
|
|
725
996
|
parser = create_parser()
|
|
726
997
|
args = parser.parse_args()
|
|
@@ -730,18 +1001,18 @@ def main():
|
|
|
730
1001
|
parser.print_help()
|
|
731
1002
|
return
|
|
732
1003
|
|
|
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)
|
|
1004
|
+
if args.command in ("run", "start"):
|
|
1005
|
+
print_banner()
|
|
1006
|
+
|
|
1007
|
+
if args.interactive or not getattr(args, "goal", None):
|
|
1008
|
+
config = build_config(args)
|
|
1009
|
+
cli_interactive_session(config)
|
|
1010
|
+
else:
|
|
1011
|
+
config = build_config(args)
|
|
1012
|
+
result = asyncio.run(run_agent(args.goal, config))
|
|
1013
|
+
# Only show result if it's not empty and not a verbose success message
|
|
1014
|
+
if result and not result.startswith("==="):
|
|
1015
|
+
print(result)
|
|
745
1016
|
|
|
746
1017
|
elif args.command == "tools":
|
|
747
1018
|
show_tools(args.info if hasattr(args, 'info') else None)
|
|
@@ -758,44 +1029,44 @@ def main():
|
|
|
758
1029
|
config = AgentConfig.load(args.load)
|
|
759
1030
|
print(f"Config loaded from {args.load}")
|
|
760
1031
|
|
|
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
|
|
1032
|
+
elif args.command == "memory":
|
|
1033
|
+
from astra.core.memory import MemoryManager
|
|
1034
|
+
memory = MemoryManager(persistence_path="./.astra_memory")
|
|
1035
|
+
|
|
1036
|
+
if args.list:
|
|
1037
|
+
if hasattr(memory, "get_recent"):
|
|
1038
|
+
recent = memory.get_recent(10)
|
|
1039
|
+
else:
|
|
1040
|
+
recent = memory.get_recent_conversations(10)
|
|
1041
|
+
if recent:
|
|
1042
|
+
for item in recent:
|
|
1043
|
+
print(f"[{item.memory_type}] {item.content[:100]}...")
|
|
1044
|
+
else:
|
|
1045
|
+
print("No memories found")
|
|
1046
|
+
elif args.search:
|
|
1047
|
+
results = memory.search(query=args.search)
|
|
1048
|
+
for item in results:
|
|
1049
|
+
print(f"[{item.id}] {item.content[:100]}...")
|
|
1050
|
+
|
|
1051
|
+
elif args.command == "search":
|
|
1052
|
+
exit_code = run_device_search(args)
|
|
1053
|
+
if exit_code != 0:
|
|
1054
|
+
sys.exit(exit_code)
|
|
1055
|
+
|
|
1056
|
+
elif args.command == "tasks":
|
|
1057
|
+
exit_code = run_tasks_command(args)
|
|
1058
|
+
if exit_code != 0:
|
|
1059
|
+
sys.exit(exit_code)
|
|
1060
|
+
|
|
1061
|
+
elif args.command == "update":
|
|
1062
|
+
exit_code = run_update_command(args)
|
|
1063
|
+
if exit_code != 0:
|
|
1064
|
+
sys.exit(exit_code)
|
|
1065
|
+
|
|
1066
|
+
elif args.command == "chat":
|
|
1067
|
+
# Interactive chat mode with multiple providers
|
|
1068
|
+
from astra.chat import start_chat_mode
|
|
1069
|
+
from astra.llm import PROVIDERS
|
|
799
1070
|
|
|
800
1071
|
# Show available providers if requested
|
|
801
1072
|
if args.list_providers:
|