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/search.py
ADDED
|
@@ -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
|