import { prisma } from "./prisma"; // Settings keys and their env var fallbacks const SETTINGS_KEYS = { // Claude Code CLI authentication (OAuth access token from Claude Max) CLAUDE_CODE_OAUTH_TOKEN: { envVar: "CLAUDE_CODE_OAUTH_TOKEN", label: "Claude Code OAuth Token", placeholder: "sk-ant-oat01-...", secret: true }, // Postiz POSTIZ_URL: { envVar: "POSTIZ_URL", label: "Postiz URL", placeholder: "http://localhost:5000" }, POSTIZ_API_KEY: { envVar: "POSTIZ_API_KEY", label: "Postiz API Key", placeholder: "your-postiz-api-key", secret: true }, // Tavily (Research) TAVILY_API_KEY: { envVar: "TAVILY_API_KEY", label: "Tavily API Key", placeholder: "tvly-...", secret: true }, // Google Gemini (NanoBanana image generation) GEMINI_API_KEY: { envVar: "GEMINI_API_KEY", label: "Google Gemini API Key", placeholder: "AIza...", secret: true }, // Nextdoor NEXTDOOR_API_TOKEN: { envVar: "NEXTDOOR_API_TOKEN", label: "Nextdoor API Token", placeholder: "your-nextdoor-token", secret: true }, NEXTDOOR_ADVERTISER_ID: { envVar: "NEXTDOOR_ADVERTISER_ID", label: "Nextdoor Advertiser ID", placeholder: "your-advertiser-id" }, } as const; export type SettingKey = keyof typeof SETTINGS_KEYS; export const SETTINGS_CONFIG = SETTINGS_KEYS; // Grouped for UI export const SETTINGS_GROUPS = [ { name: "Claude", description: "OAuth access token for the Claude Code CLI subprocess that drives every pipeline agent. Get one with `claude setup-token` then `security find-generic-password -s 'Claude Code-credentials' -a $(whoami) -w` and use the `claudeAiOauth.accessToken` field. Tokens expire — refresh here when launches start failing with auth errors.", docsUrl: "https://docs.claude.com/en/docs/claude-code/setup", keys: ["CLAUDE_CODE_OAUTH_TOKEN"] as SettingKey[], }, { name: "Postiz", description: "Self-hosted social media scheduling. Handles Instagram and TikTok publishing.", docsUrl: "https://postiz.com", keys: ["POSTIZ_URL", "POSTIZ_API_KEY"] as SettingKey[], }, { name: "Tavily", description: "AI-powered web research. Used by the Trend Scout and Research agents. Free tier: 1,000 searches/month.", docsUrl: "https://tavily.com", keys: ["TAVILY_API_KEY"] as SettingKey[], }, { name: "Gemini", description: "Google Gemini powers NanoBanana MCP for AI image generation in static ads. ~$0.04-0.13/image.", docsUrl: "https://aistudio.google.com/apikey", keys: ["GEMINI_API_KEY"] as SettingKey[], }, { name: "Nextdoor", description: "Direct Nextdoor Ads API integration for local advertising.", docsUrl: "https://developer.nextdoor.com", keys: ["NEXTDOOR_API_TOKEN", "NEXTDOOR_ADVERTISER_ID"] as SettingKey[], }, ]; /** * Get a setting value. Checks DB first, falls back to env var. */ export async function getSetting(key: SettingKey): Promise { try { const setting = await prisma.setting.findUnique({ where: { key } }); if (setting?.value) return setting.value; } catch { // DB not available, fall through to env } const config = SETTINGS_KEYS[key]; return process.env[config.envVar] || ""; } /** * Get all settings as a map. */ export async function getAllSettings(): Promise> { const result: Record = {}; for (const [key, config] of Object.entries(SETTINGS_KEYS)) { try { const setting = await prisma.setting.findUnique({ where: { key } }); result[key] = setting?.value || process.env[config.envVar] || ""; } catch { result[key] = process.env[config.envVar] || ""; } } return result; } /** * Save a setting to the database. */ export async function saveSetting(key: SettingKey, value: string) { await prisma.setting.upsert({ where: { key }, update: { value }, create: { key, value }, }); } /** * Check connectivity status for each integration. */ export async function checkIntegrationStatus(): Promise> { const settings = await getAllSettings(); const status: Record = {}; // Claude OAuth token — validate by hitting the messages API with a 1-token call. // A valid OAuth token returns 200; an expired/invalid one returns 401. if (settings.CLAUDE_CODE_OAUTH_TOKEN) { try { const res = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "anthropic-version": "2023-06-01", "anthropic-beta": "oauth-2025-04-20", Authorization: `Bearer ${settings.CLAUDE_CODE_OAUTH_TOKEN}`, }, body: JSON.stringify({ model: "claude-haiku-4-5-20251001", max_tokens: 1, messages: [{ role: "user", content: "." }], }), signal: AbortSignal.timeout(10000), }); if (res.ok) { status.claude = { connected: true }; } else if (res.status === 401) { status.claude = { connected: false, error: "Token expired or invalid" }; } else { status.claude = { connected: false, error: `HTTP ${res.status}` }; } } catch (e) { status.claude = { connected: false, error: e instanceof Error ? e.message : "Connection failed" }; } } else { status.claude = { connected: false, error: "Not configured" }; } // Postiz if (settings.POSTIZ_URL && settings.POSTIZ_API_KEY) { try { const res = await fetch(`${settings.POSTIZ_URL}/public/v1/integrations`, { headers: { Authorization: `Bearer ${settings.POSTIZ_API_KEY}` }, signal: AbortSignal.timeout(5000), }); status.postiz = { connected: res.ok }; if (!res.ok) status.postiz.error = `HTTP ${res.status}`; } catch (e) { status.postiz = { connected: false, error: e instanceof Error ? e.message : "Connection failed" }; } } else { status.postiz = { connected: false, error: "Not configured" }; } // Tavily if (settings.TAVILY_API_KEY) { status.tavily = { connected: true }; // No ping endpoint, just check if key exists } else { status.tavily = { connected: false, error: "Not configured" }; } // Gemini if (settings.GEMINI_API_KEY) { status.gemini = { connected: true }; } else { status.gemini = { connected: false, error: "Not configured" }; } // Nextdoor if (settings.NEXTDOOR_API_TOKEN && settings.NEXTDOOR_ADVERTISER_ID) { status.nextdoor = { connected: true }; } else { status.nextdoor = { connected: false, error: "Not configured" }; } return status; }