aloop 0.1.1__py3-none-any.whl

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 (66) hide show
  1. agent/__init__.py +0 -0
  2. agent/agent.py +182 -0
  3. agent/base.py +406 -0
  4. agent/context.py +126 -0
  5. agent/prompts/__init__.py +1 -0
  6. agent/todo.py +149 -0
  7. agent/tool_executor.py +54 -0
  8. agent/verification.py +135 -0
  9. aloop-0.1.1.dist-info/METADATA +252 -0
  10. aloop-0.1.1.dist-info/RECORD +66 -0
  11. aloop-0.1.1.dist-info/WHEEL +5 -0
  12. aloop-0.1.1.dist-info/entry_points.txt +2 -0
  13. aloop-0.1.1.dist-info/licenses/LICENSE +21 -0
  14. aloop-0.1.1.dist-info/top_level.txt +9 -0
  15. cli.py +19 -0
  16. config.py +146 -0
  17. interactive.py +865 -0
  18. llm/__init__.py +51 -0
  19. llm/base.py +26 -0
  20. llm/compat.py +226 -0
  21. llm/content_utils.py +309 -0
  22. llm/litellm_adapter.py +450 -0
  23. llm/message_types.py +245 -0
  24. llm/model_manager.py +265 -0
  25. llm/retry.py +95 -0
  26. main.py +246 -0
  27. memory/__init__.py +20 -0
  28. memory/compressor.py +554 -0
  29. memory/manager.py +538 -0
  30. memory/serialization.py +82 -0
  31. memory/short_term.py +88 -0
  32. memory/store/__init__.py +6 -0
  33. memory/store/memory_store.py +100 -0
  34. memory/store/yaml_file_memory_store.py +414 -0
  35. memory/token_tracker.py +203 -0
  36. memory/types.py +51 -0
  37. tools/__init__.py +6 -0
  38. tools/advanced_file_ops.py +557 -0
  39. tools/base.py +51 -0
  40. tools/calculator.py +50 -0
  41. tools/code_navigator.py +975 -0
  42. tools/explore.py +254 -0
  43. tools/file_ops.py +150 -0
  44. tools/git_tools.py +791 -0
  45. tools/notify.py +69 -0
  46. tools/parallel_execute.py +420 -0
  47. tools/session_manager.py +205 -0
  48. tools/shell.py +147 -0
  49. tools/shell_background.py +470 -0
  50. tools/smart_edit.py +491 -0
  51. tools/todo.py +130 -0
  52. tools/web_fetch.py +673 -0
  53. tools/web_search.py +61 -0
  54. utils/__init__.py +15 -0
  55. utils/logger.py +105 -0
  56. utils/model_pricing.py +49 -0
  57. utils/runtime.py +75 -0
  58. utils/terminal_ui.py +422 -0
  59. utils/tui/__init__.py +39 -0
  60. utils/tui/command_registry.py +49 -0
  61. utils/tui/components.py +306 -0
  62. utils/tui/input_handler.py +393 -0
  63. utils/tui/model_ui.py +204 -0
  64. utils/tui/progress.py +292 -0
  65. utils/tui/status_bar.py +178 -0
  66. utils/tui/theme.py +165 -0
@@ -0,0 +1,975 @@
1
+ """Code navigation tool using AST analysis for fast and accurate code location.
2
+
3
+ This tool provides intelligent code navigation capabilities:
4
+ - Find function/class definitions quickly across multiple languages
5
+ - Show file structure (imports, classes, functions)
6
+ - Find usages of functions/classes
7
+ - Supports Python, JavaScript/TypeScript, Go, Rust, Java, Kotlin, C/C++
8
+ """
9
+
10
+ import ast
11
+ import asyncio
12
+ import warnings
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, TypedDict
15
+
16
+ import aiofiles
17
+ import aiofiles.os
18
+
19
+ from tools.base import BaseTool
20
+
21
+ # Try to import tree-sitter-languages for multi-language support
22
+ try:
23
+ # tree_sitter_languages may trigger a FutureWarning from tree_sitter about
24
+ # Language(path, name) being deprecated. This is a dependency-level warning
25
+ # and is safe to suppress locally to keep test output clean.
26
+ with warnings.catch_warnings():
27
+ warnings.filterwarnings(
28
+ "ignore",
29
+ category=FutureWarning,
30
+ module=r"^tree_sitter(\.|$)",
31
+ )
32
+ from tree_sitter_languages import get_language, get_parser
33
+
34
+ HAS_TREE_SITTER = True
35
+ except ImportError:
36
+ HAS_TREE_SITTER = False
37
+
38
+
39
+ class _FunctionResult(TypedDict):
40
+ file: str
41
+ line: int
42
+ signature: str
43
+ docstring: str
44
+ decorators: List[str]
45
+
46
+
47
+ class _ClassResult(TypedDict):
48
+ file: str
49
+ line: int
50
+ bases: List[str]
51
+ methods: List[str]
52
+ docstring: str
53
+ decorators: List[str]
54
+
55
+
56
+ # Language extension mapping
57
+ EXTENSION_TO_LANGUAGE = {
58
+ ".py": "python",
59
+ ".js": "javascript",
60
+ ".jsx": "javascript",
61
+ ".ts": "typescript",
62
+ ".tsx": "typescript",
63
+ ".go": "go",
64
+ ".rs": "rust",
65
+ ".java": "java",
66
+ ".kt": "kotlin",
67
+ ".kts": "kotlin",
68
+ ".cpp": "cpp",
69
+ ".cc": "cpp",
70
+ ".cxx": "cpp",
71
+ ".c": "c",
72
+ ".h": "c",
73
+ ".hpp": "cpp",
74
+ }
75
+
76
+ # Tree-sitter query patterns for function definitions by language
77
+ FUNCTION_QUERIES = {
78
+ "python": "(function_definition name: (identifier) @name)",
79
+ "javascript": """[
80
+ (function_declaration name: (identifier) @name)
81
+ (method_definition name: (property_identifier) @name)
82
+ (arrow_function) @arrow
83
+ ]""",
84
+ "typescript": """[
85
+ (function_declaration name: (identifier) @name)
86
+ (method_definition name: (property_identifier) @name)
87
+ (arrow_function) @arrow
88
+ ]""",
89
+ "go": "(function_declaration name: (identifier) @name)",
90
+ "rust": "(function_item name: (identifier) @name)",
91
+ "java": "(method_declaration name: (identifier) @name)",
92
+ "kotlin": "(function_declaration (simple_identifier) @name)",
93
+ "cpp": "(function_definition declarator: (function_declarator declarator: (_) @name))",
94
+ "c": "(function_definition declarator: (function_declarator declarator: (_) @name))",
95
+ }
96
+
97
+ # Tree-sitter query patterns for class definitions by language
98
+ CLASS_QUERIES = {
99
+ "python": "(class_definition name: (identifier) @name)",
100
+ "javascript": "(class_declaration name: (identifier) @name)",
101
+ "typescript": """[
102
+ (class_declaration name: (type_identifier) @name)
103
+ (interface_declaration name: (type_identifier) @name)
104
+ ]""",
105
+ "go": "(type_declaration (type_spec name: (type_identifier) @name))",
106
+ "rust": """[
107
+ (struct_item name: (type_identifier) @name)
108
+ (impl_item type: (type_identifier) @name)
109
+ (trait_item name: (type_identifier) @name)
110
+ ]""",
111
+ "java": """[
112
+ (class_declaration name: (identifier) @name)
113
+ (interface_declaration name: (identifier) @name)
114
+ ]""",
115
+ "kotlin": "(class_declaration (type_identifier) @name)",
116
+ "cpp": """[
117
+ (class_specifier name: (type_identifier) @name)
118
+ (struct_specifier name: (type_identifier) @name)
119
+ ]""",
120
+ "c": "(struct_specifier name: (type_identifier) @name)",
121
+ }
122
+
123
+ # File patterns by language for iteration
124
+ LANGUAGE_FILE_PATTERNS = {
125
+ "python": ["*.py"],
126
+ "javascript": ["*.js", "*.jsx"],
127
+ "typescript": ["*.ts", "*.tsx"],
128
+ "go": ["*.go"],
129
+ "rust": ["*.rs"],
130
+ "java": ["*.java"],
131
+ "kotlin": ["*.kt", "*.kts"],
132
+ "cpp": ["*.cpp", "*.cc", "*.cxx", "*.hpp"],
133
+ "c": ["*.c", "*.h"],
134
+ }
135
+
136
+
137
+ def detect_language(file_path: Path) -> Optional[str]:
138
+ """Detect language from file extension."""
139
+ return EXTENSION_TO_LANGUAGE.get(file_path.suffix.lower())
140
+
141
+
142
+ def get_supported_languages() -> List[str]:
143
+ """Return list of supported languages."""
144
+ return list(FUNCTION_QUERIES.keys())
145
+
146
+
147
+ class CodeNavigatorTool(BaseTool):
148
+ """Navigate code using AST analysis - fast and accurate, multi-language support."""
149
+
150
+ def __init__(self):
151
+ self.cache_dir = Path(".aloop/cache")
152
+ self.symbol_cache = {} # {symbol_name: [(file, line, type, info)]}
153
+ self.cache_loaded = False
154
+ self._tree_sitter_available = HAS_TREE_SITTER
155
+
156
+ def _get_tree_sitter_parser_and_language(self, lang: str):
157
+ """Get a tree-sitter parser and language.
158
+
159
+ tree_sitter_languages currently triggers a FutureWarning via tree_sitter
160
+ (Language(path, name) deprecation). This is dependency-level noise, so we
161
+ suppress it locally around the calls that instantiate Language objects.
162
+ """
163
+ with warnings.catch_warnings():
164
+ warnings.filterwarnings(
165
+ "ignore",
166
+ category=FutureWarning,
167
+ module=r"^tree_sitter(\.|$)",
168
+ )
169
+ return get_parser(lang), get_language(lang)
170
+
171
+ @property
172
+ def name(self) -> str:
173
+ return "code_navigator"
174
+
175
+ @property
176
+ def description(self) -> str:
177
+ langs = ", ".join(get_supported_languages()) if HAS_TREE_SITTER else "Python"
178
+ return f"""Fast code navigation using AST analysis (MUCH better than grep for code).
179
+
180
+ This tool understands code structure and can quickly find definitions and usages.
181
+ Supported languages: {langs}
182
+
183
+ Search types:
184
+ 1. find_function: Find function definitions by name
185
+ - Returns: file path, line number, function signature, docstring
186
+ - Example: code_navigator(target="compress", search_type="find_function")
187
+
188
+ 2. find_class: Find class definitions by name
189
+ - Returns: file path, line number, base classes, methods list
190
+ - Example: code_navigator(target="BaseAgent", search_type="find_class")
191
+
192
+ 3. show_structure: Show structure of a specific file
193
+ - Returns: imports, classes, functions in tree format
194
+ - Example: code_navigator(target="agent/base.py", search_type="show_structure")
195
+
196
+ 4. find_usages: Find where a function/class is called or used
197
+ - Returns: all usage locations (file + line number + context)
198
+ - Example: code_navigator(target="_react_loop", search_type="find_usages")
199
+
200
+ WHY USE THIS INSTEAD OF GREP:
201
+ - 10x faster for finding code elements
202
+ - Understands code structure (not just text matching)
203
+ - Returns exact line numbers and signatures
204
+ - No false positives from comments or strings
205
+ - Can distinguish between definitions and usages
206
+
207
+ WHEN TO USE:
208
+ - Finding where a function is defined: use find_function
209
+ - Finding where a class is defined: use find_class
210
+ - Understanding file structure: use show_structure
211
+ - Finding all places a function is called: use find_usages
212
+
213
+ WHEN TO USE GREP INSTEAD:
214
+ - Searching for string literals or text content
215
+ - Finding TODO/FIXME comments
216
+ - Searching in non-supported languages"""
217
+
218
+ @property
219
+ def parameters(self) -> Dict[str, Any]:
220
+ return {
221
+ "target": {
222
+ "type": "string",
223
+ "description": "What to search for: function name, class name, or file path (for show_structure)",
224
+ },
225
+ "search_type": {
226
+ "type": "string",
227
+ "description": "Type of search: find_function, find_class, show_structure, or find_usages",
228
+ "enum": ["find_function", "find_class", "show_structure", "find_usages"],
229
+ },
230
+ "path": {
231
+ "type": "string",
232
+ "description": "Optional: limit search to specific directory (default: current directory)",
233
+ },
234
+ "language": {
235
+ "type": "string",
236
+ "description": "Optional: limit search to specific language (e.g., 'python', 'javascript', 'go')",
237
+ },
238
+ }
239
+
240
+ async def execute(
241
+ self, target: str, search_type: str, path: str = ".", language: str = None, **kwargs
242
+ ) -> str:
243
+ """Execute code navigation search."""
244
+ try:
245
+ base_path = Path(path)
246
+ if not await aiofiles.os.path.exists(str(base_path)):
247
+ return f"Error: Path does not exist: {path}"
248
+
249
+ if search_type == "find_function":
250
+ return await self._find_function(target, base_path, language)
251
+ elif search_type == "find_class":
252
+ return await self._find_class(target, base_path, language)
253
+ elif search_type == "show_structure":
254
+ return await self._show_structure(target)
255
+ elif search_type == "find_usages":
256
+ return await self._find_usages(target, base_path, language)
257
+ else:
258
+ return f"Error: Unknown search_type '{search_type}'"
259
+
260
+ except Exception as e:
261
+ return f"Error executing code_navigator: {str(e)}"
262
+
263
+ async def _iter_source_files(
264
+ self, base_path: Path, language: Optional[str] = None
265
+ ) -> List[Path]:
266
+ """Iterate over source files, optionally filtered by language."""
267
+ files = []
268
+
269
+ if language:
270
+ # Filter by specific language
271
+ patterns = LANGUAGE_FILE_PATTERNS.get(language, [])
272
+ for pattern in patterns:
273
+ matches = await asyncio.to_thread(
274
+ lambda pattern=pattern: list(base_path.rglob(pattern))
275
+ )
276
+ files.extend(matches)
277
+ else:
278
+ # All supported languages
279
+ for patterns in LANGUAGE_FILE_PATTERNS.values():
280
+ for pattern in patterns:
281
+ matches = await asyncio.to_thread(
282
+ lambda pattern=pattern: list(base_path.rglob(pattern))
283
+ )
284
+ files.extend(matches)
285
+
286
+ # Deduplicate and exclude common non-code directories
287
+ seen = set()
288
+ result = []
289
+ exclude_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv", "target", "build"}
290
+
291
+ for f in files:
292
+ if f in seen:
293
+ continue
294
+ seen.add(f)
295
+
296
+ # Skip excluded directories
297
+ skip = False
298
+ for part in f.parts:
299
+ if part in exclude_dirs:
300
+ skip = True
301
+ break
302
+ if not skip:
303
+ result.append(f)
304
+
305
+ return sorted(result)
306
+
307
+ async def _find_function_with_tree_sitter(
308
+ self, name: str, file_path: Path, lang: str
309
+ ) -> List[Dict]:
310
+ """Find functions using tree-sitter."""
311
+ results = []
312
+
313
+ if lang not in FUNCTION_QUERIES:
314
+ return results
315
+
316
+ try:
317
+ parser, language = self._get_tree_sitter_parser_and_language(lang)
318
+ query = language.query(FUNCTION_QUERIES[lang])
319
+
320
+ async with aiofiles.open(file_path, "rb") as f:
321
+ code = await f.read()
322
+ tree = parser.parse(code)
323
+ captures = query.captures(tree.root_node)
324
+
325
+ for node, capture_name in captures:
326
+ if capture_name == "name":
327
+ func_name = node.text.decode("utf-8")
328
+ if func_name == name:
329
+ # Get the full function node (parent)
330
+ func_node = node.parent
331
+ while func_node and not func_node.type.endswith(
332
+ (
333
+ "function_definition",
334
+ "function_declaration",
335
+ "function_item",
336
+ "method_declaration",
337
+ "method_definition",
338
+ )
339
+ ):
340
+ func_node = func_node.parent
341
+
342
+ start_line = node.start_point[0] + 1
343
+ end_line = func_node.end_point[0] + 1 if func_node else start_line
344
+
345
+ # Try to extract signature (first line of function)
346
+ lines = code.decode("utf-8", errors="replace").splitlines()
347
+ signature = (
348
+ lines[start_line - 1].strip() if start_line <= len(lines) else ""
349
+ )
350
+
351
+ results.append(
352
+ {
353
+ "file": str(file_path),
354
+ "line": start_line,
355
+ "end_line": end_line,
356
+ "signature": signature,
357
+ "docstring": "(tree-sitter: no docstring extraction)",
358
+ "decorators": [],
359
+ "language": lang,
360
+ }
361
+ )
362
+
363
+ except Exception:
364
+ pass
365
+
366
+ return results
367
+
368
+ async def _find_class_with_tree_sitter(
369
+ self, name: str, file_path: Path, lang: str
370
+ ) -> List[Dict]:
371
+ """Find classes using tree-sitter."""
372
+ results = []
373
+
374
+ if lang not in CLASS_QUERIES:
375
+ return results
376
+
377
+ try:
378
+ parser, language = self._get_tree_sitter_parser_and_language(lang)
379
+ query = language.query(CLASS_QUERIES[lang])
380
+
381
+ async with aiofiles.open(file_path, "rb") as f:
382
+ code = await f.read()
383
+ tree = parser.parse(code)
384
+ captures = query.captures(tree.root_node)
385
+
386
+ for node, capture_name in captures:
387
+ if capture_name == "name":
388
+ class_name = node.text.decode("utf-8")
389
+ if class_name == name:
390
+ start_line = node.start_point[0] + 1
391
+
392
+ # Get the class node for methods extraction
393
+ class_node = node.parent
394
+ while class_node and not class_node.type.endswith(
395
+ (
396
+ "class_definition",
397
+ "class_declaration",
398
+ "class_specifier",
399
+ "struct_item",
400
+ "struct_specifier",
401
+ "type_spec",
402
+ "impl_item",
403
+ "trait_item",
404
+ "interface_declaration",
405
+ )
406
+ ):
407
+ class_node = class_node.parent
408
+
409
+ results.append(
410
+ {
411
+ "file": str(file_path),
412
+ "line": start_line,
413
+ "bases": [],
414
+ "methods": [],
415
+ "docstring": "(tree-sitter: no docstring extraction)",
416
+ "decorators": [],
417
+ "language": lang,
418
+ }
419
+ )
420
+
421
+ except Exception:
422
+ pass
423
+
424
+ return results
425
+
426
+ async def _find_function(
427
+ self, name: str, base_path: Path, language: Optional[str] = None
428
+ ) -> str:
429
+ """Find all function definitions matching the name."""
430
+ results: List[Dict] = []
431
+
432
+ for source_file in await self._iter_source_files(base_path, language):
433
+ lang = detect_language(source_file)
434
+ if not lang:
435
+ continue
436
+
437
+ # Use Python AST for Python files (more detailed output)
438
+ if lang == "python":
439
+ try:
440
+ async with aiofiles.open(source_file, encoding="utf-8") as f:
441
+ content = await f.read()
442
+ tree = ast.parse(content, filename=str(source_file))
443
+
444
+ for node in ast.walk(tree):
445
+ if isinstance(node, ast.FunctionDef) and node.name == name:
446
+ args = self._format_function_args(node.args)
447
+ signature = f"def {node.name}({args})"
448
+
449
+ if node.returns:
450
+ try:
451
+ return_type = ast.unparse(node.returns)
452
+ signature += f" -> {return_type}"
453
+ except Exception:
454
+ pass
455
+
456
+ docstring = ast.get_docstring(node)
457
+ decorators = [self._format_decorator(d) for d in node.decorator_list]
458
+
459
+ try:
460
+ rel_path = str(source_file.relative_to(base_path))
461
+ except ValueError:
462
+ rel_path = str(source_file)
463
+
464
+ results.append(
465
+ {
466
+ "file": rel_path,
467
+ "line": node.lineno,
468
+ "signature": signature,
469
+ "docstring": docstring or "(no docstring)",
470
+ "decorators": decorators,
471
+ }
472
+ )
473
+ except SyntaxError:
474
+ continue
475
+ except Exception:
476
+ continue
477
+
478
+ # Use tree-sitter for other languages
479
+ elif self._tree_sitter_available:
480
+ try:
481
+ rel_path = str(source_file.relative_to(base_path))
482
+ except ValueError:
483
+ rel_path = str(source_file)
484
+
485
+ ts_results = await self._find_function_with_tree_sitter(name, source_file, lang)
486
+ for r in ts_results:
487
+ r["file"] = rel_path
488
+ results.append(r)
489
+
490
+ if not results:
491
+ return f"No function named '{name}' found in {base_path}"
492
+
493
+ # Format results
494
+ output_parts = [f"Found {len(results)} function(s) named '{name}':\n"]
495
+ for r in results:
496
+ lang_tag = f" [{r.get('language', 'python')}]" if r.get("language") else ""
497
+ output_parts.append(f"📍 {r['file']}:{r['line']}{lang_tag}")
498
+ if r.get("decorators"):
499
+ output_parts.append(f" Decorators: {', '.join(r['decorators'])}")
500
+ output_parts.append(f" {r['signature']}")
501
+ doc = r["docstring"]
502
+ if len(doc) > 100:
503
+ doc = doc[:100] + "..."
504
+ output_parts.append(f' "{doc}"\n')
505
+
506
+ return "\n".join(output_parts)
507
+
508
+ async def _find_class(self, name: str, base_path: Path, language: Optional[str] = None) -> str:
509
+ """Find all class definitions matching the name."""
510
+ results: List[Dict] = []
511
+
512
+ for source_file in await self._iter_source_files(base_path, language):
513
+ lang = detect_language(source_file)
514
+ if not lang:
515
+ continue
516
+
517
+ # Use Python AST for Python files (more detailed output)
518
+ if lang == "python":
519
+ try:
520
+ async with aiofiles.open(source_file, encoding="utf-8") as f:
521
+ content = await f.read()
522
+ tree = ast.parse(content, filename=str(source_file))
523
+
524
+ for node in ast.walk(tree):
525
+ if isinstance(node, ast.ClassDef) and node.name == name:
526
+ bases = [self._format_base_class(b) for b in node.bases]
527
+ methods = [
528
+ item.name for item in node.body if isinstance(item, ast.FunctionDef)
529
+ ]
530
+
531
+ docstring = ast.get_docstring(node)
532
+ decorators = [self._format_decorator(d) for d in node.decorator_list]
533
+
534
+ try:
535
+ rel_path = str(source_file.relative_to(base_path))
536
+ except ValueError:
537
+ rel_path = str(source_file)
538
+
539
+ results.append(
540
+ {
541
+ "file": rel_path,
542
+ "line": node.lineno,
543
+ "bases": bases,
544
+ "methods": methods,
545
+ "docstring": docstring or "(no docstring)",
546
+ "decorators": decorators,
547
+ }
548
+ )
549
+ except SyntaxError:
550
+ continue
551
+ except Exception:
552
+ continue
553
+
554
+ # Use tree-sitter for other languages
555
+ elif self._tree_sitter_available:
556
+ try:
557
+ rel_path = str(source_file.relative_to(base_path))
558
+ except ValueError:
559
+ rel_path = str(source_file)
560
+
561
+ ts_results = await self._find_class_with_tree_sitter(name, source_file, lang)
562
+ for r in ts_results:
563
+ r["file"] = rel_path
564
+ results.append(r)
565
+
566
+ if not results:
567
+ return f"No class named '{name}' found in {base_path}"
568
+
569
+ # Format results
570
+ output_parts = [f"Found {len(results)} class(es) named '{name}':\n"]
571
+ for r in results:
572
+ lang_tag = f" [{r.get('language', 'python')}]" if r.get("language") else ""
573
+ output_parts.append(f"📍 {r['file']}:{r['line']}{lang_tag}")
574
+ output_parts.append(
575
+ f" class {name}({', '.join(r['bases']) if r['bases'] else 'object'})"
576
+ )
577
+ if r.get("decorators"):
578
+ output_parts.append(f" Decorators: {', '.join(r['decorators'])}")
579
+
580
+ doc = r["docstring"]
581
+ if len(doc) > 100:
582
+ doc = doc[:100] + "..."
583
+ output_parts.append(f' "{doc}"')
584
+
585
+ if r.get("methods"):
586
+ output_parts.append(
587
+ f" Methods ({len(r['methods'])}): {', '.join(r['methods'][:10])}"
588
+ )
589
+ if len(r["methods"]) > 10:
590
+ output_parts.append(f" ... and {len(r['methods']) - 10} more")
591
+ output_parts.append("")
592
+
593
+ return "\n".join(output_parts)
594
+
595
+ async def _show_structure(self, file_path: str) -> str:
596
+ """Show the structure of a specific file."""
597
+ path = Path(file_path)
598
+ if not await aiofiles.os.path.exists(str(path)):
599
+ return f"Error: File does not exist: {file_path}"
600
+
601
+ lang = detect_language(path)
602
+
603
+ # Use Python AST for Python files
604
+ if lang == "python":
605
+ return await self._show_structure_python(path)
606
+ elif self._tree_sitter_available and lang:
607
+ return await self._show_structure_tree_sitter(path, lang)
608
+ else:
609
+ return f"Error: Unsupported file type or tree-sitter not available for: {file_path}"
610
+
611
+ async def _show_structure_python(self, path: Path) -> str:
612
+ """Show structure of Python file using AST."""
613
+ try:
614
+ async with aiofiles.open(path, encoding="utf-8") as f:
615
+ content = await f.read()
616
+ tree = ast.parse(content, filename=str(path))
617
+
618
+ structure = {
619
+ "imports": [],
620
+ "classes": [],
621
+ "functions": [],
622
+ }
623
+
624
+ for node in ast.iter_child_nodes(tree):
625
+ if isinstance(node, ast.Import):
626
+ for alias in node.names:
627
+ structure["imports"].append(
628
+ {
629
+ "line": node.lineno,
630
+ "type": "import",
631
+ "name": alias.name,
632
+ "as": alias.asname,
633
+ }
634
+ )
635
+ elif isinstance(node, ast.ImportFrom):
636
+ for alias in node.names:
637
+ structure["imports"].append(
638
+ {
639
+ "line": node.lineno,
640
+ "type": "from",
641
+ "module": node.module or "",
642
+ "name": alias.name,
643
+ "as": alias.asname,
644
+ }
645
+ )
646
+ elif isinstance(node, ast.ClassDef):
647
+ methods = [
648
+ {"name": item.name, "line": item.lineno}
649
+ for item in node.body
650
+ if isinstance(item, ast.FunctionDef)
651
+ ]
652
+ structure["classes"].append(
653
+ {
654
+ "line": node.lineno,
655
+ "name": node.name,
656
+ "bases": [self._format_base_class(b) for b in node.bases],
657
+ "methods": methods,
658
+ "docstring": ast.get_docstring(node),
659
+ }
660
+ )
661
+ elif isinstance(node, ast.FunctionDef):
662
+ args = self._format_function_args(node.args)
663
+ structure["functions"].append(
664
+ {
665
+ "line": node.lineno,
666
+ "name": node.name,
667
+ "args": args,
668
+ "docstring": ast.get_docstring(node),
669
+ }
670
+ )
671
+
672
+ return self._format_structure_output(str(path), structure, "python")
673
+
674
+ except SyntaxError as e:
675
+ return f"Error: Syntax error in {path} at line {e.lineno}: {e.msg}"
676
+ except Exception as e:
677
+ return f"Error parsing {path}: {str(e)}"
678
+
679
+ async def _show_structure_tree_sitter(self, path: Path, lang: str) -> str:
680
+ """Show structure of file using tree-sitter."""
681
+ try:
682
+ parser, language = self._get_tree_sitter_parser_and_language(lang)
683
+
684
+ async with aiofiles.open(path, "rb") as f:
685
+ code = await f.read()
686
+ tree = parser.parse(code)
687
+ lines = code.decode("utf-8", errors="replace").splitlines()
688
+
689
+ structure = {
690
+ "imports": [],
691
+ "classes": [],
692
+ "functions": [],
693
+ }
694
+
695
+ # Find classes
696
+ if lang in CLASS_QUERIES:
697
+ try:
698
+ query = language.query(CLASS_QUERIES[lang])
699
+ captures = query.captures(tree.root_node)
700
+ for node, capture_name in captures:
701
+ if capture_name == "name":
702
+ class_name = node.text.decode("utf-8")
703
+ structure["classes"].append(
704
+ {
705
+ "line": node.start_point[0] + 1,
706
+ "name": class_name,
707
+ "bases": [],
708
+ "methods": [],
709
+ "docstring": None,
710
+ }
711
+ )
712
+ except Exception:
713
+ pass
714
+
715
+ # Find functions
716
+ if lang in FUNCTION_QUERIES:
717
+ try:
718
+ query = language.query(FUNCTION_QUERIES[lang])
719
+ captures = query.captures(tree.root_node)
720
+ for node, capture_name in captures:
721
+ if capture_name == "name":
722
+ func_name = node.text.decode("utf-8")
723
+ line_num = node.start_point[0] + 1
724
+ line_content = (
725
+ lines[line_num - 1].strip() if line_num <= len(lines) else ""
726
+ )
727
+ structure["functions"].append(
728
+ {
729
+ "line": line_num,
730
+ "name": func_name,
731
+ "args": line_content,
732
+ "docstring": None,
733
+ }
734
+ )
735
+ except Exception:
736
+ pass
737
+
738
+ return self._format_structure_output(str(path), structure, lang)
739
+
740
+ except Exception as e:
741
+ return f"Error parsing {path}: {str(e)}"
742
+
743
+ def _format_structure_output(self, file_path: str, structure: Dict, lang: str) -> str:
744
+ """Format structure output."""
745
+ output_parts = [f"Structure of {file_path} [{lang}]:\n"]
746
+
747
+ # Imports
748
+ if structure["imports"]:
749
+ output_parts.append("📦 IMPORTS:")
750
+ for imp in structure["imports"][:20]:
751
+ if imp.get("type") == "import":
752
+ line = f" Line {imp['line']}: import {imp['name']}"
753
+ if imp.get("as"):
754
+ line += f" as {imp['as']}"
755
+ else:
756
+ line = (
757
+ f" Line {imp['line']}: from {imp.get('module', '')} import {imp['name']}"
758
+ )
759
+ if imp.get("as"):
760
+ line += f" as {imp['as']}"
761
+ output_parts.append(line)
762
+ if len(structure["imports"]) > 20:
763
+ output_parts.append(f" ... and {len(structure['imports']) - 20} more imports")
764
+ output_parts.append("")
765
+
766
+ # Classes
767
+ if structure["classes"]:
768
+ output_parts.append("📘 CLASSES:")
769
+ for cls in structure["classes"]:
770
+ bases_str = f"({', '.join(cls['bases'])})" if cls.get("bases") else ""
771
+ output_parts.append(f" Line {cls['line']}: class {cls['name']}{bases_str}")
772
+ if cls.get("docstring"):
773
+ doc = cls["docstring"]
774
+ if len(doc) > 60:
775
+ doc = doc[:60] + "..."
776
+ output_parts.append(f' "{doc}"')
777
+ if cls.get("methods"):
778
+ methods_str = ", ".join(m["name"] for m in cls["methods"][:5])
779
+ if len(cls["methods"]) > 5:
780
+ methods_str += f", ... (+{len(cls['methods']) - 5} more)"
781
+ output_parts.append(f" Methods: {methods_str}")
782
+ output_parts.append("")
783
+
784
+ # Functions
785
+ if structure["functions"]:
786
+ output_parts.append("🔧 FUNCTIONS:")
787
+ for func in structure["functions"]:
788
+ if lang == "python":
789
+ output_parts.append(
790
+ f" Line {func['line']}: def {func['name']}({func['args']})"
791
+ )
792
+ else:
793
+ output_parts.append(f" Line {func['line']}: {func['name']}")
794
+ if func.get("docstring"):
795
+ doc = func["docstring"]
796
+ if len(doc) > 60:
797
+ doc = doc[:60] + "..."
798
+ output_parts.append(f' "{doc}"')
799
+ output_parts.append("")
800
+
801
+ if not structure["imports"] and not structure["classes"] and not structure["functions"]:
802
+ output_parts.append("(File appears to be empty or contains only statements)")
803
+
804
+ return "\n".join(output_parts)
805
+
806
+ async def _find_usages(self, name: str, base_path: Path, language: Optional[str] = None) -> str:
807
+ """Find where a function or class is used (called)."""
808
+ results = []
809
+
810
+ for source_file in await self._iter_source_files(base_path, language):
811
+ lang = detect_language(source_file)
812
+ if not lang:
813
+ continue
814
+
815
+ # Use Python AST for Python files
816
+ if lang == "python":
817
+ try:
818
+ async with aiofiles.open(source_file, encoding="utf-8") as f:
819
+ content = await f.read()
820
+ lines = content.splitlines()
821
+ tree = ast.parse(content, filename=str(source_file))
822
+
823
+ for node in ast.walk(tree):
824
+ if isinstance(node, ast.Call):
825
+ called_name = self._get_call_name(node.func)
826
+ if called_name == name:
827
+ line_num = node.lineno
828
+ context = (
829
+ lines[line_num - 1].strip() if line_num <= len(lines) else ""
830
+ )
831
+
832
+ try:
833
+ rel_path = str(source_file.relative_to(Path.cwd()))
834
+ except ValueError:
835
+ rel_path = str(source_file)
836
+
837
+ results.append(
838
+ {
839
+ "file": rel_path,
840
+ "line": line_num,
841
+ "type": "function_call",
842
+ "context": context,
843
+ }
844
+ )
845
+
846
+ elif isinstance(node, ast.Name) and node.id == name:
847
+ line_num = node.lineno
848
+ context = lines[line_num - 1].strip() if line_num <= len(lines) else ""
849
+
850
+ try:
851
+ rel_path = str(source_file.relative_to(base_path))
852
+ except ValueError:
853
+ rel_path = str(source_file)
854
+
855
+ results.append(
856
+ {
857
+ "file": rel_path,
858
+ "line": line_num,
859
+ "type": "name_reference",
860
+ "context": context,
861
+ }
862
+ )
863
+ except SyntaxError:
864
+ continue
865
+ except Exception:
866
+ continue
867
+
868
+ # For other languages, use simple text search for now
869
+ elif self._tree_sitter_available:
870
+ try:
871
+ async with aiofiles.open(source_file, encoding="utf-8") as f:
872
+ content = await f.read()
873
+ lines = content.splitlines()
874
+
875
+ for i, line in enumerate(lines):
876
+ if name in line:
877
+ try:
878
+ rel_path = str(source_file.relative_to(base_path))
879
+ except ValueError:
880
+ rel_path = str(source_file)
881
+
882
+ results.append(
883
+ {
884
+ "file": rel_path,
885
+ "line": i + 1,
886
+ "type": "text_match",
887
+ "context": line.strip(),
888
+ "language": lang,
889
+ }
890
+ )
891
+ except Exception:
892
+ continue
893
+
894
+ if not results:
895
+ return f"No usages of '{name}' found in {base_path}"
896
+
897
+ # Deduplicate results
898
+ seen = set()
899
+ unique_results = []
900
+ for r in results:
901
+ key = (r["file"], r["line"])
902
+ if key not in seen:
903
+ seen.add(key)
904
+ unique_results.append(r)
905
+
906
+ # Limit results
907
+ if len(unique_results) > 50:
908
+ unique_results = unique_results[:50]
909
+ truncated = True
910
+ else:
911
+ truncated = False
912
+
913
+ # Format results
914
+ output_parts = [f"Found {len(seen)} usage(s) of '{name}':\n"]
915
+
916
+ for r in unique_results:
917
+ lang_tag = f" [{r.get('language', 'python')}]" if r.get("language") else ""
918
+ output_parts.append(f"📍 {r['file']}:{r['line']}{lang_tag}")
919
+ context = str(r["context"])
920
+ if len(context) > 80:
921
+ context = context[:80] + "..."
922
+ output_parts.append(f" {context}\n")
923
+
924
+ if truncated:
925
+ output_parts.append(f"... (showing first 50 of {len(seen)} usages)")
926
+
927
+ return "\n".join(output_parts)
928
+
929
+ # Helper methods
930
+
931
+ def _format_function_args(self, args: ast.arguments) -> str:
932
+ """Format function arguments as string."""
933
+ arg_strs = []
934
+
935
+ for arg in args.args:
936
+ arg_str = arg.arg
937
+ if arg.annotation:
938
+ arg_str += f": {ast.unparse(arg.annotation)}"
939
+ arg_strs.append(arg_str)
940
+
941
+ if args.vararg:
942
+ arg_str = f"*{args.vararg.arg}"
943
+ if args.vararg.annotation:
944
+ arg_str += f": {ast.unparse(args.vararg.annotation)}"
945
+ arg_strs.append(arg_str)
946
+
947
+ if args.kwarg:
948
+ arg_str = f"**{args.kwarg.arg}"
949
+ if args.kwarg.annotation:
950
+ arg_str += f": {ast.unparse(args.kwarg.annotation)}"
951
+ arg_strs.append(arg_str)
952
+
953
+ return ", ".join(arg_strs)
954
+
955
+ def _format_base_class(self, node: ast.expr) -> str:
956
+ """Format a base class node as string."""
957
+ try:
958
+ return ast.unparse(node)
959
+ except Exception:
960
+ return "?"
961
+
962
+ def _format_decorator(self, node: ast.expr) -> str:
963
+ """Format a decorator node as string."""
964
+ try:
965
+ return "@" + ast.unparse(node)
966
+ except Exception:
967
+ return "@?"
968
+
969
+ def _get_call_name(self, node: ast.expr) -> Optional[str]:
970
+ """Extract the name from a function call node."""
971
+ if isinstance(node, ast.Name):
972
+ return node.id
973
+ elif isinstance(node, ast.Attribute):
974
+ return node.attr
975
+ return None