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
JavaScript
/******/ (() => { // 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}`;