Spaces:
Running
Running
| require('dotenv').config(); | |
| const zernioPoster = require('./zernio-poster'); | |
| const postProxyPoster = require('./postproxy-poster'); | |
| const research = require('./research'); | |
| const aiContent = require('./ai-content'); | |
| const imageGen = require('./image-gen'); | |
| const scheduler = require('./scheduler'); | |
| const sheets = require('./sheets'); | |
| const dns = require('node:dns'); | |
| // HF Spaces DNS Bypass (M.O.A.B.) | |
| dns.setDefaultResultOrder('ipv4first'); | |
| dns.setServers(['8.8.8.8', '1.1.1.1', '[2001:4860:4860::8888]']); | |
| const originalLookup = dns.lookup; | |
| dns.lookup = function(hostname, options, callback) { | |
| if (typeof options === 'function') { | |
| callback = options; | |
| options = {}; | |
| } | |
| originalLookup(hostname, options, (err, address, family) => { | |
| if (err && (err.code === 'ENOTFOUND' || err.code === 'EAI_AGAIN')) { | |
| dns.resolve4(hostname, (err2, addresses) => { | |
| if (err2 || !addresses || !addresses.length) return callback(err); | |
| callback(null, addresses[0], 4); | |
| }); | |
| } else { | |
| callback(err, address, family); | |
| } | |
| }); | |
| }; | |
| const express = require('express'); | |
| const cors = require('cors'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| const memory = require('./skills/memory'); | |
| const apiCaller = require('./skills/api_caller'); | |
| const playbookManager = require('./skills/playbook_manager'); | |
| const conversationMemory = require('./skills/conversation_memory'); | |
| const setTelegramMenu = require('./skills/set_telegram_menu'); | |
| const reportGen = require('./skills/report_generator'); | |
| const hfStorage = require('./skills/hf_storage'); | |
| const trelloManager = require('./skills/trello_manager'); | |
| const hfDeployer = require('./skills/hf_deployer'); | |
| const skillCreator = require('./skills/skill_creator'); | |
| const seoWriter = require('./skills/seo_writer'); | |
| const wordpressPublisher = require('./skills/wordpress_publisher'); | |
| const googleIndexer = require('./skills/google_indexer'); | |
| const reportFlow = require('./skills/report_flow'); | |
| const carouselFlow = require('./skills/carousel_flow'); | |
| const sentimentAgent = require('./skills/sentiment_agent'); | |
| const competitorResearch = require('./competitor-research'); | |
| const analyticsEngine = require('./analytics-engine'); | |
| const viralFlow = require('./skills/viral_flow'); | |
| const salesEngine = require('./skills/sales_engine'); | |
| const mayarClient = require('./skills/mayar_client'); | |
| const costTracker = require('./skills/cost_tracker'); | |
| const scoutAgent = require('./skills/scout_agent'); | |
| const videoEngine = require('./skills/video_engine'); | |
| const quantaBridge = require('./skills/quanta_bridge'); | |
| const ceoAgent = require('./skills/ceo_agent'); | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| app.get('/', (req, res) => { | |
| res.status(200).send('VinOS Hugging Face Cloud Engine is RUNNING natively!'); | |
| }); | |
| // ============================================ | |
| // BULK INDEXING DASHBOARD API | |
| // ============================================ | |
| app.get('/indexer', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'indexer.html')); | |
| }); | |
| app.get('/viral', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'viral-dashboard.html')); | |
| }); | |
| app.get('/revenue', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'revenue-dashboard.html')); | |
| }); | |
| app.get('/landing', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'landing.html')); | |
| }); | |
| app.get('/api/landing', (req, res) => { | |
| try { | |
| const dash = salesEngine.getDashboardData(); | |
| const activeOffers = dash.activeOffers.map(o => ({ | |
| title: o.title, description: o.description || '', | |
| priceIDR: o.priceIDR, paymentUrl: o.paymentUrl, pillar: o.pillar || '' | |
| })); | |
| res.json({ success: true, offers: activeOffers }); | |
| } catch (e) { res.json({ success: true, offers: [] }); } | |
| }); | |
| // ============================================ | |
| // QUANTA BRIDGE API (for QUANTA to pull data) | |
| // ============================================ | |
| app.get('/api/scout/:keyword', async (req, res) => { | |
| try { | |
| const result = await scoutAgent.runScout(req.params.keyword); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { res.status(500).json({ error: e.message }); } | |
| }); | |
| // ============================================ | |
| // SKILL API β Authenticated endpoints for Space-to-Space calls | |
| // Used by: CEO Command Center, future external Spaces | |
| // ============================================ | |
| const { authMiddleware } = require('./skills/api_auth'); | |
| const clientCrm = require('./skills/client_crm'); | |
| const visibilityEngine = require('./skills/visibility_engine'); | |
| const researchEngine = require('./skills/research_engine'); | |
| const googleCalendar = require('./skills/google_calendar'); | |
| // --- CRM Skills --- | |
| app.post('/api/skills/crm/list', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.getAllClients() }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/crm/search', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.searchClients(req.body.query || '') }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/crm/followups', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.getFollowups(req.body.daysOverdue || 0) }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/crm/by-stage', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.getByStage(req.body.stage || 'lead') }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/crm/add', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.addClient(req.body.data || {}) }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/crm/update', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.updateClient(req.body.id, req.body.updates || {}) }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/crm/format-detail', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.formatClientDetail(req.body.id) }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/crm/format-pipeline', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: clientCrm.formatPipeline() }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| // --- Visibility Skills --- | |
| app.post('/api/skills/visibility/audit', authMiddleware, async (req, res) => { | |
| try { | |
| const result = await visibilityEngine.runVisibilityAudit(req.body.brand, req.body.options || {}); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/visibility/weekly', authMiddleware, async (req, res) => { | |
| try { | |
| const result = await visibilityEngine.weeklyBrandScan(req.body.brands || []); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| // --- Research Skills --- | |
| app.post('/api/skills/research/light', authMiddleware, async (req, res) => { | |
| try { | |
| const result = await researchEngine.runLightResearch(req.body.topic, req.body.focus); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/skills/research/visibility', authMiddleware, async (req, res) => { | |
| try { | |
| const result = await researchEngine.runVisibilityResearch(req.body.brand); | |
| res.json({ success: true, data: result }); | |
| } catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| // --- Sales & Calendar Skills --- | |
| app.get('/api/skills/sales/dashboard', authMiddleware, (req, res) => { | |
| try { res.json({ success: true, data: salesEngine.getDashboardData() }); } | |
| catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/skills/calendar/events', authMiddleware, async (req, res) => { | |
| try { | |
| if (!googleCalendar.isConnected()) return res.json({ success: true, data: { connected: false, events: [] } }); | |
| const events = await googleCalendar.getUpcomingEvents(null, req.query.days || 7); | |
| res.json({ success: true, data: { connected: true, events } }); | |
| } catch (e) { res.status(500).json({ success: false, error: e.message }); } | |
| }); | |
| // ============================================ | |
| // CEO AGENT API & DASHBOARD (deprecated β migrating to AIgoose/ceo-command-center) | |
| // ============================================ | |
| app.get('/ceo', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'ceo-dashboard.html')); | |
| }); | |
| app.get('/api/ceo/briefing', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.briefing_history?.[0] || null }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/pipeline', (req, res) => { | |
| try { | |
| const crm = require('./skills/client_crm'); | |
| res.json({ success: true, data: crm.getAllClients() }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/calendar', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.content_calendar || {} }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/goals', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.goals || {} }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/visibility', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.visibility || {} }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/funnel', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.digitalfinese_funnel || {} }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/pulse', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.heartpulse || {} }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/meetings', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.meetings || {} }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/hosting', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.hosting_domains || {} }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/ceo/products', (req, res) => { | |
| try { | |
| const db = ceoAgent.readCeoDB(); | |
| res.json({ success: true, data: db.products || [] }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| // ============================================ | |
| // VIDEO ENGINE API (proxy to Remotion Studio) | |
| // ============================================ | |
| app.get('/videos-dashboard', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'videos-dashboard.html')); | |
| }); | |
| app.get('/api/videos/templates', async (req, res) => { | |
| try { | |
| const r = await apiCaller.axiosIPv4.get(`${process.env.REMOTION_URL || 'https://AIgoose-remotion-studio.hf.space'}/api/templates`, { timeout: 10000 }); | |
| res.json(r.data); | |
| } catch (e) { res.json({ templates: [] }); } | |
| }); | |
| app.post('/api/videos/create', async (req, res) => { | |
| try { | |
| const { topic, template, duration } = req.body; | |
| // Generate script via video engine then submit | |
| const chatId = 'web'; | |
| await videoEngine.createVideo(chatId, topic || 'AI Technology', template || 'text-explainer'); | |
| res.json({ success: true, message: 'Video queued. Check the job queue.' }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/videos/jobs', async (req, res) => { | |
| try { | |
| const r = await apiCaller.axiosIPv4.get(`${process.env.REMOTION_URL || 'https://AIgoose-remotion-studio.hf.space'}/api/jobs?limit=${req.query.limit || 20}`, { timeout: 10000 }); | |
| res.json(r.data); | |
| } catch (e) { res.json({ jobs: [] }); } | |
| }); | |
| app.get('/api/videos/download/:id', async (req, res) => { | |
| try { | |
| const remotionUrl = process.env.REMOTION_URL || 'https://AIgoose-remotion-studio.hf.space'; | |
| const r = await apiCaller.axiosIPv4.get(`${remotionUrl}/api/download/${req.params.id}`, { responseType: 'stream', timeout: 60000 }); | |
| res.set('Content-Type', 'video/mp4'); | |
| res.set('Content-Disposition', `attachment; filename="${req.params.id}.mp4"`); | |
| r.data.pipe(res); | |
| } catch (e) { res.status(404).json({ error: 'Video not available' }); } | |
| }); | |
| // ============================================ | |
| // SALES ENGINE API | |
| // ============================================ | |
| app.get('/api/sales/dashboard', (req, res) => { | |
| try { res.json({ success: true, ...salesEngine.getDashboardData() }); } | |
| catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/sales/offers', (req, res) => { | |
| try { | |
| const db = salesEngine.readSalesDB(); | |
| res.json({ success: true, offers: db.offers }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/sales/offers/create', async (req, res) => { | |
| try { | |
| const { topic, priceIDR } = req.body; | |
| if (!topic) return res.json({ success: false, error: 'topic required' }); | |
| const result = await salesEngine.createOfferFromTopic(topic, priceIDR || 49000); | |
| res.json(result); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/sales/offers/:id/pause', (req, res) => { | |
| try { res.json(salesEngine.pauseOffer(req.params.id)); } | |
| catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/sales/offers/:id/retire', (req, res) => { | |
| try { res.json(salesEngine.retireOffer(req.params.id)); } | |
| catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/sales/webhook/mayar', async (req, res) => { | |
| try { | |
| const signature = req.headers['x-mayar-signature'] || req.headers['x-callback-token'] || ''; | |
| if (!mayarClient.verifyWebhook(req.body, signature)) { | |
| console.log('[Webhook] Invalid Mayar signature'); | |
| return res.status(401).json({ error: 'Invalid signature' }); | |
| } | |
| const data = req.body.data || req.body; | |
| const webhookData = { | |
| productId: data.productId || data.product_id || '', | |
| amount: data.amount || data.total || 0, | |
| email: data.email || data.customerEmail || data.customer_email || '', | |
| transactionId: data.id || data.transactionId || data.transaction_id || '' | |
| }; | |
| const result = await salesEngine.recordSale(webhookData); | |
| res.json(result); | |
| } catch (e) { | |
| console.error('[Webhook] Mayar error:', e.message); | |
| res.status(500).json({ success: false, error: e.message }); | |
| } | |
| }); | |
| // Click tracking redirect (for A/B variant attribution) | |
| app.get('/api/track', (req, res) => { | |
| const { offer, variant, action } = req.query; | |
| if (!offer) return res.status(400).json({ error: 'Missing offer param' }); | |
| const redirectUrl = salesEngine.trackClick(offer, variant || 'v0'); | |
| if (redirectUrl) { | |
| const utm = `${redirectUrl.includes('?') ? '&' : '?'}utm_source=track&utm_medium=cta&utm_campaign=${encodeURIComponent(offer)}`; | |
| res.redirect(302, redirectUrl + utm); | |
| } else { | |
| res.status(404).json({ error: 'Offer not found' }); | |
| } | |
| }); | |
| // ============================================ | |
| // VIRAL CONTENT ENGINE API | |
| // ============================================ | |
| app.get('/api/viral/brain', (req, res) => { | |
| try { res.json({ success: true, brain: viralFlow.loadBrain() }); } | |
| catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/viral/topics', (req, res) => { | |
| try { | |
| const limit = parseInt(req.query.limit) || 20; | |
| res.json({ success: true, topics: viralFlow.loadJsonl(viralFlow.TOPICS_FILE, limit) }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/viral/angles', (req, res) => { | |
| try { | |
| const limit = parseInt(req.query.limit) || 10; | |
| res.json({ success: true, angles: viralFlow.loadJsonl(viralFlow.ANGLES_FILE, limit) }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/viral/scripts', (req, res) => { | |
| try { | |
| const limit = parseInt(req.query.limit) || 10; | |
| res.json({ success: true, scripts: viralFlow.loadJsonl(viralFlow.SCRIPTS_FILE, limit) }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/viral/discover', async (req, res) => { | |
| try { | |
| const { query } = req.body; | |
| if (!query) return res.json({ success: false, error: 'query required' }); | |
| const brain = viralFlow.loadBrain(); | |
| if (!brain.icp) return res.json({ success: false, error: 'Brain not configured. Run /viral onboard in Telegram first.' }); | |
| const topics = await viralFlow.runDiscover(query, brain); | |
| res.json({ success: true, topics }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/viral/angle', async (req, res) => { | |
| try { | |
| const { topic } = req.body; | |
| if (!topic) return res.json({ success: false, error: 'topic required' }); | |
| const brain = viralFlow.loadBrain(); | |
| if (!brain.icp) return res.json({ success: false, error: 'Brain not configured. Run /viral onboard first.' }); | |
| const angles = await viralFlow.runAngle(topic, brain); | |
| res.json({ success: true, angles }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/viral/script', async (req, res) => { | |
| try { | |
| const { topic, angle, format } = req.body; | |
| if (!topic) return res.json({ success: false, error: 'topic required' }); | |
| const brain = viralFlow.loadBrain(); | |
| if (!brain.icp) return res.json({ success: false, error: 'Brain not configured. Run /viral onboard first.' }); | |
| const script = await viralFlow.runScript(topic, angle || 'Contrast Formula', format || 'shortform', brain); | |
| res.json({ success: true, script }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.post('/api/viral/publish-to-social', async (req, res) => { | |
| try { | |
| const { scriptId } = req.body; | |
| if (!scriptId) return res.json({ success: false, error: 'scriptId required' }); | |
| const scripts = viralFlow.loadJsonl(viralFlow.SCRIPTS_FILE, 200); | |
| const script = scripts.find(s => s.id === scriptId); | |
| if (!script) return res.json({ success: false, error: 'Script not found' }); | |
| const draft = await scheduler.runInteractiveCycle( | |
| { type: 'topic', value: `${script.topic}: ${script.best_hook || ''}`.trim(), tier: 'fast' }, | |
| { funnelStage: 'TOFU', language: 'both', coreMessage: (script.script || '').substring(0, 500) } | |
| ); | |
| res.json(draft); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| app.get('/api/sites', (req, res) => { | |
| try { | |
| const sites = JSON.parse(process.env.WP_SITES_JSON || '{}'); | |
| res.json({ success: true, sites: Object.keys(sites) }); | |
| } catch (e) { | |
| res.json({ success: false, error: 'WP_SITES_JSON not configured' }); | |
| } | |
| }); | |
| app.get('/api/recent-posts', async (req, res) => { | |
| const { siteId } = req.query; | |
| try { | |
| const sites = JSON.parse(process.env.WP_SITES_JSON || '{}'); | |
| const site = sites[siteId]; | |
| if (!site) return res.json({ success: false, error: 'Unknown siteId' }); | |
| const authBuffer = Buffer.from(`${site.user}:${site.pass}`).toString('base64'); | |
| const axios = require('axios'); | |
| const response = await axios.get(`${site.url}/wp-json/wp/v2/posts?per_page=20&orderby=date&order=desc&_fields=link,title,date,status`, { | |
| headers: { 'Authorization': `Basic ${authBuffer}` } | |
| }); | |
| const posts = response.data.map(p => ({ url: p.link, title: p.title.rendered, date: p.date })); | |
| res.json({ success: true, posts }); | |
| } catch (e) { | |
| res.json({ success: false, error: e.message }); | |
| } | |
| }); | |
| app.get('/api/index-status', async (req, res) => { | |
| const { url } = req.query; | |
| try { | |
| const result = await googleIndexer.getStatus(url); | |
| res.json(result); | |
| } catch (e) { | |
| res.json({ success: false, status: 'UNKNOWN', error: e.message }); | |
| } | |
| }); | |
| const DAILY_QUOTA = 200; | |
| const QUOTA_DELAY_MS = 2000; | |
| let todayQuotaDate = new Date().toDateString(); | |
| let todayQuotaUsed = 0; | |
| app.post('/api/bulk-index', async (req, res) => { | |
| const { urls } = req.body; | |
| if (!urls || !Array.isArray(urls)) return res.json({ success: false, error: 'Invalid payload' }); | |
| // Reset quota counter on new day | |
| if (new Date().toDateString() !== todayQuotaDate) { | |
| todayQuotaDate = new Date().toDateString(); | |
| todayQuotaUsed = 0; | |
| } | |
| const remaining = DAILY_QUOTA - todayQuotaUsed; | |
| const toProcess = urls.slice(0, remaining); | |
| const skipped = urls.length - toProcess.length; | |
| const results = []; | |
| for (const url of toProcess) { | |
| const r = await googleIndexer.submitUrl(url); | |
| todayQuotaUsed++; | |
| results.push({ url, success: r.success, error: r.error || null }); | |
| await new Promise(resolve => setTimeout(resolve, QUOTA_DELAY_MS)); | |
| } | |
| res.json({ success: true, processed: toProcess.length, skipped, quotaUsed: todayQuotaUsed, results }); | |
| }); | |
| app.get('/api/quota', (req, res) => { | |
| if (new Date().toDateString() !== todayQuotaDate) { todayQuotaDate = new Date().toDateString(); todayQuotaUsed = 0; } | |
| res.json({ used: todayQuotaUsed, total: DAILY_QUOTA, remaining: DAILY_QUOTA - todayQuotaUsed }); | |
| }); | |
| // ============================================ | |
| // SOCIAL AUTOPILOT API | |
| // ============================================ | |
| app.get('/social', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'social-dashboard.html')); | |
| }); | |
| app.get('/api/social/status', async (req, res) => { | |
| try { | |
| const result = await zernioPoster.listAccounts(); | |
| const db = scheduler.config; | |
| res.json({ | |
| success: true, | |
| accounts: result.accounts || [], | |
| autopilot: db.autopilot, | |
| postCount: db.posts.length, | |
| quota: db.monthly_quota | |
| }); | |
| } catch (e) { | |
| res.json({ success: false, error: e.message }); | |
| } | |
| }); | |
| app.get('/api/social/posts', (req, res) => { | |
| res.json(scheduler.config.posts); | |
| }); | |
| app.post('/api/social/research', async (req, res) => { | |
| const { url, input, inputType, tier } = req.body; | |
| // Support both legacy { url } and new { input, inputType, tier } formats | |
| if (inputType === 'topic' && input) { | |
| const draft = await scheduler.runContentCycle({ type: 'topic', value: input, tier: tier || 'fast' }); | |
| return res.json({ success: !!draft, draft }); | |
| } | |
| const targetUrl = url || input; | |
| if (!targetUrl) return res.json({ success: false, error: 'URL or topic required' }); | |
| const draft = await scheduler.runContentCycle({ type: 'url', value: targetUrl, tier: tier || 'fast' }); | |
| res.json({ success: !!draft, draft }); | |
| }); | |
| app.post('/api/social/approve', async (req, res) => { | |
| const { postId, variant, scheduleTime, revisedText, platform } = req.body; | |
| if (revisedText) { | |
| const p = scheduler.config.posts.find(x => x.id === postId); | |
| if (p) { | |
| const v = variant || 'v1'; | |
| if (platform && p[v]) { | |
| const fieldMap = { instagram: 'ig_id', threads: 'threads_id', twitter: 'x_id', linkedin: 'linkedin_id', facebook: 'fb_id', pinterest: 'pinterest_id' }; | |
| const field = fieldMap[platform]; | |
| if (field) p[v][field] = revisedText; | |
| } else if (p[v]) { | |
| ['ig_id', 'ig_en', 'x_id', 'threads_id', 'fb_id', 'pinterest_id', 'linkedin_id'].forEach(f => { if (p[v]) p[v][f] = revisedText; }); | |
| } | |
| scheduler.saveDB(); | |
| } | |
| } | |
| const result = await scheduler.approveAndPost(postId, variant || 'v1', scheduleTime); | |
| res.json(result); | |
| }); | |
| app.delete('/api/social/delete/:postId', async (req, res) => { | |
| const { postId } = req.params; | |
| scheduler.config.posts = scheduler.config.posts.filter(p => p.id !== postId); | |
| scheduler.saveDB(); | |
| res.json({ success: true }); | |
| }); | |
| app.post('/api/social/brainstorm', async (req, res) => { | |
| const { topic } = req.body; | |
| const result = await scheduler.brainstorm(topic); | |
| res.json(result); | |
| }); | |
| // Interactive pipeline: generate with TOFU/MOFU/BOFU + language + core message | |
| app.post('/api/social/generate', async (req, res) => { | |
| const { input, inputType, tier, funnelStage, language, coreMessage, slideCount } = req.body; | |
| if (!input) return res.json({ success: false, error: 'Input required' }); | |
| const result = await scheduler.runInteractiveCycle( | |
| { type: inputType || 'topic', value: input, tier: tier || 'fast' }, | |
| { funnelStage: funnelStage || 'TOFU', language: language || 'both', coreMessage: coreMessage || '', slideCount: slideCount || 7 } | |
| ); | |
| res.json(result); | |
| }); | |
| app.post('/api/social/autopilot', (req, res) => { | |
| const { enabled } = req.body; | |
| scheduler.config.autopilot = !!enabled; | |
| scheduler.saveDB(); | |
| res.json({ success: true, autopilot: !!enabled }); | |
| }); | |
| app.get('/api/social/connect/:platform', async (req, res) => { | |
| const { platform } = req.params; | |
| const postProxyPlatforms = ['twitter', 'linkedin', 'x']; | |
| // LinkedIn and X are handled by PostProxy (manual connection via their dashboard) | |
| if (postProxyPlatforms.includes(platform.toLowerCase())) { | |
| const dashUrl = process.env.POSTPROXY_DASHBOARD_URL || 'https://app.postproxy.io/accounts'; | |
| return res.json({ success: true, authUrl: dashUrl, provider: 'postproxy', platform }); | |
| } | |
| // Zernio: IG, Threads, Facebook, Pinterest | |
| try { | |
| const profileRes = await zernioPoster.getProfile(); | |
| if (!profileRes.success) return res.json({ success: false, error: 'No Zernio profile found. Ensure ZERNIO_API_KEY is set.' }); | |
| const urlRes = await zernioPoster.getConnectUrl(platform, profileRes.profile._id); | |
| res.json(urlRes); | |
| } catch (e) { | |
| res.json({ success: false, error: e.message }); | |
| } | |
| }); | |
| app.get('/api/social/init-sheets', async (req, res) => { | |
| const r = await sheets.initializeSheet(); | |
| res.json(r); | |
| }); | |
| app.get('/api/social/postproxy-profiles', async (req, res) => { | |
| const r = await postProxyPoster.getProfiles(); | |
| res.json(r); | |
| }); | |
| // ============================================ | |
| // SOCIAL AUTOPILOT β ENHANCED API ENDPOINTS | |
| // ============================================ | |
| // Social settings (model, pulse config) | |
| app.get('/api/social/settings', (req, res) => { | |
| const db = memory.readDB(); | |
| const currentModel = db.user_profile_snapshot?.social_content_model || apiCaller.SOCIAL_MODELS.FREE; | |
| const tierMap = { | |
| [apiCaller.SOCIAL_MODELS.PREMIUM]: { tier: 'premium', name: 'Claude Haiku 4.5', cost: '$1.00/$5.00' }, | |
| [apiCaller.SOCIAL_MODELS.STANDARD]: { tier: 'standard', name: 'Gemini 2.5 Flash Lite', cost: '$0.10/$0.40' }, | |
| [apiCaller.SOCIAL_MODELS.FREE]: { tier: 'free', name: 'Llama 3.3 70B', cost: 'FREE' } | |
| }; | |
| const info = tierMap[currentModel] || tierMap[apiCaller.SOCIAL_MODELS.FREE]; | |
| res.json({ success: true, model: { id: currentModel, ...info }, pulseConfig: db.pulse_config || null }); | |
| }); | |
| app.post('/api/social/settings', (req, res) => { | |
| const { socialModel, pulseConfig } = req.body; | |
| const db = memory.readDB(); | |
| if (!db.user_profile_snapshot) db.user_profile_snapshot = {}; | |
| if (socialModel) { | |
| const valid = [apiCaller.SOCIAL_MODELS.PREMIUM, apiCaller.SOCIAL_MODELS.STANDARD, apiCaller.SOCIAL_MODELS.FREE]; | |
| if (valid.includes(socialModel)) { | |
| db.user_profile_snapshot.social_content_model = socialModel; | |
| } | |
| } | |
| if (pulseConfig) { | |
| db.pulse_config = { ...(db.pulse_config || {}), ...pulseConfig }; | |
| } | |
| memory.writeDB(db); | |
| res.json({ success: true }); | |
| }); | |
| // Per-channel quota status | |
| app.get('/api/social/quota', (req, res) => { | |
| res.json({ success: true, quota: scheduler.config.monthly_quota }); | |
| }); | |
| // Account stats (Zernio + PostProxy combined) | |
| app.get('/api/social/account-stats', async (req, res) => { | |
| try { | |
| const [zernioRes, ppRes] = await Promise.allSettled([ | |
| zernioPoster.listAccounts(), | |
| postProxyPoster.getProfiles() | |
| ]); | |
| const accounts = []; | |
| if (zernioRes.status === 'fulfilled' && zernioRes.value.success) { | |
| for (const a of zernioRes.value.accounts || []) { | |
| accounts.push({ platform: a.platform, username: a.username, provider: 'zernio', id: a._id, ...a }); | |
| } | |
| } | |
| if (ppRes.status === 'fulfilled' && ppRes.value.success) { | |
| for (const p of ppRes.value.profiles || []) { | |
| accounts.push({ platform: p.platform, username: p.name || p.id, provider: 'postproxy', id: p.id, ...p }); | |
| } | |
| } | |
| res.json({ success: true, accounts }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| // Recent posts performance | |
| app.get('/api/social/analytics/recent/:count', (req, res) => { | |
| const count = Math.min(parseInt(req.params.count) || 12, 50); | |
| res.json({ success: true, posts: scheduler.getRecentPerformance(count) }); | |
| }); | |
| // Manual analytics sync | |
| app.post('/api/social/analytics/sync', async (req, res) => { | |
| try { | |
| await scheduler.syncStatusAndAnalytics(); | |
| res.json({ success: true, message: 'Analytics synced' }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| // Per-channel posting | |
| app.post('/api/social/pipeline/post/:postId', async (req, res) => { | |
| const { postId } = req.params; | |
| const { channels, variant, scheduleTime } = req.body; | |
| if (!channels || !Array.isArray(channels) || channels.length === 0) { | |
| return res.json({ success: false, error: 'channels[] required' }); | |
| } | |
| const result = await scheduler.postToChannel(postId, channels, variant || 'v1', scheduleTime); | |
| res.json(result); | |
| }); | |
| // MiroFish sentiment pre-flight for post titles | |
| app.post('/api/social/pipeline/sentiment/:postId', async (req, res) => { | |
| try { | |
| const post = scheduler.config.posts.find(p => p.id === req.params.postId); | |
| if (!post) return res.json({ success: false, error: 'Post not found' }); | |
| const { variant } = req.body; | |
| const v = post[variant || 'v1']; | |
| if (!v) return res.json({ success: false, error: 'Variant not found' }); | |
| const titles = [v.title, v.ig_en?.substring(0, 125), v.threads_en?.substring(0, 200)].filter(Boolean); | |
| const result = await sentimentAgent.quickPostPreFlight(titles); | |
| // Store sentiment results on post | |
| post.sentiment = result; | |
| scheduler.saveDB(); | |
| res.json({ success: true, sentiment: result }); | |
| } catch (e) { res.json({ success: false, error: e.message }); } | |
| }); | |
| // Competitor research | |
| app.post('/api/social/competitor-research', async (req, res) => { | |
| const { url, topic, count } = req.body; | |
| const result = await competitorResearch.analyzeCompetitors({ url, topic, count: count || 10 }); | |
| res.json(result); | |
| }); | |
| app.get('/api/social/competitor-insights', (req, res) => { | |
| const insights = competitorResearch.getCachedInsights(); | |
| res.json({ success: true, insights }); | |
| }); | |
| // Analytics engine | |
| app.get('/api/social/analytics/dashboard', (req, res) => { | |
| res.json({ success: true, ...analyticsEngine.getDashboardData() }); | |
| }); | |
| app.get('/api/social/analytics/insights', (req, res) => { | |
| res.json({ success: true, insights: analyticsEngine.getInsightsForAI() }); | |
| }); | |
| app.post('/api/social/analytics/pull', async (req, res) => { | |
| const result = await analyticsEngine.pullDailyAnalytics(); | |
| res.json(result); | |
| }); | |
| // Download post media as zip | |
| app.get('/api/social/download/:postId/zip', async (req, res) => { | |
| try { | |
| const post = scheduler.config.posts.find(p => p.id === req.params.postId); | |
| if (!post) return res.status(404).json({ success: false, error: 'Post not found' }); | |
| const archiver = require('archiver'); | |
| const archive = archiver('zip', { zlib: { level: 5 } }); | |
| res.attachment(`${post.id}_media.zip`); | |
| archive.pipe(res); | |
| // Collect media URLs and add as remote files | |
| const mediaUrls = []; | |
| if (post.type === 'carousel' && post.mediaUrls) { | |
| mediaUrls.push(...post.mediaUrls); | |
| } else if (post.v1?.mediaUrl) { | |
| mediaUrls.push(post.v1.mediaUrl); | |
| } | |
| // Add content text files for each platform | |
| const variants = ['v1', 'v2', 'v3']; | |
| for (const vk of variants) { | |
| const v = post[vk]; | |
| if (!v) continue; | |
| let content = `Title: ${v.title || ''}\n\n`; | |
| content += `--- Instagram ---\n${v.ig_id || v.ig_en || ''}\n\n`; | |
| content += `--- Threads ---\n${v.threads_id || v.threads_en || ''}\n\n`; | |
| content += `--- Twitter/X ---\n${v.x_id || v.x_en || ''}\n\n`; | |
| content += `--- LinkedIn ---\n${v.linkedin_id || v.linkedin_en || ''}\n\n`; | |
| content += `--- Facebook ---\n${v.fb_id || v.fb_en || ''}\n\n`; | |
| content += `--- Pinterest ---\n${v.pinterest_id || v.pinterest_en || ''}\n`; | |
| if (v.hashtags) content += `\n--- Hashtags ---\n${v.hashtags.join(' ')}\n`; | |
| archive.append(content, { name: `${vk}_content.txt` }); | |
| } | |
| // For media, add URLs as a reference file (can't stream remote URLs in HF easily) | |
| if (mediaUrls.length > 0) { | |
| archive.append(mediaUrls.join('\n'), { name: 'media_urls.txt' }); | |
| } | |
| await archive.finalize(); | |
| } catch (e) { | |
| if (!res.headersSent) res.status(500).json({ success: false, error: e.message }); | |
| } | |
| }); | |
| // Download post as branded text PDF | |
| app.get('/api/social/download/:postId/pdf', (req, res) => { | |
| try { | |
| const post = scheduler.config.posts.find(p => p.id === req.params.postId); | |
| if (!post) return res.status(404).json({ success: false, error: 'Post not found' }); | |
| const PDFDocument = require('pdfkit'); | |
| const doc = new PDFDocument({ size: 'A4', margin: 50 }); | |
| res.setHeader('Content-Type', 'application/pdf'); | |
| res.setHeader('Content-Disposition', `attachment; filename="${post.id}_content.pdf"`); | |
| doc.pipe(res); | |
| // Brand header | |
| doc.rect(0, 0, doc.page.width, 80).fill('#071330'); | |
| doc.fontSize(24).fillColor('#f7cb2d').text('@deeferdinand', 50, 25); | |
| doc.fontSize(10).fillColor('#ffffff').text(`Social Content Draft β ${post.funnelStage || 'TOFU'} | ${new Date(post.ts).toLocaleDateString()}`, 50, 55); | |
| doc.moveDown(3); | |
| const variants = ['v1', 'v2', 'v3']; | |
| const variantLabels = { v1: 'Variant 1: Hook Post', v2: 'Variant 2: Carousel', v3: 'Variant 3: Story/Contrarian' }; | |
| for (const vk of variants) { | |
| const v = post[vk]; | |
| if (!v) continue; | |
| doc.fillColor('#071330').fontSize(16).text(variantLabels[vk], { underline: true }); | |
| doc.moveDown(0.3); | |
| if (v.title) doc.fontSize(12).fillColor('#333').text(`Title: ${v.title}`); | |
| if (v.pillar) doc.fontSize(10).fillColor('#666').text(`Pillar: ${v.pillar}`); | |
| doc.moveDown(0.5); | |
| // Carousel slides | |
| if (v.slides && Array.isArray(v.slides)) { | |
| doc.fontSize(11).fillColor('#071330').text('Carousel Slides:', { underline: true }); | |
| for (const slide of v.slides) { | |
| doc.moveDown(0.2); | |
| const label = slide.type ? `Slide ${slide.slide} (${slide.type})` : `Slide ${slide.slide}`; | |
| doc.fontSize(10).fillColor('#333').text(label, { continued: false }); | |
| if (slide.text_en) doc.fontSize(9).fillColor('#555').text(`EN: ${slide.text_en}`); | |
| if (slide.text_id) doc.fontSize(9).fillColor('#555').text(`ID: ${slide.text_id}`); | |
| } | |
| doc.moveDown(0.5); | |
| } | |
| // Platform captions | |
| const platforms = [ | |
| ['Instagram', v.ig_en, v.ig_id], | |
| ['Threads', v.threads_en, v.threads_id], | |
| ['LinkedIn', v.linkedin_en, v.linkedin_id], | |
| ['X/Twitter', v.x_en, v.x_id], | |
| ['Facebook', v.fb_en, v.fb_id], | |
| ['Pinterest', v.pinterest_en, v.pinterest_id] | |
| ]; | |
| for (const [name, en, id] of platforms) { | |
| if (en || id) { | |
| doc.fontSize(10).fillColor('#071330').text(`${name}:`); | |
| if (en) doc.fontSize(9).fillColor('#444').text(`EN: ${en.substring(0, 500)}`); | |
| if (id) doc.fontSize(9).fillColor('#444').text(`ID: ${id.substring(0, 500)}`); | |
| doc.moveDown(0.3); | |
| } | |
| } | |
| if (v.hashtags) doc.fontSize(9).fillColor('#666').text(`Hashtags: ${v.hashtags.join(' ')}`); | |
| if (v.best_time_to_post) doc.fontSize(9).fillColor('#666').text(`Best time: ${v.best_time_to_post}`); | |
| doc.moveDown(1); | |
| if (vk !== 'v3') doc.addPage(); | |
| } | |
| doc.end(); | |
| } catch (e) { | |
| if (!res.headersSent) res.status(500).json({ success: false, error: e.message }); | |
| } | |
| }); | |
| // Visual PDF β renders carousel slides as images then combines into PDF | |
| app.get('/api/social/download/:postId/visual-pdf', async (req, res) => { | |
| try { | |
| const post = (scheduler.config.posts || []).find(p => p.id === req.params.postId); | |
| if (!post) return res.status(404).json({ success: false, error: 'Post not found' }); | |
| const v = post.v1; | |
| if (!v) return res.status(400).json({ success: false, error: 'No v1 variant found' }); | |
| const carouselGen = require('./carousel-gen'); | |
| const PDFDocument = require('pdfkit'); | |
| // Build slides from post content | |
| const slides = []; | |
| if (v.slides && Array.isArray(v.slides)) { | |
| v.slides.forEach((s, i) => { | |
| slides.push({ | |
| slide: i + 1, | |
| title: s.text_en || s.title || s.hook || '', | |
| subtitle: s.text_id || s.subtitle || '', | |
| type: s.type || (i === 0 ? 'HOOK' : ''), | |
| page: `${i + 1} / ${v.slides.length}` | |
| }); | |
| }); | |
| } else { | |
| slides.push({ | |
| slide: 1, | |
| title: v.title || v.ig_en?.substring(0, 80) || 'Untitled', | |
| subtitle: v.ig_id?.substring(0, 120) || '', | |
| type: 'HOOK', | |
| page: '1 / 1' | |
| }); | |
| } | |
| const imagePaths = await carouselGen.generateSlides(slides, post.id, 'dark'); | |
| const doc = new PDFDocument({ size: [1080, 1350], margin: 0 }); | |
| res.setHeader('Content-Type', 'application/pdf'); | |
| res.setHeader('Content-Disposition', `attachment; filename="${post.id}_visual.pdf"`); | |
| doc.pipe(res); | |
| for (let i = 0; i < imagePaths.length; i++) { | |
| if (i > 0) doc.addPage({ size: [1080, 1350], margin: 0 }); | |
| doc.image(imagePaths[i], 0, 0, { width: 1080, height: 1350 }); | |
| } | |
| doc.end(); | |
| } catch (e) { | |
| if (!res.headersSent) res.status(500).json({ success: false, error: e.message }); | |
| } | |
| }); | |
| // ============================================ | |
| // MIROFISH SENTIMENT DASHBOARD API | |
| // ============================================ | |
| app.get('/sentiment', (req, res) => { | |
| res.sendFile(path.join(__dirname, 'public', 'sentiment-dashboard.html')); | |
| }); | |
| app.get('/api/sentiment/history', (req, res) => { | |
| try { | |
| const history = sentimentAgent.getHistory(); | |
| res.json({ success: true, history }); | |
| } catch (e) { | |
| res.json({ success: false, error: e.message }); | |
| } | |
| }); | |
| app.post('/api/sentiment/run', async (req, res) => { | |
| const { topic, country, language, angle } = req.body; | |
| if (!topic) return res.json({ success: false, error: 'Topic is required' }); | |
| const c = country || 'Indonesia'; | |
| const l = language || 'id'; | |
| const a = angle || 'sentiment'; | |
| try { | |
| // Step 1: Quick analysis | |
| const result = await sentimentAgent.quickAnalysis({ topic, country: c, language: l, angle: a }); | |
| if (!result.success) return res.json(result); | |
| // Step 2: Enhanced analysis + MiroFish data in parallel | |
| const [enhancedResult, miroFishData] = await Promise.allSettled([ | |
| sentimentAgent.generateEnhancedAnalysis({ topic, country: c, language: l, angle: a, quickSummary: result.analysis }), | |
| sentimentAgent.fetchMiroFishData(null) | |
| ]); | |
| const enhancedText = enhancedResult.status === 'fulfilled' && enhancedResult.value.success | |
| ? enhancedResult.value.analysis : null; | |
| const mfData = miroFishData.status === 'fulfilled' ? miroFishData.value : null; | |
| // Step 3: Generate enhanced PDF | |
| const pdfResult = await sentimentAgent.generateEnhancedPDF({ | |
| topic, country: c, language: l, angle: a, | |
| quickAnalysis: result.analysis, | |
| enhancedAnalysis: enhancedText, | |
| miroFishData: mfData | |
| }); | |
| // Save to history | |
| sentimentAgent.saveToHistory({ | |
| topic, country: c, language: l, angle: a, | |
| mode: enhancedText ? 'enhanced' : 'quick', | |
| analysis: enhancedText || result.analysis, | |
| pdfFileName: pdfResult.success ? pdfResult.fileName : null | |
| }); | |
| res.json({ | |
| success: true, | |
| analysis: enhancedText || result.analysis, | |
| quickSummary: result.analysis, | |
| pdf: pdfResult.success ? { fileName: pdfResult.fileName } : null, | |
| hasMiroFishData: !!mfData | |
| }); | |
| } catch (e) { | |
| res.json({ success: false, error: e.message }); | |
| } | |
| }); | |
| app.get('/api/sentiment/report/:fileName', (req, res) => { | |
| const fileName = req.params.fileName; | |
| if (fileName.includes('..') || fileName.includes('/') || fileName.includes('\\')) { | |
| return res.status(400).json({ error: 'Invalid filename' }); | |
| } | |
| const filePath = path.join(__dirname, 'database', 'reports', fileName); | |
| if (!require('fs').existsSync(filePath)) { | |
| return res.status(404).json({ error: 'Report not found' }); | |
| } | |
| res.download(filePath); | |
| }); | |
| // ============================================ | |
| app.use(express.static(path.join(__dirname, 'public'))); | |
| // API Routes | |
| app.get('/api/profile', (req, res) => { | |
| const db = memory.readDB(); | |
| res.json(db.user_profile_snapshot); | |
| }); | |
| app.get('/api/sprints', (req, res) => { | |
| const db = memory.readDB(); | |
| res.json(db.active_sprints || []); | |
| }); | |
| app.get('/api/status', (req, res) => { | |
| const db = memory.readDB(); | |
| const status = { | |
| health: { | |
| openrouter: !!process.env.OPENROUTER_API_KEY, | |
| groq: !!process.env.GROQ_API_KEY, | |
| telegram: !!process.env.TELEGRAM_BOT_TOKEN, | |
| mayar: !!process.env.MAYAR_API_KEY, | |
| whop: !!process.env.WHOP_API_KEY, | |
| apify: !!process.env.APIFY_API_KEY | |
| }, | |
| costs: db.costs || { total: 0, by_model: {}, log: [] }, | |
| stats: { | |
| experiments: (db.active_sprints || []).length, | |
| offers: (db.playbooks || []).length, | |
| reach: `${(scheduler.config.posts || []).filter(p => p.status === 'posted' || p.channel_posts).length} posts`, | |
| viralTopics: viralFlow.loadJsonl(viralFlow.TOPICS_FILE).length, | |
| viralScripts: viralFlow.loadJsonl(viralFlow.SCRIPTS_FILE).length, | |
| socialDrafts: (scheduler.config.posts || []).length | |
| }, | |
| revenue: salesEngine.getDashboardData().revenue | |
| }; | |
| res.json(status); | |
| }); | |
| const dailyPulse = require('./use_cases/daily_pulse'); | |
| const offerArchitect = require('./use_cases/offer_architect'); | |
| app.post('/api/generate-image', async (req, res) => { | |
| const { prompt } = req.body; | |
| if (!prompt) return res.status(400).json({ error: "Prompt is required" }); | |
| const result = await apiCaller.generateImage(prompt); | |
| if (result.success) { | |
| const base64 = result.buffer.toString('base64'); | |
| res.json({ success: true, image_base64: base64, source: result.source }); | |
| } else { | |
| res.json(result); | |
| } | |
| }); | |
| app.post('/api/usecase/pulse', async (req, res) => { | |
| const result = await dailyPulse(); | |
| res.json(result); | |
| }); | |
| app.post('/api/usecase/offer', async (req, res) => { | |
| const { topic } = req.body; | |
| if (!topic) return res.status(400).json({ error: "Topic is required" }); | |
| const result = await offerArchitect(topic); | |
| res.json(result); | |
| }); | |
| // Daily Check-in Mechanism | |
| const DAILY_INTERVAL = 24 * 60 * 60 * 1000; | |
| const chatID = process.env.TELEGRAM_CHAT_ID; | |
| const botToken = process.env.TELEGRAM_BOT_TOKEN; | |
| if (chatID && botToken) { | |
| console.log(`Telegram Bot Active for Chat ID: ${chatID}`); | |
| setInterval(async () => { | |
| console.log("Triggering daily check-in..."); | |
| await apiCaller.sendTelegramMessage(chatID, "<b>VinOS Daily Check-in</b>\nHow is the Token Gashapon project coming along? Ready for the next arbitrage move?"); | |
| }, DAILY_INTERVAL); | |
| } | |
| // Webhook ingestion | |
| app.post('/api/webhook', (req, res) => { | |
| const data = req.body; | |
| console.log("General Webhook received:", data); | |
| res.status(200).send({ status: 'success' }); | |
| }); | |
| // SSE clients list for real-time push | |
| const sseClients = []; | |
| app.get('/api/telegram-stream', (req, res) => { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| sseClients.push(res); | |
| req.on('close', () => sseClients.splice(sseClients.indexOf(res), 1)); | |
| }); | |
| const broadcastSSE = (data) => sseClients.forEach(c => c.write(`data: ${JSON.stringify(data)}\n\n`)); | |
| app.get('/api/telegram-messages', (req, res) => { | |
| const db = memory.readDB(); | |
| res.json(db.telegram_log || []); | |
| }); | |
| app.post('/api/send-telegram', async (req, res) => { | |
| const { message } = req.body; | |
| if (!message) return res.status(400).json({ error: 'Message required' }); | |
| const chatId = process.env.TELEGRAM_CHAT_ID; | |
| const result = await apiCaller.sendTelegramMessage(chatId, `<b>[VinOS Dashboard]</b> ${message}`); | |
| broadcastSSE({ direction: 'OUT', chatId, text: `[VinOS Dashboard]: ${message}`, ts: new Date().toISOString() }); | |
| res.json(result); | |
| }); | |
| const voiceTranscriber = require('./skills/voice_transcriber'); | |
| const intentRouter = require('./skills/intent_router'); | |
| const autoCron = require('./skills/infinite_money_cron'); | |
| const responseRouter = require('./skills/response_router'); | |
| // Helper to handle resolved intents | |
| async function handleVinIntent(chatId, from, userText, confirmed = false) { | |
| const db = memory.readDB(); | |
| const autoMode = db.user_profile_snapshot?.automatic_mode || false; | |
| // 1. Resolve intent | |
| const { intent, params } = await intentRouter.resolveIntent(userText); | |
| if (!confirmed && !autoMode && (intent === 'gash' || intent === 'pulse' || intent === 'offer')) { | |
| if (!db.pending_commands) db.pending_commands = {}; | |
| db.pending_commands[chatId] = { intent, params, userText, ts: Date.now() }; | |
| memory.writeDB(db); | |
| const confirmationMsg = `π€ <b>Intent:</b> <i>${intent}</i>\n<b>Params:</b> <i>${params || 'none'}</i>\n\nShould I execute? (Reply: <b>Confirm</b> / <b>Cancel</b>)\n<i>Tip: Use /auto on for speed mode.</i>`; | |
| return await apiCaller.sendTelegramMessage(chatId, confirmationMsg); | |
| } | |
| // 2. Route to appropriate skill | |
| switch (intent) { | |
| case 'remember': | |
| await apiCaller.sendTelegramMessage(chatId, "π§ <i>Designing playbook for:</i> " + params); | |
| const playbookPrompt = `User wants me to remember this instructions or insight: "${params}"\nCreate a structured playbook including: 1. Trigger, 2. Solution, 3. Tags. Format as JSON. Respond ONLY with JSON.`; | |
| const pbGen = await apiCaller.callOpenRouter([{ role: "user", content: playbookPrompt }]); | |
| if (pbGen.success) { | |
| try { | |
| const pbData = JSON.parse(pbGen.data.replace(/```json|```/g, '')); | |
| playbookManager.savePlaybook(pbData.trigger, pbData.solution, pbData.tags); | |
| const pbId = `pb_${Date.now()}`; | |
| await hfStorage.saveRecord('playbooks', pbId, { | |
| title: `Playbook: ${pbData.trigger}`, | |
| timestamp: new Date().toISOString(), | |
| niche: pbData.tags?.[0] || "General" | |
| }, `### TRIGGER: ${pbData.trigger}\n\n### SOLUTION:\n${pbData.solution}\n\n### TAGS:\n${(pbData.tags || []).join(', ')}`); | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Playbook Saved to HF Knowledge Bank.</b>`); | |
| } catch (e) { | |
| await apiCaller.sendTelegramMessage(chatId, "β Failed to parse JSON. Saving raw instead."); | |
| playbookManager.savePlaybook(params, "Manual note", ["raw"]); | |
| } | |
| } | |
| break; | |
| case 'recall': | |
| const playbooks = playbookManager.searchPlaybooks(params); | |
| if (playbooks.length > 0) { | |
| const results = playbooks.slice(0, 3).map(pb => `π <b>${pb.trigger}</b>\n${pb.solution}`).join('\n\n'); | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Found:</b>\n\n${results}`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `π€· No playbooks found for "${params}".`); | |
| } | |
| break; | |
| case 'gash': | |
| const gashPrompt = params || userText; | |
| await apiCaller.sendTelegramMessage(chatId, "π¨ <i>Creating your image...</i>"); | |
| const gResult = await apiCaller.generateImage(gashPrompt); | |
| if (gResult.success && gResult.buffer) { | |
| // Send as photo via Telegram | |
| const FormData = require('form-data'); | |
| const gashForm = new FormData(); | |
| gashForm.append('chat_id', chatId); | |
| gashForm.append('photo', gResult.buffer, { filename: 'gash.jpg', contentType: 'image/jpeg' }); | |
| // Save image to HF + get link | |
| let hfLinkIntent = ''; | |
| try { | |
| const hfRes = await hfStorage.saveFile(`images/gash_${Date.now()}.jpg`, gResult.buffer, 'VinOS: Image gen'); | |
| if (hfRes.success) hfLinkIntent = `\nπ <a href="${hfRes.url}">View on HF</a>`; | |
| } catch (e) { console.error('[HF Auto] Image save failed:', e.message); } | |
| gashForm.append('caption', `β¨ <b>Image Created!</b>\n<i>${gashPrompt.substring(0, 100)}</i>\nπ§ Source: ${gResult.source}${hfLinkIntent}`); | |
| gashForm.append('parse_mode', 'HTML'); | |
| try { | |
| await apiCaller.axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendPhoto`, gashForm, { headers: gashForm.getHeaders() }); | |
| } catch (photoErr) { | |
| console.error("[Gash] Photo upload error:", photoErr.response?.data || photoErr.message); | |
| await apiCaller.sendTelegramMessage(chatId, "β¨ Image generated but couldn't send as photo. Source: " + gResult.source); | |
| } | |
| // Log to Trello | |
| try { | |
| await trelloManager.logImageGen(gashPrompt, gResult.source, hfLinkIntent ? hfLinkIntent.match(/href="([^"]+)"/)?.[1] : ''); | |
| } catch (e) { console.error('[Trello] Image log failed:', e.message); } | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, "β Image generation failed: " + (gResult.error || 'Unknown error')); | |
| } | |
| break; | |
| case 'pulse': | |
| await apiCaller.sendTelegramMessage(chatId, "π <i>Scanning market...</i>"); | |
| await dailyPulse(); | |
| break; | |
| case 'offer': | |
| const topic = params || userText; | |
| await apiCaller.sendTelegramMessage(chatId, `π‘ <i>Architecting offer for:</i> ${topic}`); | |
| await offerArchitect(topic); | |
| break; | |
| default: | |
| await processOrchestratorIntent(chatId, intent, userText, params); | |
| break; | |
| } | |
| } | |
| // Orchestrator handler | |
| async function processOrchestratorIntent(chatId, intent, userText, params, inputModality = 'text') { | |
| const history = conversationMemory.getHistory(chatId); | |
| const persona = fs.readFileSync(path.join(__dirname, 'prompts/vin_personality.md'), 'utf8'); | |
| const relatedPlaybooks = playbookManager.searchPlaybooks(userText).slice(0, 1); | |
| let playbookContext = ""; | |
| if (relatedPlaybooks.length > 0) { | |
| playbookContext = `\n\n# Relevant Playbook Found:\nTrigger: ${relatedPlaybooks[0].trigger}\nSolution: ${relatedPlaybooks[0].solution}`; | |
| } | |
| const messages = [ | |
| { role: "system", content: persona + playbookContext + `\n\n[System Note: Intent: ${intent}]` }, | |
| ...history, | |
| { role: "user", content: userText } | |
| ]; | |
| conversationMemory.addMessage(chatId, "user", userText); | |
| const chatResult = await apiCaller.callOpenRouter(messages); | |
| if (chatResult.success) { | |
| conversationMemory.addMessage(chatId, "assistant", chatResult.data); | |
| await responseRouter.route({ chatId, userText, inputModality, intent, responseText: chatResult.data }); | |
| // Auto-push significant conversations to HF | |
| const history = conversationMemory.getHistory(chatId) || []; | |
| if (userText.length > 200 || history.length % 10 === 0) { | |
| try { | |
| await hfStorage.saveRecord('conversations', `convo_${chatId}_${Date.now()}`, { | |
| title: `Chat: ${userText.substring(0, 50)}`, | |
| timestamp: new Date().toISOString(), | |
| niche: 'Conversation' | |
| }, `**User:** ${userText}\n\n**Vin:** ${chatResult.data}`); | |
| } catch (e) { | |
| console.error('[HF Auto] Conversation push failed:', e.message); | |
| } | |
| } | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, "I'm having trouble thinking clearly. Try again."); | |
| } | |
| } | |
| // --- Helper: handle /write context reply --- | |
| async function _handleWrite(chatId, pending, userContext) { | |
| const { targetSiteId, topic, size, pov, intent, tone } = pending; | |
| const initRes = await apiCaller.sendTelegramMessage(chatId, `π <b>SEO Engine Pipeline</b>\n[β β‘β‘β‘β‘β‘] 16% AI Architecting: "${topic}"...`); | |
| const msgId = initRes.messageId; | |
| try { | |
| const articleRes = await seoWriter.generateArticle(size, pov, intent, tone, topic, userContext); | |
| if (!articleRes.success) { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `β οΈ <b>Pipeline Failed</b>\nβ Writer Error: ${articleRes.error}`); | |
| return; | |
| } | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β‘β‘β‘β‘] 33% QC: Scoring EEAT Quality...`); | |
| const scoreRes = await seoWriter.scoreEEAT(articleRes.html); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β‘β‘β‘] 50% Image Engine: Generating ${articleRes.imagePlaceholders?.length || 0} AI Images...`); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β‘β‘] 66% Publishing: Uploading to [${targetSiteId}]...`); | |
| const pubRes = await wordpressPublisher.publishPost(articleRes, targetSiteId); | |
| if (!pubRes.success) { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `β οΈ <b>Pipeline Failed</b>\nβ WordPress Error: ${pubRes.error}`); | |
| return; | |
| } | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β β‘] 83% SEO Indexing: Google Search Console...`); | |
| const idxRes = await googleIndexer.submitUrl(pubRes.url); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β β ] 100% CRM Sync: Trello Workflow...`); | |
| const trelloRes = await trelloManager.logSeoPost(topic, targetSiteId || 'Default', pubRes.url, scoreRes.score || 'N/A'); | |
| let finalMsg = `π <b>Pipeline Complete!</b>\n\n`; | |
| finalMsg += `<b>π Article:</b> ${pubRes.url}\n`; | |
| if (scoreRes.success) finalMsg += `<b>π§ EEAT Score:</b> ${scoreRes.score}/100\n`; | |
| if (idxRes.success) finalMsg += `<b>π Google:</b> Indexed π’\n`; | |
| else finalMsg += `<b>π Google:</b> Pending π‘ (${idxRes.error})\n`; | |
| if (trelloRes.success) finalMsg += `<b>π Trello:</b> <a href="${trelloRes.url}">View Card</a> π\n`; | |
| else finalMsg += `<b>π Trello:</b> Failed π΄\n`; | |
| // Check for matching sales offer β suggest creating one if none | |
| try { | |
| const offerLink = salesEngine.injectOfferLink(topic); | |
| if (offerLink) { | |
| finalMsg += `\nπ° <b>Offer CTA injected:</b> ${offerLink}\n`; | |
| } else { | |
| finalMsg += `\nπ‘ No matching offer for "${topic}". Create one? <code>/offer ${topic}</code>\n`; | |
| } | |
| } catch (e) { /* non-critical */ } | |
| await apiCaller.sendTelegramMessage(chatId, finalMsg); | |
| // Auto-push SEO publish metadata to HF | |
| try { | |
| await hfStorage.saveRecord('seo', `seo_${Date.now()}`, { | |
| title: `SEO: ${topic}`, | |
| timestamp: new Date().toISOString(), | |
| source_url: pubRes.url, | |
| niche: 'SEO' | |
| }, `# Published: ${topic}\n\n- URL: ${pubRes.url}\n- Site: ${targetSiteId || 'Default'}\n- EEAT Score: ${scoreRes.score || 'N/A'}\n- Google Indexed: ${idxRes.success}\n- Trello: ${trelloRes.success ? trelloRes.url : 'N/A'}`); | |
| } catch (e) { | |
| console.error('[HF Auto] SEO log failed:', e.message); | |
| } | |
| } catch (err) { | |
| console.error('[_handleWrite] Error:', err.message); | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ <b>Write Pipeline Error:</b> ${err.message}`); | |
| } | |
| } | |
| // --- Helper: handle /research context reply --- | |
| async function _handleResearch(chatId, url, angle) { | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Research Auto-Pilot</b>\nAnalyzing URL with focus: <i>${angle || 'General'}</i>...`); | |
| const researchModule = require('./research'); | |
| const aiContentMod = require('./ai-content'); | |
| const imageGenMod = require('./image-gen'); | |
| const schedulerMod = require('./scheduler'); | |
| try { | |
| const scrapeRes = await researchModule.scrapePost(url, angle); | |
| if (!scrapeRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Scrape failed: ${scrapeRes.error}`); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π§ Scrape successful! Remixing content...`); | |
| const remixRes = await aiContentMod.remix(scrapeRes.data); | |
| if (!remixRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β AI Remix failed: ${remixRes.error}`); | |
| return; | |
| } | |
| const v1 = remixRes.data.variant_1; | |
| await apiCaller.sendTelegramMessage(chatId, `π¨ Generating high-conversion visual...`); | |
| const imgRes = await imageGenMod.generateAndUpload(v1.visual_prompt, 'res_v1'); | |
| if (imgRes.success) v1.mediaUrl = imgRes.publicUrl; | |
| const draftId = `res_${Date.now().toString().slice(-6)}`; | |
| const draftPost = { | |
| id: draftId, status: 'draft', created: new Date().toISOString(), | |
| input: url, platform: scrapeRes.data.platform || 'web', | |
| sourceData: scrapeRes.data, type: 'standard', | |
| v1, v2: remixRes.data.variant_2, v3: remixRes.data.variant_3 | |
| }; | |
| schedulerMod.config.posts.push(draftPost); | |
| schedulerMod.saveDB(); | |
| if (imgRes.success && v1.mediaUrl) { | |
| const FormData = require('form-data'); | |
| const fForm = new FormData(); | |
| fForm.append('chat_id', chatId); | |
| fForm.append('photo', v1.mediaUrl.startsWith('/') ? fs.createReadStream(path.join(__dirname, 'public', v1.mediaUrl)) : v1.mediaUrl); | |
| fForm.append('caption', `β <b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`); | |
| fForm.append('parse_mode', 'HTML'); | |
| await apiCaller.axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendPhoto`, fForm, { headers: fForm.getHeaders() }); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`); | |
| } | |
| } catch (err) { | |
| console.error('[_handleResearch] Error:', err.message); | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ <b>Research Error:</b> ${err.message}`); | |
| } | |
| } | |
| // Telegram Webhook | |
| app.post('/api/telegram-webhook', async (req, res) => { | |
| res.status(200).send({ status: 'received' }); | |
| (async () => { | |
| const update = req.body; | |
| const message = update.message; | |
| if (!message) return; | |
| const chatId = message.chat.id; | |
| const from = message.from?.first_name || 'User'; | |
| // Bug #1 fix: guard against undefined for non-text messages (stickers, photos w/o caption, etc.) | |
| let userText = message.text || ''; | |
| // Bug #6 fix: transcribe voice BEFORE flow intercepts so voice replies work inside multi-step flows | |
| if (message.voice && !userText) { | |
| await apiCaller.sendTelegramMessage(chatId, 'π€ <i>Transcribing...</i>'); | |
| const transcription = await voiceTranscriber.transcribeVoice(message.voice.file_id); | |
| if (transcription) { | |
| userText = transcription; | |
| apiCaller.logTelegramMessage('IN', chatId, `[Voice]: ${userText}`); | |
| // Voice-to-voice conversation: natural speech (not a /command) β Apex replies with voice | |
| if (!userText.startsWith('/')) { | |
| const vibeVoice = require('./skills/vibe_voice'); | |
| // Echo transcription so user sees what was heard | |
| await apiCaller.sendTelegramMessage(chatId, `π€ <i>"${userText}"</i>`); | |
| if (vibeVoice.isEnabled()) { | |
| const voiceResp = await ceoAgent.voiceConversation(chatId, userText); | |
| if (voiceResp.success && voiceResp.text) { | |
| await responseRouter.route({ chatId, userText, inputModality: 'voice', intent: 'chat', responseText: voiceResp.text }); | |
| } else { | |
| // Fallback: text response if voice synthesis fails | |
| await apiCaller.sendTelegramMessage(chatId, `π― ${voiceResp.text || 'Apex is thinking... try again.'}`); | |
| } | |
| } else { | |
| // VibeVoice not configured β fall through to normal text flow | |
| // userText is set, normal command router handles it below | |
| } | |
| return; | |
| } | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, 'β Transcription failed.'); | |
| return; | |
| } | |
| } | |
| // --- File Handling (Photos & Documents) --- | |
| if (message.photo || message.document) { | |
| const caption = message.caption || ''; | |
| const isProposalFile = caption.toLowerCase().startsWith('/proposal'); | |
| if (isProposalFile) { | |
| const initRes = await apiCaller.sendTelegramMessage(chatId, 'π <b>QUANTA Pipeline</b>\n[β β‘β‘β‘β‘β‘] 15% Analyzing file for Proposal...'); | |
| const msgId = initRes.messageId; | |
| try { | |
| let rfqText = ''; | |
| if (message.photo) { | |
| const fileId = message.photo[message.photo.length - 1].file_id; | |
| const fileRes = await apiCaller.downloadTelegramFile(fileId); | |
| if (fileRes.success) { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, 'π <b>QUANTA Pipeline</b>\n[β β β‘β‘β‘β‘] 30% OCR: Reading text from image via Vision...'); | |
| const ocrRes = await apiCaller.analyzeImage(fileRes.buffer, "Extract the full project requirements or RFQ details from this image. DO NOT summarize, just transcribe the core needs."); | |
| if (ocrRes.success) rfqText = ocrRes.data; | |
| else throw new Error("Vision analysis failed: " + ocrRes.error); | |
| } else throw new Error("Download failed: " + fileRes.error); | |
| } else if (message.document && message.document.mime_type === 'application/pdf') { | |
| const fileId = message.document.file_id; | |
| const fileRes = await apiCaller.downloadTelegramFile(fileId); | |
| if (fileRes.success) { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, 'π <b>QUANTA Pipeline</b>\n[β β β β‘β‘β‘] 45% Quanta is parsing your PDF document...'); | |
| const data = await quantaBridge.submitPDF(fileRes.buffer, message.document.file_name, chatId, from); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, 'π <b>QUANTA Pipeline</b>\n[β β β β β β‘] 85% Generating final summary...'); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatProposalSummary(data)); | |
| return; | |
| } else throw new Error("PDF download failed: " + fileRes.error); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, "β οΈ Only Photos or PDF documents are supported for /proposal analysis currently."); | |
| return; | |
| } | |
| // For images (where we extracted text locally), forward text to Quanta | |
| if (rfqText) { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, 'π <b>QUANTA Pipeline</b>\n[β β β β‘β‘β‘] 45% Quanta is architecting your proposal...'); | |
| const data = await quantaBridge.submitRFQ(rfqText, chatId, from); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, 'π <b>QUANTA Pipeline</b>\n[β β β β β β‘] 85% Generating final summary...'); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatProposalSummary(data)); | |
| } | |
| } catch (e) { | |
| console.error('[File-Proposal] Error:', e.message); | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ <b>File Analysis Error:</b> ${e.message}`); | |
| } | |
| return; | |
| } | |
| } | |
| // --- Clear Flow State on New Command --- | |
| const isNewCommand = userText.startsWith('/') && !['/cancel', '/confirm', '/yes', '/no'].includes(userText.toLowerCase()); | |
| const _db = memory.readDB(); | |
| if (isNewCommand && _db.pending_commands && _db.pending_commands[chatId]) { | |
| delete _db.pending_commands[chatId]; | |
| memory.writeDB(_db); | |
| await apiCaller.sendTelegramMessage(chatId, `π <i>Previous workflow cancelled. Starting new command.</i>`); | |
| } | |
| // --- Flow Intercepts --- | |
| if (_db.pending_commands?.[chatId]?.type === 'report_flow' && !isNewCommand) { | |
| await reportFlow.handle(chatId, userText); | |
| return; | |
| } | |
| if (_db.pending_commands?.[chatId]?.type === 'carousel_flow' && !isNewCommand) { | |
| await carouselFlow.handle(chatId, userText); | |
| return; | |
| } | |
| // Multi-step research flow: funnel β language β core message β generate | |
| if (_db.pending_commands?.[chatId]?.type === 'research_flow' && !isNewCommand) { | |
| const pending = _db.pending_commands[chatId]; | |
| const text = userText.trim(); | |
| if (pending.step === 'awaiting_funnel') { | |
| const funnelMap = { '1': 'TOFU', '2': 'MOFU', '3': 'BOFU', 'tofu': 'TOFU', 'mofu': 'MOFU', 'bofu': 'BOFU' }; | |
| const funnel = funnelMap[text.toLowerCase()] || 'TOFU'; | |
| pending.funnelStage = funnel; | |
| pending.step = 'awaiting_language'; | |
| memory.writeDB(_db); | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `β Stage: <b>${funnel}</b>\n\nNow, <b>language</b>?\n\n` + | |
| `1οΈβ£ <b>Both</b> (EN + ID)\n2οΈβ£ <b>EN</b> only\n3οΈβ£ <b>ID</b> only` | |
| ); | |
| return; | |
| } | |
| if (pending.step === 'awaiting_language') { | |
| const langMap = { '1': 'both', '2': 'en', '3': 'id', 'both': 'both', 'en': 'en', 'id': 'id', 'english': 'en', 'indonesian': 'id', 'indo': 'id' }; | |
| const lang = langMap[text.toLowerCase()] || 'both'; | |
| pending.language = lang; | |
| pending.step = 'awaiting_core_message'; | |
| memory.writeDB(_db); | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `β Language: <b>${lang}</b>\n\nWhat's the <b>core message</b> you want to convey?\n\n` + | |
| `(e.g. "AI can save 10 hours/week" or type <b>skip</b> to auto-detect)` | |
| ); | |
| return; | |
| } | |
| if (pending.step === 'awaiting_core_message') { | |
| const coreMessage = ['skip', 'go', 'auto'].includes(text.toLowerCase()) ? '' : text; | |
| delete _db.pending_commands[chatId]; | |
| memory.writeDB(_db); | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π <b>Generating content...</b>\n\n` + | |
| `Stage: ${pending.funnelStage} | Lang: ${pending.language} | Tier: ${pending.tier}\n` + | |
| `${coreMessage ? `Message: "${coreMessage.substring(0, 60)}"` : 'Auto-detecting best angle'}\n\n` + | |
| `This may take 30-60 seconds...` | |
| ); | |
| const result = await scheduler.runInteractiveCycle( | |
| { type: pending.inputType, value: pending.input, tier: pending.tier }, | |
| { funnelStage: pending.funnelStage, language: pending.language, coreMessage } | |
| ); | |
| if (result.success) { | |
| const v1 = result.draft.v1; | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `β <b>Draft Created!</b>\n\n` + | |
| `<b>Title:</b> ${v1.title}\n` + | |
| `<b>Pillar:</b> ${v1.pillar}\n` + | |
| `<b>Stage:</b> ${pending.funnelStage}\n\n` + | |
| `Review and approve at: ${process.env.PUBLIC_URL || 'http://localhost:3000'}/social` | |
| ); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β Generation failed: ${result.error}`); | |
| } | |
| return; | |
| } | |
| // Legacy fallback (old research_flow without step field) | |
| delete _db.pending_commands[chatId]; | |
| memory.writeDB(_db); | |
| const angle = ['skip','go'].includes(text.toLowerCase()) ? '' : text; | |
| await _handleResearch(chatId, pending.url || pending.input, angle); | |
| return; | |
| } | |
| if (_db.pending_commands?.[chatId]?.type === 'write_flow' && !isNewCommand) { | |
| const pending = _db.pending_commands[chatId]; | |
| // Expire after 30 minutes (Bug #7 fix) | |
| if (Date.now() - pending.ts > 30 * 60 * 1000) { | |
| delete _db.pending_commands[chatId]; | |
| memory.writeDB(_db); | |
| await apiCaller.sendTelegramMessage(chatId, 'β± <i>Write session expired. Please send /write again.</i>'); | |
| return; | |
| } | |
| delete _db.pending_commands[chatId]; | |
| memory.writeDB(_db); | |
| const ctx = userText.toLowerCase() === 'go' ? '' : userText; | |
| await _handleWrite(chatId, pending, ctx); | |
| return; | |
| } | |
| // CEO agent multi-step flow intercept | |
| if (_db.pending_commands?.[chatId]?.type === 'ceo_flow' && !isNewCommand) { | |
| await ceoAgent.handleFlowStep(chatId, userText, _db.pending_commands[chatId]); | |
| return; | |
| } | |
| // Viral onboard multi-step flow intercept | |
| if (_db.pending_commands?.[chatId]?.type === 'viral_onboard' && !isNewCommand) { | |
| const handled = await viralFlow.handleFlowStep(chatId, userText, _db.pending_commands[chatId]); | |
| if (handled) return; | |
| } | |
| // --- Commands --- | |
| // --- CEO Agent: /ceo command --- | |
| if (userText && userText.toLowerCase().startsWith('/ceo')) { | |
| await ceoAgent.start(chatId, userText); | |
| return; | |
| } | |
| // --- QUANTA: /proposal command --- | |
| if (userText && userText.toLowerCase().startsWith('/proposal')) { | |
| const args = userText.substring(9).trim(); | |
| if (!args || args === 'help') { | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>QUANTA Proposal Generator</b>\n\n<code>/proposal [RFQ text]</code> β Generate bilingual proposal\n<code>/proposal status [id]</code> β Check project status\n<code>/proposal list</code> β Recent projects\n\nPaste a client RFQ, TOR, or WhatsApp message after /proposal to generate a full bilingual proposal with LQS scoring, pricing, and a client portal link.`); | |
| return; | |
| } | |
| if (args.toLowerCase().startsWith('status ')) { | |
| const projectId = args.substring(7).trim(); | |
| try { | |
| await apiCaller.sendTelegramMessage(chatId, 'π Fetching project status...'); | |
| const data = await quantaBridge.getProjectStatus(projectId); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatProjectStatus(data)); | |
| } catch (e) { | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ Error: ${e.message}`); | |
| } | |
| return; | |
| } | |
| if (args.toLowerCase() === 'list') { | |
| try { | |
| const data = await quantaBridge.listProjects(); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatProjectsList(data)); | |
| } catch (e) { | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ Error: ${e.message}`); | |
| } | |
| return; | |
| } | |
| // Forward RFQ text to QUANTA | |
| try { | |
| const initRes = await apiCaller.sendTelegramMessage(chatId, 'π <b>QUANTA Pipeline</b>\n[β β‘β‘β‘β‘β‘] 15% Receiving RFQ details...'); | |
| const msgId = initRes.messageId; | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, 'π <b>QUANTA Pipeline</b>\n[β β β β‘β‘β‘] 45% Quanta is architecting your proposal...'); | |
| const data = await quantaBridge.submitRFQ(args, chatId, from); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, 'π <b>QUANTA Pipeline</b>\n[β β β β β β‘] 85% Generating final summary...'); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatProposalSummary(data)); | |
| } catch (e) { | |
| console.error(`[QUANTA] Error for ${chatId}:`, e.message); | |
| let errorMsg = `β οΈ <b>QUANTA Error</b>\n\n`; | |
| if (e.message.includes('timeout')) { | |
| errorMsg += `The system is taking longer than usual. The proposal may still be generating in the background. Check <code>/proposal list</code> in a minute.`; | |
| } else if (e.message.includes('401')) { | |
| errorMsg += `Authentication failure. Quanta agent name or API keys might be misconfigured.`; | |
| } else { | |
| errorMsg += `Failed to process RFQ: ${e.message}`; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, errorMsg); | |
| } | |
| return; | |
| } | |
| // --- QUANTA: /scrape command --- | |
| if (userText && userText.toLowerCase().startsWith('/scrape')) { | |
| const args = userText.substring(7).trim(); | |
| if (!args || args === 'help') { | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Web Scraper</b>\n\n<code>/scrape [url]</code> β Scrape website\n<code>/scrape yt [video-url]</code> β YouTube transcript\n<code>/scrape ig [username]</code> β Instagram profile\n<code>/scrape tt [username]</code> β TikTok profile\n<code>/scrape audit [domain]</code> β Full competitor audit`); | |
| return; | |
| } | |
| try { | |
| await apiCaller.sendTelegramMessage(chatId, 'π Scraping...'); | |
| if (args.toLowerCase().startsWith('yt ')) { | |
| const data = await quantaBridge.scrapeYouTube(args.substring(3).trim()); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatScrapeResult(data, 'youtube')); | |
| } else if (args.toLowerCase().startsWith('ig ')) { | |
| const data = await quantaBridge.scrapeSocial('instagram', args.substring(3).trim()); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatScrapeResult(data, 'social')); | |
| } else if (args.toLowerCase().startsWith('tt ')) { | |
| const data = await quantaBridge.scrapeSocial('tiktok', args.substring(3).trim()); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatScrapeResult(data, 'social')); | |
| } else if (args.toLowerCase().startsWith('audit ')) { | |
| const data = await quantaBridge.scrapeAudit(args.substring(6).trim()); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatScrapeResult(data, 'audit')); | |
| } else { | |
| // Default: scrape URL | |
| const data = await quantaBridge.scrapeUrl(args); | |
| await apiCaller.sendTelegramMessage(chatId, quantaBridge.formatScrapeResult(data, 'url')); | |
| } | |
| } catch (e) { | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ Scrape error: ${e.message}`); | |
| } | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/videos')) { | |
| await videoEngine.start(chatId, userText); | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/speak')) { | |
| const speakText = userText.replace(/^\/speak\s*/i, '').trim(); | |
| if (!speakText) { | |
| await apiCaller.sendTelegramMessage(chatId, 'π Usage: /speak [text]\n\nExample: /speak Hello, this is Apex speaking.'); | |
| return; | |
| } | |
| const vibeVoice = require('./skills/vibe_voice'); | |
| if (!vibeVoice.isEnabled()) { | |
| await apiCaller.sendTelegramMessage(chatId, 'β VibeVoice not configured. Set VIBEVOICE_URL env var.'); | |
| return; | |
| } | |
| const loadingMsg = await apiCaller.sendTelegramMessage(chatId, 'π Generating voice...'); | |
| const result = await vibeVoice.speak(chatId, speakText); | |
| if (!result.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Voice error: ${result.error}`); | |
| } | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/viral')) { | |
| await viralFlow.start(chatId, userText); | |
| return; | |
| } | |
| if (userText === '/sync') { | |
| await apiCaller.sendTelegramMessage(chatId, "π <b>Infrastructure Sync Started...</b>"); | |
| const res = await hfDeployer.syncCode(); | |
| await apiCaller.sendTelegramMessage(chatId, res.success ? "β <b>Deployment Successful.</b>" : `β <b>Sync Failed:</b> ${res.error}`); | |
| return; | |
| } | |
| if (userText === '/social' || userText === '/post') { | |
| const socialGuide = `π± <b>Social Media Instructions (v1.0)</b> | |
| To get the highest engagement from the AI Remixer, feed it raw text or ideas using this framework: | |
| <b>1. The Hook (First 1-3 lines)</b> | |
| Use a pattern interrupt, a bold claim, or an open loop. | |
| <i>Example: "Stop posting daily. It's killing your reach. Do this instead:"</i> | |
| <b>2. The Value (The Meat)</b> | |
| Provide 3-5 specific, actionable points or a storytelling arc (Situation β Challenge β Insight). | |
| <b>3. The Format Request (Optional)</b> | |
| Tell the AI what emotion or 'edge' you want. | |
| <i>Example: "Remix this into a contrarian take on AI productivity."</i> | |
| <b>4. Image Prompting</b> | |
| β’ Text/Face logic is automatic! If your prompt includes words like "text", "quote", "face", or "person", it routes to the premium Nano Banana model. | |
| β’ Standard visuals use Hive AI (Flux Schnell). | |
| <i>Try sending me a URL or text paragraph right now to brainstorm!</i>`; | |
| await apiCaller.sendTelegramMessage(chatId, socialGuide); | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/carousel')) { | |
| await carouselFlow.start(chatId, userText); | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/research')) { | |
| const rawInput = userText.replace(/^\/research\s*/i, '').trim(); | |
| if (!rawInput) { | |
| const researchGuide = `π <b>Research Auto-Pilot β v2.0</b> | |
| This engine researches any URL or topic and generates content with your options. | |
| <b>Usage:</b> | |
| <code>/research [URL or topic]</code> | |
| <code>/research [URL or topic] smart</code> β use Sonar Pro (deeper) | |
| <b>Supported:</b> URLs (X, IG, Threads, blogs) OR text topics | |
| <b>Flow:</b> | |
| 1. Research β 2. TOFU/MOFU/BOFU β 3. Language β 4. Core message β 5. Generate | |
| <i>Try: /research AI productivity tips</i>`; | |
| await apiCaller.sendTelegramMessage(chatId, researchGuide); | |
| return; | |
| } | |
| // Detect tier from last word | |
| const parts = rawInput.split(' '); | |
| let tier = 'fast'; | |
| if (parts[parts.length - 1]?.toLowerCase() === 'smart') { tier = 'smart'; parts.pop(); } | |
| else if (parts[parts.length - 1]?.toLowerCase() === 'fast') { tier = 'fast'; parts.pop(); } | |
| const input = parts.join(' '); | |
| // Detect if URL or topic | |
| const isUrl = input.startsWith('http') || input.includes('.com') || input.includes('.net'); | |
| const rDb = memory.readDB(); | |
| if (!rDb.pending_commands) rDb.pending_commands = {}; | |
| rDb.pending_commands[chatId] = { | |
| type: 'research_flow', | |
| step: 'awaiting_funnel', | |
| input, inputType: isUrl ? 'url' : 'topic', tier, | |
| ts: Date.now() | |
| }; | |
| memory.writeDB(rDb); | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π <b>Research:</b> ${input.substring(0, 50)}${input.length > 50 ? '...' : ''}\n` + | |
| `<b>Tier:</b> ${tier === 'smart' ? 'Smart (Sonar Pro)' : 'Fast (Sonar)'}\n\n` + | |
| `What's the <b>funnel stage</b>?\n\n` + | |
| `1οΈβ£ <b>TOFU</b> β Awareness (grow audience)\n` + | |
| `2οΈβ£ <b>MOFU</b> β Consideration (educate, build trust)\n` + | |
| `3οΈβ£ <b>BOFU</b> β Conversion (drive action)\n\n` + | |
| `Reply: <b>1</b>, <b>2</b>, <b>3</b>, or <b>TOFU/MOFU/BOFU</b>` | |
| ); | |
| return; | |
| } | |
| // /socialmodel [premium|standard|free] β switch social content AI model | |
| if (userText && userText.toLowerCase().startsWith('/socialmodel')) { | |
| const arg = userText.split(' ')[1]?.toLowerCase(); | |
| const db = memory.readDB(); | |
| if (!db.user_profile_snapshot) db.user_profile_snapshot = {}; | |
| const tierMap = { | |
| premium: { id: apiCaller.SOCIAL_MODELS.PREMIUM, name: 'Claude Haiku 4.5', cost: '$1.00/$5.00 per 1M tokens' }, | |
| standard: { id: apiCaller.SOCIAL_MODELS.STANDARD, name: 'Gemini 2.5 Flash Lite', cost: '$0.10/$0.40 per 1M tokens' }, | |
| free: { id: apiCaller.SOCIAL_MODELS.FREE, name: 'Llama 3.3 70B', cost: 'FREE' } | |
| }; | |
| if (!arg) { | |
| const current = db.user_profile_snapshot.social_content_model || apiCaller.SOCIAL_MODELS.FREE; | |
| const currentTier = Object.entries(tierMap).find(([, v]) => v.id === current); | |
| const tierName = currentTier ? currentTier[0].toUpperCase() : 'FREE'; | |
| const tierInfo = currentTier ? currentTier[1] : tierMap.free; | |
| await apiCaller.sendTelegramMessage(chatId, `π€ <b>Social Content Model</b>\n\nCurrent: <b>${tierInfo.name}</b> (${tierName})\nCost: ${tierInfo.cost}\n\nSwitch with:\n<code>/socialmodel premium</code> β Claude Haiku 4.5 (SOTA human tone)\n<code>/socialmodel standard</code> β Gemini Flash Lite (good daily driver)\n<code>/socialmodel free</code> β Llama 3.3 70B (default, zero cost)`); | |
| return; | |
| } | |
| const tier = tierMap[arg]; | |
| if (!tier) { | |
| await apiCaller.sendTelegramMessage(chatId, 'β Invalid tier. Use: <code>/socialmodel premium|standard|free</code>'); | |
| return; | |
| } | |
| db.user_profile_snapshot.social_content_model = tier.id; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Social Model β ${tier.name}</b>\nTier: ${arg.toUpperCase()}\nCost: ${tier.cost}\n\nAll new content generation will use this model.`); | |
| return; | |
| } | |
| // /compete [url or topic] β competitor research | |
| if (userText && userText.toLowerCase().startsWith('/compete')) { | |
| const input = userText.replace(/^\/compete\s*/i, '').trim(); | |
| if (!input) { | |
| await apiCaller.sendTelegramMessage(chatId, 'π <b>Competitor Research</b>\n\nUsage: <code>/compete [URL or topic]</code>\n\nAnalyzes top creators and extracts winning patterns.\n\nExamples:\n<code>/compete https://instagram.com/creator</code>\n<code>/compete AI productivity tips</code>'); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π Analyzing competitors for: "${input.substring(0, 50)}"...\nThis may take 30-60 seconds.`); | |
| const isUrl = input.startsWith('http') || input.includes('.com') || input.includes('.net'); | |
| const result = await competitorResearch.analyzeCompetitors(isUrl ? { url: input } : { topic: input }); | |
| if (result.success) { | |
| const p = result.data.patterns || {}; | |
| let msg = `π <b>Competitor Insights</b>${result.cached ? ' (cached)' : ''}\n\n`; | |
| if (p.keyInsight) msg += `π‘ <b>Key Insight:</b> ${p.keyInsight}\n\n`; | |
| if (p.topPostTypes) msg += `<b>Top Post Types:</b> ${p.topPostTypes.map(t => `${t.type} (${t.pct}%)`).join(', ')}\n`; | |
| if (p.bestHookStyles) msg += `<b>Best Hooks:</b> ${p.bestHookStyles.map(h => `${h.style} (${h.pct}%)`).join(', ')}\n`; | |
| if (p.bestTimes) msg += `<b>Best Times:</b> ${p.bestTimes.join(', ')}\n`; | |
| if (p.engagementDrivers) msg += `\n<b>Engagement Drivers:</b>\n${p.engagementDrivers.map(d => `β’ ${d}`).join('\n')}\n`; | |
| if (p.recommendation) msg += `\n<b>Recommendation:</b> ${p.recommendation}`; | |
| await apiCaller.sendTelegramMessage(chatId, msg); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β Research failed: ${result.error}`); | |
| } | |
| return; | |
| } | |
| // /pulseconfig β configure daily pulse | |
| if (userText && userText.toLowerCase().startsWith('/pulseconfig')) { | |
| const args = userText.replace(/^\/pulseconfig\s*/i, '').trim().toLowerCase(); | |
| const db = memory.readDB(); | |
| if (!db.pulse_config) db.pulse_config = { enabled: true, time: '07:00', timezone: 'Asia/Jakarta', include: ['social_analytics', 'market_trends', 'crm_updates'] }; | |
| if (!args) { | |
| const c = db.pulse_config; | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `βοΈ <b>Pulse Configuration</b>\n\n` + | |
| `<b>Enabled:</b> ${c.enabled ? 'Yes' : 'No'}\n` + | |
| `<b>Time:</b> ${c.time} WIB\n` + | |
| `<b>Sections:</b> ${(c.include || []).join(', ')}\n\n` + | |
| `Commands:\n` + | |
| `<code>/pulseconfig on</code> or <code>off</code>\n` + | |
| `<code>/pulseconfig time 08:30</code>\n` + | |
| `<code>/pulseconfig include social_analytics,market_trends</code>` | |
| ); | |
| return; | |
| } | |
| if (args === 'on' || args === 'off') { | |
| db.pulse_config.enabled = args === 'on'; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, `β Pulse ${args === 'on' ? 'enabled' : 'disabled'}.`); | |
| return; | |
| } | |
| if (args.startsWith('time ')) { | |
| const time = args.replace('time ', '').trim(); | |
| if (/^\d{1,2}:\d{2}$/.test(time)) { | |
| db.pulse_config.time = time; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, `β Pulse time set to <b>${time} WIB</b>.`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, 'β Invalid time format. Use HH:MM (e.g. 08:30).'); | |
| } | |
| return; | |
| } | |
| if (args.startsWith('include ')) { | |
| const sections = args.replace('include ', '').split(',').map(s => s.trim()); | |
| db.pulse_config.include = sections; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, `β Pulse sections: <b>${sections.join(', ')}</b>`); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, 'β Unknown option. Use: on|off|time HH:MM|include section1,section2'); | |
| return; | |
| } | |
| // /analytics β get analytics summary | |
| if (userText && userText.toLowerCase() === '/analytics') { | |
| const result = await analyticsEngine.generateDailySummary(); | |
| if (!result.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Analytics error: ${result.error || 'Unknown error'}`); | |
| } | |
| // generateDailySummary already sends via Telegram if TELEGRAM_CHAT_ID is set | |
| // If chatId differs, send directly | |
| if (chatId !== process.env.TELEGRAM_CHAT_ID) { | |
| await apiCaller.sendTelegramMessage(chatId, result.message || 'No analytics data yet.'); | |
| } | |
| return; | |
| } | |
| if (userText === '/turbo') { | |
| const db = memory.readDB(); | |
| if (!db.user_profile_snapshot) db.user_profile_snapshot = {}; | |
| db.user_profile_snapshot.active_chat_model = 'google/gemini-2.0-flash-lite-001'; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, "β‘ <b>Turbo Mode Activated!</b>\nNow using <i>Gemini 2.0 Flash Lite</i> for lightning-fast responses."); | |
| return; | |
| } | |
| if (userText === '/reason') { | |
| const db = memory.readDB(); | |
| if (!db.user_profile_snapshot) db.user_profile_snapshot = {}; | |
| db.user_profile_snapshot.active_chat_model = 'nvidia/nemotron-nano-9b-v2:free'; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, "π§ <b>Reasoning Mode Activated!</b>\nNow using <i>NVIDIA Nemotron Nano</i> for deep-think brainstorming."); | |
| return; | |
| } | |
| // Bug #2 fix: /auto on|off toggles speed mode (was advertised in /start but never implemented) | |
| if (userText && userText.toLowerCase().startsWith('/auto')) { | |
| const arg = userText.split(' ')[1]?.toLowerCase(); | |
| const db = memory.readDB(); | |
| if (!db.user_profile_snapshot) db.user_profile_snapshot = {}; | |
| const isOn = arg === 'on' || (!arg && !db.user_profile_snapshot.automatic_mode); | |
| db.user_profile_snapshot.automatic_mode = isOn; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, isOn | |
| ? 'β‘ <b>Auto Mode ON</b> β Intent actions run instantly without confirmation.' | |
| : 'π <b>Auto Mode OFF</b> β Actions will ask for confirmation before executing.' | |
| ); | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/sentiment')) { | |
| const args = userText.replace(/^\/sentiment\s*/i, '').trim(); | |
| if (!args) { | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π <b>MiroFish Sentiment Engine</b>\n\n` + | |
| `<b>Usage:</b>\n` + | |
| `<code>/sentiment [topic]</code>\n` + | |
| `<code>/sentiment [topic] | [country] | [id/en]</code>\n\n` + | |
| `<b>Examples:</b>\n` + | |
| `<code>/sentiment TikTok Shop ban</code>\n` + | |
| `<code>/sentiment Shopee ads | Indonesia | id</code>\n` + | |
| `<code>/sentiment crypto regulation | USA | en</code>\n\n` + | |
| `<b>Angles:</b> ads, sentiment, market, policy\n` + | |
| `<code>/sentiment EV subsidies | Indonesia | id | policy</code>\n\n` + | |
| `<i>Default: Indonesia, Bahasa Indonesia, sentiment angle</i>` | |
| ); | |
| return; | |
| } | |
| // Parse: topic | country | language | angle | |
| const parts = args.split('|').map(s => s.trim()); | |
| const topic = parts[0]; | |
| const country = parts[1] || 'Indonesia'; | |
| const language = (parts[2] || 'id').toLowerCase().startsWith('en') ? 'en' : 'id'; | |
| const angle = parts[3] || 'sentiment'; | |
| const langLabel = language === 'id' ? 'Bahasa Indonesia' : 'English'; | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π <b>MiroFish Sentiment Engine</b>\n\n` + | |
| `π <b>Topic:</b> ${topic}\n` + | |
| `π <b>Market:</b> ${country}\n` + | |
| `π£οΈ <b>Language:</b> ${langLabel}\n` + | |
| `π― <b>Angle:</b> ${angle}\n\n` + | |
| `<i>Running quick sentiment analysis...</i>` | |
| ); | |
| // Step 1: Quick LLM analysis β send to Telegram immediately | |
| const result = await sentimentAgent.quickAnalysis({ topic, country, language, angle }); | |
| if (!result.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Analysis failed: ${result.error}`); | |
| return; | |
| } | |
| // Send quick summary to Telegram immediately | |
| const summary = result.analysis.length > 3500 | |
| ? result.analysis.substring(0, 3500) + '\n\n<i>... (detailed PDF generating...)</i>' | |
| : result.analysis; | |
| await apiCaller.sendTelegramMessage(chatId, summary); | |
| await apiCaller.sendTelegramMessage(chatId, `<i>π Generating enhanced PDF with deep analysis...</i>`); | |
| // Step 2: Enhanced analysis + MiroFish data in parallel | |
| const [enhancedResult, miroFishData] = await Promise.allSettled([ | |
| sentimentAgent.generateEnhancedAnalysis({ topic, country, language, angle, quickSummary: result.analysis }), | |
| sentimentAgent.fetchMiroFishData(null) | |
| ]); | |
| const enhancedText = enhancedResult.status === 'fulfilled' && enhancedResult.value.success | |
| ? enhancedResult.value.analysis : null; | |
| const mfData = miroFishData.status === 'fulfilled' ? miroFishData.value : null; | |
| // Step 3: Generate enhanced PDF (last β includes all data) | |
| const pdfResult = await sentimentAgent.generateEnhancedPDF({ | |
| topic, country, language, angle, | |
| quickAnalysis: result.analysis, | |
| enhancedAnalysis: enhancedText, | |
| miroFishData: mfData | |
| }); | |
| if (pdfResult.success) { | |
| try { | |
| await apiCaller.sendTelegramDocument(chatId, pdfResult.path, `π Sentiment: ${topic}`); | |
| } catch (e) { | |
| console.error('[Sentiment] PDF send failed:', e.message); | |
| } | |
| } | |
| // Save to dashboard history | |
| sentimentAgent.saveToHistory({ | |
| topic, country, language, angle, | |
| mode: enhancedText ? 'enhanced' : 'quick', | |
| analysis: enhancedText || result.analysis, | |
| pdfFileName: pdfResult.success ? pdfResult.fileName : null | |
| }); | |
| // Create Trello card | |
| const trelloResult = await sentimentAgent.createTrelloCard( | |
| topic, country, language, enhancedText || result.analysis, pdfResult.success ? pdfResult.path : null | |
| ); | |
| // Status footer | |
| const statusParts = []; | |
| if (pdfResult.success) statusParts.push('π Enhanced PDF'); | |
| if (enhancedText) statusParts.push('π Deep analysis'); | |
| if (mfData) statusParts.push('π MiroFish data'); | |
| if (trelloResult.success) statusParts.push('π Trello card'); | |
| if (statusParts.length) { | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `β <b>Sentiment analysis complete!</b>\n${statusParts.join(' β’ ')}` | |
| ); | |
| } | |
| // Background: Try full MiroFish simulation (fire-and-forget) | |
| sentimentAgent.runSimulation({ topic, country, language, rounds: 10, angle }) | |
| .then(simResult => { | |
| if (simResult.success) { | |
| apiCaller.sendTelegramMessage(chatId, | |
| `π <b>MiroFish Deep Simulation Complete!</b>\n\n` + | |
| `Project: <code>${simResult.project_id}</code>\n` + | |
| `Simulation: <code>${simResult.simulation_id}</code>\n\n` + | |
| `<i>Full swarm intelligence report is ready in MiroFish dashboard.</i>` | |
| ); | |
| } | |
| }) | |
| .catch(() => {}); // Silent fail β analysis already delivered | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/report')) { | |
| await reportFlow.start(chatId, userText); | |
| return; | |
| } | |
| if (userText && userText.toLowerCase().startsWith('/push')) { | |
| const pushArg = userText.split(' ')[1]?.toLowerCase() || ''; | |
| if (!pushArg || !['db', 'reports', 'all'].includes(pushArg)) { | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π€ <b>/push β Manual HF Upload</b>\n\n` + | |
| `/push db β Snapshot local_db.json\n` + | |
| `/push reports β All PDFs in database/reports/\n` + | |
| `/push all β Everything above` | |
| ); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π€ <i>Pushing to HF dataset...</i>`); | |
| const results = []; | |
| if (pushArg === 'db' || pushArg === 'all') { | |
| try { | |
| const dbPath = path.join(__dirname, 'database/local_db.json'); | |
| const dbContent = fs.readFileSync(dbPath); | |
| const res = await hfStorage.saveFile(`snapshots/local_db_${Date.now()}.json`, dbContent, 'VinOS: DB snapshot'); | |
| results.push(`DB snapshot: ${res.success ? 'β ' : 'β ' + res.error}`); | |
| } catch (e) { | |
| results.push(`DB snapshot: β ${e.message}`); | |
| } | |
| } | |
| if (pushArg === 'reports' || pushArg === 'all') { | |
| try { | |
| const reportsDir = path.join(__dirname, 'database/reports'); | |
| if (fs.existsSync(reportsDir)) { | |
| const pdfs = fs.readdirSync(reportsDir).filter(f => f.endsWith('.pdf')); | |
| if (pdfs.length > 0) { | |
| const files = pdfs.map(f => ({ | |
| path: `reports_pdf/${f}`, | |
| content: fs.readFileSync(path.join(reportsDir, f)) | |
| })); | |
| const res = await hfStorage.saveBatch(files, `VinOS: Push ${pdfs.length} PDFs`); | |
| results.push(`PDFs (${pdfs.length}): ${res.success ? 'β ' : 'β ' + res.error}`); | |
| } else { | |
| results.push('PDFs: No files found'); | |
| } | |
| } else { | |
| results.push('PDFs: reports/ folder not found'); | |
| } | |
| } catch (e) { | |
| results.push(`PDFs: β ${e.message}`); | |
| } | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π€ <b>Push Complete</b>\n\n${results.join('\n')}`); | |
| return; | |
| } | |
| if (userText && (userText.toLowerCase() === 'confirm' || userText.toLowerCase() === 'cancel')) { | |
| const db = memory.readDB(); | |
| const pending = db.pending_commands?.[chatId]; | |
| if (userText.toLowerCase() === 'confirm' && pending) { | |
| delete db.pending_commands[chatId]; | |
| memory.writeDB(db); | |
| await apiCaller.sendTelegramMessage(chatId, "β <b>Confirmed.</b> Running now..."); | |
| return await handleVinIntent(chatId, from, pending.userText, true); | |
| } else if (userText.toLowerCase() === 'cancel' && pending) { | |
| delete db.pending_commands[chatId]; | |
| memory.writeDB(db); | |
| return await apiCaller.sendTelegramMessage(chatId, "π€ <b>Cancelled.</b>"); | |
| } | |
| } | |
| if (message.voice) { | |
| await apiCaller.sendTelegramMessage(chatId, "π€ <i>Transcribing...</i>"); | |
| const transcription = await voiceTranscriber.transcribeVoice(message.voice.file_id); | |
| if (transcription) { | |
| userText = transcription; | |
| apiCaller.logTelegramMessage('IN', chatId, `[Voice]: ${userText}`); | |
| } else return await apiCaller.sendTelegramMessage(chatId, "β Transcription failed."); | |
| } | |
| // Handle photo + /gash caption (Face Mode β style transfer with face preservation) | |
| if (message.photo && message.caption && message.caption.toLowerCase().startsWith('/gash')) { | |
| const caption = message.caption; | |
| const gashFaceArgs = caption.split(' ').slice(1).join(' ') || 'recreate this person in a professional setting'; | |
| const photoObj = message.photo[message.photo.length - 1]; | |
| await apiCaller.sendTelegramMessage(chatId, `πΈ <b>Face Mode Activated!</b>\n<i>Reference photo received. Generating style transfer with face preservation...</i>\n<i>${gashFaceArgs.substring(0, 80)}</i>`); | |
| try { | |
| const axios = require('axios'); | |
| const fileRes = await axios.get(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/getFile?file_id=${photoObj.file_id}`); | |
| const filePath = fileRes.data.result.file_path; | |
| const photoUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${filePath}`; | |
| // Download the reference photo as buffer | |
| const photoResponse = await axios.get(photoUrl, { responseType: 'arraybuffer' }); | |
| const photoBuffer = Buffer.from(photoResponse.data); | |
| // Generate with face-preserving multimodal (Gemini sees the photo) | |
| const facePrompt = `${gashFaceArgs}, professional photography, cinematic lighting, high detail`; | |
| const faceResult = await apiCaller.generateImageFromReference(facePrompt, photoBuffer); | |
| if (faceResult.success && faceResult.buffer) { | |
| // Save to HF + get link | |
| let hfLink = ''; | |
| try { | |
| const hfRes = await hfStorage.saveFile(`images/gash_face_${Date.now()}.jpg`, faceResult.buffer, 'VinOS: Face mode image'); | |
| if (hfRes.success) hfLink = `\nπ <a href="${hfRes.url}">View on HF</a>`; | |
| } catch (e) { console.error('[HF Auto] Face image save failed:', e.message); } | |
| const FormData = require('form-data'); | |
| const fForm = new FormData(); | |
| fForm.append('chat_id', chatId); | |
| fForm.append('photo', faceResult.buffer, { filename: 'gash_face.jpg', contentType: 'image/jpeg' }); | |
| fForm.append('caption', `β¨ <b>Face Mode Result!</b>\nπ§ ${faceResult.source}\n<i>${gashFaceArgs.substring(0, 100)}</i>${hfLink}`); | |
| fForm.append('parse_mode', 'HTML'); | |
| try { | |
| await apiCaller.axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendPhoto`, fForm, { headers: fForm.getHeaders() }); | |
| } catch (fErr) { | |
| console.error("[Face] Photo upload error:", fErr.response?.data || fErr.message); | |
| } | |
| // Log to Trello | |
| try { | |
| await trelloManager.logImageGen(gashFaceArgs, faceResult.source, hfLink ? hfLink.match(/href="([^"]+)"/)?.[1] : ''); | |
| } catch (e) { console.error('[Trello] Image log failed:', e.message); } | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β Image generation failed: ${faceResult.error || 'All providers exhausted'}`); | |
| } | |
| } catch (photoErr) { | |
| console.error('[Gash Face] Error:', photoErr.message); | |
| await apiCaller.sendTelegramMessage(chatId, `β Face mode error: ${photoErr.message}`); | |
| } | |
| return; | |
| } | |
| if (userText) { | |
| if (userText.startsWith('/start')) { | |
| const db = memory.readDB(); | |
| const currentModel = db.user_profile_snapshot?.active_chat_model || 'nvidia/nemotron-nano-9b-v2:free'; | |
| const isTurbo = currentModel.includes('google'); | |
| const modeEmoji = isTurbo ? "β‘" : "π§ "; | |
| const modeName = isTurbo ? "Turbo Mode" : "Reasoning Mode"; | |
| const startMsg = `π¦ <b>VinOS Command Center β v4.1.0</b>\n` + | |
| `Current Mode: ${modeEmoji} <b>${modeName}</b>\n\n` + | |
| `π <b>STRATEGY</b>\n` + | |
| `/report β Interactive Research β PDF Report\n` + | |
| `<i>Vin asks: topic β depth β focus β confirm</i>\n` + | |
| `β’ 1οΈβ£ Quick Brief (2-3 pages, ~30s)\n` + | |
| `β’ 2οΈβ£ Deep Analysis (5-8 pages, ~2min)\n` + | |
| `β’ 3οΈβ£ Brand Visibility Audit (presence + score)\n` + | |
| `β’ 4οΈβ£ From Your Briefing (paste text β structured PDF)\n` + | |
| `<i>Output: McKinsey-style PDF sent to this chat</i>\n\n` + | |
| `π¨ <b>CREATE</b>\n` + | |
| `/gash [prompt] β AI Image Creator\n\n` + | |
| `π§ͺ <b>ENGINES</b>\n` + | |
| `/turbo β Switch to Fast Mode\n` + | |
| `/reason β Switch to Think Mode\n\n` + | |
| `π <b>DEPLOY</b>\n` + | |
| `/sync β Push code to HF Space\n` + | |
| `/push [db|reports|all] β Push data to HF Dataset\n\n` + | |
| `π§ <b>KNOWLEDGE</b>\n` + | |
| `<i>"remember [x]"</i> β Save a playbook\n\n` + | |
| `π οΈ <b>SYSTEM</b>\n` + | |
| `/newskill [name] [desc] β Auto-code an agent\n` + | |
| `/agents β List active agent skills\n\n` + | |
| `π <b>SEO ENGINE</b>\n` + | |
| `/seo [keyword] β Architect SEO Pillar Strategy\n` + | |
| `/write [siteId] [size] [topic] β Publish SEO Article\n` + | |
| `/seo2offer [keyword] β Full pipeline: scout β offer β article β publish β index\n` + | |
| `/index [url] β Submit to Google API\n\n` + | |
| `π° <b>SALES ENGINE</b>\n` + | |
| `/offer β List active offers with payment links\n` + | |
| `/offer [topic] β Create offer (AI copy β Mayar link)\n` + | |
| `/offer pause [id] β Pause underperforming offer\n` + | |
| `/offer ab [id] β Create A/B test variant\n` + | |
| `/offer variants [id] β View variant stats\n` + | |
| `/offer board β Pipeline stage counts\n` + | |
| `/offer eval β Run offer evaluation\n` + | |
| `/revenue β Revenue summary\n` + | |
| `/revenue today β Today's sales only\n\n` + | |
| `π¬ <b>VIDEO ENGINE</b>\n` + | |
| `/videos create [topic] β AI-generate + render video\n` + | |
| `/videos templates β List video templates\n` + | |
| `/videos status β Render queue status\n\n` + | |
| `π <b>RESEARCH</b>\n` + | |
| `/scout [keyword] β Market viability scout\n` + | |
| `/costs β API spend breakdown\n` + | |
| `/landing β Your link-in-bio page\n\n` + | |
| `π― <b>CEO AGENT</b>\n` + | |
| `/ceo β Dashboard summary\n` + | |
| `/ceo briefing β Morning briefing\n` + | |
| `/ceo client add/list β Client pipeline\n` + | |
| `/ceo plan β Weekly content plan\n` + | |
| `/ceo visibility [brand] β AI visibility audit\n` + | |
| `/ceo check [brand] β Background check\n` + | |
| `/ceo pulse β System health\n` + | |
| `/ceo hosting list β Hosting & domains\n` + | |
| `/ceo meeting list β Meetings\n` + | |
| `/ceo funnel β DigitalFinese stats\n\n` + | |
| `π <b>CRM MANAGER</b>\n` + | |
| `/updates β View Trello project statuses\n` + | |
| `/move [4-char-ID] [new list] β Update card status`; | |
| await apiCaller.sendTelegramMessage(chatId, startMsg); | |
| } else if (userText.toLowerCase().startsWith('/newskill')) { | |
| const parts = userText.split(' '); | |
| const skillName = parts[1]; | |
| const skillDesc = parts.slice(2).join(' '); | |
| if (!skillName || !skillDesc) { | |
| await apiCaller.sendTelegramMessage(chatId, "β Usage: /newskill [name] [description]"); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `π οΈ <b>Architecting Skill:</b> ${skillName}...\nWriting code via LLM...`); | |
| const genRes = await skillCreator.generateSkillCode(skillName, skillDesc); | |
| if (genRes.success) { | |
| const saveRes = await skillCreator.saveSkill(skillName, genRes.code); | |
| if (saveRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Skill '${saveRes.name}' Created!</b>\nCode saved to <code>skills/${saveRes.name}.js</code>\n\nRun <b>/sync</b> to push to cloud, then register it in <code>server.js</code>.`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β Failed to save skill: ${saveRes.error}`); | |
| } | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β Code generation failed: ${genRes.error}`); | |
| } | |
| } | |
| } else if (userText.toLowerCase().startsWith('/agents')) { | |
| const files = fs.readdirSync(path.join(__dirname, 'skills')).filter(f => f.endsWith('.js')); | |
| const list = files.map(f => `β’ <code>${f}</code>`).join('\n'); | |
| await apiCaller.sendTelegramMessage(chatId, `π€ <b>Active Skills (${files.length}):</b>\n\n${list}`); | |
| } else if (userText.toLowerCase().startsWith('/gash')) { | |
| const gashArgs = userText.split(' ').slice(1).join(' '); | |
| if (!gashArgs) { | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π¨ <b>/gash β AI Image Creator</b>\n\n` + | |
| `<b>Usage:</b>\n` + | |
| `/gash [prompt] [ratio]\n\n` + | |
| `<b>Ratios:</b> 9:16, 16:9, 4:5, 4:3, 1:1 (default)\n\n` + | |
| `<b>Examples:</b>\n` + | |
| `/gash A futuristic city at sunset 16:9\n` + | |
| `/gash Professional headshot minimalistic 4:5\n\n` + | |
| `<b>πΈ Face Mode:</b> Send a photo with caption <code>/gash [prompt]</code> to use the face` | |
| ); | |
| } else { | |
| // Parse ratio from the end of prompt | |
| const ratioMatch = gashArgs.match(/\s+(9:16|16:9|4:5|4:3|1:1)\s*$/); | |
| const ratio = ratioMatch ? ratioMatch[1] : '1:1'; | |
| const gashPrompt = ratioMatch ? gashArgs.replace(ratioMatch[0], '').trim() : gashArgs; | |
| await apiCaller.sendTelegramMessage(chatId, `π¨ <i>Creating image (${ratio})...</i>\n<i>${gashPrompt.substring(0, 80)}</i>`); | |
| // Parse ratio to width/height for image gen | |
| const ratioMap = { '9:16': {w:768,h:1344}, '16:9': {w:1344,h:768}, '4:5': {w:896,h:1120}, '4:3': {w:1152,h:896}, '1:1': {w:1024,h:1024} }; | |
| const dims = ratioMap[ratio] || ratioMap['1:1']; | |
| const gashResult = await apiCaller.generateImage(gashPrompt); | |
| if (gashResult.success && gashResult.buffer) { | |
| // Save image to HF + get link | |
| let hfLinkCmd = ''; | |
| try { | |
| const hfRes = await hfStorage.saveFile(`images/gash_${Date.now()}.jpg`, gashResult.buffer, 'VinOS: Image gen'); | |
| if (hfRes.success) hfLinkCmd = `\nπ <a href="${hfRes.url}">View on HF</a>`; | |
| } catch (e) { console.error('[HF Auto] Image save failed:', e.message); } | |
| const FormData = require('form-data'); | |
| const gForm = new FormData(); | |
| gForm.append('chat_id', chatId); | |
| gForm.append('photo', gashResult.buffer, { filename: 'gash.jpg', contentType: 'image/jpeg' }); | |
| gForm.append('caption', `β¨ <b>Image Created!</b>\nπ ${ratio}\nπ§ ${gashResult.source}\n<i>${gashPrompt.substring(0, 100)}</i>${hfLinkCmd}`); | |
| gForm.append('parse_mode', 'HTML'); | |
| try { | |
| await apiCaller.axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendPhoto`, gForm, { headers: gForm.getHeaders() }); | |
| } catch (photoErr) { | |
| console.error("[Gash] Ratio upload error:", photoErr.response?.data || photoErr.message); | |
| await apiCaller.sendTelegramMessage(chatId, `β¨ Image generated (${gashResult.source}) but photo upload failed.`); | |
| } | |
| // Log to Trello | |
| try { | |
| await trelloManager.logImageGen(gashPrompt, gashResult.source, hfLinkCmd ? hfLinkCmd.match(/href="([^"]+)"/)?.[1] : ''); | |
| } catch (e) { console.error('[Trello] Image log failed:', e.message); } | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β Image generation failed: ${gashResult.error || 'All providers exhausted'}`); | |
| } | |
| } | |
| } else if (userText.toLowerCase().startsWith('/seo2offer')) { | |
| const keyword = userText.split(' ').slice(1).join(' ').trim(); | |
| if (!keyword) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Usage: <code>/seo2offer [keyword]</code>\nFull pipeline: scout β offer β article β publish β index`); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β‘β‘β‘β‘β‘] Scouting: "${keyword}"...`); | |
| // 1. Scout market viability | |
| let scoutResult; | |
| try { scoutResult = await scoutAgent.runScout(keyword); } catch (e) { scoutResult = 'Scout unavailable'; } | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β‘β‘β‘β‘] Creating offer + Mayar link...`); | |
| // 2. Create offer | |
| const offerResult = await salesEngine.createOfferFromTopic(keyword, 49000); | |
| let offerMsg = ''; | |
| if (offerResult.success) { | |
| offerMsg = `β Offer: ${offerResult.offer.title}\nπ³ ${offerResult.offer.paymentUrl || 'Pending'}`; | |
| } else { | |
| offerMsg = `β οΈ Offer: ${offerResult.verdict || 'skipped'} (${offerResult.reason || offerResult.error || ''})`; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β β‘β‘β‘] Writing SEO article...`); | |
| // 3. Write article | |
| let sites = {}; | |
| try { sites = JSON.parse(process.env.WP_SITES_JSON || "{}"); } catch(e){} | |
| const siteKeys = Object.keys(sites); | |
| const targetSite = siteKeys.length > 0 ? siteKeys[0] : null; | |
| const articleRes = await seoWriter.generateArticle('medium', '3rd person', 'Educational', 'Authoritative', keyword, ''); | |
| if (!articleRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ Article failed: ${articleRes.error}\n\n${offerMsg}\n\nπ΅οΈ <b>Scout:</b>\n${scoutResult}`); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β β β‘β‘] Publishing to WordPress (CTA auto-injected)...`); | |
| // 4. Publish (CTA injection happens inside publisher automatically) | |
| const pubRes = await wordpressPublisher.publishPost(articleRes, targetSite); | |
| if (!pubRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β οΈ Publish failed: ${pubRes.error}\n\n${offerMsg}\n\nπ΅οΈ <b>Scout:</b>\n${scoutResult}`); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>SEO-to-Revenue Pipeline</b>\n[β β β β β β‘] Indexing on Google...`); | |
| // 5. Index | |
| const idxRes = await googleIndexer.submitUrl(pubRes.url); | |
| // 6. Final report | |
| let finalMsg = `π― <b>SEO-to-Revenue Complete!</b>\n\n`; | |
| finalMsg += `π <b>Article:</b> ${pubRes.url}\n`; | |
| finalMsg += `π <b>Google:</b> ${idxRes.success ? 'Indexed π’' : 'Pending π‘'}\n`; | |
| finalMsg += `${offerMsg}\n\n`; | |
| finalMsg += `π΅οΈ <b>Scout Report:</b>\n${typeof scoutResult === 'string' ? scoutResult.substring(0, 500) : 'N/A'}`; | |
| await apiCaller.sendTelegramMessage(chatId, finalMsg); | |
| } else if (userText.toLowerCase().startsWith('/seo')) { | |
| const keyword = userText.split(' ').slice(1).join(' '); | |
| if (!keyword) { | |
| await apiCaller.sendTelegramMessage(chatId, "β Usage: /seo [main keyword]"); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `πΊοΈ <b>SEO Engine:</b> Mapping Pillar Strategy for "${keyword}"...`); | |
| const strategyRes = await seoWriter.architectSeoStrategy(keyword); | |
| if (strategyRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>SEO Blueprint Generated:</b>\n\n${strategyRes.data}\n\n<i>To write an article for any of these clusters, reply with <code>/write [Choose a Blog Title]</code></i>`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β Strategy mapping failed: ${strategyRes.error}`); | |
| } | |
| } | |
| } else if (userText.toLowerCase().startsWith('/write')) { | |
| // Parse arguments smartly handling quotes: /write site "Topic" size "POV" "Intent" "Tone" | |
| const args = userText.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []; | |
| // Remove quotes from matched strings | |
| const cleanArgs = args.map(arg => arg.replace(/^["'](.*)["']$/, '$1')); | |
| let sites = {}; | |
| try { sites = JSON.parse(process.env.WP_SITES_JSON || "{}"); } catch(e){} | |
| const siteKeys = Object.keys(sites); | |
| const defaultSite = siteKeys.length > 0 ? siteKeys[0] : 'None'; | |
| if (cleanArgs.length < 7) { | |
| const helpMsg = `β οΈ <b>Missing Parameters for /write</b>\n\n` + | |
| `To architect an elite SEO article, I need instructions. Please use quotes for multi-word parameters.\n\n` + | |
| `<b>Structure:</b>\n` + | |
| `/write [SiteID] "[Topic]" [Size] "[POV]" "[Intent]" "[Tone]"\n\n` + | |
| `<b>Available Site IDs:</b> ${siteKeys.join(', ') || 'Configure WP_SITES_JSON'}\n` + | |
| `<b>Sizes:</b> short, medium, dense\n` + | |
| `<b>POV:</b> "1st person singular", "1st person plural", "2nd person", "3rd person"\n\n` + | |
| `<b>Example:</b>\n` + | |
| `/write ${defaultSite} "Agentic Workflow" medium "1st person plural" "Educational" "Authoritative"`; | |
| await apiCaller.sendTelegramMessage(chatId, helpMsg); | |
| } else { | |
| const targetSiteId = cleanArgs[1]; | |
| const topic = cleanArgs[2]; | |
| const size = cleanArgs[3].toLowerCase(); | |
| const pov = cleanArgs[4]; | |
| const intent = cleanArgs[5]; | |
| const tone = cleanArgs[6]; | |
| if (!siteKeys.includes(targetSiteId)) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Unknown Site ID: ${targetSiteId}\nAvailable: ${siteKeys.join(', ')}`); | |
| return; | |
| } | |
| // Bug #4 fix: use DB-backed pending_commands instead of volatile global | |
| const wDb = memory.readDB(); | |
| if (!wDb.pending_commands) wDb.pending_commands = {}; | |
| wDb.pending_commands[chatId] = { type: 'write_flow', targetSiteId, topic, size, pov, intent, tone, ts: Date.now() }; | |
| memory.writeDB(wDb); | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π€ <b>Context Check for "${topic}"</b>\n\n` + | |
| `Before I write, I want to make sure I get this right.\n\n` + | |
| `β’ What is "${topic}" about in your context?\n` + | |
| `β’ Any specific angle or key points?\n` + | |
| `β’ Target audience?\n\n` + | |
| `Reply with details, or send <b>"go"</b> to let me decide.` | |
| ); | |
| } | |
| } else if (global.pendingResearch && global.pendingResearch[chatId]) { | |
| const pending = global.pendingResearch[chatId]; | |
| delete global.pendingResearch[chatId]; | |
| const angle = (userText.toLowerCase() === 'skip' || userText.toLowerCase() === 'go') ? '' : userText; | |
| const url = pending.url; | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Research Auto-Pilot</b>\nAnalyzing URL with focus: <i>${angle || 'General'}</i>...`); | |
| const researchModule = require('./research'); | |
| const aiContent = require('./ai-content'); | |
| const imageGen = require('./image-gen'); | |
| const scheduler = require('./scheduler'); | |
| const scrapeRes = await researchModule.scrapePost(url, angle); | |
| if (!scrapeRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Scrape failed: ${scrapeRes.error}`); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π§ Scrape successful! Remixing content...`); | |
| const remixRes = await aiContent.remix(scrapeRes.data); | |
| if (!remixRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β AI Remix failed: ${remixRes.error}`); | |
| return; | |
| } | |
| const v1 = remixRes.data.variant_1; | |
| await apiCaller.sendTelegramMessage(chatId, `π¨ Generating high-conversion visual...`); | |
| const imgRes = await imageGen.generateAndUpload(v1.visual_prompt, 'res_v1'); | |
| if (imgRes.success) v1.mediaUrl = imgRes.publicUrl; | |
| // Save draft | |
| const draftId = `res_${Date.now().toString().slice(-6)}`; | |
| const draftPost = { | |
| id: draftId, | |
| status: 'draft', | |
| created: new Date().toISOString(), | |
| input: url, | |
| platform: scrapeRes.data.platform || 'web', | |
| sourceData: scrapeRes.data, | |
| type: 'standard', | |
| v1, | |
| v2: remixRes.data.variant_2, | |
| v3: remixRes.data.variant_3 | |
| }; | |
| scheduler.config.posts.push(draftPost); | |
| scheduler.saveDB(); | |
| if (imgRes.success && v1.mediaUrl) { | |
| const FormData = require('form-data'); | |
| const fForm = new FormData(); | |
| fForm.append('chat_id', chatId); | |
| fForm.append('photo', v1.mediaUrl.startsWith('/') ? fs.createReadStream(path.join(__dirname, 'public', v1.mediaUrl)) : v1.mediaUrl); | |
| fForm.append('caption', `β <b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`); | |
| fForm.append('parse_mode', 'HTML'); | |
| await apiCaller.axiosIPv4.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendPhoto`, fForm, { headers: fForm.getHeaders() }); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Research Draft Ready!</b>\n\n${v1.ig_en.substring(0, 800)}...\n\nReview in Dashboard.`); | |
| } | |
| } else if (global.pendingWrites && global.pendingWrites[chatId]) { | |
| // --- HANDLE CONTEXT REPLY FOR PENDING /write --- | |
| const pending = global.pendingWrites[chatId]; | |
| delete global.pendingWrites[chatId]; | |
| const userContext = userText.toLowerCase() === 'go' ? '' : userText; | |
| const { targetSiteId, topic, size, pov, intent, tone } = pending; | |
| const initRes = await apiCaller.sendTelegramMessage(chatId, `π <b>SEO Engine Pipeline</b>\n[β β‘β‘β‘β‘β‘] 16% AI Architecting: "${topic}"...`); | |
| const msgId = initRes.messageId; | |
| const articleRes = await seoWriter.generateArticle(size, pov, intent, tone, topic, userContext); | |
| if (articleRes.success) { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β‘β‘β‘β‘] 33% QC: Scoring EEAT Quality...`); | |
| const scoreRes = await seoWriter.scoreEEAT(articleRes.html); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β‘β‘β‘] 50% Image Engine: Generating ${articleRes.imagePlaceholders?.length || 0} AI Images...`); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β‘β‘] 66% Publishing: Uploading to [${targetSiteId}]...`); | |
| const pubRes = await wordpressPublisher.publishPost(articleRes, targetSiteId); | |
| if (pubRes.success) { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β β‘] 83% SEO Indexing: Google Search Console...`); | |
| const idxRes = await googleIndexer.submitUrl(pubRes.url); | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `π <b>SEO Engine Pipeline</b>\n[β β β β β β ] 100% CRM Sync: Trello Workflow...`); | |
| const trelloRes = await trelloManager.logSeoPost(topic, targetSiteId || 'Default', pubRes.url, scoreRes.score || 'N/A'); | |
| let finalMsg = `π <b>Pipeline Complete!</b>\n\n`; | |
| finalMsg += `<b>π Article:</b> ${pubRes.url}\n`; | |
| if (scoreRes.success) finalMsg += `<b>π§ EEAT Score:</b> ${scoreRes.score}/100\n`; | |
| if (idxRes.success) finalMsg += `<b>π Google:</b> Indexed π’\n`; | |
| else finalMsg += `<b>π Google:</b> Pending π‘ (${idxRes.error})\n`; | |
| if (trelloRes.success) { | |
| finalMsg += `<b>π Trello:</b> <a href="${trelloRes.url}">View Card</a> π\n`; | |
| } else { | |
| finalMsg += `<b>π Trello:</b> Failed π΄\n`; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, finalMsg); | |
| // Auto-push SEO publish metadata to HF | |
| try { | |
| await hfStorage.saveRecord('seo', `seo_${Date.now()}`, { | |
| title: `SEO: ${topic}`, | |
| timestamp: new Date().toISOString(), | |
| source_url: pubRes.url, | |
| niche: 'SEO' | |
| }, `# Published: ${topic}\n\n- URL: ${pubRes.url}\n- Site: ${targetSiteId || 'Default'}\n- EEAT Score: ${scoreRes.score || 'N/A'}\n- Google Indexed: ${idxRes.success}\n- Trello: ${trelloRes.success ? trelloRes.url : 'N/A'}`); | |
| } catch (e) { | |
| console.error('[HF Auto] SEO log failed:', e.message); | |
| } | |
| } else { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `β οΈ <b>Pipeline Failed</b>\nβ WordPress Error: ${pubRes.error}`); | |
| } | |
| } else { | |
| if (msgId) await apiCaller.editTelegramMessage(chatId, msgId, `β οΈ <b>Pipeline Failed</b>\nβ Writer Error: ${articleRes.error}`); | |
| } | |
| } else if (userText.toLowerCase().startsWith('/updates')) { | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Checking Trello CRM...</b>`); | |
| const updateRes = await trelloManager.getProjectUpdates(); | |
| if (updateRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, updateRes.report); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β **Failed to fetch updates:** ${updateRes.error}`); | |
| } | |
| } else if (userText.toLowerCase().startsWith('/move')) { | |
| const parts = userText.split(' '); | |
| if (parts.length < 3) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Usage: /move [4-char-ID] [New List Name]`); | |
| } else { | |
| const shortId = parts[1]; | |
| const newListName = parts.slice(2).join(' '); | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Attempting to move card...</b>`); | |
| const moveRes = await trelloManager.moveCard(shortId, newListName); | |
| if (moveRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Success!</b>\nMoved card "${moveRes.cardName}" to π <b>${moveRes.listName}</b>.`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β **Move Failed:** ${moveRes.error}`); | |
| } | |
| } | |
| } else if (userText.toLowerCase().startsWith('/index')) { | |
| const url = userText.split(' ')[1]; | |
| if (!url) { | |
| await apiCaller.sendTelegramMessage(chatId, "β Usage: /index [url]"); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `π Submitting to Google Indexing...`); | |
| const idxRes = await googleIndexer.submitUrl(url); | |
| if (idxRes.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Successfully submitted to Google.</b>`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β <b>Indexing Failed:</b> ${idxRes.error}`); | |
| } | |
| } | |
| // ========== SALES ENGINE COMMANDS ========== | |
| } else if (userText.toLowerCase().startsWith('/offer')) { | |
| const args = userText.split(' ').slice(1).join(' ').trim(); | |
| if (!args) { | |
| // /offer β list active offers | |
| const dash = salesEngine.getDashboardData(); | |
| if (dash.activeOffers.length === 0) { | |
| await apiCaller.sendTelegramMessage(chatId, 'π¦ No active offers.\n\nCreate one: <code>/offer [topic]</code>'); | |
| } else { | |
| const lines = dash.activeOffers.map(o => | |
| `β’ <b>${o.title}</b>\n π΅ Rp ${(o.priceIDR || 0).toLocaleString()} | π ${o.totalSales} sales | π° Rp ${(o.totalRevenue || 0).toLocaleString()}\n ${o.paymentUrl ? `π ${o.paymentUrl}` : 'β³ Link pending'}\n <code>${o.id}</code>` | |
| ).join('\n\n'); | |
| await apiCaller.sendTelegramMessage(chatId, `π¦ <b>Active Offers (${dash.activeCount})</b>\n\n${lines}`); | |
| } | |
| } else if (args.toLowerCase().startsWith('pause ')) { | |
| // /offer pause [id] | |
| const offerId = args.split(' ')[1]; | |
| const result = salesEngine.pauseOffer(offerId); | |
| if (result.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `βΈοΈ Offer paused: <b>${result.offer.title}</b>`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β ${result.error}`); | |
| } | |
| } else if (args.toLowerCase().startsWith('retire ')) { | |
| // /offer retire [id] | |
| const offerId = args.split(' ')[1]; | |
| const result = salesEngine.retireOffer(offerId); | |
| if (result.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `ποΈ Offer retired: <b>${result.offer.title}</b>`); | |
| } else { | |
| await apiCaller.sendTelegramMessage(chatId, `β ${result.error}`); | |
| } | |
| } else if (args.toLowerCase() === 'board') { | |
| // /offer board β pipeline stage counts | |
| const db = salesEngine.readSalesDB(); | |
| const counts = {}; | |
| for (const o of db.offers) { counts[o.status] = (counts[o.status] || 0) + 1; } | |
| const stages = ['active', 'pending_link', 'pending_validation', 'paused', 'retired']; | |
| const emoji = { active: 'π’', pending_link: 'π‘', pending_validation: 'π΅', paused: 'βΈοΈ', retired: 'π΄' }; | |
| const lines = stages.filter(s => counts[s]).map(s => `${emoji[s] || 'β’'} ${s}: ${counts[s]}`).join('\n'); | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Offer Pipeline</b>\n\n${lines || 'No offers yet.'}\n\nTotal: ${db.offers.length}`); | |
| } else if (args.toLowerCase().startsWith('ab ')) { | |
| // /offer ab [id] β create A/B variant | |
| const offerId = args.split(' ')[1]; | |
| await apiCaller.sendTelegramMessage(chatId, `π§ͺ <i>Creating variant for ${offerId}...</i>`); | |
| const result = await salesEngine.createVariant(offerId); | |
| if (!result.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β ${result.error}`); | |
| } | |
| } else if (args.toLowerCase().startsWith('variants ')) { | |
| // /offer variants [id] β show all variants | |
| const offerId = args.split(' ')[1]; | |
| const result = salesEngine.getVariants(offerId); | |
| if (!result.success) { | |
| await apiCaller.sendTelegramMessage(chatId, `β ${result.error}`); | |
| } else { | |
| const lines = result.variants.map(v => { | |
| const convRate = v.clicks > 0 ? ((v.sales / v.clicks) * 100).toFixed(1) : '0.0'; | |
| return `π <b>${v.id}:</b> ${v.title}\n π ${v.sales} sales | ${v.clicks} clicks | ${convRate}% conv | ${v.isActive ? 'π’ Active' : 'π΄ Paused'}`; | |
| }).join('\n\n'); | |
| await apiCaller.sendTelegramMessage(chatId, `π§ͺ <b>Variants: ${result.offerTitle || offerId}</b>\n\n${lines}`); | |
| } | |
| } else if (args.toLowerCase().startsWith('eval')) { | |
| // /offer eval β run evaluation | |
| await apiCaller.sendTelegramMessage(chatId, 'π <i>Evaluating offers...</i>'); | |
| const result = await salesEngine.evaluateOffers(); | |
| await apiCaller.sendTelegramMessage(chatId, `β Evaluation done. ${result.actions.length} actions taken.`); | |
| } else { | |
| // /offer [topic] β create new offer | |
| await apiCaller.sendTelegramMessage(chatId, `π° <i>Creating offer for: ${args}...</i>\n<i>AI copy β Trend check β Mayar link</i>`); | |
| const result = await offerArchitect(args); | |
| if (result.success) { | |
| // Notification already sent by salesEngine | |
| } else { | |
| const msg = result.verdict === 'wait' | |
| ? `β³ <b>Topic queued for re-check</b>\nScore: ${result.score}/100\n${result.reason}` | |
| : result.verdict === 'skip' | |
| ? `βοΈ <b>Topic skipped</b>\nScore: ${result.score}/100\n${result.reason}` | |
| : `β Offer creation failed: ${result.reason}`; | |
| await apiCaller.sendTelegramMessage(chatId, msg); | |
| } | |
| } | |
| } else if (userText.toLowerCase().startsWith('/revenue')) { | |
| const args = userText.split(' ').slice(1).join(' ').trim().toLowerCase(); | |
| const dash = salesEngine.getDashboardData(); | |
| const rev = dash.revenue; | |
| if (args === 'today') { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const todaySales = rev.dailyLog.filter(d => d.date === today); | |
| const todayTotal = todaySales.reduce((s, d) => s + d.amount, 0); | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π <b>Today's Revenue</b>\n\n` + | |
| `π΅ Rp ${todayTotal.toLocaleString()}\n` + | |
| `π ${todaySales.length} transactions\n\n` + | |
| (todaySales.length > 0 ? todaySales.map(d => `β’ Rp ${d.amount.toLocaleString()} (${d.offerId})`).join('\n') : '<i>No sales today yet.</i>') | |
| ); | |
| } else { | |
| // Top offer by revenue | |
| const topOffer = dash.activeOffers.sort((a, b) => (b.totalRevenue || 0) - (a.totalRevenue || 0))[0]; | |
| // Pillar breakdown | |
| const pillarLines = Object.entries(rev.byPillar || {}).map(([p, amt]) => `β’ ${p}: Rp ${amt.toLocaleString()}`).join('\n'); | |
| await apiCaller.sendTelegramMessage(chatId, | |
| `π° <b>Revenue Dashboard</b>\n\n` + | |
| `π Total: <b>Rp ${(rev.total || 0).toLocaleString()}</b>\n` + | |
| `π This Month: <b>Rp ${(rev.thisMonth || 0).toLocaleString()}</b>\n` + | |
| `π¦ Active Offers: ${dash.activeCount}\n` + | |
| `π Total Transactions: ${dash.recentTransactions.length}\n\n` + | |
| (topOffer ? `π <b>Top Offer:</b> ${topOffer.title} (${topOffer.totalSales} sales, Rp ${(topOffer.totalRevenue || 0).toLocaleString()})\n\n` : '') + | |
| (pillarLines ? `π <b>By Pillar:</b>\n${pillarLines}` : '') | |
| ); | |
| } | |
| } else if (userText.toLowerCase().startsWith('/costs')) { | |
| const costs = costTracker.getCosts(); | |
| let msg = `πΈ <b>API Cost Tracker</b>\n\n`; | |
| msg += `π <b>Total Spend:</b> $${(costs.total || 0).toFixed(4)}\n\n`; | |
| const models = Object.entries(costs.by_model || {}).sort((a, b) => b[1] - a[1]); | |
| if (models.length > 0) { | |
| msg += `<b>By Model:</b>\n`; | |
| for (const [model, cost] of models) { | |
| msg += `β’ ${model}: $${cost.toFixed(4)}\n`; | |
| } | |
| } else { | |
| msg += `<i>No costs tracked yet.</i>`; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, msg); | |
| } else if (userText.toLowerCase().startsWith('/scout')) { | |
| const keyword = userText.split(' ').slice(1).join(' ').trim(); | |
| if (!keyword) { | |
| await apiCaller.sendTelegramMessage(chatId, `β Usage: <code>/scout [keyword]</code>\nExample: /scout AI productivity tools`); | |
| return; | |
| } | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Scouting:</b> "${keyword}"...\nAnalyzing competition, volume, and angles...`); | |
| const result = await scoutAgent.runScout(keyword); | |
| await apiCaller.sendTelegramMessage(chatId, `π΅οΈ <b>Scout Report: ${keyword}</b>\n\n${result}`); | |
| } else if (userText.toLowerCase().startsWith('/landing')) { | |
| const publicUrl = process.env.PUBLIC_URL || `https://${process.env.SPACE_ID || 'aigoose-vinos-engine'}.hf.space`; | |
| await apiCaller.sendTelegramMessage(chatId, `π <b>Your Landing Page:</b>\n${publicUrl}/landing\n\nShare this link as your link-in-bio!`); | |
| } else { | |
| await handleVinIntent(chatId, from, userText); | |
| } | |
| } | |
| })().catch(err => console.error("Webhook error:", err)); | |
| }); | |
| // Resilient Startup | |
| async function initializeVinOS() { | |
| console.log("π Initializing VinOS (Cloud Mode)..."); | |
| const PORT = process.env.PORT || 7860; | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(`β VinOS Online: http://0.0.0.0:${PORT}`); | |
| autoCron.startJobs(); | |
| }); | |
| try { | |
| await setTelegramMenu(); | |
| } catch (e) { | |
| console.error("β οΈ Startup components delayed:", e.message); | |
| } | |
| } | |
| initializeVinOS(); | |