diff --git a/src/app.ts b/src/app.ts index 2cc7b1f..41e3d76 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,6 +8,7 @@ import { registerTokenRoutes } from './routes/tokens.js'; import { registerSessionRoutes } from './routes/sessions.js'; import { registerRoomRoutes } from './routes/rooms.js'; import { registerSocialRoutes } from './routes/social.js'; +import { registerDirectoryRoutes } from './routes/directory.js'; import { setupSocketIO } from './socket/index.js'; import { register as metricsRegister } from './lib/metrics.js'; import { startMetricsCollector } from './services/metrics-collector.js'; @@ -61,6 +62,7 @@ export async function buildApp({ config }: BuildAppOptions): Promise( + '/api/companies/:companyId/agents/directory', + async (request, reply) => { + const agentId = request.headers['x-agent-id'] as string | undefined; + if (!agentId) { + return reply.code(401).send({ error: 'Missing x-agent-id header' }); + } + + // For now, AgentHub is single-tenant, so companyId is accepted but not verified + const { role } = request.query; + const limit = Math.min(parseInt(request.query.limit || '50', 10), 100); + + // Build query conditions + const conditions = []; + if (role) { + conditions.push(eq(agents.role, role)); + } + + // Fetch agents + const agentsList = await db + .select({ + id: agents.id, + name: agents.name, + displayName: agents.displayName, + urlKey: agents.urlKey, + role: agents.role, + description: agents.description, + specialties: agents.specialties, + chainOfCommand: agents.chainOfCommand, + createdAt: agents.createdAt, + }) + .from(agents) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .orderBy(agents.displayName) + .limit(limit); + + // For each agent, calculate lastActivityAt and status + const enrichedAgents = await Promise.all( + agentsList.map(async (agent) => { + // Get last activity from audit_events + const [lastActivity] = await db + .select({ ts: auditEvents.ts }) + .from(auditEvents) + .where(eq(auditEvents.agentId, agent.id)) + .orderBy(desc(auditEvents.ts)) + .limit(1); + + const lastActivityAt = lastActivity?.ts || agent.createdAt; + const now = new Date(); + const minutesSinceActivity = (now.getTime() - lastActivityAt.getTime()) / (1000 * 60); + + // Calculate status: active (<5min), idle (<60min), offline (>60min) + let status: 'active' | 'idle' | 'offline'; + if (minutesSinceActivity < 5) { + status = 'active'; + } else if (minutesSinceActivity < 60) { + status = 'idle'; + } else { + status = 'offline'; + } + + // Get social channels where agent has posted (top 3 by post count) + const socialChannelsList = await db + .select({ + id: socialChannels.id, + slug: socialChannels.slug, + name: socialChannels.name, + postCount: sql`count(${socialPosts.id})::int`, + }) + .from(socialPosts) + .innerJoin(socialChannels, eq(socialPosts.channelId, socialChannels.id)) + .where(eq(socialPosts.authorAgentId, agent.id)) + .groupBy(socialChannels.id, socialChannels.slug, socialChannels.name) + .orderBy(desc(sql`count(${socialPosts.id})`)) + .limit(3); + + // Build profile URL (using company prefix from path param) + const urlKey = agent.urlKey || agent.name; + const companyPrefix = 'BARAAA'; // TODO: derive from companyId when multi-tenant + const profileUrl = `/${companyPrefix}/agents/${urlKey}`; + + return { + id: agent.id, + name: agent.name, + urlKey: urlKey, + role: agent.role, + description: agent.description || null, + specialties: agent.specialties || [], + lastActivityAt: lastActivityAt.toISOString(), + status, + chainOfCommand: agent.chainOfCommand || null, + socialChannels: socialChannelsList.map((ch) => ({ + id: ch.id, + slug: ch.slug, + name: ch.name, + })), + profileUrl, + }; + }), + ); + + return reply.send({ + agents: enrichedAgents, + total: enrichedAgents.length, + hasMore: enrichedAgents.length === limit, + }); + }, + ); +}