/** * CF Pages Function — POST /api/webhook/telegram * MX14-E1: Edge Filtering — comandi semplici gestiti a CF (0 chiamate Railway). * * Flusso: * 1. Valida X-Telegram-Bot-Api-Secret-Token header * 2. Parse update Telegram * 3. Edge commands (/start /help /ping /stato): rispondi da CF o KV — return 200, no Railway * 4. Tutto il resto: return 200 subito, dispatch async a Railway (comportamento precedente) * → fallisce? → accoda su Supabase telegram_queue * * Env bindings (CF Pages → Settings → Variables and Secrets): * TELEGRAM_WEBHOOK_SECRET — segreto impostato in setWebhook (header validation) * TELEGRAM_BOT_TOKEN — token bot (richiesto per risposte edge dirette) * BACKEND_URL_A — HF BRAIN/HANDS Space URL * BRAIN: https://arjanit98-terminal.hf.space (primario) * HANDS: https://baida07-ai-backend-collab.hf.space (esecuzione codice) * BACKEND_URL — Railway (legacy, non più nella critical path) * INTERNAL_TOKEN — token per autenticare CF → Railway * SUPABASE_URL — per fallback queue * SUPABASE_SERVICE_KEY — service role key (bypass RLS) * AGENT_KV — KV namespace binding (id: 4db4874d2a9e49a9aed602697d8a5c45) * * MX14-E1 — comandi intercettati a edge (nessuna chiamata a Railway): * /start → risposta statica * /help → lista comandi * /ping → pong con latenza CF * /stato → stato da AGENT_KV (cache daemon) — fallback Railway se KV vuoto */ // --- CONFIGURAZIONE ARCHITETTURA 4 QUADRANTI (MX18-P0-FIX) --- const QUADRANT_A = { url: 'https://zwdoplodbdsxfrddoxmo.supabase.co', key: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp3ZG9wbG9kYmRzeGZyZGRveG1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3OTA1NjMwNSwiZXhwIjoyMDk0NjMyMzA1fQ.tGlgX4peRhWLhQmVd45tjqwiTvEFulOpplkmjkUSt9Y', role: 'BRAIN' }; const QUADRANT_B = { url: 'https://sluvxtpbtxevrcooaqou.supabase.co', key: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InpsdXZ4dHBidHhldnJjb29hcW91Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc4MjA3Mjk5MiwiZXhwIjoyMDk3NjQ4OTkyfQ.WS4cpsvtO-FGkXukCqpFF6qXfQb38VUs5rtiLwa0khk', role: 'LOGS' }; const QUADRANT_C = { project_id: 'a9ce05f8-aeca-46c1-837b-8c2ca7a11081', token: '1edad57a-a9ee-4b47-8272-5e8003c02c9c', role: 'QUEUE' }; const QUADRANT_D = { project_id: '378eaeaa-ec8d-4833-9a7f-5f57f1f2e7ed', token: '2f269320-0036-40c7-8ef6-f20bae855f4a', role: 'FAILOVER' }; interface Env { TELEGRAM_WEBHOOK_SECRET: string; TELEGRAM_BOT_TOKEN: string; BACKEND_URL_A: string; // HF BRAIN Space (arjanit98-terminal.hf.space) — no cold-start, always on BACKEND_URL: string; // Railway (legacy — sostituito da BACKEND_URL_A nella critical path) INTERNAL_TOKEN: string; SUPABASE_URL: string; SUPABASE_SERVICE_KEY: string; OWNER_CHAT_IDS: string; // CSV di chat_id autorizzati AGENT_KV: KVNamespace; OWNER_CHAT_IDS: string; // Chat ID autorizzati (virgola-separati) per comandi privilegiati GH_TOKEN_CF: string; // PAT fine-grained con actions:write su Baida98/AI (per /logs) GH_TOKEN_D: string; // PAT arypulka98-bot con actions:write su arypulka98-bot/AI (relay /run /deploy) } // ── Telegram API helpers ────────────────────────────────────────────────────── async function tgSend(chatId: number, html: string, env: Env, replyMarkup?: unknown): Promise { const token = env.TELEGRAM_BOT_TOKEN ?? ''; if (!token) return; // bot token non configurato → edge silent, Railway gestirà await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text: html, parse_mode: 'HTML', ...(replyMarkup ? { reply_markup: replyMarkup } : {}), }), }).catch(() => {}); } /** Estrae il comando (es. "/start", "/help") dall'update Telegram. */ function extractCommand(update: Record): string | null { const msg = update.message as Record | undefined; const text = (msg?.text as string | undefined ?? '').trim(); if (!text.startsWith('/')) return null; // Gestisce /cmd@BotName return text.split(/\s+/)[0].split('@')[0].toLowerCase(); } function extractChatId(update: Record): number | null { const msg = update.message as Record | undefined; const chat = msg?.chat as Record | undefined; return typeof chat?.id === 'number' ? chat.id : null; } // ── Argomenti messaggio ───────────────────────────────────────────────────── /** Estrae il testo dopo il comando (es. "/run fai X" → "fai X"). */ function extractArgs(update: Record): string { const msg = update.message as Record | undefined; const text = (msg?.text as string | undefined ?? '').trim(); return text.split(/\s+/).slice(1).join(' ').trim(); } // ── Owner / autorizzazione ────────────────────────────────────── /** * isOwner — verifica se chatId è nell'allowlist OWNER_CHAT_IDS. * Fail-CLOSED: OWNER_CHAT_IDS vuoto → nessuno è owner (sicurezza default). * IMPORTANTE: configurare OWNER_CHAT_IDS in CF Pages → Settings → Variables and Secrets. * Formato: "123456789" oppure "123456789,987654321" (più chat ID separati da virgola). */ function isOwner(chatId: number, env: Env): boolean { const raw = env.OWNER_CHAT_IDS ?? ''; if (!raw.trim()) return false; // fail-closed: nessun accesso senza OWNER_CHAT_IDS configurato return raw.split(',').map(s => s.trim()).filter(Boolean).includes(String(chatId)); } // ── GitHub API helpers (comandi privilegiati) ─────────────────────────────── /** Dispatch workflow_dispatch su Baida98/AI via GH_TOKEN_CF. */ async function ghDispatch( workflow: string, inputs: Record, env: Env ): Promise<{ ok: boolean; msg: string }> { const token = env.GH_TOKEN_CF ?? ''; if (!token) return { ok: false, msg: '⚠️ GH_TOKEN_CF non configurato in CF Pages.' }; try { const r = await fetch( `https://api.github.com/repos/Baida98/AI/actions/workflows/${workflow}/dispatches`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/vnd.github.v3+json', 'User-Agent': 'CF-Pages-TelegramBot/2.0', }, body: JSON.stringify({ ref: 'main', inputs }), signal: AbortSignal.timeout(12_000), } ); if (r.status === 204) return { ok: true, msg: '✅ Dispatch inviato (GitHub 204)' }; const body = await r.text().catch(() => ''); return { ok: false, msg: `❌ GitHub ${r.status}: ${body.slice(0, 120)}` }; } catch (e) { return { ok: false, msg: `❌ Timeout: ${String(e).slice(0, 80)}` }; } } /** Legge l'ultimo run di un workflow su Baida98/AI. */ /** Legge l'ultimo run di un workflow su arypulka98-bot/AI. */ async function ghRelayLastRun(workflow: string, env: Env): Promise { const token = env.GH_TOKEN_D ?? env.GH_TOKEN_CF ?? ''; if (!token) return '⚠️ GH_TOKEN_D/CF mancante'; try { const r = await fetch( `https://api.github.com/repos/arypulka98-bot/AI/actions/workflows/${workflow}/runs?per_page=1`, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'CF-Pages-TelegramBot/2.0', }, signal: AbortSignal.timeout(10_000), } ); if (!r.ok) return `❌ GitHub ${r.status}`; const j = await r.json() as { workflow_runs: Array<{ status: string; conclusion: string | null }> }; const run = j.workflow_runs[0]; if (!run) return '∅ mai eseguito'; const status = run.status === 'completed' ? (run.conclusion === 'success' ? '✅' : '❌') : '⏳'; return `${status} ${run.status}${run.conclusion ? ' (' + run.conclusion + ')' : ''}`; } catch { return '❌ Timeout GitHub API'; } } async function ghLastRun(workflow: string, env: Env): Promise { const token = env.GH_TOKEN_CF ?? ''; if (!token) return '⚠️ GH_TOKEN_CF mancante'; try { const r = await fetch( `https://api.github.com/repos/Baida98/AI/actions/workflows/${workflow}/runs?per_page=1`, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'CF-Pages-TelegramBot/2.0', }, signal: AbortSignal.timeout(10_000), } ); if (!r.ok) return `❌ GitHub ${r.status}`; type RunObj = { status: string; conclusion: string | null; created_at: string }; const j = await r.json() as { workflow_runs?: RunObj[] }; const run = j.workflow_runs?.[0]; if (!run) return '⚠️ Nessun run trovato'; const icon = run.conclusion === 'success' ? '✅' : run.conclusion === 'failure' ? '❌' : '🔄'; const when = run.created_at.slice(0, 16).replace('T', ' ') + ' UTC'; return `${icon} ${run.status}/${run.conclusion ?? 'running'} — ${when}`; } catch { return '❌ Timeout GitHub API'; } } /** Dispatch repository_dispatch su arypulka98-bot/AI via GH_TOKEN_D (relay hub). */ async function ghRepoDispatch( eventType: string, payload: Record, env: Env ): Promise<{ ok: boolean; msg: string }> { const token = env.GH_TOKEN_D ?? env.GH_TOKEN_CF ?? ''; if (!token) return { ok: false, msg: '⚠️ GH_TOKEN_D non configurato in CF Pages → Settings → Secrets.' }; try { const r = await fetch( 'https://api.github.com/repos/arypulka98-bot/AI/dispatches', { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/vnd.github.v3+json', 'User-Agent': 'CF-Pages-TelegramBot/2.0', }, body: JSON.stringify({ event_type: eventType, client_payload: payload }), signal: AbortSignal.timeout(12_000), } ); if (r.status === 204) return { ok: true, msg: '✅ Relay D: dispatch inviato (204)' }; const body = await r.text().catch(() => ''); return { ok: false, msg: `❌ Relay D: GitHub ${r.status}: ${body.slice(0, 120)}` }; } catch (e) { return { ok: false, msg: `❌ Relay D: Timeout: ${String(e).slice(0, 80)}` }; } } /** Dispatch workflow_dispatch su arypulka98-bot/AI via GH_TOKEN_D. */ async function ghRelayDispatch( workflow: string, inputs: Record, env: Env ): Promise<{ ok: boolean; msg: string }> { const token = env.GH_TOKEN_D ?? ''; if (!token) { // Fallback: Baida98/AI con GH_TOKEN_CF (minuti potrebbero essere esauriti) const r = await ghDispatch(workflow, inputs, env); return { ok: r.ok, msg: r.msg + '\n⚠️ GH_TOKEN_D assente — usato account A (possibili minuti esauriti)' }; } try { const r = await fetch( `https://api.github.com/repos/arypulka98-bot/AI/actions/workflows/${workflow}/dispatches`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/vnd.github.v3+json', 'User-Agent': 'CF-Pages-TelegramBot/2.0', }, body: JSON.stringify({ ref: 'main', inputs }), signal: AbortSignal.timeout(12_000), } ); if (r.status === 204) return { ok: true, msg: '✅ Relay D: dispatch inviato (204)' }; const body = await r.text().catch(() => ''); return { ok: false, msg: `❌ Relay D: GitHub ${r.status}: ${body.slice(0, 120)}` }; } catch (e) { return { ok: false, msg: `❌ Relay D: Timeout: ${String(e).slice(0, 80)}` }; } } /** Pinga i 3 HF Spaces e restituisce riga di stato ciascuno. */ async function pingHFSpaces(): Promise { const spaces = [ { name: 'BRAIN', url: 'https://arjanit98-terminal.hf.space/health' }, { name: 'HANDS', url: 'https://baida07-ai-backend-collab.hf.space/health' }, { name: 'MEMORY', url: 'https://baida07-ai-memory-backend.hf.space/health' }, ]; const results = await Promise.all(spaces.map(async (s) => { const t0 = Date.now(); try { const r = await fetch(s.url, { signal: AbortSignal.timeout(15_000) }); return `${r.ok ? '✅' : '⚠️'} ${s.name}: ${r.status} (${Date.now() - t0}ms)`; } catch { return `❌ ${s.name}: timeout (${Date.now() - t0}ms)`; } })); return results.join('\n'); } // ── Edge command handlers ───────────────────────────────────────────────────── const HELP_TEXT = '📋 Comandi disponibili:\n\n' + '— Veloci (risposta istantanea) —\n' + '/stato · /s — stato progetto (cache aggiornata ogni 5 min)\n' + '/ping — latenza server\n' + '/aiuto — questo menu\n\n' + '— Avanzati (solo proprietario) —\n' + '/run <obiettivo> — avvia Agent Kernel su GitHub Actions\n' + '/deploy — pubblica aggiornamento CF Pages\n' + '/logs — ultimi run e log di sistema\n' + '/hf — stato spazi HF (BRAIN / HANDS / MEMORY)\n\n' + '💡 Scrivi qualsiasi obiettivo liberamente — lo eseguo senza comandi!'; /** * handleEdgeCommand — gestisce i comandi semplici a CF senza passare da Railway. * @returns true se il comando è stato gestito → Railway NON va chiamato. * false se serve Railway. */ async function handleEdgeCommand( cmd: string, chatId: number, update: Record, env: Env, t0: number ): Promise { switch (cmd) { case '/start': await tgSend( chatId, '🤖 Agente AI — Assistente autonomo per lo sviluppo\n\n' + 'Scrivi il tuo obiettivo in italiano, liberamente.\n' + 'Analizzo, codifico, faccio push e ti aggiorno in tempo reale.\n\n' + '💡 es: «analizza i bug in providers.py» · «autofix degli errori»\n\n' + 'Cosa vuoi fare?', env, { inline_keyboard: [ [{ text: '🚀 Nuovo Task', callback_data: 'agent' }, { text: '📊 Status', callback_data: 'tgw_status' }], [{ text: '🩺 Health', callback_data: 'tgw_health' }, { text: '🔧 AutoFix', callback_data: 'tgw_autofix' }], [{ text: '📊 Bench', callback_data: 'tgw_bench' }, { text: '📋 Tasks', callback_data: 'tgw_tasks' }], [{ text: '🌐 Dashboard', url: 'https://agente-ai.pages.dev' }], ], } ); return true; case '/help': case '/aiuto': case '/menu': await tgSend(chatId, HELP_TEXT, env); return true; case '/ping': await tgSend(chatId, `🏓 Pong! CF Edge: ${Date.now() - t0}ms`, env); return true; case '/stato': case '/s': { // Prova KV (stato cached dal daemon Railway ogni 5 min) if (env.AGENT_KV) { const cached = await env.AGENT_KV.get('stato:latest').catch(() => null); if (cached) { await tgSend(chatId, `⚡ (KV cache)\n${cached}`, env); return true; // gestito — Railway non necessario } } // KV vuoto o non disponibile → Railway gestisce (return false) return false; } case '/run': { const goal = extractArgs(update) || 'analizza stato progetto e aggiorna AGENT_PLAN'; // Relay D (arypulka98-bot/AI) — usa budget D, non A (A esaurito fino al 1 luglio) // Richiede GH_TOKEN_D in CF Pages → Settings → Secrets. await tgSend(chatId, `🚀 Agent Kernel → relay D dispatching...\nGoal: ${goal}`, env); const r = await ghRelayDispatch('agent-kernel-b.yml', { goal, mode: 'execute', commit_memory: 'true' }, env); await tgSend(chatId, r.msg + '\n\n→ Vedi run su relay D', env ); return true; } case '/deploy': { await tgSend(chatId, '🚀 CF Pages Deploy → relay D...', env); // MX11-G4: usa repository_dispatch diretto su relay D (nessun runner account A) const sha = await fetch('https://api.github.com/repos/Baida98/AI/commits/main', { headers: { Authorization: `Bearer ${env.GH_TOKEN_CF ?? ''}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'CF-TGBot/2.0' }, signal: AbortSignal.timeout(8_000), }).then(r => r.json()).then((j: Record) => (j.sha ?? '').slice(0, 12)).catch(() => 'unknown'); const r = await ghRepoDispatch('relay-deploy', { sha, source: 'telegram-deploy', force: 'true' }, env); await tgSend(chatId, r.msg + `\nSHA: ${sha}\n\n→ Vedi run su relay D`, env ); return true; } case '/logs': { await tgSend(chatId, '📊 Recupero ultimi run GHA...', env); const [cf, kb, be, rd] = await Promise.all([ ghLastRun('cloudflare-deploy.yml', env), ghLastRun('agent-kernel-b.yml', env), ghLastRun('backend-deploy.yml', env), ghRelayLastRun('agent-kernel-b.yml', env), ]); await tgSend(chatId, '📊 Ultimi run GHA:\n\n' + '[Baida98/AI]\n' + '— CF Deploy: ' + cf + '\n' + '— Kernel B: ' + kb + '\n' + '— Backend: ' + be + '\n\n' + '[Relay D]\n' + '— Kernel D: ' + rd, env ); return true; } case '/hf': { await tgSend(chatId, '🔍 Ping HF Spaces in corso (~15s)...', env); const status = await pingHFSpaces(); await tgSend(chatId, '🤗 HF Spaces status:\n\n' + status, env); return true; } default: return false; } } // ── Railway dispatch (invariato rispetto a versione precedente) ─────────────── /** * dispatchToBrain — instrada l'update al HF BRAIN Space. * Sostituisce Railway: BRAIN gira 24/7 su HF free tier (no cold-start, no sleep). * Timeout 25s: HF può essere lento ma non si sveglia da zero. * Fallback: BACKEND_URL (Railway legacy) se BACKEND_URL_A non configurato. */ /** URL Spaces verificati live: * BRAIN: arjanit98-terminal.hf.space → /health 200, /api/telegram/process 200 * HANDS: baida07-ai-backend-collab.hf.space → /health 200 * BACKEND_URL_A punta a BRAIN (primario). HANDS come fallback esecuzione. */ const HF_BRAIN_URL = 'https://arjanit98-terminal.hf.space'; const HF_HANDS_URL = 'https://baida07-ai-backend-collab.hf.space'; async function dispatchToBrain(update: unknown, env: Env): Promise { // Prova BRAIN prima, poi HANDS come fallback (entrambi live su HF free tier) const brainUrl = (env.BACKEND_URL_A ?? HF_BRAIN_URL).replace(/\/$/, ''); for (const url of [brainUrl, HF_HANDS_URL]) { try { const res = await fetch(`${url}/api/telegram/process`, { method: 'POST', headers: { 'Content-Type': 'application/json', // BRAIN: accetta senza auth se TELEGRAM_WEBHOOK_SECRET non configurato 'Authorization': `Bearer ${env.INTERNAL_TOKEN ?? ''}`, 'User-Agent': 'CF-Pages-Webhook/2.0', }, body: JSON.stringify(update), signal: AbortSignal.timeout(20_000), }); if (res.ok) return true; } catch { /* prova url successivo */ } } return false; } async function queueToSupabase(update: unknown, env: Env): Promise { const supabaseUrl = (env.SUPABASE_URL ?? '').replace(/\/$/, ''); const supabaseKey = env.SUPABASE_SERVICE_KEY ?? ''; if (!supabaseUrl || !supabaseKey) return; try { await fetch(`${supabaseUrl}/rest/v1/telegram_queue`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': supabaseKey, 'Authorization': `Bearer ${supabaseKey}`, 'Prefer': 'return=minimal', }, body: JSON.stringify({ update_id: (update as Record)?.update_id ?? null, payload: JSON.stringify(update), processed: false, }), }); } catch { /* fire-and-forget */ } } // ── Main handler ────────────────────────────────────────────────────────────── /** true solo se il chatId è nella allowlist owner. */ function isOwner(chatId: number, env: Env): boolean { const list = (env.OWNER_CHAT_IDS ?? '').split(',').map((s) => s.trim()).filter(Boolean); return list.includes(String(chatId)); } export const onRequestPost: PagesFunction = async (ctx) => { const t0 = Date.now(); const secret = ctx.env.TELEGRAM_WEBHOOK_SECRET ?? ''; // 1. Valida secret token header (se configurato) if (secret) { const headerSecret = ctx.request.headers.get('X-Telegram-Bot-Api-Secret-Token') ?? ''; if (headerSecret !== secret) { return new Response('Unauthorized', { status: 401 }); } } // 2. Parse body let update: Record; try { update = await ctx.request.json() as Record; } catch { return new Response('Bad Request', { status: 400 }); } // 3. MX14-E1: Edge filtering — tenta gestione locale prima di Railway const cmd = extractCommand(update); const chatId = extractChatId(update); if (cmd && chatId !== null) { // Sicurezza: Blocca comandi se non è l'owner (eccetto /start e /help per privacy) const PUBLIC_CMDS = new Set(['/start', '/help', '/aiuto', '/menu', '/ping']); if (!PUBLIC_CMDS.has(cmd) && !isOwner(chatId, ctx.env)) { ctx.waitUntil(tgSend(chatId, '⚠️ Accesso Negato. Non sei autorizzato a usare questo comando.', ctx.env)); return new Response('OK', { status: 200 }); } const EDGE_CMDS = new Set(['/start', '/help', '/aiuto', '/menu', '/ping', '/stato', '/s', '/run', '/deploy', '/logs', '/hf']); if (EDGE_CMDS.has(cmd)) { // Edge command: return 200 subito, gestisci in background. // handleEdgeCommand → true: risposta inviata da CF, nessuna chiamata Railway. // handleEdgeCommand → false: KV miss (/stato cache vuota) → Railway gestisce. ctx.waitUntil( handleEdgeCommand(cmd, chatId, update, ctx.env, t0).then((handled) => { if (!handled) return dispatchToBrain(update, ctx.env).then((ok) => { if (!ok) return queueToSupabase(update, ctx.env); }); }) ); return new Response('OK', { status: 200 }); } } // 4. Comando non edge (o non-comando) → Railway come prima ctx.waitUntil( (async () => { const ok = await dispatchToBrain(update, ctx.env); if (!ok) await queueToSupabase(update, ctx.env); })() ); return new Response('OK', { status: 200 }); }; // GET: health check (esteso con info edge) export const onRequestGet: PagesFunction = async () => new Response( JSON.stringify({ ok: true, mode: 'webhook', edge_filter: 'MX14-E1', edge_commands: ['/start', '/help', '/ping', '/stato', '/s', '/run', '/deploy', '/logs', '/hf'], ts: Date.now(), }), { headers: { 'Content-Type': 'application/json' } } ); /** Invia log al Quadrante B (LOGS) in modo asincrono. */ async function logToQuadrantB(event: string, details: any) { try { await fetch(`${QUADRANT_B.url}/rest/v1/agent_logs`, { method: 'POST', headers: { 'apikey': QUADRANT_B.key, 'Authorization': `Bearer ${QUADRANT_B.key}`, 'Content-Type': 'application/json', 'Prefer': 'return=minimal' }, body: JSON.stringify({ event, details, created_at: new Date().toISOString() }) }); } catch (e) { /* silent fail per non bloccare edge */ } } /** Salva un frammento di esperienza nel Quadrante A (BRAIN). */ async function saveExperience(goal: string, outcome: string, env: Env) { try { await fetch(`${QUADRANT_A.url}/rest/v1/semantic_memory`, { method: 'POST', headers: { 'apikey': QUADRANT_A.key, 'Authorization': `Bearer ${QUADRANT_A.key}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ content: `Goal: ${goal} | Outcome: ${outcome}`, metadata: { source: 'manus-ai', ts: new Date().toISOString() } }) }); } catch (e) {} }