kopipasta 0.34.0__py3-none-any.whl → 0.36.0__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 kopipasta might be problematic. Click here for more details.
- kopipasta/cache.py +6 -3
- kopipasta/file.py +91 -56
- kopipasta/import_parser.py +141 -69
- kopipasta/main.py +611 -302
- kopipasta/prompt.py +50 -39
- kopipasta/tree_selector.py +314 -170
- {kopipasta-0.34.0.dist-info → kopipasta-0.36.0.dist-info}/METADATA +1 -1
- kopipasta-0.36.0.dist-info/RECORD +13 -0
- kopipasta-0.34.0.dist-info/RECORD +0 -13
- {kopipasta-0.34.0.dist-info → kopipasta-0.36.0.dist-info}/LICENSE +0 -0
- {kopipasta-0.34.0.dist-info → kopipasta-0.36.0.dist-info}/WHEEL +0 -0
- {kopipasta-0.34.0.dist-info → kopipasta-0.36.0.dist-info}/entry_points.txt +0 -0
- {kopipasta-0.34.0.dist-info → kopipasta-0.36.0.dist-info}/top_level.txt +0 -0
kopipasta/main.py
CHANGED
|
@@ -14,24 +14,36 @@ import pygments.util
|
|
|
14
14
|
|
|
15
15
|
import requests
|
|
16
16
|
|
|
17
|
-
from kopipasta.file import
|
|
17
|
+
from kopipasta.file import (
|
|
18
|
+
FileTuple,
|
|
19
|
+
get_human_readable_size,
|
|
20
|
+
is_binary,
|
|
21
|
+
is_ignored,
|
|
22
|
+
is_large_file,
|
|
23
|
+
read_file_contents,
|
|
24
|
+
)
|
|
18
25
|
import kopipasta.import_parser as import_parser
|
|
19
26
|
from kopipasta.tree_selector import TreeSelector
|
|
20
|
-
from kopipasta.prompt import
|
|
27
|
+
from kopipasta.prompt import (
|
|
28
|
+
generate_prompt_template,
|
|
29
|
+
get_file_snippet,
|
|
30
|
+
get_language_for_file,
|
|
31
|
+
)
|
|
21
32
|
from kopipasta.cache import save_selection_to_cache
|
|
22
33
|
|
|
34
|
+
|
|
23
35
|
def _propose_and_add_dependencies(
|
|
24
36
|
file_just_added: str,
|
|
25
37
|
project_root_abs: str,
|
|
26
38
|
files_to_include: List[FileTuple],
|
|
27
|
-
current_char_count: int
|
|
39
|
+
current_char_count: int,
|
|
28
40
|
) -> Tuple[List[FileTuple], int]:
|
|
29
41
|
"""
|
|
30
42
|
Analyzes a file for local dependencies and interactively asks the user to add them.
|
|
31
43
|
"""
|
|
32
44
|
language = get_language_for_file(file_just_added)
|
|
33
|
-
if language not in [
|
|
34
|
-
return [], 0
|
|
45
|
+
if language not in ["python", "typescript", "javascript", "tsx", "jsx"]:
|
|
46
|
+
return [], 0 # Only analyze languages we can parse
|
|
35
47
|
|
|
36
48
|
print(f"Analyzing {os.path.relpath(file_just_added)} for local dependencies...")
|
|
37
49
|
|
|
@@ -41,34 +53,46 @@ def _propose_and_add_dependencies(
|
|
|
41
53
|
return [], 0
|
|
42
54
|
|
|
43
55
|
resolved_deps_abs: Set[str] = set()
|
|
44
|
-
if language ==
|
|
45
|
-
resolved_deps_abs = import_parser.parse_python_imports(
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
if language == "python":
|
|
57
|
+
resolved_deps_abs = import_parser.parse_python_imports(
|
|
58
|
+
file_content, file_just_added, project_root_abs
|
|
59
|
+
)
|
|
60
|
+
elif language in ["typescript", "javascript", "tsx", "jsx"]:
|
|
61
|
+
resolved_deps_abs = import_parser.parse_typescript_imports(
|
|
62
|
+
file_content, file_just_added, project_root_abs
|
|
63
|
+
)
|
|
48
64
|
|
|
49
65
|
# Filter out dependencies that are already in the context
|
|
50
66
|
included_paths = {os.path.abspath(f[0]) for f in files_to_include}
|
|
51
|
-
suggested_deps = sorted(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
suggested_deps = sorted(
|
|
68
|
+
[
|
|
69
|
+
dep
|
|
70
|
+
for dep in resolved_deps_abs
|
|
71
|
+
if os.path.abspath(dep) not in included_paths
|
|
72
|
+
and os.path.abspath(dep) != os.path.abspath(file_just_added)
|
|
73
|
+
]
|
|
74
|
+
)
|
|
55
75
|
|
|
56
76
|
if not suggested_deps:
|
|
57
77
|
print("No new local dependencies found.")
|
|
58
78
|
return [], 0
|
|
59
79
|
|
|
60
|
-
print(
|
|
80
|
+
print(
|
|
81
|
+
f"\nFound {len(suggested_deps)} new local {'dependency' if len(suggested_deps) == 1 else 'dependencies'}:"
|
|
82
|
+
)
|
|
61
83
|
for i, dep_path in enumerate(suggested_deps):
|
|
62
84
|
print(f" ({i+1}) {os.path.relpath(dep_path)}")
|
|
63
85
|
|
|
64
86
|
while True:
|
|
65
|
-
choice = input(
|
|
66
|
-
|
|
87
|
+
choice = input(
|
|
88
|
+
"\nAdd dependencies? (a)ll, (n)one, or enter numbers (e.g. 1, 3-4): "
|
|
89
|
+
).lower()
|
|
90
|
+
|
|
67
91
|
deps_to_add_paths = None
|
|
68
|
-
if choice ==
|
|
92
|
+
if choice == "a":
|
|
69
93
|
deps_to_add_paths = suggested_deps
|
|
70
94
|
break
|
|
71
|
-
if choice ==
|
|
95
|
+
if choice == "n":
|
|
72
96
|
deps_to_add_paths = []
|
|
73
97
|
print(f"Skipped {len(suggested_deps)} dependencies.")
|
|
74
98
|
break
|
|
@@ -76,11 +100,11 @@ def _propose_and_add_dependencies(
|
|
|
76
100
|
# Try to parse the input as numbers directly.
|
|
77
101
|
try:
|
|
78
102
|
selected_indices = set()
|
|
79
|
-
parts = choice.replace(
|
|
80
|
-
if all(p.strip() for p in parts):
|
|
103
|
+
parts = choice.replace(" ", "").split(",")
|
|
104
|
+
if all(p.strip() for p in parts): # Ensure no empty parts like in "1,"
|
|
81
105
|
for part in parts:
|
|
82
|
-
if
|
|
83
|
-
start_str, end_str = part.split(
|
|
106
|
+
if "-" in part:
|
|
107
|
+
start_str, end_str = part.split("-", 1)
|
|
84
108
|
start = int(start_str)
|
|
85
109
|
end = int(end_str)
|
|
86
110
|
if start > end:
|
|
@@ -94,67 +118,135 @@ def _propose_and_add_dependencies(
|
|
|
94
118
|
deps_to_add_paths = [
|
|
95
119
|
suggested_deps[i] for i in sorted(list(selected_indices))
|
|
96
120
|
]
|
|
97
|
-
break
|
|
121
|
+
break # Success! Exit the loop.
|
|
98
122
|
else:
|
|
99
|
-
print(
|
|
123
|
+
print(
|
|
124
|
+
f"Error: Invalid number selection. Please choose numbers between 1 and {len(suggested_deps)}."
|
|
125
|
+
)
|
|
100
126
|
else:
|
|
101
127
|
raise ValueError("Empty part detected in input.")
|
|
102
128
|
|
|
103
|
-
|
|
104
129
|
except ValueError:
|
|
105
130
|
# This will catch any input that isn't 'a', 'n', or a valid number/range.
|
|
106
|
-
print(
|
|
107
|
-
|
|
131
|
+
print(
|
|
132
|
+
"Invalid choice. Please enter 'a', 'n', or a list/range of numbers (e.g., '1,3' or '2-4')."
|
|
133
|
+
)
|
|
134
|
+
|
|
108
135
|
if not deps_to_add_paths:
|
|
109
|
-
return [], 0
|
|
136
|
+
return [], 0 # No dependencies were selected
|
|
110
137
|
|
|
111
138
|
newly_added_files: List[FileTuple] = []
|
|
112
139
|
char_count_delta = 0
|
|
113
140
|
for dep_path in deps_to_add_paths:
|
|
114
141
|
# Assume non-large for now for simplicity, can be enhanced later
|
|
115
142
|
file_size = os.path.getsize(dep_path)
|
|
116
|
-
newly_added_files.append(
|
|
143
|
+
newly_added_files.append(
|
|
144
|
+
(dep_path, False, None, get_language_for_file(dep_path))
|
|
145
|
+
)
|
|
117
146
|
char_count_delta += file_size
|
|
118
|
-
print(
|
|
147
|
+
print(
|
|
148
|
+
f"Added dependency: {os.path.relpath(dep_path)} ({get_human_readable_size(file_size)})"
|
|
149
|
+
)
|
|
119
150
|
|
|
120
151
|
return newly_added_files, char_count_delta
|
|
121
152
|
|
|
122
153
|
except Exception as e:
|
|
123
|
-
print(
|
|
154
|
+
print(
|
|
155
|
+
f"Warning: Could not analyze dependencies for {os.path.relpath(file_just_added)}: {e}"
|
|
156
|
+
)
|
|
124
157
|
return [], 0
|
|
125
158
|
|
|
159
|
+
|
|
126
160
|
def get_colored_code(file_path, code):
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
try:
|
|
162
|
+
lexer = get_lexer_for_filename(file_path)
|
|
163
|
+
except pygments.util.ClassNotFound:
|
|
164
|
+
lexer = TextLexer()
|
|
165
|
+
return highlight(code, lexer, TerminalFormatter())
|
|
166
|
+
|
|
132
167
|
|
|
133
168
|
def read_gitignore():
|
|
134
169
|
default_ignore_patterns = [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
170
|
+
".git",
|
|
171
|
+
"node_modules",
|
|
172
|
+
"venv",
|
|
173
|
+
".venv",
|
|
174
|
+
"dist",
|
|
175
|
+
".idea",
|
|
176
|
+
"__pycache__",
|
|
177
|
+
"*.pyc",
|
|
178
|
+
".ruff_cache",
|
|
179
|
+
".mypy_cache",
|
|
180
|
+
".pytest_cache",
|
|
181
|
+
".vscode",
|
|
182
|
+
".vite",
|
|
183
|
+
".terraform",
|
|
184
|
+
"output",
|
|
185
|
+
"poetry.lock",
|
|
186
|
+
"package-lock.json",
|
|
187
|
+
".env",
|
|
188
|
+
"*.log",
|
|
189
|
+
"*.bak",
|
|
190
|
+
"*.swp",
|
|
191
|
+
"*.swo",
|
|
192
|
+
"*.tmp",
|
|
193
|
+
"tmp",
|
|
194
|
+
"temp",
|
|
195
|
+
"logs",
|
|
196
|
+
"build",
|
|
197
|
+
"target",
|
|
198
|
+
".DS_Store",
|
|
199
|
+
"Thumbs.db",
|
|
200
|
+
"*.class",
|
|
201
|
+
"*.jar",
|
|
202
|
+
"*.war",
|
|
203
|
+
"*.ear",
|
|
204
|
+
"*.sqlite",
|
|
205
|
+
"*.db",
|
|
206
|
+
"*.jpg",
|
|
207
|
+
"*.jpeg",
|
|
208
|
+
"*.png",
|
|
209
|
+
"*.gif",
|
|
210
|
+
"*.bmp",
|
|
211
|
+
"*.tiff",
|
|
212
|
+
"*.ico",
|
|
213
|
+
"*.svg",
|
|
214
|
+
"*.webp",
|
|
215
|
+
"*.mp3",
|
|
216
|
+
"*.mp4",
|
|
217
|
+
"*.avi",
|
|
218
|
+
"*.mov",
|
|
219
|
+
"*.wmv",
|
|
220
|
+
"*.flv",
|
|
221
|
+
"*.pdf",
|
|
222
|
+
"*.doc",
|
|
223
|
+
"*.docx",
|
|
224
|
+
"*.xls",
|
|
225
|
+
"*.xlsx",
|
|
226
|
+
"*.ppt",
|
|
227
|
+
"*.pptx",
|
|
228
|
+
"*.zip",
|
|
229
|
+
"*.rar",
|
|
230
|
+
"*.tar",
|
|
231
|
+
"*.gz",
|
|
232
|
+
"*.7z",
|
|
233
|
+
"*.exe",
|
|
234
|
+
"*.dll",
|
|
235
|
+
"*.so",
|
|
236
|
+
"*.dylib",
|
|
146
237
|
]
|
|
147
238
|
gitignore_patterns = default_ignore_patterns.copy()
|
|
148
239
|
|
|
149
|
-
if os.path.exists(
|
|
240
|
+
if os.path.exists(".gitignore"):
|
|
150
241
|
print(".gitignore detected.")
|
|
151
|
-
with open(
|
|
242
|
+
with open(".gitignore", "r") as file:
|
|
152
243
|
for line in file:
|
|
153
244
|
line = line.strip()
|
|
154
|
-
if line and not line.startswith(
|
|
245
|
+
if line and not line.startswith("#"):
|
|
155
246
|
gitignore_patterns.append(line)
|
|
156
247
|
return gitignore_patterns
|
|
157
248
|
|
|
249
|
+
|
|
158
250
|
def split_python_file(file_content):
|
|
159
251
|
"""
|
|
160
252
|
Splits Python code into logical chunks using the AST module.
|
|
@@ -162,21 +254,22 @@ def split_python_file(file_content):
|
|
|
162
254
|
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
163
255
|
"""
|
|
164
256
|
import ast
|
|
257
|
+
|
|
165
258
|
tree = ast.parse(file_content)
|
|
166
259
|
chunks = []
|
|
167
260
|
prev_end = 0
|
|
168
261
|
lines = file_content.splitlines(keepends=True)
|
|
169
262
|
|
|
170
263
|
def get_code(start, end):
|
|
171
|
-
return
|
|
264
|
+
return "".join(lines[start:end])
|
|
172
265
|
|
|
173
|
-
nodes = [node for node in ast.iter_child_nodes(tree) if hasattr(node,
|
|
266
|
+
nodes = [node for node in ast.iter_child_nodes(tree) if hasattr(node, "lineno")]
|
|
174
267
|
|
|
175
268
|
i = 0
|
|
176
269
|
while i < len(nodes):
|
|
177
270
|
node = nodes[i]
|
|
178
271
|
start_line = node.lineno - 1 # Convert to 0-indexed
|
|
179
|
-
end_line = getattr(node,
|
|
272
|
+
end_line = getattr(node, "end_lineno", None)
|
|
180
273
|
if end_line is None:
|
|
181
274
|
end_line = start_line + 1
|
|
182
275
|
|
|
@@ -187,7 +280,7 @@ def split_python_file(file_content):
|
|
|
187
280
|
i += 1
|
|
188
281
|
next_node = nodes[i]
|
|
189
282
|
next_start = next_node.lineno - 1
|
|
190
|
-
next_end = getattr(next_node,
|
|
283
|
+
next_end = getattr(next_node, "end_lineno", None) or next_start + 1
|
|
191
284
|
chunk_end = next_end
|
|
192
285
|
|
|
193
286
|
# Add code before the node (e.g., imports or global code)
|
|
@@ -210,18 +303,19 @@ def split_python_file(file_content):
|
|
|
210
303
|
|
|
211
304
|
return merge_small_chunks(chunks)
|
|
212
305
|
|
|
306
|
+
|
|
213
307
|
def merge_small_chunks(chunks, min_lines=10):
|
|
214
308
|
"""
|
|
215
309
|
Merges chunks to ensure each has at least min_lines lines.
|
|
216
310
|
"""
|
|
217
311
|
merged_chunks = []
|
|
218
|
-
buffer_code =
|
|
312
|
+
buffer_code = ""
|
|
219
313
|
buffer_start = None
|
|
220
314
|
buffer_end = None
|
|
221
315
|
|
|
222
316
|
for code, start_line, end_line in chunks:
|
|
223
317
|
num_lines = end_line - start_line
|
|
224
|
-
if buffer_code ==
|
|
318
|
+
if buffer_code == "":
|
|
225
319
|
buffer_code = code
|
|
226
320
|
buffer_start = start_line
|
|
227
321
|
buffer_end = end_line
|
|
@@ -231,7 +325,7 @@ def merge_small_chunks(chunks, min_lines=10):
|
|
|
231
325
|
|
|
232
326
|
if (buffer_end - buffer_start) >= min_lines:
|
|
233
327
|
merged_chunks.append((buffer_code, buffer_start, buffer_end))
|
|
234
|
-
buffer_code =
|
|
328
|
+
buffer_code = ""
|
|
235
329
|
buffer_start = None
|
|
236
330
|
buffer_end = None
|
|
237
331
|
|
|
@@ -240,6 +334,7 @@ def merge_small_chunks(chunks, min_lines=10):
|
|
|
240
334
|
|
|
241
335
|
return merged_chunks
|
|
242
336
|
|
|
337
|
+
|
|
243
338
|
def split_javascript_file(file_content):
|
|
244
339
|
"""
|
|
245
340
|
Splits JavaScript code into logical chunks using regular expressions.
|
|
@@ -248,8 +343,8 @@ def split_javascript_file(file_content):
|
|
|
248
343
|
lines = file_content.splitlines(keepends=True)
|
|
249
344
|
chunks = []
|
|
250
345
|
pattern = re.compile(
|
|
251
|
-
r
|
|
252
|
-
re.MULTILINE
|
|
346
|
+
r"^\s*(export\s+)?(async\s+)?(function\s+\w+|class\s+\w+|\w+\s*=\s*\(.*?\)\s*=>)",
|
|
347
|
+
re.MULTILINE,
|
|
253
348
|
)
|
|
254
349
|
matches = list(pattern.finditer(file_content))
|
|
255
350
|
|
|
@@ -259,9 +354,9 @@ def split_javascript_file(file_content):
|
|
|
259
354
|
prev_end_line = 0
|
|
260
355
|
for match in matches:
|
|
261
356
|
start_index = match.start()
|
|
262
|
-
start_line = file_content.count(
|
|
357
|
+
start_line = file_content.count("\n", 0, start_index)
|
|
263
358
|
if prev_end_line < start_line:
|
|
264
|
-
code =
|
|
359
|
+
code = "".join(lines[prev_end_line:start_line])
|
|
265
360
|
chunks.append((code, prev_end_line, start_line))
|
|
266
361
|
|
|
267
362
|
function_code_lines = []
|
|
@@ -270,33 +365,34 @@ def split_javascript_file(file_content):
|
|
|
270
365
|
for i in range(start_line, len(lines)):
|
|
271
366
|
line = lines[i]
|
|
272
367
|
function_code_lines.append(line)
|
|
273
|
-
brace_count += line.count(
|
|
274
|
-
if
|
|
368
|
+
brace_count += line.count("{") - line.count("}")
|
|
369
|
+
if "{" in line:
|
|
275
370
|
in_block = True
|
|
276
371
|
if in_block and brace_count == 0:
|
|
277
372
|
end_line = i + 1
|
|
278
|
-
code =
|
|
373
|
+
code = "".join(function_code_lines)
|
|
279
374
|
chunks.append((code, start_line, end_line))
|
|
280
375
|
prev_end_line = end_line
|
|
281
376
|
break
|
|
282
377
|
else:
|
|
283
378
|
end_line = len(lines)
|
|
284
|
-
code =
|
|
379
|
+
code = "".join(function_code_lines)
|
|
285
380
|
chunks.append((code, start_line, end_line))
|
|
286
381
|
prev_end_line = end_line
|
|
287
382
|
|
|
288
383
|
if prev_end_line < len(lines):
|
|
289
|
-
code =
|
|
384
|
+
code = "".join(lines[prev_end_line:])
|
|
290
385
|
chunks.append((code, prev_end_line, len(lines)))
|
|
291
386
|
|
|
292
387
|
return merge_small_chunks(chunks)
|
|
293
388
|
|
|
389
|
+
|
|
294
390
|
def split_html_file(file_content):
|
|
295
391
|
"""
|
|
296
392
|
Splits HTML code into logical chunks based on top-level elements using regular expressions.
|
|
297
393
|
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
298
394
|
"""
|
|
299
|
-
pattern = re.compile(r
|
|
395
|
+
pattern = re.compile(r"<(?P<tag>\w+)(\s|>).*?</(?P=tag)>", re.DOTALL)
|
|
300
396
|
lines = file_content.splitlines(keepends=True)
|
|
301
397
|
chunks = []
|
|
302
398
|
matches = list(pattern.finditer(file_content))
|
|
@@ -308,29 +404,30 @@ def split_html_file(file_content):
|
|
|
308
404
|
for match in matches:
|
|
309
405
|
start_index = match.start()
|
|
310
406
|
end_index = match.end()
|
|
311
|
-
start_line = file_content.count(
|
|
312
|
-
end_line = file_content.count(
|
|
407
|
+
start_line = file_content.count("\n", 0, start_index)
|
|
408
|
+
end_line = file_content.count("\n", 0, end_index)
|
|
313
409
|
|
|
314
410
|
if prev_end < start_line:
|
|
315
|
-
code =
|
|
411
|
+
code = "".join(lines[prev_end:start_line])
|
|
316
412
|
chunks.append((code, prev_end, start_line))
|
|
317
413
|
|
|
318
|
-
code =
|
|
414
|
+
code = "".join(lines[start_line:end_line])
|
|
319
415
|
chunks.append((code, start_line, end_line))
|
|
320
416
|
prev_end = end_line
|
|
321
417
|
|
|
322
418
|
if prev_end < len(lines):
|
|
323
|
-
code =
|
|
419
|
+
code = "".join(lines[prev_end:])
|
|
324
420
|
chunks.append((code, prev_end, len(lines)))
|
|
325
421
|
|
|
326
422
|
return merge_small_chunks(chunks)
|
|
327
423
|
|
|
424
|
+
|
|
328
425
|
def split_c_file(file_content):
|
|
329
426
|
"""
|
|
330
427
|
Splits C/C++ code into logical chunks using regular expressions.
|
|
331
428
|
Returns a list of tuples: (chunk_code, start_line, end_line)
|
|
332
429
|
"""
|
|
333
|
-
pattern = re.compile(r
|
|
430
|
+
pattern = re.compile(r"^\s*(?:[\w\*\s]+)\s+(\w+)\s*\([^)]*\)\s*\{", re.MULTILINE)
|
|
334
431
|
lines = file_content.splitlines(keepends=True)
|
|
335
432
|
chunks = []
|
|
336
433
|
matches = list(pattern.finditer(file_content))
|
|
@@ -341,9 +438,9 @@ def split_c_file(file_content):
|
|
|
341
438
|
prev_end_line = 0
|
|
342
439
|
for match in matches:
|
|
343
440
|
start_index = match.start()
|
|
344
|
-
start_line = file_content.count(
|
|
441
|
+
start_line = file_content.count("\n", 0, start_index)
|
|
345
442
|
if prev_end_line < start_line:
|
|
346
|
-
code =
|
|
443
|
+
code = "".join(lines[prev_end_line:start_line])
|
|
347
444
|
chunks.append((code, prev_end_line, start_line))
|
|
348
445
|
|
|
349
446
|
function_code_lines = []
|
|
@@ -352,27 +449,28 @@ def split_c_file(file_content):
|
|
|
352
449
|
for i in range(start_line, len(lines)):
|
|
353
450
|
line = lines[i]
|
|
354
451
|
function_code_lines.append(line)
|
|
355
|
-
brace_count += line.count(
|
|
356
|
-
if
|
|
452
|
+
brace_count += line.count("{") - line.count("}")
|
|
453
|
+
if "{" in line:
|
|
357
454
|
in_function = True
|
|
358
455
|
if in_function and brace_count == 0:
|
|
359
456
|
end_line = i + 1
|
|
360
|
-
code =
|
|
457
|
+
code = "".join(function_code_lines)
|
|
361
458
|
chunks.append((code, start_line, end_line))
|
|
362
459
|
prev_end_line = end_line
|
|
363
460
|
break
|
|
364
461
|
else:
|
|
365
462
|
end_line = len(lines)
|
|
366
|
-
code =
|
|
463
|
+
code = "".join(function_code_lines)
|
|
367
464
|
chunks.append((code, start_line, end_line))
|
|
368
465
|
prev_end_line = end_line
|
|
369
466
|
|
|
370
467
|
if prev_end_line < len(lines):
|
|
371
|
-
code =
|
|
468
|
+
code = "".join(lines[prev_end_line:])
|
|
372
469
|
chunks.append((code, prev_end_line, len(lines)))
|
|
373
470
|
|
|
374
471
|
return merge_small_chunks(chunks)
|
|
375
472
|
|
|
473
|
+
|
|
376
474
|
def split_generic_file(file_content):
|
|
377
475
|
"""
|
|
378
476
|
Splits generic text files into chunks based on double newlines.
|
|
@@ -382,29 +480,30 @@ def split_generic_file(file_content):
|
|
|
382
480
|
chunks = []
|
|
383
481
|
start = 0
|
|
384
482
|
for i, line in enumerate(lines):
|
|
385
|
-
if line.strip() ==
|
|
483
|
+
if line.strip() == "":
|
|
386
484
|
if start < i:
|
|
387
|
-
chunk_code =
|
|
485
|
+
chunk_code = "".join(lines[start:i])
|
|
388
486
|
chunks.append((chunk_code, start, i))
|
|
389
487
|
start = i + 1
|
|
390
488
|
if start < len(lines):
|
|
391
|
-
chunk_code =
|
|
489
|
+
chunk_code = "".join(lines[start:])
|
|
392
490
|
chunks.append((chunk_code, start, len(lines)))
|
|
393
491
|
return merge_small_chunks(chunks)
|
|
394
492
|
|
|
493
|
+
|
|
395
494
|
def select_file_patches(file_path):
|
|
396
495
|
file_content = read_file_contents(file_path)
|
|
397
496
|
language = get_language_for_file(file_path)
|
|
398
497
|
chunks = []
|
|
399
498
|
total_char_count = 0
|
|
400
499
|
|
|
401
|
-
if language ==
|
|
500
|
+
if language == "python":
|
|
402
501
|
code_chunks = split_python_file(file_content)
|
|
403
|
-
elif language ==
|
|
502
|
+
elif language == "javascript":
|
|
404
503
|
code_chunks = split_javascript_file(file_content)
|
|
405
|
-
elif language ==
|
|
504
|
+
elif language == "html":
|
|
406
505
|
code_chunks = split_html_file(file_content)
|
|
407
|
-
elif language in [
|
|
506
|
+
elif language in ["c", "cpp"]:
|
|
408
507
|
code_chunks = split_c_file(file_content)
|
|
409
508
|
else:
|
|
410
509
|
code_chunks = split_generic_file(file_content)
|
|
@@ -417,16 +516,16 @@ def select_file_patches(file_path):
|
|
|
417
516
|
print(colored_chunk)
|
|
418
517
|
while True:
|
|
419
518
|
choice = input("(y)es include / (n)o skip / (q)uit rest of file? ").lower()
|
|
420
|
-
if choice ==
|
|
519
|
+
if choice == "y":
|
|
421
520
|
chunks.append(chunk_code)
|
|
422
521
|
total_char_count += len(chunk_code)
|
|
423
522
|
break
|
|
424
|
-
elif choice ==
|
|
523
|
+
elif choice == "n":
|
|
425
524
|
if not chunks or chunks[-1] != placeholder:
|
|
426
525
|
chunks.append(placeholder)
|
|
427
526
|
total_char_count += len(placeholder)
|
|
428
527
|
break
|
|
429
|
-
elif choice ==
|
|
528
|
+
elif choice == "q":
|
|
430
529
|
print("Skipping the rest of the file.")
|
|
431
530
|
if chunks and chunks[-1] != placeholder:
|
|
432
531
|
chunks.append(placeholder)
|
|
@@ -436,65 +535,74 @@ def select_file_patches(file_path):
|
|
|
436
535
|
|
|
437
536
|
return chunks, total_char_count
|
|
438
537
|
|
|
538
|
+
|
|
439
539
|
def get_placeholder_comment(language):
|
|
440
540
|
comments = {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
541
|
+
"python": "# Skipped content\n",
|
|
542
|
+
"javascript": "// Skipped content\n",
|
|
543
|
+
"typescript": "// Skipped content\n",
|
|
544
|
+
"java": "// Skipped content\n",
|
|
545
|
+
"c": "// Skipped content\n",
|
|
546
|
+
"cpp": "// Skipped content\n",
|
|
547
|
+
"html": "<!-- Skipped content -->\n",
|
|
548
|
+
"css": "/* Skipped content */\n",
|
|
549
|
+
"default": "# Skipped content\n",
|
|
450
550
|
}
|
|
451
|
-
return comments.get(language, comments[
|
|
551
|
+
return comments.get(language, comments["default"])
|
|
552
|
+
|
|
452
553
|
|
|
453
554
|
def get_colored_file_snippet(file_path, max_lines=50, max_bytes=4096):
|
|
454
555
|
snippet = get_file_snippet(file_path, max_lines, max_bytes)
|
|
455
556
|
return get_colored_code(file_path, snippet)
|
|
456
557
|
|
|
558
|
+
|
|
457
559
|
def print_char_count(count):
|
|
458
560
|
token_estimate = count // 4
|
|
459
|
-
print(
|
|
561
|
+
print(
|
|
562
|
+
f"\rCurrent prompt size: {count} characters (~ {token_estimate} tokens)",
|
|
563
|
+
flush=True,
|
|
564
|
+
)
|
|
565
|
+
|
|
460
566
|
|
|
461
|
-
def grep_files_in_directory(
|
|
567
|
+
def grep_files_in_directory(
|
|
568
|
+
pattern: str, directory: str, ignore_patterns: List[str]
|
|
569
|
+
) -> List[Tuple[str, List[str], int]]:
|
|
462
570
|
"""
|
|
463
571
|
Search for files containing a pattern using ag (silver searcher).
|
|
464
572
|
Returns list of (filepath, preview_lines, match_count).
|
|
465
573
|
"""
|
|
466
574
|
# Check if ag is available
|
|
467
|
-
if not shutil.which(
|
|
575
|
+
if not shutil.which("ag"):
|
|
468
576
|
print("Silver Searcher (ag) not found. Install it for grep functionality:")
|
|
469
577
|
print(" - Mac: brew install the_silver_searcher")
|
|
470
578
|
print(" - Ubuntu/Debian: apt-get install silversearcher-ag")
|
|
471
579
|
print(" - Other: https://github.com/ggreer/the_silver_searcher")
|
|
472
580
|
return []
|
|
473
|
-
|
|
581
|
+
|
|
474
582
|
try:
|
|
475
583
|
# First get files with matches
|
|
476
584
|
cmd = [
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
585
|
+
"ag",
|
|
586
|
+
"--files-with-matches",
|
|
587
|
+
"--nocolor",
|
|
588
|
+
"--ignore-case",
|
|
481
589
|
pattern,
|
|
482
|
-
directory
|
|
590
|
+
directory,
|
|
483
591
|
]
|
|
484
|
-
|
|
592
|
+
|
|
485
593
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
486
594
|
if result.returncode != 0 or not result.stdout.strip():
|
|
487
595
|
return []
|
|
488
|
-
|
|
489
|
-
files = result.stdout.strip().split(
|
|
596
|
+
|
|
597
|
+
files = result.stdout.strip().split("\n")
|
|
490
598
|
grep_results = []
|
|
491
|
-
|
|
599
|
+
|
|
492
600
|
for file in files:
|
|
493
601
|
if is_ignored(file, ignore_patterns, directory) or is_binary(file):
|
|
494
602
|
continue
|
|
495
|
-
|
|
603
|
+
|
|
496
604
|
# Get match count and preview lines
|
|
497
|
-
count_cmd = [
|
|
605
|
+
count_cmd = ["ag", "--count", "--nocolor", pattern, file]
|
|
498
606
|
count_result = subprocess.run(count_cmd, capture_output=True, text=True)
|
|
499
607
|
match_count = 0
|
|
500
608
|
if count_result.stdout:
|
|
@@ -502,47 +610,47 @@ def grep_files_in_directory(pattern: str, directory: str, ignore_patterns: List[
|
|
|
502
610
|
# We need to handle filenames that might contain colons
|
|
503
611
|
stdout_line = count_result.stdout.strip()
|
|
504
612
|
# Find the last colon to separate filename from count
|
|
505
|
-
last_colon_idx = stdout_line.rfind(
|
|
613
|
+
last_colon_idx = stdout_line.rfind(":")
|
|
506
614
|
if last_colon_idx > 0:
|
|
507
615
|
try:
|
|
508
|
-
match_count = int(stdout_line[last_colon_idx + 1:])
|
|
616
|
+
match_count = int(stdout_line[last_colon_idx + 1 :])
|
|
509
617
|
except ValueError:
|
|
510
618
|
match_count = 1
|
|
511
619
|
else:
|
|
512
620
|
match_count = 1
|
|
513
|
-
|
|
621
|
+
|
|
514
622
|
# Get preview of matches (up to 3 lines)
|
|
515
623
|
preview_cmd = [
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
624
|
+
"ag",
|
|
625
|
+
"--max-count=3",
|
|
626
|
+
"--nocolor",
|
|
627
|
+
"--noheading",
|
|
628
|
+
"--numbers",
|
|
521
629
|
pattern,
|
|
522
|
-
file
|
|
630
|
+
file,
|
|
523
631
|
]
|
|
524
632
|
preview_result = subprocess.run(preview_cmd, capture_output=True, text=True)
|
|
525
633
|
preview_lines = []
|
|
526
634
|
if preview_result.stdout:
|
|
527
|
-
for line in preview_result.stdout.strip().split(
|
|
635
|
+
for line in preview_result.stdout.strip().split("\n")[:3]:
|
|
528
636
|
# Format: "line_num:content"
|
|
529
|
-
if
|
|
530
|
-
line_num, content = line.split(
|
|
637
|
+
if ":" in line:
|
|
638
|
+
line_num, content = line.split(":", 1)
|
|
531
639
|
preview_lines.append(f" {line_num}: {content.strip()}")
|
|
532
640
|
else:
|
|
533
641
|
preview_lines.append(f" {line.strip()}")
|
|
534
|
-
|
|
642
|
+
|
|
535
643
|
grep_results.append((file, preview_lines, match_count))
|
|
536
|
-
|
|
644
|
+
|
|
537
645
|
return sorted(grep_results)
|
|
538
|
-
|
|
646
|
+
|
|
539
647
|
except Exception as e:
|
|
540
648
|
print(f"Error running ag: {e}")
|
|
541
649
|
return []
|
|
542
650
|
|
|
651
|
+
|
|
543
652
|
def select_from_grep_results(
|
|
544
|
-
grep_results: List[Tuple[str, List[str], int]],
|
|
545
|
-
current_char_count: int
|
|
653
|
+
grep_results: List[Tuple[str, List[str], int]], current_char_count: int
|
|
546
654
|
) -> Tuple[List[FileTuple], int]:
|
|
547
655
|
"""
|
|
548
656
|
Let user select from grep results.
|
|
@@ -550,92 +658,125 @@ def select_from_grep_results(
|
|
|
550
658
|
"""
|
|
551
659
|
if not grep_results:
|
|
552
660
|
return [], current_char_count
|
|
553
|
-
|
|
661
|
+
|
|
554
662
|
print(f"\nFound {len(grep_results)} files:")
|
|
555
663
|
for i, (file_path, preview_lines, match_count) in enumerate(grep_results):
|
|
556
664
|
file_size = os.path.getsize(file_path)
|
|
557
665
|
file_size_readable = get_human_readable_size(file_size)
|
|
558
|
-
print(
|
|
666
|
+
print(
|
|
667
|
+
f"\n{i+1}. {os.path.relpath(file_path)} ({file_size_readable}) - {match_count} {'match' if match_count == 1 else 'matches'}"
|
|
668
|
+
)
|
|
559
669
|
for preview_line in preview_lines[:3]:
|
|
560
670
|
print(preview_line)
|
|
561
671
|
if match_count > 3:
|
|
562
672
|
print(f" ... and {match_count - 3} more matches")
|
|
563
|
-
|
|
673
|
+
|
|
564
674
|
while True:
|
|
565
675
|
print_char_count(current_char_count)
|
|
566
|
-
choice = input(
|
|
567
|
-
|
|
676
|
+
choice = input(
|
|
677
|
+
"\nSelect grep results: (a)ll / (n)one / (s)elect individually / numbers (e.g. 1,3-4) / (q)uit? "
|
|
678
|
+
).lower()
|
|
679
|
+
|
|
568
680
|
selected_files: List[FileTuple] = []
|
|
569
681
|
char_count_delta = 0
|
|
570
|
-
|
|
571
|
-
if choice ==
|
|
682
|
+
|
|
683
|
+
if choice == "a":
|
|
572
684
|
for file_path, _, _ in grep_results:
|
|
573
685
|
file_size = os.path.getsize(file_path)
|
|
574
|
-
selected_files.append(
|
|
686
|
+
selected_files.append(
|
|
687
|
+
(file_path, False, None, get_language_for_file(file_path))
|
|
688
|
+
)
|
|
575
689
|
char_count_delta += file_size
|
|
576
690
|
print(f"Added all {len(grep_results)} files from grep results.")
|
|
577
691
|
return selected_files, current_char_count + char_count_delta
|
|
578
|
-
|
|
579
|
-
elif choice ==
|
|
692
|
+
|
|
693
|
+
elif choice == "n":
|
|
580
694
|
print("Skipped all grep results.")
|
|
581
695
|
return [], current_char_count
|
|
582
|
-
|
|
583
|
-
elif choice ==
|
|
696
|
+
|
|
697
|
+
elif choice == "q":
|
|
584
698
|
print("Cancelled grep selection.")
|
|
585
699
|
return [], current_char_count
|
|
586
|
-
|
|
587
|
-
elif choice ==
|
|
700
|
+
|
|
701
|
+
elif choice == "s":
|
|
588
702
|
for i, (file_path, preview_lines, match_count) in enumerate(grep_results):
|
|
589
703
|
file_size = os.path.getsize(file_path)
|
|
590
704
|
file_size_readable = get_human_readable_size(file_size)
|
|
591
705
|
file_char_estimate = file_size
|
|
592
706
|
file_token_estimate = file_char_estimate // 4
|
|
593
|
-
|
|
594
|
-
print(
|
|
595
|
-
|
|
596
|
-
|
|
707
|
+
|
|
708
|
+
print(
|
|
709
|
+
f"\n{os.path.relpath(file_path)} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens)"
|
|
710
|
+
)
|
|
711
|
+
print(
|
|
712
|
+
f"{match_count} {'match' if match_count == 1 else 'matches'} for search pattern"
|
|
713
|
+
)
|
|
714
|
+
|
|
597
715
|
while True:
|
|
598
716
|
print_char_count(current_char_count + char_count_delta)
|
|
599
717
|
file_choice = input("(y)es / (n)o / (q)uit? ").lower()
|
|
600
|
-
|
|
601
|
-
if file_choice ==
|
|
718
|
+
|
|
719
|
+
if file_choice == "y":
|
|
602
720
|
if is_large_file(file_path):
|
|
603
721
|
while True:
|
|
604
|
-
snippet_choice = input(
|
|
605
|
-
|
|
722
|
+
snippet_choice = input(
|
|
723
|
+
f"File is large. Use (f)ull content or (s)nippet? "
|
|
724
|
+
).lower()
|
|
725
|
+
if snippet_choice in ["f", "s"]:
|
|
606
726
|
break
|
|
607
727
|
print("Invalid choice. Please enter 'f' or 's'.")
|
|
608
|
-
if snippet_choice ==
|
|
609
|
-
selected_files.append(
|
|
728
|
+
if snippet_choice == "s":
|
|
729
|
+
selected_files.append(
|
|
730
|
+
(
|
|
731
|
+
file_path,
|
|
732
|
+
True,
|
|
733
|
+
None,
|
|
734
|
+
get_language_for_file(file_path),
|
|
735
|
+
)
|
|
736
|
+
)
|
|
610
737
|
char_count_delta += len(get_file_snippet(file_path))
|
|
611
738
|
else:
|
|
612
|
-
selected_files.append(
|
|
739
|
+
selected_files.append(
|
|
740
|
+
(
|
|
741
|
+
file_path,
|
|
742
|
+
False,
|
|
743
|
+
None,
|
|
744
|
+
get_language_for_file(file_path),
|
|
745
|
+
)
|
|
746
|
+
)
|
|
613
747
|
char_count_delta += file_size
|
|
614
748
|
else:
|
|
615
|
-
selected_files.append(
|
|
749
|
+
selected_files.append(
|
|
750
|
+
(
|
|
751
|
+
file_path,
|
|
752
|
+
False,
|
|
753
|
+
None,
|
|
754
|
+
get_language_for_file(file_path),
|
|
755
|
+
)
|
|
756
|
+
)
|
|
616
757
|
char_count_delta += file_size
|
|
617
758
|
print(f"Added: {os.path.relpath(file_path)}")
|
|
618
759
|
break
|
|
619
|
-
elif file_choice ==
|
|
760
|
+
elif file_choice == "n":
|
|
620
761
|
break
|
|
621
|
-
elif file_choice ==
|
|
762
|
+
elif file_choice == "q":
|
|
622
763
|
print(f"Added {len(selected_files)} files from grep results.")
|
|
623
764
|
return selected_files, current_char_count + char_count_delta
|
|
624
765
|
else:
|
|
625
766
|
print("Invalid choice. Please enter 'y', 'n', or 'q'.")
|
|
626
|
-
|
|
767
|
+
|
|
627
768
|
print(f"Added {len(selected_files)} files from grep results.")
|
|
628
769
|
return selected_files, current_char_count + char_count_delta
|
|
629
|
-
|
|
770
|
+
|
|
630
771
|
else:
|
|
631
772
|
# Try to parse number selection
|
|
632
773
|
try:
|
|
633
774
|
selected_indices = set()
|
|
634
|
-
parts = choice.replace(
|
|
775
|
+
parts = choice.replace(" ", "").split(",")
|
|
635
776
|
if all(p.strip() for p in parts):
|
|
636
777
|
for part in parts:
|
|
637
|
-
if
|
|
638
|
-
start_str, end_str = part.split(
|
|
778
|
+
if "-" in part:
|
|
779
|
+
start_str, end_str = part.split("-", 1)
|
|
639
780
|
start = int(start_str)
|
|
640
781
|
end = int(end_str)
|
|
641
782
|
if start > end:
|
|
@@ -643,28 +784,51 @@ def select_from_grep_results(
|
|
|
643
784
|
selected_indices.update(range(start - 1, end))
|
|
644
785
|
else:
|
|
645
786
|
selected_indices.add(int(part) - 1)
|
|
646
|
-
|
|
787
|
+
|
|
647
788
|
if all(0 <= i < len(grep_results) for i in selected_indices):
|
|
648
789
|
for i in sorted(selected_indices):
|
|
649
790
|
file_path, _, _ = grep_results[i]
|
|
650
791
|
file_size = os.path.getsize(file_path)
|
|
651
|
-
selected_files.append(
|
|
792
|
+
selected_files.append(
|
|
793
|
+
(
|
|
794
|
+
file_path,
|
|
795
|
+
False,
|
|
796
|
+
None,
|
|
797
|
+
get_language_for_file(file_path),
|
|
798
|
+
)
|
|
799
|
+
)
|
|
652
800
|
char_count_delta += file_size
|
|
653
801
|
print(f"Added {len(selected_files)} files from grep results.")
|
|
654
802
|
return selected_files, current_char_count + char_count_delta
|
|
655
803
|
else:
|
|
656
|
-
print(
|
|
804
|
+
print(
|
|
805
|
+
f"Error: Invalid number selection. Please choose numbers between 1 and {len(grep_results)}."
|
|
806
|
+
)
|
|
657
807
|
else:
|
|
658
808
|
raise ValueError("Empty part detected in input.")
|
|
659
809
|
except ValueError:
|
|
660
|
-
print(
|
|
810
|
+
print(
|
|
811
|
+
"Invalid choice. Please enter 'a', 'n', 's', 'q', or a list/range of numbers."
|
|
812
|
+
)
|
|
813
|
+
|
|
661
814
|
|
|
662
|
-
def select_files_in_directory(
|
|
815
|
+
def select_files_in_directory(
|
|
816
|
+
directory: str,
|
|
817
|
+
ignore_patterns: List[str],
|
|
818
|
+
project_root_abs: str,
|
|
819
|
+
current_char_count: int = 0,
|
|
820
|
+
selected_files_set: Optional[Set[str]] = None,
|
|
821
|
+
) -> Tuple[List[FileTuple], int]:
|
|
663
822
|
if selected_files_set is None:
|
|
664
823
|
selected_files_set = set()
|
|
665
|
-
|
|
666
|
-
files = [
|
|
667
|
-
|
|
824
|
+
|
|
825
|
+
files = [
|
|
826
|
+
f
|
|
827
|
+
for f in os.listdir(directory)
|
|
828
|
+
if os.path.isfile(os.path.join(directory, f))
|
|
829
|
+
and not is_ignored(os.path.join(directory, f), ignore_patterns)
|
|
830
|
+
and not is_binary(os.path.join(directory, f))
|
|
831
|
+
]
|
|
668
832
|
|
|
669
833
|
if not files:
|
|
670
834
|
return [], current_char_count
|
|
@@ -677,53 +841,66 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
677
841
|
file_size_readable = get_human_readable_size(file_size)
|
|
678
842
|
file_char_estimate = file_size # Assuming 1 byte ≈ 1 character for text files
|
|
679
843
|
file_token_estimate = file_char_estimate // 4
|
|
680
|
-
|
|
844
|
+
|
|
681
845
|
# Show if already selected
|
|
682
846
|
if os.path.abspath(file_path) in selected_files_set:
|
|
683
|
-
print(
|
|
847
|
+
print(
|
|
848
|
+
f"✓ {file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) [already selected]"
|
|
849
|
+
)
|
|
684
850
|
else:
|
|
685
|
-
print(
|
|
851
|
+
print(
|
|
852
|
+
f"- {file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens)"
|
|
853
|
+
)
|
|
686
854
|
|
|
687
855
|
while True:
|
|
688
856
|
print_char_count(current_char_count)
|
|
689
|
-
choice = input(
|
|
857
|
+
choice = input(
|
|
858
|
+
"(y)es add all / (n)o ignore all / (s)elect individually / (g)rep / (q)uit? "
|
|
859
|
+
).lower()
|
|
690
860
|
selected_files: List[FileTuple] = []
|
|
691
861
|
char_count_delta = 0
|
|
692
|
-
|
|
693
|
-
if choice ==
|
|
862
|
+
|
|
863
|
+
if choice == "g":
|
|
694
864
|
# Grep functionality
|
|
695
865
|
pattern = input("\nEnter search pattern: ")
|
|
696
866
|
if pattern:
|
|
697
867
|
print(f"\nSearching in {directory} for '{pattern}'...")
|
|
698
|
-
grep_results = grep_files_in_directory(
|
|
699
|
-
|
|
868
|
+
grep_results = grep_files_in_directory(
|
|
869
|
+
pattern, directory, ignore_patterns
|
|
870
|
+
)
|
|
871
|
+
|
|
700
872
|
if not grep_results:
|
|
701
873
|
print(f"No files found matching '{pattern}'")
|
|
702
874
|
continue
|
|
703
|
-
|
|
704
|
-
grep_selected, new_char_count = select_from_grep_results(
|
|
705
|
-
|
|
875
|
+
|
|
876
|
+
grep_selected, new_char_count = select_from_grep_results(
|
|
877
|
+
grep_results, current_char_count
|
|
878
|
+
)
|
|
879
|
+
|
|
706
880
|
if grep_selected:
|
|
707
881
|
selected_files.extend(grep_selected)
|
|
708
882
|
current_char_count = new_char_count
|
|
709
|
-
|
|
883
|
+
|
|
710
884
|
# Update selected files set
|
|
711
885
|
for file_tuple in grep_selected:
|
|
712
886
|
selected_files_set.add(os.path.abspath(file_tuple[0]))
|
|
713
|
-
|
|
887
|
+
|
|
714
888
|
# Analyze dependencies for grep-selected files
|
|
715
889
|
files_to_analyze = [f[0] for f in grep_selected]
|
|
716
890
|
for file_path in files_to_analyze:
|
|
717
891
|
new_deps, deps_char_count = _propose_and_add_dependencies(
|
|
718
|
-
file_path,
|
|
892
|
+
file_path,
|
|
893
|
+
project_root_abs,
|
|
894
|
+
selected_files,
|
|
895
|
+
current_char_count,
|
|
719
896
|
)
|
|
720
897
|
selected_files.extend(new_deps)
|
|
721
898
|
current_char_count += deps_char_count
|
|
722
|
-
|
|
899
|
+
|
|
723
900
|
# Update selected files set with dependencies
|
|
724
901
|
for dep_tuple in new_deps:
|
|
725
902
|
selected_files_set.add(os.path.abspath(dep_tuple[0]))
|
|
726
|
-
|
|
903
|
+
|
|
727
904
|
print(f"\nReturning to directory: {directory}")
|
|
728
905
|
# Re-show the directory with updated selections
|
|
729
906
|
print("Files:")
|
|
@@ -735,53 +912,73 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
735
912
|
print(f"✓ {file} ({file_size_readable}) [already selected]")
|
|
736
913
|
else:
|
|
737
914
|
print(f"- {file} ({file_size_readable})")
|
|
738
|
-
|
|
915
|
+
|
|
739
916
|
# Ask what to do with remaining files
|
|
740
|
-
remaining_files = [
|
|
917
|
+
remaining_files = [
|
|
918
|
+
f
|
|
919
|
+
for f in files
|
|
920
|
+
if os.path.abspath(os.path.join(directory, f))
|
|
921
|
+
not in selected_files_set
|
|
922
|
+
]
|
|
741
923
|
if remaining_files:
|
|
742
924
|
while True:
|
|
743
925
|
print_char_count(current_char_count)
|
|
744
|
-
remaining_choice = input(
|
|
745
|
-
|
|
926
|
+
remaining_choice = input(
|
|
927
|
+
"(y)es add remaining / (n)o skip remaining / (s)elect more / (g)rep again / (q)uit? "
|
|
928
|
+
).lower()
|
|
929
|
+
if remaining_choice == "y":
|
|
746
930
|
# Add all remaining files
|
|
747
931
|
for file in remaining_files:
|
|
748
932
|
file_path = os.path.join(directory, file)
|
|
749
933
|
file_size = os.path.getsize(file_path)
|
|
750
|
-
selected_files.append(
|
|
934
|
+
selected_files.append(
|
|
935
|
+
(
|
|
936
|
+
file_path,
|
|
937
|
+
False,
|
|
938
|
+
None,
|
|
939
|
+
get_language_for_file(file_path),
|
|
940
|
+
)
|
|
941
|
+
)
|
|
751
942
|
current_char_count += file_size
|
|
752
943
|
selected_files_set.add(os.path.abspath(file_path))
|
|
753
|
-
|
|
944
|
+
|
|
754
945
|
# Analyze dependencies for remaining files
|
|
755
946
|
for file in remaining_files:
|
|
756
947
|
file_path = os.path.join(directory, file)
|
|
757
|
-
|
|
758
|
-
|
|
948
|
+
(
|
|
949
|
+
new_deps,
|
|
950
|
+
deps_char_count,
|
|
951
|
+
) = _propose_and_add_dependencies(
|
|
952
|
+
file_path,
|
|
953
|
+
project_root_abs,
|
|
954
|
+
selected_files,
|
|
955
|
+
current_char_count,
|
|
759
956
|
)
|
|
760
957
|
selected_files.extend(new_deps)
|
|
761
958
|
current_char_count += deps_char_count
|
|
762
|
-
|
|
959
|
+
|
|
763
960
|
print(f"Added all remaining files from {directory}")
|
|
764
961
|
return selected_files, current_char_count
|
|
765
|
-
elif remaining_choice ==
|
|
962
|
+
elif remaining_choice == "n":
|
|
766
963
|
print(f"Skipped remaining files from {directory}")
|
|
767
964
|
return selected_files, current_char_count
|
|
768
|
-
elif remaining_choice ==
|
|
965
|
+
elif remaining_choice == "s":
|
|
769
966
|
# Continue to individual selection
|
|
770
|
-
choice =
|
|
967
|
+
choice = "s"
|
|
771
968
|
break
|
|
772
|
-
elif remaining_choice ==
|
|
969
|
+
elif remaining_choice == "g":
|
|
773
970
|
# Continue to grep again
|
|
774
|
-
choice =
|
|
971
|
+
choice = "g"
|
|
775
972
|
break
|
|
776
|
-
elif remaining_choice ==
|
|
973
|
+
elif remaining_choice == "q":
|
|
777
974
|
return selected_files, current_char_count
|
|
778
975
|
else:
|
|
779
976
|
print("Invalid choice. Please try again.")
|
|
780
|
-
|
|
781
|
-
if choice ==
|
|
977
|
+
|
|
978
|
+
if choice == "s":
|
|
782
979
|
# Fall through to individual selection
|
|
783
980
|
pass
|
|
784
|
-
elif choice ==
|
|
981
|
+
elif choice == "g":
|
|
785
982
|
# Loop back to grep
|
|
786
983
|
continue
|
|
787
984
|
else:
|
|
@@ -792,51 +989,61 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
792
989
|
continue
|
|
793
990
|
else:
|
|
794
991
|
continue
|
|
795
|
-
|
|
796
|
-
if choice ==
|
|
992
|
+
|
|
993
|
+
if choice == "y":
|
|
797
994
|
files_to_add_after_loop = []
|
|
798
995
|
for file in files:
|
|
799
996
|
file_path = os.path.join(directory, file)
|
|
800
997
|
if os.path.abspath(file_path) in selected_files_set:
|
|
801
998
|
continue # Skip already selected files
|
|
802
|
-
|
|
999
|
+
|
|
803
1000
|
if is_large_file(file_path):
|
|
804
1001
|
while True:
|
|
805
|
-
snippet_choice = input(
|
|
806
|
-
|
|
1002
|
+
snippet_choice = input(
|
|
1003
|
+
f"{file} is large. Use (f)ull content or (s)nippet? "
|
|
1004
|
+
).lower()
|
|
1005
|
+
if snippet_choice in ["f", "s"]:
|
|
807
1006
|
break
|
|
808
1007
|
print("Invalid choice. Please enter 'f' or 's'.")
|
|
809
|
-
if snippet_choice ==
|
|
810
|
-
selected_files.append(
|
|
1008
|
+
if snippet_choice == "s":
|
|
1009
|
+
selected_files.append(
|
|
1010
|
+
(file_path, True, None, get_language_for_file(file_path))
|
|
1011
|
+
)
|
|
811
1012
|
char_count_delta += len(get_file_snippet(file_path))
|
|
812
1013
|
else:
|
|
813
|
-
selected_files.append(
|
|
1014
|
+
selected_files.append(
|
|
1015
|
+
(file_path, False, None, get_language_for_file(file_path))
|
|
1016
|
+
)
|
|
814
1017
|
char_count_delta += os.path.getsize(file_path)
|
|
815
1018
|
else:
|
|
816
|
-
selected_files.append(
|
|
1019
|
+
selected_files.append(
|
|
1020
|
+
(file_path, False, None, get_language_for_file(file_path))
|
|
1021
|
+
)
|
|
817
1022
|
char_count_delta += os.path.getsize(file_path)
|
|
818
1023
|
files_to_add_after_loop.append(file_path)
|
|
819
1024
|
|
|
820
1025
|
# Analyze dependencies after the loop
|
|
821
1026
|
current_char_count += char_count_delta
|
|
822
1027
|
for file_path in files_to_add_after_loop:
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1028
|
+
new_deps, deps_char_count = _propose_and_add_dependencies(
|
|
1029
|
+
file_path, project_root_abs, selected_files, current_char_count
|
|
1030
|
+
)
|
|
1031
|
+
selected_files.extend(new_deps)
|
|
1032
|
+
current_char_count += deps_char_count
|
|
826
1033
|
|
|
827
1034
|
print(f"Added all files from {directory}")
|
|
828
1035
|
return selected_files, current_char_count
|
|
829
|
-
|
|
830
|
-
elif choice ==
|
|
1036
|
+
|
|
1037
|
+
elif choice == "n":
|
|
831
1038
|
print(f"Ignored all files from {directory}")
|
|
832
1039
|
return [], current_char_count
|
|
833
|
-
|
|
834
|
-
elif choice ==
|
|
1040
|
+
|
|
1041
|
+
elif choice == "s":
|
|
835
1042
|
for file in files:
|
|
836
1043
|
file_path = os.path.join(directory, file)
|
|
837
1044
|
if os.path.abspath(file_path) in selected_files_set:
|
|
838
1045
|
continue # Skip already selected files
|
|
839
|
-
|
|
1046
|
+
|
|
840
1047
|
file_size = os.path.getsize(file_path)
|
|
841
1048
|
file_size_readable = get_human_readable_size(file_size)
|
|
842
1049
|
file_char_estimate = file_size
|
|
@@ -844,89 +1051,137 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
844
1051
|
while True:
|
|
845
1052
|
if current_char_count > 0:
|
|
846
1053
|
print_char_count(current_char_count)
|
|
847
|
-
file_choice = input(
|
|
848
|
-
|
|
1054
|
+
file_choice = input(
|
|
1055
|
+
f"{file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) (y/n/p/q)? "
|
|
1056
|
+
).lower()
|
|
1057
|
+
if file_choice == "y":
|
|
849
1058
|
file_to_add = None
|
|
850
1059
|
if is_large_file(file_path):
|
|
851
1060
|
while True:
|
|
852
|
-
snippet_choice = input(
|
|
853
|
-
|
|
1061
|
+
snippet_choice = input(
|
|
1062
|
+
f"{file} is large. Use (f)ull content or (s)nippet? "
|
|
1063
|
+
).lower()
|
|
1064
|
+
if snippet_choice in ["f", "s"]:
|
|
854
1065
|
break
|
|
855
1066
|
print("Invalid choice. Please enter 'f' or 's'.")
|
|
856
|
-
if snippet_choice ==
|
|
857
|
-
file_to_add = (
|
|
1067
|
+
if snippet_choice == "s":
|
|
1068
|
+
file_to_add = (
|
|
1069
|
+
file_path,
|
|
1070
|
+
True,
|
|
1071
|
+
None,
|
|
1072
|
+
get_language_for_file(file_path),
|
|
1073
|
+
)
|
|
858
1074
|
current_char_count += len(get_file_snippet(file_path))
|
|
859
1075
|
else:
|
|
860
|
-
file_to_add = (
|
|
1076
|
+
file_to_add = (
|
|
1077
|
+
file_path,
|
|
1078
|
+
False,
|
|
1079
|
+
None,
|
|
1080
|
+
get_language_for_file(file_path),
|
|
1081
|
+
)
|
|
861
1082
|
current_char_count += file_char_estimate
|
|
862
1083
|
else:
|
|
863
|
-
file_to_add = (
|
|
1084
|
+
file_to_add = (
|
|
1085
|
+
file_path,
|
|
1086
|
+
False,
|
|
1087
|
+
None,
|
|
1088
|
+
get_language_for_file(file_path),
|
|
1089
|
+
)
|
|
864
1090
|
current_char_count += file_char_estimate
|
|
865
|
-
|
|
1091
|
+
|
|
866
1092
|
if file_to_add:
|
|
867
1093
|
selected_files.append(file_to_add)
|
|
868
1094
|
selected_files_set.add(os.path.abspath(file_path))
|
|
869
1095
|
# Analyze dependencies immediately after adding
|
|
870
|
-
new_deps, deps_char_count = _propose_and_add_dependencies(
|
|
1096
|
+
new_deps, deps_char_count = _propose_and_add_dependencies(
|
|
1097
|
+
file_path,
|
|
1098
|
+
project_root_abs,
|
|
1099
|
+
selected_files,
|
|
1100
|
+
current_char_count,
|
|
1101
|
+
)
|
|
871
1102
|
selected_files.extend(new_deps)
|
|
872
1103
|
current_char_count += deps_char_count
|
|
873
1104
|
break
|
|
874
|
-
elif file_choice ==
|
|
1105
|
+
elif file_choice == "n":
|
|
875
1106
|
break
|
|
876
|
-
elif file_choice ==
|
|
1107
|
+
elif file_choice == "p":
|
|
877
1108
|
chunks, char_count = select_file_patches(file_path)
|
|
878
1109
|
if chunks:
|
|
879
|
-
selected_files.append(
|
|
1110
|
+
selected_files.append(
|
|
1111
|
+
(
|
|
1112
|
+
file_path,
|
|
1113
|
+
False,
|
|
1114
|
+
chunks,
|
|
1115
|
+
get_language_for_file(file_path),
|
|
1116
|
+
)
|
|
1117
|
+
)
|
|
880
1118
|
current_char_count += char_count
|
|
881
1119
|
selected_files_set.add(os.path.abspath(file_path))
|
|
882
1120
|
break
|
|
883
|
-
elif file_choice ==
|
|
1121
|
+
elif file_choice == "q":
|
|
884
1122
|
print(f"Quitting selection for {directory}")
|
|
885
1123
|
return selected_files, current_char_count
|
|
886
1124
|
else:
|
|
887
1125
|
print("Invalid choice. Please enter 'y', 'n', 'p', or 'q'.")
|
|
888
1126
|
print(f"Added {len(selected_files)} files from {directory}")
|
|
889
1127
|
return selected_files, current_char_count
|
|
890
|
-
|
|
891
|
-
elif choice ==
|
|
1128
|
+
|
|
1129
|
+
elif choice == "q":
|
|
892
1130
|
print(f"Quitting selection for {directory}")
|
|
893
1131
|
return [], current_char_count
|
|
894
1132
|
else:
|
|
895
1133
|
print("Invalid choice. Please try again.")
|
|
896
1134
|
|
|
897
1135
|
|
|
898
|
-
def process_directory(
|
|
1136
|
+
def process_directory(
|
|
1137
|
+
directory: str,
|
|
1138
|
+
ignore_patterns: List[str],
|
|
1139
|
+
project_root_abs: str,
|
|
1140
|
+
current_char_count: int = 0,
|
|
1141
|
+
selected_files_set: Optional[Set[str]] = None,
|
|
1142
|
+
) -> Tuple[List[FileTuple], Set[str], int]:
|
|
899
1143
|
if selected_files_set is None:
|
|
900
1144
|
selected_files_set = set()
|
|
901
|
-
|
|
1145
|
+
|
|
902
1146
|
files_to_include: List[FileTuple] = []
|
|
903
1147
|
processed_dirs: Set[str] = set()
|
|
904
1148
|
|
|
905
1149
|
for root, dirs, files in os.walk(directory):
|
|
906
|
-
dirs[:] = [
|
|
907
|
-
|
|
1150
|
+
dirs[:] = [
|
|
1151
|
+
d for d in dirs if not is_ignored(os.path.join(root, d), ignore_patterns)
|
|
1152
|
+
]
|
|
1153
|
+
files = [
|
|
1154
|
+
f
|
|
1155
|
+
for f in files
|
|
1156
|
+
if not is_ignored(os.path.join(root, f), ignore_patterns)
|
|
1157
|
+
and not is_binary(os.path.join(root, f))
|
|
1158
|
+
]
|
|
908
1159
|
|
|
909
1160
|
if root in processed_dirs:
|
|
910
1161
|
continue
|
|
911
1162
|
|
|
912
1163
|
print(f"\nExploring directory: {root}")
|
|
913
1164
|
choice = input("(y)es explore / (n)o skip / (q)uit? ").lower()
|
|
914
|
-
if choice ==
|
|
1165
|
+
if choice == "y":
|
|
915
1166
|
# Pass selected_files_set to track already selected files
|
|
916
1167
|
selected_files, current_char_count = select_files_in_directory(
|
|
917
|
-
root,
|
|
1168
|
+
root,
|
|
1169
|
+
ignore_patterns,
|
|
1170
|
+
project_root_abs,
|
|
1171
|
+
current_char_count,
|
|
1172
|
+
selected_files_set,
|
|
918
1173
|
)
|
|
919
1174
|
files_to_include.extend(selected_files)
|
|
920
|
-
|
|
1175
|
+
|
|
921
1176
|
# Update selected_files_set
|
|
922
1177
|
for file_tuple in selected_files:
|
|
923
1178
|
selected_files_set.add(os.path.abspath(file_tuple[0]))
|
|
924
|
-
|
|
1179
|
+
|
|
925
1180
|
processed_dirs.add(root)
|
|
926
|
-
elif choice ==
|
|
1181
|
+
elif choice == "n":
|
|
927
1182
|
dirs[:] = [] # Skip all subdirectories
|
|
928
1183
|
continue
|
|
929
|
-
elif choice ==
|
|
1184
|
+
elif choice == "q":
|
|
930
1185
|
break
|
|
931
1186
|
else:
|
|
932
1187
|
print("Invalid choice. Skipping this directory.")
|
|
@@ -934,20 +1189,25 @@ def process_directory(directory: str, ignore_patterns: List[str], project_root_a
|
|
|
934
1189
|
|
|
935
1190
|
return files_to_include, processed_dirs, current_char_count
|
|
936
1191
|
|
|
937
|
-
|
|
1192
|
+
|
|
1193
|
+
def fetch_web_content(
|
|
1194
|
+
url: str,
|
|
1195
|
+
) -> Tuple[Optional[FileTuple], Optional[str], Optional[str]]:
|
|
938
1196
|
try:
|
|
939
1197
|
response = requests.get(url)
|
|
940
1198
|
response.raise_for_status()
|
|
941
|
-
content_type = response.headers.get(
|
|
1199
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
942
1200
|
full_content = response.text
|
|
943
|
-
snippet =
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
content_type =
|
|
1201
|
+
snippet = (
|
|
1202
|
+
full_content[:10000] + "..." if len(full_content) > 10000 else full_content
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
if "json" in content_type:
|
|
1206
|
+
content_type = "json"
|
|
1207
|
+
elif "csv" in content_type:
|
|
1208
|
+
content_type = "csv"
|
|
949
1209
|
else:
|
|
950
|
-
content_type =
|
|
1210
|
+
content_type = "text"
|
|
951
1211
|
|
|
952
1212
|
return (url, False, None, content_type), full_content, snippet
|
|
953
1213
|
except requests.RequestException as e:
|
|
@@ -957,50 +1217,67 @@ def fetch_web_content(url: str) -> Tuple[Optional[FileTuple], Optional[str], Opt
|
|
|
957
1217
|
|
|
958
1218
|
def read_env_file():
|
|
959
1219
|
env_vars = {}
|
|
960
|
-
if os.path.exists(
|
|
961
|
-
with open(
|
|
1220
|
+
if os.path.exists(".env"):
|
|
1221
|
+
with open(".env", "r") as env_file:
|
|
962
1222
|
for line in env_file:
|
|
963
1223
|
line = line.strip()
|
|
964
|
-
if line and not line.startswith(
|
|
965
|
-
key, value = line.split(
|
|
1224
|
+
if line and not line.startswith("#"):
|
|
1225
|
+
key, value = line.split("=", 1)
|
|
966
1226
|
env_vars[key.strip()] = value.strip()
|
|
967
1227
|
return env_vars
|
|
968
1228
|
|
|
1229
|
+
|
|
969
1230
|
def open_editor_for_input(template: str, cursor_position: int) -> str:
|
|
970
|
-
editor = os.environ.get(
|
|
971
|
-
with tempfile.NamedTemporaryFile(
|
|
1231
|
+
editor = os.environ.get("EDITOR", "vim")
|
|
1232
|
+
with tempfile.NamedTemporaryFile(
|
|
1233
|
+
mode="w+", suffix=".md", delete=False
|
|
1234
|
+
) as temp_file:
|
|
972
1235
|
temp_file.write(template)
|
|
973
1236
|
temp_file.flush()
|
|
974
1237
|
temp_file_path = temp_file.name
|
|
975
1238
|
|
|
976
1239
|
try:
|
|
977
|
-
cursor_line = template[:cursor_position].count(
|
|
978
|
-
cursor_column = cursor_position - template.rfind(
|
|
979
|
-
|
|
980
|
-
if
|
|
981
|
-
subprocess.call(
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1240
|
+
cursor_line = template[:cursor_position].count("\n") + 1
|
|
1241
|
+
cursor_column = cursor_position - template.rfind("\n", 0, cursor_position)
|
|
1242
|
+
|
|
1243
|
+
if "vim" in editor or "nvim" in editor:
|
|
1244
|
+
subprocess.call(
|
|
1245
|
+
[
|
|
1246
|
+
editor,
|
|
1247
|
+
f"+call cursor({cursor_line}, {cursor_column})",
|
|
1248
|
+
"+startinsert",
|
|
1249
|
+
temp_file_path,
|
|
1250
|
+
]
|
|
1251
|
+
)
|
|
1252
|
+
elif "emacs" in editor:
|
|
1253
|
+
subprocess.call([editor, f"+{cursor_line}:{cursor_column}", temp_file_path])
|
|
1254
|
+
elif "nano" in editor:
|
|
1255
|
+
subprocess.call([editor, f"+{cursor_line},{cursor_column}", temp_file_path])
|
|
986
1256
|
else:
|
|
987
1257
|
subprocess.call([editor, temp_file_path])
|
|
988
1258
|
|
|
989
|
-
with open(temp_file_path,
|
|
1259
|
+
with open(temp_file_path, "r") as file:
|
|
990
1260
|
content = file.read()
|
|
991
1261
|
return content
|
|
992
1262
|
finally:
|
|
993
1263
|
os.unlink(temp_file_path)
|
|
994
1264
|
|
|
1265
|
+
|
|
995
1266
|
def main():
|
|
996
|
-
parser = argparse.ArgumentParser(
|
|
997
|
-
|
|
998
|
-
|
|
1267
|
+
parser = argparse.ArgumentParser(
|
|
1268
|
+
description="Generate a prompt with project structure, file contents, and web content."
|
|
1269
|
+
)
|
|
1270
|
+
parser.add_argument(
|
|
1271
|
+
"inputs",
|
|
1272
|
+
nargs="*",
|
|
1273
|
+
help="Files, directories, or URLs to include. Defaults to current directory.",
|
|
1274
|
+
)
|
|
1275
|
+
parser.add_argument("-t", "--task", help="Task description for the AI prompt")
|
|
999
1276
|
args = parser.parse_args()
|
|
1000
1277
|
|
|
1001
1278
|
# Default to the current directory if no inputs are provided
|
|
1002
1279
|
if not args.inputs:
|
|
1003
|
-
args.inputs.append(
|
|
1280
|
+
args.inputs.append(".")
|
|
1004
1281
|
|
|
1005
1282
|
ignore_patterns = read_gitignore()
|
|
1006
1283
|
env_vars = read_env_file()
|
|
@@ -1009,13 +1286,13 @@ def main():
|
|
|
1009
1286
|
files_to_include: List[FileTuple] = []
|
|
1010
1287
|
web_contents: Dict[str, Tuple[FileTuple, str]] = {}
|
|
1011
1288
|
current_char_count = 0
|
|
1012
|
-
|
|
1289
|
+
|
|
1013
1290
|
# Separate URLs from file/directory paths
|
|
1014
1291
|
paths_for_tree = []
|
|
1015
1292
|
files_to_preselect = []
|
|
1016
|
-
|
|
1293
|
+
|
|
1017
1294
|
for input_path in args.inputs:
|
|
1018
|
-
if input_path.startswith((
|
|
1295
|
+
if input_path.startswith(("http://", "https://")):
|
|
1019
1296
|
# Handle web content as before
|
|
1020
1297
|
result = fetch_web_content(input_path)
|
|
1021
1298
|
if result:
|
|
@@ -1024,15 +1301,15 @@ def main():
|
|
|
1024
1301
|
if is_large:
|
|
1025
1302
|
print(f"\nContent from {input_path} is large. Here's a snippet:\n")
|
|
1026
1303
|
print(get_colored_code(input_path, snippet))
|
|
1027
|
-
print("\n" + "-"*40 + "\n")
|
|
1304
|
+
print("\n" + "-" * 40 + "\n")
|
|
1028
1305
|
|
|
1029
1306
|
while True:
|
|
1030
1307
|
choice = input("Use (f)ull content or (s)nippet? ").lower()
|
|
1031
|
-
if choice in [
|
|
1308
|
+
if choice in ["f", "s"]:
|
|
1032
1309
|
break
|
|
1033
1310
|
print("Invalid choice. Please enter 'f' or 's'.")
|
|
1034
1311
|
|
|
1035
|
-
if choice ==
|
|
1312
|
+
if choice == "f":
|
|
1036
1313
|
content = full_content
|
|
1037
1314
|
is_snippet = False
|
|
1038
1315
|
print("Using full content.")
|
|
@@ -1043,12 +1320,16 @@ def main():
|
|
|
1043
1320
|
else:
|
|
1044
1321
|
content = full_content
|
|
1045
1322
|
is_snippet = False
|
|
1046
|
-
print(
|
|
1323
|
+
print(
|
|
1324
|
+
f"Content from {input_path} is not large. Using full content."
|
|
1325
|
+
)
|
|
1047
1326
|
|
|
1048
1327
|
file_tuple = (file_tuple[0], is_snippet, file_tuple[2], file_tuple[3])
|
|
1049
1328
|
web_contents[input_path] = (file_tuple, content)
|
|
1050
1329
|
current_char_count += len(content)
|
|
1051
|
-
print(
|
|
1330
|
+
print(
|
|
1331
|
+
f"Added {'snippet of ' if is_snippet else ''}web content from: {input_path}"
|
|
1332
|
+
)
|
|
1052
1333
|
print_char_count(current_char_count)
|
|
1053
1334
|
else:
|
|
1054
1335
|
abs_path = os.path.abspath(input_path)
|
|
@@ -1058,15 +1339,19 @@ def main():
|
|
|
1058
1339
|
files_to_preselect.append(abs_path)
|
|
1059
1340
|
else:
|
|
1060
1341
|
print(f"Warning: {input_path} does not exist. Skipping.")
|
|
1061
|
-
|
|
1342
|
+
|
|
1062
1343
|
# Use tree selector for file/directory selection
|
|
1063
1344
|
if paths_for_tree:
|
|
1064
1345
|
print("\nStarting interactive file selection...")
|
|
1065
|
-
print(
|
|
1066
|
-
|
|
1346
|
+
print(
|
|
1347
|
+
"Use arrow keys to navigate, Space to select, 'q' to finish. See all keys below.\n"
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1067
1350
|
tree_selector = TreeSelector(ignore_patterns, project_root_abs)
|
|
1068
1351
|
try:
|
|
1069
|
-
selected_files, file_char_count = tree_selector.run(
|
|
1352
|
+
selected_files, file_char_count = tree_selector.run(
|
|
1353
|
+
paths_for_tree, files_to_preselect
|
|
1354
|
+
)
|
|
1070
1355
|
files_to_include.extend(selected_files)
|
|
1071
1356
|
current_char_count += file_char_count
|
|
1072
1357
|
except KeyboardInterrupt:
|
|
@@ -1086,18 +1371,31 @@ def main():
|
|
|
1086
1371
|
|
|
1087
1372
|
added_files_count = len(files_to_include)
|
|
1088
1373
|
added_web_count = len(web_contents)
|
|
1089
|
-
print(
|
|
1374
|
+
print(
|
|
1375
|
+
f"Summary: Added {added_files_count} files/patches and {added_web_count} web sources."
|
|
1376
|
+
)
|
|
1090
1377
|
|
|
1091
|
-
prompt_template, cursor_position = generate_prompt_template(
|
|
1378
|
+
prompt_template, cursor_position = generate_prompt_template(
|
|
1379
|
+
files_to_include, ignore_patterns, web_contents, env_vars
|
|
1380
|
+
)
|
|
1092
1381
|
|
|
1093
1382
|
if args.task:
|
|
1094
1383
|
task_description = args.task
|
|
1095
1384
|
task_marker = "## Task Instructions\n\n"
|
|
1096
1385
|
insertion_point = prompt_template.find(task_marker)
|
|
1097
1386
|
if insertion_point != -1:
|
|
1098
|
-
final_prompt =
|
|
1387
|
+
final_prompt = (
|
|
1388
|
+
prompt_template[: insertion_point + len(task_marker)]
|
|
1389
|
+
+ task_description
|
|
1390
|
+
+ "\n\n"
|
|
1391
|
+
+ prompt_template[insertion_point + len(task_marker) :]
|
|
1392
|
+
)
|
|
1099
1393
|
else:
|
|
1100
|
-
final_prompt =
|
|
1394
|
+
final_prompt = (
|
|
1395
|
+
prompt_template[:cursor_position]
|
|
1396
|
+
+ task_description
|
|
1397
|
+
+ prompt_template[cursor_position:]
|
|
1398
|
+
)
|
|
1101
1399
|
print("\nUsing task description from -t argument.")
|
|
1102
1400
|
else:
|
|
1103
1401
|
print("\nOpening editor for task instructions...")
|
|
@@ -1111,13 +1409,15 @@ def main():
|
|
|
1111
1409
|
try:
|
|
1112
1410
|
pyperclip.copy(final_prompt)
|
|
1113
1411
|
print("\n--- Included Files & Content ---\n")
|
|
1114
|
-
for file_path, is_snippet, chunks, _ in sorted(
|
|
1412
|
+
for file_path, is_snippet, chunks, _ in sorted(
|
|
1413
|
+
files_to_include, key=lambda x: x[0]
|
|
1414
|
+
):
|
|
1115
1415
|
details = []
|
|
1116
1416
|
if is_snippet:
|
|
1117
1417
|
details.append("snippet")
|
|
1118
1418
|
if chunks is not None:
|
|
1119
1419
|
details.append(f"{len(chunks)} patches")
|
|
1120
|
-
|
|
1420
|
+
|
|
1121
1421
|
detail_str = f" ({', '.join(details)})" if details else ""
|
|
1122
1422
|
print(f"- {os.path.relpath(file_path)}{detail_str}")
|
|
1123
1423
|
|
|
@@ -1125,16 +1425,25 @@ def main():
|
|
|
1125
1425
|
is_snippet = file_tuple[1]
|
|
1126
1426
|
detail_str = " (snippet)" if is_snippet else ""
|
|
1127
1427
|
print(f"- {url}{detail_str}")
|
|
1128
|
-
|
|
1129
|
-
separator =
|
|
1428
|
+
|
|
1429
|
+
separator = (
|
|
1430
|
+
"\n"
|
|
1431
|
+
+ "=" * 40
|
|
1432
|
+
+ "\n☕🍝 Kopipasta Complete! 🍝☕\n"
|
|
1433
|
+
+ "=" * 40
|
|
1434
|
+
+ "\n"
|
|
1435
|
+
)
|
|
1130
1436
|
print(separator)
|
|
1131
|
-
|
|
1437
|
+
|
|
1132
1438
|
final_char_count = len(final_prompt)
|
|
1133
1439
|
final_token_estimate = final_char_count // 4
|
|
1134
|
-
print(
|
|
1440
|
+
print(
|
|
1441
|
+
f"Prompt has been copied to clipboard. Final size: {final_char_count} characters (~ {final_token_estimate} tokens)"
|
|
1442
|
+
)
|
|
1135
1443
|
except pyperclip.PyperclipException as e:
|
|
1136
1444
|
print(f"\nWarning: Failed to copy to clipboard: {e}")
|
|
1137
1445
|
print("You can manually copy the prompt above.")
|
|
1138
1446
|
|
|
1447
|
+
|
|
1139
1448
|
if __name__ == "__main__":
|
|
1140
|
-
main()
|
|
1449
|
+
main()
|