Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
35 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "arpitp2019/flowdash", "files": {"backend/src/main/java/com/flowdash/service/AiSettingsService.java": {"2b90ed7a3103b024cb351fa9b5bc1f2a114c2232": {"t": 1785745681.8887167, "n": 3042, "h": "5e510da1411f14db", "c": "package com.flowdash.service;\n\nimport com.flowdash.domain.AiProviderKey;\nimport com.flowdash.domain.AiProviderSetting;\nimport com.flowdash.domain.AppUser;\nimport com.flowdash.repository.AiProviderSettingRepository;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.math.BigDecimal;\n\n@Service\npublic class AiSettingsService {\n\n private final AiProviderSettingRepository settingRepository;\n\n @Value(\"${FLOWDASH_AI_PROVIDER:mock}\")\n private String defaultProvider;\n\n @Value(\"${FLOWDASH_AI_MODEL:flowdash-mock}\")\n private String defaultModel;\n\n @Value(\"${FLOWDASH_AI_BASE_URL:}\")\n private String defaultBaseUrl;\n\n public AiSettingsService(AiProviderSettingRepository settingRepository) {\n this.settingRepository = settingRepository;\n }\n\n public AiProviderSetting resolveFor(AppUser user) {\n if (user != null) {\n return settingRepository.findFirstByUserAndDefaultSelectedTrue(user)\n .orElseGet(() -> buildDefaultForUser(user));\n }\n return settingRepository.findFirstByUserIsNullAndDefaultSelectedTrue()\n .orElseGet(this::buildGlobalDefault);\n }\n\n public String resolveApiKey(AiProviderKey providerKey) {\n return switch (providerKey) {\n case GEMINI -> envOrDefault(\"GEMINI_API_KEY\", \"\");\n case CLAUDE -> envOrDefault(\"ANTHROPIC_API_KEY\", \"\");\n case MINIMAX -> envOrDefault(\"MINIMAX_API_KEY\", \"\");\n case GLM -> envOrDefault(\"GLM_API_KEY\", \"\");\n case DEEPSEEK -> envOrDefault(\"DEEPSEEK_API_KEY\", \"\");\n case OPENAI -> envOrDefault(\"OPENAI_API_KEY\", \"\");\n case GROK -> envOrDefault(\"XAI_API_KEY\", envOrDefault(\"GROK_API_KEY\", \"\"));\n case MOCK -> \"\";\n };\n }\n\n private AiProviderSetting buildDefaultForUser(AppUser user) {\n return new AiProviderSetting(user, parseProvider(defaultProvider), providerLabel(defaultProvider), defaultModel, defaultBaseUrl, true, true, new BigDecimal(\"0.30\"));\n }\n\n private AiProviderSetting buildGlobalDefault() {\n return new AiProviderSetting(null, parseProvider(defaultProvider), providerLabel(defaultProvider), defaultModel, defaultBaseUrl, true, true, new BigDecimal(\"0.30\"));\n }\n\n private static AiProviderKey parseProvider(String provider) {\n if (provider == null || provider.isBlank()) {\n return AiProviderKey.MOCK;\n }\n try {\n return AiProviderKey.valueOf(provider.trim().toUpperCase());\n } catch (Exception ignored) {\n return AiProviderKey.MOCK;\n }\n }\n\n private static String providerLabel(String provider) {\n if (provider == null || provider.isBlank()) {\n return \"Mock\";\n }\n return provider.trim();\n }\n\n private static String envOrDefault(String name, String fallback) {\n String value = System.getenv(name);\n return value == null || value.isBlank() ? fallback : value;\n }\n}\n"}, "a36c677f6ab380c3f81fe9d8f8f188b4dff6e3eb": {"t": 1785755694.5498085, "n": 3042, "h": "5e510da1411f14db", "c": "package com.flowdash.service;\n\nimport com.flowdash.domain.AiProviderKey;\nimport com.flowdash.domain.AiProviderSetting;\nimport com.flowdash.domain.AppUser;\nimport com.flowdash.repository.AiProviderSettingRepository;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Service;\n\nimport java.math.BigDecimal;\n\n@Service\npublic class AiSettingsService {\n\n private final AiProviderSettingRepository settingRepository;\n\n @Value(\"${FLOWDASH_AI_PROVIDER:mock}\")\n private String defaultProvider;\n\n @Value(\"${FLOWDASH_AI_MODEL:flowdash-mock}\")\n private String defaultModel;\n\n @Value(\"${FLOWDASH_AI_BASE_URL:}\")\n private String defaultBaseUrl;\n\n public AiSettingsService(AiProviderSettingRepository settingRepository) {\n this.settingRepository = settingRepository;\n }\n\n public AiProviderSetting resolveFor(AppUser user) {\n if (user != null) {\n return settingRepository.findFirstByUserAndDefaultSelectedTrue(user)\n .orElseGet(() -> buildDefaultForUser(user));\n }\n return settingRepository.findFirstByUserIsNullAndDefaultSelectedTrue()\n .orElseGet(this::buildGlobalDefault);\n }\n\n public String resolveApiKey(AiProviderKey providerKey) {\n return switch (providerKey) {\n case GEMINI -> envOrDefault(\"GEMINI_API_KEY\", \"\");\n case CLAUDE -> envOrDefault(\"ANTHROPIC_API_KEY\", \"\");\n case MINIMAX -> envOrDefault(\"MINIMAX_API_KEY\", \"\");\n case GLM -> envOrDefault(\"GLM_API_KEY\", \"\");\n case DEEPSEEK -> envOrDefault(\"DEEPSEEK_API_KEY\", \"\");\n case OPENAI -> envOrDefault(\"OPENAI_API_KEY\", \"\");\n case GROK -> envOrDefault(\"XAI_API_KEY\", envOrDefault(\"GROK_API_KEY\", \"\"));\n case MOCK -> \"\";\n };\n }\n\n private AiProviderSetting buildDefaultForUser(AppUser user) {\n return new AiProviderSetting(user, parseProvider(defaultProvider), providerLabel(defaultProvider), defaultModel, defaultBaseUrl, true, true, new BigDecimal(\"0.30\"));\n }\n\n private AiProviderSetting buildGlobalDefault() {\n return new AiProviderSetting(null, parseProvider(defaultProvider), providerLabel(defaultProvider), defaultModel, defaultBaseUrl, true, true, new BigDecimal(\"0.30\"));\n }\n\n private static AiProviderKey parseProvider(String provider) {\n if (provider == null || provider.isBlank()) {\n return AiProviderKey.MOCK;\n }\n try {\n return AiProviderKey.valueOf(provider.trim().toUpperCase());\n } catch (Exception ignored) {\n return AiProviderKey.MOCK;\n }\n }\n\n private static String providerLabel(String provider) {\n if (provider == null || provider.isBlank()) {\n return \"Mock\";\n }\n return provider.trim();\n }\n\n private static String envOrDefault(String name, String fallback) {\n String value = System.getenv(name);\n return value == null || value.isBlank() ? fallback : value;\n }\n}\n"}}, "legacy/server.js": {"2b90ed7a3103b024cb351fa9b5bc1f2a114c2232": {"t": 1786068489.9881594, "n": 28114, "h": "fffc040af70762a2", "c": "const http = require('http');\nconst fs = require('fs/promises');\nconst path = require('path');\n\nconst ROOT_DIR = __dirname;\nconst PORT = Number(process.env.PORT || 3000);\n\nconst MIME_TYPES = {\n '.css': 'text/css; charset=utf-8',\n '.gif': 'image/gif',\n '.html': 'text/html; charset=utf-8',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'application/javascript; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain; charset=utf-8',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.xml': 'application/xml; charset=utf-8'\n};\n\nconst BOOK_MODELS = [\n {\n name: 'Eisenhower Matrix',\n keywords: ['urgent', 'important', 'deadline', 'prioritize', 'prioritise', 'today', 'this week', 'focus'],\n why: 'Separates urgent work from important work so you do not confuse motion with progress.',\n how: 'Sort the decision into do now, schedule, delegate, or ignore.'\n },\n {\n name: 'Rubber Band Model',\n keywords: ['torn', 'between', 'stay', 'leave', 'two', 'either', 'or', 'pull'],\n why: 'Useful when two attractive directions are pulling hard in opposite directions.',\n how: 'Name the tension on each side and write what would make the tension disappear.'\n },\n {\n name: 'Yes/No Rule',\n keywords: ['simple', 'clear', 'criteria', 'rule', 'filter', 'yes', 'no', 'decision'],\n why: 'Helps when the decision should be made with a few upfront criteria instead of endless analysis.',\n how: 'Write the non-negotiable criteria first, then check the choice against them.'\n },\n {\n name: 'Choice Overload Model',\n keywords: ['too many', 'options', 'overwhelmed', 'paralyzed', 'confused', 'compare'],\n why: 'Explains why too much choice slows action and creates indecision.',\n how: 'Reduce the option set to a short list before evaluating tradeoffs.'\n },\n {\n name: 'Consequences Model',\n keywords: ['consequences', 'future', 'downstream', 'impact', 'result', 'if i do nothing'],\n why: 'Forces you to treat inaction as a real choice with consequences.',\n how: 'Write the immediate, delayed, and hidden effects of each option and of doing nothing.'\n },\n {\n name: 'The Stop Rule',\n keywords: ['research', 'stuck', 'delay', 'analysis', 'more information', 'commit'],\n why: 'Helpful when gathering more information is really just a disguised delay tactic.',\n how: 'Define the point at which you stop researching and move to action.'\n },\n {\n name: 'Hard Choice Model',\n keywords: ['safe', 'rational', 'bold', 'intuitive', 'mode'],\n why: 'Makes your default decision style visible so you can choose a better one on purpose.',\n how: 'Identify which mode you are using and whether a different mode would be smarter here.'\n },\n {\n name: 'Cognitive Bias Check',\n keywords: ['bias', 'anchoring', 'confirm', 'availability', 'fast', 'slow', 'thinking'],\n why: 'Catches the classic thinking mistakes that distort judgment.',\n how: 'Check whether you are anchored, cherry-picking evidence, over-weighting the memorable, or rushing.'\n },\n {\n name: 'Crossroads Model',\n keywords: ['career', 'job', 'path', 'future', 'life', 'major', 'change'],\n why: 'Maps where you are coming from, what matters, and what is blocking the way forward.',\n how: 'List the past, present, values, blockers, and the road that best aligns.'\n },\n {\n name: 'Rumsfeld Matrix',\n keywords: ['risk', 'uncertain', 'unknown', 'unknowns', 'safe', 'danger'],\n why: 'Clarifies what is known, unknown, and unknowable so risks do not stay fuzzy.',\n how: 'Map known knowns, known unknowns, and unknown unknowns before deciding.'\n },\n {\n name: 'Black Swan Model',\n keywords: ['rare', 'unexpected', 'extreme', 'tail risk', 'surprise', 'event'],\n why: 'Reminds you that rare events can dominate outcomes and invalidate normal expectations.',\n how: 'Ask what extreme but plausible event could break the plan.'\n },\n {\n name: 'Prisoner\\'s Dilemma',\n keywords: ['trust', 'cooperate', 'other person', 'relationship', 'partner', 'team'],\n why: 'Useful when the decision depends on trust, incentives, or repeated interactions with other people.',\n how: 'Ask whether cooperation, defection, or a repeated-game strategy is best.'\n },\n {\n name: 'Pareto Principle',\n keywords: ['80/20', 'impact', 'focus', 'effort', 'results', 'high leverage'],\n why: 'Finds the small number of actions that create most of the result.',\n how: 'Identify the 20 percent of actions that drive 80 percent of the payoff.'\n }\n];\n\nfunction env(name, fallback = '') {\n const value = process.env[name];\n return typeof value === 'string' && value.trim() ? value.trim() : fallback;\n}\n\nfunction hasApiKey(value) {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\nfunction createProviderProfiles() {\n return {\n openai: {\n key: 'openai',\n label: 'OpenAI',\n kind: 'openai-compatible',\n baseUrl: env('OPENAI_BASE_URL', 'https://api.openai.com/v1'),\n apiKey: env('OPENAI_API_KEY'),\n defaultModel: env('OPENAI_MODEL', 'gpt-4o-mini')\n },\n deepseek: {\n key: 'deepseek',\n label: 'DeepSeek',\n kind: 'openai-compatible',\n baseUrl: env('DEEPSEEK_BASE_URL', 'https://api.deepseek.com/v1'),\n apiKey: env('DEEPSEEK_API_KEY'),\n defaultModel: env('DEEPSEEK_MODEL', 'deepseek-chat')\n },\n minimax: {\n key: 'minimax',\n label: 'Minimax',\n kind: 'openai-compatible',\n baseUrl: env('MINIMAX_BASE_URL', 'https://api.minimax.chat/v1'),\n apiKey: env('MINIMAX_API_KEY'),\n defaultModel: env('MINIMAX_MODEL', 'minimax-chat')\n },\n anthropic: {\n key: 'anthropic',\n label: 'Claude / Anthropic',\n kind: 'anthropic',\n apiKey: env('ANTHROPIC_API_KEY'),\n defaultModel: env('ANTHROPIC_MODEL', 'claude-3-5-haiku-latest')\n },\n mock: {\n key: 'mock',\n label: 'Demo mode',\n kind: 'mock',\n defaultModel: env('FLOWDASH_MOCK_MODEL', 'flowdash-mock')\n }\n };\n}\n\nfunction getProviderList() {\n const profiles = createProviderProfiles();\n return Object.values(profiles).map((profile) => ({\n key: profile.key,\n label: profile.label,\n kind: profile.kind,\n defaultModel: profile.defaultModel,\n available: profile.kind === 'mock' ? true : hasApiKey(profile.apiKey)\n }));\n}\n\nfunction getDefaultProviderKey() {\n const configured = env('FLOWDASH_AI_PROVIDER');\n const profiles = createProviderProfiles();\n if (configured && profiles[configured]) {\n const profile = profiles[configured];\n if (profile.kind === 'mock' || hasApiKey(profile.apiKey)) {\n return configured;\n }\n }\n\n for (const key of ['openai', 'deepseek', 'minimax', 'anthropic']) {\n const profile = profiles[key];\n if (profile && hasApiKey(profile.apiKey)) return key;\n }\n\n return 'mock';\n}\n\nfunction getDefaultModel(providerKey) {\n const override = env('FLOWDASH_AI_MODEL');\n if (override) return override;\n const profiles = createProviderProfiles();\n const profile = profiles[providerKey] || profiles.mock;\n return profile.defaultModel || profiles.mock.defaultModel;\n}\n\nfunction getProviderProfile(providerKey) {\n const profiles = createProviderProfiles();\n const fallbackKey = getDefaultProviderKey();\n const key = profiles[providerKey] ? providerKey : fallbackKey;\n return profiles[key] || profiles.mock;\n}\n\nfunction safeJsonParse(raw, fallback) {\n if (!raw) return fallback;\n try {\n return JSON.parse(raw);\n } catch (err) {\n return fallback;\n }\n}\n\nfunction escapeRegExp(text) {\n return String(text).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction topKeywords(text, limit = 6) {\n const stopwords = new Set([\n 'the', 'and', 'for', 'with', 'that', 'this', 'your', 'from', 'have', 'will',\n 'into', 'about', 'what', 'when', 'where', 'how', 'why', 'can', 'should',\n 'would', 'could', 'then', 'than', 'they', 'them', 'their', 'there', 'been',\n 'want', 'need', 'make', 'take', 'choose', 'decision', 'decision-making',\n 'think', 'thinking', 'like', 'just', 'more', 'less', 'some', 'what', 'your',\n 'you', 'are', 'is', 'to', 'of', 'in', 'on', 'at', 'a', 'an', 'or', 'if',\n 'it', 'as', 'be', 'by', 'do', 'does', 'did', 'not', 'i', 'we', 'me', 'my'\n ]);\n\n const tokens = String(text || '')\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, ' ')\n .split(/\\s+/)\n .filter(Boolean)\n .filter((token) => token.length > 2 && !stopwords.has(token));\n\n const counts = new Map();\n tokens.forEach((token) => {\n counts.set(token, (counts.get(token) || 0) + 1);\n });\n\n return [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, limit)\n .map(([word]) => word);\n}\n\nfunction createTranscript(messages) {\n return (messages || [])\n .map((message, index) => {\n const role = message.role === 'assistant' ? 'Assistant' : 'User';\n const content = String(message.content || '').trim();\n return `${index + 1}. ${role}: ${content}`;\n })\n .join('\\n\\n');\n}\n\nfunction inferTopic(payload) {\n const threadTitle = String(payload.threadTitle || '').trim();\n if (threadTitle) return threadTitle;\n\n const lastUserMessage = [...(payload.messages || [])]\n .reverse()\n .find((message) => message.role === 'user');\n\n const rawText = lastUserMessage ? String(lastUserMessage.content || '') : '';\n const words = topKeywords(rawText, 6);\n if (words.length > 0) return words.join(' ');\n return 'your decision';\n}\n\nfunction pickRelevantModels(text) {\n const normalized = String(text || '').toLowerCase();\n const scores = BOOK_MODELS.map((model) => {\n const hits = model.keywords.reduce((count, keyword) => {\n const pattern = new RegExp(`\\\\b${escapeRegExp(keyword.toLowerCase())}\\\\b`, 'g');\n return count + ((normalized.match(pattern) || []).length > 0 ? 1 : 0);\n }, 0);\n return { ...model, hits };\n });\n\n const ranked = scores\n .filter((model) => model.hits > 0)\n .sort((a, b) => b.hits - a.hits);\n\n if (ranked.length > 0) {\n return ranked.slice(0, 4).map((model) => ({\n name: model.name,\n whyItFits: model.why,\n howToUse: model.how\n }));\n }\n\n return BOOK_MODELS.slice(0, 3).map((model) => ({\n name: model.name,\n whyItFits: model.why,\n howToUse: model.how\n }));\n}\n\nfunction lineArrayFromKeywords(keywords, fallbackLines) {\n if (keywords.length === 0) return fallbackLines;\n return keywords.map((keyword) => `Use ${keyword} as a concrete constraint, signal, or test.`);\n}\n\nfunction buildMockResponse(payload, source = 'mock') {\n const transcript = createTranscript(payload.messages);\n const focusFramework = String(payload.focusFramework || 'synthesis');\n const topic = inferTopic(payload);\n const keywords = topKeywords(transcript || topic, 6);\n const bookModels = pickRelevantModels(transcript || topic);\n const confidence = keywords.length > 4 ? 'medium' : 'low';\n\n const firstPrinciplesFacts = [\n `The decision is really about ${topic}.`,\n 'Separate what is known from what is assumed.',\n ...lineArrayFromKeywords(keywords, ['Identify the smallest reversible experiment that would reduce uncertainty.'])\n ];\n\n const inversionFailures = [\n `Avoid staying vague about ${topic}.`,\n 'Do not add complexity before you know what problem must be solved.',\n 'Do not wait for perfect certainty before taking the next cheap test.'\n ];\n\n const secondOrderEffects = [\n `Immediate effect: ${topic} becomes clearer.`,\n 'Second order effect: you learn which tradeoff matters most.',\n 'Third order effect: future choices get easier because the decision criteria are explicit.'\n ];\n\n const synthesisRecommendation = keywords.length > 0\n ? `Use a small, reversible next step on ${keywords[0]} before committing fully.`\n : 'Choose the smallest reversible next step that buys learning quickly.';\n\n const nextAction = keywords.length > 0\n ? `Write down the decision, then test ${keywords[0]} with one cheap experiment in the next 48 hours.`\n : 'Write the choice on one line, set the default option, and define a cheap test within 48 hours.';\n\n return {\n threadTitle: topic,\n assistantMessage: `Here’s the framework breakdown for ${topic}. ${synthesisRecommendation}`,\n frameworks: {\n firstPrinciples: {\n headline: 'Reduce the problem to facts, constraints, and tests.',\n coreFacts: firstPrinciplesFacts,\n assumptions: [\n `Assumption to challenge: ${topic} needs a full commitment right now.`,\n 'Assumption to test: the best answer is the one that minimizes regret, not the one that feels most polished.'\n ],\n questions: [\n 'What do we know for certain?',\n 'What would have to be true for each option to work?',\n 'What is the smallest test that would change the decision?',\n ],\n answer: `Start from the underlying facts of ${topic}, cut away the story, and test the core constraint with a small action.`\n },\n inversion: {\n headline: 'Find the ways this decision could fail, then design guardrails.',\n failureModes: inversionFailures,\n guardrails: [\n 'Define what would make this a bad decision.',\n 'Protect against delay, ambiguity, and untested assumptions.',\n 'Set a stop rule so research does not become procrastination.'\n ],\n answer: `If ${topic} fails, it will usually fail because the assumption stayed hidden or the plan was too big to test.`\n },\n secondOrder: {\n headline: 'Look past the immediate win and inspect the knock-on effects.',\n immediateEffects: [\n `You get a clearer view of ${topic}.`,\n 'You reduce emotional noise by writing the decision down.'\n ],\n secondOrderEffects: secondOrderEffects,\n answer: 'The most useful move is the one that improves future decisions, not just the current one.'\n },\n decisionModels: {\n headline: 'The most relevant models from The Decision Book.',\n models: bookModels,\n answer: `These models fit because they match the shape of the decision: ${bookModels.map((model) => model.name).join(', ')}.`\n },\n synthesis: {\n headline: 'Recommendation and next move.',\n recommendation: synthesisRecommendation,\n tradeoffs: [\n 'Fast action reduces uncertainty but can lock in a bad path if the wrong assumption is true.',\n 'More analysis can improve confidence, but only if it changes what you know.'\n ],\n risks: [\n 'Overthinking the decision instead of testing it.',\n 'Treating one model as the whole answer.'\n ],\n nextAction,\n confidence\n }\n },\n providerUsed: source,\n modelUsed: payload.model || getDefaultModel(payload.provider),\n generatedAt: new Date().toISOString()\n };\n}\n\nfunction buildSystemPrompt() {\n return [\n 'You are FlowDash Decision Coach, a sharp but friendly decision-making assistant.',\n 'Use first principles, inversion, second order thinking, and The Decision Book models.',\n 'Be practical and concise, not flowery.',\n 'Return JSON only. Do not wrap it in markdown fences.',\n 'Required JSON shape:',\n JSON.stringify({\n threadTitle: 'short title',\n assistantMessage: 'short chat reply',\n frameworks: {\n firstPrinciples: {\n headline: 'string',\n coreFacts: ['string'],\n assumptions: ['string'],\n questions: ['string'],\n answer: 'string'\n },\n inversion: {\n headline: 'string',\n failureModes: ['string'],\n guardrails: ['string'],\n answer: 'string'\n },\n secondOrder: {\n headline: 'string',\n immediateEffects: ['string'],\n secondOrderEffects: ['string'],\n answer: 'string'\n },\n decisionModels: {\n headline: 'string',\n models: [{ name: 'string', whyItFits: 'string', howToUse: 'string' }],\n answer: 'string'\n },\n synthesis: {\n headline: 'string',\n recommendation: 'string',\n tradeoffs: ['string'],\n risks: ['string'],\n nextAction: 'string',\n confidence: 'low|medium|high'\n }\n }\n }, null, 2)\n ].join('\\n');\n}\n\nfunction buildUserPrompt(payload) {\n const transcript = createTranscript(payload.messages);\n const topic = inferTopic(payload);\n const focus = String(payload.focusFramework || 'synthesis');\n return [\n `Decision topic: ${topic}`,\n `Active thinking lens: ${focus}`,\n payload.summary ? `Existing summary: ${payload.summary}` : '',\n payload.threadTitle ? `Thread title: ${payload.threadTitle}` : '',\n '',\n 'Conversation transcript:',\n transcript || '(no transcript yet)',\n '',\n 'Please answer with the full JSON object described in the system prompt.'\n ].filter(Boolean).join('\\n');\n}\n\nfunction parseProviderJson(text) {\n const raw = String(text || '').trim();\n const fencedMatch = raw.match(/```json\\s*([\\s\\S]*?)```/i) || raw.match(/```\\s*([\\s\\S]*?)```/i);\n const candidate = fencedMatch ? fencedMatch[1].trim() : raw;\n const start = candidate.indexOf('{');\n const end = candidate.lastIndexOf('}');\n const jsonText = start >= 0 && end > start ? candidate.slice(start, end + 1) : candidate;\n return JSON.parse(jsonText);\n}\n\nfunction normalizeArray(value, fallback = []) {\n if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);\n if (typeof value === 'string' && value.trim()) return [value.trim()];\n return [...fallback];\n}\n\nfunction normalizeModels(value, fallback = []) {\n if (!Array.isArray(value)) return [...fallback];\n return value\n .map((model) => ({\n name: String(model && model.name ? model.name : '').trim(),\n whyItFits: String(model && (model.whyItFits || model.why || '')).trim(),\n howToUse: String(model && (model.howToUse || model.how || '')).trim()\n }))\n .filter((model) => model.name);\n}\n\nfunction normalizeFrameworks(rawFrameworks, payload) {\n const fallback = buildMockResponse(payload, 'mock').frameworks;\n const frameworks = rawFrameworks && typeof rawFrameworks === 'object' ? rawFrameworks : {};\n\n return {\n firstPrinciples: {\n headline: String(frameworks.firstPrinciples?.headline || fallback.firstPrinciples.headline).trim(),\n coreFacts: normalizeArray(frameworks.firstPrinciples?.coreFacts, fallback.firstPrinciples.coreFacts),\n assumptions: normalizeArray(frameworks.firstPrinciples?.assumptions, fallback.firstPrinciples.assumptions),\n questions: normalizeArray(frameworks.firstPrinciples?.questions, fallback.firstPrinciples.questions),\n answer: String(frameworks.firstPrinciples?.answer || fallback.firstPrinciples.answer).trim()\n },\n inversion: {\n headline: String(frameworks.inversion?.headline || fallback.inversion.headline).trim(),\n failureModes: normalizeArray(frameworks.inversion?.failureModes, fallback.inversion.failureModes),\n guardrails: normalizeArray(frameworks.inversion?.guardrails, fallback.inversion.guardrails),\n answer: String(frameworks.inversion?.answer || fallback.inversion.answer).trim()\n },\n secondOrder: {\n headline: String(frameworks.secondOrder?.headline || fallback.secondOrder.headline).trim(),\n immediateEffects: normalizeArray(frameworks.secondOrder?.immediateEffects, fallback.secondOrder.immediateEffects),\n secondOrderEffects: normalizeArray(frameworks.secondOrder?.secondOrderEffects, fallback.secondOrder.secondOrderEffects),\n answer: String(frameworks.secondOrder?.answer || fallback.secondOrder.answer).trim()\n },\n decisionModels: {\n headline: String(frameworks.decisionModels?.headline || fallback.decisionModels.headline).trim(),\n models: normalizeModels(frameworks.decisionModels?.models, fallback.decisionModels.models),\n answer: String(frameworks.decisionModels?.answer || fallback.decisionModels.answer).trim()\n },\n synthesis: {\n headline: String(frameworks.synthesis?.headline || fallback.synthesis.headline).trim(),\n recommendation: String(frameworks.synthesis?.recommendation || fallback.synthesis.recommendation).trim(),\n tradeoffs: normalizeArray(frameworks.synthesis?.tradeoffs, fallback.synthesis.tradeoffs),\n risks: normalizeArray(frameworks.synthesis?.risks, fallback.synthesis.risks),\n nextAction: String(frameworks.synthesis?.nextAction || fallback.synthesis.nextAction).trim(),\n confidence: String(frameworks.synthesis?.confidence || fallback.synthesis.confidence || 'medium').trim() || 'medium'\n }\n };\n}\n\nfunction normalizeDecisionResponse(raw, payload, providerKey, model) {\n const fallback = buildMockResponse(payload, 'mock');\n const source = String(raw?.source || providerKey || 'provider');\n const threadTitle = String(raw?.threadTitle || fallback.threadTitle).trim() || fallback.threadTitle;\n const assistantMessage = String(raw?.assistantMessage || raw?.message || fallback.assistantMessage).trim() || fallback.assistantMessage;\n const frameworks = normalizeFrameworks(raw?.frameworks || raw?.analysis || {}, payload);\n\n if (!frameworks.synthesis.recommendation) {\n frameworks.synthesis.recommendation = fallback.frameworks.synthesis.recommendation;\n }\n\n return {\n threadTitle,\n assistantMessage,\n frameworks,\n providerUsed: source,\n modelUsed: model || payload.model || fallback.modelUsed,\n generatedAt: new Date().toISOString()\n };\n}\n\nasync function callOpenAiCompatible(profile, model, prompt) {\n const response = await fetch(`${profile.baseUrl.replace(/\\/$/, '')}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${profile.apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: prompt.system },\n { role: 'user', content: prompt.user }\n ],\n temperature: 0.2,\n max_tokens: 1600\n })\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(`OpenAI-compatible request failed (${response.status}): ${text || response.statusText}`);\n }\n\n const data = await response.json();\n const content = data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? '';\n return content;\n}\n\nasync function callAnthropic(profile, model, prompt) {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'x-api-key': profile.apiKey,\n 'anthropic-version': '2023-06-01',\n 'content-type': 'application/json'\n },\n body: JSON.stringify({\n model,\n max_tokens: 1600,\n system: prompt.system,\n messages: [\n { role: 'user', content: prompt.user }\n ],\n temperature: 0.2\n })\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => '');\n throw new Error(`Anthropic request failed (${response.status}): ${text || response.statusText}`);\n }\n\n const data = await response.json();\n if (Array.isArray(data?.content)) {\n return data.content.map((part) => part?.text || '').join('\\n');\n }\n return String(data?.content || '');\n}\n\nasync function runDecisionRelay(payload) {\n const providerKey = String(payload.provider || '').trim();\n const profile = getProviderProfile(providerKey);\n const model = String(payload.model || '').trim() || getDefaultModel(profile.key);\n const prompt = {\n system: buildSystemPrompt(),\n user: buildUserPrompt(payload)\n };\n\n if (profile.kind === 'mock' || !hasApiKey(profile.apiKey)) {\n return buildMockResponse(payload, profile.kind === 'mock' ? 'mock' : 'fallback');\n }\n\n try {\n const text = profile.kind === 'anthropic'\n ? await callAnthropic(profile, model, prompt)\n : await callOpenAiCompatible(profile, model, prompt);\n const parsed = parseProviderJson(text);\n return normalizeDecisionResponse(parsed, payload, profile.key, model);\n } catch (err) {\n const fallback = buildMockResponse(payload, 'fallback');\n return {\n ...fallback,\n assistantMessage: `${fallback.assistantMessage} (Fallback mode because the relay could not reach ${profile.label}.)`,\n providerUsed: 'fallback',\n modelUsed: model,\n generatedAt: new Date().toISOString()\n };\n }\n}\n\nfunction sendJson(res, statusCode, payload) {\n const body = JSON.stringify(payload, null, 2);\n res.writeHead(statusCode, {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Cache-Control': 'no-store'\n });\n res.end(body);\n}\n\nfunction sendText(res, statusCode, text, contentType = 'text/plain; charset=utf-8') {\n res.writeHead(statusCode, {\n 'Content-Type': contentType,\n 'Cache-Control': 'no-store'\n });\n res.end(text);\n}\n\nasync function readBody(req, limit = 1_000_000) {\n const chunks = [];\n let total = 0;\n for await (const chunk of req) {\n total += chunk.length;\n if (total > limit) {\n throw new Error('Request body too large.');\n }\n chunks.push(chunk);\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n\nfunction getContentType(filePath) {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream';\n}\n\nfunction resolveStaticPath(urlPath) {\n const safePath = decodeURIComponent(urlPath.split('?')[0] || '/');\n const normalized = safePath === '/' ? '/index.html' : safePath;\n const absolute = path.resolve(ROOT_DIR, `.${normalized}`);\n const relative = path.relative(ROOT_DIR, absolute);\n\n if (relative.startsWith('..') || path.isAbsolute(relative)) {\n throw new Error('Forbidden path.');\n }\n\n return absolute;\n}\n\nasync function serveStatic(req, res, urlPath) {\n let filePath = resolveStaticPath(urlPath);\n let stat;\n\n try {\n stat = await fs.stat(filePath);\n if (stat.isDirectory()) {\n filePath = path.join(filePath, 'index.html');\n stat = await fs.stat(filePath);\n }\n } catch (err) {\n if (path.extname(filePath)) {\n sendText(res, 404, 'Not found.');\n return;\n }\n filePath = path.join(ROOT_DIR, 'index.html');\n }\n\n try {\n const data = await fs.readFile(filePath);\n res.writeHead(200, {\n 'Content-Type': getContentType(filePath),\n 'Cache-Control': 'no-store'\n });\n res.end(data);\n } catch (err) {\n sendText(res, 404, 'Not found.');\n }\n}\n\nasync function handleDecisionConfig(res) {\n const providerList = getProviderList();\n const defaultProvider = getDefaultProviderKey();\n sendJson(res, 200, {\n defaultProvider,\n defaultModel: getDefaultModel(defaultProvider),\n demoMode: defaultProvider === 'mock',\n providers: providerList\n });\n}\n\nasync function handleDecisionChat(req, res) {\n try {\n const body = await readBody(req);\n const payload = safeJsonParse(body, null);\n if (!payload || typeof payload !== 'object') {\n sendJson(res, 400, { error: 'Invalid JSON payload.' });\n return;\n }\n\n const response = await runDecisionRelay(payload);\n sendJson(res, 200, {\n ok: true,\n ...response\n });\n } catch (err) {\n sendJson(res, 500, {\n error: err.message || 'Decision relay failed.'\n });\n }\n}\n\nconst server = http.createServer(async (req, res) => {\n const requestUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);\n\n if (req.method === 'GET' && requestUrl.pathname === '/api/decision-config') {\n await handleDecisionConfig(res);\n return;\n }\n\n if (req.method === 'POST' && requestUrl.pathname === '/api/decision-chat') {\n await handleDecisionChat(req, res);\n return;\n }\n\n if (req.method === 'GET' && requestUrl.pathname === '/health') {\n sendJson(res, 200, { ok: true });\n return;\n }\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n sendText(res, 405, 'Method not allowed.');\n return;\n }\n\n await serveStatic(req, res, requestUrl.pathname);\n});\n\nserver.listen(PORT, () => {\n console.log(`FlowDash relay listening on http://localhost:${PORT}`);\n console.log(`Default AI provider: ${getDefaultProviderKey()}`);\n});\n"}}}}