diff --git a/bin/antfarm-poller.mjs b/bin/antfarm-poller.mjs new file mode 100755 index 0000000..e42f993 --- /dev/null +++ b/bin/antfarm-poller.mjs @@ -0,0 +1,92 @@ +import { appendFileSync } from 'node:fs'; +import { randomUUID } from 'node:crypto'; +import https from 'node:https'; + +// Minimal Ant Farm API poller for the IDE Agent Kit +// Runs standalone and appends new messages to the shared queue file + +const config = { + apiKey: process.env.ANTFARM_API_KEY, + rooms: ['thinkoff-development', 'feature-admin-planning'], + queuePath: './ide-agent-queue.jsonl', + pollIntervalMs: 30000, // 30s + botHandles: ['@claudemm', '@ether', '@geminiMB', '@antigravity', '@sallygp'] // ignore ours +}; + +const lastSeen = {}; // room -> last message id + +function fetchMessages(room) { + return new Promise((resolve, reject) => { + const req = https.request({ + hostname: 'antfarm.world', + path: `/api/v1/rooms/${room}/messages?limit=20`, + method: 'GET', + headers: { 'X-API-Key': config.apiKey } + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode >= 400) return reject(new Error(`API Error: ${res.statusCode}`)); + try { resolve(JSON.parse(data).messages.reverse()); } // oldest first + catch (e) { reject(e); } + }); + }); + req.on('error', reject); + req.end(); + }); +} + +function processRoom(room) { + fetchMessages(room).then(messages => { + let newMessages = []; + if (!lastSeen[room]) { + // First run: just save latest ID, don't queue + if (messages.length > 0) lastSeen[room] = messages[messages.length - 1].id; + return; + } + + const lastIdx = messages.findIndex(m => m.id === lastSeen[room]); + if (lastIdx !== -1) { + newMessages = messages.slice(lastIdx + 1); + } else { + newMessages = messages; // all new? + } + + if (newMessages.length > 0) { + lastSeen[room] = newMessages[newMessages.length - 1].id; + newMessages.forEach(msg => { + if (config.botHandles.includes(msg.from)) return; // skip bots + + const event = { + trace_id: randomUUID(), + event_id: msg.id, + source: 'antfarm', + kind: 'antfarm.message.created', + timestamp: new Date().toISOString(), + room: room, + actor: { login: msg.from, name: msg.from_name }, + payload: { body: msg.body } + }; + appendFileSync(config.queuePath, JSON.stringify(event) + '\n'); + console.log(`[${event.timestamp}] Queued Ant Farm message in ${room} from ${msg.from}`); + }); + } + }).catch(err => console.error(`[Poll Error in ${room}]:`, err.message)); +} + +function start() { + if (!config.apiKey) { + console.error('Error: ANTFARM_API_KEY environment variable is required'); + process.exit(1); + } + console.log(`Starting Ant Farm Poller for IDE Agent Kit...`); + console.log(`Rooms: ${config.rooms.join(', ')}`); + console.log(`Interval: ${config.pollIntervalMs}ms`); + console.log(`Queue: ${config.queuePath}`); + + const tick = () => config.rooms.forEach(processRoom); + tick(); // run once immediately + setInterval(tick, config.pollIntervalMs); +} + +start(); diff --git a/package.json b/package.json index 9ed1db3..1359727 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "Let IDE AIs participate in team workflows — webhook relay, tmux runner, receipts", "type": "module", "bin": { - "ide-agent-kit": "./bin/cli.mjs" + "ide-agent-kit": "./bin/cli.mjs", + "ide-antfarm-poller": "./bin/antfarm-poller.mjs" }, "scripts": { "test": "node --test test/*.test.mjs", diff --git a/src/antfarm-poller.mjs b/src/antfarm-poller.mjs new file mode 100644 index 0000000..b46f718 --- /dev/null +++ b/src/antfarm-poller.mjs @@ -0,0 +1,76 @@ +import { readFileSync, writeFileSync, appendFileSync, existsSync } from 'node:fs'; +import { randomUUID } from 'node:crypto'; +import { execSync } from 'node:child_process'; + +const DEFAULT_POLL_SEC = 30; + +export function pollAntFarm({ apiKey, rooms, seenFile, queuePath, onMessage }) { + const seen = loadSeen(seenFile); + let newCount = 0; + + for (const room of rooms) { + try { + const raw = execSync( + `curl -sS -H "X-API-Key: ${apiKey}" "https://antfarm.world/api/v1/rooms/${room}/messages?limit=10"`, + { encoding: 'utf8', timeout: 15000 } + ); + const data = JSON.parse(raw); + const msgs = data.messages || (Array.isArray(data) ? data : []); + + for (const m of msgs) { + const mid = m.id || ''; + if (!mid || seen.has(mid)) continue; + seen.add(mid); + + const handle = m.from || m.author?.handle || '?'; + const body = m.body || ''; + const ts = m.created_at || new Date().toISOString(); + + // Append normalized event to queue + const event = { + trace_id: randomUUID(), + event_id: mid, + source: 'antfarm', + kind: 'antfarm.message.created', + timestamp: ts, + room: room, + actor: { login: handle }, + payload: { body: body.slice(0, 500), room } + }; + appendFileSync(queuePath, JSON.stringify(event) + '\n'); + + if (onMessage) onMessage(event); + newCount++; + } + } catch (e) { + console.error(`[antfarm-poller] Error polling ${room}: ${e.message}`); + } + } + + saveSeen(seenFile, seen); + return newCount; +} + +function loadSeen(path) { + if (!existsSync(path)) return new Set(); + return new Set(readFileSync(path, 'utf8').trim().split('\n').filter(Boolean)); +} + +function saveSeen(path, seen) { + const ids = [...seen].slice(-500); + writeFileSync(path, ids.join('\n') + '\n'); +} + +export function startPollerLoop({ apiKey, rooms, seenFile, queuePath, intervalSec, onMessage }) { + const interval = (intervalSec || DEFAULT_POLL_SEC) * 1000; + console.log(`[antfarm-poller] Polling ${rooms.join(', ')} every ${intervalSec || DEFAULT_POLL_SEC}s`); + + // Initial poll + const n = pollAntFarm({ apiKey, rooms, seenFile, queuePath, onMessage }); + if (n > 0) console.log(`[antfarm-poller] ${n} new messages queued`); + + return setInterval(() => { + const count = pollAntFarm({ apiKey, rooms, seenFile, queuePath, onMessage }); + if (count > 0) console.log(`[antfarm-poller] ${count} new messages queued`); + }, interval); +}