@helmisatria/mcp-chrome-bridge 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +183 -0
  2. package/dist/README.md +25 -0
  3. package/dist/agent/attachment-service.d.ts +83 -0
  4. package/dist/agent/attachment-service.js +370 -0
  5. package/dist/agent/attachment-service.js.map +1 -0
  6. package/dist/agent/ccr-detector.d.ts +59 -0
  7. package/dist/agent/ccr-detector.js +311 -0
  8. package/dist/agent/ccr-detector.js.map +1 -0
  9. package/dist/agent/chat-service.d.ts +50 -0
  10. package/dist/agent/chat-service.js +439 -0
  11. package/dist/agent/chat-service.js.map +1 -0
  12. package/dist/agent/db/client.d.ts +26 -0
  13. package/dist/agent/db/client.js +244 -0
  14. package/dist/agent/db/client.js.map +1 -0
  15. package/dist/agent/db/index.d.ts +5 -0
  16. package/dist/agent/db/index.js +22 -0
  17. package/dist/agent/db/index.js.map +1 -0
  18. package/dist/agent/db/schema.d.ts +711 -0
  19. package/dist/agent/db/schema.js +121 -0
  20. package/dist/agent/db/schema.js.map +1 -0
  21. package/dist/agent/directory-picker.d.ts +11 -0
  22. package/dist/agent/directory-picker.js +149 -0
  23. package/dist/agent/directory-picker.js.map +1 -0
  24. package/dist/agent/engines/claude.d.ts +79 -0
  25. package/dist/agent/engines/claude.js +1338 -0
  26. package/dist/agent/engines/claude.js.map +1 -0
  27. package/dist/agent/engines/codex.d.ts +48 -0
  28. package/dist/agent/engines/codex.js +822 -0
  29. package/dist/agent/engines/codex.js.map +1 -0
  30. package/dist/agent/engines/types.d.ts +133 -0
  31. package/dist/agent/engines/types.js +3 -0
  32. package/dist/agent/engines/types.js.map +1 -0
  33. package/dist/agent/message-service.d.ts +56 -0
  34. package/dist/agent/message-service.js +198 -0
  35. package/dist/agent/message-service.js.map +1 -0
  36. package/dist/agent/open-project.d.ts +25 -0
  37. package/dist/agent/open-project.js +469 -0
  38. package/dist/agent/open-project.js.map +1 -0
  39. package/dist/agent/project-service.d.ts +49 -0
  40. package/dist/agent/project-service.js +254 -0
  41. package/dist/agent/project-service.js.map +1 -0
  42. package/dist/agent/project-types.d.ts +27 -0
  43. package/dist/agent/project-types.js +3 -0
  44. package/dist/agent/project-types.js.map +1 -0
  45. package/dist/agent/session-service.d.ts +198 -0
  46. package/dist/agent/session-service.js +292 -0
  47. package/dist/agent/session-service.js.map +1 -0
  48. package/dist/agent/storage.d.ts +27 -0
  49. package/dist/agent/storage.js +73 -0
  50. package/dist/agent/storage.js.map +1 -0
  51. package/dist/agent/stream-manager.d.ts +42 -0
  52. package/dist/agent/stream-manager.js +243 -0
  53. package/dist/agent/stream-manager.js.map +1 -0
  54. package/dist/agent/tool-bridge.d.ts +44 -0
  55. package/dist/agent/tool-bridge.js +50 -0
  56. package/dist/agent/tool-bridge.js.map +1 -0
  57. package/dist/agent/types.d.ts +6 -0
  58. package/dist/agent/types.js +3 -0
  59. package/dist/agent/types.js.map +1 -0
  60. package/dist/cli.d.ts +2 -0
  61. package/dist/cli.js +224 -0
  62. package/dist/cli.js.map +1 -0
  63. package/dist/constant/index.d.ts +60 -0
  64. package/dist/constant/index.js +80 -0
  65. package/dist/constant/index.js.map +1 -0
  66. package/dist/file-handler.d.ts +41 -0
  67. package/dist/file-handler.js +295 -0
  68. package/dist/file-handler.js.map +1 -0
  69. package/dist/index.d.ts +2 -0
  70. package/dist/index.js +35 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/mcp/mcp-server-stdio.d.ts +72 -0
  73. package/dist/mcp/mcp-server-stdio.js +143 -0
  74. package/dist/mcp/mcp-server-stdio.js.map +1 -0
  75. package/dist/mcp/mcp-server.d.ts +36 -0
  76. package/dist/mcp/mcp-server.js +26 -0
  77. package/dist/mcp/mcp-server.js.map +1 -0
  78. package/dist/mcp/register-tools.d.ts +2 -0
  79. package/dist/mcp/register-tools.js +148 -0
  80. package/dist/mcp/register-tools.js.map +1 -0
  81. package/dist/mcp/stdio-config.json +3 -0
  82. package/dist/native-messaging-host.d.ts +42 -0
  83. package/dist/native-messaging-host.js +312 -0
  84. package/dist/native-messaging-host.js.map +1 -0
  85. package/dist/run_host.bat +194 -0
  86. package/dist/run_host.sh +264 -0
  87. package/dist/scripts/browser-config.d.ts +28 -0
  88. package/dist/scripts/browser-config.js +229 -0
  89. package/dist/scripts/browser-config.js.map +1 -0
  90. package/dist/scripts/build.d.ts +1 -0
  91. package/dist/scripts/build.js +126 -0
  92. package/dist/scripts/build.js.map +1 -0
  93. package/dist/scripts/constant.d.ts +4 -0
  94. package/dist/scripts/constant.js +8 -0
  95. package/dist/scripts/constant.js.map +1 -0
  96. package/dist/scripts/doctor.d.ts +70 -0
  97. package/dist/scripts/doctor.js +930 -0
  98. package/dist/scripts/doctor.js.map +1 -0
  99. package/dist/scripts/postinstall.d.ts +2 -0
  100. package/dist/scripts/postinstall.js +246 -0
  101. package/dist/scripts/postinstall.js.map +1 -0
  102. package/dist/scripts/register-dev.d.ts +1 -0
  103. package/dist/scripts/register-dev.js +5 -0
  104. package/dist/scripts/register-dev.js.map +1 -0
  105. package/dist/scripts/register.d.ts +2 -0
  106. package/dist/scripts/register.js +28 -0
  107. package/dist/scripts/register.js.map +1 -0
  108. package/dist/scripts/report.d.ts +96 -0
  109. package/dist/scripts/report.js +686 -0
  110. package/dist/scripts/report.js.map +1 -0
  111. package/dist/scripts/utils.d.ts +64 -0
  112. package/dist/scripts/utils.js +443 -0
  113. package/dist/scripts/utils.js.map +1 -0
  114. package/dist/server/index.d.ts +35 -0
  115. package/dist/server/index.js +312 -0
  116. package/dist/server/index.js.map +1 -0
  117. package/dist/server/routes/agent.d.ts +21 -0
  118. package/dist/server/routes/agent.js +971 -0
  119. package/dist/server/routes/agent.js.map +1 -0
  120. package/dist/server/routes/index.d.ts +4 -0
  121. package/dist/server/routes/index.js +9 -0
  122. package/dist/server/routes/index.js.map +1 -0
  123. package/dist/trace-analyzer.d.ts +14 -0
  124. package/dist/trace-analyzer.js +113 -0
  125. package/dist/trace-analyzer.js.map +1 -0
  126. package/dist/util/logger.d.ts +1 -0
  127. package/dist/util/logger.js +43 -0
  128. package/dist/util/logger.js.map +1 -0
  129. package/package.json +91 -0
