@mspcopilot/n8n-nodes-connectwise 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,7 +117,7 @@ class ConnectWisePsaApi {
117
117
  name: "checkForUpdates",
118
118
  type: "boolean",
119
119
  default: !0,
120
- hint: "Current version: 0.2.3 (community edition)",
120
+ hint: "Current version: 0.2.4 (community edition)",
121
121
  description: "Check for new versions of this node package and show notifications when updates are available"
122
122
  } ];
123
123
  async authenticate(credentials, requestOptions) {
@@ -74,7 +74,7 @@ const resources = {
74
74
 
75
75
  class ConnectWisePsa {
76
76
  constructor() {
77
- console.log(`🔌 ConnectWise PSA Node Loaded | 📦 v0.2.3 | 🏗️ Build: ${"community".toUpperCase()} (${"prod".toUpperCase()} mode)`);
77
+ console.log(`🔌 ConnectWise PSA Node Loaded | 📦 v0.2.4 | 🏗️ Build: ${"community".toUpperCase()} (${"prod".toUpperCase()} mode)`);
78
78
  }
79
79
  description={
80
80
  displayName: "ConnectWise PSA",
@@ -31,6 +31,12 @@ __export(field_collector_exports, {
31
31
 
32
32
  module.exports = __toCommonJS(field_collector_exports);
33
33
 
34
+ function isEmptyFixedCollection(value) {
35
+ if (!value || typeof value != "object" || Array.isArray(value)) return !1;
36
+ const entries = Object.entries(value);
37
+ return entries.length === 0 ? !0 : entries.every(([, v]) => Array.isArray(v) && v.length === 0);
38
+ }
39
+
34
40
  function collectFieldData(context, schema, operation, index) {
35
41
  const schemaFields = {}, additionalFields = {}, assignments = {}, relevantProperties = [], isPropertyShown = property => {
36
42
  if (!property.displayOptions?.show?.operation) return !0;
@@ -40,7 +46,10 @@ function collectFieldData(context, schema, operation, index) {
40
46
  const fullName = prefix + property.name;
41
47
  try {
42
48
  const value = context.getNodeParameter(fullName, index, void 0);
43
- if (value != null && value !== "") if (property.type === "collection" && typeof value == "object") for (const [collectionField, collectionValue] of Object.entries(value)) collectionValue != null && collectionValue !== "" && (target[`${fullName}.${collectionField}`] = collectionValue); else target[fullName] = value;
49
+ if (value != null && value !== "") {
50
+ if (property.type === "fixedCollection" && isEmptyFixedCollection(value)) return;
51
+ if (property.type === "collection" && typeof value == "object") for (const [collectionField, collectionValue] of Object.entries(value)) collectionValue != null && collectionValue !== "" && (target[`${fullName}.${collectionField}`] = collectionValue); else target[fullName] = value;
52
+ }
44
53
  } catch {}
45
54
  }, UI_CONTROL_FIELDS = [ "operation", "assignments", "endpoint", "ticketType", "parentId", "id" ];
46
55
  for (const field of schema.fields) if (!UI_CONTROL_FIELDS.includes(field.name) && field.x_noSubmit !== !0 && isPropertyShown(field)) if (relevantProperties.push(field),
@@ -33,9 +33,416 @@ module.exports = __toCommonJS(alert_consolidation_exports);
33
33
 
34
34
  var import_n8n_workflow = require("n8n-workflow"), import_luxon = require("luxon"), import_crypto = require("crypto"), import_client = require("../../transport/client"), import_utils = require("../../helpers/utils");
35
35
 
36
- const executeSmartConsolidationStub = async function(itemIndex) {
37
- return (0, import_utils.returnProOnlyMessage)(this, itemIndex);
38
- }, executeSmartConsolidation = executeSmartConsolidationStub;
36
+ const FINGERPRINT = `\n\x3c!--msp-copilot--\x3e`, FINGERPRINT_FAILURE = `\n\x3c!--msp-copilot:failure--\x3e`, FINGERPRINT_SUCCESS = `\n\x3c!--msp-copilot:success--\x3e`;
37
+
38
+ function buildStatus(value) {
39
+ return typeof value == "number" ? {
40
+ id: value
41
+ } : {
42
+ name: value
43
+ };
44
+ }
45
+
46
+ async function resolvePriority(context, value) {
47
+ if (value == null || value === "") return null;
48
+ if (typeof value == "number") return {
49
+ id: value
50
+ };
51
+ try {
52
+ const priorities = await import_client.connectWiseApiRequest.call(context, "GET", "/service/priorities", {}, {
53
+ pageSize: 1e3,
54
+ fields: "id,name",
55
+ optionsCache: !0
56
+ }), normalizedName = value.trim().toLowerCase(), priority = priorities.find(p => p.name.toLowerCase() === normalizedName);
57
+ return priority ? {
58
+ id: priority.id
59
+ } : (console.warn(`⚠️ Priority "${value}" not found in API response`), null);
60
+ } catch (error) {
61
+ return console.error("Error fetching priorities:", error), null;
62
+ }
63
+ }
64
+
65
+ function trimSummary(summary) {
66
+ if (summary.length <= 100) return summary;
67
+ let trimmed = summary.substring(0, 97).trim();
68
+ return trimmed.endsWith(".") ? trimmed : trimmed + "...";
69
+ }
70
+
71
+ function formatTimestamp(date = /* @__PURE__ */ new Date) {
72
+ return `\`${import_luxon.DateTime.fromJSDate(date).toFormat("yyyy-MM-dd ccc hh:mm:ss a")}\``;
73
+ }
74
+
75
+ function formatMacForSearch(mac) {
76
+ if (!mac) return "";
77
+ if (mac.includes("*")) return mac.replace(/[:\-\.\s]/g, "");
78
+ const cleanMac = mac.replace(/[:\-\.\s]/g, "").toUpperCase();
79
+ return cleanMac.length < 6 ? cleanMac : (cleanMac.match(/.{1,2}/g) || []).join("%");
80
+ }
81
+
82
+ async function findConfiguration(companyId, configurationTypeId, macAddress, ipAddress, hostnameFull, hostnameSearch) {
83
+ try {
84
+ const orConditions = [];
85
+ if (macAddress) {
86
+ const macSearchPattern = formatMacForSearch(macAddress);
87
+ orConditions.push(`macAddress like "%${macSearchPattern}%"`);
88
+ }
89
+ if (ipAddress && orConditions.push(`ipAddress="${ipAddress}"`), hostnameFull) if (hostnameFull.includes("*")) {
90
+ const hostnamePattern = hostnameFull.replace(/\*/g, "%");
91
+ orConditions.push(`name like "${hostnamePattern}"`);
92
+ } else orConditions.push(`name="${hostnameFull}"`);
93
+ if (hostnameSearch && orConditions.push(`name="${hostnameSearch}"`, `name like "${hostnameSearch}.%"`),
94
+ orConditions.length === 0) return console.log("⚠️ No configuration search criteria provided"),
95
+ null;
96
+ const fullCondition = [ `company/id=${companyId}`, "activeFlag=true", configurationTypeId ? Array.isArray(configurationTypeId) ? configurationTypeId.length > 0 ? `type/id in (${configurationTypeId.join(",")})` : null : `type/id=${configurationTypeId}` : null, `(${orConditions.join(" or ")})` ].filter(Boolean).join(" and ");
97
+ console.log(`🔍 Searching configurations with condition: ${fullCondition}`);
98
+ const configs = await import_client.connectWiseApiRequest.call(this, "GET", "/company/configurations", {}, {
99
+ conditions: fullCondition,
100
+ pageSize: 100,
101
+ orderBy: "id desc",
102
+ fields: "id,name,ipAddress,macAddress,serialNumber,modelNumber,tagNumber,notes"
103
+ });
104
+ if (!configs || configs.length === 0) return console.log("⚠️ No configuration found matching the provided identifiers"),
105
+ null;
106
+ if (console.log(`📊 Found ${configs.length} matching configuration(s), applying priority filter...`),
107
+ macAddress) {
108
+ const macPattern = formatMacForSearch(macAddress).toLowerCase();
109
+ for (const config of configs) if (config.macAddress) {
110
+ const configMac = config.macAddress.replace(/[:\-\.\s]/g, "").toLowerCase();
111
+ if (new RegExp(macPattern.replace(/\*/g, ".*"), "i").test(configMac)) return console.log(`✅ Configuration found by MAC address: ${config.name} (MAC: ${config.macAddress})`),
112
+ config;
113
+ }
114
+ }
115
+ if (ipAddress) {
116
+ if (ipAddress === "127.0.0.1") console.log("❗ Ignoring IP address 127.0.0.1 (localhost)"); else for (const config of configs) if (config.ipAddress === ipAddress) return console.log(`✅ Configuration found by IP address: ${config.name} (IP: ${config.ipAddress})`),
117
+ config;
118
+ }
119
+ if (hostnameFull && !hostnameFull.includes("*")) {
120
+ for (const config of configs) if (config.name && config.name.toLowerCase() === hostnameFull.toLowerCase()) return console.log(`✅ Configuration found by exact hostname: ${config.name}`),
121
+ config;
122
+ }
123
+ if (hostnameFull && hostnameFull.includes("*")) {
124
+ const pattern = new RegExp("^" + hostnameFull.replace(/\*/g, ".*") + "$", "i");
125
+ for (const config of configs) if (config.name && pattern.test(config.name)) return console.log(`✅ Configuration found by hostname pattern: ${config.name}`),
126
+ config;
127
+ }
128
+ if (hostnameSearch) {
129
+ for (const config of configs) if (config.name && config.name.toLowerCase() === hostnameSearch.toLowerCase()) return console.log(`✅ Configuration found by smart hostname (exact): ${config.name}`),
130
+ config;
131
+ for (const config of configs) if (config.name && config.name.toLowerCase().startsWith(hostnameSearch.toLowerCase())) return console.log(`✅ Configuration found by smart hostname (prefix): ${config.name}`),
132
+ config;
133
+ }
134
+ console.log("⚠️ No configurations matched priority filters. Found configurations:");
135
+ for (const config of configs) console.log(` - ${config.name} (ID: ${config.id}, IP: ${config.ipAddress || "N/A"}, MAC: ${config.macAddress || "N/A"})`);
136
+ return null;
137
+ } catch (error) {
138
+ return console.error("Error searching for configuration:", error), null;
139
+ }
140
+ }
141
+
142
+ async function analyzeTicketNotes(ticketId, maxComments) {
143
+ const fetchLimit = Math.max(10, maxComments), notes = await import_client.connectWiseApiRequest.call(this, "GET", `/service/tickets/${ticketId}/notes`, {}, {
144
+ orderBy: "id desc",
145
+ pageSize: fetchLimit
146
+ }), atMaxComments = notes.length >= maxComments;
147
+ let lastFingerprintedNote = null, lastFingerprintedNoteType = null, hasFailureFingerprint = !1, hasSuccessFingerprint = !1, successCount = 0, failureCount = 0;
148
+ for (const note of notes) {
149
+ const text = note.text || "";
150
+ text.includes("\x3c!--msp-copilot:failure") && (hasFailureFingerprint = !0, failureCount++,
151
+ lastFingerprintedNote || (lastFingerprintedNote = note, lastFingerprintedNoteType = "failure")),
152
+ text.includes("\x3c!--msp-copilot:success") && (hasSuccessFingerprint = !0, successCount++,
153
+ lastFingerprintedNote || (lastFingerprintedNote = note, lastFingerprintedNoteType = "success")),
154
+ !lastFingerprintedNote && text.includes("\x3c!--msp-copilot--\x3e") && (lastFingerprintedNote = note,
155
+ lastFingerprintedNoteType = "failure");
156
+ }
157
+ return {
158
+ atMaxComments: atMaxComments,
159
+ lastNote: notes[0] || null,
160
+ lastFingerprintedNote: lastFingerprintedNote,
161
+ lastFingerprintedNoteType: lastFingerprintedNoteType,
162
+ hasFailureFingerprint: hasFailureFingerprint,
163
+ hasSuccessFingerprint: hasSuccessFingerprint,
164
+ successCount: successCount,
165
+ failureCount: failureCount,
166
+ isLastNoteFingerprinted: notes[0] === lastFingerprintedNote
167
+ };
168
+ }
169
+
170
+ async function searchExistingTicket(searchKey, companyId, boardId, success, options) {
171
+ try {
172
+ const conditions = [ `company/id=${companyId}`, `externalXRef="${searchKey}"` ];
173
+ if (success) {
174
+ if (conditions.push("closedFlag=false", 'slaStatus!="Resolved"'), options.successStatus) {
175
+ const statusObj = buildStatus(options.successStatus);
176
+ "id" in statusObj ? conditions.push(`status/id!=${statusObj.id}`) : conditions.push(`status/name!="${statusObj.name}"`);
177
+ }
178
+ } else if (options.reopenSameBoardOnly && conditions.push(`board/id=${boardId}`),
179
+ options.reopenClosedTicket) {
180
+ const ageConditions = [];
181
+ if (options.reopenTicketMaxAge) {
182
+ const maxAge = (0, import_utils.parseDuration)(options.reopenTicketMaxAge);
183
+ if (maxAge > 0) {
184
+ const cutoffDate = new Date(Date.now() - maxAge).toISOString();
185
+ ageConditions.push(`_info/dateEntered>=[${cutoffDate}]`);
186
+ }
187
+ }
188
+ if (options.reopenTicketLastUpdateAge) {
189
+ const maxUpdateAge = (0, import_utils.parseDuration)(options.reopenTicketLastUpdateAge);
190
+ if (maxUpdateAge > 0) {
191
+ const cutoffDate = new Date(Date.now() - maxUpdateAge).toISOString();
192
+ ageConditions.push(`_info/lastUpdated>=[${cutoffDate}]`);
193
+ }
194
+ }
195
+ ageConditions.length > 0 && conditions.push(`(closedFlag=false or (closedFlag=true and ${ageConditions.join(" and ")}))`);
196
+ } else conditions.push("closedFlag=false");
197
+ const tickets = await import_client.connectWiseApiRequest.call(this, "GET", "/service/tickets", {}, {
198
+ conditions: conditions.join(" and "),
199
+ orderBy: "id desc",
200
+ pageSize: 1
201
+ });
202
+ return tickets && tickets.length > 0 ? tickets[0] : null;
203
+ } catch (error) {
204
+ return console.error("Error searching for ticket:", error), null;
205
+ }
206
+ }
207
+
208
+ async function createTicket(data) {
209
+ console.log(`📝 Creating new ticket with summary: ${data.summary}`);
210
+ const response = await import_client.connectWiseApiRequest.call(this, "POST", "/service/tickets", data);
211
+ return console.log(`✅ Created ticket #${response.id}`), response;
212
+ }
213
+
214
+ async function updateTicketStatus(ticketId, statusValue) {
215
+ const statusObj = buildStatus(statusValue), path = "id" in statusObj ? "status/id" : "status/name", value = "id" in statusObj ? statusObj.id : statusObj.name;
216
+ try {
217
+ return await import_client.connectWiseApiRequest.call(this, "PATCH", `/service/tickets/${ticketId}`, [ {
218
+ op: "replace",
219
+ path: path,
220
+ value: value
221
+ } ]);
222
+ } catch (error) {
223
+ if (error.message && error.message.includes("ticket object is invalid") && /Status .* not found/.test(error.message)) return console.log(`⚠️ Status ${value} not valid for ticket #${ticketId}'s current board, skipping status update`),
224
+ !1;
225
+ throw error;
226
+ }
227
+ }
228
+
229
+ async function createTicketNote(ticketId, text, internalNote) {
230
+ console.log(`📝 Creating note on ticket #${ticketId}`), await import_client.connectWiseApiRequest.call(this, "POST", `/service/tickets/${ticketId}/notes`, {
231
+ text: text,
232
+ internalAnalysisFlag: internalNote,
233
+ detailDescriptionFlag: !internalNote,
234
+ processNotifications: !1
235
+ });
236
+ }
237
+
238
+ async function updateTicketNote(ticketId, noteId, text) {
239
+ console.log(`📝 Updating note #${noteId} on ticket #${ticketId}`), await import_client.connectWiseApiRequest.call(this, "PATCH", `/service/tickets/${ticketId}/notes/${noteId}`, [ {
240
+ op: "replace",
241
+ path: "text",
242
+ value: text
243
+ } ]);
244
+ }
245
+
246
+ async function processSmartTicket(params) {
247
+ const {companyId: companyId, alertName: alertName, alertId: alertId, alertSuccess: alertSuccess, boardId: boardId, failStatus: failStatus, successStatus: successStatus, reopenStatus: reopenStatus, priority: priority, summary: summary, failMessageDetailed: failMessageDetailed, successMessageDetailed: successMessageDetailed, failMessageShort: failMessageShort, successMessageShort: successMessageShort, internalNote: internalNote = !1, reopenClosedTicket: reopenClosedTicket = !1, reopenSameBoardOnly: reopenSameBoardOnly = !0, reopenTicketMaxAge: reopenTicketMaxAge, reopenTicketLastUpdateAge: reopenTicketLastUpdateAge, maxComments: maxComments = 20, appendPreviousNote: appendPreviousNote = !1, appendPreviousNoteTimeframe: appendPreviousNoteTimeframe, appendPreviousNoteOnlyIfLastNote: appendPreviousNoteOnlyIfLastNote = !0, prependToNote: prependToNote = !0, configurationTypeId: configurationTypeId, macAddress: macAddress, ipAddress: ipAddress, hostnameFull: hostnameFull, hostnameSearch: hostnameSearch, skipIfNoConfiguration: skipIfNoConfiguration = !1, appendConfigurationDetails: appendConfigurationDetails = !1, previewMode: previewMode = !1} = params, messageShort = alertSuccess ? successMessageShort : failMessageShort, messageDetailed = alertSuccess ? successMessageDetailed : failMessageDetailed, initialMessage = messageDetailed || messageShort || (alertSuccess ? "Alert success" : "Alert failure"), searchKey = `${alertName}|${alertId}`;
248
+ let noteMessageType, notePhase, fingerprintType = "";
249
+ const existingTicket = await searchExistingTicket.call(this, searchKey, companyId, boardId, alertSuccess, {
250
+ reopenClosedTicket: reopenClosedTicket,
251
+ reopenSameBoardOnly: reopenSameBoardOnly,
252
+ reopenTicketMaxAge: reopenTicketMaxAge,
253
+ reopenTicketLastUpdateAge: reopenTicketLastUpdateAge,
254
+ successStatus: successStatus,
255
+ failStatus: failStatus
256
+ });
257
+ let action, ticket = null, ticketId = null, noteAction, notePreview, configuration = null;
258
+ if (!existingTicket && alertSuccess) return action = "NO_TICKET_TO_RESOLVE", {
259
+ action: action,
260
+ ticket: null,
261
+ ticketId: null,
262
+ previewMode: previewMode,
263
+ noteAction: void 0,
264
+ notePreview: void 0,
265
+ noteMessageType: noteMessageType,
266
+ notePhase: notePhase
267
+ };
268
+ if (!existingTicket && !alertSuccess) {
269
+ if (action = "CREATE_TICKET", (macAddress || ipAddress || hostnameFull || hostnameSearch) && (configuration = await findConfiguration.call(this, companyId, configurationTypeId, macAddress, ipAddress, hostnameFull, hostnameSearch),
270
+ skipIfNoConfiguration && !configuration)) return action = "SKIP_NO_CONFIGURATION",
271
+ {
272
+ action: action,
273
+ ticket: null,
274
+ ticketId: null,
275
+ previewMode: previewMode,
276
+ reason: "No matching configuration found and skipIfNoConfiguration is enabled",
277
+ noteAction: void 0,
278
+ notePreview: void 0,
279
+ noteMessageType: noteMessageType,
280
+ notePhase: notePhase
281
+ };
282
+ let ticketDescription = initialMessage;
283
+ if (appendPreviousNote && !alertSuccess && (ticketDescription += FINGERPRINT_FAILURE),
284
+ appendConfigurationDetails && configuration) {
285
+ let configDetails = "";
286
+ const hardwareDetails = [];
287
+ configuration.serialNumber && hardwareDetails.push(`*Serial Number:* ${configuration.serialNumber}`),
288
+ configuration.modelNumber && hardwareDetails.push(`*Model Number:* ${configuration.modelNumber}`),
289
+ configuration.tagNumber && hardwareDetails.push(`*Tag:* ${configuration.tagNumber}`),
290
+ hardwareDetails.length > 0 && (configDetails += `\n` + hardwareDetails.join(`\n`)),
291
+ configuration.notes && (configDetails += `\nNotes: ${configuration.notes}`), ticketDescription += configDetails;
292
+ }
293
+ const resolvedPriority = await resolvePriority(this, priority), newTicketData = {
294
+ company: {
295
+ id: companyId
296
+ },
297
+ board: {
298
+ id: boardId
299
+ },
300
+ summary: summary,
301
+ status: buildStatus(failStatus),
302
+ initialDescription: ticketDescription,
303
+ externalXRef: searchKey
304
+ };
305
+ if (resolvedPriority ? newTicketData.priority = resolvedPriority : priority && console.warn(`⚠️ Priority "${priority}" could not be resolved, ticket will be created without priority`),
306
+ previewMode) ticket = {
307
+ id: null,
308
+ ...newTicketData
309
+ }, ticketId = ticket.id; else if (ticket = await createTicket.call(this, newTicketData),
310
+ ticketId = ticket.id, configuration && ticketId) try {
311
+ await import_client.connectWiseApiRequest.call(this, "POST", `/service/tickets/${ticketId}/configurations`, {
312
+ id: configuration.id
313
+ });
314
+ } catch (error) {
315
+ console.error(`Warning: Could not attach configuration to ticket #${ticketId}:`, error.message);
316
+ }
317
+ } else if (existingTicket) {
318
+ if (ticket = existingTicket, ticketId = ticket.id, existingTicket.closedFlag && !alertSuccess && reopenClosedTicket) {
319
+ if (action = "REOPEN_TICKET", !previewMode && ticketId) {
320
+ const statusForReopen = reopenStatus && reopenStatus !== "" ? reopenStatus : failStatus, updatedTicket = await updateTicketStatus.call(this, ticketId, statusForReopen);
321
+ updatedTicket !== !1 ? ticket = updatedTicket : action = "REOPEN_TICKET_STATUS_ERROR";
322
+ }
323
+ } else if (alertSuccess && !existingTicket.closedFlag) {
324
+ if (action = "CLOSE_TICKET", !previewMode && ticketId) {
325
+ const updatedTicket = await updateTicketStatus.call(this, ticketId, successStatus);
326
+ updatedTicket !== !1 ? ticket = updatedTicket : action = "CLOSE_TICKET_STATUS_ERROR";
327
+ }
328
+ } else if (!alertSuccess && !existingTicket.closedFlag) {
329
+ if (action = "UPDATE_TICKET_STATUS", !previewMode && ticketId) {
330
+ const statusForUpdate = reopenStatus && reopenStatus !== "" ? reopenStatus : failStatus, updatedTicket = await updateTicketStatus.call(this, ticketId, statusForUpdate);
331
+ updatedTicket !== !1 ? ticket = updatedTicket : action = "UPDATE_TICKET_STATUS_ERROR";
332
+ }
333
+ } else action = "NO_STATUS_UPDATE";
334
+ if (ticketId) {
335
+ const noteAnalysis = await analyzeTicketNotes.call(this, ticketId, maxComments), isPostFirstSuccess = noteAnalysis.hasSuccessFingerprint;
336
+ let shouldAppendToPrevious = !1;
337
+ if (appendPreviousNote && noteAnalysis.lastFingerprintedNote) {
338
+ let canAppend = !0;
339
+ if (canAppend && appendPreviousNoteOnlyIfLastNote && !noteAnalysis.isLastNoteFingerprinted && (canAppend = !1),
340
+ canAppend && appendPreviousNoteTimeframe) {
341
+ const timeframeMs = (0, import_utils.parseDuration)(appendPreviousNoteTimeframe);
342
+ if (timeframeMs > 0) {
343
+ const noteDate = new Date(noteAnalysis.lastFingerprintedNote._info?.lastUpdated || noteAnalysis.lastFingerprintedNote.dateCreated);
344
+ Date.now() - noteDate.getTime() > timeframeMs && (console.log(`📅 Note is too old to append to (note date: ${noteDate.toISOString()}, timeframe: ${appendPreviousNoteTimeframe})`),
345
+ canAppend = !1);
346
+ }
347
+ }
348
+ shouldAppendToPrevious = canAppend;
349
+ }
350
+ let noteContent, addFingerprint = !1;
351
+ if (noteMessageType = "fallback", notePhase = "consolidation", !isPostFirstSuccess && !alertSuccess ? (noteContent = messageShort || messageDetailed || "Alert failure",
352
+ noteMessageType = messageShort ? "short" : messageDetailed ? "detailed" : "fallback",
353
+ notePhase = "pre-first-success", shouldAppendToPrevious || (addFingerprint = !0,
354
+ fingerprintType = FINGERPRINT_FAILURE)) : alertSuccess && noteAnalysis.successCount === 0 ? (noteContent = messageDetailed || messageShort || "Alert success",
355
+ noteMessageType = messageDetailed ? "detailed" : messageShort ? "short" : "fallback",
356
+ notePhase = "first-success", shouldAppendToPrevious = !1, addFingerprint = !0, fingerprintType = FINGERPRINT_SUCCESS) : !alertSuccess && noteAnalysis.lastFingerprintedNoteType === "success" ? (noteContent = messageShort || messageDetailed || "Alert failure",
357
+ noteMessageType = messageShort ? "short" : messageDetailed ? "detailed" : "fallback",
358
+ notePhase = "post-first-success", shouldAppendToPrevious = !1, addFingerprint = !0,
359
+ fingerprintType = FINGERPRINT_FAILURE) : (noteContent = messageShort || messageDetailed || (alertSuccess ? "Alert success" : "Alert failure"),
360
+ noteMessageType = messageShort ? "short" : messageDetailed ? "detailed" : "fallback",
361
+ notePhase = "consolidation", shouldAppendToPrevious || (addFingerprint = !0, fingerprintType = alertSuccess ? FINGERPRINT_SUCCESS : FINGERPRINT_FAILURE)),
362
+ shouldAppendToPrevious && noteAnalysis.lastFingerprintedNote) {
363
+ noteAction = "APPEND_TO_PREVIOUS_NOTE";
364
+ const cleanedContent = noteAnalysis.lastFingerprintedNote.text.replace(/\n<!--msp-copilot[^>]*-->/g, ""), existingIsDetailed = !cleanedContent.match(/^`\d{4}-\d{2}-\d{2}/), appendingDetailedMessage = noteMessageType === "detailed", separator = existingIsDetailed || appendingDetailedMessage ? `\n\n` : `\n`, timestampSeparator = appendingDetailedMessage ? `\n` : " ", newEntry = `${formatTimestamp()}${timestampSeparator}${noteContent}`, combinedContent = prependToNote ? `${newEntry}${separator}${cleanedContent}` : `${cleanedContent}${separator}${newEntry}`;
365
+ if (notePreview = newEntry, !previewMode) {
366
+ const originalFingerprint = noteAnalysis.lastFingerprintedNote.text.match(/\n<!--msp-copilot[^>]*-->/)?.[0] || FINGERPRINT;
367
+ await updateTicketNote.call(this, ticketId, noteAnalysis.lastFingerprintedNote.id, combinedContent + originalFingerprint);
368
+ }
369
+ } else if (noteAnalysis.atMaxComments) {
370
+ if (noteAction = "UPDATE_LAST_NOTE", noteAnalysis.lastNote) {
371
+ const cleanedContent = noteAnalysis.lastNote.text.replace(/\n<!--msp-copilot[^>]*-->/g, ""), existingIsDetailed = !cleanedContent.match(/^`\d{4}-\d{2}-\d{2}/), appendingDetailedMessage = noteMessageType === "detailed", separator = existingIsDetailed || appendingDetailedMessage ? `\n\n` : `\n`, timestampSeparator = appendingDetailedMessage ? `\n` : " ", newEntry = `${formatTimestamp()}${timestampSeparator}${noteContent}`, combinedContent = prependToNote ? `${newEntry}${separator}${cleanedContent}` : `${cleanedContent}${separator}${newEntry}`;
372
+ notePreview = newEntry, previewMode || await updateTicketNote.call(this, ticketId, noteAnalysis.lastNote.id, combinedContent + fingerprintType);
373
+ }
374
+ } else {
375
+ noteAction = "CREATE_NOTE";
376
+ const isFirstSuccess = notePhase === "first-success", timestampSeparator = !isFirstSuccess && noteMessageType === "detailed" ? `\n` : " ", newEntry = isFirstSuccess ? noteContent : `${formatTimestamp()}${timestampSeparator}${noteContent}`;
377
+ if (notePreview = newEntry, !previewMode) {
378
+ const finalNoteText = addFingerprint ? newEntry + fingerprintType : newEntry;
379
+ await createTicketNote.call(this, ticketId, finalNoteText, internalNote);
380
+ }
381
+ }
382
+ }
383
+ }
384
+ return {
385
+ action: action,
386
+ noteAction: noteAction,
387
+ notePreview: notePreview,
388
+ noteMessageType: noteMessageType,
389
+ notePhase: notePhase,
390
+ ticketId: ticketId,
391
+ previewMode: previewMode,
392
+ ticket: ticket,
393
+ configuration: configuration || void 0
394
+ };
395
+ }
396
+
397
+ const executeSmartConsolidationPro = async function(itemIndex) {
398
+ try {
399
+ const companyId = this.getNodeParameter("company", itemIndex), alertName = this.getNodeParameter("alertName", itemIndex).trim().substring(0, 40), alertIdRaw = this.getNodeParameter("alertId", itemIndex).trim(), alertId = alertIdRaw.length <= 50 ? alertIdRaw : (0,
400
+ import_crypto.createHash)("md5").update(alertIdRaw).digest("hex"), alertSuccess = this.getNodeParameter("alertSuccess", itemIndex), boardId = this.getNodeParameter("board", itemIndex), failStatus = this.getNodeParameter("failStatus", itemIndex), successStatus = this.getNodeParameter("successStatus", itemIndex), priority = this.getNodeParameter("priority", itemIndex), summary = trimSummary(this.getNodeParameter("summary", itemIndex)), failMessageDetailed = this.getNodeParameter("failMessageDetailed", itemIndex, ""), successMessageDetailed = this.getNodeParameter("successMessageDetailed", itemIndex, ""), failMessageShort = this.getNodeParameter("failMessageShort", itemIndex, ""), successMessageShort = this.getNodeParameter("successMessageShort", itemIndex, ""), internalNote = this.getNodeParameter("internalNote", itemIndex, !1), reopenClosedTicket = this.getNodeParameter("reopenClosedTicket", itemIndex, !1), reopenSameBoardOnly = this.getNodeParameter("reopenSameBoardOnly", itemIndex, !0), reopenStatus = this.getNodeParameter("reopenStatus", itemIndex, ""), reopenTicketMaxAge = this.getNodeParameter("reopenTicketMaxAge", itemIndex, ""), reopenTicketLastUpdateAge = this.getNodeParameter("reopenTicketLastUpdateAge", itemIndex, ""), maxComments = this.getNodeParameter("maxComments", itemIndex, 20), appendPreviousNote = this.getNodeParameter("appendPreviousNote", itemIndex, !1), appendPreviousNoteTimeframe = this.getNodeParameter("appendPreviousNoteTimeframe", itemIndex, ""), appendPreviousNoteOnlyIfLastNote = this.getNodeParameter("appendPreviousNoteOnlyIfLastNote", itemIndex, !0), prependToNote = this.getNodeParameter("prependToNote", itemIndex, !0), configAttachment = this.getNodeParameter("configAttachment", itemIndex, {}), configurationTypeId = configAttachment.configurationTypeId, macAddress = configAttachment.macAddress, ipAddress = configAttachment.ipAddress, hostnameFull = configAttachment.hostnameFull, hostnameSearch = configAttachment.hostnameSearch, skipIfNoConfiguration = configAttachment.skipIfNoConfiguration, appendConfigurationDetails = configAttachment.appendConfigurationDetails, previewMode = this.getNodeParameter("previewMode", itemIndex, !1), params = {
401
+ companyId: companyId,
402
+ alertName: alertName,
403
+ alertId: alertId,
404
+ alertSuccess: alertSuccess,
405
+ boardId: boardId,
406
+ failStatus: failStatus,
407
+ successStatus: successStatus,
408
+ reopenStatus: reopenStatus,
409
+ priority: priority,
410
+ summary: summary,
411
+ failMessageDetailed: failMessageDetailed,
412
+ successMessageDetailed: successMessageDetailed,
413
+ failMessageShort: failMessageShort,
414
+ successMessageShort: successMessageShort,
415
+ internalNote: internalNote,
416
+ reopenClosedTicket: reopenClosedTicket,
417
+ reopenSameBoardOnly: reopenSameBoardOnly,
418
+ reopenTicketMaxAge: reopenTicketMaxAge,
419
+ reopenTicketLastUpdateAge: reopenTicketLastUpdateAge,
420
+ maxComments: maxComments,
421
+ appendPreviousNote: appendPreviousNote,
422
+ appendPreviousNoteTimeframe: appendPreviousNoteTimeframe,
423
+ appendPreviousNoteOnlyIfLastNote: appendPreviousNoteOnlyIfLastNote,
424
+ prependToNote: prependToNote,
425
+ configurationTypeId: configurationTypeId,
426
+ macAddress: macAddress,
427
+ ipAddress: ipAddress,
428
+ hostnameFull: hostnameFull,
429
+ hostnameSearch: hostnameSearch,
430
+ skipIfNoConfiguration: skipIfNoConfiguration,
431
+ appendConfigurationDetails: appendConfigurationDetails,
432
+ previewMode: previewMode
433
+ };
434
+ return [ {
435
+ json: await processSmartTicket.call(this, params),
436
+ pairedItem: {
437
+ item: itemIndex
438
+ }
439
+ } ];
440
+ } catch (error) {
441
+ throw new import_n8n_workflow.NodeOperationError(this.getNode(), `Smart Alert Consolidation failed: ${error.message}`, {
442
+ description: error.description
443
+ });
444
+ }
445
+ }, executeSmartConsolidation = executeSmartConsolidationPro;
39
446
 
40
447
  0 && (module.exports = {
41
448
  executeSmartConsolidation: executeSmartConsolidation
@@ -75,6 +75,11 @@ const ticketFields = [ {
75
75
  value: "update",
76
76
  description: "Update a ticket",
77
77
  action: "Update a ticket"
78
+ }, {
79
+ name: "Smart Alert Consolidation ✨",
80
+ value: "smartAlertConsolidation",
81
+ description: "Create, update or close tickets with smart alert consolidation",
82
+ action: "Smart alert consolidation ✨"
78
83
  } ],
79
84
  default: "create"
80
85
  }, {
@@ -995,11 +1000,6 @@ const ticketFields = [ {
995
1000
 
996
1001
  async function execute(operation, i) {
997
1002
  if (operation === "smartAlertConsolidation") {
998
- {
999
- if (process.env.MSPCOPILOT_BETA !== "true") return (0, import_utils.returnProOnlyMessage)(this, i);
1000
- const {requireAccessKey: requireAccessKey} = await import("../../transport/access-key.js");
1001
- if (!await requireAccessKey(this, "beta")) return (0, import_utils.returnAccessKeyRequired)(this, i);
1002
- }
1003
1003
  const {executeSmartConsolidation: executeSmartConsolidation} = await import("./alert-consolidation.js");
1004
1004
  return executeSmartConsolidation.call(this, i);
1005
1005
  }
@@ -353,6 +353,13 @@ const timeEntryFields = [ {
353
353
  default: !1,
354
354
  description: "Whether to email the resource about this time entry",
355
355
  x_submitAs: "emailResourceFlag"
356
+ }, {
357
+ displayName: "Email CC",
358
+ name: "emailCcFlag",
359
+ type: "boolean",
360
+ default: !1,
361
+ description: "Whether to email the CC list on the ticket about this time entry",
362
+ x_submitAs: "emailCcFlag"
356
363
  }, {
357
364
  displayName: "Email Contact",
358
365
  name: "emailContactFlag",
@@ -39,7 +39,7 @@ module.exports = __toCommonJS(version_check_exports);
39
39
 
40
40
  var fs = __toESM(require("fs/promises")), path = __toESM(require("path")), os = __toESM(require("os")), import_common = require("../descriptions/common.descriptions"), import_hint_collector = require("../helpers/hint-collector");
41
41
 
42
- const CACHE_FILE_PATH = path.join(os.tmpdir(), "connectwise-n8n-version-cache.json"), CURRENT_VERSION = "0.2.3";
42
+ const CACHE_FILE_PATH = path.join(os.tmpdir(), "connectwise-n8n-version-cache.json"), CURRENT_VERSION = "0.2.4";
43
43
 
44
44
  let inMemoryCache = null, cacheLoadPromise = null, fetchInProgress = !1;
45
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mspcopilot/n8n-nodes-connectwise",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "n8n node to integrate with ConnectWise PSA",
5
5
  "keywords": [
6
6
  "n8n-community-node-package"