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