import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; export const SEARCH_API_KEY_NAMES = ["TAVILY_API_KEY", "EXA_API_KEY", "KEENABLE_API_KEY"] as const; type SearchApiKeyName = (typeof SEARCH_API_KEY_NAMES)[number]; type SearchEnvironment = Record; const SAFE_VALUE = /^[A-Za-z0-9._-]+$/; export function getSearchConfigPath(env: SearchEnvironment = process.env): string { if (env.MY_PI_SEARCH_CONFIG?.trim()) { return env.MY_PI_SEARCH_CONFIG.trim(); } const configHome = env.XDG_CONFIG_HOME?.trim() || join(env.HOME?.trim() || homedir(), ".config"); return join(configHome, "my-pi", "search.env"); } export function parseSearchConfig(text: string): Partial> { const parsed: Partial> = {}; for (const rawLine of text.split(/\r?\n/u)) { const line = rawLine.trim().replace(/^export[ \t]+/u, ""); if (!line || line.startsWith("#")) continue; const separator = line.indexOf("="); if (separator < 1) continue; const name = line.slice(0, separator).trim() as SearchApiKeyName; const value = line.slice(separator + 1).trim(); if (!SEARCH_API_KEY_NAMES.includes(name) || !SAFE_VALUE.test(value)) continue; parsed[name] = value; } return parsed; } /** Load package-managed search credentials without overriding explicit process environment values. */ export function loadSearchConfigEnv( env: SearchEnvironment = process.env, configPath = getSearchConfigPath(env), ): Partial> { let parsed: Partial> = {}; try { parsed = parseSearchConfig(readFileSync(configPath, "utf8")); } catch { // Missing or unreadable config is equivalent to an empty package-managed config. } const effective: Partial> = {}; for (const name of SEARCH_API_KEY_NAMES) { if (!env[name]?.trim() && parsed[name]) env[name] = parsed[name]; if (env[name]?.trim()) effective[name] = env[name]?.trim(); } return effective; } /** Load search API keys before provider extensions initialize. */ export default function searchConfigExtension(): void { loadSearchConfigEnv(); }