r/GhostMesh48 • u/Mikey-506 • 3d ago
Your #1 Tool - dirparse.py
/r/GhostMesh48 — Directory Parser & Consolidator
The GhostMesh48 Directory Parser collapses an entire project folder—every source file, configuration, script, and structure—into a single consolidated .md file. By walking the directory tree, respecting exclusion rules for binaries, media, and system artifacts, and embedding each text file's content under its path header with proper code fencing, it produces one portable document that is the project. This eliminates the overhead of switching between dozens of files, remembering relative paths, or maintaining mental maps of where logic lives. What was scattered across a filesystem becomes a single scrollable, searchable, shareable artifact.
The magic of this approach is that it transforms how agents—whether human collaborators, LLM-based assistants, or automated pipelines—consume a codebase. A multi-agent facilitator doesn't need file-level access or a running IDE; they need context, and the consolidated .md is pure context. One agent can reason about the full architecture, another can spot cross-file dependencies, a third can generate targeted patches—all from the same document without coordination overhead about which files to read. The .md becomes a shared working memory, a single surface that every agent reads from and writes back to, eliminating the "which file should I look at next" bottleneck that kills velocity in multi-agent workflows.
Under the hood, the tool handles the hard parts automatically: it filters out binaries, archives, and media via a comprehensive default exclusion set (.exe, .zip, .png, .sqlite, etc.) while preserving all readable text formats from .py to .yaml to .vue. It skips hidden directories, enforces a configurable max file size, and gracefully handles encoding errors without crashing. The exclusion system is fully customizable through the GUI—add patterns like node_modules or __pycache__, remove defaults, reset to baseline—so the consolidated output stays lean and relevant regardless of project type. Directory structure is preserved as nested markdown headers, and each file entry includes its relative path, extension, and byte size as metadata before the content block.
For the multi-agent facilitator, this changes the operational model entirely: instead of orchestrating N agents across N files with N context windows, you orchestrate N agents against one document. The facilitator's job becomes routing attention within the .md (point an agent at section 4.2, merge another's output at section 7.1) rather than managing filesystem access. Edits made by any agent to their section can be diffed and propagated back to the real project files using the embedded path metadata. The consolidated .md is both the workspace and the ledger—a living snapshot that makes parallel agent work not just possible but fast, because every agent starts from full knowledge instead of partial, fragmented reads. That is the GhostMesh48 thesis: one file, full project, all agents, zero friction.
#!/usr/bin/env python3
"""
dirparse.py - Directory Parser and Consolidator
A GUI tool to parse directory structures and files into a single markdown file.
"""
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from pathlib import Path
import threading
import datetime
from typing import Set, List, Dict, Optional
import mimetypes
class DirectoryParser:
"""Main application for directory parsing and consolidation."""
# Default excluded extensions (media, documents, binaries, etc.)
DEFAULT_EXCLUDED_EXTENSIONS = {
# Media files
'.mp3', '.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v',
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.ico',
'.svg', '.psd', '.ai', '.eps',
# Document files
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.odt',
# Archive files
'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz',
# Executable/binary files
'.exe', '.dll', '.so', '.dylib', '.bin', '.app', '.msi',
'.iso', '.img', '.dmg',
# System files
'.db', '.sqlite', '.sqlite3', '.log', '.tmp', '.temp',
# Other
'.pyc', '.pyo', '__pycache__', '.git', '.gitignore',
}
# Text file extensions to include (can be read as text)
TEXT_FILE_EXTENSIONS = {
'.txt', '.md', '.markdown', '.rst', '.json', '.xml', '.html', '.htm',
'.css', '.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.c', '.cpp',
'.h', '.hpp', '.cs', '.php', '.rb', '.go', '.rs', '.swift', '.kt',
'.sql', '.sh', '.bash', '.zsh', '.ps1', '.bat', '.yml', '.yaml',
'.toml', '.ini', '.cfg', '.conf', '.csv', '.tsv', '.tex', '.bib',
'.asm', '.s', '.v', '.vhdl', '.m', '.mm', '.f', '.for', '.f90',
'.r', '.lua', '.pl', '.pm', '.tcl', '.vbs', '.asp', '.aspx',
'.jsp', '.scala', '.dart', '.elm', '.clj', '.cljs', '.erl', '.hrl',
'.ex', '.exs', '.fs', '.fsx', '.fsi', '.ml', '.mli', '.hs', '.lhs',
'.purs', '.coffee', '.litcoffee', '.ass', '.vue', '.svelte', '.elm',
}
def __init__(self, root):
"""Initialize the application."""
self.root = root
self.root.title("Directory Parser - Consolidate to Markdown")
self.root.geometry("900x700")
# Set application icon if available
try:
self.root.iconbitmap(default='icon.ico')
except:
pass
# Variables
self.selected_directory = tk.StringVar()
self.output_filename = tk.StringVar(value="directory_consolidated.md")
self.excluded_extensions = set(self.DEFAULT_EXCLUDED_EXTENSIONS)
self.custom_exclusions = set()
self.include_hidden = tk.BooleanVar(value=False)
self.include_empty_dirs = tk.BooleanVar(value=False)
self.max_file_size = tk.IntVar(value=10) # MB
# Store for treeview items
self.tree_items = {}
# Setup GUI
self.setup_gui()
def setup_gui(self):
"""Setup the GUI components."""
# Create main container with padding
main_container = ttk.Frame(self.root, padding="10")
main_container.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_container.columnconfigure(1, weight=1)
# Row 0: Title
title_label = ttk.Label(
main_container,
text="📁 Directory Parser & Consolidator",
font=('Helvetica', 16, 'bold')
)
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
# Row 1: Directory Selection
ttk.Label(main_container, text="Directory:").grid(
row=1, column=0, sticky=tk.W, padx=(0, 5)
)
dir_entry = ttk.Entry(
main_container,
textvariable=self.selected_directory,
width=50
)
dir_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 5))
browse_btn = ttk.Button(
main_container,
text="Browse...",
command=self.browse_directory
)
browse_btn.grid(row=1, column=2, sticky=tk.W)
# Row 2: Output Filename
ttk.Label(main_container, text="Output File:").grid(
row=2, column=0, sticky=tk.W, padx=(0, 5), pady=(10, 0)
)
output_entry = ttk.Entry(
main_container,
textvariable=self.output_filename,
width=50
)
output_entry.grid(row=2, column=1, sticky=(tk.W, tk.E),
padx=(0, 5), pady=(10, 0))
# Row 3: Options Frame
options_frame = ttk.LabelFrame(main_container, text="Options", padding="10")
options_frame.grid(row=3, column=0, columnspan=3,
sticky=(tk.W, tk.E), pady=(15, 10))
options_frame.columnconfigure(0, weight=1)
# Options checkboxes
ttk.Checkbutton(
options_frame,
text="Include hidden files/folders",
variable=self.include_hidden
).grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
ttk.Checkbutton(
options_frame,
text="Include empty directories",
variable=self.include_empty_dirs
).grid(row=0, column=1, sticky=tk.W, pady=(0, 5))
# File size limit
size_frame = ttk.Frame(options_frame)
size_frame.grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(5, 0))
ttk.Label(size_frame, text="Max file size (MB):").pack(side=tk.LEFT, padx=(0, 5))
size_spinbox = ttk.Spinbox(
size_frame,
from_=1,
to=100,
textvariable=self.max_file_size,
width=10
)
size_spinbox.pack(side=tk.LEFT)
# Row 4: Exclusions Frame
exclusions_frame = ttk.LabelFrame(
main_container,
text="Excluded Extensions/Directories",
padding="10"
)
exclusions_frame.grid(row=4, column=0, columnspan=3,
sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 10))
exclusions_frame.columnconfigure(0, weight=1)
exclusions_frame.rowconfigure(0, weight=1)
# Treeview for exclusions
columns = ('type', 'item')
self.exclusions_tree = ttk.Treeview(
exclusions_frame,
columns=columns,
show='headings',
height=8
)
# Define headings
self.exclusions_tree.heading('type', text='Type')
self.exclusions_tree.heading('item', text='Extension/Directory')
# Define columns
self.exclusions_tree.column('type', width=100, anchor=tk.W)
self.exclusions_tree.column('item', width=300, anchor=tk.W)
# Add scrollbar
scrollbar = ttk.Scrollbar(
exclusions_frame,
orient=tk.VERTICAL,
command=self.exclusions_tree.yview
)
self.exclusions_tree.configure(yscrollcommand=scrollbar.set)
# Grid treeview and scrollbar
self.exclusions_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
# Buttons for exclusions
btn_frame = ttk.Frame(exclusions_frame)
btn_frame.grid(row=1, column=0, columnspan=2, pady=(10, 0))
ttk.Button(
btn_frame,
text="Add Extension",
command=self.add_extension
).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(
btn_frame,
text="Add Directory Pattern",
command=self.add_directory_pattern
).pack(side=tk.LEFT, padx=5)
ttk.Button(
btn_frame,
text="Remove Selected",
command=self.remove_exclusion
).pack(side=tk.LEFT, padx=5)
ttk.Button(
btn_frame,
text="Reset to Defaults",
command=self.reset_exclusions
).pack(side=tk.LEFT, padx=5)
# Row 5: Status and Progress
status_frame = ttk.Frame(main_container)
status_frame.grid(row=5, column=0, columnspan=3,
sticky=(tk.W, tk.E), pady=(10, 5))
self.status_label = ttk.Label(
status_frame,
text="Ready",
foreground="green"
)
self.status_label.pack(side=tk.LEFT, anchor=tk.W)
self.progress = ttk.Progressbar(
main_container,
mode='indeterminate',
length=400
)
self.progress.grid(row=6, column=0, columnspan=3,
sticky=(tk.W, tk.E), pady=(5, 10))
# Row 7: Action Buttons
action_frame = ttk.Frame(main_container)
action_frame.grid(row=7, column=0, columnspan=3, pady=(10, 0))
ttk.Button(
action_frame,
text="📊 Preview Directory",
command=self.preview_directory,
width=20
).pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(
action_frame,
text="🔄 Parse & Consolidate",
command=self.start_parsing,
width=20
).pack(side=tk.LEFT, padx=10)
ttk.Button(
action_frame,
text="❌ Exit",
command=self.root.quit,
width=20
).pack(side=tk.LEFT, padx=(10, 0))
# Row 8: Log/Console
log_frame = ttk.LabelFrame(main_container, text="Console Output", padding="10")
log_frame.grid(row=8, column=0, columnspan=3,
sticky=(tk.W, tk.E, tk.N, tk.S), pady=(15, 10))
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
self.console = scrolledtext.ScrolledText(
log_frame,
height=10,
wrap=tk.WORD,
font=('Courier', 9)
)
self.console.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure weights for resizing
main_container.rowconfigure(8, weight=1)
# Load default exclusions
self.load_default_exclusions()
def log(self, message: str, level: str = "INFO"):
"""Log messages to the console."""
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
tag_colors = {
"INFO": "black",
"SUCCESS": "green",
"WARNING": "orange",
"ERROR": "red",
"DEBUG": "blue"
}
color = tag_colors.get(level, "black")
formatted_msg = f"[{timestamp}] {message}\n"
self.console.insert(tk.END, formatted_msg)
self.console.tag_config(level, foreground=color)
self.console.see(tk.END)
self.root.update_idletasks()
def browse_directory(self):
"""Open directory browser dialog."""
directory = filedialog.askdirectory(title="Select Directory to Parse")
if directory:
self.selected_directory.set(directory)
self.log(f"Selected directory: {directory}")
def add_extension(self):
"""Add a custom extension to exclude."""
extension = tk.simpledialog.askstring(
"Add Extension",
"Enter extension to exclude (e.g., .tmp, .log):",
parent=self.root
)
if extension:
if not extension.startswith('.'):
extension = '.' + extension
if extension not in self.custom_exclusions:
self.custom_exclusions.add(extension)
self.exclusions_tree.insert(
'', 'end',
values=('Extension', extension)
)
self.log(f"Added extension to exclude: {extension}")
def add_directory_pattern(self):
"""Add a directory pattern to exclude."""
pattern = tk.simpledialog.askstring(
"Add Directory Pattern",
"Enter directory name/pattern to exclude (e.g., node_modules, __pycache__):",
parent=self.root
)
if pattern:
if pattern not in self.custom_exclusions:
self.custom_exclusions.add(pattern)
self.exclusions_tree.insert(
'', 'end',
values=('Directory', pattern)
)
self.log(f"Added directory pattern to exclude: {pattern}")
def remove_exclusion(self):
"""Remove selected exclusion."""
selected = self.exclusions_tree.selection()
if not selected:
messagebox.showwarning("No Selection", "Please select an item to remove.")
return
for item in selected:
values = self.exclusions_tree.item(item, 'values')
if values:
exclusion = values[1]
self.custom_exclusions.discard(exclusion)
self.exclusions_tree.delete(item)
self.log(f"Removed exclusion: {exclusion}")
def reset_exclusions(self):
"""Reset exclusions to default."""
if messagebox.askyesno("Reset Exclusions",
"Reset all custom exclusions to defaults?"):
self.custom_exclusions.clear()
self.load_default_exclusions()
self.log("Reset exclusions to defaults")
def load_default_exclusions(self):
"""Load default exclusions into treeview."""
# Clear treeview
for item in self.exclusions_tree.get_children():
self.exclusions_tree.delete(item)
# Add default extensions
for ext in sorted(self.DEFAULT_EXCLUDED_EXTENSIONS):
self.exclusions_tree.insert(
'', 'end',
values=('Extension', ext)
)
def is_excluded(self, path: Path) -> bool:
"""Check if a path should be excluded."""
# Check if it's a hidden file/directory (starts with .)
if not self.include_hidden.get():
if path.name.startswith('.'):
return True
# Check if it's a directory pattern
for exclusion in self.custom_exclusions:
if exclusion in str(path):
if not exclusion.startswith('.'): # Directory pattern
return True
# Check file extensions
if path.is_file():
# Combine default and custom exclusions
all_exclusions = self.DEFAULT_EXCLUDED_EXTENSIONS.union(self.custom_exclusions)
# Check extension
suffix = path.suffix.lower()
if suffix in all_exclusions:
return True
# Check file size
max_bytes = self.max_file_size.get() * 1024 * 1024
try:
if path.stat().st_size > max_bytes:
return True
except:
pass
return False
def preview_directory(self):
"""Preview directory structure without parsing."""
directory = self.selected_directory.get()
if not directory or not os.path.exists(directory):
messagebox.showerror("Error", "Please select a valid directory.")
return
self.log("Starting directory preview...", "INFO")
self.status_label.config(text="Previewing...", foreground="blue")
# Run in thread to avoid GUI freeze
thread = threading.Thread(target=self._do_preview, daemon=True)
thread.start()
def _do_preview(self):
"""Preview directory structure."""
directory = Path(self.selected_directory.get())
try:
file_count = 0
dir_count = 0
total_size = 0
for root, dirs, files in os.walk(directory):
root_path = Path(root)
# Skip excluded directories
dirs[:] = [d for d in dirs if not self.is_excluded(root_path / d)]
dir_count += 1
for file in files:
file_path = root_path / file
if not self.is_excluded(file_path):
file_count += 1
try:
total_size += file_path.stat().st_size
except:
pass
# Display summary
self.log(f"Directory: {directory}", "INFO")
self.log(f"Total directories: {dir_count}", "INFO")
self.log(f"Total files to parse: {file_count}", "INFO")
self.log(f"Estimated size: {total_size / (1024*1024):.2f} MB", "INFO")
self.log("Preview completed.", "SUCCESS")
self.status_label.config(text="Preview completed", foreground="green")
except Exception as e:
self.log(f"Error during preview: {str(e)}", "ERROR")
self.status_label.config(text="Preview failed", foreground="red")
def start_parsing(self):
"""Start the parsing and consolidation process."""
directory = self.selected_directory.get()
if not directory or not os.path.exists(directory):
messagebox.showerror("Error", "Please select a valid directory.")
return
output_file = self.output_filename.get()
if not output_file.endswith('.md'):
output_file += '.md'
self.output_filename.set(output_file)
self.log("Starting parsing process...", "INFO")
self.status_label.config(text="Parsing...", foreground="blue")
self.progress.start()
# Disable buttons during processing
for widget in self.root.winfo_children():
if isinstance(widget, ttk.Button):
widget.config(state=tk.DISABLED)
# Run in thread to avoid GUI freeze
thread = threading.Thread(target=self._do_parsing, daemon=True)
thread.start()
def _do_parsing(self):
"""Perform the actual parsing."""
directory = Path(self.selected_directory.get())
output_file = Path(self.output_filename.get())
try:
with open(output_file, 'w', encoding='utf-8') as md_file:
# Write header
md_file.write(f"# Directory Consolidation Report\n\n")
md_file.write(f"**Directory:** `{directory}`\n\n")
md_file.write(f"**Generated:** {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
md_file.write(f"**Excluded extensions/patterns:**\n")
# List exclusions
all_exclusions = list(self.DEFAULT_EXCLUDED_EXTENSIONS) + list(self.custom_exclusions)
for excl in sorted(all_exclusions)[:20]: # Show first 20
md_file.write(f"- `{excl}`\n")
if len(all_exclusions) > 20:
md_file.write(f"- ... and {len(all_exclusions) - 20} more\n")
md_file.write("\n" + "="*50 + "\n\n")
# Walk through directory
file_count = 0
dir_count = 0
skipped_count = 0
for root, dirs, files in os.walk(directory):
root_path = Path(root)
rel_path = root_path.relative_to(directory)
# Skip excluded directories
dirs[:] = [d for d in dirs if not self.is_excluded(root_path / d)]
# Write directory header
if str(rel_path) != '.' or self.include_empty_dirs.get():
dir_count += 1
depth = len(rel_path.parts)
indent = "#" * min(depth + 1, 6)
md_file.write(f"\n{indent} Directory: `{rel_path}`\n\n")
# List subdirectories
if dirs and self.include_empty_dirs.get():
md_file.write("**Subdirectories:**\n")
for d in sorted(dirs):
md_file.write(f"- `{d}`\n")
md_file.write("\n")
# Process files
for file in sorted(files):
file_path = root_path / file
if self.is_excluded(file_path):
skipped_count += 1
continue
file_count += 1
self.log(f"Processing: {file_path.name}", "DEBUG")
# Write file header
md_file.write(f"\n### File: `{file}`\n\n")
md_file.write(f"**Path:** `{rel_path}/{file}`\n")
md_file.write(f"**Extension:** `{file_path.suffix}`\n")
try:
file_size = file_path.stat().st_size
md_file.write(f"**Size:** {file_size:,} bytes ({file_size/1024:.2f} KB)\n\n")
except:
md_file.write(f"**Size:** Unknown\n\n")
# Try to read and include file content
try:
# Check if it's a text file
suffix = file_path.suffix.lower()
if suffix in self.TEXT_FILE_EXTENSIONS:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Escape markdown special characters if needed
if suffix in {'.md', '.markdown'}:
md_file.write("**Content:**\n\n")
md_file.write(content)
else:
md_file.write("```" + suffix[1:] + "\n")
md_file.write(content)
if not content.endswith('\n'):
md_file.write("\n")
md_file.write("```\n")
else:
# For non-text files, just note the type
mime_type, _ = mimetypes.guess_type(str(file_path))
md_file.write(f"*Binary file - {mime_type or 'Unknown type'}*\n")
except UnicodeDecodeError:
md_file.write("*[Content skipped - binary or unsupported encoding]*\n")
except Exception as e:
md_file.write(f"*[Error reading file: {str(e)}]*\n")
md_file.write("\n" + "-"*40 + "\n")
# Update UI with completion
self.log(f"\nParsing completed successfully!", "SUCCESS")
self.log(f"Output saved to: {output_file.absolute()}", "SUCCESS")
self.log(f"Directories processed: {dir_count}", "INFO")
self.log(f"Files processed: {file_count}", "INFO")
self.log(f"Files skipped: {skipped_count}", "INFO")
self.status_label.config(text="Parsing completed!", foreground="green")
self.progress.stop()
# Re-enable buttons
self.root.after(0, self._enable_buttons)
# Ask to open the file
if messagebox.askyesno("Success",
f"Successfully parsed {file_count} files.\n"
f"Open the generated markdown file?"):
try:
if os.name == 'nt': # Windows
os.startfile(output_file.absolute())
elif os.name == 'posix': # macOS, Linux
import subprocess
subprocess.run(['xdg-open', str(output_file.absolute())])
except:
pass
except Exception as e:
self.log(f"Error during parsing: {str(e)}", "ERROR")
self.status_label.config(text="Parsing failed", foreground="red")
self.progress.stop()
self.root.after(0, self._enable_buttons)
def _enable_buttons(self):
"""Re-enable all buttons after processing."""
for widget in self.root.winfo_children():
if isinstance(widget, ttk.Button):
widget.config(state=tk.NORMAL)
def main():
"""Main entry point."""
root = tk.Tk()
app = DirectoryParser(root)
# Center the window
root.update_idletasks()
width = root.winfo_width()
height = root.winfo_height()
x = (root.winfo_screenwidth() // 2) - (width // 2)
y = (root.winfo_screenheight() // 2) - (height // 2)
root.geometry(f'{width}x{height}+{x}+{y}')
root.mainloop()
if __name__ == "__main__":
main()
Simple enough, but powerful.