kopipasta 0.27.0__py3-none-any.whl → 0.28.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
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  import csv
3
3
  import io
4
- import json
5
4
  import os
6
5
  import argparse
7
- import sys
8
6
  import re
9
7
  import subprocess
10
8
  import tempfile
@@ -18,105 +16,10 @@ import pygments.util
18
16
 
19
17
  import requests
20
18
 
21
- from pydantic import BaseModel, Field
22
- import traceback
23
- from google import genai
24
- from google.genai.types import GenerateContentConfig
25
- from prompt_toolkit import prompt # Added for multiline input
26
-
27
19
  import kopipasta.import_parser as import_parser
28
20
 
29
21
  FileTuple = Tuple[str, bool, Optional[List[str]], str]
30
22
 
31
- class SimplePatchItem(BaseModel):
32
- """A single change described by reasoning, file path, original text, and new text."""
33
- reasoning: str = Field(..., description="Explanation for why this specific change is proposed.")
34
- file_path: str = Field(..., description="Relative path to the file to be modified.")
35
- original_text: str = Field(..., description="The exact, unique block of text to be replaced.")
36
- new_text: str = Field(..., description="The text to replace the original_text with.")
37
-
38
- class SimplePatchArgs(BaseModel):
39
- """A list of proposed code changes."""
40
- patches: List[SimplePatchItem] = Field(..., description="A list of patches to apply.")
41
-
42
- def apply_simple_patch(patch_item: SimplePatchItem) -> bool:
43
- """
44
- Applies a single patch defined by replacing original_text with new_text in file_path.
45
-
46
- Validates that the file exists and the original_text is unique.
47
- """
48
- print(f"\nApplying patch to: {patch_item.file_path}")
49
- print(f"Reasoning: {patch_item.reasoning}")
50
- print("-" * 20)
51
-
52
- file_path = patch_item.file_path
53
- original_text = patch_item.original_text
54
- new_text = patch_item.new_text
55
-
56
- # --- Validation ---
57
- if not os.path.exists(file_path):
58
- print(f"❌ Error: File not found: {file_path}")
59
- print("-" * 20)
60
- return False
61
-
62
- try:
63
- # Read the file content, attempting to preserve line endings implicitly
64
- with open(file_path, 'r', encoding='utf-8', newline='') as f:
65
- content = f.read()
66
-
67
- # Check for unique occurrence of original_text
68
- occurrences = content.count(original_text)
69
- if occurrences == 0:
70
- print(f"❌ Error: Original text not found in {file_path}.")
71
- # Optional: print a snippet of the expected text for debugging
72
- # print(f" Expected original text snippet: '{original_text[:100]}...'")
73
- print("-" * 20)
74
- return False
75
- elif occurrences > 1:
76
- print(f"❌ Error: Original text is not unique in {file_path} (found {occurrences} times).")
77
- print(f" Patch cannot be applied automatically due to ambiguity.")
78
- # print(f" Ambiguous original text snippet: '{original_text[:100]}...'")
79
- print("-" * 20)
80
- return False
81
-
82
- # --- Application ---
83
- # Replace the single unique occurrence
84
- new_content = content.replace(original_text, new_text, 1)
85
-
86
- # Heuristic to check if a newline might be needed at the end
87
- original_ends_with_newline = content.endswith(('\n', '\r'))
88
- new_ends_with_newline = new_content.endswith(('\n', '\r'))
89
-
90
- if original_ends_with_newline and not new_ends_with_newline and new_content:
91
- # Try to determine the original newline type
92
- if content.endswith('\r\n'):
93
- new_content += '\r\n'
94
- else: # Assume '\n' otherwise
95
- new_content += '\n'
96
- elif not original_ends_with_newline and new_ends_with_newline:
97
- # If original didn't end with newline, remove the one added by replacement
98
- # This is less common but possible if new_text ends with \n and original_text didn't
99
- new_content = new_content.rstrip('\r\n')
100
-
101
-
102
- # Write the modified content back
103
- with open(file_path, 'w', encoding='utf-8', newline='') as f:
104
- f.write(new_content)
105
-
106
- print(f"✅ Patch applied successfully to {file_path}.")
107
- print("-" * 20)
108
- return True
109
-
110
- except IOError as e:
111
- print(f"❌ Error reading or writing file {file_path}: {e}")
112
- print("-" * 20)
113
- return False
114
- except Exception as e:
115
- print(f"❌ An unexpected error occurred during patch application: {e}")
116
- traceback.print_exc()
117
- print("-" * 20)
118
- return False
119
-
120
23
  def _propose_and_add_dependencies(
121
24
  file_just_added: str,
122
25
  project_root_abs: str,
@@ -887,50 +790,40 @@ def generate_prompt_template(files_to_include: List[FileTuple], ignore_patterns:
887
790
  prompt += "\n\n"
888
791
  prompt += "## Instructions for Achieving the Task\n\n"
889
792
  analysis_text = (
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"
793
+ "### Partnership Principles\n\n"
794
+ "We work as collaborative partners. You provide technical expertise and critical thinking. "
795
+ "I have exclusive access to my codebase, real environment, external services, and actual users. "
796
+ "Never assume project file contents - always ask to see them.\n\n"
797
+ "**Critical Thinking**: Challenge poor approaches, identify risks, suggest better alternatives. Don't be a yes-man.\n\n"
798
+ "**Anti-Hallucination**: Never write placeholder code for files in ## Project Structure. Use [STOP - NEED FILE: filename] and wait.\n\n"
799
+ "**Hard Stops**: End with [AWAITING USER RESPONSE] when you need input. Don't continue with assumptions.\n\n"
800
+ "### Development Workflow\n\n"
801
+ "We work in two modes:\n"
802
+ "- **Iterative Mode**: Build incrementally, show only changes\n"
803
+ "- **Consolidation Mode**: When I request, provide clean final version\n\n"
804
+ "1. **Understand & Analyze**:\n"
805
+ " - Rephrase task, identify issues, list needed files\n"
806
+ " - Challenge problematic aspects\n"
807
+ " - End: 'I need: [files]. Is this correct?' [AWAITING USER RESPONSE]\n\n"
808
+ "2. **Plan**:\n"
809
+ " - Present 2-3 approaches with pros/cons\n"
810
+ " - Recommend best approach\n"
811
+ " - End: 'Which approach?' [AWAITING USER RESPONSE]\n\n"
812
+ "3. **Implement Iteratively**:\n"
813
+ " - Small, testable increments\n"
814
+ " - Track failed attempts: `Attempt 1: [FAILED] X→Y (learned: Z)`\n"
815
+ " - After 3 failures, request diagnostics\n\n"
816
+ "4. **Code Presentation**:\n"
817
+ " - Always: `// FILE: path/to/file.ext`\n"
818
+ " - Iterative: Show only changes with context\n"
819
+ " - Consolidation: Smart choice - minimal changes = show patches, extensive = full file\n\n"
820
+ "5. **Test & Validate**:\n"
821
+ " - 'Test with: [command]. Share any errors.' [AWAITING USER RESPONSE]\n"
822
+ " - Include debug outputs\n"
823
+ " - May return to implementation based on results\n\n"
824
+ "### Permissions & Restrictions\n\n"
825
+ "**You MAY**: Request project files, ask me to test code/services, challenge my approach, refuse without info\n\n"
826
+ "**You MUST NOT**: Assume project file contents, continue past [AWAITING USER RESPONSE], be agreeable when you see problems\n"
934
827
  )
935
828
  prompt += analysis_text
936
829
  return prompt, cursor_position
@@ -961,218 +854,10 @@ def open_editor_for_input(template: str, cursor_position: int) -> str:
961
854
  finally:
962
855
  os.unlink(temp_file_path)
963
856
 
964
- def start_chat_session(initial_prompt: str):
965
- """Starts an interactive chat session with the Gemini API using google-genai."""
966
- if not genai:
967
- # Error message already printed during import if it failed
968
- sys.exit(1)
969
-
970
- # The google-genai library automatically uses GOOGLE_API_KEY env var if set
971
- # We still check if it's set to provide a clearer error message upfront
972
- if not os.environ.get('GOOGLE_API_KEY'):
973
- print("Error: GOOGLE_API_KEY environment variable not set.")
974
- print("Please set the GOOGLE_API_KEY environment variable with your API key.")
975
- sys.exit(1)
976
-
977
- try:
978
- # Create the client - it will use the env var automatically
979
- client = genai.Client()
980
- print("Google GenAI Client created (using GOOGLE_API_KEY).")
981
- # You could add a check here like listing models to verify the key early
982
- # print("Available models:", [m.name for m in client.models.list()])
983
- except Exception as e:
984
- print(f"Error creating Google GenAI client: {e}")
985
- print("Please ensure your GOOGLE_API_KEY is valid and has permissions.")
986
- sys.exit(1)
987
-
988
- model_name = 'gemini-2.5-pro-exp-03-25'
989
- config = GenerateContentConfig(temperature=0.0)
990
- print(f"Using model: {model_name}")
991
-
992
- try:
993
- # Create a chat session using the client
994
- chat = client.chats.create(model=model_name, config=config)
995
- # Note: History is managed by the chat object itself
996
-
997
- print("\n--- Starting Interactive Chat with Gemini ---")
998
- print("Type /q to quit, /help or /? for help, /review to make clear summary, /patch to request a diff patch.")
999
-
1000
- # Send the initial prompt using send_message_stream
1001
- print("\n🤖 Gemini:")
1002
- full_response_text = ""
1003
- # Use send_message_stream for streaming responses
1004
- response_stream = chat.send_message_stream(initial_prompt, config=config)
1005
- for chunk in response_stream:
1006
- print(chunk.text, end="", flush=True)
1007
- full_response_text += chunk.text
1008
- print("\n" + "-"*20)
1009
-
1010
- while True:
1011
- is_patch_request = False
1012
- try:
1013
- # Print the header on a separate line
1014
- print("👤 You (Submit with Esc+Enter):")
1015
- # Get input using prompt_toolkit with a minimal indicator
1016
- user_input = prompt(">> ", multiline=True)
1017
- # prompt_toolkit raises EOFError on Ctrl+D, so this handler remains correct.
1018
- except EOFError:
1019
- print("\nExiting...")
1020
- break
1021
- except KeyboardInterrupt: # Handle Ctrl+C
1022
- print("\nExiting...")
1023
- break
1024
-
1025
- if user_input.lower() == '/q':
1026
- break
1027
- elif user_input.endswith('/patch'):
1028
- is_patch_request = True
1029
- # Extract message before /patch
1030
- user_message = user_input[:-len('/patch')].strip()
1031
- print(f"\n🛠️ Requesting patches... (Context: '{user_message}' if provided)")
1032
- elif user_input.lower() == '/review':
1033
- user_message = user_input = "Review and reflect on the solution. Summarize and write a minimal, complete set of changes needed for the solution. Do not use + and - style diff. Instead use comments to point where to place the code. Make it easy to copy and paste the solution."
1034
- elif not user_input:
1035
- continue # Ignore empty input
1036
- else:
1037
- user_message = user_input # Regular message
1038
-
1039
-
1040
- # --- Handle Patch Request ---
1041
- if is_patch_request:
1042
- print("🤖 Gemini: Thinking... (generating code changes)")
1043
- # Include user message part if it exists
1044
- patch_context = f"Based on our conversation and specifically: \"{user_message}\"\n\n" if user_message else "Based on our conversation,\n\n"
1045
-
1046
- patch_request_prompt = (
1047
- patch_context +
1048
- "Generate the necessary code changes to fulfill the request. Provide the changes as a JSON list, where each item "
1049
- "is an object with the following keys:\n"
1050
- "- 'reasoning': Explain why this specific change is needed.\n"
1051
- "- 'file_path': The relative path to the file to modify.\n"
1052
- "- 'original_text': The exact, unique block of text to replace.\n"
1053
- "- 'new_text': The text to replace original_text with. Do not include any temporary comments like '// CHANGE BEGINS' or '/* PATCH START */'.\n"
1054
- "Ensure 'original_text' is unique within the specified 'file_path'. "
1055
- "Respond ONLY with the JSON object conforming to this structure: { \"patches\": [ { patch_item_1 }, { patch_item_2 }, ... ] }"
1056
- )
1057
-
1058
- try:
1059
- # Request the response using the new schema
1060
- response = chat.send_message(
1061
- patch_request_prompt,
1062
- config=GenerateContentConfig(
1063
- response_schema=SimplePatchArgs.model_json_schema(),
1064
- response_mime_type='application/json',
1065
- temperature=0.0
1066
- )
1067
- )
1068
-
1069
- print("🤖 Gemini: Received potential patches.")
1070
- try:
1071
- # Validate and parse args using the Pydantic model
1072
- # Explicitly validate the dictionary returned by response.parsed
1073
- if isinstance(response.parsed, dict):
1074
- patch_args = SimplePatchArgs.model_validate(response.parsed)
1075
- else:
1076
- # Handle unexpected type if response.parsed isn't a dict
1077
- print(f"❌ Error: Expected a dictionary for patches, but got type {type(response.parsed)}")
1078
- print(f" Content: {response.parsed}")
1079
- continue # Skip further processing for this response
1080
-
1081
- if not patch_args or not patch_args.patches:
1082
- print("🤖 Gemini: No patches were proposed in the response.")
1083
- print("-" * 20)
1084
- continue
1085
-
1086
- print("\nProposed Patches:")
1087
- print("=" * 30)
1088
- for i, patch_item in enumerate(patch_args.patches):
1089
- print(f"Patch {i+1}/{len(patch_args.patches)}:")
1090
- print(f" File: {patch_item.file_path}")
1091
- print(f" Reasoning: {patch_item.reasoning}")
1092
- # Optionally show snippets of original/new text for review
1093
- print(f" Original (snippet): '{patch_item.original_text[:80].strip()}...'")
1094
- print(f" New (snippet): '{patch_item.new_text[:80].strip()}...'")
1095
- print("-" * 20)
1096
-
1097
- confirm = input(f"Apply these {len(patch_args.patches)} patches? (y/N): ").lower()
1098
- if confirm == 'y':
1099
- applied_count = 0
1100
- failed_count = 0
1101
- for patch_item in patch_args.patches:
1102
- # Call the new apply function for each patch
1103
- success = apply_simple_patch(patch_item)
1104
- if success:
1105
- applied_count += 1
1106
- else:
1107
- failed_count += 1
1108
-
1109
- print("\nPatch Application Summary:")
1110
- if applied_count > 0:
1111
- print(f"✅ Successfully applied {applied_count} patches.")
1112
- if failed_count > 0:
1113
- print(f"❌ Failed to apply {failed_count} patches.")
1114
- if applied_count == 0 and failed_count == 0: # Should not happen if list wasn't empty
1115
- print("⚪ No patches were applied.")
1116
- print("=" * 30)
1117
- else:
1118
- print("🤖 Gemini: Patches not applied by user.")
1119
- print("-" * 20)
1120
-
1121
- except Exception as e: # Catch Pydantic validation errors or other issues
1122
- print(f"❌ Error processing patch response: {e}")
1123
- # Attempt to show the raw response text if parsing failed
1124
- raw_text = ""
1125
- try:
1126
- if response.parts:
1127
- raw_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
1128
- elif hasattr(response, 'text'):
1129
- raw_text = response.text
1130
- except Exception:
1131
- pass # Ignore errors getting raw text
1132
- if raw_text:
1133
- print(f" Received response text:\n{raw_text}")
1134
- else:
1135
- print(f" Received response content: {response}") # Fallback representation
1136
-
1137
- except Exception as e:
1138
- print(f"\n❌ An error occurred while requesting patches from Gemini: {e}")
1139
- print(" Please check your connection, API key, and model permissions/capabilities.")
1140
- print("-" * 20)
1141
-
1142
- continue # Go to next loop iteration after handling /patch
1143
- elif user_input.strip() in ['/help', '/?']:
1144
- print("🤖 Gemini: Available commands:")
1145
- print(" /q - Quit the chat session.")
1146
- print(" /patch - Request a diff patch (not fully implemented yet).")
1147
- print(" /review - Pre-fill input with a review/summary prompt template.")
1148
- print(" /help or /? - Show this help message.")
1149
- print("-" * 20)
1150
- continue
1151
- elif not user_input.strip(): # Ignore empty input
1152
- continue
1153
-
1154
- print("\n🤖 Gemini:")
1155
- full_response_text = ""
1156
- try:
1157
- # Use send_message_stream for subsequent messages
1158
- response_stream = chat.send_message_stream(user_input, config=config)
1159
- for chunk in response_stream:
1160
- print(chunk.text, end="", flush=True)
1161
- full_response_text += chunk.text
1162
- print("\n" + "-"*20)
1163
- except Exception as e:
1164
- print(f"\nAn unexpected error occurred: {e}")
1165
- print("Try again or type 'exit'.")
1166
-
1167
- except Exception as e:
1168
- # Catch other potential errors
1169
- print(f"\nAn error occurred setting up the chat session: {e}")
1170
-
1171
857
  def main():
1172
858
  parser = argparse.ArgumentParser(description="Generate a prompt with project structure, file contents, and web content.")
1173
859
  parser.add_argument('inputs', nargs='+', help='Files, directories, or URLs to include in the prompt')
1174
860
  parser.add_argument('-t', '--task', help='Task description for the AI prompt')
1175
- parser.add_argument('-I', '--interactive', action='store_true', help='Start an interactive chat session after generating the prompt.')
1176
861
  args = parser.parse_args()
1177
862
 
1178
863
  ignore_patterns = read_gitignore()
@@ -1306,47 +991,34 @@ def main():
1306
991
 
1307
992
  prompt_template, cursor_position = generate_prompt_template(files_to_include, ignore_patterns, web_contents, env_vars)
1308
993
 
1309
- if args.interactive:
1310
- print("\nPreparing initial prompt for editing...")
1311
- if args.task:
1312
- editor_initial_content = prompt_template[:cursor_position] + args.task + prompt_template[cursor_position:]
1313
- print("Pre-populating editor with task provided via --task argument.")
994
+ if args.task:
995
+ task_description = args.task
996
+ task_marker = "## Task Instructions\n\n"
997
+ insertion_point = prompt_template.find(task_marker)
998
+ if insertion_point != -1:
999
+ final_prompt = prompt_template[:insertion_point + len(task_marker)] + task_description + "\n\n" + prompt_template[insertion_point + len(task_marker):]
1314
1000
  else:
1315
- editor_initial_content = prompt_template
1316
- print("Opening editor for you to add the task instructions.")
1317
-
1318
- initial_chat_prompt = open_editor_for_input(editor_initial_content, cursor_position)
1319
- print("Editor closed. Starting interactive chat session...")
1320
- start_chat_session(initial_chat_prompt)
1001
+ final_prompt = prompt_template[:cursor_position] + task_description + prompt_template[cursor_position:]
1002
+ print("\nUsing task description from -t argument.")
1321
1003
  else:
1322
- if args.task:
1323
- task_description = args.task
1324
- task_marker = "## Task Instructions\n\n"
1325
- insertion_point = prompt_template.find(task_marker)
1326
- if insertion_point != -1:
1327
- final_prompt = prompt_template[:insertion_point + len(task_marker)] + task_description + "\n\n" + prompt_template[insertion_point + len(task_marker):]
1328
- else:
1329
- final_prompt = prompt_template[:cursor_position] + task_description + prompt_template[cursor_position:]
1330
- print("\nUsing task description from -t argument.")
1331
- else:
1332
- print("\nOpening editor for task instructions...")
1333
- final_prompt = open_editor_for_input(prompt_template, cursor_position)
1334
-
1335
- print("\n\nGenerated prompt:")
1336
- print("-" * 80)
1337
- print(final_prompt)
1338
- print("-" * 80)
1339
-
1340
- try:
1341
- pyperclip.copy(final_prompt)
1342
- separator = "\n" + "=" * 40 + "\n☕🍝 Kopipasta Complete! 🍝☕\n" + "=" * 40 + "\n"
1343
- print(separator)
1344
- final_char_count = len(final_prompt)
1345
- final_token_estimate = final_char_count // 4
1346
- print(f"Prompt has been copied to clipboard. Final size: {final_char_count} characters (~ {final_token_estimate} tokens)")
1347
- except pyperclip.PyperclipException as e:
1348
- print(f"\nWarning: Failed to copy to clipboard: {e}")
1349
- print("You can manually copy the prompt above.")
1004
+ print("\nOpening editor for task instructions...")
1005
+ final_prompt = open_editor_for_input(prompt_template, cursor_position)
1006
+
1007
+ print("\n\nGenerated prompt:")
1008
+ print("-" * 80)
1009
+ print(final_prompt)
1010
+ print("-" * 80)
1011
+
1012
+ try:
1013
+ pyperclip.copy(final_prompt)
1014
+ separator = "\n" + "=" * 40 + "\n☕🍝 Kopipasta Complete! 🍝☕\n" + "=" * 40 + "\n"
1015
+ print(separator)
1016
+ final_char_count = len(final_prompt)
1017
+ final_token_estimate = final_char_count // 4
1018
+ print(f"Prompt has been copied to clipboard. Final size: {final_char_count} characters (~ {final_token_estimate} tokens)")
1019
+ except pyperclip.PyperclipException as e:
1020
+ print(f"\nWarning: Failed to copy to clipboard: {e}")
1021
+ print("You can manually copy the prompt above.")
1350
1022
 
1351
1023
  if __name__ == "__main__":
1352
1024
  main()
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.1
2
+ Name: kopipasta
3
+ Version: 0.28.0
4
+ Summary: A CLI tool to generate prompts with project structure and file contents
5
+ Home-page: https://github.com/mkorpela/kopipasta
6
+ Author: Mikko Korpela
7
+ Author-email: mikko.korpela@gmail.com
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pyperclip==1.9.0
23
+ Requires-Dist: requests==2.32.3
24
+ Requires-Dist: Pygments==2.18.0
25
+
26
+ # kopipasta
27
+
28
+ [![Version](https://img.shields.io/pypi/v/kopipasta.svg)](https://pypi.python.org/pypi/kopipasta)
29
+ [![Downloads](http://pepy.tech/badge/kopipasta)](http://pepy.tech/project/kopipasta)
30
+
31
+ A CLI tool for taking **full, transparent control** of your LLM context. No black boxes.
32
+
33
+ <img src="kopipasta.jpg" alt="kopipasta" width="300">
34
+
35
+ - An LLM told me that "kopi" means Coffee in some languages... and a Diffusion model then made this delicious soup.
36
+
37
+ ## The Philosophy: You Control the Context
38
+
39
+ Many AI coding assistants use Retrieval-Augmented Generation (RAG) to automatically find what *they think* is relevant context. This is a black box. When the LLM gives a bad answer, you can't debug it because you don't know what context it was actually given.
40
+
41
+ **`kopipasta` is the opposite.** I built it for myself on the principle of **explicit context control**. You are in the driver's seat. You decide *exactly* what files, functions, and snippets go into the prompt. This transparency is the key to getting reliable, debuggable results from an LLM.
42
+
43
+ It's a "smart copy" command for your project, not a magic wand.
44
+
45
+ ## How It Works
46
+
47
+ The workflow is dead simple:
48
+
49
+ 1. **Gather:** Run `kopipasta` and point it at the files, directories, and URLs that matter for your task.
50
+ 2. **Select:** The tool interactively helps you choose what to include. For large files, you can send just a snippet or even hand-pick individual functions.
51
+ 3. **Define:** Your default editor (`$EDITOR`) opens for you to write your instructions to the LLM.
52
+ 4. **Paste:** The final, comprehensive prompt is now on your clipboard, ready to be pasted into ChatGPT, Gemini, Claude, or your LLM of choice.
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ # Using pipx (recommended for CLI tools)
58
+ pipx install kopipasta
59
+
60
+ # Or using standard pip
61
+ pip install kopipasta
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ```bash
67
+ kopipasta [options] [files_or_directories_or_urls...]
68
+ ```
69
+
70
+ **Arguments:**
71
+
72
+ * `[files_or_directories_or_urls...]`: One or more paths to files, directories, or web URLs to use as the starting point for your context.
73
+
74
+ **Options:**
75
+
76
+ * `-t TASK`, `--task TASK`: Provide the task description directly on the command line, skipping the editor.
77
+
78
+ ## Key Features
79
+
80
+ * **Total Context Control:** Interactively select files, directories, snippets, or even individual functions. You see everything that goes into the prompt.
81
+ * **Transparent & Explicit:** No hidden RAG. You know exactly what's in the prompt because you built it. This makes debugging LLM failures possible.
82
+ * **Web-Aware:** Pulls in content directly from URLs—perfect for API documentation.
83
+ * **Safety First:**
84
+ * Automatically respects your `.gitignore` rules.
85
+ * Detects if you're about to include secrets from a `.env` file and asks what to do.
86
+ * **Context-Aware:** Keeps a running total of the prompt size (in characters and estimated tokens) so you don't overload the LLM's context window.
87
+ * **Developer-Friendly:**
88
+ * Uses your familiar `$EDITOR` for writing task descriptions.
89
+ * Copies the final prompt directly to your clipboard.
90
+ * Provides syntax highlighting during chunk selection.
91
+
92
+ ## A Real-World Example
93
+
94
+ I had a bug where my `setup.py` didn't include all the dependencies from `requirements.txt`.
95
+
96
+ 1. I ran `kopipasta -t "Update setup.py to read dependencies dynamically from requirements.txt" setup.py requirements.txt`.
97
+ 2. The tool confirmed the inclusion of both files and copied the complete prompt to my clipboard.
98
+ 3. I pasted the prompt into my LLM chat window.
99
+ 4. I copied the LLM's suggested code back into my local `setup.py`.
100
+ 5. I tested the changes and committed.
101
+
102
+ No manual file reading, no clumsy copy-pasting, just a clean, context-rich prompt that I had full control over.
103
+
104
+ ## Configuration
105
+
106
+ Set your preferred command-line editor via the `EDITOR` environment variable.
107
+ ```bash
108
+ export EDITOR=nvim # or vim, nano, code --wait, etc.
109
+ ```
@@ -0,0 +1,9 @@
1
+ kopipasta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ kopipasta/import_parser.py,sha256=yLzkMlQm2avKjfqcpMY0PxbA_2ihV9gSYJplreWIPEQ,12424
3
+ kopipasta/main.py,sha256=MUTi4vj_OWTwb2Y0PqQvm--oaX3FKHSrqAUAIDvcPwU,43910
4
+ kopipasta-0.28.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
5
+ kopipasta-0.28.0.dist-info/METADATA,sha256=XxmONaSfjOxhNSh4X31mdahvKxKwfwtQz0IxIA1lpFc,4838
6
+ kopipasta-0.28.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
7
+ kopipasta-0.28.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
8
+ kopipasta-0.28.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
9
+ kopipasta-0.28.0.dist-info/RECORD,,
@@ -1,171 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: kopipasta
3
- Version: 0.27.0
4
- Summary: A CLI tool to generate prompts with project structure and file contents
5
- Home-page: https://github.com/mkorpela/kopipasta
6
- Author: Mikko Korpela
7
- Author-email: mikko.korpela@gmail.com
8
- License: MIT
9
- Classifier: Development Status :: 3 - Alpha
10
- Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
- Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.8
15
- Classifier: Programming Language :: Python :: 3.9
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Requires-Python: >=3.8
20
- Description-Content-Type: text/markdown
21
- License-File: LICENSE
22
- Requires-Dist: pyperclip==1.9.0
23
- Requires-Dist: requests==2.32.3
24
- Requires-Dist: Pygments==2.18.0
25
- Requires-Dist: google-genai==1.8.0
26
- Requires-Dist: prompt-toolkit==3.0.50
27
-
28
- # kopipasta
29
-
30
- [![Version](https://img.shields.io/pypi/v/kopipasta.svg)](https://pypi.python.org/pypi/kopipasta)
31
- [![Downloads](http://pepy.tech/badge/kopipasta)](http://pepy.tech/project/kopipasta)
32
-
33
- Streamline your interaction with LLMs for coding tasks. `kopipasta` helps you provide comprehensive context (project structure, file contents, web content) and facilitates an interactive, patch-based workflow. Go beyond TAB TAB TAB and take control of your LLM context.
34
-
35
- <img src="kopipasta.jpg" alt="kopipasta" width="300">
36
-
37
- - An LLM told me that kopi means Coffee in some languages.. and a Diffusion model then made this delicious soup.
38
-
39
- ## Installation
40
-
41
- You can install kopipasta using pipx (recommended) or pip:
42
-
43
- ```bash
44
- # Using pipx (recommended)
45
- pipx install kopipasta
46
-
47
- # Or using pip
48
- pip install kopipasta
49
- ```
50
-
51
- ## Usage
52
-
53
- ```bash
54
- kopipasta [options] [files_or_directories_or_urls...]
55
- ```
56
-
57
- **Arguments:**
58
-
59
- * `[files_or_directories_or_urls...]`: Paths to files, directories, or web URLs to include as context.
60
-
61
- **Options:**
62
-
63
- * `-t TASK`, `--task TASK`: Provide the task description directly via the command line. If omitted (and not using `-I`), an editor will open for you to write the task.
64
- * `-I`, `--interactive`: Start an interactive chat session with Google's Gemini model after preparing the context. Requires `GOOGLE_API_KEY` environment variable.
65
-
66
- **Examples:**
67
-
68
- 1. **Generate prompt and copy to clipboard (classic mode):**
69
- ```bash
70
- # Interactively select files from src/, include config.json, fetch web content,
71
- # then open editor for task input. Copy final prompt to clipboard.
72
- kopipasta src/ config.json https://example.com/api-docs
73
-
74
- # Provide task directly, include specific files, copy final prompt.
75
- kopipasta -t "Refactor setup.py to read deps from requirements.txt" setup.py requirements.txt
76
- ```
77
-
78
- 2. **Start an interactive chat session:**
79
- ```bash
80
- # Interactively select files, provide task directly, then start chat.
81
- kopipasta -I -t "Implement the apply_simple_patch function" kopipasta/main.py
82
-
83
- # Interactively select files, open editor for initial task, then start chat.
84
- kopipasta -I kopipasta/ tests/
85
- ```
86
-
87
- ## Workflow
88
-
89
- `kopipasta` is designed to support the following workflow when working with LLMs (like Gemini, ChatGPT, Claude, etc.) for coding tasks:
90
-
91
- 1. **Gather Context:** Run `kopipasta` with the relevant files, directories, and URLs. Interactively select exactly what content (full files, snippets, or specific code chunks/patches) should be included.
92
- 2. **Define Task:** Provide your coding task instructions, either via the `-t` flag or through your default editor.
93
- 3. **Interact (if using `-I`):**
94
- * `kopipasta` prepares the context and your task as an initial prompt.
95
- * An interactive chat session starts (currently using Google Gemini via `google-genai`).
96
- * Discuss the task, clarify requirements, and ask the LLM to generate code.
97
- * The initial prompt includes instructions guiding the LLM to provide incremental changes and clear explanations.
98
- 4. **Request Patches (`-I` mode):**
99
- * During the chat, use the `/patch` command to ask the LLM to provide the proposed changes in a structured format.
100
- * `kopipasta` will prompt you to review the proposed patches (file, reasoning, code change).
101
- 5. **Apply Patches (`-I` mode):**
102
- * If you approve, `kopipasta` will attempt to automatically apply the patches to your local files. It validates that the original code exists and is unique before applying.
103
- 6. **Test & Iterate:** Test the changes locally. If further changes are needed, continue the chat, request new patches, or make manual edits.
104
- 7. **Commit:** Once satisfied, commit the changes.
105
-
106
- For non-interactive mode, `kopipasta` generates the complete prompt (context + task) and copies it to your clipboard (Step 1 & 2). You can then paste this into your preferred LLM interface and proceed manually from Step 3 onwards.
107
-
108
- ## Features
109
-
110
- * **Comprehensive Context Generation:** Creates structured prompts including:
111
- * Project directory tree overview.
112
- * Selected file contents.
113
- * Content fetched from web URLs.
114
- * Your specific task instructions.
115
- * **Interactive File Selection:**
116
- * Guides you through selecting files and directories.
117
- * Option to include full file content, a snippet (first lines/bytes), or **select specific code chunks/patches** for large or complex files.
118
- * Syntax highlighting during chunk selection for supported languages.
119
- * Ignores files based on common `.gitignore` patterns and detects binary files.
120
- * Displays estimated character/token counts during selection.
121
- * **Web Content Fetching:** Includes content directly from URLs. Handles JSON/CSV content types.
122
- * **Editor Integration:** Opens your preferred editor (`$EDITOR`) to input task instructions (if not using `-t`).
123
- * **Environment Variable Handling:** Detects potential secrets from a `.env` file in included content and prompts you to mask, skip, or keep them.
124
- * **Clipboard Integration:** Automatically copies the generated prompt to the clipboard (non-interactive mode).
125
- * **Interactive Chat Mode (`-I`, `--interactive`):**
126
- * Starts a chat session directly after context generation.
127
- * Uses the `google-genai` library to interact with Google's Gemini models.
128
- * Requires the `GOOGLE_API_KEY` environment variable to be set.
129
- * Includes built-in instructions for the LLM to encourage clear, iterative responses.
130
- * **Patch Management (`-I` mode):**
131
- * `/patch` command to request structured code changes from the LLM.
132
- * Prompts user to review proposed patches (reasoning, file, original/new code snippets).
133
- * **Automatic patch application** to local files upon confirmation.
134
-
135
- ## Configuration
136
-
137
- * **Editor:** Set the `EDITOR` environment variable to your preferred command-line editor (e.g., `vim`, `nvim`, `nano`, `emacs`, `code --wait`).
138
- * **API Key (for `-I` mode):** Set the `GOOGLE_API_KEY` environment variable with your Google AI Studio API key to use the interactive chat feature.
139
-
140
- ## Real life example (Non-Interactive)
141
-
142
- Context: I had a bug where `setup.py` didn't include all dependencies listed in `requirements.txt`.
143
-
144
- 1. `kopipasta -t "Update setup.py to read dependencies dynamically from requirements.txt" setup.py requirements.txt`
145
- 2. Paste the generated prompt (copied to clipboard) into my preferred LLM chat interface.
146
- 3. Review the LLM's proposed code.
147
- 4. Copy the code and update `setup.py` manually.
148
- 5. Test the changes.
149
-
150
- ## Real life example (Interactive)
151
-
152
- Context: I want to refactor a function in `main.py`.
153
-
154
- 1. `export GOOGLE_API_KEY="YOUR_API_KEY_HERE"` (ensure key is set)
155
- 2. `kopipasta -I -t "Refactor the handle_content function in main.py to be more modular" module/main.py`
156
- 3. The tool gathers context, shows the file size, and confirms inclusion.
157
- 4. An interactive chat session starts with the context and task sent to Gemini.
158
- 5. Chat with the LLM:
159
- * *User:* "Proceed"
160
- * *LLM:* "Okay, I understand. My plan is to..."
161
- * *User:* "Looks good."
162
- * *LLM:* "Here's the first part of the refactoring..." (shows code)
163
- 6. Use the `/patch` command:
164
- * *User:* `/patch`
165
- * `kopipasta` asks the LLM for structured patches.
166
- * `kopipasta` displays proposed patches: "Apply 1 patch to module/main.py? (y/N):"
167
- 7. Apply the patch:
168
- * *User:* `y`
169
- * `kopipasta` applies the change to `module/main.py`.
170
- 8. Test locally. If it works, commit. If not, continue chatting, request more patches, or debug.
171
-
@@ -1,9 +0,0 @@
1
- kopipasta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- kopipasta/import_parser.py,sha256=yLzkMlQm2avKjfqcpMY0PxbA_2ihV9gSYJplreWIPEQ,12424
3
- kopipasta/main.py,sha256=gTIPH5dScVKym_5QFrWE3gmChvrFaIgMJyvnTBxc_iI,62607
4
- kopipasta-0.27.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
5
- kopipasta-0.27.0.dist-info/METADATA,sha256=ubZEEhK8410J4kZ5x1MTAWfm2EXPO4NtLqbpUB44rcM,8610
6
- kopipasta-0.27.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
7
- kopipasta-0.27.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
8
- kopipasta-0.27.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
9
- kopipasta-0.27.0.dist-info/RECORD,,