Implement Backend API, MCP Server, and Gitea integration
All checks were successful
Build and Push Backend / build (push) Successful in 5s
All checks were successful
Build and Push Backend / build (push) Successful in 5s
- Add REST API routes for projects, tasks, and agents (CRUD operations) - Implement MCP Server with 4 core tools: - get_next_task: Assign tasks to agents - update_task_status: Update task states - create_branch: Create Git branches via Gitea API - create_pull_request: Create PRs via Gitea API - Add Gitea API client for repository operations - Fix database migration error handling for existing tables - Connect all routes to Bun.serve() main server Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
303
src/api/routes/agents.ts
Normal file
303
src/api/routes/agents.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Agents API Routes
|
||||
* CRUD operations and status management for Claude Code agents
|
||||
*/
|
||||
|
||||
import { db } from '../../db/client'
|
||||
import { agents, tasks } from '../../db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
/**
|
||||
* Handle all agent routes
|
||||
*/
|
||||
export async function handleAgentRoutes(req: Request, url: URL): Promise<Response> {
|
||||
const method = req.method
|
||||
const pathParts = url.pathname.split('/').filter(Boolean)
|
||||
|
||||
// GET /api/agents - List all agents
|
||||
if (method === 'GET' && pathParts.length === 2) {
|
||||
return await listAgents(url)
|
||||
}
|
||||
|
||||
// GET /api/agents/:id - Get single agent
|
||||
if (method === 'GET' && pathParts.length === 3) {
|
||||
const agentId = pathParts[2]
|
||||
return await getAgent(agentId)
|
||||
}
|
||||
|
||||
// POST /api/agents - Register agent
|
||||
if (method === 'POST' && pathParts.length === 2) {
|
||||
return await registerAgent(req)
|
||||
}
|
||||
|
||||
// PATCH /api/agents/:id - Update agent
|
||||
if (method === 'PATCH' && pathParts.length === 3) {
|
||||
const agentId = pathParts[2]
|
||||
return await updateAgent(agentId, req)
|
||||
}
|
||||
|
||||
// POST /api/agents/:id/heartbeat - Update heartbeat
|
||||
if (method === 'POST' && pathParts.length === 4 && pathParts[3] === 'heartbeat') {
|
||||
const agentId = pathParts[2]
|
||||
return await updateHeartbeat(agentId)
|
||||
}
|
||||
|
||||
// DELETE /api/agents/:id - Unregister agent
|
||||
if (method === 'DELETE' && pathParts.length === 3) {
|
||||
const agentId = pathParts[2]
|
||||
return await unregisterAgent(agentId)
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 })
|
||||
}
|
||||
|
||||
/**
|
||||
* List agents with optional status filter
|
||||
*/
|
||||
async function listAgents(url: URL): Promise<Response> {
|
||||
try {
|
||||
const status = url.searchParams.get('status')
|
||||
|
||||
let query = db.select().from(agents)
|
||||
|
||||
if (status) {
|
||||
query = query.where(eq(agents.status, status as any)) as any
|
||||
}
|
||||
|
||||
const allAgents = await query
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
data: allAgents,
|
||||
count: allAgents.length,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error listing agents:', error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Failed to list agents',
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single agent
|
||||
*/
|
||||
async function getAgent(agentId: string): Promise<Response> {
|
||||
try {
|
||||
const agent = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.id, agentId))
|
||||
.limit(1)
|
||||
|
||||
if (agent.length === 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Agent not found',
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
data: agent[0],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error getting agent:', error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Failed to get agent',
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register new agent (called when pod starts)
|
||||
*/
|
||||
async function registerAgent(req: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await req.json()
|
||||
|
||||
// Validate required fields
|
||||
if (!body.podName) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'podName is required',
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if agent with this podName already exists
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.podName, body.podName))
|
||||
.limit(1)
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Agent already exists, return existing
|
||||
return Response.json({
|
||||
success: true,
|
||||
data: existing[0],
|
||||
message: 'Agent already registered',
|
||||
})
|
||||
}
|
||||
|
||||
const newAgent = {
|
||||
id: randomUUID(),
|
||||
podName: body.podName,
|
||||
k8sNamespace: body.k8sNamespace || 'agents',
|
||||
status: 'idle' as const,
|
||||
currentTaskId: null,
|
||||
tasksCompleted: 0,
|
||||
lastHeartbeat: new Date(),
|
||||
}
|
||||
|
||||
await db.insert(agents).values(newAgent)
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
data: newAgent,
|
||||
}, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error registering agent:', error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Failed to register agent',
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update agent status and current task
|
||||
*/
|
||||
async function updateAgent(agentId: string, req: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await req.json()
|
||||
|
||||
// Check if agent exists
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.id, agentId))
|
||||
.limit(1)
|
||||
|
||||
if (existing.length === 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Agent not found',
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// Build update object
|
||||
const updateData: any = {}
|
||||
|
||||
if (body.status !== undefined) updateData.status = body.status
|
||||
if (body.currentTaskId !== undefined) updateData.currentTaskId = body.currentTaskId
|
||||
if (body.tasksCompleted !== undefined) updateData.tasksCompleted = body.tasksCompleted
|
||||
|
||||
await db
|
||||
.update(agents)
|
||||
.set(updateData)
|
||||
.where(eq(agents.id, agentId))
|
||||
|
||||
// Get updated agent
|
||||
const updated = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.id, agentId))
|
||||
.limit(1)
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
data: updated[0],
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating agent:', error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Failed to update agent',
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update agent heartbeat (keep-alive)
|
||||
*/
|
||||
async function updateHeartbeat(agentId: string): Promise<Response> {
|
||||
try {
|
||||
// Check if agent exists
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.id, agentId))
|
||||
.limit(1)
|
||||
|
||||
if (existing.length === 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Agent not found',
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// Update heartbeat timestamp
|
||||
await db
|
||||
.update(agents)
|
||||
.set({ lastHeartbeat: new Date() })
|
||||
.where(eq(agents.id, agentId))
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: 'Heartbeat updated',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error updating heartbeat:', error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Failed to update heartbeat',
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister agent (called when pod terminates)
|
||||
*/
|
||||
async function unregisterAgent(agentId: string): Promise<Response> {
|
||||
try {
|
||||
// Check if agent exists
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(agents)
|
||||
.where(eq(agents.id, agentId))
|
||||
.limit(1)
|
||||
|
||||
if (existing.length === 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Agent not found',
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
// If agent has a current task, set it to null
|
||||
if (existing[0].currentTaskId) {
|
||||
await db
|
||||
.update(tasks)
|
||||
.set({ assignedAgentId: null })
|
||||
.where(eq(tasks.id, existing[0].currentTaskId))
|
||||
}
|
||||
|
||||
// Delete agent
|
||||
await db.delete(agents).where(eq(agents.id, agentId))
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: 'Agent unregistered',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error unregistering agent:', error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Failed to unregister agent',
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user