tunacode-cli 0.0.39__py3-none-any.whl → 0.0.40__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 tunacode-cli might be problematic. Click here for more details.

tunacode/constants.py CHANGED
@@ -7,7 +7,7 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.39"
10
+ APP_VERSION = "0.0.40"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -18,6 +18,9 @@ CONFIG_FILE_NAME = "tunacode.json"
18
18
  # Default limits
19
19
  MAX_FILE_SIZE = 100 * 1024 # 100KB
20
20
  MAX_COMMAND_OUTPUT = 5000 # 5000 chars
21
+ MAX_FILES_IN_DIR = 50
22
+ MAX_TOTAL_DIR_SIZE = 2 * 1024 * 1024 # 2 MB
23
+
21
24
 
22
25
  # Command output processing
23
26
  COMMAND_OUTPUT_THRESHOLD = 3500 # Length threshold for truncation
@@ -115,6 +118,13 @@ ERROR_COMMAND_NOT_FOUND = "Error: Command not found or failed to execute:"
115
118
  ERROR_COMMAND_EXECUTION = (
116
119
  "Error: Command not found or failed to execute: {command}. Details: {error}"
117
120
  )
121
+ # Directory expansion errors
122
+ ERROR_DIR_TOO_LARGE = (
123
+ "Error: Directory '{path}' expansion aborted. Total size exceeds {limit_mb:.1f} MB limit."
124
+ )
125
+ ERROR_DIR_TOO_MANY_FILES = (
126
+ "Error: Directory '{path}' expansion aborted. Exceeds limit of {limit} files."
127
+ )
118
128
 
119
129
  # Command output messages
120
130
  CMD_OUTPUT_NO_OUTPUT = "No output."
@@ -51,50 +51,156 @@ def ext_to_lang(path: str) -> str:
51
51
 
52
52
 
53
53
  def expand_file_refs(text: str) -> Tuple[str, List[str]]:
