ara-cli 0.1.9.69__py3-none-any.whl → 0.1.10.8__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.

Potentially problematic release.


This version of ara-cli might be problematic. Click here for more details.

Files changed (150) hide show
  1. ara_cli/__init__.py +18 -2
  2. ara_cli/__main__.py +248 -62
  3. ara_cli/ara_command_action.py +155 -86
  4. ara_cli/ara_config.py +226 -80
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/create.py +75 -0
  11. ara_cli/ara_subcommands/delete.py +22 -0
  12. ara_cli/ara_subcommands/extract.py +22 -0
  13. ara_cli/ara_subcommands/fetch_templates.py +14 -0
  14. ara_cli/ara_subcommands/list.py +65 -0
  15. ara_cli/ara_subcommands/list_tags.py +25 -0
  16. ara_cli/ara_subcommands/load.py +48 -0
  17. ara_cli/ara_subcommands/prompt.py +136 -0
  18. ara_cli/ara_subcommands/read.py +47 -0
  19. ara_cli/ara_subcommands/read_status.py +20 -0
  20. ara_cli/ara_subcommands/read_user.py +20 -0
  21. ara_cli/ara_subcommands/reconnect.py +27 -0
  22. ara_cli/ara_subcommands/rename.py +22 -0
  23. ara_cli/ara_subcommands/scan.py +14 -0
  24. ara_cli/ara_subcommands/set_status.py +22 -0
  25. ara_cli/ara_subcommands/set_user.py +22 -0
  26. ara_cli/ara_subcommands/template.py +16 -0
  27. ara_cli/artefact_autofix.py +649 -68
  28. ara_cli/artefact_creator.py +8 -11
  29. ara_cli/artefact_deleter.py +2 -4
  30. ara_cli/artefact_fuzzy_search.py +22 -10
  31. ara_cli/artefact_link_updater.py +4 -4
  32. ara_cli/artefact_lister.py +29 -55
  33. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  34. ara_cli/artefact_models/artefact_load.py +11 -3
  35. ara_cli/artefact_models/artefact_model.py +146 -39
  36. ara_cli/artefact_models/artefact_templates.py +70 -44
  37. ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
  38. ara_cli/artefact_models/epic_artefact_model.py +34 -26
  39. ara_cli/artefact_models/feature_artefact_model.py +203 -64
  40. ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
  41. ara_cli/artefact_models/serialize_helper.py +1 -1
  42. ara_cli/artefact_models/task_artefact_model.py +83 -15
  43. ara_cli/artefact_models/userstory_artefact_model.py +37 -27
  44. ara_cli/artefact_models/vision_artefact_model.py +23 -42
  45. ara_cli/artefact_reader.py +92 -91
  46. ara_cli/artefact_renamer.py +8 -4
  47. ara_cli/artefact_scan.py +66 -3
  48. ara_cli/chat.py +622 -162
  49. ara_cli/chat_agent/__init__.py +0 -0
  50. ara_cli/chat_agent/agent_communicator.py +62 -0
  51. ara_cli/chat_agent/agent_process_manager.py +211 -0
  52. ara_cli/chat_agent/agent_status_manager.py +73 -0
  53. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  54. ara_cli/commands/__init__.py +0 -0
  55. ara_cli/commands/command.py +7 -0
  56. ara_cli/commands/extract_command.py +15 -0
  57. ara_cli/commands/load_command.py +65 -0
  58. ara_cli/commands/load_image_command.py +34 -0
  59. ara_cli/commands/read_command.py +117 -0
  60. ara_cli/completers.py +144 -0
  61. ara_cli/directory_navigator.py +37 -4
  62. ara_cli/error_handler.py +134 -0
  63. ara_cli/file_classifier.py +6 -5
  64. ara_cli/file_lister.py +1 -1
  65. ara_cli/file_loaders/__init__.py +0 -0
  66. ara_cli/file_loaders/binary_file_loader.py +33 -0
  67. ara_cli/file_loaders/document_file_loader.py +34 -0
  68. ara_cli/file_loaders/document_reader.py +245 -0
  69. ara_cli/file_loaders/document_readers.py +233 -0
  70. ara_cli/file_loaders/file_loader.py +50 -0
  71. ara_cli/file_loaders/file_loaders.py +123 -0
  72. ara_cli/file_loaders/image_processor.py +89 -0
  73. ara_cli/file_loaders/markdown_reader.py +75 -0
  74. ara_cli/file_loaders/text_file_loader.py +187 -0
  75. ara_cli/global_file_lister.py +51 -0
  76. ara_cli/list_filter.py +1 -1
  77. ara_cli/output_suppressor.py +1 -1
  78. ara_cli/prompt_extractor.py +215 -88
  79. ara_cli/prompt_handler.py +521 -134
  80. ara_cli/prompt_rag.py +2 -2
  81. ara_cli/tag_extractor.py +83 -38
  82. ara_cli/template_loader.py +245 -0
  83. ara_cli/template_manager.py +18 -13
  84. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  85. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  86. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  87. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  88. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  89. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  90. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  91. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  92. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  93. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  94. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  95. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  96. ara_cli/update_config_prompt.py +9 -3
  97. ara_cli/version.py +1 -1
  98. ara_cli-0.1.10.8.dist-info/METADATA +241 -0
  99. ara_cli-0.1.10.8.dist-info/RECORD +193 -0
  100. tests/test_ara_command_action.py +73 -59
  101. tests/test_ara_config.py +341 -36
  102. tests/test_artefact_autofix.py +1060 -0
  103. tests/test_artefact_link_updater.py +3 -3
  104. tests/test_artefact_lister.py +52 -132
  105. tests/test_artefact_renamer.py +2 -2
  106. tests/test_artefact_scan.py +327 -33
  107. tests/test_chat.py +2063 -498
  108. tests/test_file_classifier.py +24 -1
  109. tests/test_file_creator.py +3 -5
  110. tests/test_file_lister.py +1 -1
  111. tests/test_global_file_lister.py +131 -0
  112. tests/test_list_filter.py +2 -2
  113. tests/test_prompt_handler.py +746 -0
  114. tests/test_tag_extractor.py +19 -13
  115. tests/test_template_loader.py +192 -0
  116. tests/test_template_manager.py +5 -4
  117. tests/test_update_config_prompt.py +2 -2
  118. ara_cli/ara_command_parser.py +0 -327
  119. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  120. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  121. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  122. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  123. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  124. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  125. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  126. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  127. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  128. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  129. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  130. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  131. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  132. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  133. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  134. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  135. ara_cli/templates/template.businessgoal +0 -10
  136. ara_cli/templates/template.capability +0 -10
  137. ara_cli/templates/template.epic +0 -15
  138. ara_cli/templates/template.example +0 -6
  139. ara_cli/templates/template.feature +0 -26
  140. ara_cli/templates/template.issue +0 -14
  141. ara_cli/templates/template.keyfeature +0 -15
  142. ara_cli/templates/template.task +0 -6
  143. ara_cli/templates/template.userstory +0 -17
  144. ara_cli/templates/template.vision +0 -14
  145. ara_cli-0.1.9.69.dist-info/METADATA +0 -16
  146. ara_cli-0.1.9.69.dist-info/RECORD +0 -158
  147. tests/test_ara_autofix.py +0 -219
  148. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
  149. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
  150. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,89 @@
