diff --git a/Releases/v4.0.3/.claude/PAI-Install/engine/actions.ts b/Releases/v4.0.3/.claude/PAI-Install/engine/actions.ts index 6b8817cef..0862c41a2 100644 --- a/Releases/v4.0.3/.claude/PAI-Install/engine/actions.ts +++ b/Releases/v4.0.3/.claude/PAI-Install/engine/actions.ts @@ -724,28 +724,29 @@ export async function runConfiguration( await emit({ event: "message", content: "API keys saved securely." }); } - // Create symlinks so all consumers can find the .env - // Voice server reads ~/.env, hooks read ~/.claude/.env + // Create a symlink at ~/.claude/.env for PAI hooks that still read from + // the claude-scoped path. ~/.claude/ is PAI's own namespace so the symlink + // is safe; we do NOT symlink ~/.env anymore because ~/.env belongs to the + // user — they may have their own shell environment file there unrelated to + // PAI. VoiceServer and other components now read from envPath + // (~/.config/PAI/.env) directly, with an optional overlay from ~/.env if + // the user chooses to put PAI-relevant keys there. if (existsSync(envPath)) { - const symlinkPaths = [ - join(paiDir, ".env"), // ~/.claude/.env - join(homedir(), ".env"), // ~/.env (voice server reads this) - ]; - for (const symlinkPath of symlinkPaths) { - try { - // Remove stale symlink or file before creating - if (existsSync(symlinkPath)) { - const stat = lstatSync(symlinkPath); - if (stat.isSymbolicLink()) { - unlinkSync(symlinkPath); - } else { - continue; // Don't overwrite a real file - } + const claudeEnvSymlink = join(paiDir, ".env"); + try { + if (existsSync(claudeEnvSymlink)) { + const stat = lstatSync(claudeEnvSymlink); + if (stat.isSymbolicLink()) { + unlinkSync(claudeEnvSymlink); + } else { + // Don't overwrite a real file — skip creating the symlink. } - symlinkSync(envPath, symlinkPath); - } catch { - // Permission error or path conflict } + if (!existsSync(claudeEnvSymlink)) { + symlinkSync(envPath, claudeEnvSymlink); + } + } catch { + // Permission error or path conflict — non-fatal. } } diff --git a/Releases/v4.0.3/.claude/VoiceServer/server.ts b/Releases/v4.0.3/.claude/VoiceServer/server.ts index 9f5dec95c..bc71e0f0d 100644 --- a/Releases/v4.0.3/.claude/VoiceServer/server.ts +++ b/Releases/v4.0.3/.claude/VoiceServer/server.ts @@ -20,10 +20,13 @@ import { homedir } from "os"; import { join } from "path"; import { existsSync, readFileSync } from "fs"; -// Load .env from user home directory -const envPath = join(homedir(), '.env'); -if (existsSync(envPath)) { - const envContent = await Bun.file(envPath).text(); +// Load .env files — XDG-compliant location first, then ~/.env as optional +// user overlay. The user owns ~/.env for their own purposes; PAI-managed +// secrets live at ~/.config/PAI/.env so they never collide with user's file. +// Values in ~/.env win in the case of key collisions (explicit user override). +async function loadEnvFile(path: string): Promise { + if (!existsSync(path)) return; + const envContent = await Bun.file(path).text(); envContent.split('\n').forEach(line => { const [key, value] = line.split('='); if (key && value && !key.startsWith('#')) { @@ -32,12 +35,20 @@ if (existsSync(envPath)) { }); } +const xdgEnvPath = join(homedir(), '.config', 'PAI', '.env'); +const homeEnvPath = join(homedir(), '.env'); + +// Load PAI's managed secrets first (canonical location) +await loadEnvFile(xdgEnvPath); +// Then user overlay — any key in ~/.env wins over the XDG file +await loadEnvFile(homeEnvPath); + const PORT = parseInt(process.env.PORT || "8888"); const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY; if (!ELEVENLABS_API_KEY) { - console.error('⚠️ ELEVENLABS_API_KEY not found in ~/.env'); - console.error('Add: ELEVENLABS_API_KEY=your_key_here'); + console.error('⚠️ ELEVENLABS_API_KEY not found'); + console.error(`Add it to ${xdgEnvPath} (or ~/.env) as: ELEVENLABS_API_KEY=your_key_here`); } // ==========================================================================