diff --git a/server/src/modules/assets/assets.routes.ts b/server/src/modules/assets/assets.routes.ts new file mode 100644 index 00000000..2c7d1463 --- /dev/null +++ b/server/src/modules/assets/assets.routes.ts @@ -0,0 +1,74 @@ +import express, { type NextFunction, type Request, type Response } from 'express'; +import multer from 'multer'; + +import { asyncHandler } from '@/shared/http/async-handler.js'; +import { AppError } from '@/shared/utils/app-error.js'; +import { createApiErrorResponse, createApiSuccessResponse } from '@/shared/http/api-response.js'; +import { llmAssetsService } from '@/modules/assets/assets.service.js'; +import { logger } from '@/shared/utils/logger.js'; + +const router = express.Router(); +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + files: 10, + fileSize: 20 * 1024 * 1024, + }, +}); + +/** + * Reads optional query/body values and trims surrounding whitespace. + */ +const readOptionalQueryString = (value: unknown): string | undefined => { + if (typeof value !== 'string') { + return undefined; + } + + const normalized = value.trim(); + return normalized.length > 0 ? normalized : undefined; +}; + +/** + * Uploads one or more images into `.cloudcli/assets` so providers can reuse file paths. + */ +router.post( + '/images', + upload.array('images', 10), + asyncHandler(async (req: Request, res: Response) => { + const workspacePath = readOptionalQueryString((req.body as Record | undefined)?.workspacePath); + const filesValue = (req as Request & { files?: unknown }).files; + const files = Array.isArray(filesValue) ? filesValue as Array<{ + originalname: string; + mimetype: string; + size: number; + buffer: Buffer; + }> : []; + const images = await llmAssetsService.storeUploadedImages(files, { workspacePath }); + res.status(201).json(createApiSuccessResponse({ images })); + }), +); + +/** + * Normalizes route-level failures to a consistent JSON API shape. + */ +router.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => { + if (res.headersSent) { + return; + } + + if (error instanceof AppError) { + res + .status(error.statusCode) + .json(createApiErrorResponse(error.code, error.message, undefined, error.details)); + return; + } + + const message = error instanceof Error ? error.message : 'Unexpected assets route failure.'; + logger.error(message, { + module: 'assets.routes', + }); + + res.status(500).json(createApiErrorResponse('INTERNAL_ERROR', message)); +}); + +export default router; diff --git a/server/src/modules/llm/services/assets.service.ts b/server/src/modules/assets/assets.service.ts similarity index 100% rename from server/src/modules/llm/services/assets.service.ts rename to server/src/modules/assets/assets.service.ts diff --git a/server/src/modules/llm/llm.routes.ts b/server/src/modules/llm/llm.routes.ts index 27c6fa2c..1797df57 100644 --- a/server/src/modules/llm/llm.routes.ts +++ b/server/src/modules/llm/llm.routes.ts @@ -1,5 +1,4 @@ import express, { type NextFunction, type Request, type Response } from 'express'; -import multer from 'multer'; import path from 'node:path'; import { asyncHandler } from '@/shared/http/async-handler.js'; @@ -7,7 +6,6 @@ import { AppError } from '@/shared/utils/app-error.js'; import { createApiErrorResponse, createApiSuccessResponse } from '@/shared/http/api-response.js'; import { llmService } from '@/modules/llm/services/llm.service.js'; import { llmSessionsService } from '@/modules/llm/services/sessions.service.js'; -import { llmAssetsService } from '@/modules/llm/services/assets.service.js'; import type { McpScope, McpTransport, UpsertMcpServerInput } from '@/modules/llm/services/mcp.service.js'; import { llmMcpService } from '@/modules/llm/services/mcp.service.js'; import { llmSkillsService } from '@/modules/llm/services/skills.service.js'; @@ -16,13 +14,6 @@ import type { LLMProvider } from '@/shared/types/app.js'; import { logger } from '@/shared/utils/logger.js'; const router = express.Router(); -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - files: 10, - fileSize: 20 * 1024 * 1024, - }, -}); /** * Safely reads an Express path parameter that may arrive as string or string[]. @@ -305,26 +296,6 @@ router.post( }), ); -/** - * Uploads one or more images into `.cloudcli/assets` so providers can reuse file paths. - */ -router.post( - '/assets/images', - upload.array('images', 10), - asyncHandler(async (req: Request, res: Response) => { - const workspacePath = readOptionalQueryString((req.body as Record | undefined)?.workspacePath); - const filesValue = (req as Request & { files?: unknown }).files; - const files = Array.isArray(filesValue) ? filesValue as Array<{ - originalname: string; - mimetype: string; - size: number; - buffer: Buffer; - }> : []; - const images = await llmAssetsService.storeUploadedImages(files, { workspacePath }); - res.status(201).json(createApiSuccessResponse({ images })); - }), -); - /** * Lists MCP servers for one provider grouped by user/local/project scopes. */ diff --git a/server/src/modules/llm/session-indexers/session-indexer.utils.ts b/server/src/modules/llm/session-indexers/session-indexer.utils.ts index 0ed7213e..4767a88a 100644 --- a/server/src/modules/llm/session-indexers/session-indexer.utils.ts +++ b/server/src/modules/llm/session-indexers/session-indexer.utils.ts @@ -74,7 +74,6 @@ export async function findFilesRecursivelyCreatedAfter( fileList: string[] = [], ): Promise { try { - console.log("HEY THERE!") const entries = await fsp.readdir(rootDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(rootDir, entry.name); diff --git a/server/src/modules/llm/tests/llm-unifier.images.test.ts b/server/src/modules/llm/tests/llm-unifier.images.test.ts index 08aa3e05..1ff3a1a4 100644 --- a/server/src/modules/llm/tests/llm-unifier.images.test.ts +++ b/server/src/modules/llm/tests/llm-unifier.images.test.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import test from 'node:test'; import { AppError } from '@/shared/utils/app-error.js'; -import { llmAssetsService } from '@/modules/llm/services/assets.service.js'; +import { llmAssetsService } from '@/modules/assets/assets.service.js'; import { ClaudeProvider } from '@/modules/llm/providers/claude.provider.js'; import { CodexProvider } from '@/modules/llm/providers/codex.provider.js'; import { CursorProvider } from '@/modules/llm/providers/cursor.provider.js'; diff --git a/server/src/runner.ts b/server/src/runner.ts index 17bfcedd..b184d103 100644 --- a/server/src/runner.ts +++ b/server/src/runner.ts @@ -66,6 +66,7 @@ const [ projectsInlineRoutes, filesRoutes, llmRoutes, + assetsRoutes, ] = await Promise.all([ importRoute('./modules/health/health.routes.js'), importRoute('./modules/system/system.routes.js'), @@ -92,6 +93,7 @@ const [ importRoute('./modules/projects/projects.inline.routes.js'), importRoute('./modules/files/files.routes.js'), importRoute('./modules/llm/llm.routes.js'), + importRoute('./modules/assets/assets.routes.js'), ]); // ---------- MIDDLEWARES ---------------- @@ -187,6 +189,9 @@ app.use('/api/workspaces', authenticateToken, workspacesRoutes); // Unified LLM provider API routes (protected) app.use('/api/llm', authenticateToken, llmRoutes); +// Shared assets API routes (protected) +app.use('/api/assets', authenticateToken, assetsRoutes); + // Agent API Routes (uses API key authentication) app.use('/api/agent', agentRoutes);