From 39f84a84d29f947baab69a245992967581103f15 Mon Sep 17 00:00:00 2001 From: entropyy0 Date: Tue, 3 Feb 2026 12:26:52 +1100 Subject: [PATCH] feat: support API keys in config file (~/.summarize/config.json) Add an 'apiKeys' section to the config file so API keys can be stored persistently without polluting the shell environment: { "apiKeys": { "openai": "sk-...", "anthropic": "sk-ant-...", "google": "...", "openrouter": "sk-or-...", "xai": "...", "zai": "...", "apify": "...", "firecrawl": "...", "fal": "..." } } Precedence: environment variables > config file apiKeys. This maintains full backward compatibility. Supported providers: openai, anthropic, google, xai, openrouter, zai, apify, firecrawl, fal. Closes #59 --- src/config.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++ src/run/run-env.ts | 31 +++++++++++++++++++--------- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/config.ts b/src/config.ts index d1e6beca..9bcf3145 100644 --- a/src/config.ts +++ b/src/config.ts @@ -62,6 +62,18 @@ export type GoogleConfig = { baseUrl?: string } +export type ApiKeysConfig = { + openai?: string + anthropic?: string + google?: string + xai?: string + openrouter?: string + zai?: string + apify?: string + firecrawl?: string + fal?: string +} + export type LoggingLevel = 'debug' | 'info' | 'warn' | 'error' export type LoggingFormat = 'json' | 'pretty' export type LoggingConfig = { @@ -179,6 +191,12 @@ export type SummarizeConfig = { google?: GoogleConfig xai?: XaiConfig logging?: LoggingConfig + /** + * API keys for LLM providers and services. + * + * Precedence: environment variables > config file apiKeys. + */ + apiKeys?: ApiKeysConfig } function isRecord(value: unknown): value is Record { @@ -1000,6 +1018,37 @@ export function loadSummarizeConfig({ env }: { env: Record { + const value = (parsed as Record).apiKeys + if (typeof value === 'undefined') return undefined + if (!isRecord(value)) { + throw new Error(`Invalid config file ${path}: "apiKeys" must be an object.`) + } + const keys: Record = {} + const allowed = [ + 'openai', + 'anthropic', + 'google', + 'xai', + 'openrouter', + 'zai', + 'apify', + 'firecrawl', + 'fal', + ] + for (const [key, val] of Object.entries(value)) { + const k = key.trim().toLowerCase() + if (!allowed.includes(k)) { + throw new Error(`Invalid config file ${path}: unknown apiKeys provider "${key}".`) + } + if (typeof val !== 'string' || val.trim().length === 0) { + throw new Error(`Invalid config file ${path}: "apiKeys.${key}" must be a non-empty string.`) + } + keys[k] = val.trim() + } + return Object.keys(keys).length > 0 ? (keys as import('./config.js').ApiKeysConfig) : undefined + })() + return { config: { ...(model ? { model } : {}), @@ -1017,6 +1066,7 @@ export function loadSummarizeConfig({ env }: { env: Record configForCli: SummarizeConfig | null }): EnvState { - const xaiKeyRaw = typeof envForRun.XAI_API_KEY === 'string' ? envForRun.XAI_API_KEY : null + const cfgKeys = configForCli?.apiKeys + const xaiKeyRaw = + typeof envForRun.XAI_API_KEY === 'string' ? envForRun.XAI_API_KEY : (cfgKeys?.xai ?? null) const openaiBaseUrl = (() => { const envValue = normalizeBaseUrl(envForRun.OPENAI_BASE_URL) if (envValue) return envValue @@ -69,7 +71,7 @@ export function resolveEnvState({ ? envForRun.Z_AI_API_KEY : typeof envForRun.ZAI_API_KEY === 'string' ? envForRun.ZAI_API_KEY - : null + : (cfgKeys?.zai ?? null) const zaiBaseUrlRaw = typeof envForRun.Z_AI_BASE_URL === 'string' ? envForRun.Z_AI_BASE_URL @@ -77,15 +79,21 @@ export function resolveEnvState({ ? envForRun.ZAI_BASE_URL : null const openRouterKeyRaw = - typeof envForRun.OPENROUTER_API_KEY === 'string' ? envForRun.OPENROUTER_API_KEY : null + typeof envForRun.OPENROUTER_API_KEY === 'string' + ? envForRun.OPENROUTER_API_KEY + : (cfgKeys?.openrouter ?? null) const openaiKeyRaw = - typeof envForRun.OPENAI_API_KEY === 'string' ? envForRun.OPENAI_API_KEY : null + typeof envForRun.OPENAI_API_KEY === 'string' + ? envForRun.OPENAI_API_KEY + : (cfgKeys?.openai ?? null) const apiKey = typeof openaiBaseUrl === 'string' && /openrouter\.ai/i.test(openaiBaseUrl) ? (openRouterKeyRaw ?? openaiKeyRaw) : openaiKeyRaw const apifyToken = - typeof envForRun.APIFY_API_TOKEN === 'string' ? envForRun.APIFY_API_TOKEN : null + typeof envForRun.APIFY_API_TOKEN === 'string' + ? envForRun.APIFY_API_TOKEN + : (cfgKeys?.apify ?? null) const ytDlpPath = (() => { const explicit = typeof envForRun.YT_DLP_PATH === 'string' ? envForRun.YT_DLP_PATH.trim() : '' if (explicit.length > 0) return explicit @@ -101,11 +109,16 @@ export function resolveEnvState({ const value = raw.trim() return value.length > 0 ? value : null })() - const falApiKey = typeof envForRun.FAL_KEY === 'string' ? envForRun.FAL_KEY : null + const falApiKey = + typeof envForRun.FAL_KEY === 'string' ? envForRun.FAL_KEY : (cfgKeys?.fal ?? null) const firecrawlKey = - typeof envForRun.FIRECRAWL_API_KEY === 'string' ? envForRun.FIRECRAWL_API_KEY : null + typeof envForRun.FIRECRAWL_API_KEY === 'string' + ? envForRun.FIRECRAWL_API_KEY + : (cfgKeys?.firecrawl ?? null) const anthropicKeyRaw = - typeof envForRun.ANTHROPIC_API_KEY === 'string' ? envForRun.ANTHROPIC_API_KEY : null + typeof envForRun.ANTHROPIC_API_KEY === 'string' + ? envForRun.ANTHROPIC_API_KEY + : (cfgKeys?.anthropic ?? null) const googleKeyRaw = typeof envForRun.GEMINI_API_KEY === 'string' ? envForRun.GEMINI_API_KEY @@ -113,7 +126,7 @@ export function resolveEnvState({ ? envForRun.GOOGLE_GENERATIVE_AI_API_KEY : typeof envForRun.GOOGLE_API_KEY === 'string' ? envForRun.GOOGLE_API_KEY - : null + : (cfgKeys?.google ?? null) const firecrawlApiKey = firecrawlKey && firecrawlKey.trim().length > 0 ? firecrawlKey : null const firecrawlConfigured = firecrawlApiKey !== null