UNPKG

mcp-chrome-bridge

Version:

Chrome Native-Messaging host (Node)

971 lines 44.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerAgentRoutes = registerAgentRoutes; const constant_1 = require("../../constant"); const project_service_1 = require("../../agent/project-service"); const message_service_1 = require("../../agent/message-service"); const session_service_1 = require("../../agent/session-service"); const project_service_2 = require("../../agent/project-service"); const storage_1 = require("../../agent/storage"); const directory_picker_1 = require("../../agent/directory-picker"); const attachment_service_1 = require("../../agent/attachment-service"); const open_project_1 = require("../../agent/open-project"); // Valid engine names for validation const VALID_ENGINE_NAMES = ['claude', 'codex', 'cursor', 'qwen', 'glm']; function isValidEngineName(name) { return VALID_ENGINE_NAMES.includes(name); } // Valid open project targets const VALID_OPEN_TARGETS = ['vscode', 'terminal']; function isValidOpenTarget(target) { return VALID_OPEN_TARGETS.includes(target); } // ============================================================ // Route Registration // ============================================================ /** * Register all agent-related routes on the Fastify instance. */ function registerAgentRoutes(fastify, options) { const { streamManager, chatService } = options; // ============================================================ // Engine Routes // ============================================================ fastify.get('/agent/engines', async (_request, reply) => { try { const engines = chatService.getEngineInfos(); reply.status(constant_1.HTTP_STATUS.OK).send({ engines }); } catch (error) { fastify.log.error({ err: error }, 'Failed to list agent engines'); if (!reply.sent) { reply .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR) .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } } }); // ============================================================ // Project Routes // ============================================================ fastify.get('/agent/projects', async (_request, reply) => { try { const projects = await (0, project_service_1.listProjects)(); reply.status(constant_1.HTTP_STATUS.OK).send({ projects }); } catch (error) { if (!reply.sent) { reply .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR) .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } } }); fastify.post('/agent/projects', async (request, reply) => { try { const body = request.body; if (!body || !body.name || !body.rootPath) { reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ error: 'name and rootPath are required to create a project' }); return; } const project = await (0, project_service_1.upsertProject)(body); reply.status(constant_1.HTTP_STATUS.OK).send({ project }); } catch (error) { const message = error instanceof Error ? error.message : String(error); reply .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR) .send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } }); fastify.delete('/agent/projects/:id', async (request, reply) => { const { id } = request.params; if (!id) { reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'project id is required' }); return; } try { await (0, project_service_1.deleteProject)(id); reply.status(constant_1.HTTP_STATUS.NO_CONTENT).send(); } catch (error) { if (!reply.sent) { reply .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR) .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } } }); // Path validation API fastify.post('/agent/projects/validate-path', async (request, reply) => { const { rootPath } = request.body || {}; if (!rootPath || typeof rootPath !== 'string') { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'rootPath is required' }); } try { const result = await (0, project_service_1.validateRootPath)(rootPath); return reply.send(result); } catch (error) { const message = error instanceof Error ? error.message : String(error); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message }); } }); // Create directory API fastify.post('/agent/projects/create-directory', async (request, reply) => { const { absolutePath } = request.body || {}; if (!absolutePath || typeof absolutePath !== 'string') { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'absolutePath is required' }); } try { await (0, project_service_1.createProjectDirectory)(absolutePath); return reply.send({ success: true, path: absolutePath }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: message }); } }); // Get default workspace directory fastify.get('/agent/projects/default-workspace', async (_request, reply) => { try { const workspaceDir = (0, storage_1.getDefaultWorkspaceDir)(); return reply.send({ success: true, path: workspaceDir }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message }); } }); // Get default project root for a given project name fastify.post('/agent/projects/default-root', async (request, reply) => { const { projectName } = request.body || {}; if (!projectName || typeof projectName !== 'string') { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectName is required' }); } try { const rootPath = (0, storage_1.getDefaultProjectRoot)(projectName); return reply.send({ success: true, path: rootPath }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message }); } }); // Open directory picker dialog fastify.post('/agent/projects/pick-directory', async (_request, reply) => { try { const result = await (0, directory_picker_1.openDirectoryPicker)('Select Project Directory'); if (result.success && result.path) { return reply.send({ success: true, path: result.path }); } else if (result.cancelled) { return reply.send({ success: false, cancelled: true }); } else { return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: result.error || 'Failed to open directory picker', }); } } catch (error) { const message = error instanceof Error ? error.message : String(error); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message }); } }); // ============================================================ // Session Routes // ============================================================ // List all sessions across all projects fastify.get('/agent/sessions', async (_request, reply) => { try { const sessions = await (0, session_service_1.getAllSessions)(); return reply.status(constant_1.HTTP_STATUS.OK).send({ sessions }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to list all sessions'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // List sessions for a project fastify.get('/agent/projects/:projectId/sessions', async (request, reply) => { const { projectId } = request.params; if (!projectId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' }); } try { const sessions = await (0, session_service_1.getSessionsByProject)(projectId); return reply.status(constant_1.HTTP_STATUS.OK).send({ sessions }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to list sessions'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Create a new session for a project fastify.post('/agent/projects/:projectId/sessions', async (request, reply) => { const { projectId } = request.params; const body = request.body || {}; if (!projectId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' }); } if (!body.engineName) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'engineName is required' }); } if (!isValidEngineName(body.engineName)) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: `Invalid engineName. Must be one of: ${VALID_ENGINE_NAMES.join(', ')}`, }); } try { // Verify project exists const project = await (0, project_service_2.getProject)(projectId); if (!project) { return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Project not found' }); } const session = await (0, session_service_1.createSession)(projectId, body.engineName, { name: body.name, model: body.model, permissionMode: body.permissionMode, allowDangerouslySkipPermissions: body.allowDangerouslySkipPermissions, systemPromptConfig: body.systemPromptConfig, optionsConfig: body.optionsConfig, }); return reply.status(constant_1.HTTP_STATUS.CREATED).send({ session }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to create session'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Get a specific session fastify.get('/agent/sessions/:sessionId', async (request, reply) => { const { sessionId } = request.params; if (!sessionId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' }); } try { const session = await (0, session_service_1.getSession)(sessionId); if (!session) { return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' }); } return reply.status(constant_1.HTTP_STATUS.OK).send({ session }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to get session'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Update a session fastify.patch('/agent/sessions/:sessionId', async (request, reply) => { const { sessionId } = request.params; const updates = request.body || {}; if (!sessionId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' }); } try { const existing = await (0, session_service_1.getSession)(sessionId); if (!existing) { return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' }); } await (0, session_service_1.updateSession)(sessionId, updates); const updated = await (0, session_service_1.getSession)(sessionId); return reply.status(constant_1.HTTP_STATUS.OK).send({ session: updated }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to update session'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Delete a session fastify.delete('/agent/sessions/:sessionId', async (request, reply) => { const { sessionId } = request.params; if (!sessionId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' }); } try { await (0, session_service_1.deleteSession)(sessionId); return reply.status(constant_1.HTTP_STATUS.NO_CONTENT).send(); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to delete session'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Get message history for a session fastify.get('/agent/sessions/:sessionId/history', async (request, reply) => { const { sessionId } = request.params; if (!sessionId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' }); } const limitRaw = request.query.limit; const offsetRaw = request.query.offset; const limit = Number.parseInt(limitRaw || '', 10); const offset = Number.parseInt(offsetRaw || '', 10); const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 0; const safeOffset = Number.isFinite(offset) && offset >= 0 ? offset : 0; try { const session = await (0, session_service_1.getSession)(sessionId); if (!session) { return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' }); } const [messages, totalCount] = await Promise.all([ (0, message_service_1.getMessagesBySessionId)(sessionId, safeLimit, safeOffset), (0, message_service_1.getMessagesCountBySessionId)(sessionId), ]); return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true, sessionId, messages, totalCount, pagination: { limit: safeLimit, offset: safeOffset, count: messages.length, hasMore: safeLimit > 0 ? safeOffset + messages.length < totalCount : false, }, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to get session history'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Reset a session conversation (clear messages + engineSessionId) fastify.post('/agent/sessions/:sessionId/reset', async (request, reply) => { const { sessionId } = request.params; if (!sessionId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' }); } try { const existing = await (0, session_service_1.getSession)(sessionId); if (!existing) { return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' }); } // Clear resume state first, then delete messages await (0, session_service_1.updateSession)(sessionId, { engineSessionId: null }); const deletedMessages = await (0, message_service_1.deleteMessagesBySessionId)(sessionId); const updated = await (0, session_service_1.getSession)(sessionId); return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true, sessionId, deletedMessages, clearedEngineSessionId: Boolean(existing.engineSessionId), session: updated || null, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to reset session'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Get Claude management info for a session fastify.get('/agent/sessions/:sessionId/claude-info', async (request, reply) => { const { sessionId } = request.params; if (!sessionId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' }); } try { const session = await (0, session_service_1.getSession)(sessionId); if (!session) { return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' }); } return reply.status(constant_1.HTTP_STATUS.OK).send({ managementInfo: session.managementInfo || null, sessionId, engineName: session.engineName, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to get Claude info'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // Get aggregated Claude management info for a project // Returns the most recent management info from any Claude session in the project fastify.get('/agent/projects/:projectId/claude-info', async (request, reply) => { var _a, _b; const { projectId } = request.params; if (!projectId) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' }); } try { const project = await (0, project_service_2.getProject)(projectId); if (!project) { return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Project not found' }); } // Get only Claude sessions (more efficient than fetching all and filtering) const claudeSessions = await (0, session_service_1.getSessionsByProjectAndEngine)(projectId, 'claude'); const sessionsWithInfo = claudeSessions.filter((s) => s.managementInfo); // Sort by lastUpdated in management info (fallback to session.updatedAt for old data) sessionsWithInfo.sort((a, b) => { var _a, _b; const aTime = ((_a = a.managementInfo) === null || _a === void 0 ? void 0 : _a.lastUpdated) || a.updatedAt || ''; const bTime = ((_b = b.managementInfo) === null || _b === void 0 ? void 0 : _b.lastUpdated) || b.updatedAt || ''; return bTime.localeCompare(aTime); }); const latestInfo = ((_a = sessionsWithInfo[0]) === null || _a === void 0 ? void 0 : _a.managementInfo) || null; const sourceSessionId = (_b = sessionsWithInfo[0]) === null || _b === void 0 ? void 0 : _b.id; return reply.status(constant_1.HTTP_STATUS.OK).send({ managementInfo: latestInfo, sourceSessionId, projectId, sessionsWithInfo: sessionsWithInfo.length, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to get project Claude info'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // ============================================================ // Open Project Routes // ============================================================ /** * POST /agent/sessions/:sessionId/open * Open session's project directory in VSCode or terminal. */ fastify.post('/agent/sessions/:sessionId/open', async (request, reply) => { const { sessionId } = request.params; const { target } = request.body || {}; if (!sessionId) { return reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ success: false, error: 'sessionId is required' }); } if (!target || typeof target !== 'string') { return reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ success: false, error: 'target is required' }); } if (!isValidOpenTarget(target)) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ success: false, error: `Invalid target. Must be one of: ${VALID_OPEN_TARGETS.join(', ')}`, }); } try { // Get session and its project const session = await (0, session_service_1.getSession)(sessionId); if (!session) { return reply .status(constant_1.HTTP_STATUS.NOT_FOUND) .send({ success: false, error: 'Session not found' }); } const project = await (0, project_service_2.getProject)(session.projectId); if (!project) { return reply .status(constant_1.HTTP_STATUS.NOT_FOUND) .send({ success: false, error: 'Project not found' }); } // Open the project directory const result = await (0, open_project_1.openProjectDirectory)(project.rootPath, target); if (result.success) { return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true }); } return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: result.error, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to open session project'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); /** * POST /agent/projects/:projectId/open * Open project directory in VSCode or terminal. */ fastify.post('/agent/projects/:projectId/open', async (request, reply) => { const { projectId } = request.params; const { target } = request.body || {}; if (!projectId) { return reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ success: false, error: 'projectId is required' }); } if (!target || typeof target !== 'string') { return reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ success: false, error: 'target is required' }); } if (!isValidOpenTarget(target)) { return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ success: false, error: `Invalid target. Must be one of: ${VALID_OPEN_TARGETS.join(', ')}`, }); } try { const project = await (0, project_service_2.getProject)(projectId); if (!project) { return reply .status(constant_1.HTTP_STATUS.NOT_FOUND) .send({ success: false, error: 'Project not found' }); } // Open the project directory const result = await (0, open_project_1.openProjectDirectory)(project.rootPath, target); if (result.success) { return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true }); } return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: result.error, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to open project'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); /** * POST /agent/projects/:projectId/open-file * Open a file in VSCode at a specific line/column. * * Request body: * - filePath: string (required) - File path (relative or absolute) * - line?: number - Line number (1-based) * - column?: number - Column number (1-based) */ fastify.post('/agent/projects/:projectId/open-file', async (request, reply) => { const { projectId } = request.params; const { filePath, line, column } = request.body || {}; if (!projectId) { return reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ success: false, error: 'projectId is required' }); } if (!filePath || typeof filePath !== 'string') { return reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ success: false, error: 'filePath is required' }); } try { const project = await (0, project_service_2.getProject)(projectId); if (!project) { return reply .status(constant_1.HTTP_STATUS.NOT_FOUND) .send({ success: false, error: 'Project not found' }); } // Open the file in VSCode const result = await (0, open_project_1.openFileInVSCode)(project.rootPath, filePath, line, column); if (result.success) { return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true }); } return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: result.error, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to open file in VSCode'); return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // ============================================================ // Chat Message Routes // ============================================================ fastify.get('/agent/chat/:projectId/messages', async (request, reply) => { const { projectId } = request.params; if (!projectId) { reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' }); return; } const limitRaw = request.query.limit; const offsetRaw = request.query.offset; const limit = Number.parseInt(limitRaw || '', 10); const offset = Number.parseInt(offsetRaw || '', 10); const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 50; const safeOffset = Number.isFinite(offset) && offset >= 0 ? offset : 0; try { const [messages, totalCount] = await Promise.all([ (0, message_service_1.getMessagesByProjectId)(projectId, safeLimit, safeOffset), (0, message_service_1.getMessagesCountByProjectId)(projectId), ]); reply.status(constant_1.HTTP_STATUS.OK).send({ success: true, data: messages, totalCount, pagination: { limit: safeLimit, offset: safeOffset, count: messages.length, hasMore: safeOffset + messages.length < totalCount, }, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to load agent chat messages'); reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: 'Failed to fetch messages', message: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); fastify.post('/agent/chat/:projectId/messages', async (request, reply) => { const { projectId } = request.params; if (!projectId) { reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' }); return; } const body = request.body || {}; const content = typeof body.content === 'string' ? body.content.trim() : ''; if (!content) { reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ success: false, error: 'content is required' }); return; } const rawRole = typeof body.role === 'string' ? body.role.toLowerCase().trim() : 'user'; const role = rawRole === 'assistant' || rawRole === 'system' || rawRole === 'tool' ? rawRole : 'user'; const rawType = typeof body.messageType === 'string' ? body.messageType.toLowerCase() : ''; const allowedTypes = ['chat', 'tool_use', 'tool_result', 'status']; const fallbackType = role === 'system' ? 'status' : 'chat'; const messageType = allowedTypes.includes(rawType) && rawType ? rawType : fallbackType; try { const stored = await (0, message_service_1.createMessage)({ projectId, role, messageType, content, metadata: body.metadata, sessionId: body.sessionId, conversationId: body.conversationId, cliSource: body.cliSource, requestId: body.requestId, id: body.id, createdAt: body.createdAt, }); reply.status(constant_1.HTTP_STATUS.CREATED).send({ success: true, data: stored }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to create agent chat message'); reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: 'Failed to create message', message: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); fastify.delete('/agent/chat/:projectId/messages', async (request, reply) => { const { projectId } = request.params; if (!projectId) { reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' }); return; } const { conversationId } = request.query; try { const deleted = await (0, message_service_1.deleteMessagesByProjectId)(projectId, conversationId || undefined); reply.status(constant_1.HTTP_STATUS.OK).send({ success: true, deleted }); } catch (error) { const message = error instanceof Error ? error.message : String(error); fastify.log.error({ err: error }, 'Failed to delete agent chat messages'); reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ success: false, error: 'Failed to delete messages', message: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR, }); } }); // ============================================================ // Chat Streaming Routes (SSE) // ============================================================ fastify.get('/agent/chat/:sessionId/stream', async (request, reply) => { const { sessionId } = request.params; if (!sessionId) { reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ error: 'sessionId is required for agent stream' }); return; } try { reply.raw.writeHead(constant_1.HTTP_STATUS.OK, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); // Ensure client immediately receives an open event reply.raw.write(':\n\n'); streamManager.addSseStream(sessionId, reply.raw); const connectedEvent = { type: 'connected', data: { sessionId, transport: 'sse', timestamp: new Date().toISOString(), }, }; streamManager.publish(connectedEvent); reply.raw.on('close', () => { streamManager.removeSseStream(sessionId, reply.raw); }); } catch (error) { if (!reply.sent) { reply.code(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send(constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR); } } }); // ============================================================ // Chat Action Routes // ============================================================ fastify.post('/agent/chat/:sessionId/act', { // Increase body limit to support image attachments (base64 encoded) // Default Fastify limit is 1MB, which is too small for images config: { rawBody: false, }, bodyLimit: 50 * 1024 * 1024, // 50MB to support multiple images }, async (request, reply) => { const { sessionId } = request.params; const payload = request.body; if (!sessionId) { reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ error: 'sessionId is required for agent act' }); return; } try { const { requestId } = await chatService.handleAct(sessionId, payload); const response = { requestId, sessionId, status: 'accepted', }; reply.status(constant_1.HTTP_STATUS.OK).send(response); } catch (error) { const message = error instanceof Error ? error.message : String(error); reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } }); // Cancel specific request fastify.delete('/agent/chat/:sessionId/cancel/:requestId', async (request, reply) => { const { sessionId, requestId } = request.params; if (!sessionId || !requestId) { reply .status(constant_1.HTTP_STATUS.BAD_REQUEST) .send({ error: 'sessionId and requestId are required' }); return; } const cancelled = chatService.cancelExecution(requestId); if (cancelled) { reply.status(constant_1.HTTP_STATUS.OK).send({ success: true, message: 'Execution cancelled', requestId, sessionId, }); } else { reply.status(constant_1.HTTP_STATUS.OK).send({ success: false, message: 'No running execution found with this requestId', requestId, sessionId, }); } }); // Cancel all executions for a session fastify.delete('/agent/chat/:sessionId/cancel', async (request, reply) => { const { sessionId } = request.params; if (!sessionId) { reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' }); return; } const cancelledCount = chatService.cancelSessionExecutions(sessionId); reply.status(constant_1.HTTP_STATUS.OK).send({ success: true, cancelledCount, sessionId, }); }); // ============================================================ // Attachment Routes // ============================================================ /** * GET /agent/attachments/stats * Get statistics for all attachment caches. */ fastify.get('/agent/attachments/stats', async (_request, reply) => { try { const stats = await attachment_service_1.attachmentService.getAttachmentStats(); // Enrich with project names from database const projects = await (0, project_service_1.listProjects)(); const projectMap = new Map(projects.map((p) => [p.id, p.name])); const dbProjectIds = new Set(projects.map((p) => p.id)); const enrichedProjects = stats.projects.map((p) => ({ ...p, projectName: projectMap.get(p.projectId), existsInDb: dbProjectIds.has(p.projectId), })); const orphanProjectIds = stats.projects .filter((p) => !dbProjectIds.has(p.projectId)) .map((p) => p.projectId); const response = { success: true, rootDir: stats.rootDir, totalFiles: stats.totalFiles, totalBytes: stats.totalBytes, projects: enrichedProjects, orphanProjectIds, }; reply.status(constant_1.HTTP_STATUS.OK).send(response); } catch (error) { fastify.log.error({ err: error }, 'Failed to get attachment stats'); reply .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR) .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } }); /** * GET /agent/attachments/:projectId/:filename * Serve an attachment file. */ fastify.get('/agent/attachments/:projectId/:filename', async (request, reply) => { var _a; const { projectId, filename } = request.params; try { // Validate and get file const buffer = await attachment_service_1.attachmentService.readAttachment(projectId, filename); // Determine content type from filename extension const ext = (_a = filename.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase(); let contentType = 'application/octet-stream'; switch (ext) { case 'png': contentType = 'image/png'; break; case 'jpg': case 'jpeg': contentType = 'image/jpeg'; break; case 'gif': contentType = 'image/gif'; break; case 'webp': contentType = 'image/webp'; break; } reply .header('Content-Type', contentType) .header('Cache-Control', 'public, max-age=31536000, immutable') .send(buffer); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes('Invalid') || message.includes('traversal')) { reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: message }); return; } // File not found or read error reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Attachment not found' }); } }); /** * DELETE /agent/attachments/:projectId * Clean up attachments for a specific project. */ fastify.delete('/agent/attachments/:projectId', async (request, reply) => { const { projectId } = request.params; try { const result = await attachment_service_1.attachmentService.cleanupAttachments({ projectIds: [projectId] }); const response = { success: true, scope: 'project', removedFiles: result.removedFiles, removedBytes: result.removedBytes, results: result.results, }; reply.status(constant_1.HTTP_STATUS.OK).send(response); } catch (error) { fastify.log.error({ err: error }, 'Failed to cleanup project attachments'); reply .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR) .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } }); /** * DELETE /agent/attachments * Clean up attachments for all or selected projects. */ fastify.delete('/agent/attachments', async (request, reply) => { try { const body = request.body; const projectIds = body === null || body === void 0 ? void 0 : body.projectIds; const result = await attachment_service_1.attachmentService.cleanupAttachments(projectIds ? { projectIds } : undefined); const scope = projectIds && projectIds.length > 0 ? 'selected' : 'all'; const response = { success: true, scope, removedFiles: result.removedFiles, removedBytes: result.removedBytes, results: result.results, }; reply.status(constant_1.HTTP_STATUS.OK).send(response); } catch (error) { fastify.log.error({ err: error }, 'Failed to cleanup attachments'); reply .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR) .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR }); } }); } //# sourceMappingURL=agent.js.map