py-for-ai 0.1.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.
@@ -0,0 +1,3 @@
1
+ .venv/
2
+ .idea/
3
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maksym Khlystun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-for-ai
3
+ Version: 0.1.0
4
+ Summary: A command-line utility to pack Python codebases into a single text file for AI context, with clipboard support.
5
+ Project-URL: Homepage, https://github.com/Maksum867/py-ai
6
+ Author: Maksym Khlystun
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development
13
+ Classifier: Topic :: Utilities
14
+ Requires-Python: >=3.8
15
+ Requires-Dist: pyperclip>=1.8.2
16
+ Description-Content-Type: text/markdown
17
+
18
+ <div align="center">
19
+
20
+ # py-ai πŸš€
21
+
22
+ [![PyPI version](https://img.shields.io/pypi/v/py-ai-pack.svg)](https://pypi.org/project/py-ai-pack/)
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Maksum867/py-ai/blob/main/LICENSE)
24
+ [![Python Support](https://img.shields.io/pypi/pyversions/py-ai-pack.svg)](https://pypi.org/project/py-ai-pack/)
25
+
26
+ `py-ai` is a lightweight, zero-configuration command-line interface (CLI) tool designed for developers. It recursively scans your local Python codebase, filters out unnecessary files, and compiles your entire projectβ€”complete with a beautiful ASCII directory treeβ€”into a single, organized text file while automatically copying it to your clipboard.
27
+
28
+ Perfect for instantly feeding your codebase context into Large Language Models (LLMs) like ChatGPT, Claude, and Gemini.
29
+
30
+ </div>
31
+
32
+ ---
33
+
34
+ ## ✨ Features
35
+
36
+ - **Smart Filtering**: Automatically ignores VCS folders (`.git`), virtual environments (`.venv`, `venv`), IDE settings (`.vscode`, `.idea`), build artifacts (`dist`, `build`), and binary files (images, archives, databases).
37
+ - **ASCII Directory Tree**: Generates a clean, sorted representation of your project layout at the top of the output.
38
+ - **Normalized File Markers**: Wraps each file's code in distinct delimiters (`--- START OF FILE: ... ---`) using normalized cross-platform POSIX paths (`/`).
39
+ - **Clipboard Integration**: Automatically copies the packed content. Gracefully falls back with a warning in headless/SSH environments rather than crashing.
40
+ - **PyPI & Hatchling Ready**: Out-of-the-box support for modern PEP 621 packaging.
41
+
42
+ ---
43
+
44
+ ## πŸ“‚ Project Layout
45
+
46
+ ```text
47
+ py_ai_project/
48
+ β”œβ”€β”€ pyproject.toml
49
+ β”œβ”€β”€ README.md
50
+ └── src/
51
+ └── py_ai/
52
+ β”œβ”€β”€ __init__.py
53
+ β”œβ”€β”€ __main__.py
54
+ β”œβ”€β”€ core.py
55
+ └── cli.py
56
+ ```
57
+
58
+ ---
59
+
60
+ ## πŸš€ Quick Start
61
+
62
+ ### Installation
63
+
64
+ Install the utility directly from PyPI:
65
+
66
+ ```bash
67
+ pip install py-ai
68
+ ```
69
+
70
+ *(Or install it locally for development)*:
71
+
72
+ ```bash
73
+ git clone https://github.com/Maksum867/py-ai.git
74
+ cd py_ai_project
75
+ pip install -e .
76
+ ```
77
+
78
+ ### Usage
79
+
80
+ Run the utility from any terminal session:
81
+
82
+ ```bash
83
+ # 1. Pack current directory and copy to clipboard:
84
+ pyai
85
+
86
+ # 2. Pack a specific directory:
87
+ pyai /path/to/your/project
88
+
89
+ # 3. Save the results to a custom file:
90
+ pyai . -o custom_output.txt
91
+
92
+ # 4. Run without clipboard copy (ideal for remote servers or CI/CD):
93
+ pyai --no-clipboard
94
+ ```
95
+
96
+ ---
97
+
98
+ ## πŸ“„ Output Format Example
99
+
100
+ ```text
101
+ ================================================================================
102
+ PROJECT CONTEXT PACK: my_project
103
+ Generated on: 2026-07-31 12:00:00
104
+ Total files packed: 2
105
+ ================================================================================
106
+
107
+ ================================================================================
108
+ DIRECTORY TREE
109
+ ================================================================================
110
+ my_project/
111
+ β”œβ”€β”€ src/
112
+ β”‚ └── main.py
113
+ └── pyproject.toml
114
+
115
+ ================================================================================
116
+ FILES CONTENT
117
+ ================================================================================
118
+
119
+ --- START OF FILE: pyproject.toml ---
120
+ [project]
121
+ name = "my_project"
122
+ version = "0.1.0"
123
+ --- END OF FILE: pyproject.toml ---
124
+
125
+ --- START OF FILE: src/main.py ---
126
+ def main():
127
+ print("Hello, AI!")
128
+ --- END OF FILE: src/main.py ---
129
+ ```
130
+
131
+ ---
132
+
133
+ ## πŸ› οΈ Requirements & Dependencies
134
+
135
+ - Python `3.8` or higher.
136
+ - `pyperclip` (for clipboard operations).
@@ -0,0 +1,119 @@
1
+ <div align="center">
2
+
3
+ # py-ai πŸš€
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/py-ai-pack.svg)](https://pypi.org/project/py-ai-pack/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Maksum867/py-ai/blob/main/LICENSE)
7
+ [![Python Support](https://img.shields.io/pypi/pyversions/py-ai-pack.svg)](https://pypi.org/project/py-ai-pack/)
8
+
9
+ `py-ai` is a lightweight, zero-configuration command-line interface (CLI) tool designed for developers. It recursively scans your local Python codebase, filters out unnecessary files, and compiles your entire projectβ€”complete with a beautiful ASCII directory treeβ€”into a single, organized text file while automatically copying it to your clipboard.
10
+
11
+ Perfect for instantly feeding your codebase context into Large Language Models (LLMs) like ChatGPT, Claude, and Gemini.
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## ✨ Features
18
+
19
+ - **Smart Filtering**: Automatically ignores VCS folders (`.git`), virtual environments (`.venv`, `venv`), IDE settings (`.vscode`, `.idea`), build artifacts (`dist`, `build`), and binary files (images, archives, databases).
20
+ - **ASCII Directory Tree**: Generates a clean, sorted representation of your project layout at the top of the output.
21
+ - **Normalized File Markers**: Wraps each file's code in distinct delimiters (`--- START OF FILE: ... ---`) using normalized cross-platform POSIX paths (`/`).
22
+ - **Clipboard Integration**: Automatically copies the packed content. Gracefully falls back with a warning in headless/SSH environments rather than crashing.
23
+ - **PyPI & Hatchling Ready**: Out-of-the-box support for modern PEP 621 packaging.
24
+
25
+ ---
26
+
27
+ ## πŸ“‚ Project Layout
28
+
29
+ ```text
30
+ py_ai_project/
31
+ β”œβ”€β”€ pyproject.toml
32
+ β”œβ”€β”€ README.md
33
+ └── src/
34
+ └── py_ai/
35
+ β”œβ”€β”€ __init__.py
36
+ β”œβ”€β”€ __main__.py
37
+ β”œβ”€β”€ core.py
38
+ └── cli.py
39
+ ```
40
+
41
+ ---
42
+
43
+ ## πŸš€ Quick Start
44
+
45
+ ### Installation
46
+
47
+ Install the utility directly from PyPI:
48
+
49
+ ```bash
50
+ pip install py-ai
51
+ ```
52
+
53
+ *(Or install it locally for development)*:
54
+
55
+ ```bash
56
+ git clone https://github.com/Maksum867/py-ai.git
57
+ cd py_ai_project
58
+ pip install -e .
59
+ ```
60
+
61
+ ### Usage
62
+
63
+ Run the utility from any terminal session:
64
+
65
+ ```bash
66
+ # 1. Pack current directory and copy to clipboard:
67
+ pyai
68
+
69
+ # 2. Pack a specific directory:
70
+ pyai /path/to/your/project
71
+
72
+ # 3. Save the results to a custom file:
73
+ pyai . -o custom_output.txt
74
+
75
+ # 4. Run without clipboard copy (ideal for remote servers or CI/CD):
76
+ pyai --no-clipboard
77
+ ```
78
+
79
+ ---
80
+
81
+ ## πŸ“„ Output Format Example
82
+
83
+ ```text
84
+ ================================================================================
85
+ PROJECT CONTEXT PACK: my_project
86
+ Generated on: 2026-07-31 12:00:00
87
+ Total files packed: 2
88
+ ================================================================================
89
+
90
+ ================================================================================
91
+ DIRECTORY TREE
92
+ ================================================================================
93
+ my_project/
94
+ β”œβ”€β”€ src/
95
+ β”‚ └── main.py
96
+ └── pyproject.toml
97
+
98
+ ================================================================================
99
+ FILES CONTENT
100
+ ================================================================================
101
+
102
+ --- START OF FILE: pyproject.toml ---
103
+ [project]
104
+ name = "my_project"
105
+ version = "0.1.0"
106
+ --- END OF FILE: pyproject.toml ---
107
+
108
+ --- START OF FILE: src/main.py ---
109
+ def main():
110
+ print("Hello, AI!")
111
+ --- END OF FILE: src/main.py ---
112
+ ```
113
+
114
+ ---
115
+
116
+ ## πŸ› οΈ Requirements & Dependencies
117
+
118
+ - Python `3.8` or higher.
119
+ - `pyperclip` (for clipboard operations).
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "py-for-ai"
7
+ version = "0.1.0"
8
+ description = "A command-line utility to pack Python codebases into a single text file for AI context, with clipboard support."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Maksym Khlystun"}
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Software Development",
20
+ "Topic :: Utilities",
21
+ ]
22
+ dependencies = [
23
+ "pyperclip>=1.8.2",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Maksum867/py-ai"
28
+
29
+ [project.scripts]
30
+ pyai = "py_ai.cli:main"
31
+ py-ai = "py_ai.cli:main"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/py_ai"]
@@ -0,0 +1,6 @@
1
+ """
2
+ py-ai: A Python CLI utility to package a codebase into a single text file/clipboard for AI context.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "Maksym Khlystun"
@@ -0,0 +1,7 @@
1
+ """
2
+ Entry point for running the utility as a module: `python -m py_ai`
3
+ """
4
+ from py_ai.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,89 @@
1
+ """
2
+ cli.py module
3
+ =============
4
+ Command-line interface (CLI) for the py-ai utility using the built-in argparse.
5
+ Provides user interaction, argument parsing, and triggers the packing logic.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ from pathlib import Path
11
+ from py_ai.core import pack_project
12
+
13
+
14
+ def main():
15
+ """
16
+ Main entry point for the command-line interface. Parses CLI arguments,
17
+ starts the project packaging process, and prints results to the console.
18
+ """
19
+ parser = argparse.ArgumentParser(
20
+ description="py-ai: A command-line utility to pack your project's codebase into a single text file and copy it to the clipboard for LLM context."
21
+ )
22
+
23
+ parser.add_argument(
24
+ "root_dir",
25
+ nargs="?",
26
+ default=".",
27
+ help="Path to the project directory to pack (default: current directory '.')"
28
+ )
29
+
30
+ parser.add_argument(
31
+ "-o", "--output",
32
+ default="ai_context.txt",
33
+ help="Name or path of the output file (default: 'ai_context.txt')"
34
+ )
35
+
36
+ parser.add_argument(
37
+ "--no-clipboard",
38
+ action="store_true",
39
+ help="Disable automatic copying of the generated context to the system clipboard (useful for headless servers or CI/CD pipelines)."
40
+ )
41
+
42
+ args = parser.parse_args()
43
+
44
+ root_path = Path(args.root_dir)
45
+ output_path = Path(args.output)
46
+
47
+ print(f"πŸ” Scanning project directory: {root_path.resolve()}")
48
+
49
+ try:
50
+ # Trigger the core packing logic
51
+ stats = pack_project(
52
+ root_dir=root_path,
53
+ output_file=output_path,
54
+ copy_to_clipboard=not args.no_clipboard
55
+ )
56
+
57
+ print("\n✨ Project successfully packed!")
58
+ print(f"πŸ“ Root Directory: {stats['root_dir']}")
59
+ print(f"πŸ“„ Output File: {stats['output_file']}")
60
+ print(f"πŸ“¦ Packed Files: {stats['packed_count']}")
61
+
62
+ if stats['failed_count'] > 0:
63
+ print(f"⚠️ Skipped Files: {stats['failed_count']} (see warning messages above)")
64
+
65
+ if stats['clipboard_copied']:
66
+ print("πŸ“‹ Project context copied to clipboard successfully!")
67
+ else:
68
+ if args.no_clipboard:
69
+ print("ℹ️ Clipboard copying was disabled by the --no-clipboard flag.")
70
+ else:
71
+ print(
72
+ f"⚠️ Could not copy to clipboard.\n"
73
+ f" Details: {stats['clipboard_error'] or 'Unknown error'}.\n"
74
+ f" The output was still saved successfully to the output file."
75
+ )
76
+
77
+ except FileNotFoundError as e:
78
+ print(f"❌ Error: {e}", file=sys.stderr)
79
+ sys.exit(1)
80
+ except IOError as e:
81
+ print(f"❌ Write Error: {e}", file=sys.stderr)
82
+ sys.exit(1)
83
+ except Exception as e:
84
+ print(f"❌ Unexpected Error: {e}", file=sys.stderr)
85
+ sys.exit(1)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,264 @@
1
+ """
2
+ core.py module
3
+ ==============
4
+ Contains the core business logic for the py-ai utility:
5
+ - File and directory ignore checks (should_ignore).
6
+ - Project directory tree generation in ASCII format.
7
+ - Gathering allowed file contents and writing them to the final context file.
8
+ - Copying the generated output to the system clipboard.
9
+ """
10
+
11
+ import os
12
+ import sys
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+ import pyperclip
16
+
17
+ # Standard lists of exclusions for ignore checks
18
+ IGNORED_NAMES = {
19
+ # Version control and system folders
20
+ '.git', '.github', '.gitlab', '.svn', '.hg', 'node_modules',
21
+ # Python runtime and caches
22
+ '__pycache__', '.venv', 'venv', 'env', '.env', '.pytest_cache',
23
+ '.mypy_cache', '.ruff_cache', '.tox', '.nox', '.egg-info', 'build', 'dist',
24
+ # IDE and editor settings
25
+ '.idea', '.vscode', '.settings',
26
+ # OS system files
27
+ '.DS_Store', 'Thumbs.db', 'desktop.ini'
28
+ }
29
+
30
+ IGNORED_EXTENSIONS = {
31
+ # Compiled Python and binary artifacts
32
+ '.pyc', '.pyo', '.pyd', '.class', '.o', '.obj', '.dll', '.so', '.dylib',
33
+ # Archives/Compressed files
34
+ '.zip', '.tar', '.gz', '.bz2', '.xz', '.rar', '.7z', '.tgz',
35
+ # Images and multimedia
36
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.tiff', '.svg',
37
+ '.mp3', '.wav', '.ogg', '.flac', '.mp4', '.mkv', '.avi', '.mov', '.webm',
38
+ # Fonts
39
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
40
+ # Databases and binary documents
41
+ '.db', '.sqlite', '.sqlite3', '.pdf', '.epub',
42
+ # Office documents
43
+ '.docx', '.xlsx', '.pptx', '.doc', '.xls', '.ppt'
44
+ }
45
+
46
+
47
+ def should_ignore(path: Path, root_dir: Path) -> bool:
48
+ """
49
+ Checks if a file or directory should be ignored based on standard exclusions.
50
+
51
+ :param path: Path to the file or folder to verify.
52
+ :param root_dir: Root directory of the project.
53
+ :return: True if the path should be ignored, False otherwise.
54
+ """
55
+ try:
56
+ # Resolve to absolute paths to avoid issues with relative path operations
57
+ abs_root = root_dir.resolve()
58
+ abs_path = path.resolve()
59
+ # Calculate path relative to the root directory
60
+ rel_path = abs_path.relative_to(abs_root)
61
+ except ValueError:
62
+ # Fallback if the path is not under the root directory
63
+ rel_path = path
64
+
65
+ # Check each part of the relative path.
66
+ # If any parent folder is in the ignored list, ignore the entire subtree.
67
+ for part in rel_path.parts:
68
+ if part in IGNORED_NAMES or part.lower() in IGNORED_NAMES:
69
+ return True
70
+
71
+ # Ignore any hidden folders/files (starting with a dot),
72
+ # except allowed configuration files such as .gitignore or .env.example
73
+ if part.startswith('.') and part not in ('.gitignore', '.env.example', '.env.template', '.pylintrc'):
74
+ return True
75
+
76
+ # Additional file-level checks for extensions and file names
77
+ if path.is_file():
78
+ if path.suffix.lower() in IGNORED_EXTENSIONS:
79
+ return True
80
+ if path.name.lower() in IGNORED_NAMES:
81
+ return True
82
+
83
+ return False
84
+
85
+
86
+ def build_tree_lines(path: Path, root_dir: Path, prefix: str = "", is_last: bool = True, is_root: bool = False) -> list[str]:
87
+ """
88
+ Recursively generates the ASCII project tree, skipping ignored files and folders.
89
+
90
+ :param path: Current path (file or folder).
91
+ :param root_dir: Root directory of the project (for should_ignore checks).
92
+ :param prefix: Prefix for the current line (indentation and connection symbols).
93
+ :param is_last: Whether the current item is the last one in its parent directory.
94
+ :param is_root: Whether the current item is the root folder of the project.
95
+ :return: List of strings forming the ASCII tree.
96
+ """
97
+ lines = []
98
+
99
+ # Root directory displays with its name and a trailing slash
100
+ if is_root:
101
+ lines.append(f"{path.name}/")
102
+ else:
103
+ connector = "└── " if is_last else "β”œβ”€β”€ "
104
+ display_name = path.name + ("/" if path.is_dir() else "")
105
+ lines.append(f"{prefix}{connector}{display_name}")
106
+
107
+ if path.is_dir():
108
+ try:
109
+ # List directory items, sort them: directories first, then files (case-insensitive)
110
+ # Filter out any ignored items immediately
111
+ items = sorted(
112
+ [item for item in path.iterdir() if not should_ignore(item, root_dir)],
113
+ key=lambda x: (not x.is_dir(), x.name.lower())
114
+ )
115
+ except OSError as e:
116
+ # Handle permission errors or system issues gracefully in the tree
117
+ connector = "└── " if is_last else "β”œβ”€β”€ "
118
+ lines.append(f"{prefix}{connector}[Permission denied/Error accessing directory: {e}]")
119
+ return lines
120
+
121
+ for i, item in enumerate(items):
122
+ item_is_last = (i == len(items) - 1)
123
+ # Calculate next indentation prefix for child elements
124
+ if is_root:
125
+ next_prefix = ""
126
+ else:
127
+ next_prefix = prefix + (" " if is_last else "β”‚ ")
128
+
129
+ lines.extend(build_tree_lines(item, root_dir, next_prefix, item_is_last, is_root=False))
130
+
131
+ return lines
132
+
133
+
134
+ def pack_project(root_dir: str | Path, output_file: str | Path, copy_to_clipboard: bool = True) -> dict:
135
+ """
136
+ Recursively scans the project, builds an ASCII tree, gathers all text file contents,
137
+ saves the aggregated output to a single file, and copies it to the system clipboard.
138
+
139
+ :param root_dir: Path to the root folder of the project.
140
+ :param output_file: Path to the target output text file.
141
+ :param copy_to_clipboard: Whether to copy the generated context to the clipboard.
142
+ :return: Dictionary containing execution statistics.
143
+ """
144
+ root_path = Path(root_dir).resolve()
145
+ output_path = Path(output_file).resolve()
146
+
147
+ if not root_path.exists() or not root_path.is_dir():
148
+ raise FileNotFoundError(f"The specified directory '{root_path}' does not exist or is not a directory.")
149
+
150
+ # 1. Generate the ASCII directory tree
151
+ tree_lines = build_tree_lines(root_path, root_path, is_root=True)
152
+ tree_text = "\n".join(tree_lines)
153
+
154
+ # 2. Collect all allowed files for code packing
155
+ files_to_pack = []
156
+
157
+ def recursive_walk(current_dir: Path):
158
+ try:
159
+ # Sort items for a deterministic walking order
160
+ items = sorted(current_dir.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
161
+ except PermissionError as e:
162
+ print(f"Warning: Permission denied reading directory {current_dir}: {e}", file=sys.stderr)
163
+ return
164
+
165
+ for item in items:
166
+ if should_ignore(item, root_path):
167
+ continue
168
+ if item.is_dir():
169
+ recursive_walk(item)
170
+ elif item.is_file():
171
+ # Avoid packing the output file itself if it is located inside the root project directory
172
+ if item.resolve() == output_path:
173
+ continue
174
+ files_to_pack.append(item)
175
+
176
+ recursive_walk(root_path)
177
+
178
+ # 3. Read file contents and format blocks
179
+ content_blocks = []
180
+ packed_count = 0
181
+ failed_count = 0
182
+
183
+ for file_path in files_to_pack:
184
+ try:
185
+ rel_path = file_path.relative_to(root_path).as_posix()
186
+ except ValueError:
187
+ rel_path = file_path.as_posix() if hasattr(file_path, "as_posix") else str(file_path)
188
+
189
+ try:
190
+ # Read files using UTF-8 encoding
191
+ with open(file_path, "r", encoding="utf-8") as f:
192
+ content = f.read()
193
+
194
+ # Format the code block with clean delimiters
195
+ block = f"--- START OF FILE: {rel_path} ---\n{content}\n--- END OF FILE: {rel_path} ---"
196
+ content_blocks.append(block)
197
+ packed_count += 1
198
+ except UnicodeDecodeError:
199
+ # Handle binary files that cannot be decoded as standard text
200
+ print(f"Warning: Skipped '{rel_path}' due to encoding error (likely a binary file).", file=sys.stderr)
201
+ failed_count += 1
202
+ except PermissionError:
203
+ print(f"Warning: Skipped '{rel_path}' due to permission error.", file=sys.stderr)
204
+ failed_count += 1
205
+ except Exception as e:
206
+ print(f"Warning: Skipped '{rel_path}' due to an error: {e}", file=sys.stderr)
207
+ failed_count += 1
208
+
209
+ # 4. Formulate the single aggregated output
210
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
211
+
212
+ header = (
213
+ "================================================================================\n"
214
+ f"PROJECT CONTEXT PACK: {root_path.name}\n"
215
+ f"Generated on: {timestamp}\n"
216
+ f"Total files packed: {packed_count}\n"
217
+ "================================================================================\n"
218
+ )
219
+
220
+ tree_section = (
221
+ "================================================================================\n"
222
+ "DIRECTORY TREE\n"
223
+ "================================================================================\n"
224
+ f"{tree_text}\n"
225
+ )
226
+
227
+ files_section_header = (
228
+ "================================================================================\n"
229
+ "FILES CONTENT\n"
230
+ "================================================================================\n"
231
+ )
232
+
233
+ files_content = "\n\n".join(content_blocks)
234
+ full_output = f"{header}\n{tree_section}\n{files_section_header}\n{files_content}\n"
235
+
236
+ # 5. Write to the output file
237
+ try:
238
+ output_path.parent.mkdir(parents=True, exist_ok=True)
239
+ with open(output_path, "w", encoding="utf-8") as f:
240
+ f.write(full_output)
241
+ except Exception as e:
242
+ raise IOError(f"Failed to write results to '{output_path}': {e}")
243
+
244
+ # 6. Copy to the system clipboard if enabled
245
+ clipboard_copied = False
246
+ clipboard_error = None
247
+
248
+ if copy_to_clipboard:
249
+ try:
250
+ pyperclip.copy(full_output)
251
+ clipboard_copied = True
252
+ except Exception as e:
253
+ clipboard_error = str(e)
254
+ print(f"Warning: Could not copy to clipboard: {e}", file=sys.stderr)
255
+
256
+ return {
257
+ "root_dir": str(root_path),
258
+ "output_file": str(output_path),
259
+ "packed_count": packed_count,
260
+ "failed_count": failed_count,
261
+ "clipboard_copied": clipboard_copied,
262
+ "clipboard_error": clipboard_error,
263
+ "file_size": len(full_output)
264
+ }