241 lines
8.4 KiB
TypeScript
241 lines
8.4 KiB
TypeScript
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
|
import { ToolRegistryService } from '@features/assistant/backend/skills/tool-registry.service';
|
|
import { SprintsService } from './sprints.service';
|
|
import { TasksService } from '@features/tasks/backend/tasks.service';
|
|
|
|
@Injectable()
|
|
export class ProjectsToolsProvider implements OnApplicationBootstrap {
|
|
constructor(
|
|
private readonly toolRegistry: ToolRegistryService,
|
|
private readonly sprintsService: SprintsService,
|
|
private readonly tasksService: TasksService,
|
|
) {}
|
|
|
|
onApplicationBootstrap(): void {
|
|
this.toolRegistry.register({
|
|
name: 'create_sprint',
|
|
description: 'Create a new sprint in a project with start/end dates and optional goal',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
projectId: { type: 'string', description: 'Project UUID' },
|
|
name: { type: 'string', description: 'Sprint name' },
|
|
startDate: { type: 'string', description: 'Start date in YYYY-MM-DD format' },
|
|
endDate: { type: 'string', description: 'End date in YYYY-MM-DD format' },
|
|
goal: { type: 'string', description: 'Sprint goal (optional)' },
|
|
},
|
|
required: ['projectId', 'name', 'startDate', 'endDate'],
|
|
},
|
|
handler: async (params) => {
|
|
try {
|
|
const sprint = await this.sprintsService.createSprint({
|
|
projectId: params.projectId as string,
|
|
name: params.name as string,
|
|
startDate: params.startDate as string,
|
|
endDate: params.endDate as string,
|
|
goal: params.goal as string | undefined,
|
|
});
|
|
return {
|
|
ok: true,
|
|
output: `Created sprint "${sprint.name}" (id: ${sprint.id}), status: ${sprint.status}`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: `Failed to create sprint: ${error instanceof Error ? error.message : error}`,
|
|
};
|
|
}
|
|
},
|
|
always: false,
|
|
});
|
|
|
|
this.toolRegistry.register({
|
|
name: 'list_sprints',
|
|
description: 'List sprints, optionally filtered by domain or status',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
domainId: { type: 'string', description: 'Filter by domain UUID' },
|
|
status: {
|
|
type: 'string',
|
|
description: 'Filter by status: planned, active, completed, cancelled',
|
|
},
|
|
limit: { type: 'integer', description: 'Max results (default 10)' },
|
|
},
|
|
},
|
|
handler: async (params) => {
|
|
try {
|
|
const result = await this.sprintsService.findAll({
|
|
domainId: params.domainId as string | undefined,
|
|
status: params.status as string | undefined,
|
|
limit: (params.limit as number) ?? 10,
|
|
page: 1,
|
|
} as Record<string, unknown>);
|
|
if (result.data.length === 0) {
|
|
return { ok: true, output: 'No sprints found matching the filters.' };
|
|
}
|
|
const summary = result.data
|
|
.map(
|
|
(s) =>
|
|
`- [${s.status}] ${s.name} (${s.startDate} → ${s.endDate})${s.goal ? ` — Goal: ${s.goal}` : ''}`,
|
|
)
|
|
.join('\n');
|
|
return {
|
|
ok: true,
|
|
output: `Found ${result.meta.total} sprints (showing ${result.data.length}):\n${summary}`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: `Failed to list sprints: ${error instanceof Error ? error.message : error}`,
|
|
};
|
|
}
|
|
},
|
|
always: false,
|
|
});
|
|
|
|
this.toolRegistry.register({
|
|
name: 'get_sprint_tasks',
|
|
description: 'Get a sprint with all its tasks grouped by status',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
sprintId: { type: 'string', description: 'Sprint UUID' },
|
|
},
|
|
required: ['sprintId'],
|
|
},
|
|
handler: async (params) => {
|
|
try {
|
|
const sprint = await this.sprintsService.findOneWithTasks(params.sprintId as string);
|
|
const grouped: Record<string, string[]> = {};
|
|
for (const t of sprint.tasks) {
|
|
if (!grouped[t.status]) grouped[t.status] = [];
|
|
grouped[t.status].push(`[${t.priority}] ${t.title}`);
|
|
}
|
|
const sections = Object.entries(grouped)
|
|
.map(
|
|
([status, tasks]) =>
|
|
`${status.toUpperCase()} (${tasks.length}):\n${tasks.map((t) => ` - ${t}`).join('\n')}`,
|
|
)
|
|
.join('\n\n');
|
|
const header = [
|
|
`Sprint: ${sprint.name} [${sprint.status}]`,
|
|
`Period: ${sprint.startDate} → ${sprint.endDate}`,
|
|
sprint.goal ? `Goal: ${sprint.goal}` : null,
|
|
`Total tasks: ${sprint.tasks.length}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
return {
|
|
ok: true,
|
|
output:
|
|
sprint.tasks.length > 0
|
|
? `${header}\n\n${sections}`
|
|
: `${header}\n\nNo tasks assigned to this sprint.`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: `Failed to get sprint tasks: ${error instanceof Error ? error.message : error}`,
|
|
};
|
|
}
|
|
},
|
|
always: false,
|
|
});
|
|
|
|
this.toolRegistry.register({
|
|
name: 'update_sprint',
|
|
description: "Update a sprint's status, goal, or retrospective",
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
sprintId: { type: 'string', description: 'Sprint UUID' },
|
|
status: {
|
|
type: 'string',
|
|
description: 'New status: planned, active, completed, cancelled',
|
|
},
|
|
goal: { type: 'string', description: 'Updated sprint goal' },
|
|
retrospective: { type: 'string', description: 'Sprint retrospective notes' },
|
|
name: { type: 'string', description: 'Rename the sprint' },
|
|
},
|
|
required: ['sprintId'],
|
|
},
|
|
handler: async (params) => {
|
|
try {
|
|
const dto: Record<string, unknown> = {};
|
|
if (params.status) dto['status'] = params.status;
|
|
if (params.goal) dto['goal'] = params.goal;
|
|
if (params.retrospective) dto['retrospective'] = params.retrospective;
|
|
if (params.name) dto['name'] = params.name;
|
|
const sprint = await this.sprintsService.updateSprint(
|
|
params.sprintId as string,
|
|
dto,
|
|
);
|
|
return {
|
|
ok: true,
|
|
output: `Updated sprint "${sprint.name}" — status: ${sprint.status}`,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: `Failed to update sprint: ${error instanceof Error ? error.message : error}`,
|
|
};
|
|
}
|
|
},
|
|
always: false,
|
|
});
|
|
|
|
this.toolRegistry.register({
|
|
name: 'assign_task_to_sprint',
|
|
description: 'Assign an existing task to a sprint',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
taskId: { type: 'string', description: 'Task UUID' },
|
|
sprintId: { type: 'string', description: 'Sprint UUID' },
|
|
},
|
|
required: ['taskId', 'sprintId'],
|
|
},
|
|
handler: async (params) => {
|
|
try {
|
|
const task = await this.tasksService.updateTask(params.taskId as string, {
|
|
sprintId: params.sprintId as string,
|
|
});
|
|
return { ok: true, output: `Assigned task "${task.title}" to sprint` };
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: `Failed to assign task: ${error instanceof Error ? error.message : error}`,
|
|
};
|
|
}
|
|
},
|
|
always: false,
|
|
});
|
|
|
|
this.toolRegistry.register({
|
|
name: 'remove_task_from_sprint',
|
|
description: 'Remove a task from its sprint (unassign)',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
taskId: { type: 'string', description: 'Task UUID' },
|
|
},
|
|
required: ['taskId'],
|
|
},
|
|
handler: async (params) => {
|
|
try {
|
|
const task = await this.tasksService.updateTask(params.taskId as string, {
|
|
sprintId: null,
|
|
});
|
|
return { ok: true, output: `Removed task "${task.title}" from sprint` };
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: `Failed to remove task from sprint: ${error instanceof Error ? error.message : error}`,
|
|
};
|
|
}
|
|
},
|
|
always: false,
|
|
});
|
|
}
|
|
}
|