diff --git a/packages/services/agent-service/src/agent.module.ts b/packages/services/agent-service/src/agent.module.ts index 5e2b7fb..af350e8 100644 --- a/packages/services/agent-service/src/agent.module.ts +++ b/packages/services/agent-service/src/agent.module.ts @@ -6,6 +6,9 @@ import { AgentController } from './interfaces/rest/controllers/agent.controller' import { SessionController } from './interfaces/rest/controllers/session.controller'; import { RiskRulesController } from './interfaces/rest/controllers/risk-rules.controller'; import { TenantAgentConfigController } from './interfaces/rest/controllers/tenant-agent-config.controller'; +import { AgentConfigController } from './interfaces/rest/controllers/agent-config.controller'; +import { SkillsController } from './interfaces/rest/controllers/skills.controller'; +import { HooksController } from './interfaces/rest/controllers/hooks.controller'; import { AgentStreamGateway } from './interfaces/ws/agent-stream.gateway'; import { EngineRegistry } from './infrastructure/engines/engine-registry'; import { ClaudeCodeCliEngine } from './infrastructure/engines/claude-code-cli/claude-code-engine'; @@ -19,20 +22,35 @@ import { AllowedToolsResolverService } from './domain/services/allowed-tools-res import { SessionRepository } from './infrastructure/repositories/session.repository'; import { TaskRepository } from './infrastructure/repositories/task.repository'; import { TenantAgentConfigRepository } from './infrastructure/repositories/tenant-agent-config.repository'; +import { AgentConfigRepository } from './infrastructure/repositories/agent-config.repository'; +import { AgentSkillRepository } from './infrastructure/repositories/agent-skill.repository'; +import { HookScriptRepository } from './infrastructure/repositories/hook-script.repository'; import { TenantAgentConfigService } from './infrastructure/services/tenant-agent-config.service'; +import { AgentConfigService } from './infrastructure/services/agent-config.service'; +import { AgentSkillService } from './infrastructure/services/agent-skill.service'; +import { HookScriptService } from './infrastructure/services/hook-script.service'; import { AgentSession } from './domain/entities/agent-session.entity'; import { AgentTask } from './domain/entities/agent-task.entity'; import { CommandRecord } from './domain/entities/command-record.entity'; import { StandingOrderRef } from './domain/entities/standing-order.entity'; import { TenantAgentConfig } from './domain/entities/tenant-agent-config.entity'; +import { AgentConfig } from './domain/entities/agent-config.entity'; +import { AgentSkill } from './domain/entities/agent-skill.entity'; +import { HookScript } from './domain/entities/hook-script.entity'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), DatabaseModule.forRoot(), - TypeOrmModule.forFeature([AgentSession, AgentTask, CommandRecord, StandingOrderRef, TenantAgentConfig]), + TypeOrmModule.forFeature([ + AgentSession, AgentTask, CommandRecord, StandingOrderRef, + TenantAgentConfig, AgentConfig, AgentSkill, HookScript, + ]), + ], + controllers: [ + AgentController, SessionController, RiskRulesController, + TenantAgentConfigController, AgentConfigController, SkillsController, HooksController, ], - controllers: [AgentController, SessionController, RiskRulesController, TenantAgentConfigController], providers: [ AgentStreamGateway, EngineRegistry, @@ -47,7 +65,13 @@ import { TenantAgentConfig } from './domain/entities/tenant-agent-config.entity' SessionRepository, TaskRepository, TenantAgentConfigRepository, + AgentConfigRepository, + AgentSkillRepository, + HookScriptRepository, TenantAgentConfigService, + AgentConfigService, + AgentSkillService, + HookScriptService, ], }) export class AgentModule {} diff --git a/packages/services/agent-service/src/domain/entities/agent-config.entity.ts b/packages/services/agent-service/src/domain/entities/agent-config.entity.ts new file mode 100644 index 0000000..45f0181 --- /dev/null +++ b/packages/services/agent-service/src/domain/entities/agent-config.entity.ts @@ -0,0 +1,41 @@ +/** + * Per-tenant agent configuration entity. + * + * Stores the global agent settings for a tenant: + * - engine: which Claude engine to use (claude-cli or claude-api) + * - systemPrompt: base system prompt prepended to every agent interaction + * - maxTurns: maximum agentic turns per task + * - maxBudget: maximum USD spend per task + * - allowedTools: which SDK tools the agent may invoke + */ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('agent_configs') +export class AgentConfig { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ type: 'varchar', length: 20 }) + tenantId!: string; + + @Column({ type: 'varchar', length: 30, default: 'claude-cli' }) + engine!: string; + + @Column({ type: 'text', default: '' }) + systemPrompt!: string; + + @Column({ type: 'int', default: 10 }) + maxTurns!: number; + + @Column({ type: 'float', default: 5.0 }) + maxBudget!: number; + + @Column({ type: 'varchar', array: true, default: "'{Bash,Read,Write,Glob,Grep}'" }) + allowedTools!: string[]; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; +} diff --git a/packages/services/agent-service/src/domain/entities/agent-skill.entity.ts b/packages/services/agent-service/src/domain/entities/agent-skill.entity.ts new file mode 100644 index 0000000..96ca0f8 --- /dev/null +++ b/packages/services/agent-service/src/domain/entities/agent-skill.entity.ts @@ -0,0 +1,40 @@ +/** + * Per-tenant agent skill entity. + * + * Skills are reusable script templates that the AI agent can execute. + * Each skill has a category, script content, and can be enabled/disabled. + */ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('agent_skills') +export class AgentSkill { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ type: 'varchar', length: 20 }) + tenantId!: string; + + @Column({ type: 'varchar', length: 100 }) + name!: string; + + @Column({ type: 'text', default: '' }) + description!: string; + + @Column({ type: 'varchar', length: 30, default: 'custom' }) + category!: string; + + @Column({ type: 'text', default: '' }) + script!: string; + + @Column({ type: 'varchar', array: true, default: '{}' }) + tags!: string[]; + + @Column({ type: 'boolean', default: true }) + enabled!: boolean; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; +} diff --git a/packages/services/agent-service/src/domain/entities/hook-script.entity.ts b/packages/services/agent-service/src/domain/entities/hook-script.entity.ts new file mode 100644 index 0000000..3b3f586 --- /dev/null +++ b/packages/services/agent-service/src/domain/entities/hook-script.entity.ts @@ -0,0 +1,46 @@ +/** + * Per-tenant hook script entity. + * + * Hook scripts run at specific lifecycle points during agent tool execution: + * - PreToolUse: runs before a tool is invoked (can block execution) + * - PostToolUse: runs after a tool completes (for auditing/logging) + * - PreNotification: runs before sending a notification + * - PostNotification: runs after sending a notification + */ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('hook_scripts') +export class HookScript { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ type: 'varchar', length: 20 }) + tenantId!: string; + + @Column({ type: 'varchar', length: 100 }) + name!: string; + + @Column({ type: 'varchar', length: 30 }) + event!: string; + + @Column({ type: 'varchar', length: 200, default: '*' }) + toolPattern!: string; + + @Column({ type: 'text', default: '' }) + script!: string; + + @Column({ type: 'int', default: 30 }) + timeout!: number; + + @Column({ type: 'boolean', default: true }) + enabled!: boolean; + + @Column({ type: 'text', default: '' }) + description!: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; +} diff --git a/packages/services/agent-service/src/infrastructure/repositories/agent-config.repository.ts b/packages/services/agent-service/src/infrastructure/repositories/agent-config.repository.ts new file mode 100644 index 0000000..7e0624a --- /dev/null +++ b/packages/services/agent-service/src/infrastructure/repositories/agent-config.repository.ts @@ -0,0 +1,20 @@ +/** + * Repository for AgentConfig. + * Extends TenantAwareRepository for schema-per-tenant isolation. + */ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { TenantAwareRepository } from '@it0/database'; +import { AgentConfig } from '../../domain/entities/agent-config.entity'; + +@Injectable() +export class AgentConfigRepository extends TenantAwareRepository { + constructor(dataSource: DataSource) { + super(dataSource, AgentConfig); + } + + async findByTenantId(tenantId: string): Promise { + const repo = await this.getRepository(); + return repo.findOneBy({ tenantId } as any); + } +} diff --git a/packages/services/agent-service/src/infrastructure/repositories/agent-skill.repository.ts b/packages/services/agent-service/src/infrastructure/repositories/agent-skill.repository.ts new file mode 100644 index 0000000..8ad4db1 --- /dev/null +++ b/packages/services/agent-service/src/infrastructure/repositories/agent-skill.repository.ts @@ -0,0 +1,30 @@ +/** + * Repository for AgentSkill. + * Extends TenantAwareRepository for schema-per-tenant isolation. + */ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { TenantAwareRepository } from '@it0/database'; +import { AgentSkill } from '../../domain/entities/agent-skill.entity'; + +@Injectable() +export class AgentSkillRepository extends TenantAwareRepository { + constructor(dataSource: DataSource) { + super(dataSource, AgentSkill); + } + + async findByTenantId(tenantId: string): Promise { + const repo = await this.getRepository(); + return repo.find({ where: { tenantId } as any, order: { createdAt: 'DESC' } }); + } + + async findOneByIdAndTenant(id: string, tenantId: string): Promise { + const repo = await this.getRepository(); + return repo.findOneBy({ id, tenantId } as any); + } + + async deleteById(id: string): Promise { + const repo = await this.getRepository(); + await repo.delete(id); + } +} diff --git a/packages/services/agent-service/src/infrastructure/repositories/hook-script.repository.ts b/packages/services/agent-service/src/infrastructure/repositories/hook-script.repository.ts new file mode 100644 index 0000000..01147bd --- /dev/null +++ b/packages/services/agent-service/src/infrastructure/repositories/hook-script.repository.ts @@ -0,0 +1,30 @@ +/** + * Repository for HookScript. + * Extends TenantAwareRepository for schema-per-tenant isolation. + */ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { TenantAwareRepository } from '@it0/database'; +import { HookScript } from '../../domain/entities/hook-script.entity'; + +@Injectable() +export class HookScriptRepository extends TenantAwareRepository { + constructor(dataSource: DataSource) { + super(dataSource, HookScript); + } + + async findByTenantId(tenantId: string): Promise { + const repo = await this.getRepository(); + return repo.find({ where: { tenantId } as any, order: { createdAt: 'DESC' } }); + } + + async findOneByIdAndTenant(id: string, tenantId: string): Promise { + const repo = await this.getRepository(); + return repo.findOneBy({ id, tenantId } as any); + } + + async deleteById(id: string): Promise { + const repo = await this.getRepository(); + await repo.delete(id); + } +} diff --git a/packages/services/agent-service/src/infrastructure/services/agent-config.service.ts b/packages/services/agent-service/src/infrastructure/services/agent-config.service.ts new file mode 100644 index 0000000..854a60c --- /dev/null +++ b/packages/services/agent-service/src/infrastructure/services/agent-config.service.ts @@ -0,0 +1,45 @@ +/** + * Service for managing per-tenant agent configuration (engine, system prompt, max turns, budget, tools). + */ +import { Injectable } from '@nestjs/common'; +import { AgentConfigRepository } from '../repositories/agent-config.repository'; +import { AgentConfig } from '../../domain/entities/agent-config.entity'; + +export interface UpdateAgentConfigDto { + engine?: string; + system_prompt?: string; + max_turns?: number; + max_budget?: number; + allowed_tools?: string[]; +} + +@Injectable() +export class AgentConfigService { + constructor(private readonly repo: AgentConfigRepository) {} + + async findByTenantId(tenantId: string): Promise { + return this.repo.findByTenantId(tenantId); + } + + async create(tenantId: string, dto: UpdateAgentConfigDto): Promise { + const config = new AgentConfig(); + config.tenantId = tenantId; + this.applyDto(config, dto); + return this.repo.save(config); + } + + async update(id: string, tenantId: string, dto: UpdateAgentConfigDto): Promise { + const config = await this.repo.findByTenantId(tenantId); + if (!config || config.id !== id) return null; + this.applyDto(config, dto); + return this.repo.save(config); + } + + private applyDto(config: AgentConfig, dto: UpdateAgentConfigDto): void { + if (dto.engine !== undefined) config.engine = dto.engine; + if (dto.system_prompt !== undefined) config.systemPrompt = dto.system_prompt; + if (dto.max_turns !== undefined) config.maxTurns = dto.max_turns; + if (dto.max_budget !== undefined) config.maxBudget = dto.max_budget; + if (dto.allowed_tools !== undefined) config.allowedTools = dto.allowed_tools; + } +} diff --git a/packages/services/agent-service/src/infrastructure/services/agent-skill.service.ts b/packages/services/agent-service/src/infrastructure/services/agent-skill.service.ts new file mode 100644 index 0000000..294ea93 --- /dev/null +++ b/packages/services/agent-service/src/infrastructure/services/agent-skill.service.ts @@ -0,0 +1,65 @@ +/** + * Service for managing per-tenant agent skills (CRUD). + */ +import { Injectable } from '@nestjs/common'; +import { AgentSkillRepository } from '../repositories/agent-skill.repository'; +import { AgentSkill } from '../../domain/entities/agent-skill.entity'; + +export interface CreateSkillDto { + name: string; + description?: string; + category?: string; + script?: string; + tags?: string[]; + enabled?: boolean; +} + +export interface UpdateSkillDto { + name?: string; + description?: string; + category?: string; + script?: string; + tags?: string[]; + enabled?: boolean; +} + +@Injectable() +export class AgentSkillService { + constructor(private readonly repo: AgentSkillRepository) {} + + async findAllByTenant(tenantId: string): Promise<{ data: AgentSkill[]; total: number }> { + const skills = await this.repo.findByTenantId(tenantId); + return { data: skills, total: skills.length }; + } + + async create(tenantId: string, dto: CreateSkillDto): Promise { + const skill = new AgentSkill(); + skill.tenantId = tenantId; + skill.name = dto.name; + skill.description = dto.description ?? ''; + skill.category = dto.category ?? 'custom'; + skill.script = dto.script ?? ''; + skill.tags = dto.tags ?? []; + skill.enabled = dto.enabled ?? true; + return this.repo.save(skill); + } + + async update(id: string, tenantId: string, dto: UpdateSkillDto): Promise { + const skill = await this.repo.findOneByIdAndTenant(id, tenantId); + if (!skill) return null; + if (dto.name !== undefined) skill.name = dto.name; + if (dto.description !== undefined) skill.description = dto.description; + if (dto.category !== undefined) skill.category = dto.category; + if (dto.script !== undefined) skill.script = dto.script; + if (dto.tags !== undefined) skill.tags = dto.tags; + if (dto.enabled !== undefined) skill.enabled = dto.enabled; + return this.repo.save(skill); + } + + async delete(id: string, tenantId: string): Promise { + const skill = await this.repo.findOneByIdAndTenant(id, tenantId); + if (!skill) return false; + await this.repo.deleteById(id); + return true; + } +} diff --git a/packages/services/agent-service/src/infrastructure/services/hook-script.service.ts b/packages/services/agent-service/src/infrastructure/services/hook-script.service.ts new file mode 100644 index 0000000..bffd5cb --- /dev/null +++ b/packages/services/agent-service/src/infrastructure/services/hook-script.service.ts @@ -0,0 +1,69 @@ +/** + * Service for managing per-tenant hook scripts (CRUD). + */ +import { Injectable } from '@nestjs/common'; +import { HookScriptRepository } from '../repositories/hook-script.repository'; +import { HookScript } from '../../domain/entities/hook-script.entity'; + +export interface CreateHookDto { + name: string; + event: string; + toolPattern?: string; + script: string; + timeout?: number; + enabled?: boolean; + description?: string; +} + +export interface UpdateHookDto { + name?: string; + event?: string; + toolPattern?: string; + script?: string; + timeout?: number; + enabled?: boolean; + description?: string; +} + +@Injectable() +export class HookScriptService { + constructor(private readonly repo: HookScriptRepository) {} + + async findAllByTenant(tenantId: string): Promise<{ data: HookScript[]; total: number }> { + const hooks = await this.repo.findByTenantId(tenantId); + return { data: hooks, total: hooks.length }; + } + + async create(tenantId: string, dto: CreateHookDto): Promise { + const hook = new HookScript(); + hook.tenantId = tenantId; + hook.name = dto.name; + hook.event = dto.event; + hook.toolPattern = dto.toolPattern ?? '*'; + hook.script = dto.script; + hook.timeout = dto.timeout ?? 30; + hook.enabled = dto.enabled ?? true; + hook.description = dto.description ?? ''; + return this.repo.save(hook); + } + + async update(id: string, tenantId: string, dto: UpdateHookDto): Promise { + const hook = await this.repo.findOneByIdAndTenant(id, tenantId); + if (!hook) return null; + if (dto.name !== undefined) hook.name = dto.name; + if (dto.event !== undefined) hook.event = dto.event; + if (dto.toolPattern !== undefined) hook.toolPattern = dto.toolPattern; + if (dto.script !== undefined) hook.script = dto.script; + if (dto.timeout !== undefined) hook.timeout = dto.timeout; + if (dto.enabled !== undefined) hook.enabled = dto.enabled; + if (dto.description !== undefined) hook.description = dto.description; + return this.repo.save(hook); + } + + async delete(id: string, tenantId: string): Promise { + const hook = await this.repo.findOneByIdAndTenant(id, tenantId); + if (!hook) return false; + await this.repo.deleteById(id); + return true; + } +} diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/agent-config.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/agent-config.controller.ts new file mode 100644 index 0000000..7444766 --- /dev/null +++ b/packages/services/agent-service/src/interfaces/rest/controllers/agent-config.controller.ts @@ -0,0 +1,77 @@ +/** + * REST controller for per-tenant agent configuration (engine, prompt, turns, budget, tools). + * + * Endpoints (all JWT-protected): + * GET /api/v1/agent-config → Get current tenant's config (returns defaults if none set) + * POST /api/v1/agent-config → Create new config + * PUT /api/v1/agent-config/:id → Update existing config + */ +import { Controller, Get, Post, Put, Body, Param, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { TenantId } from '@it0/common'; +import { AgentConfigService, UpdateAgentConfigDto } from '../../../infrastructure/services/agent-config.service'; + +@Controller('api/v1/agent-config') +@UseGuards(AuthGuard('jwt')) +export class AgentConfigController { + constructor(private readonly configService: AgentConfigService) {} + + @Get() + async getConfig(@TenantId() tenantId: string) { + const config = await this.configService.findByTenantId(tenantId); + if (!config) { + return { + engine: 'claude-cli', + system_prompt: '', + max_turns: 10, + max_budget: 5.0, + allowed_tools: ['Bash', 'Read', 'Write', 'Glob', 'Grep'], + }; + } + + return { + id: config.id, + engine: config.engine, + system_prompt: config.systemPrompt, + max_turns: config.maxTurns, + max_budget: config.maxBudget, + allowed_tools: config.allowedTools, + }; + } + + @Post() + async createConfig( + @TenantId() tenantId: string, + @Body() dto: UpdateAgentConfigDto, + ) { + const config = await this.configService.create(tenantId, dto); + return { + id: config.id, + engine: config.engine, + system_prompt: config.systemPrompt, + max_turns: config.maxTurns, + max_budget: config.maxBudget, + allowed_tools: config.allowedTools, + }; + } + + @Put(':id') + async updateConfig( + @TenantId() tenantId: string, + @Param('id') id: string, + @Body() dto: UpdateAgentConfigDto, + ) { + const config = await this.configService.update(id, tenantId, dto); + if (!config) { + return { error: 'Config not found' }; + } + return { + id: config.id, + engine: config.engine, + system_prompt: config.systemPrompt, + max_turns: config.maxTurns, + max_budget: config.maxBudget, + allowed_tools: config.allowedTools, + }; + } +} diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/hooks.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/hooks.controller.ts new file mode 100644 index 0000000..828a7ec --- /dev/null +++ b/packages/services/agent-service/src/interfaces/rest/controllers/hooks.controller.ts @@ -0,0 +1,57 @@ +/** + * REST controller for per-tenant hook scripts (CRUD). + * + * Endpoints (all JWT-protected): + * GET /api/v1/agent/hooks → List all hooks for current tenant + * POST /api/v1/agent/hooks → Create a new hook + * PUT /api/v1/agent/hooks/:id → Update an existing hook + * DELETE /api/v1/agent/hooks/:id → Delete a hook + */ +import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, NotFoundException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { TenantId } from '@it0/common'; +import { HookScriptService, CreateHookDto, UpdateHookDto } from '../../../infrastructure/services/hook-script.service'; + +@Controller('api/v1/agent/hooks') +@UseGuards(AuthGuard('jwt')) +export class HooksController { + constructor(private readonly hookService: HookScriptService) {} + + @Get() + async list(@TenantId() tenantId: string) { + return this.hookService.findAllByTenant(tenantId); + } + + @Post() + async create( + @TenantId() tenantId: string, + @Body() dto: CreateHookDto, + ) { + return this.hookService.create(tenantId, dto); + } + + @Put(':id') + async update( + @TenantId() tenantId: string, + @Param('id') id: string, + @Body() dto: UpdateHookDto, + ) { + const hook = await this.hookService.update(id, tenantId, dto); + if (!hook) { + throw new NotFoundException('Hook not found'); + } + return hook; + } + + @Delete(':id') + async remove( + @TenantId() tenantId: string, + @Param('id') id: string, + ) { + const deleted = await this.hookService.delete(id, tenantId); + if (!deleted) { + throw new NotFoundException('Hook not found'); + } + return { message: 'Hook deleted' }; + } +} diff --git a/packages/services/agent-service/src/interfaces/rest/controllers/skills.controller.ts b/packages/services/agent-service/src/interfaces/rest/controllers/skills.controller.ts new file mode 100644 index 0000000..5ee6041 --- /dev/null +++ b/packages/services/agent-service/src/interfaces/rest/controllers/skills.controller.ts @@ -0,0 +1,57 @@ +/** + * REST controller for per-tenant agent skills (CRUD). + * + * Endpoints (all JWT-protected): + * GET /api/v1/agent/skills → List all skills for current tenant + * POST /api/v1/agent/skills → Create a new skill + * PUT /api/v1/agent/skills/:id → Update an existing skill + * DELETE /api/v1/agent/skills/:id → Delete a skill + */ +import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, NotFoundException } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { TenantId } from '@it0/common'; +import { AgentSkillService, CreateSkillDto, UpdateSkillDto } from '../../../infrastructure/services/agent-skill.service'; + +@Controller('api/v1/agent/skills') +@UseGuards(AuthGuard('jwt')) +export class SkillsController { + constructor(private readonly skillService: AgentSkillService) {} + + @Get() + async list(@TenantId() tenantId: string) { + return this.skillService.findAllByTenant(tenantId); + } + + @Post() + async create( + @TenantId() tenantId: string, + @Body() dto: CreateSkillDto, + ) { + return this.skillService.create(tenantId, dto); + } + + @Put(':id') + async update( + @TenantId() tenantId: string, + @Param('id') id: string, + @Body() dto: UpdateSkillDto, + ) { + const skill = await this.skillService.update(id, tenantId, dto); + if (!skill) { + throw new NotFoundException('Skill not found'); + } + return skill; + } + + @Delete(':id') + async remove( + @TenantId() tenantId: string, + @Param('id') id: string, + ) { + const deleted = await this.skillService.delete(id, tenantId); + if (!deleted) { + throw new NotFoundException('Skill not found'); + } + return { message: 'Skill deleted' }; + } +}