# --- Configuration - Paths provided by user ---$ScriptInstallDir = "D:\scripts"$ShimDir = Join-Path$ScriptInstallDir"shims"$ScriptName = "llmtxt.ps1"$ShimName = "llmtxt.bat"$ScriptPath = Join-Path$ScriptInstallDir$ScriptName$ShimPath = Join-Path$ShimDir$ShimName# --- Create Directories ---try{if(-not(Test-Path$ScriptInstallDir)){Write-Host"Creating script directory: $ScriptInstallDir"New-Item-ItemType Directory -Path $ScriptInstallDir-Force -ErrorAction Stop |Out-Null}else{Write-Host"Script directory already exists: $ScriptInstallDir"-ForegroundColor Cyan
}if(-not(Test-Path$ShimDir)){Write-Host"Creating shim directory: $ShimDir"New-Item-ItemType Directory -Path $ShimDir-Force -ErrorAction Stop |Out-Null}else{Write-Host"Shim directory already exists: $ShimDir"-ForegroundColor Cyan
}}catch{Write-Error"Failed to create directories. Check permissions for 'D:\scripts'. Error: $($_.Exception.Message)"exit 1
}# --- PowerShell Script Content (llmtxt.ps1) ---# Final Fix Attempt: Uses Where-Object to filter *.pyc files after Get-ChildItem.$PowerShellScriptContent = @'
<#
.SYNOPSIS
Generates a directory tree structure and concatenates the content of all text files
within a specified directory (recursively), then copies the combined text to the clipboard. Excludes *.pyc files reliably using Where-Object.
.DESCRIPTION
This script first builds a text representation of the directory tree under the target path.
Then, it gets all files recursively and filters out *.pyc files using Where-Object.
It then reads the content of the remaining files.
Finally, it combines the tree structure and all file contents into a single string
and copies it to the Windows clipboard using Set-Clipboard.
.PARAMETER Path
The directory path to process. If not provided, the current working directory is used.
.EXAMPLE
.\llmtxt.ps1 C:\MyProject
Processes the C:\MyProject directory.
.EXAMPLE
.\llmtxt.ps1
Processes the current directory.
.NOTES
Author: AI Assistant
Requires PowerShell 5.1 or later.
Handles basic permission errors.
File content is read using UTF-8 encoding.
Excludes *.pyc files by filtering with Where-Object { $_.Extension -ne '.pyc' }.
Large amounts of text might cause performance issues or clipboard limitations.
#>
param(
[Parameter(Mandatory=$false, Position=0)]
[ValidateScript({
if (-not (Test-Path $_)) { throw "Path does not exist: $_" }
if (-not (Test-Path $_ -PathType Container)) { throw "Path is not a directory: $_" }
return $true
})]
[string]$Path = $PWD.Path # Default to current directory
)
# --- Configuration ---
$BranchChars = @{
Continue = '├── '
End = '└── '
Vertical = '│ '
Blank = ''
}
$TreeHeader = "====== DIRECTORY TREE ======"
$ContentHeader = "====== FILE CONTENTS ======"
$FileSeparatorTemplate = "--- File: {0} ---"
$FileEndSeparatorTemplate = "--- End File: {0} ---`n"
# Define the extension to exclude (used in Where-Object filter)
$ExcludeExtensionString = '.pyc' # Note: This is the literal extension string now
# --- Script Body ---
try {
$targetPath = Resolve-Path -LiteralPath $Path -ErrorAction Stop
} catch {
Write-Error "Error resolving path '$Path': $($_.Exception.Message)"
exit 1
}
$basePathLength = $targetPath.Path.Length
$outputBuilder = [System.Text.StringBuilder]::new()
# --- Function to Build Directory Tree Recursively ---
function Get-DirectoryTreeString {
param(
[Parameter(Mandatory=$true)]
[System.IO.DirectoryInfo]$Directory,
[Parameter(Mandatory=$true)][AllowEmptyString()][string]$Prefix,
[Parameter(Mandatory=$true)][AllowEmptyString()][string]$BaseDirName
)
if ($Prefix -eq '' -and $BaseDirName -ne '') {
$outputBuilder.AppendLine($BaseDirName) | Out-Null
}
$items = @()
try {
$items = Get-ChildItem -LiteralPath $Directory.FullName -Force -ErrorAction SilentlyContinue | Sort-Object @{Expression={$_.PSIsContainer}; Descending=$true}, Name
} catch [System.UnauthorizedAccessException] {
$outputBuilder.AppendLine("${Prefix}$($BranchChars.Continue)[Access Denied]") | Out-Null; return
} catch {
$outputBuilder.AppendLine("${Prefix}$($BranchChars.Continue)[Error Listing: $($_.Exception.Message)]") | Out-Null; return
}
$count = $items.Count
for ($i = 0; $i -lt $count; $i++) {
$item = $items[$i]
$isLast = ($i -eq $count - 1)
if ($isLast) {
$connector = $BranchChars.End
$newPrefix = $Prefix + $BranchChars.Blank
} else {
$connector = $BranchChars.Continue
$newPrefix = $Prefix + $BranchChars.Vertical
}
$outputBuilder.AppendLine("$Prefix$connector$($item.Name)") | Out-Null
if ($item.PSIsContainer) {
Get-DirectoryTreeString -Directory $item -Prefix $newPrefix -BaseDirName ""
}
}
}
# --- Function to Get File Contents ---
function Get-FileContentsString {
param(
[Parameter(Mandatory=$true)]
[string]$RootPath
)
$global:outputBuilder
$global:ExcludeExtensionString # Ensure we can read the global variable
$files = @()
try {
# --- FIX IS HERE: Using Where-Object for reliable filtering ---
$files = Get-ChildItem -LiteralPath $RootPath -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.Extension -ne $ExcludeExtensionString } | Sort-Object FullName
# Note: Removed -Exclude parameter from Get-ChildItem
} catch [System.UnauthorizedAccessException] {
$outputBuilder.AppendLine("`n[Access Denied while searching for files in '$RootPath']") | Out-Null; return
} catch {
$outputBuilder.AppendLine("`n[Error searching for files in '$RootPath': $($_.Exception.Message)]") | Out-Null; return
}
if ($files.Count -gt 0) {
$outputBuilder.AppendLine("`n$ContentHeader") | Out-Null
# This loop should now ONLY process files that passed the Where-Object filter
foreach ($file in $files) {
$normRootPath = $RootPath
if (-not ($normRootPath.EndsWith('\') -or $normRootPath.EndsWith('/'))) { $normRootPath += '\' }
$relativePath = if ($normRootPath.Length -ge $file.FullName.Length) { $file.Name } else { $file.FullName.Substring($normRootPath.Length) }
# Print header ONLY for files that will be read
$outputBuilder.AppendLine("`n$($FileSeparatorTemplate -f $relativePath)") | Out-Null
try {
$content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 -ErrorAction Stop
$outputBuilder.AppendLine($content) | Out-Null
} catch [System.UnauthorizedAccessException] {
$outputBuilder.AppendLine("[Access Denied reading file]") | Out-Null
} catch [System.IO.IOException] {
$outputBuilder.AppendLine("[IO Error reading file: $($_.Exception.Message)]") | Out-Null
} catch {
$outputBuilder.AppendLine("[Error reading file (maybe not UTF-8?): $($_.Exception.Message)]") | Out-Null
} finally {
$outputBuilder.AppendLine($FileEndSeparatorTemplate -f $relativePath) | Out-Null
}
}
} else {
$outputBuilder.AppendLine("`nNo suitable files found (excluding extension '$ExcludeExtensionString') in '$RootPath' or its subdirectories.") | Out-Null
}
}
# --- Main Execution ---
Write-Host "Processing directory: $($targetPath.Path)" -ForegroundColor Cyan
Write-Host "Building output for clipboard (excluding extension '$ExcludeExtensionString')..." -ForegroundColor Cyan
# 1. Build Directory Tree
$outputBuilder.AppendLine($TreeHeader) | Out-Null
$rootDirInfo = Get-Item -LiteralPath $targetPath.Path
Get-DirectoryTreeString -Directory $rootDirInfo -Prefix "" -BaseDirName $rootDirInfo.Name
# 2. Build File Contents (will now reliably skip excluded files)
Get-FileContentsString -RootPath $targetPath.Path
# 3. Copy to Clipboard
try {
$finalOutput = $outputBuilder.ToString().Trim()
if ($finalOutput.Length -gt 0) {
Set-Clipboard -Value $finalOutput -ErrorAction Stop
Write-Host "Directory tree and file contents copied to clipboard." -ForegroundColor Green
Write-Host "Total characters copied: $($finalOutput.Length)" -ForegroundColor Green
} else {
Write-Warning "No content was generated to copy to clipboard."
Set-Clipboard -Value ""
}
} catch {
Write-Error "Failed to copy to clipboard: $($_.Exception.Message)"
if ($_.Exception.Message -like '*Insufficient memory*') {
Write-Warning "The generated text might be too large for the clipboard."
}
exit 1
}
exit 0 # Success
'@
# --- Save the PowerShell Script ---try{$PowerShellScriptContent|Out-File-FilePath $ScriptPath-Encoding utf8 -Force -ErrorAction Stop
Write-Host"PowerShell script saved to: $ScriptPath"}catch{Write-Error"Failed to save PowerShell script '$ScriptPath'. Check permissions. Error: $($_.Exception.Message)"exit 1
}# --- Create the Batch Shim Content ---$BatchShimContent = @"
@echo off
rem This batch file executes the llmtxt.ps1 PowerShell script.
rem It passes all command line arguments (%%*) to the PowerShell script.
powershell.exe -ExecutionPolicy Bypass -NoProfile -File "$ScriptPath" %*
"@
# --- Save the Batch Shim ---try{$BatchShimContent|Out-File-FilePath $ShimPath-Encoding ascii -Force -ErrorAction Stop
Write-Host"Batch shim saved to: $ShimPath"}catch{Write-Error"Failed to save Batch shim '$ShimPath'. Check permissions. Error: $($_.Exception.Message)"}# --- Add Shim Directory to User PATH Environment Variable ---Write-Host"Adding '$ShimDir' to User PATH environment variable..."try{$CurrentUserPath = [System.Environment]::GetEnvironmentVariable('Path','User')$PathItems = $CurrentUserPath-split ';'|Where-Object{$_-ne''}|Select-Object-Unique
if($PathItems-notcontains$ShimDir){$NewPathItems = $PathItems+$ShimDir$NewUserPath = $NewPathItems-join';'[System.Environment]::SetEnvironmentVariable('Path',$NewUserPath,'User')Write-Host"'$ShimDir' added to User PATH."-ForegroundColor Green
Write-Host"IMPORTANT: You MUST RESTART your terminal (cmd/PowerShell) for the PATH change to take effect."-ForegroundColor Yellow
}else{Write-Host"'$ShimDir' is already in the User PATH."-ForegroundColor Cyan
}}catch{Write-Error"Failed to update User PATH environment variable. Error: $($_.Exception.Message)"Write-Warning"You might need to manually add '$ShimDir' to your User PATH environment variable."}Write-Host"`nSetup complete!"-ForegroundColor Green
Write-Host"Open a NEW terminal window and try typing: llmtxt"Write-Host"Or specify a path: llmtxt D:\SomeFolder"