kopipasta 0.35.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/main.py CHANGED
@@ -14,24 +14,36 @@ import pygments.util
14
14
 
15
15
  import requests
16
16
 
17
- from kopipasta.file import FileTuple, get_human_readable_size, is_binary, is_ignored, is_large_file, read_file_contents
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 generate_prompt_template, get_file_snippet, get_language_for_file
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 ['python', 'typescript', 'javascript', 'tsx', 'jsx']:
34
- return [], 0 # Only analyze languages we can parse
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 == 'python':
45
- resolved_deps_abs = import_parser.parse_python_imports(file_content, file_just_added, project_root_abs)
46
- elif language in ['typescript', 'javascript', 'tsx', 'jsx']:
47
- resolved_deps_abs = import_parser.parse_typescript_imports(file_content, file_just_added, project_root_abs)
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
- dep for dep in resolved_deps_abs
53
- if os.path.abspath(dep) not in included_paths and os.path.abspath(dep) != os.path.abspath(file_just_added)
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(f"\nFound {len(suggested_deps)} new local {'dependency' if len(suggested_deps) == 1 else 'dependencies'}:")
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("\nAdd dependencies? (a)ll, (n)one, or enter numbers (e.g. 1, 3-4): ").lower()
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 == 'a':
92
+ if choice == "a":
69
93
  deps_to_add_paths = suggested_deps
70
94
  break
71
- if choice == 'n':
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(' ', '').split(',')
80
- if all(p.strip() for p in parts): # Ensure no empty parts like in "1,"
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 '-' in part:
83
- start_str, end_str = part.split('-', 1)
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 # Success! Exit the loop.
121
+ break # Success! Exit the loop.
98
122
  else:
99
- print(f"Error: Invalid number selection. Please choose numbers between 1 and {len(suggested_deps)}.")
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("Invalid choice. Please enter 'a', 'n', or a list/range of numbers (e.g., '1,3' or '2-4').")
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 # No dependencies were selected
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((dep_path, False, None, get_language_for_file(dep_path)))
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(f"Added dependency: {os.path.relpath(dep_path)} ({get_human_readable_size(file_size)})")
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(f"Warning: Could not analyze dependencies for {os.path.relpath(file_just_added)}: {e}")
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
- try:
128
- lexer = get_lexer_for_filename(file_path)
129
- except pygments.util.ClassNotFound:
130
- lexer = TextLexer()
131
- return highlight(code, lexer, TerminalFormatter())
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
- '.git', 'node_modules', 'venv', '.venv', 'dist', '.idea', '__pycache__',
136
- '*.pyc', '.ruff_cache', '.mypy_cache', '.pytest_cache', '.vscode', '.vite',
137
- '.terraform', 'output', 'poetry.lock', 'package-lock.json', '.env',
138
- '*.log', '*.bak', '*.swp', '*.swo', '*.tmp', 'tmp', 'temp', 'logs',
139
- 'build', 'target', '.DS_Store', 'Thumbs.db', '*.class', '*.jar',
140
- '*.war', '*.ear', '*.sqlite', '*.db',
141
- '*.jpg', '*.jpeg', '*.png', '*.gif', '*.bmp', '*.tiff',
142
- '*.ico', '*.svg', '*.webp', '*.mp3', '*.mp4', '*.avi',
143
- '*.mov', '*.wmv', '*.flv', '*.pdf', '*.doc', '*.docx',
144
- '*.xls', '*.xlsx', '*.ppt', '*.pptx', '*.zip', '*.rar',
145
- '*.tar', '*.gz', '*.7z', '*.exe', '*.dll', '*.so', '*.dylib'
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('.gitignore'):
240
+ if os.path.exists(".gitignore"):
150
241
  print(".gitignore detected.")
151
- with open('.gitignore', 'r') as file:
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 ''.join(lines[start:end])
264
+ return "".join(lines[start:end])
172
265
 
173
- nodes = [node for node in ast.iter_child_nodes(tree) if hasattr(node, 'lineno')]
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, 'end_lineno', None)
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, 'end_lineno', None) or next_start + 1
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'^\s*(export\s+)?(async\s+)?(function\s+\w+|class\s+\w+|\w+\s*=\s*\(.*?\)\s*=>)',
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('\n', 0, start_index)
357
+ start_line = file_content.count("\n", 0, start_index)
263
358
  if prev_end_line < start_line:
