Hookd Relay: Express server bridging extension to Horus AI agents
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user