feat(P4-006): skill management — catalog, install, config

Add SkillsService with CRUD operations against the skills table,
toggle enable/disable, and findByName lookup. Wire SkillsController
with REST endpoints at /api/skills (list, get, create, update,
toggle, delete). Skills support builtin/community/custom sources
with JSON config storage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 08:55:10 -05:00
parent 38ae82b370
commit 14eb8855e4
6 changed files with 147 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ import { TasksModule } from './tasks/tasks.module.js';
import { CoordModule } from './coord/coord.module.js'; import { CoordModule } from './coord/coord.module.js';
import { MemoryModule } from './memory/memory.module.js'; import { MemoryModule } from './memory/memory.module.js';
import { LogModule } from './log/log.module.js'; import { LogModule } from './log/log.module.js';
import { SkillsModule } from './skills/skills.module.js';
@Module({ @Module({
imports: [ imports: [
@@ -27,6 +28,7 @@ import { LogModule } from './log/log.module.js';
CoordModule, CoordModule,
MemoryModule, MemoryModule,
LogModule, LogModule,
SkillsModule,
], ],
controllers: [HealthController], controllers: [HealthController],
}) })

View File

@@ -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');
}
}

View File

@@ -0,0 +1,15 @@
export interface CreateSkillDto {
name: string;
description?: string;
version?: string;
source?: 'builtin' | 'community' | 'custom';
config?: Record<string, unknown>;
enabled?: boolean;
}
export interface UpdateSkillDto {
description?: string;
version?: string;
config?: Record<string, unknown>;
enabled?: boolean;
}

View File

@@ -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 {}

View File

@@ -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<Skill[]> {
return this.db.select().from(skills);
}
async findEnabled(): Promise<Skill[]> {
return this.db.select().from(skills).where(eq(skills.enabled, true));
}
async findById(id: string): Promise<Skill | undefined> {
const rows = await this.db.select().from(skills).where(eq(skills.id, id));
return rows[0];
}
async findByName(name: string): Promise<Skill | undefined> {
const rows = await this.db.select().from(skills).where(eq(skills.name, name));
return rows[0];
}
async create(data: NewSkill): Promise<Skill> {
const rows = await this.db.insert(skills).values(data).returning();
return rows[0]!;
}
async update(id: string, data: Partial<NewSkill>): Promise<Skill | undefined> {
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<boolean> {
const rows = await this.db.delete(skills).where(eq(skills.id, id)).returning();
return rows.length > 0;
}
async toggle(id: string, enabled: boolean): Promise<Skill | undefined> {
return this.update(id, { enabled });
}
}

View File

@@ -42,7 +42,7 @@
| P4-003 | in-progress | Phase 4 | @mosaic/log — log ingest, parsing, tiered storage | — | #36 | | 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-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-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 | | 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-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 | | P5-002 | done | Phase 5 | @mosaic/discord-plugin — Discord bot + channel plugin | #61 | #42 |