264
- code = ''.join(lines[prev_end_line:start_line])
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('{') - line.count('}')
274
- if '{' in line:
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 = ''.join(function_code_lines)
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 = ''.join(function_code_lines)
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 = ''.join(lines[prev_end_line:])
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'<(?P<tag>\w+)(\s|>).*?</(?P=tag)>', re.DOTALL)
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('\n', 0, start_index)
312
- end_line = file_content.count('\n', 0, end_index)
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 = ''.join(lines[prev_end:start_line])
411
+ code = "".join(lines[prev_end:start_line])
316
412
  chunks.append((code, prev_end, start_line))
317
413
 
318
- code = ''.join(lines[start_line:end_line])
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 = ''.join(lines[prev_end:])
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'^\s*(?:[\w\*\s]+)\s+(\w+)\s*\([^)]*\)\s*\{', re.MULTILINE)
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('\n', 0, start_index)
441
+ start_line = file_content.count("\n", 0, start_index)
345
442
  if prev_end_line < start_line:
346
- code = ''.join(lines[prev_end_line:start_line])
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('{') - line.count('}')
356
- if '{' in line:
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 = ''.join(function_code_lines)
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 = ''.join(function_code_lines)
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 = ''.join(lines[prev_end_line:])
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 = ''.join(lines[start:i])
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 = ''.join(lines[start:])
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 == 'python':
500
+ if language == "python":
402
501
  code_chunks = split_python_file(file_content)
403
- elif language == 'javascript':
502
+ elif language == "javascript":
404
503
  code_chunks = split_javascript_file(file_content)
405
- elif language == 'html':
504
+ elif language == "html":
406
505
  code_chunks = split_html_file(file_content)
407
- elif language in ['c', 'cpp']:
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 == 'y':
519
+ if choice == "y":
421
520
  chunks.append(chunk_code)
422
521
  total_char_count += len(chunk_code)
423
522
  break
424
- elif choice == 'n':
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 == 'q':
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
- 'python': '# Skipped content\n',
442
- 'javascript': '// Skipped content\n',
443
- 'typescript': '// Skipped content\n',
444
- 'java': '// Skipped content\n',
445
- 'c': '// Skipped content\n',
446
- 'cpp': '// Skipped content\n',
447
- 'html': '<!-- Skipped content -->\n',
448
- 'css': '/* Skipped content */\n',
449
- 'default': '# Skipped content\n'
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['default'])
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(f"\rCurrent prompt size: {count} characters (~ {token_estimate} tokens)", flush=True)
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(pattern: str, directory: str, ignore_patterns: List[str]) -> List[Tuple[str, List[str], int]]:
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('ag'):
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
- 'ag',
478
- '--files-with-matches',
479
- '--nocolor',
480
- '--ignore-case',
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('\n')
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 = ['ag', '--count', '--nocolor', pattern, file]
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
- 'ag',
517
- '--max-count=3',
518
- '--nocolor',
519
- '--noheading',
520
- '--numbers',
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('\n')[:3]:
635
+ for line in preview_result.stdout.strip().split("\n")[:3]:
528
636
  # Format: "line_num:content"
529
- if ':' in line:
530
- line_num, content = line.split(':', 1)
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(f"\n{i+1}. {os.path.relpath(file_path)} ({file_size_readable}) - {match_count} {'match' if match_count == 1 else 'matches'}")
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("\nSelect grep results: (a)ll / (n)one / (s)elect individually / numbers (e.g. 1,3-4) / (q)uit? ").lower()
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 == 'a':
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((file_path, False, None, get_language_for_file(file_path)))
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 == 'n':
692
+
693
+ elif choice == "n":
580
694
  print("Skipped all grep results.")
581
695
  return [], current_char_count
582
-
583
- elif choice == 'q':
696
+
697
+ elif choice == "q":
584
698
  print("Cancelled grep selection.")
585
699
  return [], current_char_count
