Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions bin/antfarm-poller.mjs
Original file line number Diff line number Diff line change
@@ -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`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Paginate Ant Farm polling beyond fixed 20-message window

The poller always requests messages?limit=20 and does not paginate or otherwise backfill older unseen messages, so if a room gets more than 20 messages between polling intervals (or while the poller is down), the older unseen messages fall out of the window and are never enqueued. This causes silent event loss in busy rooms.

Useful? React with 👍 / 👎.

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();
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
76 changes: 76 additions & 0 deletions src/antfarm-poller.mjs
Original file line number Diff line number Diff line change
@@ -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);
}