UNPKG

ai-workflow-utils

Version:

A comprehensive automation platform that streamlines software development workflows by integrating AI-powered content generation with popular development tools like Jira, Bitbucket, and email systems. Includes startup service management for automatic syst

1,525 lines (1,396 loc) 414 kB
/******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 21: /***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => { /* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(938); /* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(axios__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _logger_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(179); /* harmony import */ var _utils_chat_config_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(937); /** * Ollama Service - Handles Ollama local LLM interactions * Follows service pattern with static methods for stateless operations */ class OllamaService { /** * Generate chat response using Ollama API * @param {Object} messageData - Formatted message data for Ollama * @returns {Promise<string>} AI response content */ static async generateResponse(messageData) { const config = ChatProviderConfig.getOllamaConfig(); logger.info(`Making Ollama API request to: ${config.baseUrl}/api/generate`); logger.info(`Using model: ${config.model}`); const requestPayload = { model: config.model, ...messageData }; try { const response = await axios.post(`${config.baseUrl}/api/generate`, requestPayload, { timeout: 120000 // 2 minute timeout for local models }); logger.info(`Ollama API response received`); return response.data?.response; } catch (error) { this._handleApiError(error); } } /** * Generate streaming chat response using Ollama API * @param {Object} messageData - Formatted message data for Ollama * @returns {Promise<Object>} Axios response stream */ static async generateStreamingResponse(messageData) { const config = ChatProviderConfig.getOllamaConfig(); const requestPayload = { model: config.model, ...messageData, stream: true }; try { const response = await axios.post(`${config.baseUrl}/api/generate`, requestPayload, { responseType: 'stream', timeout: 120000 }); return response; } catch (error) { this._handleApiError(error); } } /** * Check if Ollama service is available * @returns {Promise<boolean>} True if Ollama is accessible */ static async isAvailable() { try { const config = ChatProviderConfig.getOllamaConfig(); const response = await axios.get(`${config.baseUrl}/api/tags`, { timeout: 5000 }); return response.status === 200; } catch (error) { logger.warn(`Ollama service not available: ${error.message}`); return false; } } /** * Handle API errors with detailed logging and user-friendly messages * @private * @param {Error} error - Axios error object * @throws {Error} Processed error with user-friendly message */ static _handleApiError(error) { if (error.response) { logger.error(`Ollama API error - Status: ${error.response.status}`); logger.error(`Error data: ${JSON.stringify(error.response.data, null, 2)}`); throw new Error(`Ollama API Error (${error.response.status}): ${error.response.data?.error || error.message}`); } else if (error.request) { logger.error(`Ollama network error: ${error.message}`); throw new Error(`Network error: Unable to reach Ollama server`); } else { logger.error(`Ollama request setup error: ${error.message}`); throw new Error(`Ollama request error: ${error.message}`); } } } /* unused harmony default export */ var __WEBPACK_DEFAULT_EXPORT__ = ((/* unused pure expression or super */ null && (OllamaService))); /***/ }), /***/ 22: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ $0: () => (/* reexport safe */ _PRLangChainService_js__WEBPACK_IMPORTED_MODULE_2__.A), /* harmony export */ Zi: () => (/* reexport safe */ _JiraLangChainService_js__WEBPACK_IMPORTED_MODULE_1__.A), /* harmony export */ ts: () => (/* reexport safe */ _LangChainServiceFactory_js__WEBPACK_IMPORTED_MODULE_3__.A) /* harmony export */ }); /* harmony import */ var _BaseLangChainService_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(156); /* harmony import */ var _JiraLangChainService_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(584); /* harmony import */ var _PRLangChainService_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(350); /* harmony import */ var _LangChainServiceFactory_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(696); // Export all LangChain services // For backward compatibility, export the factory as the main service // Export individual service instances /***/ }), /***/ 33: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { // EXPORTS __webpack_require__.d(__webpack_exports__, { $F: () => (/* reexport */ jira_controller), RI: () => (/* reexport */ fetchJiraSummaries) }); // UNUSED EXPORTS: AttachmentProcessor, CustomFieldProcessor, ERROR_MESSAGES, EnvironmentConfig, ErrorHandler, FILE_UPLOAD, ISSUE_TYPES, ISSUE_TYPE_MAPPING, JIRA_ENDPOINTS, JiraApiService, JiraAttachment, JiraAttachmentService, JiraContentService, JiraIssue, JiraSummaryService, PRIORITY_LEVELS, SSE_HEADERS, ValidationUtils // EXTERNAL MODULE: external "axios" var external_axios_ = __webpack_require__(938); var external_axios_default = /*#__PURE__*/__webpack_require__.n(external_axios_); // EXTERNAL MODULE: ./server/logger.js + 1 modules var server_logger = __webpack_require__(179); ;// ./server/controllers/jira/utils/environment-config.js /** * Environment configuration management for Jira operations */ class EnvironmentConfig { /** * Get Jira configuration from environment * @returns {Object} Jira configuration */ static get() { const config = { jiraUrl: process.env.JIRA_URL, jiraToken: process.env.JIRA_TOKEN }; // Validate required configuration if (!config.jiraUrl || !config.jiraToken) { const missing = []; if (!config.jiraUrl) missing.push('JIRA_URL'); if (!config.jiraToken) missing.push('JIRA_TOKEN'); throw new Error(`Missing required environment variables: ${missing.join(', ')}`); } return config; } /** * Get Jira API base URL * @returns {string} Base URL */ static getBaseUrl() { const { jiraUrl } = this.get(); return jiraUrl.endsWith('/') ? jiraUrl.slice(0, -1) : jiraUrl; } /** * Get authentication headers for Jira API * @returns {Object} Headers object */ static getAuthHeaders() { const { jiraToken } = this.get(); return { 'Authorization': `Bearer ${jiraToken}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }; } /** * Get attachment upload headers * @returns {Object} Headers object */ static getAttachmentHeaders() { const { jiraToken } = this.get(); return { 'X-Atlassian-Token': 'no-check', 'Authorization': `Bearer ${jiraToken}` }; } /** * Validate environment configuration * @returns {boolean} True if valid */ static validate() { try { this.get(); return true; } catch (error) { server_logger/* default */.A.error(`Jira configuration validation failed: ${error.message}`); return false; } } } ;// ./server/controllers/jira/utils/constants.js /** * Jira-specific constants */ // Issue type mappings for AI template selection const ISSUE_TYPE_MAPPING = { "Bug": "JIRA_BUG", "Task": "JIRA_TASK", "Story": "JIRA_STORY" }; // Priority levels const PRIORITY_LEVELS = { HIGHEST: "Highest", HIGH: "High", MEDIUM: "Medium", LOW: "Low", LOWEST: "Lowest" }; // Issue types const ISSUE_TYPES = { BUG: "Bug", TASK: "Task", STORY: "Story", EPIC: "Epic", SUB_TASK: "Sub-task" }; // API endpoints const JIRA_ENDPOINTS = { ISSUE: "/rest/api/2/issue", SEARCH: "/rest/api/2/search", ATTACHMENTS: issueKey => `/rest/api/2/issue/${issueKey}/attachments` }; // File upload constants const FILE_UPLOAD = { MAX_SIZE: 10 * 1024 * 1024, // 10MB ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt', '.doc', '.docx', '.mp4', '.mov'], UPLOAD_DIR: 'uploads/' }; // Server-Sent Events constants const SSE_HEADERS = { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Cache-Control' }; // Error messages const ERROR_MESSAGES = { INVALID_PAYLOAD: "Invalid request payload", MISSING_FILE: "Missing file in request", MISSING_ISSUE_KEY: "Missing issueKey in request payload", MISSING_REQUIRED_FIELDS: "Missing required fields", CREATE_FAILED: "Failed to create Jira issue", UPLOAD_FAILED: "Failed to upload image to Jira", FETCH_FAILED: "Failed to fetch Jira issue", PREVIEW_FAILED: "Failed to generate issue preview" }; ;// ./server/controllers/jira/utils/error-handler.js /** * Error handling utilities for Jira operations */ class ErrorHandler { /** * Handle API errors and send appropriate response * @param {Error} error - The error object * @param {string} context - Context where error occurred * @param {Object} res - Express response object */ static handleApiError(error, context, res) { server_logger/* default */.A.error(`${context}: ${error.message}`, { stack: error.stack, context }); // Check if response has already been sent if (res.headersSent) { return; } let statusCode = 500; let message = "Internal server error"; let details = error.message; // Handle Axios errors if (error.response) { statusCode = error.response.status; message = error.response.data?.message || error.response.statusText; details = error.response.data; } else if (error.code === 'ECONNREFUSED') { statusCode = 503; message = "Service unavailable - unable to connect to Jira"; } else if (error.message.includes('Missing required')) { statusCode = 400; message = error.message; } res.status(statusCode).json({ success: false, error: message, details: false ? 0 : undefined }); } /** * Handle streaming errors (Server-Sent Events) * @param {Error} error - The error object * @param {string} context - Context where error occurred * @param {Object} res - Express response object */ static handleStreamingError(error, context, res) { server_logger/* default */.A.error(`${context}: ${error.message}`, { stack: error.stack, context }); if (!res.headersSent) { res.write(`data: ${JSON.stringify({ type: 'error', error: context, details: error.message })}\n\n`); } } /** * Validate required fields in request body * @param {Object} body - Request body * @param {Array<string>} requiredFields - Required field names * @throws {Error} If validation fails */ static validateRequiredFields(body, requiredFields) { const missing = requiredFields.filter(field => { const value = body[field]; return value === undefined || value === null || value === ''; }); if (missing.length > 0) { throw new Error(`${ERROR_MESSAGES.MISSING_REQUIRED_FIELDS}: ${missing.join(', ')}`); } } /** * Create validation error * @param {string} message - Error message * @returns {Error} Validation error */ static createValidationError(message) { const error = new Error(message); error.name = 'ValidationError'; error.statusCode = 400; return error; } /** * Create service error * @param {string} message - Error message * @param {number} statusCode - HTTP status code * @returns {Error} Service error */ static createServiceError(message, statusCode = 500) { const error = new Error(message); error.name = 'ServiceError'; error.statusCode = statusCode; return error; } } // EXTERNAL MODULE: external "path" var external_path_ = __webpack_require__(928); var external_path_default = /*#__PURE__*/__webpack_require__.n(external_path_); ;// ./server/controllers/jira/utils/validation-utils.js /** * Input validation utilities for Jira operations */ class ValidationUtils { /** * Validate issue creation data * @param {Object} data - Issue data * @returns {Object} Validation result */ static validateIssueData(data) { const errors = []; const { summary, description, issueType, priority, projectType } = data; // Required fields if (!summary || typeof summary !== 'string' || summary.trim().length === 0) { errors.push('Summary is required and must be a non-empty string'); } if (!description || typeof description !== 'string' || description.trim().length === 0) { errors.push('Description is required and must be a non-empty string'); } if (!issueType || !Object.values(ISSUE_TYPES).includes(issueType)) { errors.push(`Issue type must be one of: ${Object.values(ISSUE_TYPES).join(', ')}`); } if (!projectType || typeof projectType !== 'string' || projectType.trim().length === 0) { errors.push('Project type is required and must be a non-empty string'); } // Optional but validated fields if (priority && !Object.values(PRIORITY_LEVELS).includes(priority)) { errors.push(`Priority must be one of: ${Object.values(PRIORITY_LEVELS).join(', ')}`); } return { isValid: errors.length === 0, errors }; } /** * Validate file upload data * @param {Object} file - Multer file object * @param {string} issueKey - Jira issue key * @returns {Object} Validation result */ static validateFileUpload(file, issueKey) { const errors = []; if (!file) { errors.push('File is required'); } else { // Check file size if (file.size > FILE_UPLOAD.MAX_SIZE) { errors.push(`File size must be less than ${FILE_UPLOAD.MAX_SIZE / (1024 * 1024)}MB`); } // Check file extension const ext = external_path_default().extname(file.originalname).toLowerCase(); if (!FILE_UPLOAD.ALLOWED_EXTENSIONS.includes(ext)) { errors.push(`File extension ${ext} is not allowed. Allowed: ${FILE_UPLOAD.ALLOWED_EXTENSIONS.join(', ')}`); } } if (!issueKey || typeof issueKey !== 'string' || !/^[A-Z]+-\d+$/.test(issueKey)) { errors.push('Valid issue key is required (format: PROJECT-123)'); } return { isValid: errors.length === 0, errors }; } /** * Validate custom fields array * @param {Array} customFields - Custom fields array * @returns {Object} Validation result */ static validateCustomFields(customFields) { const errors = []; if (customFields && !Array.isArray(customFields)) { errors.push('Custom fields must be an array'); return { isValid: false, errors }; } if (customFields) { customFields.forEach((field, index) => { if (!field.key || typeof field.key !== 'string') { errors.push(`Custom field at index ${index} must have a valid key`); } if (field.value === undefined || field.value === null) { errors.push(`Custom field at index ${index} must have a value`); } }); } return { isValid: errors.length === 0, errors }; } /** * Validate preview request data * @param {Object} data - Preview request data * @returns {Object} Validation result */ static validatePreviewData(data) { const errors = []; const { prompt, images, issueType } = data; if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { errors.push('Prompt is required and must be a non-empty string'); } if (images && !Array.isArray(images)) { errors.push('Images must be an array'); } if (issueType && !Object.values(ISSUE_TYPES).includes(issueType)) { errors.push(`Issue type must be one of: ${Object.values(ISSUE_TYPES).join(', ')}`); } return { isValid: errors.length === 0, errors }; } /** * Validate issue keys array * @param {Array} issueKeys - Array of issue keys * @returns {Object} Validation result */ static validateIssueKeys(issueKeys) { const errors = []; if (!Array.isArray(issueKeys)) { errors.push('Issue keys must be an array'); return { isValid: false, errors }; } if (issueKeys.length === 0) { errors.push('At least one issue key is required'); return { isValid: false, errors }; } const invalidKeys = issueKeys.filter(key => !key || typeof key !== 'string' || !/^[A-Z]+-\d+$/.test(key)); if (invalidKeys.length > 0) { errors.push(`Invalid issue key format: ${invalidKeys.join(', ')}. Expected format: PROJECT-123`); } return { isValid: errors.length === 0, errors }; } } ;// ./server/controllers/jira/models/jira-issue.js /** * Jira Issue data model and validation */ class JiraIssue { constructor(data) { this.summary = data.summary; this.description = data.description; this.issueType = data.issueType; this.priority = data.priority || PRIORITY_LEVELS.MEDIUM; this.projectType = data.projectType; this.customFields = data.customFields || []; } /** * Validate issue data * @param {Object} data - Raw issue data * @throws {Error} If validation fails */ static validate(data) { const validation = ValidationUtils.validateIssueData(data); if (!validation.isValid) { throw new Error(`Validation failed: ${validation.errors.join(', ')}`); } // Validate custom fields if provided if (data.customFields) { const customFieldValidation = ValidationUtils.validateCustomFields(data.customFields); if (!customFieldValidation.isValid) { throw new Error(`Custom field validation failed: ${customFieldValidation.errors.join(', ')}`); } } } /** * Create JiraIssue instance from request data * @param {Object} data - Request data * @returns {JiraIssue} JiraIssue instance */ static fromRequest(data) { this.validate(data); return new JiraIssue(data); } /** * Convert to Jira API payload format * @returns {Object} Jira API payload */ toJiraPayload() { // Process custom fields into Jira format const processedCustomFields = this.processCustomFields(); return { fields: { project: { key: this.projectType }, summary: this.summary, description: this.description, issuetype: { name: this.issueType }, priority: { name: this.priority }, ...processedCustomFields } }; } /** * Process custom fields for Jira API * @returns {Object} Processed custom fields */ processCustomFields() { const processedFields = {}; if (this.customFields && Array.isArray(this.customFields)) { this.customFields.forEach(field => { if (field.key && field.value !== undefined && field.value !== null) { const val = field.value; // Check if the value is a string that looks like JSON const isLikelyJson = typeof val === 'string' && val.trim().startsWith('{') && val.trim().endsWith('}') || val.trim().startsWith('[') && val.trim().endsWith(']'); if (isLikelyJson) { try { // Sanitize object keys like { id: "007" } const sanitized = val.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":'); processedFields[field.key] = JSON.parse(sanitized); } catch (parseError) { // Fallback to string if JSON parsing fails console.warn(`Failed to parse JSON for field ${field.key}:`, parseError.message); processedFields[field.key] = val; } } else { processedFields[field.key] = val; } } }); } return processedFields; } /** * Get AI template type for this issue * @returns {string} Template type */ getTemplateType() { return ISSUE_TYPE_MAPPING[this.issueType] || ISSUE_TYPE_MAPPING.Task; } /** * Convert to plain object * @returns {Object} Plain object representation */ toObject() { return { summary: this.summary, description: this.description, issueType: this.issueType, priority: this.priority, projectType: this.projectType, customFields: this.customFields }; } /** * Get display-friendly representation * @returns {Object} Display object */ toDisplay() { return { summary: this.summary, issueType: this.issueType, priority: this.priority, project: this.projectType, customFieldsCount: this.customFields.length }; } } ;// ./server/controllers/jira/services/jira-api-service.js /** * Jira API service for external API interactions */ class JiraApiService { /** * Create a Jira issue * @param {Object} payload - Jira issue payload * @returns {Promise<Object>} Created issue data */ static async createIssue(payload) { try { const baseUrl = EnvironmentConfig.getBaseUrl(); const headers = EnvironmentConfig.getAuthHeaders(); const url = `${baseUrl}${JIRA_ENDPOINTS.ISSUE}`; server_logger/* default */.A.info('Creating Jira issue', { url }); const jiraPayload = new JiraIssue(payload); const response = await external_axios_default().post(url, jiraPayload.toJiraPayload(), { headers }); server_logger/* default */.A.info('Jira issue created successfully', { issueKey: response.data.key, issueId: response.data.id }); return response?.data; } catch (error) { server_logger/* default */.A.error('Failed to create Jira issue', { error: error.message, status: error.response?.status, data: error.response?.data }); throw ErrorHandler.createServiceError(`Failed to create Jira issue: ${error.response?.data?.errorMessages?.join(', ') || error.message}`, error.response?.status || 500); } } /** * Fetch a single Jira issue * @param {string} issueId - Issue ID or key * @returns {Promise<Object>} Issue data */ static async fetchIssue(issueId) { try { const baseUrl = EnvironmentConfig.getBaseUrl(); const headers = EnvironmentConfig.getAuthHeaders(); const url = `${baseUrl}${JIRA_ENDPOINTS.ISSUE}/${issueId}`; server_logger/* default */.A.info('Fetching Jira issue', { issueId, url }); const response = await external_axios_default().get(url, { headers }); server_logger/* default */.A.info('Jira issue fetched successfully', { issueKey: response.data.key, summary: response.data.fields?.summary }); return response.data; } catch (error) { server_logger/* default */.A.error('Failed to fetch Jira issue', { issueId, error: error.message, status: error.response?.status }); throw ErrorHandler.createServiceError(`Failed to fetch Jira issue: ${error.message}`, error.response?.status || 500); } } /** * Upload attachment to Jira issue * @param {string} issueKey - Issue key * @param {Object} formData - FormData with file * @returns {Promise<Object>} Upload response */ static async uploadAttachment(issueKey, formData) { try { const baseUrl = EnvironmentConfig.getBaseUrl(); const attachmentHeaders = EnvironmentConfig.getAttachmentHeaders(); const url = `${baseUrl}${JIRA_ENDPOINTS.ATTACHMENTS(issueKey)}`; // Merge form data headers with authentication headers const headers = { ...attachmentHeaders, ...formData.getHeaders() }; server_logger/* default */.A.info('Uploading attachment to Jira issue', { issueKey, url }); const response = await external_axios_default().post(url, formData, { headers }); server_logger/* default */.A.info('Attachment uploaded successfully', { issueKey, attachmentCount: response.data?.length || 0 }); return response.data; } catch (error) { server_logger/* default */.A.error('Failed to upload attachment', { issueKey, error: error.message, status: error.response?.status }); throw ErrorHandler.createServiceError(`Failed to upload attachment: ${error.message}`, error.response?.status || 500); } } /** * Search for Jira issues using JQL * @param {string} jql - JQL query string * @param {Array<string>} fields - Fields to include in response * @param {number} maxResults - Maximum number of results * @returns {Promise<Object>} Search results */ static async searchIssues(jql, fields = ['summary'], maxResults = 50) { try { const baseUrl = EnvironmentConfig.getBaseUrl(); const headers = EnvironmentConfig.getAuthHeaders(); const params = new URLSearchParams({ jql, fields: fields.join(','), maxResults: maxResults.toString() }); const url = `${baseUrl}${JIRA_ENDPOINTS.SEARCH}?${params}`; server_logger/* default */.A.info('Searching Jira issues', { jql, fields, maxResults }); const response = await external_axios_default().get(url, { headers }); server_logger/* default */.A.info('Jira search completed', { totalResults: response.data.total, returnedResults: response.data.issues?.length || 0 }); return response.data; } catch (error) { server_logger/* default */.A.error('Failed to search Jira issues', { jql, error: error.message, status: error.response?.status }); throw ErrorHandler.createServiceError(`Failed to search Jira issues: ${error.message}`, error.response?.status || 500); } } /** * Fetch summaries for multiple issues * @param {Array<string>} issueKeys - Array of issue keys * @returns {Promise<Object>} Map of issue key to summary */ static async fetchIssueSummaries(issueKeys) { try { if (!issueKeys || issueKeys.length === 0) { return {}; } const jql = `issueKey in (${issueKeys.join(',')})`; const searchResult = await this.searchIssues(jql, ['summary'], issueKeys.length); const summariesMap = {}; if (searchResult.issues) { searchResult.issues.forEach(issue => { summariesMap[issue.key] = issue.fields.summary; }); } server_logger/* default */.A.info('Fetched issue summaries', { requestedCount: issueKeys.length, foundCount: Object.keys(summariesMap).length }); return summariesMap; } catch (error) { server_logger/* default */.A.error('Failed to fetch issue summaries', { issueKeys, error: error.message }); // Return empty object instead of throwing, to gracefully handle failures return {}; } } /** * Test Jira connection * @returns {Promise<boolean>} True if connection successful */ static async testConnection() { try { const baseUrl = EnvironmentConfig.getBaseUrl(); const headers = EnvironmentConfig.getAuthHeaders(); const url = `${baseUrl}/rest/api/2/myself`; await external_axios_default().get(url, { headers }); return true; } catch (error) { server_logger/* default */.A.error('Jira connection test failed', { error: error.message }); return false; } } } ;// ./server/controllers/jira/services/jira-summary-service.js /** * Jira summary service for summary fetching operations */ class JiraSummaryService { /** * Fetch summaries for multiple issue keys (alias for fetchSummaries) * @param {Array<string>} issueKeys - Array of issue keys * @returns {Promise<Object>} Map of issue key to summary */ static async fetchJiraSummaries(issueKeys) { return this.fetchSummaries(issueKeys); } /** * Fetch summaries for multiple issue keys * @param {Array<string>} issueKeys - Array of issue keys * @returns {Promise<Object>} Map of issue key to summary */ static async fetchSummaries(issueKeys) { try { // Validate input const validation = ValidationUtils.validateIssueKeys(issueKeys); if (!validation.isValid) { throw ErrorHandler.createValidationError(validation.errors.join(', ')); } // Remove duplicates and filter empty values const uniqueKeys = [...new Set(issueKeys.filter(Boolean))]; if (uniqueKeys.length === 0) { return {}; } server_logger/* default */.A.info('Fetching Jira summaries', { requestedKeys: issueKeys.length, uniqueKeys: uniqueKeys.length, keys: uniqueKeys }); // Fetch summaries from Jira API const summariesMap = await JiraApiService.fetchIssueSummaries(uniqueKeys); server_logger/* default */.A.info('Jira summaries fetched successfully', { requestedCount: uniqueKeys.length, foundCount: Object.keys(summariesMap).length, missingKeys: uniqueKeys.filter(key => !summariesMap[key]) }); return summariesMap; } catch (error) { server_logger/* default */.A.error('Error fetching Jira summaries', { issueKeys, error: error.message }); // Return empty object for graceful degradation return {}; } } /** * Merge Jira summaries with table data * @param {Array<Array>} tableData - 2D array representing table data * @returns {Promise<Array<Array>>} Updated table data with summaries */ static async fetchAndMergeJiraSummary(tableData) { try { // Validate table data structure if (!Array.isArray(tableData) || tableData.length < 2) { throw ErrorHandler.createValidationError("Invalid table data"); } const headers = tableData[0]; const jiraKeyIndex = headers.indexOf('Jira URL'); if (jiraKeyIndex === -1) { throw ErrorHandler.createValidationError("Missing 'Jira URL' column"); } server_logger/* default */.A.info('Processing table data for Jira summaries', { rowCount: tableData.length - 1, columnCount: headers.length, jiraUrlColumnIndex: jiraKeyIndex }); // Add 'Summary' column if not already present if (!headers.includes('Summary')) { headers.push('Summary'); } // Extract Jira keys from table data const issueKeys = tableData.slice(1) // Skip header row .map(row => this.extractIssueKeyFromUrl(row[jiraKeyIndex])).filter(Boolean); // Remove empty values server_logger/* default */.A.info('Extracted issue keys from table', { extractedKeys: issueKeys.length, uniqueKeys: [...new Set(issueKeys)].length }); // Fetch summaries const summariesMap = await this.fetchSummaries(issueKeys); // Merge summaries into table data const summaryColumnIndex = headers.length - 1; for (let i = 1; i < tableData.length; i++) { const row = tableData[i]; const jiraUrl = row[jiraKeyIndex]; const issueKey = this.extractIssueKeyFromUrl(jiraUrl); // Set summary or empty string if not found row[summaryColumnIndex] = summariesMap[issueKey] || ''; } server_logger/* default */.A.info('Successfully merged Jira summaries with table data', { processedRows: tableData.length - 1, summariesFound: Object.keys(summariesMap).length }); return tableData; } catch (error) { server_logger/* default */.A.error('Error merging Jira summaries with table data', { error: error.message, tableDataLength: tableData?.length || 0 }); throw error; } } /** * Extract issue key from Jira URL * @param {string} jiraUrl - Jira URL or issue key * @returns {string|null} Extracted issue key or null */ static extractIssueKeyFromUrl(jiraUrl) { if (!jiraUrl || typeof jiraUrl !== 'string') { return null; } // If it's already an issue key (PROJECT-123 format) const directKeyMatch = jiraUrl.match(/^[A-Z]+-\d+$/); if (directKeyMatch) { return jiraUrl; } // Extract from URL patterns const urlPatterns = [/\/browse\/([A-Z]+-\d+)/, // Standard browse URL /\/issues\/([A-Z]+-\d+)/, // Issues URL /\/([A-Z]+-\d+)(?:\?|$)/, // Issue key at end of path /([A-Z]+-\d+)/ // Fallback: any issue key pattern ]; for (const pattern of urlPatterns) { const match = jiraUrl.match(pattern); if (match && match[1]) { return match[1]; } } server_logger/* default */.A.warn('Could not extract issue key from URL', { jiraUrl }); return null; } /** * Get summaries for a specific project * @param {string} projectKey - Project key * @param {number} limit - Maximum number of issues to retrieve * @returns {Promise<Object>} Map of issue key to summary */ static async getProjectSummaries(projectKey, limit = 100) { try { if (!projectKey || typeof projectKey !== 'string') { throw ErrorHandler.createValidationError('Project key is required'); } const jql = `project = ${projectKey} ORDER BY created DESC`; const searchResult = await JiraApiService.searchIssues(jql, ['summary'], limit); const summariesMap = {}; if (searchResult.issues) { searchResult.issues.forEach(issue => { summariesMap[issue.key] = issue.fields.summary; }); } server_logger/* default */.A.info('Fetched project summaries', { projectKey, totalFound: Object.keys(summariesMap).length, limit }); return summariesMap; } catch (error) { server_logger/* default */.A.error('Error fetching project summaries', { projectKey, error: error.message }); return {}; } } /** * Search issues by summary text * @param {string} searchText - Text to search in summaries * @param {string} projectKey - Optional project key to limit search * @param {number} limit - Maximum number of results * @returns {Promise<Array>} Array of issues with summaries */ static async searchBySummary(searchText, projectKey = null, limit = 50) { try { if (!searchText || typeof searchText !== 'string') { throw ErrorHandler.createValidationError('Search text is required'); } let jql = `summary ~ "${searchText}"`; if (projectKey) { jql = `project = ${projectKey} AND ${jql}`; } jql += ' ORDER BY updated DESC'; const searchResult = await JiraApiService.searchIssues(jql, ['summary', 'status', 'assignee'], limit); const issues = searchResult.issues?.map(issue => ({ key: issue.key, summary: issue.fields.summary, status: issue.fields.status?.name, assignee: issue.fields.assignee?.displayName })) || []; server_logger/* default */.A.info('Search by summary completed', { searchText, projectKey, resultsFound: issues.length }); return issues; } catch (error) { server_logger/* default */.A.error('Error searching by summary', { searchText, projectKey, error: error.message }); return []; } } /** * Get summary statistics for a project * @param {string} projectKey - Project key * @returns {Promise<Object>} Summary statistics */ static async getSummaryStats(projectKey) { try { const summaries = await this.getProjectSummaries(projectKey, 1000); const summaryTexts = Object.values(summaries); const stats = { totalIssues: summaryTexts.length, averageLength: summaryTexts.length > 0 ? Math.round(summaryTexts.reduce((sum, text) => sum + text.length, 0) / summaryTexts.length) : 0, longestSummary: Math.max(...summaryTexts.map(text => text.length), 0), shortestSummary: summaryTexts.length > 0 ? Math.min(...summaryTexts.map(text => text.length)) : 0, commonWords: this.getCommonWords(summaryTexts) }; server_logger/* default */.A.info('Generated summary statistics', { projectKey, ...stats }); return stats; } catch (error) { server_logger/* default */.A.error('Error generating summary statistics', { projectKey, error: error.message }); return { totalIssues: 0, averageLength: 0, longestSummary: 0, shortestSummary: 0, commonWords: [] }; } } /** * Extract common words from summaries * @param {Array<string>} summaries - Array of summary texts * @returns {Array} Top 10 common words */ static getCommonWords(summaries) { const wordCounts = {}; const stopWords = new Set(['the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'a', 'an']); summaries.forEach(summary => { const words = summary.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).filter(word => word.length > 2 && !stopWords.has(word)); words.forEach(word => { wordCounts[word] = (wordCounts[word] || 0) + 1; }); }); return Object.entries(wordCounts).sort(([, a], [, b]) => b - a).slice(0, 10).map(([word, count]) => ({ word, count })); } } // EXTERNAL MODULE: ./server/services/langchain/index.js var langchain = __webpack_require__(22); ;// ./server/controllers/jira/services/jira-content-service.js /** * Jira content service for AI-powered content generation */ class JiraContentService { /** * Generate AI-powered issue preview with streaming * @param {Object} data - Preview request data * @param {Array} images - Image data array * @param {Object} res - Express response object for streaming */ static async streamPreviewContent(data, images, res) { try { // Validate input data const validation = ValidationUtils.validatePreviewData(data); if (!validation.isValid) { throw ErrorHandler.createValidationError(validation.errors.join(', ')); } const { prompt, issueType = 'Task' } = data; // Get template type for AI service const templateType = ISSUE_TYPE_MAPPING[issueType] || ISSUE_TYPE_MAPPING.Task; server_logger/* default */.A.info('Generating AI preview for Jira issue', { issueType, templateType, hasImages: images && images.length > 0, promptLength: prompt.length }); // Set up Server-Sent Events headers res.writeHead(200, SSE_HEADERS); // Use the specialized Jira LangChain service for streaming await langchain/* jiraLangChainService */.Zi.streamContent({ prompt }, images || [], templateType, res); server_logger/* default */.A.info('AI preview generation completed', { issueType, templateType }); } catch (error) { server_logger/* default */.A.error('Error generating AI preview', { error: error.message, issueType: data?.issueType, stack: error.stack }); // Handle streaming error ErrorHandler.handleStreamingError(error, `Failed to generate ${data?.issueType || 'issue'} preview`, res); } } /** * Generate issue content without streaming (for internal use) * @param {Object} data - Content generation data * @param {Array} images - Image data array * @returns {Promise<string>} Generated content */ static async generateContent(data, images = []) { try { const validation = ValidationUtils.validatePreviewData(data); if (!validation.isValid) { throw ErrorHandler.createValidationError(validation.errors.join(', ')); } const { prompt, issueType = 'Task' } = data; const templateType = ISSUE_TYPE_MAPPING[issueType] || ISSUE_TYPE_MAPPING.Task; server_logger/* default */.A.info('Generating AI content for Jira issue', { issueType, templateType, hasImages: images.length > 0 }); // Generate content using LangChain service const content = await langchain/* jiraLangChainService */.Zi.generateContent({ prompt }, images, templateType); server_logger/* default */.A.info('AI content generation completed', { issueType, contentLength: content?.length || 0 }); return content; } catch (error) { server_logger/* default */.A.error('Error generating AI content', { error: error.message, issueType: data?.issueType }); throw ErrorHandler.createServiceError(`Failed to generate content: ${error.message}`); } } /** * Enhance issue description with AI * @param {string} description - Original description * @param {string} issueType - Issue type * @returns {Promise<string>} Enhanced description */ static async enhanceDescription(description, issueType = 'Task') { try { if (!description || typeof description !== 'string') { throw ErrorHandler.createValidationError('Description is required'); } const enhancementPrompt = `Please enhance and improve the following ${issueType.toLowerCase()} description while maintaining its core meaning and requirements:\n\n${description}`; const enhancedContent = await this.generateContent({ prompt: enhancementPrompt, issueType }); server_logger/* default */.A.info('Description enhanced successfully', { originalLength: description.length, enhancedLength: enhancedContent?.length || 0, issueType }); return enhancedContent || description; // Fallback to original if enhancement fails } catch (error) { server_logger/* default */.A.error('Error enhancing description', { error: error.message, issueType }); // Return original description on error return description; } } /** * Generate issue summary from description * @param {string} description - Issue description * @param {string} issueType - Issue type * @returns {Promise<string>} Generated summary */ static async generateSummary(description, issueType = 'Task') { try { if (!description || typeof description !== 'string') { throw ErrorHandler.createValidationError('Description is required'); } const summaryPrompt = `Generate a concise, clear summary (max 100 characters) for this ${issueType.toLowerCase()}:\n\n${description}`; const summary = await this.generateContent({ prompt: summaryPrompt, issueType }); // Ensure summary is not too long const cleanSummary = summary?.trim().substring(0, 100) || `${issueType} - Auto-generated`; server_logger/* default */.A.info('Summary generated successfully', { summaryLength: cleanSummary.length, issueType }); return cleanSummary; } catch (error) { server_logger/* default */.A.error('Error generating summary', { error: error.message, issueType }); // Return fallback summary return `${issueType} - Auto-generated`; } } /** * Generate acceptance criteria for stories * @param {string} description - Story description * @returns {Promise<string>} Generated acceptance criteria */ static async generateAcceptanceCriteria(description) { try { if (!description || typeof description !== 'string') { throw ErrorHandler.createValidationError('Description is required'); } const criteriaPrompt = `Generate clear, testable acceptance criteria for this user story:\n\n${description}`; const criteria = await this.generateContent({ prompt: criteriaPrompt, issueType: 'Story' }); server_logger/* default */.A.info('Acceptance criteria generated successfully', { criteriaLength: criteria?.length || 0 }); return criteria || 'Acceptance criteria to be defined'; } catch (error) { server_logger/* default */.A.error('Error generating acceptance criteria', { error: error.message }); return 'Acceptance criteria to be defined'; } } /** * Get available issue types for AI generation * @returns {Object} Available issue types with descriptions */ static getAvailableIssueTypes() { return { Bug: { description: 'A problem or defect in the software', templateType: ISSUE_TYPE_MAPPING.Bug, aiCapabilities: ['Bug report generation', 'Steps to reproduce', 'Expected vs actual behavior'] }, Task: { description: 'A unit of work to be completed', templateType: ISSUE_TYPE_MAPPING.Task, aiCapabilities: ['Task breakdown', 'Implementation details', 'Checklist generation'] }, Story: { description: 'A user story representing a feature or requirement', templateType: ISSUE_TYPE_MAPPING.Story, aiCapabilities: ['User story formatting', 'Acceptance criteria', 'Use case scenarios'] } }; } /** * Generate AI comment reply * @param {string} comment - Original comment to reply to * @param {string} context - Additional context about the issue * @param {string} tone - Tone for the reply (professional, friendly, technical) * @returns {Promise<string>} Generated reply */ static async generateCommentReply(comment, context = '', tone = 'professional') { try { if (!comment || typeof comment !== 'string') { throw ErrorHandler.createValidationError('Comment is required'); } const toneInstructions = { professional: 'professional and business-appropriate', friendly: 'friendly and approachable while remaining professional', technical: 'technical and detailed with specific implementation guidance' }; const toneInstruction = toneInstructions[tone] || toneInstructions.professional; const replyPrompt = `Generate a ${toneInstruction} reply to this Jira comment. ${context ? `Context: ${context}` : ''}\n\nOriginal comment:\n${comment}\n\nReply:`; const reply = await this.generateContent({ prompt: replyPrompt, issueType: 'Task' }); server_logger/* default */.A.info('Comment reply generated successfully', { originalLength: comment.length, replyLength: reply?.length || 0, tone }); return reply || 'Thank you for your comment. I will review and follow up accordingly.'; } catch (error) { server_logger/* default */.A.error('Error generating comment reply', { error: error.message, tone }); return 'Thank you for your comment. I will review and follow up accordingly.'; } } /** * Format comment using AI for better readability * @param {string} comment - Comment to format * @param {string} format - Target format (jira, markdown, plain) * @returns {Promise<string>} Formatted comment */ static async formatComment(comment, format = 'jira') { try { if (!comment || typeof comment !== 'string') { throw ErrorHandler.createValidationError('Comment is required'); } const formatInstructions = { jira: 'Format this comment using Jira markup syntax for better readability. Use *bold*, _italic_, {{monospace}}, bullet points, numbered lists, and proper line breaks where appropriate.', markdown: 'Format this comment using proper Markdown syntax with **bold**, *italic*, `code`, bullet points, numbered lists, and appropriate headings.', plain: 'Format this comment as plain text with proper paragraph breaks and clear structure.' }; const formatInstruction = formatInstructions[format] || formatInstructions.jira; const formatPrompt = `${formatInstruction}\n\nOriginal comment:\n${comment}\n\nFormatted comment:`; const formatted = await this.generateContent({ prompt: formatPrompt, issueType: 'Task' }); server_logger/* default */.A.info('Comment formatted successfully', { originalLength: comment.length, formattedLength: formatted?.length || 0, format }); return formatted || comment; // Fallback to original if formatting fails } catch (error) { server_logger/* default */.A.error('Error formatting comment', { error: error.message, format }); return comment; // Return original comment on error } } /** * Analyze comment sentiment and suggest improvements * @param {string} comment - Comment to analyze * @returns {Promise<Object>} Analysis result with sentiment and suggestions */ static async analyzeCommentSentiment(comment) { try { if (!comment || typeof comment !== 'string') { throw ErrorHandler.createValidationError('Comment is required'); } const analysisPrompt = `Analyze the sentiment and tone of this Jira comment and provide suggestions for improvement if needed. Return your analysis in JSON format with fields: sentiment (positive/neutral/negative), tone (professional/casual/aggressive), suggestions (array), and improved_version (string).\n\nComment:\n${comment}`;