astraagent 2.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.env.template +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +333 -0
  4. package/astra/__init__.py +15 -0
  5. package/astra/__pycache__/__init__.cpython-314.pyc +0 -0
  6. package/astra/__pycache__/chat.cpython-314.pyc +0 -0
  7. package/astra/__pycache__/cli.cpython-314.pyc +0 -0
  8. package/astra/__pycache__/prompts.cpython-314.pyc +0 -0
  9. package/astra/__pycache__/updater.cpython-314.pyc +0 -0
  10. package/astra/chat.py +763 -0
  11. package/astra/cli.py +913 -0
  12. package/astra/core/__init__.py +8 -0
  13. package/astra/core/__pycache__/__init__.cpython-314.pyc +0 -0
  14. package/astra/core/__pycache__/agent.cpython-314.pyc +0 -0
  15. package/astra/core/__pycache__/config.cpython-314.pyc +0 -0
  16. package/astra/core/__pycache__/memory.cpython-314.pyc +0 -0
  17. package/astra/core/__pycache__/reasoning.cpython-314.pyc +0 -0
  18. package/astra/core/__pycache__/state.cpython-314.pyc +0 -0
  19. package/astra/core/agent.py +515 -0
  20. package/astra/core/config.py +247 -0
  21. package/astra/core/memory.py +782 -0
  22. package/astra/core/reasoning.py +423 -0
  23. package/astra/core/state.py +366 -0
  24. package/astra/core/voice.py +144 -0
  25. package/astra/llm/__init__.py +32 -0
  26. package/astra/llm/__pycache__/__init__.cpython-314.pyc +0 -0
  27. package/astra/llm/__pycache__/providers.cpython-314.pyc +0 -0
  28. package/astra/llm/providers.py +530 -0
  29. package/astra/planning/__init__.py +117 -0
  30. package/astra/prompts.py +289 -0
  31. package/astra/reflection/__init__.py +181 -0
  32. package/astra/search.py +469 -0
  33. package/astra/tasks.py +466 -0
  34. package/astra/tools/__init__.py +17 -0
  35. package/astra/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  36. package/astra/tools/__pycache__/advanced.cpython-314.pyc +0 -0
  37. package/astra/tools/__pycache__/base.cpython-314.pyc +0 -0
  38. package/astra/tools/__pycache__/browser.cpython-314.pyc +0 -0
  39. package/astra/tools/__pycache__/file.cpython-314.pyc +0 -0
  40. package/astra/tools/__pycache__/git.cpython-314.pyc +0 -0
  41. package/astra/tools/__pycache__/memory_tool.cpython-314.pyc +0 -0
  42. package/astra/tools/__pycache__/python.cpython-314.pyc +0 -0
  43. package/astra/tools/__pycache__/shell.cpython-314.pyc +0 -0
  44. package/astra/tools/__pycache__/web.cpython-314.pyc +0 -0
  45. package/astra/tools/__pycache__/windows.cpython-314.pyc +0 -0
  46. package/astra/tools/advanced.py +251 -0
  47. package/astra/tools/base.py +344 -0
  48. package/astra/tools/browser.py +93 -0
  49. package/astra/tools/file.py +476 -0
  50. package/astra/tools/git.py +74 -0
  51. package/astra/tools/memory_tool.py +89 -0
  52. package/astra/tools/python.py +238 -0
  53. package/astra/tools/shell.py +183 -0
  54. package/astra/tools/web.py +804 -0
  55. package/astra/tools/windows.py +542 -0
  56. package/astra/updater.py +450 -0
  57. package/astra/utils/__init__.py +230 -0
  58. package/bin/astraagent.js +73 -0
  59. package/bin/postinstall.js +25 -0
  60. package/config.json.template +52 -0
  61. package/main.py +16 -0
  62. package/package.json +51 -0
  63. package/pyproject.toml +72 -0
@@ -0,0 +1,469 @@
1
+ """Device Search and File Discovery System - Search entire device for files and directories."""
2
+
3
+ import os
4
+ import fnmatch
5
+ from pathlib import Path
6
+ from typing import List, Dict, Optional, Set
7
+ from datetime import datetime, timedelta
8
+
9
+
10
+ class DeviceSearcher:
11
+ """Search files and directories across entire device."""
12
+
13
+ # Common root paths for different OS
14
+ WINDOWS_ROOTS = ['C:\\', 'D:\\', 'E:\\', 'F:\\']
15
+ LINUX_ROOTS = ['/home', '/root', '/tmp', '/var']
16
+ MAC_ROOTS = ['/Users', '/tmp', '/var']
17
+
18
+ # Directories to skip (too slow or system dirs)
19
+ SKIP_DIRS = {
20
+ '.git', '__pycache__', '.venv', 'venv', 'node_modules',
21
+ 'System Volume Information', '$Recycle.Bin', '.cache',
22
+ 'AppData', 'ProgramData', '.npm', '.config'
23
+ }
24
+
25
+ # File extensions to search
26
+ COMMON_EXTENSIONS = {
27
+ 'document': ['.txt', '.md', '.doc', '.docx', '.pdf'],
28
+ 'code': ['.py', '.js', '.ts', '.html', '.css', '.json'],
29
+ 'image': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'],
30
+ 'video': ['.mp4', '.avi', '.mkv', '.mov', '.webm'],
31
+ 'audio': ['.mp3', '.wav', '.aac', '.flac', '.ogg'],
32
+ 'compressed': ['.zip', '.rar', '.7z', '.tar', '.gz'],
33
+ 'data': ['.csv', '.xlsx', '.xls', '.sql', '.db']
34
+ }
35
+
36
+ def __init__(self, max_results: int = 100, skip_system: bool = True):
37
+ """Initialize device searcher.
38
+
39
+ Args:
40
+ max_results: Maximum number of results to return
41
+ skip_system: Whether to skip system directories
42
+ """
43
+ self.max_results = max_results
44
+ self.skip_system = skip_system
45
+ self.paths_searched = 0
46
+ self.errors = 0
47
+
48
+ def should_skip_dir(self, dir_path: str) -> bool:
49
+ """Check if directory should be skipped."""
50
+ if not self.skip_system:
51
+ return False
52
+
53
+ dir_name = os.path.basename(dir_path)
54
+
55
+ # Skip hidden directories (on Unix)
56
+ if dir_name.startswith('.') and os.name != 'nt':
57
+ return True
58
+
59
+ # Skip known system directories
60
+ for skip in self.SKIP_DIRS:
61
+ if skip.lower() in dir_path.lower():
62
+ return True
63
+
64
+ return False
65
+
66
+ def search_by_name(self, filename: str, search_paths: Optional[List[str]] = None,
67
+ recursive: bool = True) -> List[str]:
68
+ """Search for file/directory by name (supports wildcards).
69
+
70
+ Args:
71
+ filename: Name to search (can use * wildcards)
72
+ search_paths: Paths to search in. If None, searches common locations
73
+ recursive: Whether to search recursively
74
+
75
+ Returns:
76
+ List of matching file paths
77
+ """
78
+ if search_paths is None:
79
+ search_paths = self._get_default_search_paths()
80
+
81
+ results = []
82
+ self.paths_searched = 0
83
+ self.errors = 0
84
+
85
+ for search_path in search_paths:
86
+ if not os.path.exists(search_path):
87
+ continue
88
+
89
+ if recursive:
90
+ results.extend(
91
+ self._search_recursive(search_path, filename)
92
+ )
93
+ else:
94
+ results.extend(
95
+ self._search_single_dir(search_path, filename)
96
+ )
97
+
98
+ if len(results) >= self.max_results:
99
+ break
100
+
101
+ return results[:self.max_results]
102
+
103
+ def search_by_extension(self, ext: str, search_paths: Optional[List[str]] = None,
104
+ limit: int = 50) -> List[str]:
105
+ """Search for files by extension.
106
+
107
+ Args:
108
+ ext: File extension (with or without dot)
109
+ search_paths: Paths to search
110
+ limit: Maximum results
111
+
112
+ Returns:
113
+ List of matching file paths
114
+ """
115
+ if not ext.startswith('.'):
116
+ ext = '.' + ext
117
+
118
+ pattern = f'*{ext}'
119
+ results = self.search_by_name(pattern, search_paths)
120
+ return results[:limit]
121
+
122
+ def search_by_category(self, category: str, search_paths: Optional[List[str]] = None,
123
+ limit: int = 50) -> List[str]:
124
+ """Search for files by category (document, code, image, etc).
125
+
126
+ Args:
127
+ category: File category
128
+ search_paths: Paths to search
129
+ limit: Maximum results
130
+
131
+ Returns:
132
+ List of matching file paths
133
+ """
134
+ if category not in self.COMMON_EXTENSIONS:
135
+ return []
136
+
137
+ results = []
138
+ extensions = self.COMMON_EXTENSIONS[category]
139
+
140
+ for ext in extensions:
141
+ results.extend(self.search_by_extension(ext, search_paths, limit))
142
+
143
+ return results[:limit]
144
+
145
+ def search_by_date(self, search_paths: Optional[List[str]] = None,
146
+ days_ago: int = 7) -> List[Dict]:
147
+ """Search for files modified in last N days.
148
+
149
+ Args:
150
+ search_paths: Paths to search
151
+ days_ago: Number of days to look back
152
+
153
+ Returns:
154
+ List of dicts with filepath and modification time
155
+ """
156
+ if search_paths is None:
157
+ search_paths = self._get_default_search_paths()
158
+
159
+ cutoff_time = datetime.now() - timedelta(days=days_ago)
160
+ results = []
161
+
162
+ for search_path in search_paths:
163
+ if not os.path.exists(search_path):
164
+ continue
165
+
166
+ try:
167
+ for root, dirs, files in os.walk(search_path):
168
+ if self.should_skip_dir(root):
169
+ dirs.clear() # Don't descend into skipped dirs
170
+ continue
171
+
172
+ for file in files:
173
+ filepath = os.path.join(root, file)
174
+ try:
175
+ mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
176
+ if mtime > cutoff_time:
177
+ results.append({
178
+ 'path': filepath,
179
+ 'modified': mtime.isoformat(),
180
+ 'seconds_ago': int((datetime.now() - mtime).total_seconds())
181
+ })
182
+
183
+ if len(results) >= self.max_results:
184
+ return results
185
+ except (OSError, ValueError):
186
+ self.errors += 1
187
+ continue
188
+ except Exception:
189
+ self.errors += 1
190
+ continue
191
+
192
+ return results
193
+
194
+ def search_by_size(self, search_paths: Optional[List[str]] = None,
195
+ min_size: Optional[int] = None,
196
+ max_size: Optional[int] = None,
197
+ limit: int = 50) -> List[Dict]:
198
+ """Search for files by size.
199
+
200
+ Args:
201
+ search_paths: Paths to search
202
+ min_size: Minimum file size in bytes
203
+ max_size: Maximum file size in bytes
204
+ limit: Maximum results
205
+
206
+ Returns:
207
+ List of dicts with filepath and size
208
+ """
209
+ if search_paths is None:
210
+ search_paths = self._get_default_search_paths()
211
+
212
+ results = []
213
+
214
+ for search_path in search_paths:
215
+ if not os.path.exists(search_path):
216
+ continue
217
+
218
+ try:
219
+ for root, dirs, files in os.walk(search_path):
220
+ if self.should_skip_dir(root):
221
+ dirs.clear()
222
+ continue
223
+
224
+ for file in files:
225
+ filepath = os.path.join(root, file)
226
+ try:
227
+ size = os.path.getsize(filepath)
228
+
229
+ if min_size and size < min_size:
230
+ continue
231
+ if max_size and size > max_size:
232
+ continue
233
+
234
+ results.append({
235
+ 'path': filepath,
236
+ 'size': size,
237
+ 'size_mb': round(size / 1024 / 1024, 2)
238
+ })
239
+
240
+ if len(results) >= limit:
241
+ return results
242
+ except (OSError, ValueError):
243
+ self.errors += 1
244
+ continue
245
+ except Exception:
246
+ self.errors += 1
247
+ continue
248
+
249
+ return results
250
+
251
+ def search_by_content(self, search_text: str, file_types: Optional[List[str]] = None,
252
+ search_paths: Optional[List[str]] = None,
253
+ limit: int = 30) -> List[Dict]:
254
+ """Search for text content in files.
255
+
256
+ Args:
257
+ search_text: Text to search for
258
+ file_types: File extensions to search in
259
+ search_paths: Paths to search
260
+ limit: Maximum results
261
+
262
+ Returns:
263
+ List of dicts with filepath and match count
264
+ """
265
+ if search_paths is None:
266
+ search_paths = self._get_default_search_paths()
267
+
268
+ if file_types is None:
269
+ file_types = ['.txt', '.md', '.py', '.js', '.html', '.json', '.csv']
270
+
271
+ results = []
272
+
273
+ for search_path in search_paths:
274
+ if not os.path.exists(search_path):
275
+ continue
276
+
277
+ try:
278
+ for root, dirs, files in os.walk(search_path):
279
+ if self.should_skip_dir(root):
280
+ dirs.clear()
281
+ continue
282
+
283
+ for file in files:
284
+ if not any(file.endswith(ext) for ext in file_types):
285
+ continue
286
+
287
+ filepath = os.path.join(root, file)
288
+ try:
289
+ with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
290
+ content = f.read()
291
+ count = content.lower().count(search_text.lower())
292
+
293
+ if count > 0:
294
+ results.append({
295
+ 'path': filepath,
296
+ 'matches': count,
297
+ 'file_size': len(content)
298
+ })
299
+
300
+ if len(results) >= limit:
301
+ return results
302
+ except (OSError, ValueError):
303
+ self.errors += 1
304
+ continue
305
+ except Exception:
306
+ self.errors += 1
307
+ continue
308
+
309
+ return results
310
+
311
+ def _search_recursive(self, search_path: str, pattern: str) -> List[str]:
312
+ """Recursively search directory for files matching pattern."""
313
+ results = []
314
+
315
+ try:
316
+ for root, dirs, files in os.walk(search_path):
317
+ # Remove skipped directories to prevent descending
318
+ dirs[:] = [d for d in dirs if not self.should_skip_dir(os.path.join(root, d))]
319
+
320
+ self.paths_searched += 1
321
+
322
+ # Search directories
323
+ for dir_name in dirs:
324
+ if fnmatch.fnmatch(dir_name.lower(), pattern.lower()):
325
+ results.append(os.path.join(root, dir_name))
326
+ if len(results) >= self.max_results:
327
+ return results
328
+
329
+ # Search files
330
+ for file_name in files:
331
+ if fnmatch.fnmatch(file_name.lower(), pattern.lower()):
332
+ results.append(os.path.join(root, file_name))
333
+ if len(results) >= self.max_results:
334
+ return results
335
+ except Exception:
336
+ self.errors += 1
337
+
338
+ return results
339
+
340
+ def _search_single_dir(self, search_path: str, pattern: str) -> List[str]:
341
+ """Search single directory (non-recursive) for files matching pattern."""
342
+ results = []
343
+
344
+ try:
345
+ for item in os.listdir(search_path):
346
+ if fnmatch.fnmatch(item.lower(), pattern.lower()):
347
+ results.append(os.path.join(search_path, item))
348
+ if len(results) >= self.max_results:
349
+ return results
350
+ except Exception:
351
+ self.errors += 1
352
+
353
+ return results
354
+
355
+ def _get_default_search_paths(self) -> List[str]:
356
+ """Get default search paths based on OS."""
357
+ paths = []
358
+
359
+ # Add user home directory (fastest to search)
360
+ home = Path.home()
361
+ if home.exists():
362
+ paths.append(str(home))
363
+
364
+ # Add Desktop, Documents, Downloads
365
+ for subdir in ['Desktop', 'Documents', 'Downloads', 'OneDrive']:
366
+ subpath = home / subdir
367
+ if subpath.exists():
368
+ paths.append(str(subpath))
369
+
370
+ # Add root paths based on OS
371
+ current_drive = os.path.splitdrive(os.getcwd())[0]
372
+ if current_drive:
373
+ paths.insert(0, current_drive + '\\')
374
+
375
+ return paths
376
+
377
+ def get_directory_contents(self, path: str, show_hidden: bool = False) -> Dict:
378
+ """Get contents of a directory.
379
+
380
+ Args:
381
+ path: Directory path
382
+ show_hidden: Whether to show hidden files
383
+
384
+ Returns:
385
+ Dict with directories and files
386
+ """
387
+ try:
388
+ if not os.path.isdir(path):
389
+ return {'error': f'Not a directory: {path}'}
390
+
391
+ dirs = []
392
+ files = []
393
+
394
+ for item in os.listdir(path):
395
+ item_path = os.path.join(path, item)
396
+
397
+ # Skip hidden files if requested
398
+ if not show_hidden and item.startswith('.'):
399
+ continue
400
+
401
+ if os.path.isdir(item_path):
402
+ dirs.append({
403
+ 'name': item,
404
+ 'path': item_path,
405
+ 'type': 'dir'
406
+ })
407
+ else:
408
+ try:
409
+ size = os.path.getsize(item_path)
410
+ files.append({
411
+ 'name': item,
412
+ 'path': item_path,
413
+ 'type': 'file',
414
+ 'size': size,
415
+ 'size_kb': round(size / 1024, 2)
416
+ })
417
+ except:
418
+ pass
419
+
420
+ # Sort
421
+ dirs.sort(key=lambda x: x['name'].lower())
422
+ files.sort(key=lambda x: x['name'].lower())
423
+
424
+ return {
425
+ 'path': path,
426
+ 'directories': dirs[:20], # Limit display
427
+ 'files': files[:20],
428
+ 'total_dirs': len(dirs),
429
+ 'total_files': len(files)
430
+ }
431
+ except Exception as e:
432
+ return {'error': str(e)}
433
+
434
+
435
+ def format_search_results(results: List, title: str = "Search Results") -> str:
436
+ """Format search results for display."""
437
+ if not results:
438
+ return f"āŒ No results found for: {title}"
439
+
440
+ output = f"\nšŸ” {title} ({len(results)} result{'s' if len(results) != 1 else ''}):\n"
441
+
442
+ for i, result in enumerate(results[:20], 1): # Show max 20
443
+ if isinstance(result, dict):
444
+ if 'path' in result:
445
+ path = result['path']
446
+ info = []
447
+
448
+ if 'size_mb' in result:
449
+ info.append(f"{result['size_mb']} MB")
450
+ elif 'size_kb' in result:
451
+ info.append(f"{result['size_kb']} KB")
452
+ elif 'size' in result:
453
+ info.append(f"{result['size']} bytes")
454
+
455
+ if 'matches' in result:
456
+ info.append(f"{result['matches']} matches")
457
+
458
+ if 'modified' in result:
459
+ info.append(f"modified {result['seconds_ago']}s ago")
460
+
461
+ info_str = f" [{', '.join(info)}]" if info else ""
462
+ output += f" {i}. {path}{info_str}\n"
463
+ else:
464
+ output += f" {i}. {result}\n"
465
+
466
+ if len(results) > 20:
467
+ output += f"\n ... and {len(results) - 20} more results\n"
468
+
469
+ return output