kopipasta 0.25.0__py3-none-any.whl → 0.27.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/import_parser.py +284 -0
- kopipasta/main.py +220 -75
- {kopipasta-0.25.0.dist-info → kopipasta-0.27.0.dist-info}/METADATA +1 -1
- kopipasta-0.27.0.dist-info/RECORD +9 -0
- kopipasta-0.25.0.dist-info/RECORD +0 -8
- {kopipasta-0.25.0.dist-info → kopipasta-0.27.0.dist-info}/LICENSE +0 -0
- {kopipasta-0.25.0.dist-info → kopipasta-0.27.0.dist-info}/WHEEL +0 -0
- {kopipasta-0.25.0.dist-info → kopipasta-0.27.0.dist-info}/entry_points.txt +0 -0
- {kopipasta-0.25.0.dist-info → kopipasta-0.27.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
import ast
|
|
5
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
6
|
+
|
|
7
|
+
# --- Global Cache for tsconfig.json data ---
|
|
8
|
+
# Key: absolute path to tsconfig.json file
|
|
9
|
+
# Value: Tuple (absolute_base_url: Optional[str], alias_paths_map: Dict[str, List[str]])
|
|
10
|
+
_tsconfig_configs_cache: Dict[str, Tuple[Optional[str], Dict[str, List[str]]]] = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# --- TypeScript Alias and Import Resolution ---
|
|
14
|
+
|
|
15
|
+
def find_relevant_tsconfig_path(file_path_abs: str, project_root_abs: str) -> Optional[str]:
|
|
16
|
+
"""
|
|
17
|
+
Finds the most relevant tsconfig.json by searching upwards from the file's directory,
|
|
18
|
+
stopping at project_root_abs.
|
|
19
|
+
Searches for 'tsconfig.json' first, then 'tsconfig.*.json' in each directory.
|
|
20
|
+
"""
|
|
21
|
+
current_dir = os.path.dirname(os.path.normpath(file_path_abs))
|
|
22
|
+
project_root_abs_norm = os.path.normpath(project_root_abs)
|
|
23
|
+
|
|
24
|
+
while current_dir.startswith(project_root_abs_norm) and len(current_dir) >= len(project_root_abs_norm):
|
|
25
|
+
potential_tsconfig = os.path.join(current_dir, "tsconfig.json")
|
|
26
|
+
if os.path.isfile(potential_tsconfig):
|
|
27
|
+
return os.path.normpath(potential_tsconfig)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
variant_tsconfigs = sorted([
|
|
31
|
+
f for f in os.listdir(current_dir)
|
|
32
|
+
if f.startswith("tsconfig.") and f.endswith(".json") and
|
|
33
|
+
os.path.isfile(os.path.join(current_dir, f))
|
|
34
|
+
])
|
|
35
|
+
if variant_tsconfigs:
|
|
36
|
+
return os.path.normpath(os.path.join(current_dir, variant_tsconfigs[0]))
|
|
37
|
+
except OSError:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
if current_dir == project_root_abs_norm:
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
parent_dir = os.path.dirname(current_dir)
|
|
44
|
+
if parent_dir == current_dir:
|
|
45
|
+
break
|
|
46
|
+
current_dir = parent_dir
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_tsconfig_config(tsconfig_path_abs: str) -> Tuple[Optional[str], Dict[str, List[str]]]:
|
|
51
|
+
"""
|
|
52
|
+
Loads baseUrl and paths from a specific tsconfig.json.
|
|
53
|
+
Caches results.
|
|
54
|
+
Returns (absolute_base_url, paths_map).
|
|
55
|
+
"""
|
|
56
|
+
if tsconfig_path_abs in _tsconfig_configs_cache:
|
|
57
|
+
return _tsconfig_configs_cache[tsconfig_path_abs]
|
|
58
|
+
|
|
59
|
+
if not os.path.isfile(tsconfig_path_abs):
|
|
60
|
+
_tsconfig_configs_cache[tsconfig_path_abs] = (None, {})
|
|
61
|
+
return None, {}
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
with open(tsconfig_path_abs, 'r', encoding='utf-8') as f:
|
|
65
|
+
content = f.read()
|
|
66
|
+
content = re.sub(r"//.*?\n", "\n", content)
|
|
67
|
+
content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
|
|
68
|
+
config = json.loads(content)
|
|
69
|
+
|
|
70
|
+
compiler_options = config.get("compilerOptions", {})
|
|
71
|
+
tsconfig_dir = os.path.dirname(tsconfig_path_abs)
|
|
72
|
+
base_url_from_config = compiler_options.get("baseUrl", ".")
|
|
73
|
+
abs_base_url = os.path.normpath(os.path.join(tsconfig_dir, base_url_from_config))
|
|
74
|
+
|
|
75
|
+
paths = compiler_options.get("paths", {})
|
|
76
|
+
processed_paths = {key: (val if isinstance(val, list) else [val]) for key, val in paths.items()}
|
|
77
|
+
|
|
78
|
+
# print(f"DEBUG: Loaded config from {os.path.relpath(tsconfig_path_abs)}: effective abs_baseUrl='{abs_base_url}', {len(processed_paths)} path alias(es).")
|
|
79
|
+
_tsconfig_configs_cache[tsconfig_path_abs] = (abs_base_url, processed_paths)
|
|
80
|
+
return abs_base_url, processed_paths
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"Warning: Could not parse {os.path.relpath(tsconfig_path_abs)}: {e}")
|
|
83
|
+
_tsconfig_configs_cache[tsconfig_path_abs] = (None, {})
|
|
84
|
+
return None, {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _probe_ts_path_candidates(candidate_base_path_abs: str) -> Optional[str]:
|
|
88
|
+
"""
|
|
89
|
+
Given a candidate base absolute path, tries to find a corresponding file.
|
|
90
|
+
"""
|
|
91
|
+
possible_extensions = ['.ts', '.tsx', '.js', '.jsx', '.json']
|
|
92
|
+
|
|
93
|
+
if os.path.isfile(candidate_base_path_abs):
|
|
94
|
+
return candidate_base_path_abs
|
|
95
|
+
|
|
96
|
+
stem, original_ext = os.path.splitext(candidate_base_path_abs)
|
|
97
|
+
base_for_ext_check = stem if original_ext.lower() in possible_extensions else candidate_base_path_abs
|
|
98
|
+
|
|
99
|
+
for ext in possible_extensions:
|
|
100
|
+
path_with_ext = base_for_ext_check + ext
|
|
101
|
+
if os.path.isfile(path_with_ext):
|
|
102
|
+
return path_with_ext
|
|
103
|
+
|
|
104
|
+
if os.path.isdir(base_for_ext_check):
|
|
105
|
+
for ext in possible_extensions:
|
|
106
|
+
index_file_path = os.path.join(base_for_ext_check, "index" + ext)
|
|
107
|
+
if os.path.isfile(index_file_path):
|
|
108
|
+
return index_file_path
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def resolve_ts_import_path(
|
|
113
|
+
import_str: str,
|
|
114
|
+
current_file_dir_abs: str,
|
|
115
|
+
abs_base_url: Optional[str],
|
|
116
|
+
alias_map: Dict[str, List[str]]
|
|
117
|
+
) -> Optional[str]:
|
|
118
|
+
"""
|
|
119
|
+
Resolves a TypeScript import string to an absolute file path.
|
|
120
|
+
"""
|
|
121
|
+
candidate_targets_abs: List[str] = []
|
|
122
|
+
sorted_alias_keys = sorted(alias_map.keys(), key=len, reverse=True)
|
|
123
|
+
alias_matched_and_resolved = False
|
|
124
|
+
|
|
125
|
+
for alias_pattern in sorted_alias_keys:
|
|
126
|
+
alias_prefix_pattern = alias_pattern.replace("/*", "")
|
|
127
|
+
if import_str.startswith(alias_prefix_pattern):
|
|
128
|
+
import_suffix = import_str[len(alias_prefix_pattern):]
|
|
129
|
+
for mapping_path_template_list in alias_map[alias_pattern]:
|
|
130
|
+
for mapping_path_template in (mapping_path_template_list if isinstance(mapping_path_template_list, list) else [mapping_path_template_list]):
|
|
131
|
+
if "/*" in alias_pattern :
|
|
132
|
+
resolved_relative_to_base = mapping_path_template.replace("*", import_suffix, 1)
|
|
133
|
+
else:
|
|
134
|
+
resolved_relative_to_base = mapping_path_template
|
|
135
|
+
if abs_base_url:
|
|
136
|
+
abs_candidate = os.path.normpath(os.path.join(abs_base_url, resolved_relative_to_base))
|
|
137
|
+
candidate_targets_abs.append(abs_candidate)
|
|
138
|
+
else:
|
|
139
|
+
print(f"Warning: TS Alias '{alias_pattern}' used, but no abs_base_url for context of '{current_file_dir_abs}'.")
|
|
140
|
+
if candidate_targets_abs:
|
|
141
|
+
alias_matched_and_resolved = True
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
if not alias_matched_and_resolved and import_str.startswith('.'):
|
|
145
|
+
abs_candidate = os.path.normpath(os.path.join(current_file_dir_abs, import_str))
|
|
146
|
+
candidate_targets_abs.append(abs_candidate)
|
|
147
|
+
elif not alias_matched_and_resolved and abs_base_url and not import_str.startswith('.'):
|
|
148
|
+
abs_candidate = os.path.normpath(os.path.join(abs_base_url, import_str))
|
|
149
|
+
candidate_targets_abs.append(abs_candidate)
|
|
150
|
+
|
|
151
|
+
for cand_abs_path in candidate_targets_abs:
|
|
152
|
+
resolved_file = _probe_ts_path_candidates(cand_abs_path)
|
|
153
|
+
if resolved_file:
|
|
154
|
+
return resolved_file
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def parse_typescript_imports(
|
|
159
|
+
file_content: str,
|
|
160
|
+
file_path_abs: str,
|
|
161
|
+
project_root_abs: str
|
|
162
|
+
) -> Set[str]:
|
|
163
|
+
resolved_imports_abs_paths = set()
|
|
164
|
+
relevant_tsconfig_abs_path = find_relevant_tsconfig_path(file_path_abs, project_root_abs)
|
|
165
|
+
|
|
166
|
+
abs_base_url, alias_map = None, {}
|
|
167
|
+
if relevant_tsconfig_abs_path:
|
|
168
|
+
abs_base_url, alias_map = load_tsconfig_config(relevant_tsconfig_abs_path)
|
|
169
|
+
else:
|
|
170
|
+
# print(f"Warning: No tsconfig.json found for {os.path.relpath(file_path_abs, project_root_abs)}. Import resolution might be limited.")
|
|
171
|
+
abs_base_url = project_root_abs
|
|
172
|
+
|
|
173
|
+
import_regex = re.compile(
|
|
174
|
+
r"""
|
|
175
|
+
(?:import|export)
|
|
176
|
+
(?:\s+(?:type\s+)?(?:[\w*{}\s,\[\]:\."'`-]+)\s+from)?
|
|
177
|
+
\s*['"`]([^'"\n`]+?)['"`]
|
|
178
|
+
|require\s*\(\s*['"`]([^'"\n`]+?)['"`]\s*\)
|
|
179
|
+
|import\s*\(\s*['"`]([^'"\n`]+?)['"`]\s*\)
|
|
180
|
+
""",
|
|
181
|
+
re.VERBOSE | re.MULTILINE
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
current_file_dir_abs = os.path.dirname(file_path_abs)
|
|
185
|
+
|
|
186
|
+
for match in import_regex.finditer(file_content):
|
|
187
|
+
import_str_candidate = next((g for g in match.groups() if g is not None), None)
|
|
188
|
+
if import_str_candidate:
|
|
189
|
+
is_likely_external = (
|
|
190
|
+
not import_str_candidate.startswith(('.', '/')) and
|
|
191
|
+
not any(import_str_candidate.startswith(alias_pattern.replace("/*", "")) for alias_pattern in alias_map) and
|
|
192
|
+
not (abs_base_url and os.path.exists(os.path.join(abs_base_url, import_str_candidate))) and
|
|
193
|
+
(import_str_candidate.count('/') == 0 or (import_str_candidate.startswith('@') and import_str_candidate.count('/') == 1)) and
|
|
194
|
+
'.' not in import_str_candidate.split('/')[0]
|
|
195
|
+
)
|
|
196
|
+
if is_likely_external:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
resolved_abs_path = resolve_ts_import_path(
|
|
200
|
+
import_str_candidate,
|
|
201
|
+
current_file_dir_abs,
|
|
202
|
+
abs_base_url,
|
|
203
|
+
alias_map
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if resolved_abs_path:
|
|
207
|
+
norm_resolved_path = os.path.normpath(resolved_abs_path)
|
|
208
|
+
if norm_resolved_path.startswith(os.path.normpath(project_root_abs)):
|
|
209
|
+
resolved_imports_abs_paths.add(norm_resolved_path)
|
|
210
|
+
return resolved_imports_abs_paths
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# --- Python Import Resolution ---
|
|
214
|
+
|
|
215
|
+
def resolve_python_import(
|
|
216
|
+
module_name_parts: List[str],
|
|
217
|
+
current_file_dir_abs: str,
|
|
218
|
+
project_root_abs: str,
|
|
219
|
+
level: int
|
|
220
|
+
) -> Optional[str]:
|
|
221
|
+
base_path_to_search = ""
|
|
222
|
+
if level > 0:
|
|
223
|
+
base_path_to_search = current_file_dir_abs
|
|
224
|
+
for _ in range(level - 1):
|
|
225
|
+
base_path_to_search = os.path.dirname(base_path_to_search)
|
|
226
|
+
else:
|
|
227
|
+
base_path_to_search = project_root_abs
|
|
228
|
+
|
|
229
|
+
candidate_rel_path = os.path.join(*module_name_parts)
|
|
230
|
+
potential_abs_path = os.path.join(base_path_to_search, candidate_rel_path)
|
|
231
|
+
|
|
232
|
+
py_file = potential_abs_path + ".py"
|
|
233
|
+
if os.path.isfile(py_file):
|
|
234
|
+
return os.path.normpath(py_file)
|
|
235
|
+
|
|
236
|
+
init_file = os.path.join(potential_abs_path, "__init__.py")
|
|
237
|
+
if os.path.isdir(potential_abs_path) and os.path.isfile(init_file):
|
|
238
|
+
return os.path.normpath(init_file)
|
|
239
|
+
|
|
240
|
+
if level == 0 and base_path_to_search == project_root_abs:
|
|
241
|
+
src_base_path = os.path.join(project_root_abs, "src")
|
|
242
|
+
if os.path.isdir(src_base_path):
|
|
243
|
+
potential_abs_path_src = os.path.join(src_base_path, candidate_rel_path)
|
|
244
|
+
py_file_src = potential_abs_path_src + ".py"
|
|
245
|
+
if os.path.isfile(py_file_src):
|
|
246
|
+
return os.path.normpath(py_file_src)
|
|
247
|
+
init_file_src = os.path.join(potential_abs_path_src, "__init__.py")
|
|
248
|
+
if os.path.isdir(potential_abs_path_src) and os.path.isfile(init_file_src):
|
|
249
|
+
return os.path.normpath(init_file_src)
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def parse_python_imports(file_content: str, file_path_abs: str, project_root_abs: str) -> Set[str]:
|
|
254
|
+
resolved_imports = set()
|
|
255
|
+
current_file_dir_abs = os.path.dirname(file_path_abs)
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
tree = ast.parse(file_content, filename=file_path_abs)
|
|
259
|
+
except SyntaxError:
|
|
260
|
+
# print(f"Warning: Syntax error in {file_path_abs}, cannot parse Python imports.")
|
|
261
|
+
return resolved_imports
|
|
262
|
+
|
|
263
|
+
for node in ast.walk(tree):
|
|
264
|
+
if isinstance(node, ast.Import):
|
|
265
|
+
for alias in node.names:
|
|
266
|
+
module_parts = alias.name.split('.')
|
|
267
|
+
resolved = resolve_python_import(module_parts, current_file_dir_abs, project_root_abs, level=0)
|
|
268
|
+
if resolved and os.path.exists(resolved) and os.path.normpath(resolved).startswith(os.path.normpath(project_root_abs)):
|
|
269
|
+
resolved_imports.add(os.path.normpath(resolved))
|
|
270
|
+
elif isinstance(node, ast.ImportFrom):
|
|
271
|
+
level_to_resolve = node.level
|
|
272
|
+
if node.module:
|
|
273
|
+
module_parts = node.module.split('.')
|
|
274
|
+
resolved = resolve_python_import(module_parts, current_file_dir_abs, project_root_abs, level_to_resolve)
|
|
275
|
+
if resolved and os.path.exists(resolved) and os.path.normpath(resolved).startswith(os.path.normpath(project_root_abs)):
|
|
276
|
+
resolved_imports.add(os.path.normpath(resolved))
|
|
277
|
+
else:
|
|
278
|
+
for alias in node.names:
|
|
279
|
+
item_name_parts = alias.name.split('.')
|
|
280
|
+
resolved = resolve_python_import(item_name_parts, current_file_dir_abs, project_root_abs, level=level_to_resolve)
|
|
281
|
+
if resolved and os.path.exists(resolved) and os.path.normpath(resolved).startswith(os.path.normpath(project_root_abs)):
|
|
282
|
+
resolved_imports.add(os.path.normpath(resolved))
|
|
283
|
+
return resolved_imports
|
|
284
|
+
|
kopipasta/main.py
CHANGED
|
@@ -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((
|
|
575
|
-
|
|
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((
|
|
578
|
-
|
|
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((
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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,29 +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
|
-
"
|
|
767
|
-
"
|
|
768
|
-
"
|
|
769
|
-
"
|
|
770
|
-
"
|
|
771
|
-
"
|
|
772
|
-
"
|
|
773
|
-
"
|
|
774
|
-
"
|
|
775
|
-
"
|
|
776
|
-
"
|
|
777
|
-
"
|
|
778
|
-
"
|
|
779
|
-
"
|
|
780
|
-
"
|
|
781
|
-
"
|
|
782
|
-
"
|
|
783
|
-
"
|
|
784
|
-
"
|
|
785
|
-
"
|
|
786
|
-
"
|
|
787
|
-
"
|
|
788
|
-
"
|
|
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"
|
|
789
934
|
)
|
|
790
935
|
prompt += analysis_text
|
|
791
936
|
return prompt, cursor_position
|
|
@@ -1032,6 +1177,8 @@ def main():
|
|
|
1032
1177
|
|
|
1033
1178
|
ignore_patterns = read_gitignore()
|
|
1034
1179
|
env_vars = read_env_file()
|
|
1180
|
+
project_root_abs = os.path.abspath(os.getcwd())
|
|
1181
|
+
|
|
1035
1182
|
|
|
1036
1183
|
files_to_include: List[FileTuple] = []
|
|
1037
1184
|
processed_dirs = set()
|
|
@@ -1074,53 +1221,59 @@ def main():
|
|
|
1074
1221
|
print(f"Added {'snippet of ' if is_snippet else ''}web content from: {input_path}")
|
|
1075
1222
|
print_char_count(current_char_count)
|
|
1076
1223
|
elif os.path.isfile(input_path):
|
|
1077
|
-
|
|
1078
|
-
if not is_ignored(
|
|
1079
|
-
file_size = os.path.getsize(
|
|
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)
|
|
1080
1227
|
file_size_readable = get_human_readable_size(file_size)
|
|
1081
1228
|
file_char_estimate = file_size
|
|
1082
|
-
language = get_language_for_file(
|
|
1229
|
+
language = get_language_for_file(abs_input_path)
|
|
1230
|
+
file_to_add = None
|
|
1083
1231
|
|
|
1084
|
-
if is_large_file(
|
|
1085
|
-
print(f"\nFile {
|
|
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.")
|
|
1086
1234
|
print("Preview (first ~50 lines or 4KB):")
|
|
1087
|
-
print(get_colored_file_snippet(
|
|
1235
|
+
print(get_colored_file_snippet(abs_input_path))
|
|
1088
1236
|
print("-" * 40)
|
|
1089
1237
|
while True:
|
|
1090
1238
|
print_char_count(current_char_count)
|
|
1091
|
-
choice = input(f"How to include large file {
|
|
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()
|
|
1092
1240
|
if choice == 'f':
|
|
1093
|
-
|
|
1241
|
+
file_to_add = (abs_input_path, False, None, language)
|
|
1094
1242
|
current_char_count += file_char_estimate
|
|
1095
|
-
print(f"Added full file: {
|
|
1243
|
+
print(f"Added full file: {get_relative_path(abs_input_path)}")
|
|
1096
1244
|
break
|
|
1097
1245
|
elif choice == 's':
|
|
1098
|
-
snippet_content = get_file_snippet(
|
|
1099
|
-
|
|
1246
|
+
snippet_content = get_file_snippet(abs_input_path)
|
|
1247
|
+
file_to_add = (abs_input_path, True, None, language)
|
|
1100
1248
|
current_char_count += len(snippet_content)
|
|
1101
|
-
print(f"Added snippet of file: {
|
|
1249
|
+
print(f"Added snippet of file: {get_relative_path(abs_input_path)}")
|
|
1102
1250
|
break
|
|
1103
1251
|
elif choice == 'p':
|
|
1104
|
-
chunks, char_count = select_file_patches(
|
|
1252
|
+
chunks, char_count = select_file_patches(abs_input_path)
|
|
1105
1253
|
if chunks:
|
|
1106
|
-
|
|
1254
|
+
file_to_add = (abs_input_path, False, chunks, language)
|
|
1107
1255
|
current_char_count += char_count
|
|
1108
|
-
print(f"Added selected patches from file: {
|
|
1256
|
+
print(f"Added selected patches from file: {get_relative_path(abs_input_path)}")
|
|
1109
1257
|
else:
|
|
1110
|
-
print(f"No patches selected for {
|
|
1258
|
+
print(f"No patches selected for {get_relative_path(abs_input_path)}. Skipping file.")
|
|
1111
1259
|
break
|
|
1112
1260
|
elif choice == 'n':
|
|
1113
|
-
print(f"Skipped large file: {
|
|
1261
|
+
print(f"Skipped large file: {get_relative_path(abs_input_path)}")
|
|
1114
1262
|
break
|
|
1115
1263
|
else:
|
|
1116
1264
|
print("Invalid choice. Please enter 'f', 's', 'p', or 'n'.")
|
|
1117
1265
|
else:
|
|
1118
|
-
|
|
1119
|
-
files_to_include.append((input_path, False, None, language))
|
|
1266
|
+
file_to_add = (abs_input_path, False, None, language)
|
|
1120
1267
|
current_char_count += file_char_estimate
|
|
1121
|
-
print(f"Added file: {
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
+
|
|
1124
1277
|
print_char_count(current_char_count)
|
|
1125
1278
|
|
|
1126
1279
|
else:
|
|
@@ -1129,10 +1282,11 @@ def main():
|
|
|
1129
1282
|
elif is_binary(input_path):
|
|
1130
1283
|
print(f"Ignoring binary file: {input_path}")
|
|
1131
1284
|
else:
|
|
1132
|
-
print(f"Ignoring file: {input_path}")
|
|
1285
|
+
print(f"Ignoring file: {input_path}")
|
|
1133
1286
|
elif os.path.isdir(input_path):
|
|
1134
1287
|
print(f"\nProcessing directory specified directly: {input_path}")
|
|
1135
|
-
|
|
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)
|
|
1136
1290
|
files_to_include.extend(dir_files)
|
|
1137
1291
|
processed_dirs.update(dir_processed)
|
|
1138
1292
|
else:
|
|
@@ -1144,42 +1298,34 @@ def main():
|
|
|
1144
1298
|
|
|
1145
1299
|
print("\nFile and web content selection complete.")
|
|
1146
1300
|
print_char_count(current_char_count) # Print final count before prompt generation
|
|
1147
|
-
print(f"Summary: Added {len(files_to_include)} files and {len(web_contents)} web sources.")
|
|
1148
1301
|
|
|
1149
1302
|
added_files_count = len(files_to_include)
|
|
1150
|
-
added_dirs_count = len(processed_dirs)
|
|
1303
|
+
added_dirs_count = len(processed_dirs)
|
|
1151
1304
|
added_web_count = len(web_contents)
|
|
1152
1305
|
print(f"Summary: Added {added_files_count} files/patches from {added_dirs_count} directories and {added_web_count} web sources.")
|
|
1153
1306
|
|
|
1154
1307
|
prompt_template, cursor_position = generate_prompt_template(files_to_include, ignore_patterns, web_contents, env_vars)
|
|
1155
1308
|
|
|
1156
|
-
# Logic branching for interactive mode vs. clipboard mode
|
|
1157
1309
|
if args.interactive:
|
|
1158
1310
|
print("\nPreparing initial prompt for editing...")
|
|
1159
|
-
# Determine the initial content for the editor
|
|
1160
1311
|
if args.task:
|
|
1161
|
-
# Pre-populate the task section if --task was provided
|
|
1162
1312
|
editor_initial_content = prompt_template[:cursor_position] + args.task + prompt_template[cursor_position:]
|
|
1163
1313
|
print("Pre-populating editor with task provided via --task argument.")
|
|
1164
1314
|
else:
|
|
1165
|
-
# Use the template as is (user will add task in the editor)
|
|
1166
1315
|
editor_initial_content = prompt_template
|
|
1167
1316
|
print("Opening editor for you to add the task instructions.")
|
|
1168
1317
|
|
|
1169
|
-
# Always open the editor in interactive mode
|
|
1170
1318
|
initial_chat_prompt = open_editor_for_input(editor_initial_content, cursor_position)
|
|
1171
1319
|
print("Editor closed. Starting interactive chat session...")
|
|
1172
|
-
start_chat_session(initial_chat_prompt)
|
|
1320
|
+
start_chat_session(initial_chat_prompt)
|
|
1173
1321
|
else:
|
|
1174
|
-
# Original non-interactive behavior
|
|
1175
1322
|
if args.task:
|
|
1176
1323
|
task_description = args.task
|
|
1177
|
-
# Insert task description before "## Task Instructions"
|
|
1178
1324
|
task_marker = "## Task Instructions\n\n"
|
|
1179
1325
|
insertion_point = prompt_template.find(task_marker)
|
|
1180
1326
|
if insertion_point != -1:
|
|
1181
1327
|
final_prompt = prompt_template[:insertion_point + len(task_marker)] + task_description + "\n\n" + prompt_template[insertion_point + len(task_marker):]
|
|
1182
|
-
else:
|
|
1328
|
+
else:
|
|
1183
1329
|
final_prompt = prompt_template[:cursor_position] + task_description + prompt_template[cursor_position:]
|
|
1184
1330
|
print("\nUsing task description from -t argument.")
|
|
1185
1331
|
else:
|
|
@@ -1191,7 +1337,6 @@ def main():
|
|
|
1191
1337
|
print(final_prompt)
|
|
1192
1338
|
print("-" * 80)
|
|
1193
1339
|
|
|
1194
|
-
# Copy the prompt to clipboard
|
|
1195
1340
|
try:
|
|
1196
1341
|
pyperclip.copy(final_prompt)
|
|
1197
1342
|
separator = "\n" + "=" * 40 + "\n☕🍝 Kopipasta Complete! 🍝☕\n" + "=" * 40 + "\n"
|
|
@@ -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=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,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
kopipasta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
kopipasta/main.py,sha256=U4_31xXY2xzYZ-Exm1B6TYzofHfH3NU4iEq6XBHyBj0,53647
|
|
3
|
-
kopipasta-0.25.0.dist-info/LICENSE,sha256=xw4C9TAU7LFu4r_MwSbky90uzkzNtRwAo3c51IWR8lk,1091
|
|
4
|
-
kopipasta-0.25.0.dist-info/METADATA,sha256=03L8Zbl0k7pgrrSxL-_DsWbIT-zTRFkENabTcC1MHmo,8610
|
|
5
|
-
kopipasta-0.25.0.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
6
|
-
kopipasta-0.25.0.dist-info/entry_points.txt,sha256=but54qDNz1-F8fVvGstq_QID5tHjczP7bO7rWLFkc6Y,50
|
|
7
|
-
kopipasta-0.25.0.dist-info/top_level.txt,sha256=iXohixMuCdw8UjGDUp0ouICLYBDrx207sgZIJ9lxn0o,10
|
|
8
|
-
kopipasta-0.25.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|