kopipasta 0.29.0__tar.gz → 0.31.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.
- {kopipasta-0.29.0/kopipasta.egg-info → kopipasta-0.31.0}/PKG-INFO +1 -1
- kopipasta-0.31.0/kopipasta/cache.py +37 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta/main.py +11 -2
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta/tree_selector.py +167 -36
- {kopipasta-0.29.0 → kopipasta-0.31.0/kopipasta.egg-info}/PKG-INFO +1 -1
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta.egg-info/SOURCES.txt +1 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/setup.py +1 -1
- {kopipasta-0.29.0 → kopipasta-0.31.0}/LICENSE +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/MANIFEST.in +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/README.md +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta/__init__.py +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta/file.py +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta/import_parser.py +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta/prompt.py +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta.egg-info/dependency_links.txt +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta.egg-info/entry_points.txt +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta.egg-info/requires.txt +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/kopipasta.egg-info/top_level.txt +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/requirements.txt +0 -0
- {kopipasta-0.29.0 → kopipasta-0.31.0}/setup.cfg +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
# Define FileTuple for type hinting
|
|
7
|
+
FileTuple = Tuple[str, bool, List[str] | None, str]
|
|
8
|
+
|
|
9
|
+
def get_cache_file_path() -> Path:
|
|
10
|
+
"""Gets the cross-platform path to the cache file for the last selection."""
|
|
11
|
+
cache_dir = Path.home() / ".cache" / "kopipasta"
|
|
12
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
return cache_dir / "last_selection.json"
|
|
14
|
+
|
|
15
|
+
def save_selection_to_cache(files_to_include: List[FileTuple]):
|
|
16
|
+
"""Saves the list of selected file relative paths to the cache."""
|
|
17
|
+
cache_file = get_cache_file_path()
|
|
18
|
+
relative_paths = sorted([os.path.relpath(f[0]) for f in files_to_include])
|
|
19
|
+
try:
|
|
20
|
+
with open(cache_file, 'w', encoding='utf-8') as f:
|
|
21
|
+
json.dump(relative_paths, f, indent=2)
|
|
22
|
+
except IOError as e:
|
|
23
|
+
print(f"\nWarning: Could not save selection to cache: {e}")
|
|
24
|
+
|
|
25
|
+
def load_selection_from_cache() -> List[str]:
|
|
26
|
+
"""Loads the list of selected files from the cache file."""
|
|
27
|
+
cache_file = get_cache_file_path()
|
|
28
|
+
if not cache_file.exists():
|
|
29
|
+
return []
|
|
30
|
+
try:
|
|
31
|
+
with open(cache_file, 'r', encoding='utf-8') as f:
|
|
32
|
+
paths = json.load(f)
|
|
33
|
+
# Filter out paths that no longer exist
|
|
34
|
+
return [p for p in paths if os.path.exists(p)]
|
|
35
|
+
except (IOError, json.JSONDecodeError) as e:
|
|
36
|
+
print(f"\nWarning: Could not load previous selection from cache: {e}")
|
|
37
|
+
return []
|
|
@@ -18,6 +18,7 @@ from kopipasta.file import FileTuple, get_human_readable_size, is_binary, is_ign
|
|
|
18
18
|
import kopipasta.import_parser as import_parser
|
|
19
19
|
from kopipasta.tree_selector import TreeSelector
|
|
20
20
|
from kopipasta.prompt import generate_prompt_template, get_file_snippet, get_language_for_file
|
|
21
|
+
from kopipasta.cache import save_selection_to_cache
|
|
21
22
|
|
|
22
23
|
def _propose_and_add_dependencies(
|
|
23
24
|
file_just_added: str,
|
|
@@ -993,10 +994,14 @@ def open_editor_for_input(template: str, cursor_position: int) -> str:
|
|
|
993
994
|
|
|
994
995
|
def main():
|
|
995
996
|
parser = argparse.ArgumentParser(description="Generate a prompt with project structure, file contents, and web content.")
|
|
996
|
-
parser.add_argument('inputs', nargs='
|
|
997
|
+
parser.add_argument('inputs', nargs='*', help='Files, directories, or URLs to include. Defaults to current directory.')
|
|
997
998
|
parser.add_argument('-t', '--task', help='Task description for the AI prompt')
|
|
998
999
|
args = parser.parse_args()
|
|
999
1000
|
|
|
1001
|
+
# Default to the current directory if no inputs are provided
|
|
1002
|
+
if not args.inputs:
|
|
1003
|
+
args.inputs.append('.')
|
|
1004
|
+
|
|
1000
1005
|
ignore_patterns = read_gitignore()
|
|
1001
1006
|
env_vars = read_env_file()
|
|
1002
1007
|
project_root_abs = os.path.abspath(os.getcwd())
|
|
@@ -1054,7 +1059,7 @@ def main():
|
|
|
1054
1059
|
# Use tree selector for file/directory selection
|
|
1055
1060
|
if paths_for_tree:
|
|
1056
1061
|
print("\nStarting interactive file selection...")
|
|
1057
|
-
print("Use arrow keys to navigate, Space to select, 'q' to finish.
|
|
1062
|
+
print("Use arrow keys to navigate, Space to select, 'q' to finish. See all keys below.\n")
|
|
1058
1063
|
|
|
1059
1064
|
tree_selector = TreeSelector(ignore_patterns, project_root_abs)
|
|
1060
1065
|
try:
|
|
@@ -1069,6 +1074,10 @@ def main():
|
|
|
1069
1074
|
print("No files or web content were selected. Exiting.")
|
|
1070
1075
|
return
|
|
1071
1076
|
|
|
1077
|
+
# Save the final selection for the next run
|
|
1078
|
+
if files_to_include:
|
|
1079
|
+
save_selection_to_cache(files_to_include)
|
|
1080
|
+
|
|
1072
1081
|
print("\nFile and web content selection complete.")
|
|
1073
1082
|
print_char_count(current_char_count)
|
|
1074
1083
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shutil
|
|
2
3
|
from typing import Dict, List, Optional, Tuple
|
|
3
4
|
from rich.console import Console
|
|
4
5
|
from rich.tree import Tree
|
|
5
6
|
from rich.panel import Panel
|
|
6
|
-
from rich.table import Table
|
|
7
7
|
from rich.text import Text
|
|
8
|
-
from rich.live import Live
|
|
9
|
-
from rich.layout import Layout
|
|
10
8
|
import click
|
|
11
9
|
|
|
12
10
|
from kopipasta.file import FileTuple, is_binary, is_ignored, get_human_readable_size
|
|
13
11
|
from kopipasta.prompt import get_file_snippet, get_language_for_file
|
|
12
|
+
from kopipasta.cache import load_selection_from_cache
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class FileNode:
|
|
@@ -52,6 +51,7 @@ class TreeSelector:
|
|
|
52
51
|
self.visible_nodes: List[FileNode] = []
|
|
53
52
|
self.char_count = 0
|
|
54
53
|
self.quit_selection = False
|
|
54
|
+
self.viewport_offset = 0 # First visible item index
|
|
55
55
|
|
|
56
56
|
def build_tree(self, paths: List[str]) -> FileNode:
|
|
57
57
|
"""Build tree structure from given paths"""
|
|
@@ -154,17 +154,49 @@ class TreeSelector:
|
|
|
154
154
|
return result
|
|
155
155
|
|
|
156
156
|
def _build_display_tree(self) -> Tree:
|
|
157
|
-
"""Build Rich tree for display"""
|
|
158
|
-
|
|
157
|
+
"""Build Rich tree for display with viewport"""
|
|
158
|
+
# Get terminal size
|
|
159
|
+
term_width, term_height = shutil.get_terminal_size()
|
|
159
160
|
|
|
160
|
-
#
|
|
161
|
+
# Reserve space for header, help panel, and status
|
|
162
|
+
available_height = term_height - 15 # Adjust based on your UI
|
|
163
|
+
available_height = max(5, available_height) # Minimum height
|
|
164
|
+
|
|
165
|
+
# Flatten tree to get all visible nodes
|
|
161
166
|
flat_tree = self._flatten_tree(self.root)
|
|
162
167
|
self.visible_nodes = [node for node, _ in flat_tree]
|
|
163
168
|
|
|
164
|
-
#
|
|
169
|
+
# Calculate viewport
|
|
170
|
+
if self.visible_nodes:
|
|
171
|
+
# Ensure current selection is visible
|
|
172
|
+
if self.current_index < self.viewport_offset:
|
|
173
|
+
self.viewport_offset = self.current_index
|
|
174
|
+
elif self.current_index >= self.viewport_offset + available_height:
|
|
175
|
+
self.viewport_offset = self.current_index - available_height + 1
|
|
176
|
+
|
|
177
|
+
# Clamp viewport to valid range
|
|
178
|
+
max_offset = max(0, len(self.visible_nodes) - available_height)
|
|
179
|
+
self.viewport_offset = max(0, min(self.viewport_offset, max_offset))
|
|
180
|
+
else:
|
|
181
|
+
self.viewport_offset = 0
|
|
182
|
+
|
|
183
|
+
# Create tree with scroll indicators
|
|
184
|
+
tree_title = "📁 Project Files"
|
|
185
|
+
if self.viewport_offset > 0:
|
|
186
|
+
tree_title += f" ↑ ({self.viewport_offset} more)"
|
|
187
|
+
|
|
188
|
+
tree = Tree(tree_title, guide_style="dim")
|
|
189
|
+
|
|
190
|
+
# Build tree structure - only for visible portion
|
|
165
191
|
node_map = {}
|
|
192
|
+
viewport_end = min(len(flat_tree), self.viewport_offset + available_height)
|
|
193
|
+
|
|
194
|
+
# Track what level each visible item is at for proper tree structure
|
|
195
|
+
level_stacks = {} # level -> stack of tree nodes
|
|
166
196
|
|
|
167
|
-
for i
|
|
197
|
+
for i in range(self.viewport_offset, viewport_end):
|
|
198
|
+
node, level = flat_tree[i]
|
|
199
|
+
|
|
168
200
|
# Determine style and icon
|
|
169
201
|
is_current = i == self.current_index
|
|
170
202
|
style = "bold cyan" if is_current else ""
|
|
@@ -193,40 +225,45 @@ class TreeSelector:
|
|
|
193
225
|
label.append(f"{selection} ", style="dim")
|
|
194
226
|
label.append(f"{icon} {node.name}{size_str}", style=style)
|
|
195
227
|
|
|
196
|
-
# Add to tree at correct
|
|
197
|
-
|
|
198
|
-
if node.parent and node.parent.path == os.path.abspath("."):
|
|
228
|
+
# Add to tree at correct level
|
|
229
|
+
if level == 0:
|
|
199
230
|
tree_node = tree.add(label)
|
|
200
|
-
|
|
231
|
+
level_stacks[0] = tree_node
|
|
201
232
|
else:
|
|
202
|
-
# Find parent
|
|
203
|
-
|
|
204
|
-
if
|
|
205
|
-
parent_tree =
|
|
233
|
+
# Find parent at previous level
|
|
234
|
+
parent_level = level - 1
|
|
235
|
+
if parent_level in level_stacks:
|
|
236
|
+
parent_tree = level_stacks[parent_level]
|
|
206
237
|
tree_node = parent_tree.add(label)
|
|
207
|
-
|
|
238
|
+
level_stacks[level] = tree_node
|
|
208
239
|
else:
|
|
209
|
-
# Fallback - add to root
|
|
210
|
-
|
|
211
|
-
|
|
240
|
+
# Fallback - add to root with indentation indicator
|
|
241
|
+
indent_label = Text()
|
|
242
|
+
indent_label.append(" " * level + f"{selection} ", style="dim")
|
|
243
|
+
indent_label.append(f"{icon} {node.name}{size_str}", style=style)
|
|
244
|
+
tree_node = tree.add(indent_label)
|
|
245
|
+
level_stacks[level] = tree_node
|
|
246
|
+
|
|
247
|
+
# Add scroll indicator at bottom if needed
|
|
248
|
+
if viewport_end < len(self.visible_nodes):
|
|
249
|
+
remaining = len(self.visible_nodes) - viewport_end
|
|
250
|
+
tree.add(Text(f"↓ ({remaining} more items)", style="dim italic"))
|
|
212
251
|
|
|
213
252
|
return tree
|
|
214
|
-
|
|
253
|
+
|
|
215
254
|
def _show_help(self) -> Panel:
|
|
216
255
|
"""Create help panel"""
|
|
217
256
|
help_text = """[bold]Navigation:[/bold]
|
|
218
|
-
↑/k:
|
|
257
|
+
↑/k: Up ↓/j: Down →/l/Enter: Expand ←/h: Collapse
|
|
219
258
|
|
|
220
259
|
[bold]Selection:[/bold]
|
|
221
260
|
Space: Toggle file/dir a: Add all in dir s: Snippet mode
|
|
222
261
|
|
|
223
262
|
[bold]Actions:[/bold]
|
|
224
|
-
g: Grep in directory
|
|
225
|
-
|
|
226
|
-
[bold]Status:[/bold]
|
|
227
|
-
Selected: [green]● Full[/green] [yellow]◐ Snippet[/yellow] ○ Not selected"""
|
|
263
|
+
r: Reuse last selection g: Grep in directory d: Show dependencies
|
|
264
|
+
q: Quit and finalize"""
|
|
228
265
|
|
|
229
|
-
return Panel(help_text, title="Keyboard
|
|
266
|
+
return Panel(help_text, title="Keyboard Controls", border_style="dim", expand=False)
|
|
230
267
|
|
|
231
268
|
def _get_status_bar(self) -> str:
|
|
232
269
|
"""Create status bar with selection info"""
|
|
@@ -317,7 +354,7 @@ Selected: [green]● Full[/green] [yellow]◐ Snippet[/yellow] ○ Not selecte
|
|
|
317
354
|
self.char_count += node.size
|
|
318
355
|
|
|
319
356
|
def _toggle_directory(self, node: FileNode):
|
|
320
|
-
"""Toggle all files in a directory"""
|
|
357
|
+
"""Toggle all files in a directory, now fully recursive."""
|
|
321
358
|
if not node.is_dir:
|
|
322
359
|
return
|
|
323
360
|
|
|
@@ -330,6 +367,9 @@ Selected: [green]● Full[/green] [yellow]◐ Snippet[/yellow] ○ Not selecte
|
|
|
330
367
|
|
|
331
368
|
def collect_files(n: FileNode):
|
|
332
369
|
if n.is_dir:
|
|
370
|
+
# CRITICAL FIX: Ensure sub-directory children are loaded before recursing
|
|
371
|
+
if not n.children:
|
|
372
|
+
self._scan_directory(n.path, n)
|
|
333
373
|
for child in n.children:
|
|
334
374
|
collect_files(child)
|
|
335
375
|
else:
|
|
@@ -341,16 +381,92 @@ Selected: [green]● Full[/green] [yellow]◐ Snippet[/yellow] ○ Not selecte
|
|
|
341
381
|
any_unselected = any(os.path.abspath(f.path) not in self.selected_files for f in all_files)
|
|
342
382
|
|
|
343
383
|
if any_unselected:
|
|
344
|
-
# Select all unselected
|
|
384
|
+
# Select all unselected files
|
|
345
385
|
for file_node in all_files:
|
|
346
|
-
|
|
347
|
-
|
|
386
|
+
abs_path = os.path.abspath(file_node.path)
|
|
387
|
+
if abs_path not in self.selected_files:
|
|
388
|
+
self.selected_files[abs_path] = (False, None)
|
|
389
|
+
self.char_count += file_node.size
|
|
348
390
|
else:
|
|
349
|
-
# Unselect all
|
|
391
|
+
# Unselect all files
|
|
350
392
|
for file_node in all_files:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
393
|
+
abs_path = os.path.abspath(file_node.path)
|
|
394
|
+
if abs_path in self.selected_files:
|
|
395
|
+
is_snippet, _ = self.selected_files[abs_path]
|
|
396
|
+
del self.selected_files[abs_path]
|
|
397
|
+
if is_snippet:
|
|
398
|
+
self.char_count -= len(get_file_snippet(file_node.path))
|
|
399
|
+
else:
|
|
400
|
+
self.char_count -= file_node.size
|
|
401
|
+
|
|
402
|
+
def _propose_and_apply_last_selection(self):
|
|
403
|
+
"""Loads paths from cache, shows a confirmation dialog, and applies the selection if confirmed."""
|
|
404
|
+
cached_paths = load_selection_from_cache()
|
|
405
|
+
|
|
406
|
+
if not cached_paths:
|
|
407
|
+
self.console.print(Panel("[yellow]No cached selection found to reuse.[/yellow]", title="Info", border_style="dim"))
|
|
408
|
+
click.pause("Press any key to continue...")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# Categorize cached paths for the preview
|
|
412
|
+
files_to_add = []
|
|
413
|
+
files_already_selected = []
|
|
414
|
+
files_not_found = []
|
|
415
|
+
|
|
416
|
+
for rel_path in cached_paths:
|
|
417
|
+
abs_path = os.path.abspath(rel_path)
|
|
418
|
+
if not os.path.isfile(abs_path):
|
|
419
|
+
files_not_found.append(rel_path)
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
if abs_path in self.selected_files:
|
|
423
|
+
files_already_selected.append(rel_path)
|
|
424
|
+
else:
|
|
425
|
+
files_to_add.append(rel_path)
|
|
426
|
+
|
|
427
|
+
# Build the rich text for the confirmation panel
|
|
428
|
+
preview_text = Text()
|
|
429
|
+
if files_to_add:
|
|
430
|
+
preview_text.append("The following files will be ADDED:\n", style="bold")
|
|
431
|
+
for path in sorted(files_to_add):
|
|
432
|
+
preview_text.append(" ")
|
|
433
|
+
preview_text.append("+", style="cyan")
|
|
434
|
+
preview_text.append(f" {path}\n")
|
|
435
|
+
|
|
436
|
+
if files_already_selected:
|
|
437
|
+
preview_text.append("\nAlready selected (no change):\n", style="bold dim")
|
|
438
|
+
for path in sorted(files_already_selected):
|
|
439
|
+
preview_text.append(f" ✓ {path}\n")
|
|
440
|
+
|
|
441
|
+
if files_not_found:
|
|
442
|
+
preview_text.append("\nNot found on disk (will be skipped):\n", style="bold dim")
|
|
443
|
+
for path in sorted(files_not_found):
|
|
444
|
+
preview_text.append(" ")
|
|
445
|
+
preview_text.append("-", style="red")
|
|
446
|
+
preview_text.append(f" {path}\n")
|
|
447
|
+
|
|
448
|
+
# Display the confirmation panel and prompt
|
|
449
|
+
self.console.clear()
|
|
450
|
+
self.console.print(Panel(preview_text, title="[bold cyan]Reuse Last Selection?", border_style="cyan", padding=(1, 2)))
|
|
451
|
+
|
|
452
|
+
if not files_to_add:
|
|
453
|
+
self.console.print("\n[yellow]No new files to add from the last selection.[/yellow]")
|
|
454
|
+
click.pause("Press any key to continue...")
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
# Use click.confirm for a simple and effective y/n prompt
|
|
458
|
+
if not click.confirm(f"\nAdd {len(files_to_add)} file(s) to your current selection?", default=True):
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
# If confirmed, apply the changes
|
|
462
|
+
for rel_path in files_to_add:
|
|
463
|
+
abs_path = os.path.abspath(rel_path)
|
|
464
|
+
if os.path.isfile(abs_path) and abs_path not in self.selected_files:
|
|
465
|
+
file_size = os.path.getsize(abs_path)
|
|
466
|
+
self.selected_files[abs_path] = (False, None)
|
|
467
|
+
self.char_count += file_size
|
|
468
|
+
self._ensure_path_visible(abs_path)
|
|
469
|
+
|
|
354
470
|
def _ensure_path_visible(self, file_path: str):
|
|
355
471
|
"""Ensure a file path is visible in the tree by expanding parent directories"""
|
|
356
472
|
abs_file_path = os.path.abspath(file_path)
|
|
@@ -456,6 +572,20 @@ Selected: [green]● Full[/green] [yellow]◐ Snippet[/yellow] ○ Not selecte
|
|
|
456
572
|
self.current_index = max(0, self.current_index - 1)
|
|
457
573
|
elif key in ['\x1b[B', 'j']: # Down arrow or j
|
|
458
574
|
self.current_index = min(len(self.visible_nodes) - 1, self.current_index + 1)
|
|
575
|
+
elif key == '\x1b[5~': # Page Up
|
|
576
|
+
term_width, term_height = shutil.get_terminal_size()
|
|
577
|
+
page_size = max(1, term_height - 15)
|
|
578
|
+
self.current_index = max(0, self.current_index - page_size)
|
|
579
|
+
elif key == '\x1b[6~': # Page Down
|
|
580
|
+
term_width, term_height = shutil.get_terminal_size()
|
|
581
|
+
page_size = max(1, term_height - 15)
|
|
582
|
+
self.current_index = min(len(self.visible_nodes) - 1, self.current_index + page_size)
|
|
583
|
+
elif key == '\x1b[H': # Home - go to top
|
|
584
|
+
self.current_index = 0
|
|
585
|
+
elif key == '\x1b[F': # End - go to bottom
|
|
586
|
+
self.current_index = len(self.visible_nodes) - 1
|
|
587
|
+
elif key == 'G': # Shift+G - go to bottom (vim style)
|
|
588
|
+
self.current_index = len(self.visible_nodes) - 1
|
|
459
589
|
elif key in ['\x1b[C', 'l', '\r']: # Right arrow, l, or Enter
|
|
460
590
|
if current_node.is_dir:
|
|
461
591
|
current_node.expanded = True
|
|
@@ -480,10 +610,11 @@ Selected: [green]● Full[/green] [yellow]◐ Snippet[/yellow] ○ Not selecte
|
|
|
480
610
|
self._toggle_directory(current_node)
|
|
481
611
|
|
|
482
612
|
# Handle actions
|
|
613
|
+
elif key == 'r': # Reuse last selection
|
|
614
|
+
self._propose_and_apply_last_selection()
|
|
483
615
|
elif key == 'g': # Grep
|
|
484
616
|
self.console.print() # Add some space
|
|
485
617
|
self._handle_grep(current_node)
|
|
486
|
-
click.pause("Press any key to continue...")
|
|
487
618
|
elif key == 'd': # Dependencies
|
|
488
619
|
self.console.print() # Add some space
|
|
489
620
|
self._show_dependencies(current_node)
|
|
@@ -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.
|
|
13
|
+
version="0.31.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|