Skip to main content
Glama

RanchHand

by anteew
http.mjs8.06 kB
import http from 'node:http'; import os from 'node:os'; import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import { embeddings, chatCompletion } from './src/oai_client.mjs'; import { store } from './src/store_memory.mjs'; import { getProfiles, mergeProfiles } from './src/profiles.mjs'; const HOST = process.env.RANCHHAND_HOST || '127.0.0.1'; const PORT = parseInt(process.env.RANCHHAND_PORT || '41414', 10); const TWI_DIR = path.join(os.homedir(), '.threadweaverinc'); const AUTH_DIR = path.join(TWI_DIR, 'auth'); const SECRET_FILE = process.env.TWI_SECRET_FILE || path.join(AUTH_DIR, 'shared_secret.txt'); function ensureDir(p) { try { fs.mkdirSync(p, { recursive: true }); } catch (_) {} } function ensureSecret() { ensureDir(AUTH_DIR); try { if (fs.existsSync(SECRET_FILE)) { const s = fs.readFileSync(SECRET_FILE, 'utf8').trim(); if (s) return s; } } catch (_) {} const rand = crypto.randomBytes(32).toString('hex'); try { fs.writeFileSync(SECRET_FILE, rand, { mode: 0o600 }); } catch (_) {} return rand; } const SECRET = ensureSecret(); function parseJson(req) { return new Promise((resolve, reject) => { let data = ''; req.on('data', (c) => { data += c; if (data.length > 10 * 1024 * 1024) { req.destroy(); reject(new Error('payload-too-large')); } }); req.on('end', () => { try { resolve(JSON.parse(data || '{}')); } catch (e) { reject(e); } }); req.on('error', reject); }); } function send(res, status, obj) { const body = JSON.stringify(obj); res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(body); } function unauthorized(res) { send(res, 401, { ok: false, error: 'unauthorized' }); } function smallSample(items, n = 3) { const out = []; for (const it of items.slice(0, n)) { out.push({ ts: it.ts, userName: it.userName || it.userId || null, textSnippet: String(it.text || '').slice(0, 160) }); } return out; } const server = http.createServer(async (req, res) => { try { const url = new URL(req.url || '/', `http://${req.headers.host}`); if (url.pathname === '/health') { return send(res, 200, { ok: true, service: 'ranchhand', host: HOST, port: PORT }); } if (req.method === 'GET' && url.pathname === '/profiles') { return send(res, 200, { ok: true, profiles: getProfiles() }); } if (req.method === 'POST' && url.pathname === '/profiles') { const body = await parseJson(req); const merged = mergeProfiles(body || {}); return send(res, 200, { ok: true, profiles: merged }); } // Auth check const token = req.headers['x-ranchhand-token']; if (token !== SECRET) return unauthorized(res); if (req.method === 'POST' && url.pathname === '/ingest/slack') { const body = await parseJson(req); const namespace = String(body?.namespace || '').trim(); const items = Array.isArray(body?.items) ? body.items : []; if (!namespace || !items.length) return send(res, 400, { ok: false, error: 'bad-request' }); const jobId = crypto.randomUUID(); // Chunk: naive split by ~512 words without overlap; future: token-aware const chunks = []; const CHUNK_WORDS = Math.max(64, Math.min(4096, parseInt(process.env.RH_CHUNK_WORDS || '512', 10))); for (const it of items) { const words = String(it.text || '').split(/\s+/); for (let i = 0; i < words.length; i += CHUNK_WORDS) { const part = words.slice(i, i + CHUNK_WORDS).join(' '); if (!part.trim()) continue; chunks.push({ ns: namespace, id: `${it.ts || ''}:${i/CHUNK_WORDS|0}`, text: part, metadata: { source: 'slack', ts: it.ts || null, userName: it.userName || null } }); } } // Embed (batch) const inputs = chunks.map(c => c.text); const prof = getProfiles(); const embedModel = prof?.embed?.model || undefined; let embs; try { const out = await embeddings({ model: embedModel, input: inputs }); embs = out?.data?.map(d => d.embedding) || []; } catch (e) { return send(res, 500, { ok: false, error: 'embed-failed', detail: String(e) }); } // Upsert const upserts = []; for (let i = 0; i < chunks.length; i++) { upserts.push({ id: `${chunks[i].id}`, vector: embs[i] || [], text: chunks[i].text, metadata: chunks[i].metadata }); } const u = store.upsertMany(namespace, upserts); const counts = { items: items.length, chunks: chunks.length, embeddings: u.count }; return send(res, 200, { ok: true, jobId, namespace, counts, sample: smallSample(items) }); } if (req.method === 'POST' && url.pathname === '/query') { const body = await parseJson(req); const ns = String(body?.namespace || '').trim(); const query = String(body?.query || ''); const topK = Math.max(1, Math.min(50, parseInt(body?.topK || '5', 10))); const withText = body?.withText !== false; if (!ns || !query) return send(res, 400, { ok: false, error: 'bad-request' }); let qvec; try { const out = await embeddings({ input: query }); qvec = out?.data?.[0]?.embedding || []; } catch (e) { return send(res, 500, { ok: false, error: 'embed-failed', detail: String(e) }); } const results = store.query(ns, qvec, topK, withText); return send(res, 200, { ok: true, results }); } if (req.method === 'POST' && url.pathname === '/answer') { const body = await parseJson(req); const ns = String(body?.namespace || '').trim(); const query = String(body?.query || ''); const topK = Math.max(1, Math.min(10, parseInt(body?.topK || '5', 10))); if (!ns || !query) return send(res, 400, { ok: false, error: 'bad-request' }); // Embed query let qvec; try { const out = await embeddings({ input: query }); qvec = out?.data?.[0]?.embedding || []; } catch (e) { return send(res, 500, { ok: false, error: 'embed-failed', detail: String(e) }); } // Retrieve context const results = store.query(ns, qvec, topK, true); const context = results.map((r, i) => `Source [${i+1}]:\n${String(r.text||'').slice(0, 800)}`).join('\n\n'); const prof = getProfiles(); const model = body?.model || prof?.summarize_retrieval?.model; const temperature = (typeof body?.temperature === 'number') ? body.temperature : (prof?.summarize_retrieval?.temperature ?? 0.1); const max_tokens = (typeof body?.max_tokens === 'number') ? body.max_tokens : (prof?.summarize_retrieval?.max_tokens ?? 256); const system = 'You are a helpful assistant. Answer concisely using only the provided sources. Cite sources with bracketed numbers like [1], [2]. If unsure, say you do not have enough information.'; const user = `Question:\n${query}\n\nContext:\n${context}`; try { const out = await chatCompletion({ model, messages: [{ role: 'system', content: system }, { role: 'user', content: user }], temperature, max_tokens }); const answer = out.text || ''; const citations = results.map((r, i) => ({ index: i+1, id: r.id, score: r.score, snippet: String(r.text||'').slice(0, 240), metadata: r.metadata })); return send(res, 200, { ok: true, answer, citations, used: { topK, model, temperature, max_tokens } }); } catch (e) { return send(res, 500, { ok: false, error: 'generate-failed', detail: String(e) }); } } if (req.method === 'GET' && url.pathname.startsWith('/secret')) { // For debugging local setup only; avoid exposing secret. return send(res, 200, { ok: true, present: !!SECRET_FILE }); } return send(res, 404, { ok: false, error: 'not-found' }); } catch (e) { return send(res, 500, { ok: false, error: String(e?.message || e) }); } }); server.listen(PORT, HOST, () => { // eslint-disable-next-line no-console console.log(`RanchHand HTTP listening at http://${HOST}:${PORT}`); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/anteew/RanchHand'

If you have feedback or need assistance with the MCP directory API, please join our Discord server