54
- """Expand @file references with file contents wrapped in code fences.
54
+ """
55
+ Expands @-references with file or directory contents wrapped in code fences.
56
+ - @path/to/file.ext: Reads a single file.
57
+ - @path/to/dir/: Reads all files in a directory (non-recursive).
58
+ - @path/to/dir/**: Reads all files in a directory and its subdirectories.
55
59
 
56
60
  Args:
57
- text: The input text potentially containing @file references.
61
+ text: The input text potentially containing @-references.
58
62
 
59
63
  Returns:
60
- Tuple[str, List[str]]: A tuple containing:
61
- - Text with any @file references replaced by the file's contents
62
- - List of absolute paths of files that were successfully expanded
64
+ A tuple containing:
65
+ - Text with references replaced by file/directory contents.
66
+ - List of absolute paths of files that were successfully expanded.
63
67
 
64
68
  Raises:
65
- ValueError: If a referenced file does not exist or is too large.
69
+ ValueError: If a referenced path does not exist.
66
70
  """
67
71
  import os
68
72
  import re
69
73
 
70
74
  from tunacode.constants import (
75
+ ERROR_DIR_TOO_LARGE,
76
+ ERROR_DIR_TOO_MANY_FILES,
71
77
  ERROR_FILE_NOT_FOUND,
72
- ERROR_FILE_TOO_LARGE,
73
- MAX_FILE_SIZE,
74
- MSG_FILE_SIZE_LIMIT,
78
+ MAX_FILES_IN_DIR,
79
+ MAX_TOTAL_DIR_SIZE,
75
80
  )
76
81
 
77
- pattern = re.compile(r"@([\w./_-]+)")
82
+ # Regex now includes trailing / and ** to capture directory intentions
83
+ pattern = re.compile(r"@([\w./\-_*]+)")
78
84
  expanded_files = []
79
85
 
80
86
  def replacer(match: re.Match) -> str:
81
- path = match.group(1)
82
- if not os.path.exists(path):
83
- raise ValueError(ERROR_FILE_NOT_FOUND.format(filepath=path))
87
+ path_spec = match.group(1)
84
88
 
85
- if os.path.getsize(path) > MAX_FILE_SIZE:
86
- raise ValueError(ERROR_FILE_TOO_LARGE.format(filepath=path) + MSG_FILE_SIZE_LIMIT)
89
+ is_recursive = path_spec.endswith("/**")
90
+ is_dir = path_spec.endswith("/")
87
91
 
88
- with open(path, "r", encoding="utf-8") as f:
89
- content = f.read()
92
+ # Determine the actual path to check on the filesystem
93
+ if is_recursive:
94
+ base_path = path_spec[:-3]
95
+ elif is_dir:
96
+ base_path = path_spec[:-1]
97
+ else:
98
+ base_path = path_spec
99
+
100
+ if not os.path.exists(base_path):
101
+ raise ValueError(ERROR_FILE_NOT_FOUND.format(filepath=base_path))
102
+
103
+ # For Recursive Directory Expansion ---
104
+ if is_recursive:
105
+ if not os.path.isdir(base_path):
106
+ raise ValueError(
107
+ f"Error: Path '{base_path}' for recursive expansion is not a directory."
108
+ )
109
+
110
+ all_contents = [f"\n=== START RECURSIVE EXPANSION: {path_spec} ===\n"]
111
+ total_size, file_count = 0, 0
112
+
113
+ for root, _, filenames in os.walk(base_path):
114
+ for filename in filenames:
115
+ if file_count >= MAX_FILES_IN_DIR:
116
+ all_contents.append(
117
+ ERROR_DIR_TOO_MANY_FILES.format(path=base_path, limit=MAX_FILES_IN_DIR)
118
+ )
119
+ break
120
+
121
+ file_path = os.path.join(root, filename)
122
+ content, size = _read_and_format_file(file_path, expanded_files)
123
+
124
+ if total_size + size > MAX_TOTAL_DIR_SIZE:
125
+ all_contents.append(
126
+ ERROR_DIR_TOO_LARGE.format(
127
+ path=base_path, limit_mb=MAX_TOTAL_DIR_SIZE / (1024 * 1024)
128
+ )
129
+ )
130
+ break
131
+
132
+ all_contents.append(content)
133
+ total_size += size
134
+ file_count += 1
135
+ if file_count >= MAX_FILES_IN_DIR or total_size > MAX_TOTAL_DIR_SIZE:
136
+ break
137
+
138
+ all_contents.append(f"\n=== END RECURSIVE EXPANSION: {path_spec} ===\n")
139
+ return "".join(all_contents)
140
+
141
+ # For Non-Recursive Directory Expansion
142
+ if is_dir:
143
+ if not os.path.isdir(base_path):
144
+ raise ValueError(
145
+ f"Error: Path '{base_path}' for directory expansion is not a directory."
146
+ )
147
+
148
+ all_contents = [f"\n=== START DIRECTORY EXPANSION: {path_spec} ===\n"]
149
+ total_size, file_count = 0, 0
150
+
151
+ for item_name in sorted(os.listdir(base_path)):
152
+ item_path = os.path.join(base_path, item_name)
153
+ if os.path.isfile(item_path):
154
+ if file_count >= MAX_FILES_IN_DIR:
155
+ all_contents.append(
156
+ ERROR_DIR_TOO_MANY_FILES.format(path=base_path, limit=MAX_FILES_IN_DIR)
157
+ )
158
+ break
159
+
160
+ content, size = _read_and_format_file(item_path, expanded_files)
161
+ if total_size + size > MAX_TOTAL_DIR_SIZE:
162
+ all_contents.append(
163
+ ERROR_DIR_TOO_LARGE.format(
164
+ path=base_path, limit_mb=MAX_TOTAL_DIR_SIZE / (1024 * 1024)
165
+ )
166
+ )
167
+ break
168
+
169
+ all_contents.append(content)
170
+ total_size += size
171
+ file_count += 1
172
+
173
+ all_contents.append(f"\n=== END DIRECTORY EXPANSION: {path_spec} ===\n")
174
+ return "".join(all_contents)
175
+
176
+ # For Single File Expansion
177
+ if os.path.isfile(base_path):
178
+ content, _ = _read_and_format_file(base_path, expanded_files)
179
+ return content
180
+
181
+ raise ValueError(f"Path '{base_path}' is not a valid file or directory specification.")
90
182
 
91
- # Track the absolute path of the file
92
- abs_path = os.path.abspath(path)
93
- expanded_files.append(abs_path)
183
+ expanded_text = pattern.sub(replacer, text)
184
+ return expanded_text, list(set(expanded_files))
94
185
 
95
- lang = ext_to_lang(path)
96
- # Add clear headers to indicate this is a file reference, not code to write
97
- return f"\n=== FILE REFERENCE: {path} ===\n```{lang}\n{content}\n```\n=== END FILE REFERENCE: {path} ===\n"
98
186
 
99
- expanded_text = pattern.sub(replacer, text)
100
- return expanded_text, expanded_files
187
+ def _read_and_format_file(file_path: str, expanded_files_tracker: List[str]) -> Tuple[str, int]:
188
+ """Reads a single file, formats it, and checks size limits."""
189
+ from tunacode.constants import MAX_FILE_SIZE
190
+
191
+ if os.path.getsize(file_path) > MAX_FILE_SIZE:
192
+ # Instead of raising an error, we'll just note it and skip or process gets terminated.
193
+ return f"\n--- SKIPPED (too large): {file_path} ---\n", 0
194
+
195
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
196
+ content = f.read()
197
+
198
+ abs_path = os.path.abspath(file_path)
199
+ expanded_files_tracker.append(abs_path)
200
+
201
+ lang = ext_to_lang(file_path)
202
+ header = f"=== FILE REFERENCE: {file_path} ==="
203
+ footer = f"=== END FILE REFERENCE: {file_path} ==="
204
+
205
+ formatted_content = f"\n{header}\n```{lang}\n{content}\n```\n{footer}\n"
206
+ return formatted_content, len(content.encode("utf-8"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.39
3
+ Version: 0.0.40
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -31,6 +31,7 @@ Requires-Dist: pytest; extra == "dev"
31
31
  Requires-Dist: pytest-cov; extra == "dev"
32
32
  Requires-Dist: pytest-asyncio; extra == "dev"
33
33
  Requires-Dist: textual-dev; extra == "dev"
34
+ Requires-Dist: pre-commit; extra == "dev"
34
35
  Dynamic: license-file
35
36
 
36
37
  # TunaCode
@@ -1,5 +1,5 @@
1
1
  tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tunacode/constants.py,sha256=fIAgJ0_fPxJgXWyaF-pWyVbZ7VK1gZnjVETFTVKB2lU,4074
2
+ tunacode/constants.py,sha256=70p2W_hEiyypHaSXzMjnsHlbJsS8kg95-YRDi-iNEs8,4406
3
3
  tunacode/context.py,sha256=6sterdRvPOyG3LU0nEAXpBsEPZbO3qtPyTlJBi-_VXE,2612
4
4
  tunacode/exceptions.py,sha256=mTWXuWyr1k16CGLWN2tsthDGi7lbx1JK0ekIqogYDP8,3105
5
5
  tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -76,12 +76,12 @@ tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDg
76
76
  tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
77
77
  tunacode/utils/security.py,sha256=e_zo9VmcOKFjgFMr9GOBIFhAmND4PBlJZgY7zqnsGjI,6548
78
78
  tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
79
- tunacode/utils/text_utils.py,sha256=IiRviMqz5uoAbid8emkRXxgvQz6KE27ZeQom-qh9ymI,2984
79
+ tunacode/utils/text_utils.py,sha256=6YBD9QfkDO44-6jxnwRWIpmfIifPG-NqMzy_O2NAouc,7277
80
80
  tunacode/utils/token_counter.py,sha256=nGCWwrHHFbKywqeDCEuJnADCkfJuzysWiB6cCltJOKI,648
81
81
  tunacode/utils/user_configuration.py,sha256=Ilz8dpGVJDBE2iLWHAPT0xR8D51VRKV3kIbsAz8Bboc,3275
82
- tunacode_cli-0.0.39.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
83
- tunacode_cli-0.0.39.dist-info/METADATA,sha256=WZEtECuHPPI7pnDuhnxR5K4LwFT-2ReaMPFeG_GKF5g,5064
84
- tunacode_cli-0.0.39.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
- tunacode_cli-0.0.39.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
86
- tunacode_cli-0.0.39.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
87
- tunacode_cli-0.0.39.dist-info/RECORD,,
82
+ tunacode_cli-0.0.40.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
83
+ tunacode_cli-0.0.40.dist-info/METADATA,sha256=77mabiAPHOIxkWTQjhTkzhKxENwIGRO4yA_xhbkHW3c,5106
84
+ tunacode_cli-0.0.40.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ tunacode_cli-0.0.40.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
86
+ tunacode_cli-0.0.40.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
87
+ tunacode_cli-0.0.40.dist-info/RECORD,,