/* * Local server for the SDE meeting toolkit. * Serves the static tool files. * Proxies AI draft requests to the Anthropic API, so the API key * stays on the server and never appears in the browser. */ const http = require('http'); const fs = require('fs'); const path = require('path'); const ROOT = __dirname; const PORT = process.env.PORT || 5173; const ANTHROPIC_VERSION = '2023-06-01'; const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.skill': 'application/zip' }; function loadEnvFile() { const envPath = path.join(ROOT, '.env'); let content; try { content = fs.readFileSync(envPath, 'utf8'); } catch (err) { return; // no .env file; that is allowed, the AI draft route will report it } content.split('\n').forEach((line) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) return; const idx = trimmed.indexOf('='); if (idx === -1) return; const key = trimmed.slice(0, idx).trim(); const value = trimmed.slice(idx + 1).trim().replace(/^["']|["']$/g, ''); if (key && !(key in process.env)) process.env[key] = value; }); } loadEnvFile(); function sendJson(res, status, obj) { const body = JSON.stringify(obj); res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(body); } function proxyToClaude(req, res) { if (!process.env.ANTHROPIC_API_KEY) { sendJson(res, 500, { error: 'ANTHROPIC_API_KEY is not set. Add it to the .env file in this folder, then restart the server.' }); return; } let body = ''; req.on('data', (chunk) => { body += chunk; }); req.on('end', async () => { try { const upstream = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': ANTHROPIC_VERSION }, body }); const text = await upstream.text(); res.writeHead(upstream.status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(text); } catch (err) { sendJson(res, 502, { error: 'Could not reach the Anthropic API: ' + err.message }); } }); } function serveStatic(req, res) { const urlPath = decodeURIComponent(req.url.split('?')[0]); if (urlPath === '/') { const index = `

SDE Meeting Toolkit

`; res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(index); return; } const filePath = path.normalize(path.join(ROOT, urlPath)); if (!filePath.startsWith(ROOT)) { res.writeHead(403); res.end('Forbidden'); return; } fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not found: ' + urlPath); return; } const ext = path.extname(filePath); res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' }); res.end(data); }); } const server = http.createServer((req, res) => { if (req.method === 'POST' && req.url === '/api/claude') { proxyToClaude(req, res); return; } if (req.method === 'GET') { serveStatic(req, res); return; } res.writeHead(405, { 'Content-Type': 'text/plain' }); res.end('Method not allowed'); }); server.listen(PORT, () => { console.log(`SDE meeting toolkit running at http://localhost:${PORT}`); console.log(process.env.ANTHROPIC_API_KEY ? 'ANTHROPIC_API_KEY loaded: AI draft button is live.' : 'No ANTHROPIC_API_KEY found: AI draft button will return an error until you add one to .env.'); });