@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,042 lines • 42.3 kB
JavaScript
import { randomUUID } from 'crypto';
import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js';
/**
* Create a new issue
*/
const createIssueTool = createTool({
name: 'create_issue',
description: 'Create a new issue (bug, feature, enhancement, etc.)',
category: 'issue-tracking',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['bug', 'feature', 'enhancement', 'documentation', 'question'],
description: 'Type of issue'
},
title: {
type: 'string',
description: 'Issue title',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Detailed description of the issue'
},
priority: {
type: 'string',
enum: ['critical', 'high', 'medium', 'low'],
description: 'Issue priority level'
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Labels for categorization',
maxItems: 20
},
affectedModules: {
type: 'array',
items: { type: 'string' },
description: 'Modules affected by this issue',
maxItems: 10
},
assignedTo: {
type: 'string',
description: 'User to assign the issue to'
},
relatedIssues: {
type: 'array',
items: {
type: 'string',
pattern: '^ISSUE-\\d{4}$'
},
description: 'Related issue IDs',
maxItems: 10
}
},
required: ['type', 'title', 'description', 'priority'],
additionalProperties: false
},
async execute(input, context) {
try {
// Generate issue ID
const countResult = await context.db.get('SELECT COUNT(*) as count FROM issue_tracker_issues WHERE project_id = ?', [context.projectId || 'default']);
const issueNumber = (countResult.data?.count || 0) + 1;
const issueId = `ISSUE-${issueNumber.toString().padStart(4, '0')}`;
const now = Date.now();
// Create issue
const result = await context.db.run(`INSERT INTO issue_tracker_issues
(id, project_id, type, title, description, status, priority, created_by,
assigned_to, labels, affected_modules, related_issues, metadata,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
issueId,
context.projectId || 'default',
input.type,
input.title,
input.description,
'open',
input.priority,
context.userId || 'system',
input.assignedTo || null,
JSON.stringify(input.labels || []),
JSON.stringify(input.affectedModules || []),
JSON.stringify(input.relatedIssues || []),
JSON.stringify({}),
now,
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to create issue',
category: 'system'
});
}
// Add creation comment
await context.db.run(`INSERT INTO issue_tracker_comments
(id, issue_id, project_id, author, content, type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
randomUUID(),
issueId,
context.projectId || 'default',
context.userId || 'system',
`Issue created: ${input.title}`,
'status-change',
now
]);
return createSuccessResult({
issue: {
id: issueId,
type: input.type,
title: input.title,
description: input.description,
status: 'open',
priority: input.priority,
createdBy: context.userId || 'system',
assignedTo: input.assignedTo || null,
labels: input.labels || [],
affectedModules: input.affectedModules || [],
relatedIssues: input.relatedIssues || [],
createdAt: new Date(now).toISOString(),
updatedAt: new Date(now).toISOString()
},
message: `Issue ${issueId} created successfully`
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to create issue: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Update an existing issue
*/
const updateIssueTool = createTool({
name: 'update_issue',
description: 'Update an existing issue',
category: 'issue-tracking',
inputSchema: {
type: 'object',
properties: {
issueId: {
type: 'string',
description: 'Issue ID to update',
pattern: '^ISSUE-\\d{4}$'
},
type: {
type: 'string',
enum: ['bug', 'feature', 'enhancement', 'documentation', 'question'],
description: 'Type of issue'
},
title: {
type: 'string',
description: 'Issue title',
minLength: 1,
maxLength: 200
},
description: {
type: 'string',
description: 'Detailed description'
},
status: {
type: 'string',
enum: ['open', 'in-progress', 'resolved', 'closed', 'wont-fix'],
description: 'Issue status'
},
priority: {
type: 'string',
enum: ['critical', 'high', 'medium', 'low'],
description: 'Priority level'
},
assignedTo: {
type: ['string', 'null'],
description: 'User to assign to (null to unassign)'
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Labels for categorization',
maxItems: 20
},
affectedModules: {
type: 'array',
items: { type: 'string' },
description: 'Affected modules',
maxItems: 10
},
relatedIssues: {
type: 'array',
items: {
type: 'string',
pattern: '^ISSUE-\\d{4}$'
},
description: 'Related issue IDs',
maxItems: 10
},
resolution: {
type: 'string',
description: 'Resolution description'
}
},
required: ['issueId'],
additionalProperties: false
},
async execute(input, context) {
try {
// Check if issue exists
const issueResult = await context.db.get('SELECT * FROM issue_tracker_issues WHERE id = ? AND project_id = ?', [input.issueId, context.projectId || 'default']);
if (!issueResult.success || !issueResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Issue not found',
category: 'validation'
});
}
const oldIssue = issueResult.data;
const now = Date.now();
const updates = [];
const values = [];
const changes = [];
// Build update query
if (input.type !== undefined) {
updates.push('type = ?');
values.push(input.type);
if (input.type !== oldIssue.type) {
changes.push(`type: ${oldIssue.type} → ${input.type}`);
}
}
if (input.title !== undefined) {
updates.push('title = ?');
values.push(input.title);
if (input.title !== oldIssue.title) {
changes.push(`title updated`);
}
}
if (input.description !== undefined) {
updates.push('description = ?');
values.push(input.description);
changes.push('description updated');
}
if (input.status !== undefined) {
updates.push('status = ?');
values.push(input.status);
if (input.status !== oldIssue.status) {
changes.push(`status: ${oldIssue.status} → ${input.status}`);
}
}
if (input.priority !== undefined) {
updates.push('priority = ?');
values.push(input.priority);
if (input.priority !== oldIssue.priority) {
changes.push(`priority: ${oldIssue.priority} → ${input.priority}`);
}
}
if (input.assignedTo !== undefined) {
updates.push('assigned_to = ?');
values.push(input.assignedTo);
const oldAssignee = oldIssue.assigned_to || 'unassigned';
const newAssignee = input.assignedTo || 'unassigned';
if (oldAssignee !== newAssignee) {
changes.push(`assigned to: ${oldAssignee} → ${newAssignee}`);
}
}
if (input.labels !== undefined) {
updates.push('labels = ?');
values.push(JSON.stringify(input.labels));
changes.push('labels updated');
}
if (input.affectedModules !== undefined) {
updates.push('affected_modules = ?');
values.push(JSON.stringify(input.affectedModules));
changes.push('affected modules updated');
}
if (input.relatedIssues !== undefined) {
updates.push('related_issues = ?');
values.push(JSON.stringify(input.relatedIssues));
changes.push('related issues updated');
}
if (input.resolution !== undefined) {
updates.push('resolution = ?');
values.push(input.resolution);
changes.push('resolution added');
}
// Handle closed status
if (input.status === 'closed' || input.status === 'resolved') {
updates.push('closed_at = ?');
values.push(now);
}
updates.push('updated_at = ?');
values.push(now);
if (updates.length === 1) {
return createErrorResult({
code: 'VALIDATION_ERROR',
message: 'No updates provided',
category: 'validation'
});
}
// Update issue
values.push(input.issueId, context.projectId || 'default');
const updateResult = await context.db.run(`UPDATE issue_tracker_issues SET ${updates.join(', ')} WHERE id = ? AND project_id = ?`, values);
if (!updateResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to update issue',
category: 'system'
});
}
// Add update comment if changes were made
if (changes.length > 0) {
await context.db.run(`INSERT INTO issue_tracker_comments
(id, issue_id, project_id, author, content, type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
randomUUID(),
input.issueId,
context.projectId || 'default',
context.userId || 'system',
`Issue updated: ${changes.join(', ')}`,
'status-change',
now
]);
}
// Get updated issue
const updatedResult = await context.db.get('SELECT * FROM issue_tracker_issues WHERE id = ? AND project_id = ?', [input.issueId, context.projectId || 'default']);
return createSuccessResult({
issue: {
id: updatedResult.data.id,
type: updatedResult.data.type,
title: updatedResult.data.title,
description: updatedResult.data.description,
status: updatedResult.data.status,
priority: updatedResult.data.priority,
createdBy: updatedResult.data.created_by,
assignedTo: updatedResult.data.assigned_to,
labels: JSON.parse(updatedResult.data.labels || '[]'),
affectedModules: JSON.parse(updatedResult.data.affected_modules || '[]'),
relatedIssues: JSON.parse(updatedResult.data.related_issues || '[]'),
resolution: updatedResult.data.resolution,
closedAt: updatedResult.data.closed_at ? new Date(updatedResult.data.closed_at).toISOString() : null,
createdAt: new Date(updatedResult.data.created_at).toISOString(),
updatedAt: new Date(updatedResult.data.updated_at).toISOString()
},
changes,
message: `Issue ${input.issueId} updated successfully`
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to update issue: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Add a comment to an issue
*/
const addCommentTool = createTool({
name: 'add_issue_comment',
description: 'Add a comment to an issue',
category: 'issue-tracking',
inputSchema: {
type: 'object',
properties: {
issueId: {
type: 'string',
description: 'Issue ID to comment on',
pattern: '^ISSUE-\\d{4}$'
},
content: {
type: 'string',
description: 'Comment content',
minLength: 1,
maxLength: 5000
},
type: {
type: 'string',
enum: ['comment', 'status-change', 'assignment', 'resolution'],
description: 'Type of comment',
default: 'comment'
}
},
required: ['issueId', 'content'],
additionalProperties: false
},
async execute(input, context) {
try {
// Verify issue exists
const issueResult = await context.db.get('SELECT id FROM issue_tracker_issues WHERE id = ? AND project_id = ?', [input.issueId, context.projectId || 'default']);
if (!issueResult.success || !issueResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Issue not found',
category: 'validation'
});
}
const commentId = randomUUID();
const now = Date.now();
// Add comment
const result = await context.db.run(`INSERT INTO issue_tracker_comments
(id, issue_id, project_id, author, content, type, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
commentId,
input.issueId,
context.projectId || 'default',
context.userId || 'system',
input.content,
input.type || 'comment',
now
]);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to add comment',
category: 'system'
});
}
// Update issue's updated_at timestamp
await context.db.run('UPDATE issue_tracker_issues SET updated_at = ? WHERE id = ? AND project_id = ?', [now, input.issueId, context.projectId || 'default']);
return createSuccessResult({
comment: {
id: commentId,
issueId: input.issueId,
author: context.userId || 'system',
content: input.content,
type: input.type || 'comment',
createdAt: new Date(now).toISOString()
},
message: 'Comment added successfully'
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to add comment: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* List issues with filtering
*/
const listIssuesTool = createTool({
name: 'list_issues',
description: 'List issues with optional filtering',
category: 'issue-tracking',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
type: {
type: 'array',
items: {
type: 'string',
enum: ['bug', 'feature', 'enhancement', 'documentation', 'question']
},
description: 'Filter by issue types'
},
status: {
type: 'array',
items: {
type: 'string',
enum: ['open', 'in-progress', 'resolved', 'closed', 'wont-fix']
},
description: 'Filter by status'
},
priority: {
type: 'array',
items: {
type: 'string',
enum: ['critical', 'high', 'medium', 'low']
},
description: 'Filter by priority'
},
assignedTo: {
type: 'string',
description: 'Filter by assignee'
},
createdBy: {
type: 'string',
description: 'Filter by creator'
},
labels: {
type: 'array',
items: { type: 'string' },
description: 'Filter by labels (any match)'
},
affectedModules: {
type: 'array',
items: { type: 'string' },
description: 'Filter by affected modules (any match)'
},
limit: {
type: 'integer',
description: 'Maximum number of results',
minimum: 1,
maximum: 100,
default: 20
},
offset: {
type: 'integer',
description: 'Offset for pagination',
minimum: 0,
default: 0
}
},
required: [],
additionalProperties: false
},
async execute(input, context) {
try {
let query = 'SELECT * FROM issue_tracker_issues WHERE project_id = ?';
const params = [context.projectId || 'default'];
// Build filters
if (input.type && input.type.length > 0) {
query += ` AND type IN (${input.type.map(() => '?').join(',')})`;
params.push(...input.type);
}
if (input.status && input.status.length > 0) {
query += ` AND status IN (${input.status.map(() => '?').join(',')})`;
params.push(...input.status);
}
if (input.priority && input.priority.length > 0) {
query += ` AND priority IN (${input.priority.map(() => '?').join(',')})`;
params.push(...input.priority);
}
if (input.assignedTo) {
query += ' AND assigned_to = ?';
params.push(input.assignedTo);
}
if (input.createdBy) {
query += ' AND created_by = ?';
params.push(input.createdBy);
}
// Order by priority and creation date
query += ' ORDER BY CASE priority WHEN "critical" THEN 1 WHEN "high" THEN 2 WHEN "medium" THEN 3 ELSE 4 END, created_at DESC';
// Add pagination
query += ' LIMIT ? OFFSET ?';
params.push(input.limit || 20, input.offset || 0);
const result = await context.db.query(query, params);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to list issues',
category: 'system'
});
}
const issues = [];
for (const row of result.data || []) {
// Apply label and module filters in memory
const labels = JSON.parse(row.labels || '[]');
const affectedModules = JSON.parse(row.affected_modules || '[]');
if (input.labels && input.labels.length > 0) {
if (!input.labels.some(label => labels.includes(label))) {
continue;
}
}
if (input.affectedModules && input.affectedModules.length > 0) {
if (!input.affectedModules.some(module => affectedModules.includes(module))) {
continue;
}
}
// Get comment count
const commentResult = await context.db.get('SELECT COUNT(*) as count FROM issue_tracker_comments WHERE issue_id = ?', [row.id]);
issues.push({
id: row.id,
type: row.type,
title: row.title,
description: row.description,
status: row.status,
priority: row.priority,
createdBy: row.created_by,
assignedTo: row.assigned_to,
labels,
affectedModules,
relatedIssues: JSON.parse(row.related_issues || '[]'),
resolution: row.resolution,
commentCount: commentResult.data?.count || 0,
closedAt: row.closed_at ? new Date(row.closed_at).toISOString() : null,
createdAt: new Date(row.created_at).toISOString(),
updatedAt: new Date(row.updated_at).toISOString()
});
}
// Get total count for pagination
let countQuery = 'SELECT COUNT(*) as count FROM issue_tracker_issues WHERE project_id = ?';
const countParams = [context.projectId || 'default'];
if (input.type && input.type.length > 0) {
countQuery += ` AND type IN (${input.type.map(() => '?').join(',')})`;
countParams.push(...input.type);
}
if (input.status && input.status.length > 0) {
countQuery += ` AND status IN (${input.status.map(() => '?').join(',')})`;
countParams.push(...input.status);
}
if (input.priority && input.priority.length > 0) {
countQuery += ` AND priority IN (${input.priority.map(() => '?').join(',')})`;
countParams.push(...input.priority);
}
if (input.assignedTo) {
countQuery += ' AND assigned_to = ?';
countParams.push(input.assignedTo);
}
if (input.createdBy) {
countQuery += ' AND created_by = ?';
countParams.push(input.createdBy);
}
const countResult = await context.db.get(countQuery, countParams);
return createSuccessResult({
issues,
pagination: {
total: countResult.data?.count || 0,
limit: input.limit || 20,
offset: input.offset || 0,
hasMore: (input.offset || 0) + issues.length < (countResult.data?.count || 0)
}
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to list issues: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get a specific issue with all details
*/
const getIssueTool = createTool({
name: 'get_issue',
description: 'Get detailed information about a specific issue',
category: 'issue-tracking',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
issueId: {
type: 'string',
description: 'Issue ID to retrieve',
pattern: '^ISSUE-\\d{4}$'
}
},
required: ['issueId'],
additionalProperties: false
},
async execute(input, context) {
try {
// Get issue
const issueResult = await context.db.get('SELECT * FROM issue_tracker_issues WHERE id = ? AND project_id = ?', [input.issueId, context.projectId || 'default']);
if (!issueResult.success || !issueResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Issue not found',
category: 'validation'
});
}
const issue = issueResult.data;
// Get comments
const commentsResult = await context.db.query('SELECT * FROM issue_tracker_comments WHERE issue_id = ? ORDER BY created_at ASC', [input.issueId]);
const comments = (commentsResult.data || []).map((comment) => ({
id: comment.id,
issueId: comment.issue_id,
author: comment.author,
content: comment.content,
type: comment.type,
editedAt: comment.edited_at ? new Date(comment.edited_at).toISOString() : null,
createdAt: new Date(comment.created_at).toISOString()
}));
// Get attachments
const attachmentsResult = await context.db.query('SELECT * FROM issue_tracker_attachments WHERE issue_id = ? ORDER BY uploaded_at DESC', [input.issueId]);
const attachments = (attachmentsResult.data || []).map((attachment) => ({
id: attachment.id,
filename: attachment.filename,
path: attachment.path,
size: attachment.size,
mimeType: attachment.mime_type,
uploadedBy: attachment.uploaded_by,
uploadedAt: new Date(attachment.uploaded_at).toISOString()
}));
return createSuccessResult({
issue: {
id: issue.id,
type: issue.type,
title: issue.title,
description: issue.description,
status: issue.status,
priority: issue.priority,
createdBy: issue.created_by,
assignedTo: issue.assigned_to,
labels: JSON.parse(issue.labels || '[]'),
affectedModules: JSON.parse(issue.affected_modules || '[]'),
relatedIssues: JSON.parse(issue.related_issues || '[]'),
resolution: issue.resolution,
closedAt: issue.closed_at ? new Date(issue.closed_at).toISOString() : null,
metadata: JSON.parse(issue.metadata || '{}'),
createdAt: new Date(issue.created_at).toISOString(),
updatedAt: new Date(issue.updated_at).toISOString(),
comments,
attachments
}
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get issue: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Delete an issue
*/
const deleteIssueTool = createTool({
name: 'delete_issue',
description: 'Delete an issue and all its associated data',
category: 'issue-tracking',
inputSchema: {
type: 'object',
properties: {
issueId: {
type: 'string',
description: 'Issue ID to delete',
pattern: '^ISSUE-\\d{4}$'
}
},
required: ['issueId'],
additionalProperties: false
},
async execute(input, context) {
try {
// Check if issue exists
const issueResult = await context.db.get('SELECT * FROM issue_tracker_issues WHERE id = ? AND project_id = ?', [input.issueId, context.projectId || 'default']);
if (!issueResult.success || !issueResult.data) {
return createErrorResult({
code: 'RESOURCE_NOT_FOUND',
message: 'Issue not found',
category: 'validation'
});
}
// Delete issue (CASCADE will handle comments and attachments)
const deleteResult = await context.db.run('DELETE FROM issue_tracker_issues WHERE id = ? AND project_id = ?', [input.issueId, context.projectId || 'default']);
if (!deleteResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to delete issue',
category: 'system'
});
}
return createSuccessResult({
message: `Issue ${input.issueId} deleted successfully`,
deletedIssue: {
id: issueResult.data.id,
title: issueResult.data.title,
type: issueResult.data.type,
status: issueResult.data.status
}
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to delete issue: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Get issue metrics and statistics
*/
const issueMetricsTool = createTool({
name: 'issue_metrics',
description: 'Get issue metrics and statistics',
category: 'issue-tracking',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
dateRange: {
type: 'object',
properties: {
startDate: {
type: 'string',
format: 'date',
description: 'Start date (YYYY-MM-DD)'
},
endDate: {
type: 'string',
format: 'date',
description: 'End date (YYYY-MM-DD)'
}
}
},
groupBy: {
type: 'string',
enum: ['type', 'status', 'priority', 'assignee', 'module'],
description: 'Group metrics by field'
}
},
required: [],
additionalProperties: false
},
async execute(input, context) {
try {
let baseQuery = 'SELECT * FROM issue_tracker_issues WHERE project_id = ?';
const params = [context.projectId || 'default'];
// Apply date range filter
if (input.dateRange?.startDate) {
baseQuery += ' AND created_at >= ?';
params.push(new Date(input.dateRange.startDate).getTime());
}
if (input.dateRange?.endDate) {
baseQuery += ' AND created_at <= ?';
params.push(new Date(input.dateRange.endDate).getTime());
}
const result = await context.db.query(baseQuery, params);
if (!result.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to get metrics',
category: 'system'
});
}
const issues = result.data || [];
// Calculate basic metrics
const totalIssues = issues.length;
const openIssues = issues.filter((i) => ['open', 'in-progress'].includes(i.status)).length;
const closedIssues = issues.filter((i) => ['resolved', 'closed'].includes(i.status)).length;
const criticalIssues = issues.filter((i) => i.priority === 'critical').length;
// Group by requested field
let groupedMetrics = {};
if (input.groupBy) {
switch (input.groupBy) {
case 'type':
groupedMetrics = issues.reduce((acc, issue) => {
acc[issue.type] = (acc[issue.type] || 0) + 1;
return acc;
}, {});
break;
case 'status':
groupedMetrics = issues.reduce((acc, issue) => {
acc[issue.status] = (acc[issue.status] || 0) + 1;
return acc;
}, {});
break;
case 'priority':
groupedMetrics = issues.reduce((acc, issue) => {
acc[issue.priority] = (acc[issue.priority] || 0) + 1;
return acc;
}, {});
break;
case 'assignee':
groupedMetrics = issues.reduce((acc, issue) => {
const assignee = issue.assigned_to || 'unassigned';
acc[assignee] = (acc[assignee] || 0) + 1;
return acc;
}, {});
break;
case 'module':
for (const issue of issues) {
const modules = JSON.parse(issue.affected_modules || '[]');
for (const module of modules) {
groupedMetrics[module] = (groupedMetrics[module] || 0) + 1;
}
}
break;
}
}
// Calculate average resolution time for closed issues
const resolvedIssues = issues.filter((i) => i.closed_at && ['resolved', 'closed'].includes(i.status));
let avgResolutionTime = 0;
if (resolvedIssues.length > 0) {
const totalTime = resolvedIssues.reduce((sum, issue) => {
return sum + (issue.closed_at - issue.created_at);
}, 0);
avgResolutionTime = Math.round(totalTime / resolvedIssues.length / (1000 * 60 * 60)); // in hours
}
return createSuccessResult({
metrics: {
totalIssues,
openIssues,
closedIssues,
criticalIssues,
avgResolutionTimeHours: avgResolutionTime,
issuesByType: {
bug: issues.filter((i) => i.type === 'bug').length,
feature: issues.filter((i) => i.type === 'feature').length,
enhancement: issues.filter((i) => i.type === 'enhancement').length,
documentation: issues.filter((i) => i.type === 'documentation').length,
question: issues.filter((i) => i.type === 'question').length
},
issuesByPriority: {
critical: criticalIssues,
high: issues.filter((i) => i.priority === 'high').length,
medium: issues.filter((i) => i.priority === 'medium').length,
low: issues.filter((i) => i.priority === 'low').length
}
},
groupedMetrics: input.groupBy ? groupedMetrics : null,
dateRange: input.dateRange || null
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to get metrics: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Search issues by text
*/
const searchIssuesTool = createTool({
name: 'search_issues',
description: 'Search issues by text in title, description, and optionally comments',
category: 'issue-tracking',
readOnly: true,
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
minLength: 2,
maxLength: 100
},
includeComments: {
type: 'boolean',
description: 'Include comment content in search',
default: false
},
limit: {
type: 'integer',
description: 'Maximum number of results',
minimum: 1,
maximum: 50,
default: 20
}
},
required: ['query'],
additionalProperties: false
},
async execute(input, context) {
try {
const searchPattern = `%${input.query}%`;
// Search in issues
const issueQuery = `
SELECT DISTINCT i.* FROM issue_tracker_issues i
WHERE i.project_id = ?
AND (i.title LIKE ? OR i.description LIKE ? OR i.id LIKE ?)
ORDER BY i.created_at DESC
LIMIT ?
`;
const issueResult = await context.db.query(issueQuery, [context.projectId || 'default', searchPattern, searchPattern, searchPattern, input.limit || 20]);
if (!issueResult.success) {
return createErrorResult({
code: 'DATABASE_ERROR',
message: 'Failed to search issues',
category: 'system'
});
}
const issueIds = new Set((issueResult.data || []).map((i) => i.id));
// Search in comments if requested
if (input.includeComments) {
const commentQuery = `
SELECT DISTINCT i.* FROM issue_tracker_issues i
JOIN issue_tracker_comments c ON i.id = c.issue_id
WHERE i.project_id = ?
AND c.content LIKE ?
AND i.id NOT IN (${Array.from(issueIds).map(() => '?').join(',') || "''"})
ORDER BY i.created_at DESC
LIMIT ?
`;
const remainingLimit = (input.limit || 20) - issueIds.size;
if (remainingLimit > 0) {
const commentResult = await context.db.query(commentQuery, [context.projectId || 'default', searchPattern, ...Array.from(issueIds), remainingLimit]);
if (commentResult.success && commentResult.data) {
issueResult.data.push(...commentResult.data);
}
}
}
const issues = (issueResult.data || []).map((issue) => ({
id: issue.id,
type: issue.type,
title: issue.title,
description: issue.description.substring(0, 200) + (issue.description.length > 200 ? '...' : ''),
status: issue.status,
priority: issue.priority,
createdBy: issue.created_by,
assignedTo: issue.assigned_to,
labels: JSON.parse(issue.labels || '[]'),
affectedModules: JSON.parse(issue.affected_modules || '[]'),
createdAt: new Date(issue.created_at).toISOString(),
updatedAt: new Date(issue.updated_at).toISOString()
}));
return createSuccessResult({
issues,
query: input.query,
includeComments: input.includeComments || false,
resultCount: issues.length
});
}
catch (error) {
return createErrorResult({
code: 'EXECUTION_ERROR',
message: `Failed to search issues: ${error instanceof Error ? error.message : 'Unknown error'}`,
category: 'execution'
});
}
}
});
/**
* Setup issue tracking tools
*/
export async function setupIssueTrackingTools() {
return {
module: 'issue-tracking',
tools: [
createIssueTool,
updateIssueTool,
addCommentTool,
listIssuesTool,
getIssueTool,
deleteIssueTool,
issueMetricsTool,
searchIssuesTool
]
};
}
//# sourceMappingURL=tools.js.map