Invoke-Claude.ps1
|
[CmdletBinding(PositionalBinding=$false)] param( [switch]$Login, [switch]$ForceMemoryRedirect, [Parameter(ValueFromRemainingArguments)] [string[]]$ClaudeArgs = @() ) begin { . (Join-Path $PSScriptRoot 'Get-ClaudeExecutable.ps1') . (Join-Path $PSScriptRoot 'Test-CorePowerShell.ps1') . (Join-Path $PSScriptRoot 'Test-ProfileSetup.ps1') . (Join-Path $PSScriptRoot 'Test-IsModuleUpToDate.ps1') . (Join-Path $PSScriptRoot 'Enable-ClaudeMemoryRedirect.ps1') . (Join-Path $PSScriptRoot 'Test-AnthropicProxy.ps1') Test-CorePowerShell -ScriptPath $MyInvocation.MyCommand.Path ` -BoundParameters $PSBoundParameters -RemainingArgs $ClaudeArgs ` -Verbose:($VerbosePreference -eq 'Continue') Test-ProfileSetup # Enable PowerShell tool in all cases. $env:CLAUDE_CODE_USE_POWERSHELL_TOOL = "1" # Use $script:onWindows (not $script:isWindows) to avoid colliding with the read-only automatic variable $IsWindows. $script:onWindows = $IsWindows -or $env:OS -eq 'Windows_NT' # Platform-appropriate home directory. $script:homeDir = [System.Environment]::GetFolderPath('UserProfile') # Redirect ~/.claude/projects to $env:CLAUDE_MEMORY_DIR if set (one-time tip if not). Enable-ClaudeMemoryRedirect -HomeDir $script:homeDir -Force:$ForceMemoryRedirect # Expose the profile.ps1 path so Claude's PowerShell tool can source it with -NoProfile. # Any pwsh subprocess spawned from this session inherits PWRCLAUDE_PROFILE automatically. $env:PWRCLAUDE_PROFILE = Join-Path $PSScriptRoot 'profile.ps1' # Prefer ~/.local/bin native install first $sep = [System.IO.Path]::PathSeparator $claudePath = Join-Path $HOME '.local' 'bin' if (Test-Path $claudePath) { $env:Path = "$claudePath$sep$env:Path" } Test-AnthropicProxy -Login:$Login } process { # Welcome banner $pwrClaudeVersion = (Get-Module PwrClaude -ErrorAction SilentlyContinue)?.Version ?? '?' Write-Host "Welcome to PwrClaude v$pwrClaudeVersion" -ForegroundColor Cyan Test-IsModuleUpToDate # --- System prompt assembly --- # Helper: shorten paths under MyDocuments to ${MyDocuments}\... $script:myDocs = [Environment]::GetFolderPath('MyDocuments') function Format-DirectivePath([string]$p) { if ($p.StartsWith($script:myDocs, [System.StringComparison]::OrdinalIgnoreCase)) { '${MyDocuments}\' + $p.Substring($script:myDocs.Length).TrimStart('\') } else { $p } } # Start with the built-in PwrClaude directives. $systemPromptParts = [System.Collections.Generic.List[string]]::new() $injectedPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $claudeMd = Join-Path $PSScriptRoot 'CLAUDE.md' if (Test-Path $claudeMd) { $claudeMdFull = (Resolve-Path $claudeMd).Path $null = $injectedPaths.Add($claudeMdFull) Write-Host "Injecting PwrClaude directives: $(Format-DirectivePath $claudeMdFull)" $systemPromptParts.Add((Get-Content $claudeMd -Raw)) } # Append user-specific directives from CLAUDE_USER_MD tree. # Set this variable to a directory; every CLAUDE.md found recursively inside it is loaded. # Uses @<path> file-reference syntax so Claude Code resolves the content itself # rather than inlining the full text into the system prompt argument. if ($env:CLAUDE_USER_MD) { try { $resolvedUserMd = Invoke-Expression "`"$env:CLAUDE_USER_MD`"" } catch { Write-Host "WARNING: CLAUDE_USER_MD expression failed to evaluate: $env:CLAUDE_USER_MD" -ForegroundColor Yellow Write-Host " Error: $_" -ForegroundColor Yellow $resolvedUserMd = $null } if ($resolvedUserMd -and (Test-Path $resolvedUserMd)) { Write-Verbose "Resolved CLAUDE_USER_MD: $resolvedUserMd" $userMdFiles = Get-ChildItem -Path $resolvedUserMd -Filter 'CLAUDE.md' -Recurse -ErrorAction SilentlyContinue foreach ($f in $userMdFiles) { Write-Host "Injecting user directives: $(Format-DirectivePath (Resolve-Path $f.FullName).Path)" $systemPromptParts.Add("@$($f.FullName)") } } elseif ($resolvedUserMd) { Write-Host "WARNING: CLAUDE_USER_MD path does not exist: $resolvedUserMd" -ForegroundColor Yellow if ($resolvedUserMd -ne $env:CLAUDE_USER_MD) { Write-Host " (resolved from expression: $env:CLAUDE_USER_MD)" -ForegroundColor Yellow } } } else { $suppressFile = Join-Path $script:homeDir '.claude' 'suppress_user_md_hint' if (-not (Test-Path $suppressFile)) { Write-Host "" Write-Host "Tip: set `$env:CLAUDE_USER_MD to a directory containing CLAUDE.md files with your" Write-Host " personal directives. Claude will load and obey them automatically every session." Write-Host "" Write-Host " The value is evaluated as a PowerShell expression, so you can use variables:" Write-Host " `$env:CLAUDE_USER_MD = '`${env:USERPROFILE}\my-claude-directives'" Write-Host " `$env:CLAUDE_USER_MD = '`${env:OneDrive}\Claude\directives'" Write-Host "" Write-Host " Place CLAUDE.md files anywhere in that directory tree — all are loaded recursively." Write-Host "" $answer = Read-Host "Show this tip next time? [Y/n/never/e(xit)]" if ($answer -match '^[nN]ever$') { New-Item -ItemType File -Path $suppressFile -Force | Out-Null Write-Host "Got it — tip suppressed permanently. Set CLAUDE_USER_MD any time to enable personal directives." } elseif ($answer -match '^[nN]$') { New-Item -ItemType File -Path $suppressFile -Force | Out-Null Write-Host "Got it — tip suppressed. Set CLAUDE_USER_MD any time to enable personal directives." } elseif ($answer -match '^([eE]|[eE]xit)$') { return } Write-Host "" } } # Inject CLAUDE.md from loaded modules and from PSModulePath so installed-but-not-imported # modules (e.g. PwrStash) are included without requiring the user to load them first. # Skip oh-my-posh-core — its ModuleBase points to a source tree that happens to have a CLAUDE.md. $skipModuleNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $null = $skipModuleNames.Add('oh-my-posh-core') # First pass: loaded modules — inject CLAUDE.md only $loadedModules = Get-Module | Where-Object { -not $skipModuleNames.Contains($_.Name) } foreach ($mod in $loadedModules) { $directiveFile = Join-Path $mod.ModuleBase 'CLAUDE.md' if (Test-Path $directiveFile) { $resolved = (Resolve-Path $directiveFile).Path if ($injectedPaths.Add($resolved)) { Write-Host "Loading module directives from $($mod.Name): $(Format-DirectivePath $resolved)" $systemPromptParts.Add("@$directiveFile") } } } # Second pass: scan PSModulePath for CLAUDE.md in installed-but-not-loaded modules. # Only CLAUDE.md is checked here (README files in arbitrary modules may not be directives). $psModulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator foreach ($modulePath in $psModulePaths) { if (-not (Test-Path $modulePath)) { continue } foreach ($modDir in (Get-ChildItem -LiteralPath $modulePath -Directory -ErrorAction SilentlyContinue)) { if ($skipModuleNames.Contains($modDir.Name)) { continue } # Module dirs may be flat (ModuleName/CLAUDE.md) or versioned (ModuleName/1.0.0/CLAUDE.md). # Use Select-Object -ExpandProperty rather than .FullName on the collected result so that # an empty pipeline contributes nothing (avoids null elements that break Join-Path). $candidateDirs = @($modDir.FullName) + @( Get-ChildItem -LiteralPath $modDir.FullName -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^\d+\.\d+' } | Select-Object -ExpandProperty FullName ) foreach ($dir in $candidateDirs) { $claudeMdPath = Join-Path $dir 'CLAUDE.md' if (-not (Test-Path $claudeMdPath)) { continue } $resolved = (Resolve-Path $claudeMdPath).Path if ($injectedPaths.Add($resolved)) { Write-Host "Loading module directives from $($modDir.Name) (PSModulePath): $(Format-DirectivePath $resolved)" $systemPromptParts.Add("@$claudeMdPath") } } } } $systemPromptArgs = if ($systemPromptParts.Count -gt 0) { Write-Host "System prompt assembled: $($systemPromptParts.Count) directive(s)" @('--append-system-prompt', ($systemPromptParts -join "`n`n")) } else { @() } # --- End system prompt assembly --- $claudeExe = Get-ClaudeExecutable if (-not $claudeExe) { Write-Error "Could not locate the 'claude' executable. Install Claude Code." return } & $claudeExe @systemPromptArgs @ClaudeArgs } |