diff --git a/apps/gateway/package.json b/apps/gateway/package.json index ec9c2a0..3498fa4 100644 --- a/apps/gateway/package.json +++ b/apps/gateway/package.json @@ -14,6 +14,7 @@ "dependencies": { "@mariozechner/pi-coding-agent": "~0.57.1", "@mosaic/auth": "workspace:^", + "@mosaic/brain": "workspace:^", "@mosaic/db": "workspace:^", "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0", diff --git a/apps/gateway/src/app.module.ts b/apps/gateway/src/app.module.ts index 19e0e85..02adb52 100644 --- a/apps/gateway/src/app.module.ts +++ b/apps/gateway/src/app.module.ts @@ -2,11 +2,26 @@ import { Module } from '@nestjs/common'; import { HealthController } from './health/health.controller.js'; import { DatabaseModule } from './database/database.module.js'; import { AuthModule } from './auth/auth.module.js'; +import { BrainModule } from './brain/brain.module.js'; import { AgentModule } from './agent/agent.module.js'; import { ChatModule } from './chat/chat.module.js'; +import { ConversationsModule } from './conversations/conversations.module.js'; +import { ProjectsModule } from './projects/projects.module.js'; +import { MissionsModule } from './missions/missions.module.js'; +import { TasksModule } from './tasks/tasks.module.js'; @Module({ - imports: [DatabaseModule, AuthModule, AgentModule, ChatModule], + imports: [ + DatabaseModule, + AuthModule, + BrainModule, + AgentModule, + ChatModule, + ConversationsModule, + ProjectsModule, + MissionsModule, + TasksModule, + ], controllers: [HealthController], }) export class AppModule {} diff --git a/apps/gateway/src/brain/brain.module.ts b/apps/gateway/src/brain/brain.module.ts new file mode 100644 index 0000000..41dba54 --- /dev/null +++ b/apps/gateway/src/brain/brain.module.ts @@ -0,0 +1,19 @@ +import { Global, Module } from '@nestjs/common'; +import { createBrain, type Brain } from '@mosaic/brain'; +import type { Db } from '@mosaic/db'; +import { DB } from '../database/database.module.js'; + +export const BRAIN = 'BRAIN'; + +@Global() +@Module({ + providers: [ + { + provide: BRAIN, + useFactory: (db: Db): Brain => createBrain(db), + inject: [DB], + }, + ], + exports: [BRAIN], +}) +export class BrainModule {} diff --git a/apps/gateway/src/conversations/conversations.controller.ts b/apps/gateway/src/conversations/conversations.controller.ts new file mode 100644 index 0000000..1e64eac --- /dev/null +++ b/apps/gateway/src/conversations/conversations.controller.ts @@ -0,0 +1,83 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Inject, + NotFoundException, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import type { Brain } from '@mosaic/brain'; +import { BRAIN } from '../brain/brain.module.js'; +import { AuthGuard } from '../auth/auth.guard.js'; +import { CurrentUser } from '../auth/current-user.decorator.js'; +import type { + CreateConversationDto, + UpdateConversationDto, + SendMessageDto, +} from './conversations.dto.js'; + +@Controller('api/conversations') +@UseGuards(AuthGuard) +export class ConversationsController { + constructor(@Inject(BRAIN) private readonly brain: Brain) {} + + @Get() + async list(@CurrentUser() user: { id: string }) { + return this.brain.conversations.findAll(user.id); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const conversation = await this.brain.conversations.findById(id); + if (!conversation) throw new NotFoundException('Conversation not found'); + return conversation; + } + + @Post() + async create(@CurrentUser() user: { id: string }, @Body() dto: CreateConversationDto) { + return this.brain.conversations.create({ + userId: user.id, + title: dto.title, + projectId: dto.projectId, + }); + } + + @Patch(':id') + async update(@Param('id') id: string, @Body() dto: UpdateConversationDto) { + const conversation = await this.brain.conversations.update(id, dto); + if (!conversation) throw new NotFoundException('Conversation not found'); + return conversation; + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id') id: string) { + const deleted = await this.brain.conversations.remove(id); + if (!deleted) throw new NotFoundException('Conversation not found'); + } + + @Get(':id/messages') + async listMessages(@Param('id') id: string) { + const conversation = await this.brain.conversations.findById(id); + if (!conversation) throw new NotFoundException('Conversation not found'); + return this.brain.conversations.findMessages(id); + } + + @Post(':id/messages') + async addMessage(@Param('id') id: string, @Body() dto: SendMessageDto) { + const conversation = await this.brain.conversations.findById(id); + if (!conversation) throw new NotFoundException('Conversation not found'); + return this.brain.conversations.addMessage({ + conversationId: id, + role: dto.role, + content: dto.content, + metadata: dto.metadata, + }); + } +} diff --git a/apps/gateway/src/conversations/conversations.dto.ts b/apps/gateway/src/conversations/conversations.dto.ts new file mode 100644 index 0000000..db7fd04 --- /dev/null +++ b/apps/gateway/src/conversations/conversations.dto.ts @@ -0,0 +1,15 @@ +export interface CreateConversationDto { + title?: string; + projectId?: string; +} + +export interface UpdateConversationDto { + title?: string; + projectId?: string | null; +} + +export interface SendMessageDto { + role: 'user' | 'assistant' | 'system'; + content: string; + metadata?: Record; +} diff --git a/apps/gateway/src/conversations/conversations.module.ts b/apps/gateway/src/conversations/conversations.module.ts new file mode 100644 index 0000000..19f3912 --- /dev/null +++ b/apps/gateway/src/conversations/conversations.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { ConversationsController } from './conversations.controller.js'; + +@Module({ + controllers: [ConversationsController], +}) +export class ConversationsModule {} diff --git a/apps/gateway/src/missions/missions.controller.ts b/apps/gateway/src/missions/missions.controller.ts new file mode 100644 index 0000000..e7e4c02 --- /dev/null +++ b/apps/gateway/src/missions/missions.controller.ts @@ -0,0 +1,60 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Inject, + NotFoundException, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import type { Brain } from '@mosaic/brain'; +import { BRAIN } from '../brain/brain.module.js'; +import { AuthGuard } from '../auth/auth.guard.js'; +import type { CreateMissionDto, UpdateMissionDto } from './missions.dto.js'; + +@Controller('api/missions') +@UseGuards(AuthGuard) +export class MissionsController { + constructor(@Inject(BRAIN) private readonly brain: Brain) {} + + @Get() + async list() { + return this.brain.missions.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const mission = await this.brain.missions.findById(id); + if (!mission) throw new NotFoundException('Mission not found'); + return mission; + } + + @Post() + async create(@Body() dto: CreateMissionDto) { + return this.brain.missions.create({ + name: dto.name, + description: dto.description, + projectId: dto.projectId, + status: dto.status, + }); + } + + @Patch(':id') + async update(@Param('id') id: string, @Body() dto: UpdateMissionDto) { + const mission = await this.brain.missions.update(id, dto); + if (!mission) throw new NotFoundException('Mission not found'); + return mission; + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id') id: string) { + const deleted = await this.brain.missions.remove(id); + if (!deleted) throw new NotFoundException('Mission not found'); + } +} diff --git a/apps/gateway/src/missions/missions.dto.ts b/apps/gateway/src/missions/missions.dto.ts new file mode 100644 index 0000000..4c0519b --- /dev/null +++ b/apps/gateway/src/missions/missions.dto.ts @@ -0,0 +1,14 @@ +export interface CreateMissionDto { + name: string; + description?: string; + projectId?: string; + status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed'; +} + +export interface UpdateMissionDto { + name?: string; + description?: string | null; + projectId?: string | null; + status?: 'planning' | 'active' | 'paused' | 'completed' | 'failed'; + metadata?: Record | null; +} diff --git a/apps/gateway/src/missions/missions.module.ts b/apps/gateway/src/missions/missions.module.ts new file mode 100644 index 0000000..530bf95 --- /dev/null +++ b/apps/gateway/src/missions/missions.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { MissionsController } from './missions.controller.js'; + +@Module({ + controllers: [MissionsController], +}) +export class MissionsModule {} diff --git a/apps/gateway/src/projects/projects.controller.ts b/apps/gateway/src/projects/projects.controller.ts new file mode 100644 index 0000000..1d6761f --- /dev/null +++ b/apps/gateway/src/projects/projects.controller.ts @@ -0,0 +1,61 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Inject, + NotFoundException, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import type { Brain } from '@mosaic/brain'; +import { BRAIN } from '../brain/brain.module.js'; +import { AuthGuard } from '../auth/auth.guard.js'; +import { CurrentUser } from '../auth/current-user.decorator.js'; +import type { CreateProjectDto, UpdateProjectDto } from './projects.dto.js'; + +@Controller('api/projects') +@UseGuards(AuthGuard) +export class ProjectsController { + constructor(@Inject(BRAIN) private readonly brain: Brain) {} + + @Get() + async list() { + return this.brain.projects.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const project = await this.brain.projects.findById(id); + if (!project) throw new NotFoundException('Project not found'); + return project; + } + + @Post() + async create(@CurrentUser() user: { id: string }, @Body() dto: CreateProjectDto) { + return this.brain.projects.create({ + name: dto.name, + description: dto.description, + status: dto.status, + ownerId: user.id, + }); + } + + @Patch(':id') + async update(@Param('id') id: string, @Body() dto: UpdateProjectDto) { + const project = await this.brain.projects.update(id, dto); + if (!project) throw new NotFoundException('Project not found'); + return project; + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id') id: string) { + const deleted = await this.brain.projects.remove(id); + if (!deleted) throw new NotFoundException('Project not found'); + } +} diff --git a/apps/gateway/src/projects/projects.dto.ts b/apps/gateway/src/projects/projects.dto.ts new file mode 100644 index 0000000..8294975 --- /dev/null +++ b/apps/gateway/src/projects/projects.dto.ts @@ -0,0 +1,12 @@ +export interface CreateProjectDto { + name: string; + description?: string; + status?: 'active' | 'paused' | 'completed' | 'archived'; +} + +export interface UpdateProjectDto { + name?: string; + description?: string | null; + status?: 'active' | 'paused' | 'completed' | 'archived'; + metadata?: Record | null; +} diff --git a/apps/gateway/src/projects/projects.module.ts b/apps/gateway/src/projects/projects.module.ts new file mode 100644 index 0000000..3398bf8 --- /dev/null +++ b/apps/gateway/src/projects/projects.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { ProjectsController } from './projects.controller.js'; + +@Module({ + controllers: [ProjectsController], +}) +export class ProjectsModule {} diff --git a/apps/gateway/src/tasks/tasks.controller.ts b/apps/gateway/src/tasks/tasks.controller.ts new file mode 100644 index 0000000..7437c73 --- /dev/null +++ b/apps/gateway/src/tasks/tasks.controller.ts @@ -0,0 +1,79 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Inject, + NotFoundException, + Param, + Patch, + Post, + Query, + UseGuards, +} from '@nestjs/common'; +import type { Brain } from '@mosaic/brain'; +import { BRAIN } from '../brain/brain.module.js'; +import { AuthGuard } from '../auth/auth.guard.js'; +import type { CreateTaskDto, UpdateTaskDto } from './tasks.dto.js'; + +@Controller('api/tasks') +@UseGuards(AuthGuard) +export class TasksController { + constructor(@Inject(BRAIN) private readonly brain: Brain) {} + + @Get() + async list( + @Query('projectId') projectId?: string, + @Query('missionId') missionId?: string, + @Query('status') status?: string, + ) { + if (projectId) return this.brain.tasks.findByProject(projectId); + if (missionId) return this.brain.tasks.findByMission(missionId); + if (status) + return this.brain.tasks.findByStatus( + status as Parameters[0], + ); + return this.brain.tasks.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const task = await this.brain.tasks.findById(id); + if (!task) throw new NotFoundException('Task not found'); + return task; + } + + @Post() + async create(@Body() dto: CreateTaskDto) { + return this.brain.tasks.create({ + title: dto.title, + description: dto.description, + status: dto.status, + priority: dto.priority, + projectId: dto.projectId, + missionId: dto.missionId, + assignee: dto.assignee, + tags: dto.tags, + dueDate: dto.dueDate ? new Date(dto.dueDate) : undefined, + }); + } + + @Patch(':id') + async update(@Param('id') id: string, @Body() dto: UpdateTaskDto) { + const task = await this.brain.tasks.update(id, { + ...dto, + dueDate: dto.dueDate ? new Date(dto.dueDate) : dto.dueDate === null ? null : undefined, + }); + if (!task) throw new NotFoundException('Task not found'); + return task; + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id') id: string) { + const deleted = await this.brain.tasks.remove(id); + if (!deleted) throw new NotFoundException('Task not found'); + } +} diff --git a/apps/gateway/src/tasks/tasks.dto.ts b/apps/gateway/src/tasks/tasks.dto.ts new file mode 100644 index 0000000..3acaa07 --- /dev/null +++ b/apps/gateway/src/tasks/tasks.dto.ts @@ -0,0 +1,24 @@ +export interface CreateTaskDto { + title: string; + description?: string; + status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled'; + priority?: 'critical' | 'high' | 'medium' | 'low'; + projectId?: string; + missionId?: string; + assignee?: string; + tags?: string[]; + dueDate?: string; +} + +export interface UpdateTaskDto { + title?: string; + description?: string | null; + status?: 'not-started' | 'in-progress' | 'blocked' | 'done' | 'cancelled'; + priority?: 'critical' | 'high' | 'medium' | 'low'; + projectId?: string | null; + missionId?: string | null; + assignee?: string | null; + tags?: string[] | null; + dueDate?: string | null; + metadata?: Record | null; +} diff --git a/apps/gateway/src/tasks/tasks.module.ts b/apps/gateway/src/tasks/tasks.module.ts new file mode 100644 index 0000000..0bf92cf --- /dev/null +++ b/apps/gateway/src/tasks/tasks.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { TasksController } from './tasks.controller.js'; + +@Module({ + controllers: [TasksController], +}) +export class TasksModule {} diff --git a/docs/TASKS.md b/docs/TASKS.md index 2d93b72..7ba69bb 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -17,8 +17,8 @@ | P1-002 | done | Phase 1 | Auth middleware — BetterAuth session validation | #71 | #11 | | P1-003 | done | Phase 1 | @mosaic/brain — migrate from v0, PG backend | #71 | #12 | | P1-004 | done | Phase 1 | @mosaic/queue — migrate from v0 | #71 | #13 | -| P1-005 | not-started | Phase 1 | Gateway routes — conversations CRUD + messages | — | #14 | -| P1-006 | not-started | Phase 1 | Gateway routes — tasks, projects, missions CRUD | — | #15 | +| P1-005 | done | Phase 1 | Gateway routes — conversations CRUD + messages | #72 | #14 | +| P1-006 | done | Phase 1 | Gateway routes — tasks, projects, missions CRUD | #72 | #15 | | P1-007 | done | Phase 1 | WebSocket server — chat streaming | #61 | #16 | | P1-008 | done | Phase 1 | Basic agent dispatch — single provider | #61 | #17 | | P1-009 | not-started | Phase 1 | Verify Phase 1 — gateway functional, API tested | — | #18 | diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d336b64..8e3b27c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: '@mosaic/auth': specifier: workspace:^ version: link:../../packages/auth + '@mosaic/brain': + specifier: workspace:^ + version: link:../../packages/brain '@mosaic/db': specifier: workspace:^ version: link:../../packages/db