filemerger-cli 0.3.1__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,23 @@
1
+ This project is licensed under the MIT License.
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2026 binaryfleet
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: filemerger-cli
3
+ Version: 0.3.1
4
+ Summary: Developer CLI tool to consolidate project files into a single output
5
+ Author: BinaryFleet
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/binaryfleet/filemerger
8
+ Project-URL: Repository, https://github.com/binaryfleet/filemerger
9
+ Project-URL: Issues, https://github.com/binaryfleet/filemerger/issues
10
+ Keywords: cli,developer-tools,file-merger,llm,code-review
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pathspec>=0.11
15
+ Dynamic: license-file
16
+
17
+ # FileMerger
18
+
19
+ **FileMerger** is a developer-focused CLI tool that consolidates project files into a
20
+ single plain-text output.
21
+
22
+ It is designed to help developers:
23
+ - Share complete code context with AI tools (ChatGPT, Gemini, Grok, Claude, etc.)
24
+ - Review large codebases
25
+ - Create audit or snapshot files
26
+ - Prepare structured input for analysis
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install filemerger-cli
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Basic Usage
39
+
40
+ Merge a directory:
41
+
42
+ ```bash
43
+ filemerger src/
44
+ ```
45
+
46
+ Specify output file:
47
+
48
+ ```bash
49
+ filemerger src/ --output context.txt
50
+ ```
51
+
52
+ Dry run (no file written):
53
+
54
+ ```bash
55
+ filemerger . --dry-run
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Output Modes
61
+
62
+ FileMerger supports multiple output modes depending on **who (or what)** will consume the output.
63
+
64
+ ### 1. Default Mode (Human-Readable)
65
+
66
+ ```bash
67
+ filemerger src/
68
+ ```
69
+
70
+ **Use this when:**
71
+
72
+ * You want to read the output yourself
73
+ * You are reviewing or auditing code
74
+ * You want clear visual separation
75
+
76
+ **Characteristics:**
77
+
78
+ * File lists and headers
79
+ * Visual separators
80
+ * Structured, readable layout
81
+
82
+ ---
83
+
84
+ ### 2. LLM Mode (`--llm`)
85
+
86
+ ```bash
87
+ filemerger src/ --llm
88
+ ```
89
+
90
+ **Use this when:**
91
+
92
+ * The output will be pasted into an AI system
93
+ * You want deterministic file references
94
+ * You want to reduce semantic noise
95
+
96
+ **Characteristics:**
97
+
98
+ * Files are numbered (`[1]`, `[2]`, …)
99
+ * No decorative separators
100
+ * Simple, predictable structure
101
+
102
+ Example:
103
+
104
+ ```
105
+ [1] path/to/file.py
106
+ <content>
107
+
108
+ [2] another/file.js
109
+ <content>
110
+ ```
111
+
112
+ ---
113
+
114
+ ### 3. LLM Compact Mode (`--llm-compact`)
115
+
116
+ ```bash
117
+ filemerger src/ --llm-compact
118
+ ```
119
+
120
+ **Use this when:**
121
+
122
+ * Token limits are tight
123
+ * The project is very large
124
+ * Maximum efficiency matters
125
+
126
+ **Characteristics:**
127
+
128
+ * Same structure as `--llm`
129
+ * Fewer blank lines
130
+ * Minimal formatting overhead
131
+
132
+ ---
133
+
134
+ ### 4. Statistics
135
+
136
+ Use `--stats` to print merge statistics:
137
+
138
+ ```bash
139
+ filemerger src/ --stats
140
+ ```
141
+
142
+ Reported values:
143
+
144
+ * Number of files
145
+ * Total lines
146
+ * Total bytes
147
+ * Skipped files (binary / non-UTF8)
148
+
149
+ ---
150
+
151
+ ### 5. AI Marker Mode (`--ai-markers`)
152
+
153
+ ```bash
154
+ filemerger src/ --ai-markers
155
+ ````
156
+
157
+ **Use this when:**
158
+
159
+ * You need strong, explicit file boundaries for AI systems
160
+ * You want deterministic multi-file reasoning
161
+ * You are feeding large structured context into LLMs
162
+ * You need machine-parsable output
163
+
164
+ **Characteristics:**
165
+
166
+ * Explicit file boundary markers
167
+ * Clear begin/end delimiters
168
+ * Unambiguous separation between files
169
+ * Designed for reliable AI ingestion
170
+
171
+ Example:
172
+
173
+ ```
174
+ <<<FILE 1: path/to/file.py>>>
175
+ <content>
176
+ <<<END FILE>>>
177
+
178
+ <<<FILE 2: another/file.js>>>
179
+ <content>
180
+ <<<END FILE>>>
181
+ ```
182
+
183
+ ---
184
+
185
+
186
+ ## Configuration (Optional)
187
+
188
+ FileMerger supports an optional `.filemerger.toml` file in the project root.
189
+
190
+ Example:
191
+
192
+ ```toml
193
+ [filters]
194
+ max_file_size_mb = 1
195
+ exclude_dirs = ["tests"]
196
+
197
+ [output]
198
+ separator_length = 60
199
+ ```
200
+
201
+ If the file is not present, default behavior is used.
202
+
203
+ ---
204
+
205
+ ## License
206
+
207
+ This project is licensed under the MIT License.
208
+ See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,192 @@
1
+ # FileMerger
2
+
3
+ **FileMerger** is a developer-focused CLI tool that consolidates project files into a
4
+ single plain-text output.
5
+
6
+ It is designed to help developers:
7
+ - Share complete code context with AI tools (ChatGPT, Gemini, Grok, Claude, etc.)
8
+ - Review large codebases
9
+ - Create audit or snapshot files
10
+ - Prepare structured input for analysis
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install filemerger-cli
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Basic Usage
23
+
24
+ Merge a directory:
25
+
26
+ ```bash
27
+ filemerger src/
28
+ ```
29
+
30
+ Specify output file:
31
+
32
+ ```bash
33
+ filemerger src/ --output context.txt
34
+ ```
35
+
36
+ Dry run (no file written):
37
+
38
+ ```bash
39
+ filemerger . --dry-run
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Output Modes
45
+
46
+ FileMerger supports multiple output modes depending on **who (or what)** will consume the output.
47
+
48
+ ### 1. Default Mode (Human-Readable)
49
+
50
+ ```bash
51
+ filemerger src/
52
+ ```
53
+
54
+ **Use this when:**
55
+
56
+ * You want to read the output yourself
57
+ * You are reviewing or auditing code
58
+ * You want clear visual separation
59
+
60
+ **Characteristics:**
61
+
62
+ * File lists and headers
63
+ * Visual separators
64
+ * Structured, readable layout
65
+
66
+ ---
67
+
68
+ ### 2. LLM Mode (`--llm`)
69
+
70
+ ```bash
71
+ filemerger src/ --llm
72
+ ```
73
+
74
+ **Use this when:**
75
+
76
+ * The output will be pasted into an AI system
77
+ * You want deterministic file references
78
+ * You want to reduce semantic noise
79
+
80
+ **Characteristics:**
81
+
82
+ * Files are numbered (`[1]`, `[2]`, …)
83
+ * No decorative separators
84
+ * Simple, predictable structure
85
+
86
+ Example:
87
+
88
+ ```
89
+ [1] path/to/file.py
90
+ <content>
91
+
92
+ [2] another/file.js
93
+ <content>
94
+ ```
95
+
96
+ ---
97
+
98
+ ### 3. LLM Compact Mode (`--llm-compact`)
99
+
100
+ ```bash
101
+ filemerger src/ --llm-compact
102
+ ```
103
+
104
+ **Use this when:**
105
+
106
+ * Token limits are tight
107
+ * The project is very large
108
+ * Maximum efficiency matters
109
+
110
+ **Characteristics:**
111
+
112
+ * Same structure as `--llm`
113
+ * Fewer blank lines
114
+ * Minimal formatting overhead
115
+
116
+ ---
117
+
118
+ ### 4. Statistics
119
+
120
+ Use `--stats` to print merge statistics:
121
+
122
+ ```bash
123
+ filemerger src/ --stats
124
+ ```
125
+
126
+ Reported values:
127
+
128
+ * Number of files
129
+ * Total lines
130
+ * Total bytes
131
+ * Skipped files (binary / non-UTF8)
132
+
133
+ ---
134
+
135
+ ### 5. AI Marker Mode (`--ai-markers`)
136
+
137
+ ```bash
138
+ filemerger src/ --ai-markers
139
+ ````
140
+
141
+ **Use this when:**
142
+
143
+ * You need strong, explicit file boundaries for AI systems
144
+ * You want deterministic multi-file reasoning
145
+ * You are feeding large structured context into LLMs
146
+ * You need machine-parsable output
147
+
148
+ **Characteristics:**
149
+
150
+ * Explicit file boundary markers
151
+ * Clear begin/end delimiters
152
+ * Unambiguous separation between files
153
+ * Designed for reliable AI ingestion
154
+
155
+ Example:
156
+
157
+ ```
158
+ <<<FILE 1: path/to/file.py>>>
159
+ <content>
160
+ <<<END FILE>>>
161
+
162
+ <<<FILE 2: another/file.js>>>
163
+ <content>
164
+ <<<END FILE>>>
165
+ ```
166
+
167
+ ---
168
+
169
+
170
+ ## Configuration (Optional)
171
+
172
+ FileMerger supports an optional `.filemerger.toml` file in the project root.
173
+
174
+ Example:
175
+
176
+ ```toml
177
+ [filters]
178
+ max_file_size_mb = 1
179
+ exclude_dirs = ["tests"]
180
+
181
+ [output]
182
+ separator_length = 60
183
+ ```
184
+
185
+ If the file is not present, default behavior is used.
186
+
187
+ ---
188
+
189
+ ## License
190
+
191
+ This project is licensed under the MIT License.
192
+ See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,4 @@
1
+ __all__ = [
2
+ "collect_files",
3
+ "merge_files",
4
+ ]
@@ -0,0 +1,91 @@
1
+ import argparse
2
+ import sys
3
+ import os
4
+
5
+ from .core import collect_files, merge_files
6
+ from .utils import normalize_output_filename
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(
11
+ description="Consolidate project files into a single text output"
12
+ )
13
+ parser.add_argument(
14
+ "paths",
15
+ nargs="+",
16
+ help="Files or directories to include"
17
+ )
18
+ parser.add_argument(
19
+ "-o", "--output",
20
+ help="Output file name (always saved as .txt)"
21
+ )
22
+ parser.add_argument(
23
+ "--dry-run",
24
+ action="store_true",
25
+ help="Show files that would be included without writing output"
26
+ )
27
+ parser.add_argument(
28
+ "--stats",
29
+ action="store_true",
30
+ help="Print merge statistics"
31
+ )
32
+ parser.add_argument(
33
+ "--llm",
34
+ action="store_true",
35
+ help="Optimize output for LLM context ingestion"
36
+ )
37
+ parser.add_argument(
38
+ "--llm-compact",
39
+ action="store_true",
40
+ help="More compact LLM output with fewer blank lines"
41
+ )
42
+ parser.add_argument(
43
+ "--ai-markers",
44
+ action="store_true",
45
+ help="Use explicit AI-friendly file boundary markers"
46
+ )
47
+
48
+ args = parser.parse_args()
49
+
50
+ if args.llm_compact:
51
+ args.llm = True
52
+
53
+ output_file = normalize_output_filename(args.output)
54
+ output_file = os.path.join(os.getcwd(), output_file)
55
+
56
+ files = collect_files(args.paths, output_file=output_file)
57
+
58
+ if not files:
59
+ print("No valid files found.")
60
+ sys.exit(2)
61
+
62
+ if args.dry_run:
63
+ print("Files to be included:")
64
+ for f in files:
65
+ print(f" - {f}")
66
+
67
+ if args.stats:
68
+ print("\nStats:")
69
+ print(f" Files: {len(files)}")
70
+ sys.exit(0)
71
+
72
+ stats = merge_files(
73
+ files,
74
+ output_file,
75
+ llm_mode=args.llm,
76
+ llm_compact=args.llm_compact,
77
+ ai_markers=args.ai_markers,
78
+ )
79
+
80
+ print(f"✔ Merged {stats.files} files into {output_file}")
81
+
82
+ if args.stats:
83
+ print("\nStats:")
84
+ print(f" Files: {stats.files}")
85
+ print(f" Lines: {stats.lines}")
86
+ print(f" Bytes: {stats.bytes}")
87
+ print(f" Skipped files: {stats.skipped_files}")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,28 @@
1
+ ALLOWED_EXTENSIONS = {
2
+ ".py", ".js", ".json", ".html", ".css", ".txt", ".md"
3
+ }
4
+
5
+ EXCLUDED_FILES = {
6
+ ".DS_Store",
7
+ }
8
+
9
+ EXCLUDED_DIRECTORIES = {
10
+ "__pycache__",
11
+ ".git",
12
+ ".idea",
13
+ ".vscode",
14
+ "node_modules",
15
+ "migrations",
16
+ "tests",
17
+ ".pytest_cache",
18
+ ".mypy_cache",
19
+ ".ruff_cache",
20
+ "env",
21
+ ".env",
22
+ ".venv",
23
+ }
24
+
25
+ DEFAULT_SEPARATOR_LENGTH = 90
26
+ MAX_FILE_SIZE_MB = 2
27
+
28
+ DEFAULT_OUTPUT_FILE = "filemerger-output.txt"
@@ -0,0 +1,75 @@
1
+ import os
2
+ from typing import List, Set
3
+ from .filters import is_allowed_file
4
+ from .gitignore import load_gitignore
5
+ from .user_config import load_user_config
6
+ from .config import EXCLUDED_DIRECTORIES, MAX_FILE_SIZE_MB
7
+ from .format_default import DefaultFormatter
8
+ from .format_llm import LLMFormatter
9
+ from .format_ai import AIMarkerFormatter
10
+ from .stats import MergeStats
11
+
12
+ def collect_files(
13
+ paths: List[str],
14
+ *,
15
+ output_file: str | None = None,
16
+ ) -> List[str]:
17
+ collected: Set[str] = set()
18
+ root = os.getcwd()
19
+
20
+ gitignore_spec = load_gitignore(root)
21
+ user_config = load_user_config()
22
+
23
+ max_mb = user_config.get("filters", {}).get("max_file_size_mb", MAX_FILE_SIZE_MB)
24
+ excluded_dirs = set(EXCLUDED_DIRECTORIES)
25
+ excluded_dirs.update(
26
+ user_config.get("filters", {}).get("exclude_dirs", [])
27
+ )
28
+
29
+ max_file_size_bytes = int(max_mb * 1024 * 1024)
30
+
31
+ for path in paths:
32
+ if os.path.isfile(path) and is_allowed_file(
33
+ path,
34
+ output_file=output_file,
35
+ gitignore_spec=gitignore_spec,
36
+ root=root,
37
+ max_file_size_bytes=max_file_size_bytes,
38
+ excluded_dirs=excluded_dirs,
39
+ ):
40
+ collected.add(os.path.abspath(path))
41
+
42
+ elif os.path.isdir(path):
43
+ for current_root, _, files in os.walk(path):
44
+ for name in sorted(files):
45
+ full_path = os.path.join(current_root, name)
46
+ if is_allowed_file(
47
+ full_path,
48
+ output_file=output_file,
49
+ gitignore_spec=gitignore_spec,
50
+ root=root,
51
+ max_file_size_bytes=max_file_size_bytes,
52
+ excluded_dirs=excluded_dirs,
53
+ ):
54
+ collected.add(os.path.abspath(full_path))
55
+
56
+ return sorted(collected)
57
+
58
+
59
+ def merge_files(
60
+ files: List[str],
61
+ output_file: str,
62
+ *,
63
+ llm_mode: bool = False,
64
+ llm_compact: bool = False,
65
+ ai_markers: bool = False,
66
+ ) -> MergeStats:
67
+
68
+ if ai_markers:
69
+ formatter = AIMarkerFormatter()
70
+ elif llm_mode or llm_compact:
71
+ formatter = LLMFormatter(compact=llm_compact)
72
+ else:
73
+ formatter = DefaultFormatter()
74
+
75
+ return formatter.write(files, output_file)
@@ -0,0 +1,37 @@
1
+ import os
2
+ from .gitignore import is_ignored
3
+
4
+ def is_allowed_file(
5
+ path: str,
6
+ *,
7
+ output_file: str | None = None,
8
+ gitignore_spec=None,
9
+ root: str | None = None,
10
+ max_file_size_bytes: int,
11
+ excluded_dirs: set[str],
12
+ ) -> bool:
13
+ if not os.path.isfile(path):
14
+ return False
15
+
16
+ if output_file and os.path.abspath(path) == os.path.abspath(output_file):
17
+ return False
18
+
19
+ if gitignore_spec and root and is_ignored(path, root=root, spec=gitignore_spec):
20
+ return False
21
+
22
+ if os.path.splitext(path)[1].lower() not in {
23
+ ".py", ".js", ".json", ".html", ".css", ".txt", ".md"
24
+ }:
25
+ return False
26
+
27
+ parts = path.split(os.sep)
28
+ if any(part in excluded_dirs for part in parts):
29
+ return False
30
+
31
+ try:
32
+ if os.path.getsize(path) > max_file_size_bytes:
33
+ return False
34
+ except OSError:
35
+ return False
36
+
37
+ return True
@@ -0,0 +1,44 @@
1
+ from typing import List
2
+ from .stats import MergeStats
3
+
4
+ class AIMarkerFormatter:
5
+ """
6
+ Explicit AI-friendly formatter using strong file boundary markers.
7
+
8
+ Format:
9
+
10
+ <<<FILE 1: path/to/file.py>>>
11
+ <content>
12
+ <<<END FILE>>>
13
+
14
+ """
15
+
16
+ def write(self, files: List[str], output_file: str) -> MergeStats:
17
+ stats = MergeStats(files=len(files))
18
+
19
+ with open(output_file, "w", encoding="utf-8") as out:
20
+ for idx, file_path in enumerate(files, start=1):
21
+ header = f"<<<FILE {idx}: {file_path}>>>\n"
22
+ out.write(header)
23
+ stats.lines += 1
24
+ stats.bytes += len(header.encode("utf-8"))
25
+
26
+ try:
27
+ with open(file_path, "r", encoding="utf-8") as f:
28
+ content = f.read().rstrip() + "\n"
29
+ out.write(content)
30
+ stats.lines += content.count("\n")
31
+ stats.bytes += len(content.encode("utf-8"))
32
+ except UnicodeDecodeError:
33
+ skipped = "[Skipped: binary or non-UTF8 file]\n"
34
+ out.write(skipped)
35
+ stats.lines += 1
36
+ stats.bytes += len(skipped.encode("utf-8"))
37
+ stats.skipped_files += 1
38
+
39
+ footer = "<<<END FILE>>>\n\n"
40
+ out.write(footer)
41
+ stats.lines += 2
42
+ stats.bytes += len(footer.encode("utf-8"))
43
+
44
+ return stats
@@ -0,0 +1,53 @@
1
+ from typing import List
2
+ from .stats import MergeStats
3
+ from .user_config import load_user_config
4
+ from .config import DEFAULT_SEPARATOR_LENGTH
5
+
6
+ class DefaultFormatter:
7
+ def write(self, files: List[str], output_file: str) -> MergeStats:
8
+ user_config = load_user_config()
9
+ sep_len = user_config.get("output", {}).get(
10
+ "separator_length", DEFAULT_SEPARATOR_LENGTH
11
+ )
12
+ separator = "-" * int(sep_len)
13
+
14
+ stats = MergeStats(files=len(files))
15
+
16
+ with open(output_file, "w", encoding="utf-8") as out:
17
+ header = "FILES INCLUDED\n" + separator + "\n"
18
+ out.write(header)
19
+ stats.lines += header.count("\n")
20
+ stats.bytes += len(header.encode("utf-8"))
21
+
22
+ for f in files:
23
+ line = f"{f}\n"
24
+ out.write(line)
25
+ stats.lines += 1
26
+ stats.bytes += len(line.encode("utf-8"))
27
+
28
+ out.write("\n")
29
+ stats.lines += 1
30
+ stats.bytes += 1
31
+
32
+ for file_path in files:
33
+ block_header = (
34
+ f"{separator}\nFILE: {file_path}\n{separator}\n"
35
+ )
36
+ out.write(block_header)
37
+ stats.lines += block_header.count("\n")
38
+ stats.bytes += len(block_header.encode("utf-8"))
39
+
40
+ try:
41
+ with open(file_path, "r", encoding="utf-8") as f:
42
+ content = f.read().rstrip() + "\n"
43
+ out.write(content)
44
+ stats.lines += content.count("\n")
45
+ stats.bytes += len(content.encode("utf-8"))
46
+ except UnicodeDecodeError:
47
+ skipped = "[Skipped: binary or non-UTF8 file]\n"
48
+ out.write(skipped)
49
+ stats.lines += 1
50
+ stats.bytes += len(skipped.encode("utf-8"))
51
+ stats.skipped_files += 1
52
+
53
+ return stats
@@ -0,0 +1,36 @@
1
+ from typing import List
2
+ from .stats import MergeStats
3
+
4
+ class LLMFormatter:
5
+ def __init__(self, *, compact: bool = False):
6
+ self.compact = compact
7
+
8
+ def write(self, files: List[str], output_file: str) -> MergeStats:
9
+ stats = MergeStats(files=len(files))
10
+
11
+ with open(output_file, "w", encoding="utf-8") as out:
12
+ for idx, file_path in enumerate(files, start=1):
13
+ header = f"[{idx}] {file_path}\n"
14
+ out.write(header)
15
+ stats.lines += 1
16
+ stats.bytes += len(header.encode("utf-8"))
17
+
18
+ try:
19
+ with open(file_path, "r", encoding="utf-8") as f:
20
+ content = f.read().rstrip() + ("\n" if self.compact else "\n\n")
21
+ out.write(content)
22
+ stats.lines += content.count("\n")
23
+ stats.bytes += len(content.encode("utf-8"))
24
+ except UnicodeDecodeError:
25
+ skipped = "[Skipped: binary or non-UTF8 file]\n"
26
+ out.write(skipped)
27
+ stats.lines += 1
28
+ stats.bytes += len(skipped.encode("utf-8"))
29
+ stats.skipped_files += 1
30
+
31
+ if not self.compact:
32
+ out.write("\n")
33
+ stats.lines += 1
34
+ stats.bytes += 1
35
+
36
+ return stats
@@ -0,0 +1,6 @@
1
+ from typing import List
2
+ from .stats import MergeStats
3
+
4
+ class BaseFormatter:
5
+ def write(self, files: List[str], output_file: str) -> MergeStats:
6
+ raise NotImplementedError
@@ -0,0 +1,29 @@
1
+ import os
2
+ import pathspec
3
+ from typing import Optional
4
+
5
+ def load_gitignore(root: str) -> Optional[pathspec.PathSpec]:
6
+ """
7
+ Load .gitignore from the given root directory.
8
+ Returns None if no .gitignore exists.
9
+ """
10
+ gitignore_path = os.path.join(root, ".gitignore")
11
+ if not os.path.isfile(gitignore_path):
12
+ return None
13
+
14
+ with open(gitignore_path, "r", encoding="utf-8") as f:
15
+ lines = f.read().splitlines()
16
+
17
+ return pathspec.PathSpec.from_lines("gitwildmatch", lines)
18
+
19
+ def is_ignored(
20
+ path: str,
21
+ *,
22
+ root: str,
23
+ spec: Optional[pathspec.PathSpec],
24
+ ) -> bool:
25
+ if not spec:
26
+ return False
27
+
28
+ rel_path = os.path.relpath(path, root)
29
+ return spec.match_file(rel_path)
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class MergeStats:
5
+ files: int = 0
6
+ lines: int = 0
7
+ bytes: int = 0
8
+ skipped_files: int = 0
@@ -0,0 +1,15 @@
1
+ import os
2
+ import tomllib
3
+ from typing import Dict, Any
4
+
5
+ def load_user_config() -> Dict[str, Any]:
6
+ """
7
+ Load .filemerger.toml from the current working directory.
8
+ Returns empty dict if not found.
9
+ """
10
+ config_path = os.path.join(os.getcwd(), ".filemerger.toml")
11
+ if not os.path.isfile(config_path):
12
+ return {}
13
+
14
+ with open(config_path, "rb") as f:
15
+ return tomllib.load(f)
@@ -0,0 +1,20 @@
1
+ import os
2
+ from .config import DEFAULT_OUTPUT_FILE
3
+
4
+ def normalize_output_filename(output: str | None) -> str:
5
+ """
6
+ Normalize output filename:
7
+ - Default to DEFAULT_OUTPUT_FILE
8
+ - Force .txt extension
9
+ - Strip directory components (always write to CWD)
10
+ """
11
+ if not output:
12
+ return DEFAULT_OUTPUT_FILE
13
+
14
+ base = os.path.basename(output)
15
+ name, ext = os.path.splitext(base)
16
+
17
+ if ext.lower() != ".txt":
18
+ return f"{name}.txt"
19
+
20
+ return base
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: filemerger-cli
3
+ Version: 0.3.1
4
+ Summary: Developer CLI tool to consolidate project files into a single output
5
+ Author: BinaryFleet
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/binaryfleet/filemerger
8
+ Project-URL: Repository, https://github.com/binaryfleet/filemerger
9
+ Project-URL: Issues, https://github.com/binaryfleet/filemerger/issues
10
+ Keywords: cli,developer-tools,file-merger,llm,code-review
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pathspec>=0.11
15
+ Dynamic: license-file
16
+
17
+ # FileMerger
18
+
19
+ **FileMerger** is a developer-focused CLI tool that consolidates project files into a
20
+ single plain-text output.
21
+
22
+ It is designed to help developers:
23
+ - Share complete code context with AI tools (ChatGPT, Gemini, Grok, Claude, etc.)
24
+ - Review large codebases
25
+ - Create audit or snapshot files
26
+ - Prepare structured input for analysis
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install filemerger-cli
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Basic Usage
39
+
40
+ Merge a directory:
41
+
42
+ ```bash
43
+ filemerger src/
44
+ ```
45
+
46
+ Specify output file:
47
+
48
+ ```bash
49
+ filemerger src/ --output context.txt
50
+ ```
51
+
52
+ Dry run (no file written):
53
+
54
+ ```bash
55
+ filemerger . --dry-run
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Output Modes
61
+
62
+ FileMerger supports multiple output modes depending on **who (or what)** will consume the output.
63
+
64
+ ### 1. Default Mode (Human-Readable)
65
+
66
+ ```bash
67
+ filemerger src/
68
+ ```
69
+
70
+ **Use this when:**
71
+
72
+ * You want to read the output yourself
73
+ * You are reviewing or auditing code
74
+ * You want clear visual separation
75
+
76
+ **Characteristics:**
77
+
78
+ * File lists and headers
79
+ * Visual separators
80
+ * Structured, readable layout
81
+
82
+ ---
83
+
84
+ ### 2. LLM Mode (`--llm`)
85
+
86
+ ```bash
87
+ filemerger src/ --llm
88
+ ```
89
+
90
+ **Use this when:**
91
+
92
+ * The output will be pasted into an AI system
93
+ * You want deterministic file references
94
+ * You want to reduce semantic noise
95
+
96
+ **Characteristics:**
97
+
98
+ * Files are numbered (`[1]`, `[2]`, …)
99
+ * No decorative separators
100
+ * Simple, predictable structure
101
+
102
+ Example:
103
+
104
+ ```
105
+ [1] path/to/file.py
106
+ <content>
107
+
108
+ [2] another/file.js
109
+ <content>
110
+ ```
111
+
112
+ ---
113
+
114
+ ### 3. LLM Compact Mode (`--llm-compact`)
115
+
116
+ ```bash
117
+ filemerger src/ --llm-compact
118
+ ```
119
+
120
+ **Use this when:**
121
+
122
+ * Token limits are tight
123
+ * The project is very large
124
+ * Maximum efficiency matters
125
+
126
+ **Characteristics:**
127
+
128
+ * Same structure as `--llm`
129
+ * Fewer blank lines
130
+ * Minimal formatting overhead
131
+
132
+ ---
133
+
134
+ ### 4. Statistics
135
+
136
+ Use `--stats` to print merge statistics:
137
+
138
+ ```bash
139
+ filemerger src/ --stats
140
+ ```
141
+
142
+ Reported values:
143
+
144
+ * Number of files
145
+ * Total lines
146
+ * Total bytes
147
+ * Skipped files (binary / non-UTF8)
148
+
149
+ ---
150
+
151
+ ### 5. AI Marker Mode (`--ai-markers`)
152
+
153
+ ```bash
154
+ filemerger src/ --ai-markers
155
+ ````
156
+
157
+ **Use this when:**
158
+
159
+ * You need strong, explicit file boundaries for AI systems
160
+ * You want deterministic multi-file reasoning
161
+ * You are feeding large structured context into LLMs
162
+ * You need machine-parsable output
163
+
164
+ **Characteristics:**
165
+
166
+ * Explicit file boundary markers
167
+ * Clear begin/end delimiters
168
+ * Unambiguous separation between files
169
+ * Designed for reliable AI ingestion
170
+
171
+ Example:
172
+
173
+ ```
174
+ <<<FILE 1: path/to/file.py>>>
175
+ <content>
176
+ <<<END FILE>>>
177
+
178
+ <<<FILE 2: another/file.js>>>
179
+ <content>
180
+ <<<END FILE>>>
181
+ ```
182
+
183
+ ---
184
+
185
+
186
+ ## Configuration (Optional)
187
+
188
+ FileMerger supports an optional `.filemerger.toml` file in the project root.
189
+
190
+ Example:
191
+
192
+ ```toml
193
+ [filters]
194
+ max_file_size_mb = 1
195
+ exclude_dirs = ["tests"]
196
+
197
+ [output]
198
+ separator_length = 60
199
+ ```
200
+
201
+ If the file is not present, default behavior is used.
202
+
203
+ ---
204
+
205
+ ## License
206
+
207
+ This project is licensed under the MIT License.
208
+ See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ filemerger/__init__.py
5
+ filemerger/cli.py
6
+ filemerger/config.py
7
+ filemerger/core.py
8
+ filemerger/filters.py
9
+ filemerger/format_ai.py
10
+ filemerger/format_default.py
11
+ filemerger/format_llm.py
12
+ filemerger/formatter.py
13
+ filemerger/gitignore.py
14
+ filemerger/stats.py
15
+ filemerger/user_config.py
16
+ filemerger/utils.py
17
+ filemerger_cli.egg-info/PKG-INFO
18
+ filemerger_cli.egg-info/SOURCES.txt
19
+ filemerger_cli.egg-info/dependency_links.txt
20
+ filemerger_cli.egg-info/entry_points.txt
21
+ filemerger_cli.egg-info/requires.txt
22
+ filemerger_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ filemerger = filemerger.cli:main
@@ -0,0 +1 @@
1
+ pathspec>=0.11
@@ -0,0 +1 @@
1
+ filemerger
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "filemerger-cli"
3
+ version = "0.3.1"
4
+ description = "Developer CLI tool to consolidate project files into a single output"
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ license = { text = "MIT" }
8
+
9
+ authors = [
10
+ { name = "BinaryFleet" }
11
+ ]
12
+
13
+ keywords = ["cli", "developer-tools", "file-merger", "llm", "code-review"]
14
+
15
+ dependencies = [
16
+ "pathspec>=0.11"
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/binaryfleet/filemerger"
21
+ Repository = "https://github.com/binaryfleet/filemerger"
22
+ Issues = "https://github.com/binaryfleet/filemerger/issues"
23
+
24
+ [project.scripts]
25
+ filemerger = "filemerger.cli:main"
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=61.0"]
29
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+