586
-
587
- elif choice == 's':
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(f"\n{os.path.relpath(file_path)} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens)")
595
- print(f"{match_count} {'match' if match_count == 1 else 'matches'} for search pattern")
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 == 'y':
718
+
719
+ if file_choice == "y":
602
720
  if is_large_file(file_path):
603
721
  while True:
604
- snippet_choice = input(f"File is large. Use (f)ull content or (s)nippet? ").lower()
605
- if snippet_choice in ['f', 's']:
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 == 's':
609
- selected_files.append((file_path, True, None, get_language_for_file(file_path)))
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((file_path, False, None, get_language_for_file(file_path)))
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((file_path, False, None, get_language_for_file(file_path)))
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 == 'n':
760
+ elif file_choice == "n":
620
761
  break
621
- elif file_choice == 'q':
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(' ', '').split(',')
775
+ parts = choice.replace(" ", "").split(",")
635
776
  if all(p.strip() for p in parts):
636
777
  for part in parts:
637
- if '-' in part:
638
- start_str, end_str = part.split('-', 1)
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((file_path, False, None, get_language_for_file(file_path)))
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(f"Error: Invalid number selection. Please choose numbers between 1 and {len(grep_results)}.")
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("Invalid choice. Please enter 'a', 'n', 's', 'q', or a list/range of numbers.")
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(directory: str, ignore_patterns: List[str], project_root_abs: str, current_char_count: int = 0, selected_files_set: Optional[Set[str]] = None) -> Tuple[List[FileTuple], int]:
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 = [f for f in os.listdir(directory)
667
- if os.path.isfile(os.path.join(directory, f)) and not is_ignored(os.path.join(directory, f), ignore_patterns) and not is_binary(os.path.join(directory, f))]
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(f"✓ {file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) [already selected]")
847
+ print(
848
+ f"✓ {file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) [already selected]"
849
+ )
684
850
  else:
685
- print(f"- {file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens)")
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("(y)es add all / (n)o ignore all / (s)elect individually / (g)rep / (q)uit? ").lower()
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 == 'g':
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(pattern, directory, ignore_patterns)
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(grep_results, current_char_count)
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, project_root_abs, selected_files, current_char_count
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 = [f for f in files if os.path.abspath(os.path.join(directory, f)) not in selected_files_set]
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("(y)es add remaining / (n)o skip remaining / (s)elect more / (g)rep again / (q)uit? ").lower()
745
- if remaining_choice == 'y':
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((file_path, False, None, get_language_for_file(file_path)))
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
- new_deps, deps_char_count = _propose_and_add_dependencies(
758
- file_path, project_root_abs, selected_files, current_char_count
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 == 'n':
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 == 's':
965
+ elif remaining_choice == "s":
769
966
  # Continue to individual selection
770
- choice = 's'
967
+ choice = "s"
771
968
  break
772
- elif remaining_choice == 'g':
969
+ elif remaining_choice == "g":
773
970
  # Continue to grep again
774
- choice = 'g'
971
+ choice = "g"
775
972
  break
776
- elif remaining_choice == 'q':
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 == 's':
977
+
978
+ if choice == "s":
782
979
  # Fall through to individual selection
783
980
  pass
784
- elif choice == 'g':
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 == 'y':
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(f"{file} is large. Use (f)ull content or (s)nippet? ").lower()
806
- if snippet_choice in ['f', 's']:
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 == 's':
810
- selected_files.append((file_path, True, None, get_language_for_file(file_path)))
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((file_path, False, None, get_language_for_file(file_path)))
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((file_path, False, None, get_language_for_file(file_path)))
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
- new_deps, deps_char_count = _propose_and_add_dependencies(file_path, project_root_abs, selected_files, current_char_count)
824
- selected_files.extend(new_deps)
825
- current_char_count += deps_char_count
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 == 'n':
1036
+
1037
+ elif choice == "n":
831
1038
  print(f"Ignored all files from {directory}")
832
1039
  return [], current_char_count
833
-
834
- elif choice == 's':
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(f"{file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) (y/n/p/q)? ").lower()
848
- if file_choice == 'y':
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(f"{file} is large. Use (f)ull content or (s)nippet? ").lower()
853
- if snippet_choice in ['f', 's']:
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 == 's':
857
- file_to_add = (file_path, True, None, get_language_for_file(file_path))
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 = (file_path, False, None, get_language_for_file(file_path))
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 = (file_path, False, None, get_language_for_file(file_path))
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(file_path, project_root_abs, selected_files, current_char_count)
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 == 'n':
1105
+ elif file_choice == "n":
875
1106
  break
876
- elif file_choice == 'p':
1107
+ elif file_choice == "p":
877
1108
  chunks, char_count = select_file_patches(file_path)
878
1109
  if chunks:
879
- selected_files.append((file_path, False, chunks, get_language_for_file(file_path)))
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 == 'q':
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 == 'q':
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(directory: str, ignore_patterns: List[str], project_root_abs: str, current_char_count: int = 0, selected_files_set: Optional[Set[str]] = None) -> Tuple[List[FileTuple], Set[str], int]:
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[:] = [d for d in dirs if not is_ignored(os.path.join(root, d), ignore_patterns)]
907
- files = [f for f in files if not is_ignored(os.path.join(root, f), ignore_patterns) and not is_binary(os.path.join(root, f))]
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 == 'y':
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, ignore_patterns, project_root_abs, current_char_count, selected_files_set
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 == 'n':
1181
+ elif choice == "n":
927
1182
  dirs[:] = [] # Skip all subdirectories
928
1183
  continue
929
- elif choice == 'q':
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
- def fetch_web_content(url: str) -> Tuple[Optional[FileTuple], Optional[str], Optional[str]]:
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('content-type', '').lower()
1199
+ content_type = response.headers.get("content-type", "").lower()
942
1200
  full_content = response.text
943
- snippet = full_content[:10000] + "..." if len(full_content) > 10000 else full_content
944
-
945
- if 'json' in content_type:
946
- content_type = 'json'
947
- elif 'csv' in content_type:
948
- content_type = 'csv'
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 = 'text'
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('.env'):
961
- with open('.env', 'r') as env_file:
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('=', 1)
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('EDITOR', 'vim')
971
- with tempfile.NamedTemporaryFile(mode='w+', suffix='.md', delete=False) as temp_file:
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('\n') + 1
978
- cursor_column = cursor_position - template.rfind('\n', 0, cursor_position)
979
-
980
- if 'vim' in editor or 'nvim' in editor:
981
- subprocess.call([editor, f'+call cursor({cursor_line}, {cursor_column})', '+startinsert', temp_file_path])
982
- elif 'emacs' in editor:
983
- subprocess.call([editor, f'+{cursor_line}:{cursor_column}', temp_file_path])
984
- elif 'nano' in editor:
985
- subprocess.call([editor, f'+{cursor_line},{cursor_column}', temp_file_path])
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, 'r') as file:
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(description="Generate a prompt with project structure, file contents, and web content.")
997
- parser.add_argument('inputs', nargs='*', help='Files, directories, or URLs to include. Defaults to current directory.')
998
- parser.add_argument('-t', '--task', help='Task description for the AI prompt')
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(('http://', 'https://')):
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 ['f', 's']:
1308
+ if choice in ["f", "s"]:
1032
1309
  break
1033
1310
  print("Invalid choice. Please enter 'f' or 's'.")
1034
1311
 
1035
- if choice == 'f':
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(f"Content from {input_path} is not large. Using full content.")
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(f"Added {'snippet of ' if is_snippet else ''}web content from: {input_path}")
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("Use arrow keys to navigate, Space to select, 'q' to finish. See all keys below.\n")
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(paths_for_tree, files_to_preselect)
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(f"Summary: Added {added_files_count} files/patches and {added_web_count} web sources.")
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(files_to_include, ignore_patterns, web_contents, env_vars)
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 = prompt_template[:insertion_point + len(task_marker)] + task_description + "\n\n" + prompt_template[insertion_point + len(task_marker):]
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 = prompt_template[:cursor_position] + task_description + prompt_template[cursor_position:]
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(files_to_include, key=lambda x: x[0]):
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 = "\n" + "=" * 40 + "\n☕🍝 Kopipasta Complete! 🍝☕\n" + "=" * 40 + "\n"
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(f"Prompt has been copied to clipboard. Final size: {final_char_count} characters (~ {final_token_estimate} tokens)")
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()