From 6a17223c906d0f009778074dc4d36ada32578d18 Mon Sep 17 00:00:00 2001 From: Petrus Pennanen Date: Sat, 21 Feb 2026 20:16:26 +0100 Subject: [PATCH 1/2] feat(cli): add antfarm room API poller script --- bin/antfarm-poller.mjs | 92 ++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100755 bin/antfarm-poller.mjs 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", From 34bd93fd0352cfb2fe55f884a4ad11b6fdf1fdad Mon Sep 17 00:00:00 2001 From: ThinkOffApp Date: Sat, 21 Feb 2026 21:16:53 +0200 Subject: [PATCH 2/2] Add Ant Farm room poller module for IDE Agent Kit Polls joined rooms via Ant Farm API, normalizes messages to antfarm.message.created events, appends to shared JSONL queue. Tracks seen IDs to avoid duplicates. Co-Authored-By: Claude Opus 4.6 --- src/antfarm-poller.mjs | 76 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/antfarm-poller.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); +}