@@ -0,0 +1,971 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAgentRoutes = registerAgentRoutes;
4
+ const constant_1 = require("../../constant");
5
+ const project_service_1 = require("../../agent/project-service");
6
+ const message_service_1 = require("../../agent/message-service");
7
+ const session_service_1 = require("../../agent/session-service");
8
+ const project_service_2 = require("../../agent/project-service");
9
+ const storage_1 = require("../../agent/storage");
10
+ const directory_picker_1 = require("../../agent/directory-picker");
11
+ const attachment_service_1 = require("../../agent/attachment-service");
12
+ const open_project_1 = require("../../agent/open-project");
13
+ // Valid engine names for validation
14
+ const VALID_ENGINE_NAMES = ['claude', 'codex', 'cursor', 'qwen', 'glm'];
15
+ function isValidEngineName(name) {
16
+ return VALID_ENGINE_NAMES.includes(name);
17
+ }
18
+ // Valid open project targets
19
+ const VALID_OPEN_TARGETS = ['vscode', 'terminal'];
20
+ function isValidOpenTarget(target) {
21
+ return VALID_OPEN_TARGETS.includes(target);
22
+ }
23
+ // ============================================================
24
+ // Route Registration
25
+ // ============================================================
26
+ /**
27
+ * Register all agent-related routes on the Fastify instance.
28
+ */
29
+ function registerAgentRoutes(fastify, options) {
30
+ const { streamManager, chatService } = options;
31
+ // ============================================================
32
+ // Engine Routes
33
+ // ============================================================
34
+ fastify.get('/agent/engines', async (_request, reply) => {
35
+ try {
36
+ const engines = chatService.getEngineInfos();
37
+ reply.status(constant_1.HTTP_STATUS.OK).send({ engines });
38
+ }
39
+ catch (error) {
40
+ fastify.log.error({ err: error }, 'Failed to list agent engines');
41
+ if (!reply.sent) {
42
+ reply
43
+ .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR)
44
+ .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
45
+ }
46
+ }
47
+ });
48
+ // ============================================================
49
+ // Project Routes
50
+ // ============================================================
51
+ fastify.get('/agent/projects', async (_request, reply) => {
52
+ try {
53
+ const projects = await (0, project_service_1.listProjects)();
54
+ reply.status(constant_1.HTTP_STATUS.OK).send({ projects });
55
+ }
56
+ catch (error) {
57
+ if (!reply.sent) {
58
+ reply
59
+ .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR)
60
+ .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
61
+ }
62
+ }
63
+ });
64
+ fastify.post('/agent/projects', async (request, reply) => {
65
+ try {
66
+ const body = request.body;
67
+ if (!body || !body.name || !body.rootPath) {
68
+ reply
69
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
70
+ .send({ error: 'name and rootPath are required to create a project' });
71
+ return;
72
+ }
73
+ const project = await (0, project_service_1.upsertProject)(body);
74
+ reply.status(constant_1.HTTP_STATUS.OK).send({ project });
75
+ }
76
+ catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ reply
79
+ .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR)
80
+ .send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
81
+ }
82
+ });
83
+ fastify.delete('/agent/projects/:id', async (request, reply) => {
84
+ const { id } = request.params;
85
+ if (!id) {
86
+ reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'project id is required' });
87
+ return;
88
+ }
89
+ try {
90
+ await (0, project_service_1.deleteProject)(id);
91
+ reply.status(constant_1.HTTP_STATUS.NO_CONTENT).send();
92
+ }
93
+ catch (error) {
94
+ if (!reply.sent) {
95
+ reply
96
+ .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR)
97
+ .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
98
+ }
99
+ }
100
+ });
101
+ // Path validation API
102
+ fastify.post('/agent/projects/validate-path', async (request, reply) => {
103
+ const { rootPath } = request.body || {};
104
+ if (!rootPath || typeof rootPath !== 'string') {
105
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'rootPath is required' });
106
+ }
107
+ try {
108
+ const result = await (0, project_service_1.validateRootPath)(rootPath);
109
+ return reply.send(result);
110
+ }
111
+ catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message });
114
+ }
115
+ });
116
+ // Create directory API
117
+ fastify.post('/agent/projects/create-directory', async (request, reply) => {
118
+ const { absolutePath } = request.body || {};
119
+ if (!absolutePath || typeof absolutePath !== 'string') {
120
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'absolutePath is required' });
121
+ }
122
+ try {
123
+ await (0, project_service_1.createProjectDirectory)(absolutePath);
124
+ return reply.send({ success: true, path: absolutePath });
125
+ }
126
+ catch (error) {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: message });
129
+ }
130
+ });
131
+ // Get default workspace directory
132
+ fastify.get('/agent/projects/default-workspace', async (_request, reply) => {
133
+ try {
134
+ const workspaceDir = (0, storage_1.getDefaultWorkspaceDir)();
135
+ return reply.send({ success: true, path: workspaceDir });
136
+ }
137
+ catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message });
140
+ }
141
+ });
142
+ // Get default project root for a given project name
143
+ fastify.post('/agent/projects/default-root', async (request, reply) => {
144
+ const { projectName } = request.body || {};
145
+ if (!projectName || typeof projectName !== 'string') {
146
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectName is required' });
147
+ }
148
+ try {
149
+ const rootPath = (0, storage_1.getDefaultProjectRoot)(projectName);
150
+ return reply.send({ success: true, path: rootPath });
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message });
155
+ }
156
+ });
157
+ // Open directory picker dialog
158
+ fastify.post('/agent/projects/pick-directory', async (_request, reply) => {
159
+ try {
160
+ const result = await (0, directory_picker_1.openDirectoryPicker)('Select Project Directory');
161
+ if (result.success && result.path) {
162
+ return reply.send({ success: true, path: result.path });
163
+ }
164
+ else if (result.cancelled) {
165
+ return reply.send({ success: false, cancelled: true });
166
+ }
167
+ else {
168
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
169
+ success: false,
170
+ error: result.error || 'Failed to open directory picker',
171
+ });
172
+ }
173
+ }
174
+ catch (error) {
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: message });
177
+ }
178
+ });
179
+ // ============================================================
180
+ // Session Routes
181
+ // ============================================================
182
+ // List all sessions across all projects
183
+ fastify.get('/agent/sessions', async (_request, reply) => {
184
+ try {
185
+ const sessions = await (0, session_service_1.getAllSessions)();
186
+ return reply.status(constant_1.HTTP_STATUS.OK).send({ sessions });
187
+ }
188
+ catch (error) {
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ fastify.log.error({ err: error }, 'Failed to list all sessions');
191
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
192
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
193
+ });
194
+ }
195
+ });
196
+ // List sessions for a project
197
+ fastify.get('/agent/projects/:projectId/sessions', async (request, reply) => {
198
+ const { projectId } = request.params;
199
+ if (!projectId) {
200
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' });
201
+ }
202
+ try {
203
+ const sessions = await (0, session_service_1.getSessionsByProject)(projectId);
204
+ return reply.status(constant_1.HTTP_STATUS.OK).send({ sessions });
205
+ }
206
+ catch (error) {
207
+ const message = error instanceof Error ? error.message : String(error);
208
+ fastify.log.error({ err: error }, 'Failed to list sessions');
209
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
210
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
211
+ });
212
+ }
213
+ });
214
+ // Create a new session for a project
215
+ fastify.post('/agent/projects/:projectId/sessions', async (request, reply) => {
216
+ const { projectId } = request.params;
217
+ const body = request.body || {};
218
+ if (!projectId) {
219
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' });
220
+ }
221
+ if (!body.engineName) {
222
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'engineName is required' });
223
+ }
224
+ if (!isValidEngineName(body.engineName)) {
225
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({
226
+ error: `Invalid engineName. Must be one of: ${VALID_ENGINE_NAMES.join(', ')}`,
227
+ });
228
+ }
229
+ try {
230
+ // Verify project exists
231
+ const project = await (0, project_service_2.getProject)(projectId);
232
+ if (!project) {
233
+ return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Project not found' });
234
+ }
235
+ const session = await (0, session_service_1.createSession)(projectId, body.engineName, {
236
+ name: body.name,
237
+ model: body.model,
238
+ permissionMode: body.permissionMode,
239
+ allowDangerouslySkipPermissions: body.allowDangerouslySkipPermissions,
240
+ systemPromptConfig: body.systemPromptConfig,
241
+ optionsConfig: body.optionsConfig,
242
+ });
243
+ return reply.status(constant_1.HTTP_STATUS.CREATED).send({ session });
244
+ }
245
+ catch (error) {
246
+ const message = error instanceof Error ? error.message : String(error);
247
+ fastify.log.error({ err: error }, 'Failed to create session');
248
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
249
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
250
+ });
251
+ }
252
+ });
253
+ // Get a specific session
254
+ fastify.get('/agent/sessions/:sessionId', async (request, reply) => {
255
+ const { sessionId } = request.params;
256
+ if (!sessionId) {
257
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' });
258
+ }
259
+ try {
260
+ const session = await (0, session_service_1.getSession)(sessionId);
261
+ if (!session) {
262
+ return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' });
263
+ }
264
+ return reply.status(constant_1.HTTP_STATUS.OK).send({ session });
265
+ }
266
+ catch (error) {
267
+ const message = error instanceof Error ? error.message : String(error);
268
+ fastify.log.error({ err: error }, 'Failed to get session');
269
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
270
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
271
+ });
272
+ }
273
+ });
274
+ // Update a session
275
+ fastify.patch('/agent/sessions/:sessionId', async (request, reply) => {
276
+ const { sessionId } = request.params;
277
+ const updates = request.body || {};
278
+ if (!sessionId) {
279
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' });
280
+ }
281
+ try {
282
+ const existing = await (0, session_service_1.getSession)(sessionId);
283
+ if (!existing) {
284
+ return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' });
285
+ }
286
+ await (0, session_service_1.updateSession)(sessionId, updates);
287
+ const updated = await (0, session_service_1.getSession)(sessionId);
288
+ return reply.status(constant_1.HTTP_STATUS.OK).send({ session: updated });
289
+ }
290
+ catch (error) {
291
+ const message = error instanceof Error ? error.message : String(error);
292
+ fastify.log.error({ err: error }, 'Failed to update session');
293
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
294
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
295
+ });
296
+ }
297
+ });
298
+ // Delete a session
299
+ fastify.delete('/agent/sessions/:sessionId', async (request, reply) => {
300
+ const { sessionId } = request.params;
301
+ if (!sessionId) {
302
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' });
303
+ }
304
+ try {
305
+ await (0, session_service_1.deleteSession)(sessionId);
306
+ return reply.status(constant_1.HTTP_STATUS.NO_CONTENT).send();
307
+ }
308
+ catch (error) {
309
+ const message = error instanceof Error ? error.message : String(error);
310
+ fastify.log.error({ err: error }, 'Failed to delete session');
311
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
312
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
313
+ });
314
+ }
315
+ });
316
+ // Get message history for a session
317
+ fastify.get('/agent/sessions/:sessionId/history', async (request, reply) => {
318
+ const { sessionId } = request.params;
319
+ if (!sessionId) {
320
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' });
321
+ }
322
+ const limitRaw = request.query.limit;
323
+ const offsetRaw = request.query.offset;
324
+ const limit = Number.parseInt(limitRaw || '', 10);
325
+ const offset = Number.parseInt(offsetRaw || '', 10);
326
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 0;
327
+ const safeOffset = Number.isFinite(offset) && offset >= 0 ? offset : 0;
328
+ try {
329
+ const session = await (0, session_service_1.getSession)(sessionId);
330
+ if (!session) {
331
+ return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' });
332
+ }
333
+ const [messages, totalCount] = await Promise.all([
334
+ (0, message_service_1.getMessagesBySessionId)(sessionId, safeLimit, safeOffset),
335
+ (0, message_service_1.getMessagesCountBySessionId)(sessionId),
336
+ ]);
337
+ return reply.status(constant_1.HTTP_STATUS.OK).send({
338
+ success: true,
339
+ sessionId,
340
+ messages,
341
+ totalCount,
342
+ pagination: {
343
+ limit: safeLimit,
344
+ offset: safeOffset,
345
+ count: messages.length,
346
+ hasMore: safeLimit > 0 ? safeOffset + messages.length < totalCount : false,
347
+ },
348
+ });
349
+ }
350
+ catch (error) {
351
+ const message = error instanceof Error ? error.message : String(error);
352
+ fastify.log.error({ err: error }, 'Failed to get session history');
353
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
354
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
355
+ });
356
+ }
357
+ });
358
+ // Reset a session conversation (clear messages + engineSessionId)
359
+ fastify.post('/agent/sessions/:sessionId/reset', async (request, reply) => {
360
+ const { sessionId } = request.params;
361
+ if (!sessionId) {
362
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' });
363
+ }
364
+ try {
365
+ const existing = await (0, session_service_1.getSession)(sessionId);
366
+ if (!existing) {
367
+ return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' });
368
+ }
369
+ // Clear resume state first, then delete messages
370
+ await (0, session_service_1.updateSession)(sessionId, { engineSessionId: null });
371
+ const deletedMessages = await (0, message_service_1.deleteMessagesBySessionId)(sessionId);
372
+ const updated = await (0, session_service_1.getSession)(sessionId);
373
+ return reply.status(constant_1.HTTP_STATUS.OK).send({
374
+ success: true,
375
+ sessionId,
376
+ deletedMessages,
377
+ clearedEngineSessionId: Boolean(existing.engineSessionId),
378
+ session: updated || null,
379
+ });
380
+ }
381
+ catch (error) {
382
+ const message = error instanceof Error ? error.message : String(error);
383
+ fastify.log.error({ err: error }, 'Failed to reset session');
384
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
385
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
386
+ });
387
+ }
388
+ });
389
+ // Get Claude management info for a session
390
+ fastify.get('/agent/sessions/:sessionId/claude-info', async (request, reply) => {
391
+ const { sessionId } = request.params;
392
+ if (!sessionId) {
393
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' });
394
+ }
395
+ try {
396
+ const session = await (0, session_service_1.getSession)(sessionId);
397
+ if (!session) {
398
+ return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Session not found' });
399
+ }
400
+ return reply.status(constant_1.HTTP_STATUS.OK).send({
401
+ managementInfo: session.managementInfo || null,
402
+ sessionId,
403
+ engineName: session.engineName,
404
+ });
405
+ }
406
+ catch (error) {
407
+ const message = error instanceof Error ? error.message : String(error);
408
+ fastify.log.error({ err: error }, 'Failed to get Claude info');
409
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
410
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
411
+ });
412
+ }
413
+ });
414
+ // Get aggregated Claude management info for a project
415
+ // Returns the most recent management info from any Claude session in the project
416
+ fastify.get('/agent/projects/:projectId/claude-info', async (request, reply) => {
417
+ var _a, _b;
418
+ const { projectId } = request.params;
419
+ if (!projectId) {
420
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' });
421
+ }
422
+ try {
423
+ const project = await (0, project_service_2.getProject)(projectId);
424
+ if (!project) {
425
+ return reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Project not found' });
426
+ }
427
+ // Get only Claude sessions (more efficient than fetching all and filtering)
428
+ const claudeSessions = await (0, session_service_1.getSessionsByProjectAndEngine)(projectId, 'claude');
429
+ const sessionsWithInfo = claudeSessions.filter((s) => s.managementInfo);
430
+ // Sort by lastUpdated in management info (fallback to session.updatedAt for old data)
431
+ sessionsWithInfo.sort((a, b) => {
432
+ var _a, _b;
433
+ const aTime = ((_a = a.managementInfo) === null || _a === void 0 ? void 0 : _a.lastUpdated) || a.updatedAt || '';
434
+ const bTime = ((_b = b.managementInfo) === null || _b === void 0 ? void 0 : _b.lastUpdated) || b.updatedAt || '';
435
+ return bTime.localeCompare(aTime);
436
+ });
437
+ const latestInfo = ((_a = sessionsWithInfo[0]) === null || _a === void 0 ? void 0 : _a.managementInfo) || null;
438
+ const sourceSessionId = (_b = sessionsWithInfo[0]) === null || _b === void 0 ? void 0 : _b.id;
439
+ return reply.status(constant_1.HTTP_STATUS.OK).send({
440
+ managementInfo: latestInfo,
441
+ sourceSessionId,
442
+ projectId,
443
+ sessionsWithInfo: sessionsWithInfo.length,
444
+ });
445
+ }
446
+ catch (error) {
447
+ const message = error instanceof Error ? error.message : String(error);
448
+ fastify.log.error({ err: error }, 'Failed to get project Claude info');
449
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
450
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
451
+ });
452
+ }
453
+ });
454
+ // ============================================================
455
+ // Open Project Routes
456
+ // ============================================================
457
+ /**
458
+ * POST /agent/sessions/:sessionId/open
459
+ * Open session's project directory in VSCode or terminal.
460
+ */
461
+ fastify.post('/agent/sessions/:sessionId/open', async (request, reply) => {
462
+ const { sessionId } = request.params;
463
+ const { target } = request.body || {};
464
+ if (!sessionId) {
465
+ return reply
466
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
467
+ .send({ success: false, error: 'sessionId is required' });
468
+ }
469
+ if (!target || typeof target !== 'string') {
470
+ return reply
471
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
472
+ .send({ success: false, error: 'target is required' });
473
+ }
474
+ if (!isValidOpenTarget(target)) {
475
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({
476
+ success: false,
477
+ error: `Invalid target. Must be one of: ${VALID_OPEN_TARGETS.join(', ')}`,
478
+ });
479
+ }
480
+ try {
481
+ // Get session and its project
482
+ const session = await (0, session_service_1.getSession)(sessionId);
483
+ if (!session) {
484
+ return reply
485
+ .status(constant_1.HTTP_STATUS.NOT_FOUND)
486
+ .send({ success: false, error: 'Session not found' });
487
+ }
488
+ const project = await (0, project_service_2.getProject)(session.projectId);
489
+ if (!project) {
490
+ return reply
491
+ .status(constant_1.HTTP_STATUS.NOT_FOUND)
492
+ .send({ success: false, error: 'Project not found' });
493
+ }
494
+ // Open the project directory
495
+ const result = await (0, open_project_1.openProjectDirectory)(project.rootPath, target);
496
+ if (result.success) {
497
+ return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true });
498
+ }
499
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
500
+ success: false,
501
+ error: result.error,
502
+ });
503
+ }
504
+ catch (error) {
505
+ const message = error instanceof Error ? error.message : String(error);
506
+ fastify.log.error({ err: error }, 'Failed to open session project');
507
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
508
+ success: false,
509
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
510
+ });
511
+ }
512
+ });
513
+ /**
514
+ * POST /agent/projects/:projectId/open
515
+ * Open project directory in VSCode or terminal.
516
+ */
517
+ fastify.post('/agent/projects/:projectId/open', async (request, reply) => {
518
+ const { projectId } = request.params;
519
+ const { target } = request.body || {};
520
+ if (!projectId) {
521
+ return reply
522
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
523
+ .send({ success: false, error: 'projectId is required' });
524
+ }
525
+ if (!target || typeof target !== 'string') {
526
+ return reply
527
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
528
+ .send({ success: false, error: 'target is required' });
529
+ }
530
+ if (!isValidOpenTarget(target)) {
531
+ return reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({
532
+ success: false,
533
+ error: `Invalid target. Must be one of: ${VALID_OPEN_TARGETS.join(', ')}`,
534
+ });
535
+ }
536
+ try {
537
+ const project = await (0, project_service_2.getProject)(projectId);
538
+ if (!project) {
539
+ return reply
540
+ .status(constant_1.HTTP_STATUS.NOT_FOUND)
541
+ .send({ success: false, error: 'Project not found' });
542
+ }
543
+ // Open the project directory
544
+ const result = await (0, open_project_1.openProjectDirectory)(project.rootPath, target);
545
+ if (result.success) {
546
+ return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true });
547
+ }
548
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
549
+ success: false,
550
+ error: result.error,
551
+ });
552
+ }
553
+ catch (error) {
554
+ const message = error instanceof Error ? error.message : String(error);
555
+ fastify.log.error({ err: error }, 'Failed to open project');
556
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
557
+ success: false,
558
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
559
+ });
560
+ }
561
+ });
562
+ /**
563
+ * POST /agent/projects/:projectId/open-file
564
+ * Open a file in VSCode at a specific line/column.
565
+ *
566
+ * Request body:
567
+ * - filePath: string (required) - File path (relative or absolute)
568
+ * - line?: number - Line number (1-based)
569
+ * - column?: number - Column number (1-based)
570
+ */
571
+ fastify.post('/agent/projects/:projectId/open-file', async (request, reply) => {
572
+ const { projectId } = request.params;
573
+ const { filePath, line, column } = request.body || {};
574
+ if (!projectId) {
575
+ return reply
576
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
577
+ .send({ success: false, error: 'projectId is required' });
578
+ }
579
+ if (!filePath || typeof filePath !== 'string') {
580
+ return reply
581
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
582
+ .send({ success: false, error: 'filePath is required' });
583
+ }
584
+ try {
585
+ const project = await (0, project_service_2.getProject)(projectId);
586
+ if (!project) {
587
+ return reply
588
+ .status(constant_1.HTTP_STATUS.NOT_FOUND)
589
+ .send({ success: false, error: 'Project not found' });
590
+ }
591
+ // Open the file in VSCode
592
+ const result = await (0, open_project_1.openFileInVSCode)(project.rootPath, filePath, line, column);
593
+ if (result.success) {
594
+ return reply.status(constant_1.HTTP_STATUS.OK).send({ success: true });
595
+ }
596
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
597
+ success: false,
598
+ error: result.error,
599
+ });
600
+ }
601
+ catch (error) {
602
+ const message = error instanceof Error ? error.message : String(error);
603
+ fastify.log.error({ err: error }, 'Failed to open file in VSCode');
604
+ return reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
605
+ success: false,
606
+ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
607
+ });
608
+ }
609
+ });
610
+ // ============================================================
611
+ // Chat Message Routes
612
+ // ============================================================
613
+ fastify.get('/agent/chat/:projectId/messages', async (request, reply) => {
614
+ const { projectId } = request.params;
615
+ if (!projectId) {
616
+ reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' });
617
+ return;
618
+ }
619
+ const limitRaw = request.query.limit;
620
+ const offsetRaw = request.query.offset;
621
+ const limit = Number.parseInt(limitRaw || '', 10);
622
+ const offset = Number.parseInt(offsetRaw || '', 10);
623
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 50;
624
+ const safeOffset = Number.isFinite(offset) && offset >= 0 ? offset : 0;
625
+ try {
626
+ const [messages, totalCount] = await Promise.all([
627
+ (0, message_service_1.getMessagesByProjectId)(projectId, safeLimit, safeOffset),
628
+ (0, message_service_1.getMessagesCountByProjectId)(projectId),
629
+ ]);
630
+ reply.status(constant_1.HTTP_STATUS.OK).send({
631
+ success: true,
632
+ data: messages,
633
+ totalCount,
634
+ pagination: {
635
+ limit: safeLimit,
636
+ offset: safeOffset,
637
+ count: messages.length,
638
+ hasMore: safeOffset + messages.length < totalCount,
639
+ },
640
+ });
641
+ }
642
+ catch (error) {
643
+ const message = error instanceof Error ? error.message : String(error);
644
+ fastify.log.error({ err: error }, 'Failed to load agent chat messages');
645
+ reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
646
+ success: false,
647
+ error: 'Failed to fetch messages',
648
+ message: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
649
+ });
650
+ }
651
+ });
652
+ fastify.post('/agent/chat/:projectId/messages', async (request, reply) => {
653
+ const { projectId } = request.params;
654
+ if (!projectId) {
655
+ reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' });
656
+ return;
657
+ }
658
+ const body = request.body || {};
659
+ const content = typeof body.content === 'string' ? body.content.trim() : '';
660
+ if (!content) {
661
+ reply
662
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
663
+ .send({ success: false, error: 'content is required' });
664
+ return;
665
+ }
666
+ const rawRole = typeof body.role === 'string' ? body.role.toLowerCase().trim() : 'user';
667
+ const role = rawRole === 'assistant' || rawRole === 'system' || rawRole === 'tool'
668
+ ? rawRole
669
+ : 'user';
670
+ const rawType = typeof body.messageType === 'string' ? body.messageType.toLowerCase() : '';
671
+ const allowedTypes = ['chat', 'tool_use', 'tool_result', 'status'];
672
+ const fallbackType = role === 'system' ? 'status' : 'chat';
673
+ const messageType = allowedTypes.includes(rawType) && rawType
674
+ ? rawType
675
+ : fallbackType;
676
+ try {
677
+ const stored = await (0, message_service_1.createMessage)({
678
+ projectId,
679
+ role,
680
+ messageType,
681
+ content,
682
+ metadata: body.metadata,
683
+ sessionId: body.sessionId,
684
+ conversationId: body.conversationId,
685
+ cliSource: body.cliSource,
686
+ requestId: body.requestId,
687
+ id: body.id,
688
+ createdAt: body.createdAt,
689
+ });
690
+ reply.status(constant_1.HTTP_STATUS.CREATED).send({ success: true, data: stored });
691
+ }
692
+ catch (error) {
693
+ const message = error instanceof Error ? error.message : String(error);
694
+ fastify.log.error({ err: error }, 'Failed to create agent chat message');
695
+ reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
696
+ success: false,
697
+ error: 'Failed to create message',
698
+ message: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
699
+ });
700
+ }
701
+ });
702
+ fastify.delete('/agent/chat/:projectId/messages', async (request, reply) => {
703
+ const { projectId } = request.params;
704
+ if (!projectId) {
705
+ reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'projectId is required' });
706
+ return;
707
+ }
708
+ const { conversationId } = request.query;
709
+ try {
710
+ const deleted = await (0, message_service_1.deleteMessagesByProjectId)(projectId, conversationId || undefined);
711
+ reply.status(constant_1.HTTP_STATUS.OK).send({ success: true, deleted });
712
+ }
713
+ catch (error) {
714
+ const message = error instanceof Error ? error.message : String(error);
715
+ fastify.log.error({ err: error }, 'Failed to delete agent chat messages');
716
+ reply.status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
717
+ success: false,
718
+ error: 'Failed to delete messages',
719
+ message: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR,
720
+ });
721
+ }
722
+ });
723
+ // ============================================================
724
+ // Chat Streaming Routes (SSE)
725
+ // ============================================================
726
+ fastify.get('/agent/chat/:sessionId/stream', async (request, reply) => {
727
+ const { sessionId } = request.params;
728
+ if (!sessionId) {
729
+ reply
730
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
731
+ .send({ error: 'sessionId is required for agent stream' });
732
+ return;
733
+ }
734
+ try {
735
+ reply.raw.writeHead(constant_1.HTTP_STATUS.OK, {
736
+ 'Content-Type': 'text/event-stream',
737
+ 'Cache-Control': 'no-cache',
738
+ Connection: 'keep-alive',
739
+ });
740
+ // Ensure client immediately receives an open event
741
+ reply.raw.write(':\n\n');
742
+ streamManager.addSseStream(sessionId, reply.raw);
743
+ const connectedEvent = {
744
+ type: 'connected',
745
+ data: {
746
+ sessionId,
747
+ transport: 'sse',
748
+ timestamp: new Date().toISOString(),
749
+ },
750
+ };
751
+ streamManager.publish(connectedEvent);
752
+ reply.raw.on('close', () => {
753
+ streamManager.removeSseStream(sessionId, reply.raw);
754
+ });
755
+ }
756
+ catch (error) {
757
+ if (!reply.sent) {
758
+ reply.code(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR).send(constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
759
+ }
760
+ }
761
+ });
762
+ // ============================================================
763
+ // Chat Action Routes
764
+ // ============================================================
765
+ fastify.post('/agent/chat/:sessionId/act', {
766
+ // Increase body limit to support image attachments (base64 encoded)
767
+ // Default Fastify limit is 1MB, which is too small for images
768
+ config: {
769
+ rawBody: false,
770
+ },
771
+ bodyLimit: 50 * 1024 * 1024, // 50MB to support multiple images
772
+ }, async (request, reply) => {
773
+ const { sessionId } = request.params;
774
+ const payload = request.body;
775
+ if (!sessionId) {
776
+ reply
777
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
778
+ .send({ error: 'sessionId is required for agent act' });
779
+ return;
780
+ }
781
+ try {
782
+ const { requestId } = await chatService.handleAct(sessionId, payload);
783
+ const response = {
784
+ requestId,
785
+ sessionId,
786
+ status: 'accepted',
787
+ };
788
+ reply.status(constant_1.HTTP_STATUS.OK).send(response);
789
+ }
790
+ catch (error) {
791
+ const message = error instanceof Error ? error.message : String(error);
792
+ reply
793
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
794
+ .send({ error: message || constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
795
+ }
796
+ });
797
+ // Cancel specific request
798
+ fastify.delete('/agent/chat/:sessionId/cancel/:requestId', async (request, reply) => {
799
+ const { sessionId, requestId } = request.params;
800
+ if (!sessionId || !requestId) {
801
+ reply
802
+ .status(constant_1.HTTP_STATUS.BAD_REQUEST)
803
+ .send({ error: 'sessionId and requestId are required' });
804
+ return;
805
+ }
806
+ const cancelled = chatService.cancelExecution(requestId);
807
+ if (cancelled) {
808
+ reply.status(constant_1.HTTP_STATUS.OK).send({
809
+ success: true,
810
+ message: 'Execution cancelled',
811
+ requestId,
812
+ sessionId,
813
+ });
814
+ }
815
+ else {
816
+ reply.status(constant_1.HTTP_STATUS.OK).send({
817
+ success: false,
818
+ message: 'No running execution found with this requestId',
819
+ requestId,
820
+ sessionId,
821
+ });
822
+ }
823
+ });
824
+ // Cancel all executions for a session
825
+ fastify.delete('/agent/chat/:sessionId/cancel', async (request, reply) => {
826
+ const { sessionId } = request.params;
827
+ if (!sessionId) {
828
+ reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: 'sessionId is required' });
829
+ return;
830
+ }
831
+ const cancelledCount = chatService.cancelSessionExecutions(sessionId);
832
+ reply.status(constant_1.HTTP_STATUS.OK).send({
833
+ success: true,
834
+ cancelledCount,
835
+ sessionId,
836
+ });
837
+ });
838
+ // ============================================================
839
+ // Attachment Routes
840
+ // ============================================================
841
+ /**
842
+ * GET /agent/attachments/stats
843
+ * Get statistics for all attachment caches.
844
+ */
845
+ fastify.get('/agent/attachments/stats', async (_request, reply) => {
846
+ try {
847
+ const stats = await attachment_service_1.attachmentService.getAttachmentStats();
848
+ // Enrich with project names from database
849
+ const projects = await (0, project_service_1.listProjects)();
850
+ const projectMap = new Map(projects.map((p) => [p.id, p.name]));
851
+ const dbProjectIds = new Set(projects.map((p) => p.id));
852
+ const enrichedProjects = stats.projects.map((p) => ({
853
+ ...p,
854
+ projectName: projectMap.get(p.projectId),
855
+ existsInDb: dbProjectIds.has(p.projectId),
856
+ }));
857
+ const orphanProjectIds = stats.projects
858
+ .filter((p) => !dbProjectIds.has(p.projectId))
859
+ .map((p) => p.projectId);
860
+ const response = {
861
+ success: true,
862
+ rootDir: stats.rootDir,
863
+ totalFiles: stats.totalFiles,
864
+ totalBytes: stats.totalBytes,
865
+ projects: enrichedProjects,
866
+ orphanProjectIds,
867
+ };
868
+ reply.status(constant_1.HTTP_STATUS.OK).send(response);
869
+ }
870
+ catch (error) {
871
+ fastify.log.error({ err: error }, 'Failed to get attachment stats');
872
+ reply
873
+ .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR)
874
+ .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
875
+ }
876
+ });
877
+ /**
878
+ * GET /agent/attachments/:projectId/:filename
879
+ * Serve an attachment file.
880
+ */
881
+ fastify.get('/agent/attachments/:projectId/:filename', async (request, reply) => {
882
+ var _a;
883
+ const { projectId, filename } = request.params;
884
+ try {
885
+ // Validate and get file
886
+ const buffer = await attachment_service_1.attachmentService.readAttachment(projectId, filename);
887
+ // Determine content type from filename extension
888
+ const ext = (_a = filename.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
889
+ let contentType = 'application/octet-stream';
890
+ switch (ext) {
891
+ case 'png':
892
+ contentType = 'image/png';
893
+ break;
894
+ case 'jpg':
895
+ case 'jpeg':
896
+ contentType = 'image/jpeg';
897
+ break;
898
+ case 'gif':
899
+ contentType = 'image/gif';
900
+ break;
901
+ case 'webp':
902
+ contentType = 'image/webp';
903
+ break;
904
+ }
905
+ reply
906
+ .header('Content-Type', contentType)
907
+ .header('Cache-Control', 'public, max-age=31536000, immutable')
908
+ .send(buffer);
909
+ }
910
+ catch (error) {
911
+ const message = error instanceof Error ? error.message : String(error);
912
+ if (message.includes('Invalid') || message.includes('traversal')) {
913
+ reply.status(constant_1.HTTP_STATUS.BAD_REQUEST).send({ error: message });
914
+ return;
915
+ }
916
+ // File not found or read error
917
+ reply.status(constant_1.HTTP_STATUS.NOT_FOUND).send({ error: 'Attachment not found' });
918
+ }
919
+ });
920
+ /**
921
+ * DELETE /agent/attachments/:projectId
922
+ * Clean up attachments for a specific project.
923
+ */
924
+ fastify.delete('/agent/attachments/:projectId', async (request, reply) => {
925
+ const { projectId } = request.params;
926
+ try {
927
+ const result = await attachment_service_1.attachmentService.cleanupAttachments({ projectIds: [projectId] });
928
+ const response = {
929
+ success: true,
930
+ scope: 'project',
931
+ removedFiles: result.removedFiles,
932
+ removedBytes: result.removedBytes,
933
+ results: result.results,
934
+ };
935
+ reply.status(constant_1.HTTP_STATUS.OK).send(response);
936
+ }
937
+ catch (error) {
938
+ fastify.log.error({ err: error }, 'Failed to cleanup project attachments');
939
+ reply
940
+ .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR)
941
+ .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
942
+ }
943
+ });
944
+ /**
945
+ * DELETE /agent/attachments
946
+ * Clean up attachments for all or selected projects.
947
+ */
948
+ fastify.delete('/agent/attachments', async (request, reply) => {
949
+ try {
950
+ const body = request.body;
951
+ const projectIds = body === null || body === void 0 ? void 0 : body.projectIds;
952
+ const result = await attachment_service_1.attachmentService.cleanupAttachments(projectIds ? { projectIds } : undefined);
953
+ const scope = projectIds && projectIds.length > 0 ? 'selected' : 'all';
954
+ const response = {
955
+ success: true,
956
+ scope,
957
+ removedFiles: result.removedFiles,
958
+ removedBytes: result.removedBytes,
959
+ results: result.results,
960
+ };
961
+ reply.status(constant_1.HTTP_STATUS.OK).send(response);
962
+ }
963
+ catch (error) {
964
+ fastify.log.error({ err: error }, 'Failed to cleanup attachments');
965
+ reply
966
+ .status(constant_1.HTTP_STATUS.INTERNAL_SERVER_ERROR)
967
+ .send({ error: constant_1.ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
968
+ }
969
+ });
970
+ }
971
+ //# sourceMappingURL=agent.js.map