Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions Releases/v4.0.3/.claude/PAI-Install/engine/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
}

Expand Down
23 changes: 17 additions & 6 deletions Releases/v4.0.3/.claude/VoiceServer/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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('#')) {
Expand All @@ -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`);
}

// ==========================================================================
Expand Down