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:
@@ -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],
|
||||||
})
|
})
|
||||||
|
|||||||
67
apps/gateway/src/skills/skills.controller.ts
Normal file
67
apps/gateway/src/skills/skills.controller.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/gateway/src/skills/skills.dto.ts
Normal file
15
apps/gateway/src/skills/skills.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
10
apps/gateway/src/skills/skills.module.ts
Normal file
10
apps/gateway/src/skills/skills.module.ts
Normal 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 {}
|
||||||
52
apps/gateway/src/skills/skills.service.ts
Normal file
52
apps/gateway/src/skills/skills.service.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user