1
+ import os
2
+ import base64
3
+ import tempfile
4
+ import requests
5
+ from typing import Optional, Tuple
6
+ import re
7
+ from ara_cli.prompt_handler import describe_image
8
+
9
+
10
+ class ImageProcessor:
11
+ """Handles image processing operations"""
12
+
13
+ @staticmethod
14
+ def process_base64_image(image_ref: str, base64_pattern: re.Pattern) -> Optional[Tuple[str, str]]:
15
+ """Process base64 encoded image and return description"""
16
+ base64_match = base64_pattern.match(image_ref)
17
+ if not base64_match:
18
+ return None
19
+
20
+ image_format = base64_match.group(1)
21
+ base64_data = base64_match.group(2)
22
+ image_data = base64.b64decode(base64_data)
23
+
24
+ # Create a temporary file to send to LLM
25
+ with tempfile.NamedTemporaryFile(suffix=f'.{image_format}', delete=False) as tmp_file:
26
+ tmp_file.write(image_data)
27
+ tmp_file_path = tmp_file.name
28
+
29
+ try:
30
+ description = describe_image(tmp_file_path)
31
+ return f"Image: (base64 embedded {image_format} image)\n[{description}]", None
32
+ finally:
33
+ os.unlink(tmp_file_path)
34
+
35
+ @staticmethod
36
+ def process_url_image(image_ref: str) -> Tuple[str, Optional[str]]:
37
+ """Process image from URL and return description"""
38
+ if not image_ref.startswith(('http://', 'https://')):
39
+ return "", None
40
+
41
+ try:
42
+ response = requests.get(image_ref, timeout=10)
43
+ response.raise_for_status()
44
+
45
+ # Determine file extension from content-type
46
+ content_type = response.headers.get('content-type', '')
47
+ ext = ImageProcessor._get_extension_from_content_type(content_type, image_ref)
48
+
49
+ # Create temporary file
50
+ with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_file:
51
+ tmp_file.write(response.content)
52
+ tmp_file_path = tmp_file.name
53
+
54
+ try:
55
+ description = describe_image(tmp_file_path)
56
+ return f"Image: {image_ref}\n[{description}]", None
57
+ finally:
58
+ os.unlink(tmp_file_path)
59
+
60
+ except Exception as e:
61
+ error_msg = f"Could not download image: {str(e)}"
62
+ return f"Image: {image_ref}\n[{error_msg}]", error_msg
63
+
64
+ @staticmethod
65
+ def process_local_image(image_ref: str, base_dir: str) -> Tuple[str, Optional[str]]:
66
+ """Process local image file and return description"""
67
+ if os.path.isabs(image_ref):
68
+ local_image_path = image_ref
69
+ else:
70
+ local_image_path = os.path.join(base_dir, image_ref)
71
+
72
+ if os.path.exists(local_image_path):
73
+ description = describe_image(local_image_path)
74
+ return f"Image: {image_ref}\n[{description}]", None
75
+ else:
76
+ error_msg = f"Image file not found"
77
+ return f"Image: {image_ref}\n[{error_msg}]", f"Local image not found: {local_image_path}"
78
+
79
+ @staticmethod
80
+ def _get_extension_from_content_type(content_type: str, url: str) -> str:
81
+ """Determine file extension from content type or URL"""
82
+ if 'image/jpeg' in content_type:
83
+ return '.jpg'
84
+ elif 'image/png' in content_type:
85
+ return '.png'
86
+ elif 'image/gif' in content_type:
87
+ return '.gif'
88
+ else:
89
+ return os.path.splitext(url)[1] or '.png'
@@ -0,0 +1,75 @@
1
+ import os
2
+ import re
3
+ from typing import Optional
4
+ from charset_normalizer import from_path
5
+ from ara_cli.file_loaders.image_processor import ImageProcessor
6
+
7
+
8
+ class MarkdownReader:
9
+ """Handles markdown file reading with optional image extraction"""
10
+
11
+ def __init__(self, file_path: str):
12
+ self.file_path = file_path
13
+ self.base_dir = os.path.dirname(file_path)
14
+ self.image_processor = ImageProcessor()
15
+
16
+ def read(self, extract_images: bool = False) -> str:
17
+ """Read markdown file and optionally extract/describe images"""
18
+ # Detect and use the most appropriate encoding
19
+ result = from_path(self.file_path).best()
20
+ if not result:
21
+ print(f"Failed to detect encoding for {self.file_path}")
22
+ return ""
23
+ content = str(result)
24
+
25
+ if not extract_images:
26
+ return content
27
+
28
+ return self._process_images(content)
29
+
30
+ def _process_images(self, content: str) -> str:
31
+ """Process all images in markdown content"""
32
+ # Pattern to match markdown images: ![alt text](url or path)
33
+ image_pattern = re.compile(r"!\[([^\]]*)\]\(([^\)]+)\)")
34
+ base64_pattern = re.compile(r"data:image/([^;]+);base64,([^)]+)")
35
+
36
+ # Process each image reference
37
+ for match in image_pattern.finditer(content):
38
+ image_ref = match.group(2)
39
+ replacement = self._process_single_image(image_ref, base64_pattern)
40
+
41
+ if replacement:
42
+ content = content.replace(match.group(0), replacement, 1)
43
+
44
+ return content
45
+
46
+ def _process_single_image(
47
+ self, image_ref: str, base64_pattern: re.Pattern
48
+ ) -> Optional[str]:
49
+ """Process a single image reference"""
50
+ try:
51
+ # Try base64 first
52
+ result = self.image_processor.process_base64_image(
53
+ image_ref, base64_pattern
54
+ )
55
+ if result:
56
+ return result[0]
57
+
58
+ # Try URL
59
+ result, error = self.image_processor.process_url_image(image_ref)
60
+ if result:
61
+ if error:
62
+ print(f"Warning: {error}")
63
+ return result
64
+
65
+ # Try local file
66
+ result, error = self.image_processor.process_local_image(
67
+ image_ref, self.base_dir
68
+ )
69
+ if error:
70
+ print(f"Warning: {error}")
71
+ return result
72
+
73
+ except Exception as e:
74
+ print(f"Warning: Could not process image {image_ref}: {e}")
75
+ return None
@@ -0,0 +1,187 @@
1
+ import os
2
+ import re
3
+ import base64
4
+ import tempfile
5
+ from typing import Optional, Tuple
6
+ import requests
7
+ from charset_normalizer import from_path
8
+ from ara_cli.prompt_handler import describe_image
9
+ from ara_cli.file_loaders.file_loader import FileLoader
10
+
11
+
12
+ class TextFileLoader(FileLoader):
13
+ """Loads text files"""
14
+ def load(self, file_path: str, prefix: str = "", suffix: str = "",
15
+ block_delimiter: str = "", extract_images: bool = False, **kwargs) -> bool:
16
+ """Load text file with optional markdown image extraction"""
17
+
18
+ is_md_file = file_path.lower().endswith('.md')
19
+
20
+ if is_md_file and extract_images:
21
+ reader = MarkdownReader(file_path)
22
+ file_content = reader.read(extract_images=True).replace('\r\n', '\n')
23
+ else:
24
+ # Use charset-normalizer to detect encoding
25
+ encoded_content = from_path(file_path).best()
26
+ if not encoded_content:
27
+ print(f"Failed to detect encoding for {file_path}")
28
+ return False
29
+ file_content = str(encoded_content).replace('\r\n', '\n')
30
+
31
+ if block_delimiter:
32
+ file_content = f"{block_delimiter}\n{file_content}\n{block_delimiter}"
33
+
34
+ write_content = f"{prefix}{file_content}{suffix}\n"
35
+
36
+ with open(self.chat.chat_name, 'a', encoding='utf-8') as chat_file:
37
+ chat_file.write(write_content)
38
+
39
+ return True
40
+
41
+
42
+ class MarkdownReader:
43
+ """Handles markdown file reading with optional image extraction"""
44
+
45
+ def __init__(self, file_path: str):
46
+ self.file_path = file_path
47
+ self.base_dir = os.path.dirname(file_path)
48
+ self.image_processor = ImageProcessor()
49
+
50
+ def read(self, extract_images: bool = False) -> str:
51
+ """Read markdown file and optionally extract/describe images"""
52
+ with open(self.file_path, 'r', encoding='utf-8') as file:
53
+ content = file.read()
54
+
55
+ if not extract_images:
56
+ return content
57
+
58
+ return self._process_images(content)
59
+
60
+ def _process_images(self, content: str) -> str:
61
+ """Process all images in markdown content"""
62
+ # Pattern to match markdown images: ![alt text](url or path)
63
+ image_pattern = re.compile(r'!\[([^\]]*)\]\(([^\)]+)\)')
64
+ base64_pattern = re.compile(r'data:image/([^;]+);base64,([^)]+)')
65
+
66
+ # Process each image reference
67
+ for match in image_pattern.finditer(content):
68
+ image_ref = match.group(2)
69
+ replacement = self._process_single_image(image_ref, base64_pattern)
70
+
71
+ if replacement:
72
+ content = content.replace(match.group(0), replacement, 1)
73
+
74
+ return content
75
+
76
+ def _process_single_image(self, image_ref: str, base64_pattern: re.Pattern) -> Optional[str]:
77
+ """Process a single image reference"""
78
+ try:
79
+ # Try base64 first
80
+ result = self.image_processor.process_base64_image(
81
+ image_ref, base64_pattern)
82
+ if result:
83
+ return result[0]
84
+
85
+ # Try URL
86
+ result, error = self.image_processor.process_url_image(image_ref)
87
+ if result:
88
+ if error:
89
+ print(f"Warning: {error}")
90
+ return result
91
+
92
+ # Try local file
93
+ result, error = self.image_processor.process_local_image(
94
+ image_ref, self.base_dir)
95
+ if error:
96
+ print(f"Warning: {error}")
97
+ return result
98
+
99
+ except Exception as e:
100
+ print(f"Warning: Could not process image {image_ref}: {e}")
101
+ return None
102
+
103
+
104
+ class ImageProcessor:
105
+ """Handles image processing operations"""
106
+
107
+ @staticmethod
108
+ def process_base64_image(
109
+ image_ref: str,
110
+ base64_pattern: re.Pattern
111
+ ) -> Optional[Tuple[str, str]]:
112
+ """Process base64 encoded image and return description"""
113
+ base64_match = base64_pattern.match(image_ref)
114
+ if not base64_match:
115
+ return None
116
+
117
+ image_format = base64_match.group(1)
118
+ base64_data = base64_match.group(2)
119
+ image_data = base64.b64decode(base64_data)
120
+
121
+ # Create a temporary file to send to LLM
122
+ with tempfile.NamedTemporaryFile(suffix=f'.{image_format}', delete=False) as tmp_file:
123
+ tmp_file.write(image_data)
124
+ tmp_file_path = tmp_file.name
125
+
126
+ try:
127
+ description = describe_image(tmp_file_path)
128
+ return f"Image: (base64 embedded {image_format} image)\n[{description}]", None
129
+ finally:
130
+ os.unlink(tmp_file_path)
131
+
132
+ @staticmethod
133
+ def process_url_image(image_ref: str) -> Tuple[str, Optional[str]]:
134
+ """Process image from URL and return description"""
135
+ if not image_ref.startswith(('http://', 'https://')):
136
+ return "", None
137
+
138
+ try:
139
+ response = requests.get(image_ref, timeout=10)
140
+ response.raise_for_status()
141
+
142
+ # Determine file extension from content-type
143
+ content_type = response.headers.get('content-type', '')
144
+ ext = ImageProcessor._get_extension_from_content_type(
145
+ content_type, image_ref)
146
+
147
+ # Create temporary file
148
+ with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_file:
149
+ tmp_file.write(response.content)
150
+ tmp_file_path = tmp_file.name
151
+
152
+ try:
153
+ description = describe_image(tmp_file_path)
154
+ return f"Image: {image_ref}\n[{description}]", None
155
+ finally:
156
+ os.unlink(tmp_file_path)
157
+
158
+ except Exception as e:
159
+ error_msg = f"Could not download image: {str(e)}"
160
+ return f"Image: {image_ref}\n[{error_msg}]", error_msg
161
+
162
+ @staticmethod
163
+ def process_local_image(image_ref: str, base_dir: str) -> Tuple[str, Optional[str]]:
164
+ """Process local image file and return description"""
165
+ if os.path.isabs(image_ref):
166
+ local_image_path = image_ref
167
+ else:
168
+ local_image_path = os.path.join(base_dir, image_ref)
169
+
170
+ if os.path.exists(local_image_path):
171
+ description = describe_image(local_image_path)
172
+ return f"Image: {image_ref}\n[{description}]", None
173
+ else:
174
+ error_msg = f"Image file not found"
175
+ return f"Image: {image_ref}\n[{error_msg}]", f"Local image not found: {local_image_path}"
176
+
177
+ @staticmethod
178
+ def _get_extension_from_content_type(content_type: str, url: str) -> str:
179
+ """Determine file extension from content type or URL"""
180
+ if 'image/jpeg' in content_type:
181
+ return '.jpg'
182
+ elif 'image/png' in content_type:
183
+ return '.png'
184
+ elif 'image/gif' in content_type:
185
+ return '.gif'
186
+ else:
187
+ return os.path.splitext(url)[1] or '.png'
@@ -0,0 +1,51 @@
1
+ import os
2
+ import fnmatch
3
+ from typing import List, Dict, Any
4
+
5
+ DirTree = Dict[str, Any]
6
+
7
+ def _build_tree(root_path: str, patterns: List[str]) -> DirTree:
8
+ """Creates a nested dictionary representing the directory structure in the specified path."""
9
+ tree: DirTree = {'files': [], 'dirs': {}}
10
+ try:
11
+ for item in os.listdir(root_path):
12
+ item_path = os.path.join(root_path, item)
13
+ if os.path.isdir(item_path):
14
+ subtree = _build_tree(item_path, patterns)
15
+ if subtree['files'] or subtree['dirs']:
16
+ tree['dirs'][item] = subtree
17
+ elif os.path.isfile(item_path):
18
+ if any(fnmatch.fnmatch(item, pattern) for pattern in patterns):
19
+ tree['files'].append(item)
20
+ except OSError as e:
21
+ print(f"Warning: Could not access path {root_path}: {e}")
22
+ return tree
23
+
24
+ def _write_tree_to_markdown(md_file, tree: DirTree, level: int):
25
+ """Writes the tree data structure to the file in markdown format."""
26
+ indent = ' ' * level
27
+ for filename in sorted(tree['files']):
28
+ md_file.write(f"{indent}- [] {filename}\n")
29
+
30
+ for dirname, subtree in sorted(tree['dirs'].items()):
31
+ md_file.write(f"{' ' * (level -1)}{'#' * (level + 1)} {dirname}\n")
32
+ _write_tree_to_markdown(md_file, subtree, level + 1)
33
+
34
+ def generate_global_markdown_listing(directories: List[str], file_patterns: List[str], output_file: str):
35
+ """Creates a hierarchical list of markdown files for global directories. Uses the absolute path as the top heading and relative names for children."""
36
+ with open(output_file, 'w', encoding='utf-8') as md_file:
37
+ for directory in directories:
38
+ abs_dir = os.path.abspath(directory)
39
+
40
+ if not os.path.isdir(abs_dir):
41
+ print(f"Warning: Global directory not found: {abs_dir}")
42
+ md_file.write(f"# {directory}\n")
43
+ md_file.write(f" - !! Warning: Global directory not found: {abs_dir}\n\n")
44
+ continue
45
+
46
+ tree = _build_tree(abs_dir, file_patterns)
47
+
48
+ if tree['files'] or tree['dirs']:
49
+ md_file.write(f"# {abs_dir}\n")
50
+ _write_tree_to_markdown(md_file, tree, 1)
51
+ md_file.write("\n")
ara_cli/list_filter.py CHANGED
@@ -35,7 +35,7 @@ class ListFilterMonad:
35
35
  def default_content_retrieval(file):
36
36
  # Default strategy assumes file is a path and attempts to read it
37
37
  try:
38
- with open(file, 'r') as f:
38
+ with open(file, 'r', encoding='utf-8') as f:
39
39
  return f.read()
40
40
  except Exception as e:
41
41
  print(f"Error reading file {file}: {e}")
@@ -6,7 +6,7 @@ from contextlib import contextmanager
6
6
  @contextmanager
7
7
  def suppress_stdout(suppress=False):
8
8
  if suppress:
9
- with open(os.devnull, "w") as devnull:
9
+ with open(os.devnull, "w", encoding="utf-8") as devnull:
10
10
  old_stdout = sys.stdout
11
11
  sys.stdout = devnull
12
12
  try: