← back to Butlr
routes/agent-log.js
38 lines
// Live AI-agent transcript route — reads pm2 log lines for a given call,
// parses the "[ai-agent] call=<id> heard:/reply:" entries, returns JSON.
// Used by the listen page to render a live transcript without depending
// on Twilio Media Streams (which require an account-level toggle).
const express = require('express');
const { execFile } = require('child_process');
const router = express.Router();
const CALL_ID_RE = /^[A-Za-z0-9_-]{6,16}$/;
router.get('/api/calls/:call_id/agent-log', (req, res) => {
const callId = req.params.call_id;
if (!CALL_ID_RE.test(callId)) {
return res.status(404).json({ ok: false, error: 'bad_id' });
}
// pm2 logs butlr --lines 1000 --nostream --raw → grep for this call
execFile('pm2', ['logs', 'butlr', '--lines', '1000', '--nostream', '--raw'], { timeout: 4000, maxBuffer: 2_000_000 }, (err, stdout) => {
if (err && !stdout) return res.json({ ok: true, turns: [], error: err.code || 'pm2_err' });
const lines = (stdout || '').split('\n');
const turns = [];
const heardRe = new RegExp(`\\[ai-agent\\] call=${callId} heard: "([^"]*)"\\s+turn=(\\d+)`);
const replyRe = new RegExp(`\\[ai-agent\\] call=${callId} reply: "([^"]*)"\\s+action=\\(([^)]*)\\)`);
for (const line of lines) {
let m;
if ((m = line.match(heardRe))) {
turns.push({ kind: 'heard', text: m[1], turn: parseInt(m[2], 10) });
} else if ((m = line.match(replyRe))) {
turns.push({ kind: 'reply', text: m[1], action: m[2] });
}
}
res.set('Cache-Control', 'no-store');
res.json({ ok: true, callId, count: turns.length, turns });
});
});
module.exports = router;