diff --git a/apps/gateway/src/app.module.ts b/apps/gateway/src/app.module.ts index c41fabc..17ab9b9 100644 --- a/apps/gateway/src/app.module.ts +++ b/apps/gateway/src/app.module.ts @@ -12,6 +12,7 @@ import { TasksModule } from './tasks/tasks.module.js'; import { CoordModule } from './coord/coord.module.js'; import { MemoryModule } from './memory/memory.module.js'; import { LogModule } from './log/log.module.js'; +import { SkillsModule } from './skills/skills.module.js'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { LogModule } from './log/log.module.js'; CoordModule, MemoryModule, LogModule, + SkillsModule, ], controllers: [HealthController], }) diff --git a/apps/gateway/src/skills/skills.controller.ts b/apps/gateway/src/skills/skills.controller.ts new file mode 100644 index 0000000..8661db6 --- /dev/null +++ b/apps/gateway/src/skills/skills.controller.ts @@ -0,0 +1,67 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + NotFoundException, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { SkillsService } from './skills.service.js'; +import { AuthGuard } from '../auth/auth.guard.js'; +import type { CreateSkillDto, UpdateSkillDto } from './skills.dto.js'; + +@Controller('api/skills') +@UseGuards(AuthGuard) +export class SkillsController { + constructor(private readonly skills: SkillsService) {} + + @Get() + async list() { + return this.skills.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + const skill = await this.skills.findById(id); + if (!skill) throw new NotFoundException('Skill not found'); + return skill; + } + + @Post() + async create(@Body() dto: CreateSkillDto) { + return this.skills.create({ + name: dto.name, + description: dto.description, + version: dto.version, + source: dto.source, + config: dto.config, + enabled: dto.enabled, + }); + } + + @Patch(':id') + async update(@Param('id') id: string, @Body() dto: UpdateSkillDto) { + const skill = await this.skills.update(id, dto); + if (!skill) throw new NotFoundException('Skill not found'); + return skill; + } + + @Patch(':id/toggle') + async toggle(@Param('id') id: string, @Body() body: { enabled: boolean }) { + const skill = await this.skills.toggle(id, body.enabled); + if (!skill) throw new NotFoundException('Skill not found'); + return skill; + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id') id: string) { + const deleted = await this.skills.remove(id); + if (!deleted) throw new NotFoundException('Skill not found'); + } +} diff --git a/apps/gateway/src/skills/skills.dto.ts b/apps/gateway/src/skills/skills.dto.ts new file mode 100644 index 0000000..e2a1f4d --- /dev/null +++ b/apps/gateway/src/skills/skills.dto.ts @@ -0,0 +1,15 @@ +export interface CreateSkillDto { + name: string; + description?: string; + version?: string; + source?: 'builtin' | 'community' | 'custom'; + config?: Record; + enabled?: boolean; +} + +export interface UpdateSkillDto { + description?: string; + version?: string; + config?: Record; + enabled?: boolean; +} diff --git a/apps/gateway/src/skills/skills.module.ts b/apps/gateway/src/skills/skills.module.ts new file mode 100644 index 0000000..49ab88d --- /dev/null +++ b/apps/gateway/src/skills/skills.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { SkillsService } from './skills.service.js'; +import { SkillsController } from './skills.controller.js'; + +@Module({ + providers: [SkillsService], + controllers: [SkillsController], + exports: [SkillsService], +}) +export class SkillsModule {} diff --git a/apps/gateway/src/skills/skills.service.ts b/apps/gateway/src/skills/skills.service.ts new file mode 100644 index 0000000..b015e0d --- /dev/null +++ b/apps/gateway/src/skills/skills.service.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { eq, type Db, skills } from '@mosaic/db'; +import { DB } from '../database/database.module.js'; + +type Skill = typeof skills.$inferSelect; +type NewSkill = typeof skills.$inferInsert; + +@Injectable() +export class SkillsService { + constructor(@Inject(DB) private readonly db: Db) {} + + async findAll(): Promise { + return this.db.select().from(skills); + } + + async findEnabled(): Promise { + return this.db.select().from(skills).where(eq(skills.enabled, true)); + } + + async findById(id: string): Promise { + const rows = await this.db.select().from(skills).where(eq(skills.id, id)); + return rows[0]; + } + + async findByName(name: string): Promise { + const rows = await this.db.select().from(skills).where(eq(skills.name, name)); + return rows[0]; + } + + async create(data: NewSkill): Promise { + const rows = await this.db.insert(skills).values(data).returning(); + return rows[0]!; + } + + async update(id: string, data: Partial): Promise { + const rows = await this.db + .update(skills) + .set({ ...data, updatedAt: new Date() }) + .where(eq(skills.id, id)) + .returning(); + return rows[0]; + } + + async remove(id: string): Promise { + const rows = await this.db.delete(skills).where(eq(skills.id, id)).returning(); + return rows.length > 0; + } + + async toggle(id: string, enabled: boolean): Promise { + return this.update(id, { enabled }); + } +} diff --git a/docs/TASKS.md b/docs/TASKS.md index 811a813..6d01bc7 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -42,7 +42,7 @@ | P4-003 | in-progress | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 | | P4-004 | in-progress | Phase 4 | Summarization pipeline — Haiku-tier LLM + cron | — | #37 | | P4-005 | in-progress | Phase 4 | Memory integration — inject into agent sessions | — | #38 | -| P4-006 | not-started | Phase 4 | Skill management — catalog, install, config | — | #39 | +| P4-006 | in-progress | Phase 4 | Skill management — catalog, install, config | — | #39 | | P4-007 | not-started | Phase 4 | Verify Phase 4 — memory + log pipeline working | — | #40 | | P5-001 | not-started | Phase 5 | Plugin host — gateway plugin loading + channel interface | — | #41 | | P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |