kopipasta 0.26.0__tar.gz → 0.27.0__tar.gz

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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -1,5 +1,3 @@
1
- # kopipasta/import_parser.py
2
-
3
1
  import os
4
2
  import re
5
3
  import json
@@ -24,6 +24,8 @@ from google import genai
24
24
  from google.genai.types import GenerateContentConfig
25
25
  from prompt_toolkit import prompt # Added for multiline input
26
26
 
27
+ import kopipasta.import_parser as import_parser
28
+
27
29
  FileTuple = Tuple[str, bool, Optional[List[str]], str]
28
30
 
29
31
  class SimplePatchItem(BaseModel):
@@ -115,6 +117,109 @@ def apply_simple_patch(patch_item: SimplePatchItem) -> bool:
115
117
  print("-" * 20)
116
118
  return False
117
119
 
120
+ def _propose_and_add_dependencies(
121
+ file_just_added: str,
122
+ project_root_abs: str,
123
+ files_to_include: List[FileTuple],
124
+ current_char_count: int
125
+ ) -> Tuple[List[FileTuple], int]:
126
+ """
127
+ Analyzes a file for local dependencies and interactively asks the user to add them.
128
+ """
129
+ language = get_language_for_file(file_just_added)
130
+ if language not in ['python', 'typescript', 'javascript', 'tsx', 'jsx']:
131
+ return [], 0 # Only analyze languages we can parse
132
+
133
+ print(f"Analyzing {get_relative_path(file_just_added)} for local dependencies...")
134
+
135
+ try:
136
+ file_content = read_file_contents(file_just_added)
137
+ if not file_content:
138
+ return [], 0
139
+
140
+ resolved_deps_abs: Set[str] = set()
141
+ if language == 'python':
142
+ resolved_deps_abs = import_parser.parse_python_imports(file_content, file_just_added, project_root_abs)
143
+ elif language in ['typescript', 'javascript', 'tsx', 'jsx']:
144
+ resolved_deps_abs = import_parser.parse_typescript_imports(file_content, file_just_added, project_root_abs)
145
+
146
+ # Filter out dependencies that are already in the context
147
+ included_paths = {os.path.abspath(f[0]) for f in files_to_include}
148
+ suggested_deps = sorted([
149
+ dep for dep in resolved_deps_abs
150
+ if os.path.abspath(dep) not in included_paths and os.path.abspath(dep) != os.path.abspath(file_just_added)
151
+ ])
152
+
153
+ if not suggested_deps:
154
+ print("No new local dependencies found.")
155
+ return [], 0
156
+
157
+ print(f"\nFound {len(suggested_deps)} new local {'dependency' if len(suggested_deps) == 1 else 'dependencies'}:")
158
+ for i, dep_path in enumerate(suggested_deps):
159
+ print(f" ({i+1}) {get_relative_path(dep_path)}")
160
+
161
+ while True:
162
+ choice = input("\nAdd dependencies? (a)ll, (n)one, or enter numbers (e.g. 1, 3-4): ").lower()
163
+
164
+ deps_to_add_paths = None
165
+ if choice == 'a':
166
+ deps_to_add_paths = suggested_deps
167
+ break
168
+ if choice == 'n':
169
+ deps_to_add_paths = []
170
+ print(f"Skipped {len(suggested_deps)} dependencies.")
171
+ break
172
+
173
+ # Try to parse the input as numbers directly.
174
+ try:
175
+ selected_indices = set()
176
+ parts = choice.replace(' ', '').split(',')
177
+ if all(p.strip() for p in parts): # Ensure no empty parts like in "1,"
178
+ for part in parts:
179
+ if '-' in part:
180
+ start_str, end_str = part.split('-', 1)
181
+ start = int(start_str)
182
+ end = int(end_str)
183
+ if start > end:
184
+ start, end = end, start
185
+ selected_indices.update(range(start - 1, end))
186
+ else:
187
+ selected_indices.add(int(part) - 1)
188
+
189
+ # Validate that all selected numbers are within the valid range
190
+ if all(0 <= i < len(suggested_deps) for i in selected_indices):
191
+ deps_to_add_paths = [
192
+ suggested_deps[i] for i in sorted(list(selected_indices))
193
+ ]
194
+ break # Success! Exit the loop.
195
+ else:
196
+ print(f"Error: Invalid number selection. Please choose numbers between 1 and {len(suggested_deps)}.")
197
+ else:
198
+ raise ValueError("Empty part detected in input.")
199
+
200
+
201
+ except ValueError:
202
+ # This will catch any input that isn't 'a', 'n', or a valid number/range.
203
+ print("Invalid choice. Please enter 'a', 'n', or a list/range of numbers (e.g., '1,3' or '2-4').")
204
+
205
+ if not deps_to_add_paths:
206
+ return [], 0 # No dependencies were selected
207
+
208
+ newly_added_files: List[FileTuple] = []
209
+ char_count_delta = 0
210
+ for dep_path in deps_to_add_paths:
211
+ # Assume non-large for now for simplicity, can be enhanced later
212
+ file_size = os.path.getsize(dep_path)
213
+ newly_added_files.append((dep_path, False, None, get_language_for_file(dep_path)))
214
+ char_count_delta += file_size
215
+ print(f"Added dependency: {get_relative_path(dep_path)} ({get_human_readable_size(file_size)})")
216
+
217
+ return newly_added_files, char_count_delta
218
+
219
+ except Exception as e:
220
+ print(f"Warning: Could not analyze dependencies for {get_relative_path(file_just_added)}: {e}")
221
+ return [], 0
222
+
118
223
  def get_colored_code(file_path, code):
119
224
  try:
120
225
  lexer = get_lexer_for_filename(file_path)
@@ -540,7 +645,7 @@ def print_char_count(count):
540
645
  token_estimate = count // 4
541
646
  print(f"\rCurrent prompt size: {count} characters (~ {token_estimate} tokens)", flush=True)
542
647
 
543
- def select_files_in_directory(directory: str, ignore_patterns: List[str], current_char_count: int = 0) -> Tuple[List[FileTuple], int]:
648
+ def select_files_in_directory(directory: str, ignore_patterns: List[str], project_root_abs: str, current_char_count: int = 0) -> Tuple[List[FileTuple], int]:
544
649
  files = [f for f in os.listdir(directory)
545
650
  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))]
546
651
 
@@ -561,7 +666,9 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], curren
561
666
  print_char_count(current_char_count)
562
667
  choice = input("(y)es add all / (n)o ignore all / (s)elect individually / (q)uit? ").lower()
563
668
  selected_files: List[FileTuple] = []
669
+ char_count_delta = 0
564
670
  if choice == 'y':
671
+ files_to_add_after_loop = []
565
672
  for file in files:
566
673
  file_path = os.path.join(directory, file)
567
674
  if is_large_file(file_path):
@@ -571,14 +678,23 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], curren
571
678
  break
572
679
  print("Invalid choice. Please enter 'f' or 's'.")
573
680
  if snippet_choice == 's':
574
- selected_files.append((file, True, None, get_language_for_file(file)))
575
- current_char_count += len(get_file_snippet(file_path))
681
+ selected_files.append((file_path, True, None, get_language_for_file(file_path)))
682
+ char_count_delta += len(get_file_snippet(file_path))
576
683
  else:
577
- selected_files.append((file, False, None, get_language_for_file(file)))
578
- current_char_count += os.path.getsize(file_path)
684
+ selected_files.append((file_path, False, None, get_language_for_file(file_path)))
685
+ char_count_delta += os.path.getsize(file_path)
579
686
  else:
580
- selected_files.append((file, False, None, get_language_for_file(file)))
581
- current_char_count += os.path.getsize(file_path)
687
+ selected_files.append((file_path, False, None, get_language_for_file(file_path)))
688
+ char_count_delta += os.path.getsize(file_path)
689
+ files_to_add_after_loop.append(file_path)
690
+
691
+ # Analyze dependencies after the loop
692
+ current_char_count += char_count_delta
693
+ for file_path in files_to_add_after_loop:
694
+ new_deps, deps_char_count = _propose_and_add_dependencies(file_path, project_root_abs, selected_files, current_char_count)
695
+ selected_files.extend(new_deps)
696
+ current_char_count += deps_char_count
697
+
582
698
  print(f"Added all files from {directory}")
583
699
  return selected_files, current_char_count
584
700
  elif choice == 'n':
@@ -596,6 +712,7 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], curren
596
712
  print_char_count(current_char_count)
597
713
  file_choice = input(f"{file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) (y/n/p/q)? ").lower()
598
714
  if file_choice == 'y':
715
+ file_to_add = None
599
716
  if is_large_file(file_path):
600
717
  while True:
601
718
  snippet_choice = input(f"{file} is large. Use (f)ull content or (s)nippet? ").lower()
@@ -603,14 +720,21 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], curren
603
720
  break
604
721
  print("Invalid choice. Please enter 'f' or 's'.")
605
722
  if snippet_choice == 's':
606
- selected_files.append((file, True, None, get_language_for_file(file_path)))
723
+ file_to_add = (file_path, True, None, get_language_for_file(file_path))
607
724
  current_char_count += len(get_file_snippet(file_path))
608
725
  else:
609
- selected_files.append((file, False, None, get_language_for_file(file_path)))
726
+ file_to_add = (file_path, False, None, get_language_for_file(file_path))
610
727
  current_char_count += file_char_estimate
611
728
  else:
612
- selected_files.append((file, False, None, get_language_for_file(file_path)))
729
+ file_to_add = (file_path, False, None, get_language_for_file(file_path))
613
730
  current_char_count += file_char_estimate
731
+
732
+ if file_to_add:
733
+ selected_files.append(file_to_add)
734
+ # Analyze dependencies immediately after adding
735
+ new_deps, deps_char_count = _propose_and_add_dependencies(file_path, project_root_abs, selected_files, current_char_count)
736
+ selected_files.extend(new_deps)
737
+ current_char_count += deps_char_count
614
738
  break
615
739
  elif file_choice == 'n':
616
740
  break
@@ -633,7 +757,7 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], curren
633
757
  else:
634
758
  print("Invalid choice. Please try again.")
635
759
 
636
- def process_directory(directory: str, ignore_patterns: List[str], current_char_count: int = 0) -> Tuple[List[FileTuple], Set[str], int]:
760
+ def process_directory(directory: str, ignore_patterns: List[str], project_root_abs: str, current_char_count: int = 0) -> Tuple[List[FileTuple], Set[str], int]:
637
761
  files_to_include: List[FileTuple] = []
638
762
  processed_dirs: Set[str] = set()
639
763
 
@@ -647,10 +771,10 @@ def process_directory(directory: str, ignore_patterns: List[str], current_char_c
647
771
  print(f"\nExploring directory: {root}")
648
772
  choice = input("(y)es explore / (n)o skip / (q)uit? ").lower()
649
773
  if choice == 'y':
650
- selected_files, current_char_count = select_files_in_directory(root, ignore_patterns, current_char_count)
651
- for file_tuple in selected_files:
652
- full_path = os.path.join(root, file_tuple[0])
653
- files_to_include.append((full_path, file_tuple[1], file_tuple[2], file_tuple[3]))
774
+ # Pass project_root_abs down
775
+ selected_files, current_char_count = select_files_in_directory(root, ignore_patterns, project_root_abs, current_char_count)
776
+ # The paths in selected_files are already absolute now
777
+ files_to_include.extend(selected_files)
654
778
  processed_dirs.add(root)
655
779
  elif choice == 'n':
656
780
  dirs[:] = [] # Skip all subdirectories
@@ -763,47 +887,50 @@ def generate_prompt_template(files_to_include: List[FileTuple], ignore_patterns:
763
887
  prompt += "\n\n"
764
888
  prompt += "## Instructions for Achieving the Task\n\n"
765
889
  analysis_text = (
766
- "### Core Principles\n"
767
- "- Mark uncertainties with [UNCERTAIN] and request clarification instead of guessing\n"
768
- "- Request missing files immediately - never write placeholder code\n"
769
- "- After 3 failed attempts, summarize learnings and request specific diagnostic info\n"
770
- "- Ask me to test code, external services, APIs, databases, and integrations frequently\n\n"
771
- "### Development Workflow\n"
772
- "**Phase 1: E2E Draft** - Build complete solution incrementally. I may comment but won't test until draft is complete.\n"
773
- "- Signal completion: [E2E DRAFT COMPLETE]\n"
774
- "- If fundamental blockers found, pause and discuss\n\n"
775
- "**Phase 2: Final Review** - When I request consolidation:\n"
776
- "- Provide complete, copy-paste ready code (no diffs)\n"
777
- "- Group related changes together\n"
778
- "- Order files by dependency\n\n"
779
- "### Task Execution\n\n"
780
- "1. **Understand**: Rephrase task, identify missing files, mark assumptions ([UNCERTAIN]/[MISSING]/[CONFIRMED])\n\n"
781
- "2. **Plan**: Provide 2-3 approaches when feasible, identify risks, list required files\n\n"
782
- "3. **Implement**: Small increments, track attempts with learnings:\n"
783
- " ```\n"
784
- " 1.[FAILED] X→Y error (learned: not type issue)\n"
785
- " 2.[FAILED] Z→same (learned: runtime error)\n"
786
- " 3.[CURRENT] Need: full stack trace + value of param\n"
787
- " ```\n\n"
788
- "4. **Present Code**: \n"
789
- " - **Phase 1 (Incremental)**:\n"
790
- " - Small changes: Show only modified lines with context\n"
791
- " - Large changes: Show changed functions/classes with `// ... existing code ...` for unchanged parts\n"
792
- " - Always specify filename at start of code block\n"
793
- " - **Phase 2 (Consolidation)**: Show complete final code for easy copying\n"
794
- " - **Missing files**: Never write placeholders:\n"
795
- " ```\n"
796
- " # MISSING: utils.py - need process_data implementation\n"
797
- " # REQUEST: Please provide utils.py\n"
798
- " ```\n\n"
799
- "5. **Debug**: Include strategic outputs, request specific diagnostics, admit uncertainty early\n\n"
800
- "### You Have Permission To\n"
801
- "- Request any file shown in tree but not provided\n"
802
- "- Ask me to run code and share outputs\n"
803
- "- Test external dependencies: APIs, databases, services, integration points\n"
804
- "- Request specific diagnostic information\n"
805
- "- Suggest pausing when blocked\n"
806
- "- Ask me to verify assumptions about external systems\n"
890
+ "### Your Operating Model: The Expert Council\n\n"
891
+ "You will operate as a council of expert personas in a direct, collaborative partnership with me, the user. Your entire thinking process should be externalized as a dialogue between these personas. Any persona can and should interact directly with me.\n\n"
892
+ "#### Core Personas\n"
893
+ "- **Facilitator**: The lead coordinator. You summarize discussions, ensure the team stays on track, and are responsible for presenting the final, consolidated plans and code to me.\n"
894
+ "- **Architect**: Focuses on high-level design, system structure, dependencies, scalability, and long-term maintainability. Asks 'Is this well-designed?'\n"
895
+ "- **Builder**: The hands-on engineer. Focuses on implementation details, writing clean and functional code, and debugging. Asks 'How do we build this?' **When presenting code obey Rules for Presenting Code**\n"
896
+ "- **Critique**: The quality advocate. Focuses on identifying edge cases, potential bugs, and security vulnerabilities. Asks 'What could go wrong?' and provides constructive critique on both the team's proposals and my (the user's) requests.\n\n"
897
+ "#### Dynamic Personas\n"
898
+ "When the task requires it, introduce other specialist personas. For example: UX Designer, DevOps Engineer, Data Scientist, etc.\n\n"
899
+ "### Development Workflow: A Structured Dialogue\n\n"
900
+ "Follow this conversational workflow. Prefix direct communication to me with the persona's name (e.g., 'Critique to User: ...').\n\n"
901
+ "1. **Understand & Clarify (Dialogue)**\n"
902
+ " - **Facilitator**: Start by stating the goal as you understand it.\n"
903
+ " - **All**: Discuss the requirements. Any persona can directly ask me for clarification. Identify and list any missing information ([MISSING]) or assumptions ([ASSUMPTION]).\n"
904
+ " - **Critique**: Directly challenge any part of my request that seems ambiguous, risky, or suboptimal.\n\n"
905
+ "2. **Plan (Dialogue)**\n"
906
+ " - **Architect**: Propose one or more high-level plans. Directly ask me for input on tradeoffs if necessary.\n"
907
+ " - **Critique**: Challenge the plans. Point out risks or edge cases.\n"
908
+ " - **Builder**: Comment on the implementation feasibility.\n"
909
+ " - **Facilitator**: Synthesize the discussion, select the best plan, and present a clear, step-by-step summary to me for approval.\n\n"
910
+ "3. **Implement (Code & Dialogue)**\n"
911
+ " - **Facilitator**: State which step of the plan is being implemented.\n"
912
+ " - **Builder**: Write the code. If you encounter an issue, you can ask me directly for more context.\n"
913
+ " - **Critique**: Review the code and the underlying assumptions.\n"
914
+ " - **Facilitator**: Present the final, reviewed code for that step. Ask me to test it if appropriate.\n\n"
915
+ "4. **Present E2E Draft (Consolidation)**\n"
916
+ " - When I ask you to consolidate, your goal is to provide a clear, unambiguous set of changes that I can directly copy and paste into my editor.\n"
917
+ " - **Present changes grouped by file.** For each change, provide the **entire updated function, class, or logical block** to make replacement easy.\n"
918
+ " - Use context comments to show where the new code block fits.\n"
919
+ " - **Do not use a diff format (`+` or `-` lines).** Provide the final, clean code.\n\n"
920
+ "5. **Validate & Iterate (Feedback Loop)**\n"
921
+ " - After presenting the consolidated draft, you will explicitly prompt me for testing and wait for my feedback.\n"
922
+ " - Upon receiving my feedback, the **Facilitator** will announce the next step.\n"
923
+ " - If the feedback indicates a minor bug, you will return to **Step 3 (Implement)** to fix it.\n"
924
+ " - If the feedback reveals a fundamental design flaw or misunderstanding, you will return to **Step 2 (Plan)** or even **Step 1 (Understand)** to re-evaluate the approach.\n\n"
925
+ "### Rules for Presenting Code\n\n"
926
+ "- **Always specify the filename** at the start of a code block (e.g., `// FILE: kopipasta/main.py`).\n"
927
+ "- **Incremental Changes**: During implementation, show only the changed functions or classes. Use comments like `// ... existing code ...` to represent unchanged parts.\n"
928
+ "- **Missing Files**: Never write placeholder code. Instead, state it's missing and request it.\n\n"
929
+ "### Your Permissions\n\n"
930
+ "- **You are empowered to interact with me directly.** Any persona can ask me questions or provide constructive feedback on my requests, assumptions, or the provided context.\n"
931
+ "- You MUST request any file shown in the project tree that you need but was not provided.\n"
932
+ "- You MUST ask me to run code, test integrations (APIs, databases), and share the output.\n"
933
+ "- You MUST ask for clarification instead of making risky assumptions.\n"
807
934
  )
808
935
  prompt += analysis_text
809
936
  return prompt, cursor_position
@@ -1050,6 +1177,8 @@ def main():
1050
1177
 
1051
1178
  ignore_patterns = read_gitignore()
1052
1179
  env_vars = read_env_file()
1180
+ project_root_abs = os.path.abspath(os.getcwd())
1181
+
1053
1182
 
1054
1183
  files_to_include: List[FileTuple] = []
1055
1184
  processed_dirs = set()
@@ -1092,53 +1221,59 @@ def main():
1092
1221
  print(f"Added {'snippet of ' if is_snippet else ''}web content from: {input_path}")
1093
1222
  print_char_count(current_char_count)
1094
1223
  elif os.path.isfile(input_path):
1095
- # Handle files provided directly via command line
1096
- if not is_ignored(input_path, ignore_patterns) and not is_binary(input_path):
1097
- file_size = os.path.getsize(input_path)
1224
+ abs_input_path = os.path.abspath(input_path)
1225
+ if not is_ignored(abs_input_path, ignore_patterns) and not is_binary(abs_input_path):
1226
+ file_size = os.path.getsize(abs_input_path)
1098
1227
  file_size_readable = get_human_readable_size(file_size)
1099
1228
  file_char_estimate = file_size
1100
- language = get_language_for_file(input_path)
1229
+ language = get_language_for_file(abs_input_path)
1230
+ file_to_add = None
1101
1231
 
1102
- if is_large_file(input_path):
1103
- print(f"\nFile {input_path} ({file_size_readable}, ~{file_char_estimate} chars) is large.")
1232
+ if is_large_file(abs_input_path):
1233
+ print(f"\nFile {get_relative_path(abs_input_path)} ({file_size_readable}, ~{file_char_estimate} chars) is large.")
1104
1234
  print("Preview (first ~50 lines or 4KB):")
1105
- print(get_colored_file_snippet(input_path))
1235
+ print(get_colored_file_snippet(abs_input_path))
1106
1236
  print("-" * 40)
1107
1237
  while True:
1108
1238
  print_char_count(current_char_count)
1109
- choice = input(f"How to include large file {input_path}? (f)ull / (s)nippet / (p)atches / (n)o skip: ").lower()
1239
+ choice = input(f"How to include large file {get_relative_path(abs_input_path)}? (f)ull / (s)nippet / (p)atches / (n)o skip: ").lower()
1110
1240
  if choice == 'f':
1111
- files_to_include.append((input_path, False, None, language))
1241
+ file_to_add = (abs_input_path, False, None, language)
1112
1242
  current_char_count += file_char_estimate
1113
- print(f"Added full file: {input_path}")
1243
+ print(f"Added full file: {get_relative_path(abs_input_path)}")
1114
1244
  break
1115
1245
  elif choice == 's':
1116
- snippet_content = get_file_snippet(input_path)
1117
- files_to_include.append((input_path, True, None, language))
1246
+ snippet_content = get_file_snippet(abs_input_path)
1247
+ file_to_add = (abs_input_path, True, None, language)
1118
1248
  current_char_count += len(snippet_content)
1119
- print(f"Added snippet of file: {input_path}")
1249
+ print(f"Added snippet of file: {get_relative_path(abs_input_path)}")
1120
1250
  break
1121
1251
  elif choice == 'p':
1122
- chunks, char_count = select_file_patches(input_path)
1252
+ chunks, char_count = select_file_patches(abs_input_path)
1123
1253
  if chunks:
1124
- files_to_include.append((input_path, False, chunks, language))
1254
+ file_to_add = (abs_input_path, False, chunks, language)
1125
1255
  current_char_count += char_count
1126
- print(f"Added selected patches from file: {input_path}")
1256
+ print(f"Added selected patches from file: {get_relative_path(abs_input_path)}")
1127
1257
  else:
1128
- print(f"No patches selected for {input_path}. Skipping file.")
1258
+ print(f"No patches selected for {get_relative_path(abs_input_path)}. Skipping file.")
1129
1259
  break
1130
1260
  elif choice == 'n':
1131
- print(f"Skipped large file: {input_path}")
1261
+ print(f"Skipped large file: {get_relative_path(abs_input_path)}")
1132
1262
  break
1133
1263
  else:
1134
1264
  print("Invalid choice. Please enter 'f', 's', 'p', or 'n'.")
1135
1265
  else:
1136
- # Automatically include non-large files
1137
- files_to_include.append((input_path, False, None, language))
1266
+ file_to_add = (abs_input_path, False, None, language)
1138
1267
  current_char_count += file_char_estimate
1139
- print(f"Added file: {input_path} ({file_size_readable})")
1140
-
1141
- # Display current count after processing the file
1268
+ print(f"Added file: {get_relative_path(abs_input_path)} ({file_size_readable})")
1269
+
1270
+ if file_to_add:
1271
+ files_to_include.append(file_to_add)
1272
+ # --- NEW: Call dependency analysis ---
1273
+ new_deps, deps_char_count = _propose_and_add_dependencies(abs_input_path, project_root_abs, files_to_include, current_char_count)
1274
+ files_to_include.extend(new_deps)
1275
+ current_char_count += deps_char_count
1276
+
1142
1277
  print_char_count(current_char_count)
1143
1278
 
1144
1279
  else:
@@ -1147,10 +1282,11 @@ def main():
1147
1282
  elif is_binary(input_path):
1148
1283
  print(f"Ignoring binary file: {input_path}")
1149
1284
  else:
1150
- print(f"Ignoring file: {input_path}") # Should not happen if logic is correct, but fallback.
1285
+ print(f"Ignoring file: {input_path}")
1151
1286
  elif os.path.isdir(input_path):
1152
1287
  print(f"\nProcessing directory specified directly: {input_path}")
1153
- dir_files, dir_processed, current_char_count = process_directory(input_path, ignore_patterns, current_char_count)
1288
+ # Pass project_root_abs to process_directory
1289
+ dir_files, dir_processed, current_char_count = process_directory(input_path, ignore_patterns, project_root_abs, current_char_count)
1154
1290
  files_to_include.extend(dir_files)
1155
1291
  processed_dirs.update(dir_processed)
1156
1292
  else:
@@ -1162,42 +1298,34 @@ def main():
1162
1298
 
1163
1299
  print("\nFile and web content selection complete.")
1164
1300
  print_char_count(current_char_count) # Print final count before prompt generation
1165
- print(f"Summary: Added {len(files_to_include)} files and {len(web_contents)} web sources.")
1166
1301
 
1167
1302
  added_files_count = len(files_to_include)
1168
- added_dirs_count = len(processed_dirs) # Count unique processed directories
1303
+ added_dirs_count = len(processed_dirs)
1169
1304
  added_web_count = len(web_contents)
1170
1305
  print(f"Summary: Added {added_files_count} files/patches from {added_dirs_count} directories and {added_web_count} web sources.")
1171
1306
 
1172
1307
  prompt_template, cursor_position = generate_prompt_template(files_to_include, ignore_patterns, web_contents, env_vars)
1173
1308
 
1174
- # Logic branching for interactive mode vs. clipboard mode
1175
1309
  if args.interactive:
1176
1310
  print("\nPreparing initial prompt for editing...")
1177
- # Determine the initial content for the editor
1178
1311
  if args.task:
1179
- # Pre-populate the task section if --task was provided
1180
1312
  editor_initial_content = prompt_template[:cursor_position] + args.task + prompt_template[cursor_position:]
1181
1313
  print("Pre-populating editor with task provided via --task argument.")
1182
1314
  else:
1183
- # Use the template as is (user will add task in the editor)
1184
1315
  editor_initial_content = prompt_template
1185
1316
  print("Opening editor for you to add the task instructions.")
1186
1317
 
1187
- # Always open the editor in interactive mode
1188
1318
  initial_chat_prompt = open_editor_for_input(editor_initial_content, cursor_position)
1189
1319
  print("Editor closed. Starting interactive chat session...")
1190
- start_chat_session(initial_chat_prompt) # Start the chat with the edited prompt else:
1320
+ start_chat_session(initial_chat_prompt)
1191
1321
  else:
1192
- # Original non-interactive behavior
1193
1322
  if args.task:
1194
1323
  task_description = args.task
1195
- # Insert task description before "## Task Instructions"
1196
1324
  task_marker = "## Task Instructions\n\n"
1197
1325
  insertion_point = prompt_template.find(task_marker)
1198
1326
  if insertion_point != -1:
1199
1327
  final_prompt = prompt_template[:insertion_point + len(task_marker)] + task_description + "\n\n" + prompt_template[insertion_point + len(task_marker):]
1200
- else: # Fallback if marker not found
1328
+ else:
1201
1329
  final_prompt = prompt_template[:cursor_position] + task_description + prompt_template[cursor_position:]
1202
1330
  print("\nUsing task description from -t argument.")
1203
1331
  else:
@@ -1209,7 +1337,6 @@ def main():
1209
1337
  print(final_prompt)
1210
1338
  print("-" * 80)
1211
1339
 
1212
- # Copy the prompt to clipboard
1213
1340
  try:
1214
1341
  pyperclip.copy(final_prompt)
1215
1342
  separator = "\n" + "=" * 40 + "\n☕🍝 Kopipasta Complete! 🍝☕\n" + "=" * 40 + "\n"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -10,7 +10,7 @@ with open("requirements.txt", "r", encoding="utf-8") as f:
10
10
 
11
11
  setup(
12
12
  name="kopipasta",
13
- version="0.26.0",
13
+ version="0.27.0",
14
14
  author="Mikko Korpela",
15
15
  author_email="mikko.korpela@gmail.com",
16
16
  description="A CLI tool to generate prompts with project structure and file contents",
File without changes
File without changes
File without changes
File without changes
File without changes