// Hookd Relay — AI processing bridge between Chrome extension and Horus agents const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); const app = express(); app.use(cors()); app.use(express.json({ limit: '10mb' })); const ITEMS_FILE = '/root/.openclaw/workspace/chrome-extensions/hookd/hookd-items.json'; const RELAY_PORT = 4321; // Categories for AI classification const CATEGORIES = ['AI', 'News', 'Ideas', 'Memes', 'Other']; // Simple keyword-based classifier (fallback when no LLM available) function classifyWithKeywords(text) { const lower = text.toLowerCase(); if (/ai|gpt|llm|model|changelog|gpu|cuda|neural|openai|anthropic|gemini|claude|chatbot/.test(lower)) return 'AI'; if (/news|breaking|launch|report|announced|released|updated|version|update/.test(lower)) return 'News'; if (/idea|concept|thought|what if|imagine|inspiration| brainstorm/.test(lower)) return 'Ideas'; if (/meme|lmao|lol|funny|rofl|comedy|😂|wtf|brokie/.test(lower)) return 'Memes'; return 'Other'; } // Score relevance 1-10 based on keywords and engagement signals function scoreRelevance(text, item) { let score = 5; const lower = text.toLowerCase(); if (/important|breaking|urgent|alert/.test(lower)) score += 2; if (/tutorial|guide|how to|explained/.test(lower)) score += 1; if (/deal|discount|free|offer|save/.test(lower)) score += 1; if (item.type === 'dm') score += 1; return Math.min(10, score); } // --- Routes --- // Health / status app.get('/status', (req, res) => { res.json({ ok: true, relay: 'hookd', timestamp: new Date().toISOString() }); }); // Get all items (for Horus agents) app.get('/items', (req, res) => { try { if (!fs.existsSync(ITEMS_FILE)) return res.json({ items: [] }); const raw = fs.readFileSync(ITEMS_FILE, 'utf8'); res.json({ items: JSON.parse(raw) }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Save items from extension app.post('/items', (req, res) => { try { let existing = []; if (fs.existsSync(ITEMS_FILE)) { existing = JSON.parse(fs.readFileSync(ITEMS_FILE, 'utf8')); } const newItems = req.body.items || []; newItems.forEach(item => { item.id = item.id || Date.now(); item.saved = true; item.time = item.time || new Date().toISOString(); existing.unshift(item); }); fs.writeFileSync(ITEMS_FILE, JSON.stringify(existing, null, 2)); res.json({ success: true, count: newItems.length, total: existing.length }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Process items with AI (keyword-based + hook into Horus if available) app.post('/process', async (req, res) => { try { const { items } = req.body; if (!items || !Array.isArray(items)) return res.status(400).json({ error: 'items array required' }); const results = []; // Try to call Horus agent for AI analysis let agentAnalysis = null; try { // Call Horus via local Hermes CLI const { execSync } = require('child_process'); const content = items.map(i => `[${i.id}] @${i.author}: ${i.content || ''}`).join('\n'); const analysis = execSync( `cd /root/.hermes && echo '${content.replace(/'/g, "'\\''")}' | timeout 20 hermes --model claude-sonnet-4 --no-stream "Analyze these X posts and for each one respond with ONLY valid JSON: {id, score(1-10), category(AI/News/Ideas/Memes/Other), summary, tags}. Items: ${content}" 2>/dev/null`, { timeout: 25000 } ).toString(); agentAnalysis = JSON.parse(analysis); } catch { // Agent not available or parse failed — fall back to keywords } items.forEach(item => { const text = `${item.content || ''} ${item.author || ''}`; let result; if (agentAnalysis && agentAnalysis.find(a => a.id === item.id)) { result = agentAnalysis.find(a => a.id === item.id); } else { const category = classifyWithKeywords(text); const score = scoreRelevance(text, item); result = { id: item.id, category, score, summary: text.slice(0, 80) + '...', tags: [category, score >= 7 ? 'high-value' : 'normal'] }; } results.push(result); }); // Also save processed items to hookd-items.json if (fs.existsSync(ITEMS_FILE)) { const stored = JSON.parse(fs.readFileSync(ITEMS_FILE, 'utf8')); const updated = stored.map(item => { const processed = results.find(r => r.id === item.id); if (processed) { return { ...item, aiProcessed: true, ...processed }; } return item; }); fs.writeFileSync(ITEMS_FILE, JSON.stringify(updated, null, 2)); } res.json({ success: true, results }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Delete items app.delete('/items', (req, res) => { try { const { ids } = req.body; if (!fs.existsSync(ITEMS_FILE)) return res.json({ success: true, deleted: 0 }); let items = JSON.parse(fs.readFileSync(ITEMS_FILE, 'utf8')); const before = items.length; items = items.filter(i => !ids.includes(String(i.id))); fs.writeFileSync(ITEMS_FILE, JSON.stringify(items, null, 2)); res.json({ success: true, deleted: before - items.length }); } catch (e) { res.status(500).json({ error: e.message }); } }); // Export items as JSON (for download) app.get('/export', (req, res) => { try { if (!fs.existsSync(ITEMS_FILE)) return res.json({ items: [] }); const items = JSON.parse(fs.readFileSync(ITEMS_FILE, 'utf8')); res.setHeader('Content-Disposition', 'attachment; filename=hookd-export.json'); res.json({ items }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.listen(RELAY_PORT, '127.0.0.1', () => { console.log(`Hookd Relay running on http://127.0.0.1:${RELAY_PORT}`); });