snow-flow 1.3.21 → 1.3.23
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.
- package/dist/mcp/servicenow-deployment-mcp.js +258 -9
- package/dist/mcp/servicenow-flow-composer-mcp.js +148 -23
- package/dist/memory/memory-client.js +30 -2
- package/dist/memory/memory-operations.js +63 -11
- package/dist/utils/servicenow-client.js +99 -30
- package/dist/utils/xml-first-flow-generator.js +24 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -789,6 +789,56 @@ class ServiceNowDeploymentMCP {
|
|
|
789
789
|
};
|
|
790
790
|
let troubleshootingSteps = '';
|
|
791
791
|
if (is403Error(directError) || is403Error(tableError)) {
|
|
792
|
+
// CRITICAL FIX: Check if widget was actually created despite 403 error
|
|
793
|
+
this.logger.info('403 error detected, verifying if widget was actually created...');
|
|
794
|
+
const verificationResult = await this.verifyWidgetInServiceNow(args.name);
|
|
795
|
+
if (verificationResult.exists) {
|
|
796
|
+
// Widget was created successfully despite 403 error!
|
|
797
|
+
this.logger.info('🎉 Widget verification SUCCESS: Widget exists despite 403 error', {
|
|
798
|
+
widgetName: args.name,
|
|
799
|
+
sys_id: verificationResult.sys_id,
|
|
800
|
+
completenessScore: verificationResult.completenessScore
|
|
801
|
+
});
|
|
802
|
+
// Format successful response similar to normal deployment
|
|
803
|
+
const credentials = await this.oauth.loadCredentials();
|
|
804
|
+
const widgetUrl = credentials?.instance ?
|
|
805
|
+
`https://${credentials.instance}/sp_config?id=widget_editor&sys_id=${verificationResult.sys_id}` :
|
|
806
|
+
'ServiceNow instance URL not available';
|
|
807
|
+
return {
|
|
808
|
+
content: [
|
|
809
|
+
{
|
|
810
|
+
type: 'text',
|
|
811
|
+
text: `✅ Widget deployed successfully! (Despite 403 error)
|
|
812
|
+
|
|
813
|
+
🎯 Widget Details:
|
|
814
|
+
- Name: ${args.name}
|
|
815
|
+
- Title: ${verificationResult.title || args.title}
|
|
816
|
+
- Sys ID: ${verificationResult.sys_id}
|
|
817
|
+
- Deployment Method: ${deploymentMethod} (with error recovery)
|
|
818
|
+
- Verification: ✅ Confirmed (${verificationResult.completenessScore}/100 complete)
|
|
819
|
+
|
|
820
|
+
📦 Update Set:
|
|
821
|
+
- Name: ${updateSetName}
|
|
822
|
+
- ID: ${updateSetId || 'None'}
|
|
823
|
+
- Status: ${updateSetId ? '✅ Tracked' : '⚠️ Not tracked'}
|
|
824
|
+
|
|
825
|
+
🔗 Direct Links:
|
|
826
|
+
- Widget Editor: ${widgetUrl}
|
|
827
|
+
- Service Portal Designer: https://${credentials?.instance}/sp_config?id=designer
|
|
828
|
+
|
|
829
|
+
🔧 Note: Widget was created successfully despite receiving a 403 error. This is a known issue with ServiceNow permissions that has been automatically resolved.
|
|
830
|
+
|
|
831
|
+
⚡ **Ready for Testing**
|
|
832
|
+
Your widget has been deployed and is ready for testing in Service Portal.`
|
|
833
|
+
}
|
|
834
|
+
]
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// Widget was NOT created, continue with error handling
|
|
838
|
+
this.logger.warn('Widget verification failed: Widget does not exist after deployment attempts', {
|
|
839
|
+
widgetName: args.name,
|
|
840
|
+
verificationDetails: verificationResult.debugInfo
|
|
841
|
+
});
|
|
792
842
|
// Run authentication diagnostics automatically on 403 errors
|
|
793
843
|
this.logger.info('403 error detected, running automatic authentication diagnostics...');
|
|
794
844
|
let diagnosticsResult = '';
|
|
@@ -2079,15 +2129,44 @@ ${sessionSummary.statusCounts.pending > 0 ? '- 📋 Complete pending deployments
|
|
|
2079
2129
|
if (!data || typeof data !== 'object') {
|
|
2080
2130
|
throw new Error('Invalid diagnostics data received from ServiceNow');
|
|
2081
2131
|
}
|
|
2082
|
-
// Format test results
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2132
|
+
// Format test results with enhanced null safety
|
|
2133
|
+
let testResults = 'No test results available';
|
|
2134
|
+
if (tests && typeof tests === 'object') {
|
|
2135
|
+
try {
|
|
2136
|
+
const entries = Object.entries(tests);
|
|
2137
|
+
if (entries.length > 0) {
|
|
2138
|
+
testResults = entries.map(([name, result]) => {
|
|
2139
|
+
// CRITICAL: Extra null checks for each property
|
|
2140
|
+
const status = result?.status || 'Unknown';
|
|
2141
|
+
const description = result?.description || 'No description';
|
|
2142
|
+
const error = result?.error && typeof result.error === 'string' ? `- Error: ${result.error}` : '';
|
|
2143
|
+
const httpStatus = result?.http_status && typeof result.http_status === 'number' ? `- HTTP Status: ${result.http_status}` : '';
|
|
2144
|
+
return `**${name}:** ${status}
|
|
2145
|
+
- ${description}
|
|
2146
|
+
${error}
|
|
2147
|
+
${httpStatus}`;
|
|
2148
|
+
}).join('\n\n');
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
catch (testFormatError) {
|
|
2152
|
+
this.logger.error('Error formatting test results:', testFormatError);
|
|
2153
|
+
testResults = '❌ Error formatting test results - check ServiceNow connection';
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
// Format recommendations with null safety
|
|
2157
|
+
let recommendationText = '';
|
|
2158
|
+
if (args.include_recommendations !== false && Array.isArray(recommendations) && recommendations.length > 0) {
|
|
2159
|
+
try {
|
|
2160
|
+
const validRecommendations = recommendations.filter(rec => rec && typeof rec === 'string');
|
|
2161
|
+
if (validRecommendations.length > 0) {
|
|
2162
|
+
recommendationText = `\n\n**🔧 Troubleshooting Recommendations:**\n${validRecommendations.map((rec) => `- ${rec}`).join('\n')}`;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
catch (recFormatError) {
|
|
2166
|
+
this.logger.error('Error formatting recommendations:', recFormatError);
|
|
2167
|
+
recommendationText = '\n\n**🔧 Troubleshooting Recommendations:**\n- Unable to format recommendations due to error';
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2091
2170
|
// Generate URL fix recommendation if we detect the trailing slash issue
|
|
2092
2171
|
const urlFixRecommendation = data.instance_url && typeof data.instance_url === 'string' && data.instance_url.includes('//')
|
|
2093
2172
|
? '\n\n**🚨 CRITICAL URL ISSUE DETECTED:**\n- Your SNOW_INSTANCE in .env has a trailing slash\n- This causes malformed URLs like https://instance.com//api/\n- Remove the trailing slash from SNOW_INSTANCE=your-instance.com/'
|
|
@@ -5506,6 +5585,176 @@ Use individual deployment tools like \`snow_deploy_${args.type}\` with manual co
|
|
|
5506
5585
|
|
|
5507
5586
|
**Error Details**: ${error.message || error}`;
|
|
5508
5587
|
}
|
|
5588
|
+
/**
|
|
5589
|
+
* Verify widget exists in ServiceNow with comprehensive retry logic
|
|
5590
|
+
* Addresses the critical false negative bug where widgets show 403 errors but are actually created
|
|
5591
|
+
*/
|
|
5592
|
+
async verifyWidgetInServiceNow(widgetName) {
|
|
5593
|
+
const maxRetries = 5;
|
|
5594
|
+
const baseDelay = 2000; // Start with 2 seconds
|
|
5595
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
5596
|
+
try {
|
|
5597
|
+
// Progressive delay - ServiceNow needs time to process
|
|
5598
|
+
if (attempt > 1) {
|
|
5599
|
+
const delay = baseDelay * attempt; // 2s, 4s, 6s, 8s, 10s
|
|
5600
|
+
this.logger.info(`Waiting ${delay}ms before widget verification attempt ${attempt}/${maxRetries}`);
|
|
5601
|
+
await this.sleep(delay);
|
|
5602
|
+
}
|
|
5603
|
+
this.logger.info(`Widget verification attempt ${attempt}/${maxRetries}`, { widgetName });
|
|
5604
|
+
// Multi-table verification approach (similar to flow verification)
|
|
5605
|
+
const [mainWidgetCheck, widgetSearchCheck] = await Promise.allSettled([
|
|
5606
|
+
// Check 1: Direct sp_widget table search by name
|
|
5607
|
+
this.client.searchRecords('sp_widget', `name=${widgetName}`, 1),
|
|
5608
|
+
// Check 2: Broader search with ID field
|
|
5609
|
+
this.client.searchRecords('sp_widget', `name=${widgetName}^ORid=${widgetName}`, 5)
|
|
5610
|
+
]);
|
|
5611
|
+
let mainWidget = null;
|
|
5612
|
+
let searchResults = null;
|
|
5613
|
+
let verificationDetails = {
|
|
5614
|
+
attempt,
|
|
5615
|
+
mainWidgetCheck: 'pending',
|
|
5616
|
+
widgetSearchCheck: 'pending',
|
|
5617
|
+
totalFound: 0
|
|
5618
|
+
};
|
|
5619
|
+
// Process main widget check
|
|
5620
|
+
if (mainWidgetCheck.status === 'fulfilled' && mainWidgetCheck.value.success) {
|
|
5621
|
+
mainWidget = mainWidgetCheck.value.data?.[0];
|
|
5622
|
+
verificationDetails.mainWidgetCheck = mainWidget ? 'found' : 'not_found';
|
|
5623
|
+
verificationDetails.totalFound = mainWidgetCheck.value.data?.length || 0;
|
|
5624
|
+
}
|
|
5625
|
+
else {
|
|
5626
|
+
verificationDetails.mainWidgetCheck = `failed: ${mainWidgetCheck.status === 'rejected' ? mainWidgetCheck.reason : 'unknown error'}`;
|
|
5627
|
+
}
|
|
5628
|
+
// Process widget search check
|
|
5629
|
+
if (widgetSearchCheck.status === 'fulfilled' && widgetSearchCheck.value.success) {
|
|
5630
|
+
searchResults = widgetSearchCheck.value.data || [];
|
|
5631
|
+
verificationDetails.widgetSearchCheck = `found_${searchResults.length}`;
|
|
5632
|
+
// Use search results if main check didn't find anything
|
|
5633
|
+
if (!mainWidget && searchResults.length > 0) {
|
|
5634
|
+
mainWidget = searchResults[0];
|
|
5635
|
+
verificationDetails.totalFound = searchResults.length;
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
else {
|
|
5639
|
+
verificationDetails.widgetSearchCheck = `failed: ${widgetSearchCheck.status === 'rejected' ? widgetSearchCheck.reason : 'unknown error'}`;
|
|
5640
|
+
}
|
|
5641
|
+
// Calculate completeness score
|
|
5642
|
+
let completenessScore = 0;
|
|
5643
|
+
if (mainWidget) {
|
|
5644
|
+
completenessScore += mainWidget.sys_id ? 25 : 0;
|
|
5645
|
+
completenessScore += mainWidget.name ? 25 : 0;
|
|
5646
|
+
completenessScore += mainWidget.title ? 25 : 0;
|
|
5647
|
+
completenessScore += mainWidget.template ? 25 : 0;
|
|
5648
|
+
}
|
|
5649
|
+
if (mainWidget && completenessScore >= 75) {
|
|
5650
|
+
// Widget found and appears complete
|
|
5651
|
+
this.logger.info(`✅ Widget verification SUCCESS on attempt ${attempt}`, {
|
|
5652
|
+
widgetName,
|
|
5653
|
+
sys_id: mainWidget.sys_id,
|
|
5654
|
+
completenessScore,
|
|
5655
|
+
totalRetries: attempt
|
|
5656
|
+
});
|
|
5657
|
+
return {
|
|
5658
|
+
exists: true,
|
|
5659
|
+
sys_id: mainWidget.sys_id,
|
|
5660
|
+
name: mainWidget.name,
|
|
5661
|
+
title: mainWidget.title,
|
|
5662
|
+
completenessScore,
|
|
5663
|
+
attempt,
|
|
5664
|
+
verificationDetails,
|
|
5665
|
+
debugInfo: {
|
|
5666
|
+
foundVia: verificationDetails.mainWidgetCheck === 'found' ? 'main_check' : 'search_check',
|
|
5667
|
+
totalFound: verificationDetails.totalFound,
|
|
5668
|
+
retriesNeeded: attempt
|
|
5669
|
+
}
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
else if (mainWidget && completenessScore < 75) {
|
|
5673
|
+
// Widget found but incomplete - might still be processing
|
|
5674
|
+
this.logger.warn(`⚠️ Widget found but incomplete on attempt ${attempt}`, {
|
|
5675
|
+
widgetName,
|
|
5676
|
+
sys_id: mainWidget.sys_id,
|
|
5677
|
+
completenessScore,
|
|
5678
|
+
remainingRetries: maxRetries - attempt
|
|
5679
|
+
});
|
|
5680
|
+
if (attempt === maxRetries) {
|
|
5681
|
+
// Last attempt - return what we have
|
|
5682
|
+
return {
|
|
5683
|
+
exists: true,
|
|
5684
|
+
sys_id: mainWidget.sys_id,
|
|
5685
|
+
name: mainWidget.name,
|
|
5686
|
+
title: mainWidget.title,
|
|
5687
|
+
completenessScore,
|
|
5688
|
+
attempt,
|
|
5689
|
+
verificationDetails,
|
|
5690
|
+
debugInfo: {
|
|
5691
|
+
warning: 'Widget exists but appears incomplete',
|
|
5692
|
+
foundVia: verificationDetails.mainWidgetCheck === 'found' ? 'main_check' : 'search_check',
|
|
5693
|
+
totalFound: verificationDetails.totalFound,
|
|
5694
|
+
retriesNeeded: attempt
|
|
5695
|
+
}
|
|
5696
|
+
};
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
else {
|
|
5700
|
+
// Widget not found
|
|
5701
|
+
this.logger.warn(`❌ Widget not found on attempt ${attempt}`, {
|
|
5702
|
+
widgetName,
|
|
5703
|
+
verificationDetails,
|
|
5704
|
+
remainingRetries: maxRetries - attempt
|
|
5705
|
+
});
|
|
5706
|
+
if (attempt === maxRetries) {
|
|
5707
|
+
// Final attempt failed
|
|
5708
|
+
return {
|
|
5709
|
+
exists: false,
|
|
5710
|
+
attempt,
|
|
5711
|
+
verificationDetails,
|
|
5712
|
+
debugInfo: {
|
|
5713
|
+
finalAttempt: true,
|
|
5714
|
+
allChecks: {
|
|
5715
|
+
mainWidgetCheck: verificationDetails.mainWidgetCheck,
|
|
5716
|
+
widgetSearchCheck: verificationDetails.widgetSearchCheck
|
|
5717
|
+
},
|
|
5718
|
+
totalRetries: maxRetries
|
|
5719
|
+
}
|
|
5720
|
+
};
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
catch (verificationError) {
|
|
5725
|
+
this.logger.error(`Widget verification attempt ${attempt} failed`, {
|
|
5726
|
+
widgetName,
|
|
5727
|
+
error: verificationError instanceof Error ? verificationError.message : String(verificationError),
|
|
5728
|
+
remainingRetries: maxRetries - attempt
|
|
5729
|
+
});
|
|
5730
|
+
if (attempt === maxRetries) {
|
|
5731
|
+
// Final attempt - return failure with error details
|
|
5732
|
+
return {
|
|
5733
|
+
exists: false,
|
|
5734
|
+
attempt,
|
|
5735
|
+
error: verificationError instanceof Error ? verificationError.message : String(verificationError),
|
|
5736
|
+
debugInfo: {
|
|
5737
|
+
finalAttempt: true,
|
|
5738
|
+
verificationError: true,
|
|
5739
|
+
totalRetries: maxRetries
|
|
5740
|
+
}
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
// Should not reach here, but safety fallback
|
|
5746
|
+
return {
|
|
5747
|
+
exists: false,
|
|
5748
|
+
attempt: maxRetries,
|
|
5749
|
+
debugInfo: { unexpectedFallback: true }
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5752
|
+
/**
|
|
5753
|
+
* Sleep utility for retry delays
|
|
5754
|
+
*/
|
|
5755
|
+
sleep(ms) {
|
|
5756
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
5757
|
+
}
|
|
5509
5758
|
/**
|
|
5510
5759
|
* Format successful deployment response
|
|
5511
5760
|
*/
|
|
@@ -352,17 +352,36 @@ class ServiceNowFlowComposerMCP {
|
|
|
352
352
|
console.log('✅ XML generated:', xmlResult.filePath);
|
|
353
353
|
// Auto-deploy with fallback strategies
|
|
354
354
|
const deployResult = await this.deployWithFallback(xmlResult.filePath, flowDefinition);
|
|
355
|
-
//
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
358
|
-
|
|
355
|
+
// Comprehensive deployment verification
|
|
356
|
+
const verification = await this.verifyFlowInServiceNow(parsedIntent.flowName);
|
|
357
|
+
if (!verification.verified) {
|
|
358
|
+
// Deployment claimed success but flow not found - this is the critical bug!
|
|
359
|
+
const errorMsg = `Flow deployment reported success but verification failed: ${verification.reason}`;
|
|
360
|
+
// Log detailed verification results for debugging
|
|
361
|
+
this.logger.error('CRITICAL: False positive deployment detected', {
|
|
362
|
+
flowName: parsedIntent.flowName,
|
|
363
|
+
verificationAttempts: verification.attempts,
|
|
364
|
+
partialResults: verification.partial_results,
|
|
365
|
+
searchQuery: verification.search_query,
|
|
366
|
+
error: verification.error
|
|
367
|
+
});
|
|
368
|
+
throw new Error(errorMsg);
|
|
359
369
|
}
|
|
370
|
+
// Success with comprehensive verification
|
|
360
371
|
deploymentResult = {
|
|
361
372
|
success: true,
|
|
362
373
|
method: deployResult.strategy,
|
|
363
374
|
xml_file: xmlResult.filePath,
|
|
364
375
|
message: `✅ Flow deployed via ${deployResult.strategy} and verified in ServiceNow!`,
|
|
365
|
-
flow_sys_id:
|
|
376
|
+
flow_sys_id: verification.sys_id,
|
|
377
|
+
flow_url: verification.url,
|
|
378
|
+
verification_score: verification.completeness_score,
|
|
379
|
+
verification_details: {
|
|
380
|
+
has_flow: true,
|
|
381
|
+
has_snapshot: verification.has_snapshot,
|
|
382
|
+
has_trigger: verification.has_trigger,
|
|
383
|
+
attempts_needed: verification.verification_attempt
|
|
384
|
+
}
|
|
366
385
|
};
|
|
367
386
|
}
|
|
368
387
|
catch (xmlError) {
|
|
@@ -1987,30 +2006,136 @@ ${categoryFilteredResults.length === 0 ? `🔍 **No templates found matching you
|
|
|
1987
2006
|
return mapping[activityType.toLowerCase()] || 'script';
|
|
1988
2007
|
}
|
|
1989
2008
|
/**
|
|
1990
|
-
*
|
|
2009
|
+
* Comprehensive flow verification with retry logic
|
|
1991
2010
|
*/
|
|
1992
2011
|
async verifyFlowInServiceNow(flowName) {
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2012
|
+
const maxRetries = 5;
|
|
2013
|
+
const baseDelay = 2000; // Start with 2 seconds
|
|
2014
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2015
|
+
try {
|
|
2016
|
+
// Progressive delay - ServiceNow needs time to process
|
|
2017
|
+
if (attempt > 1) {
|
|
2018
|
+
const delay = baseDelay * attempt; // 2s, 4s, 6s, 8s, 10s
|
|
2019
|
+
this.logger.info(`Waiting ${delay}ms before verification attempt ${attempt}/${maxRetries}`);
|
|
2020
|
+
await this.sleep(delay);
|
|
2021
|
+
}
|
|
2022
|
+
const ServiceNowClient = (await Promise.resolve().then(() => __importStar(require('../utils/servicenow-client.js')))).ServiceNowClient;
|
|
2023
|
+
const client = new ServiceNowClient();
|
|
2024
|
+
// Multi-table verification for comprehensive check
|
|
2025
|
+
const verificationPromises = [
|
|
2026
|
+
// Check main flow table
|
|
2027
|
+
client.makeRequest({
|
|
2028
|
+
method: 'GET',
|
|
2029
|
+
url: '/api/now/table/sys_hub_flow',
|
|
2030
|
+
params: {
|
|
2031
|
+
sysparm_query: `name=${flowName}^ORsys_name=${flowName}`,
|
|
2032
|
+
sysparm_fields: 'sys_id,name,sys_name,active,table,description',
|
|
2033
|
+
sysparm_limit: 5
|
|
2034
|
+
}
|
|
2035
|
+
}),
|
|
2036
|
+
// Check flow snapshots
|
|
2037
|
+
client.makeRequest({
|
|
2038
|
+
method: 'GET',
|
|
2039
|
+
url: '/api/now/table/sys_hub_flow_snapshot',
|
|
2040
|
+
params: {
|
|
2041
|
+
sysparm_query: `flow.name=${flowName}^ORflow.sys_name=${flowName}`,
|
|
2042
|
+
sysparm_fields: 'sys_id,flow,name,active',
|
|
2043
|
+
sysparm_limit: 5
|
|
2044
|
+
}
|
|
2045
|
+
}),
|
|
2046
|
+
// Check trigger instances
|
|
2047
|
+
client.makeRequest({
|
|
2048
|
+
method: 'GET',
|
|
2049
|
+
url: '/api/now/table/sys_hub_trigger_instance',
|
|
2050
|
+
params: {
|
|
2051
|
+
sysparm_query: `flow.name=${flowName}^ORflow.sys_name=${flowName}`,
|
|
2052
|
+
sysparm_fields: 'sys_id,flow,trigger_type',
|
|
2053
|
+
sysparm_limit: 5
|
|
2054
|
+
}
|
|
2055
|
+
})
|
|
2056
|
+
];
|
|
2057
|
+
const [flowCheck, snapshotCheck, triggerCheck] = await Promise.allSettled(verificationPromises);
|
|
2058
|
+
// Analyze results
|
|
2059
|
+
const flowExists = flowCheck.status === 'fulfilled' &&
|
|
2060
|
+
flowCheck.value.result &&
|
|
2061
|
+
flowCheck.value.result.length > 0;
|
|
2062
|
+
const snapshotExists = snapshotCheck.status === 'fulfilled' &&
|
|
2063
|
+
snapshotCheck.value.result &&
|
|
2064
|
+
snapshotCheck.value.result.length > 0;
|
|
2065
|
+
const triggerExists = triggerCheck.status === 'fulfilled' &&
|
|
2066
|
+
triggerCheck.value.result &&
|
|
2067
|
+
triggerCheck.value.result.length > 0;
|
|
2068
|
+
// Comprehensive verification result
|
|
2069
|
+
if (flowExists) {
|
|
2070
|
+
const flowData = flowCheck.value.result[0];
|
|
2071
|
+
return {
|
|
2072
|
+
verified: true,
|
|
2073
|
+
sys_id: flowData.sys_id,
|
|
2074
|
+
name: flowData.name || flowData.sys_name,
|
|
2075
|
+
active: flowData.active,
|
|
2076
|
+
table: flowData.table,
|
|
2077
|
+
has_snapshot: snapshotExists,
|
|
2078
|
+
has_trigger: triggerExists,
|
|
2079
|
+
verification_attempt: attempt,
|
|
2080
|
+
verification_method: 'multi_table_comprehensive',
|
|
2081
|
+
completeness_score: (flowExists ? 1 : 0) + (snapshotExists ? 1 : 0) + (triggerExists ? 1 : 0),
|
|
2082
|
+
url: `https://${await this.getInstanceUrl()}/flow-designer/flow/${flowData.sys_id}`
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
// Log partial results for debugging
|
|
2086
|
+
this.logger.warn(`Verification attempt ${attempt}: Flow=${flowExists}, Snapshot=${snapshotExists}, Trigger=${triggerExists}`);
|
|
2087
|
+
// If this is the last attempt, return detailed failure info
|
|
2088
|
+
if (attempt === maxRetries) {
|
|
2089
|
+
return {
|
|
2090
|
+
verified: false,
|
|
2091
|
+
reason: 'Flow not found after comprehensive verification',
|
|
2092
|
+
attempts: maxRetries,
|
|
2093
|
+
partial_results: {
|
|
2094
|
+
flow_found: flowExists,
|
|
2095
|
+
snapshot_found: snapshotExists,
|
|
2096
|
+
trigger_found: triggerExists
|
|
2097
|
+
},
|
|
2098
|
+
search_query: `name=${flowName}`,
|
|
2099
|
+
recommendation: 'Check ServiceNow Flow Designer manually or verify deployment was successful'
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
catch (error) {
|
|
2104
|
+
this.logger.warn(`Verification attempt ${attempt} failed:`, error);
|
|
2105
|
+
// If this is the last attempt, include error details
|
|
2106
|
+
if (attempt === maxRetries) {
|
|
2107
|
+
return {
|
|
2108
|
+
verified: false,
|
|
2109
|
+
reason: 'Verification failed due to error',
|
|
2110
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2111
|
+
attempts: maxRetries
|
|
2112
|
+
};
|
|
2003
2113
|
}
|
|
2004
|
-
});
|
|
2005
|
-
if (flowCheck.result && flowCheck.result.length > 0) {
|
|
2006
|
-
return flowCheck.result[0];
|
|
2007
2114
|
}
|
|
2008
|
-
return null;
|
|
2009
2115
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2116
|
+
return {
|
|
2117
|
+
verified: false,
|
|
2118
|
+
reason: 'Max verification attempts exceeded',
|
|
2119
|
+
attempts: maxRetries
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Helper to get instance URL for flow links
|
|
2124
|
+
*/
|
|
2125
|
+
async getInstanceUrl() {
|
|
2126
|
+
try {
|
|
2127
|
+
const credentials = await this.oauth.loadCredentials();
|
|
2128
|
+
return credentials?.instance || 'your-instance.service-now.com';
|
|
2013
2129
|
}
|
|
2130
|
+
catch {
|
|
2131
|
+
return 'your-instance.service-now.com';
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Sleep utility
|
|
2136
|
+
*/
|
|
2137
|
+
sleep(ms) {
|
|
2138
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2014
2139
|
}
|
|
2015
2140
|
/**
|
|
2016
2141
|
* Generate manual import guide for failed deployments
|
|
@@ -43,15 +43,43 @@ class MemoryClient {
|
|
|
43
43
|
await this.operations.setContext(this.sessionId, options.key, options.value, this.agentId, options.expires, options.permissions);
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* Retrieve data from shared context
|
|
46
|
+
* Retrieve data from shared context with agent isolation
|
|
47
47
|
*/
|
|
48
48
|
async retrieve(options) {
|
|
49
|
-
|
|
49
|
+
// CRITICAL FIX: Pass agent ID for proper memory isolation
|
|
50
|
+
const context = await this.operations.getContext(this.sessionId, options.key, this.agentId);
|
|
50
51
|
if (!context) {
|
|
51
52
|
return options.defaultValue !== undefined ? options.defaultValue : null;
|
|
52
53
|
}
|
|
53
54
|
return context.context_value;
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Retrieve data from truly shared context (no agent isolation)
|
|
58
|
+
* Use this when agents need to access shared coordination data
|
|
59
|
+
*/
|
|
60
|
+
async retrieveShared(options) {
|
|
61
|
+
// Try shared prefix first
|
|
62
|
+
const sharedKey = `__shared__::${options.key}`;
|
|
63
|
+
let context = await this.operations.getContext(this.sessionId, sharedKey);
|
|
64
|
+
// Fallback to original key for backward compatibility
|
|
65
|
+
if (!context) {
|
|
66
|
+
context = await this.operations.getContext(this.sessionId, options.key);
|
|
67
|
+
}
|
|
68
|
+
if (!context) {
|
|
69
|
+
return options.defaultValue !== undefined ? options.defaultValue : null;
|
|
70
|
+
}
|
|
71
|
+
return context.context_value;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Store data in truly shared context (no agent isolation)
|
|
75
|
+
* Use this when you want all agents to access the same data
|
|
76
|
+
*/
|
|
77
|
+
async storeShared(options) {
|
|
78
|
+
// Use a special shared prefix to avoid conflicts with namespaced keys
|
|
79
|
+
const sharedKey = `__shared__::${options.key}`;
|
|
80
|
+
await this.operations.setContext(this.sessionId, sharedKey, options.value, this.agentId, // Still track who created it
|
|
81
|
+
options.expires, options.permissions);
|
|
82
|
+
}
|
|
55
83
|
/**
|
|
56
84
|
* Get all context for current session
|
|
57
85
|
*/
|
|
@@ -278,23 +278,49 @@ class MemoryOperations {
|
|
|
278
278
|
}
|
|
279
279
|
// ==================== Shared Context Operations ====================
|
|
280
280
|
/**
|
|
281
|
-
*
|
|
281
|
+
* Create agent-specific namespaced key for memory isolation
|
|
282
|
+
* This prevents agents from overwriting each other's memory within the same session
|
|
283
|
+
*/
|
|
284
|
+
createNamespacedKey(context_key, agent_id) {
|
|
285
|
+
// Use a delimiter that's unlikely to conflict with normal keys
|
|
286
|
+
return `${agent_id}::${context_key}`;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Parse namespaced key to extract original key and agent ID
|
|
290
|
+
*/
|
|
291
|
+
parseNamespacedKey(namespaced_key) {
|
|
292
|
+
const parts = namespaced_key.split('::');
|
|
293
|
+
if (parts.length === 2) {
|
|
294
|
+
return { agent_id: parts[0], original_key: parts[1] };
|
|
295
|
+
}
|
|
296
|
+
return { agent_id: null, original_key: namespaced_key };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Store shared context with agent isolation
|
|
282
300
|
*/
|
|
283
301
|
async setContext(session_id, context_key, context_value, created_by_agent, expires_at, access_permissions) {
|
|
284
302
|
try {
|
|
303
|
+
// CRITICAL FIX: Create agent-specific namespace for the context key
|
|
304
|
+
// This ensures agents don't overwrite each other's memory
|
|
305
|
+
const namespacedKey = this.createNamespacedKey(context_key, created_by_agent);
|
|
285
306
|
this.memory.run(`
|
|
286
307
|
INSERT OR REPLACE INTO shared_context
|
|
287
308
|
(session_id, context_key, context_value, created_by_agent, expires_at, access_permissions)
|
|
288
309
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
289
310
|
`, [
|
|
290
311
|
session_id,
|
|
291
|
-
|
|
312
|
+
namespacedKey,
|
|
292
313
|
typeof context_value === 'string' ? context_value : JSON.stringify(context_value),
|
|
293
314
|
created_by_agent,
|
|
294
315
|
expires_at ? expires_at.toISOString() : null,
|
|
295
316
|
access_permissions ? JSON.stringify(access_permissions) : null
|
|
296
317
|
]);
|
|
297
|
-
this.logger.debug('Context stored
|
|
318
|
+
this.logger.debug('Context stored with agent isolation', {
|
|
319
|
+
session_id,
|
|
320
|
+
original_key: context_key,
|
|
321
|
+
namespaced_key: namespacedKey,
|
|
322
|
+
created_by_agent
|
|
323
|
+
});
|
|
298
324
|
}
|
|
299
325
|
catch (error) {
|
|
300
326
|
this.logger.error('Failed to store context', error);
|
|
@@ -302,18 +328,43 @@ class MemoryOperations {
|
|
|
302
328
|
}
|
|
303
329
|
}
|
|
304
330
|
/**
|
|
305
|
-
* Get shared context
|
|
331
|
+
* Get shared context with agent isolation support
|
|
306
332
|
*/
|
|
307
|
-
async getContext(session_id, context_key) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
333
|
+
async getContext(session_id, context_key, requesting_agent) {
|
|
334
|
+
// CRITICAL FIX: Try to get agent-specific namespaced key first
|
|
335
|
+
let result = null;
|
|
336
|
+
if (requesting_agent) {
|
|
337
|
+
const namespacedKey = this.createNamespacedKey(context_key, requesting_agent);
|
|
338
|
+
result = this.memory.get(`
|
|
339
|
+
SELECT * FROM shared_context
|
|
340
|
+
WHERE session_id = ? AND context_key = ?
|
|
341
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
342
|
+
`, [session_id, namespacedKey]);
|
|
343
|
+
this.logger.debug('Attempting namespaced context retrieval', {
|
|
344
|
+
session_id,
|
|
345
|
+
original_key: context_key,
|
|
346
|
+
namespaced_key: namespacedKey,
|
|
347
|
+
requesting_agent,
|
|
348
|
+
found: !!result
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// Fallback: Try to get the original key for backward compatibility or shared data
|
|
352
|
+
if (!result) {
|
|
353
|
+
result = this.memory.get(`
|
|
354
|
+
SELECT * FROM shared_context
|
|
355
|
+
WHERE session_id = ? AND context_key = ?
|
|
356
|
+
AND (expires_at IS NULL OR expires_at > datetime('now'))
|
|
357
|
+
`, [session_id, context_key]);
|
|
358
|
+
this.logger.debug('Fallback context retrieval', {
|
|
359
|
+
session_id,
|
|
360
|
+
context_key,
|
|
361
|
+
found: !!result
|
|
362
|
+
});
|
|
363
|
+
}
|
|
313
364
|
if (result) {
|
|
314
365
|
try {
|
|
315
366
|
// Try to parse JSON values
|
|
316
|
-
if (result.context_value && result.context_value.startsWith('{') || result.context_value.startsWith('[')) {
|
|
367
|
+
if (result.context_value && (result.context_value.startsWith('{') || result.context_value.startsWith('['))) {
|
|
317
368
|
result.context_value = JSON.parse(result.context_value);
|
|
318
369
|
}
|
|
319
370
|
if (result.access_permissions) {
|
|
@@ -322,6 +373,7 @@ class MemoryOperations {
|
|
|
322
373
|
}
|
|
323
374
|
catch (e) {
|
|
324
375
|
// If parsing fails, return as-is
|
|
376
|
+
this.logger.warn('Failed to parse stored JSON context', { context_key, error: e });
|
|
325
377
|
}
|
|
326
378
|
}
|
|
327
379
|
return result;
|
|
@@ -38,17 +38,27 @@ class ServiceNowClient {
|
|
|
38
38
|
const method = (config.method || 'GET').toLowerCase();
|
|
39
39
|
const url = config.url || config.endpoint;
|
|
40
40
|
const data = config.data || config.body;
|
|
41
|
+
// CRITICAL FIX: Properly merge headers to allow content-type overrides for XML requests
|
|
42
|
+
const requestConfig = {
|
|
43
|
+
...config,
|
|
44
|
+
headers: {
|
|
45
|
+
...this.client.defaults.headers.common,
|
|
46
|
+
...this.client.defaults.headers[method],
|
|
47
|
+
...config.headers // This ensures custom headers (like Content-Type: application/xml) override defaults
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
this.logger.debug('🔧 Final request config headers:', requestConfig.headers);
|
|
41
51
|
switch (method) {
|
|
42
52
|
case 'get':
|
|
43
|
-
return this.client.get(url, { params: config.params, ...
|
|
53
|
+
return this.client.get(url, { params: config.params, ...requestConfig });
|
|
44
54
|
case 'post':
|
|
45
|
-
return this.client.post(url, data,
|
|
55
|
+
return this.client.post(url, data, requestConfig);
|
|
46
56
|
case 'put':
|
|
47
|
-
return this.client.put(url, data,
|
|
57
|
+
return this.client.put(url, data, requestConfig);
|
|
48
58
|
case 'patch':
|
|
49
|
-
return this.client.patch(url, data,
|
|
59
|
+
return this.client.patch(url, data, requestConfig);
|
|
50
60
|
case 'delete':
|
|
51
|
-
return this.client.delete(url,
|
|
61
|
+
return this.client.delete(url, requestConfig);
|
|
52
62
|
default:
|
|
53
63
|
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
54
64
|
}
|
|
@@ -289,9 +299,19 @@ class ServiceNowClient {
|
|
|
289
299
|
};
|
|
290
300
|
try {
|
|
291
301
|
const createResponse = await this.client.post(`${this.getBaseUrl()}/api/now/table/sp_widget`, testWidget);
|
|
302
|
+
// CRITICAL FIX: Add null safety for response processing
|
|
303
|
+
if (!createResponse || !createResponse.data || !createResponse.data.result || !createResponse.data.result.sys_id) {
|
|
304
|
+
throw new Error('Widget creation succeeded but response structure is unexpected - unable to verify sys_id');
|
|
305
|
+
}
|
|
292
306
|
const sys_id = createResponse.data.result.sys_id;
|
|
293
|
-
// Immediately delete the test widget
|
|
294
|
-
|
|
307
|
+
// Immediately delete the test widget with error handling
|
|
308
|
+
try {
|
|
309
|
+
await this.client.delete(`${this.getBaseUrl()}/api/now/table/sp_widget/${sys_id}`);
|
|
310
|
+
}
|
|
311
|
+
catch (deleteError) {
|
|
312
|
+
this.logger.warn(`Test widget created but cleanup failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`);
|
|
313
|
+
// Don't fail the test just because cleanup failed
|
|
314
|
+
}
|
|
295
315
|
return { success: true, data: { message: 'Widget write permissions confirmed' } };
|
|
296
316
|
}
|
|
297
317
|
catch (error) {
|
|
@@ -304,7 +324,19 @@ class ServiceNowClient {
|
|
|
304
324
|
description: 'Check user roles and permissions',
|
|
305
325
|
test: async () => {
|
|
306
326
|
const response = await this.client.get(`${this.getBaseUrl()}/api/now/table/sys_user_role?sysparm_query=user=javascript:gs.getUserID()`);
|
|
307
|
-
|
|
327
|
+
// CRITICAL FIX: Add null safety for response processing
|
|
328
|
+
if (!response || !response.data || !response.data.result) {
|
|
329
|
+
return { success: true, data: { roles: [], warning: 'Unable to retrieve user roles - response structure unexpected' } };
|
|
330
|
+
}
|
|
331
|
+
// Safely process roles with null checks
|
|
332
|
+
const roles = Array.isArray(response.data.result)
|
|
333
|
+
? response.data.result.map((r) => {
|
|
334
|
+
if (!r || typeof r !== 'object')
|
|
335
|
+
return 'Unknown Role';
|
|
336
|
+
return r.role?.display_value || r.role || 'Unknown Role';
|
|
337
|
+
}).filter(role => role && role !== 'Unknown Role')
|
|
338
|
+
: [];
|
|
339
|
+
return { success: true, data: { roles } };
|
|
308
340
|
}
|
|
309
341
|
}
|
|
310
342
|
];
|
|
@@ -330,17 +362,27 @@ class ServiceNowClient {
|
|
|
330
362
|
};
|
|
331
363
|
}
|
|
332
364
|
}
|
|
333
|
-
// Analyze results
|
|
334
|
-
const failedTests =
|
|
335
|
-
|
|
365
|
+
// Analyze results with null safety
|
|
366
|
+
const failedTests = diagnostics.tests && typeof diagnostics.tests === 'object'
|
|
367
|
+
? Object.values(diagnostics.tests).filter((test) => test && test.status && test.status.includes('FAIL'))
|
|
368
|
+
: [];
|
|
369
|
+
const passedTests = diagnostics.tests && typeof diagnostics.tests === 'object'
|
|
370
|
+
? Object.values(diagnostics.tests).filter((test) => test && test.status && test.status.includes('PASS'))
|
|
371
|
+
: [];
|
|
336
372
|
diagnostics.summary = {
|
|
337
|
-
total_tests: tests.length,
|
|
338
|
-
passed: passedTests.length,
|
|
339
|
-
failed: failedTests.length,
|
|
340
|
-
overall_status: failedTests.length === 0 ? '✅ ALL SYSTEMS GO' : '⚠️ ISSUES DETECTED'
|
|
373
|
+
total_tests: tests.length || 0,
|
|
374
|
+
passed: passedTests.length || 0,
|
|
375
|
+
failed: failedTests.length || 0,
|
|
376
|
+
overall_status: failedTests.length === 0 && passedTests.length > 0 ? '✅ ALL SYSTEMS GO' : '⚠️ ISSUES DETECTED'
|
|
341
377
|
};
|
|
342
|
-
// Generate recommendations
|
|
343
|
-
|
|
378
|
+
// Generate recommendations with null safety
|
|
379
|
+
try {
|
|
380
|
+
diagnostics.recommendations = this.generateAuthRecommendations(diagnostics.tests);
|
|
381
|
+
}
|
|
382
|
+
catch (recError) {
|
|
383
|
+
this.logger.error('Error generating recommendations:', recError);
|
|
384
|
+
diagnostics.recommendations = ['⚠️ Unable to generate recommendations due to error'];
|
|
385
|
+
}
|
|
344
386
|
return {
|
|
345
387
|
success: failedTests.length === 0,
|
|
346
388
|
data: diagnostics,
|
|
@@ -359,24 +401,51 @@ class ServiceNowClient {
|
|
|
359
401
|
*/
|
|
360
402
|
generateAuthRecommendations(tests) {
|
|
361
403
|
const recommendations = [];
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
404
|
+
// CRITICAL FIX: Add comprehensive null safety checks
|
|
405
|
+
if (!tests || typeof tests !== 'object') {
|
|
406
|
+
recommendations.push('⚠️ Unable to analyze test results due to missing test data');
|
|
407
|
+
return recommendations;
|
|
408
|
+
}
|
|
409
|
+
// Safe check for Widget Write Test
|
|
410
|
+
const widgetTest = tests['Widget Write Test'];
|
|
411
|
+
if (widgetTest?.status && typeof widgetTest.status === 'string' && widgetTest.status.includes('FAIL')) {
|
|
412
|
+
const error = widgetTest.error;
|
|
413
|
+
if (error && typeof error === 'string') {
|
|
414
|
+
if (error.includes('403')) {
|
|
415
|
+
recommendations.push('🔐 Widget deployment failed with 403 Forbidden. Check OAuth scopes: ensure \'useraccount\' and \'glide_system_administration\' scopes are enabled');
|
|
416
|
+
recommendations.push('👤 Verify user has sp_portal_manager or admin role in ServiceNow');
|
|
417
|
+
recommendations.push('🛡️ Check if instance has deployment restrictions for external applications');
|
|
418
|
+
}
|
|
419
|
+
if (error.includes('401')) {
|
|
420
|
+
recommendations.push('🔑 Authentication failed. Re-run: snow-flow auth login');
|
|
421
|
+
}
|
|
368
422
|
}
|
|
369
|
-
|
|
370
|
-
recommendations.push('
|
|
423
|
+
else {
|
|
424
|
+
recommendations.push('🔐 Widget write test failed with unknown error. Check ServiceNow permissions');
|
|
371
425
|
}
|
|
372
426
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
427
|
+
// Safe check for User Role Check
|
|
428
|
+
const roleTest = tests['User Role Check'];
|
|
429
|
+
if (roleTest?.status && typeof roleTest.status === 'string' && roleTest.status.includes('PASS')) {
|
|
430
|
+
const roles = roleTest.result?.roles;
|
|
431
|
+
if (Array.isArray(roles)) {
|
|
432
|
+
const hasRequiredRole = roles.some((role) => {
|
|
433
|
+
if (typeof role === 'string') {
|
|
434
|
+
return role.includes('admin') || role.includes('sp_portal');
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
});
|
|
438
|
+
if (!hasRequiredRole) {
|
|
439
|
+
recommendations.push('⚠️ User lacks admin or portal management roles. Contact ServiceNow admin to assign appropriate roles');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
recommendations.push('⚠️ Unable to verify user roles. Check if user has admin or portal management permissions');
|
|
377
444
|
}
|
|
378
445
|
}
|
|
379
|
-
|
|
446
|
+
// Safe check for Update Set Access
|
|
447
|
+
const updateSetTest = tests['Update Set Access'];
|
|
448
|
+
if (updateSetTest?.status && typeof updateSetTest.status === 'string' && updateSetTest.status.includes('FAIL')) {
|
|
380
449
|
recommendations.push('📦 Update Set access failed. Ensure user has update_set_manager or admin role');
|
|
381
450
|
}
|
|
382
451
|
if (recommendations.length === 0) {
|
|
@@ -102,6 +102,28 @@ class XMLFirstFlowGenerator {
|
|
|
102
102
|
.replace(/"/g, '"')
|
|
103
103
|
.replace(/'/g, ''');
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Escape JSON content for XML serialization (no CDATA needed)
|
|
107
|
+
* CRITICAL FIX: Properly handle JSON content in XML to avoid parsing errors
|
|
108
|
+
*/
|
|
109
|
+
escapeForXml(jsonContent) {
|
|
110
|
+
if (!jsonContent)
|
|
111
|
+
return '';
|
|
112
|
+
// First escape XML special characters
|
|
113
|
+
let escaped = jsonContent
|
|
114
|
+
.replace(/&/g, '&')
|
|
115
|
+
.replace(/</g, '<')
|
|
116
|
+
.replace(/>/g, '>')
|
|
117
|
+
.replace(/"/g, '"')
|
|
118
|
+
.replace(/'/g, ''');
|
|
119
|
+
// Additional escaping for problematic sequences that could break XML parsing
|
|
120
|
+
escaped = escaped
|
|
121
|
+
.replace(/]]>/g, ']]>') // Escape CDATA end sequences
|
|
122
|
+
.replace(/\r?\n/g, ' ') // Preserve line breaks as XML entities
|
|
123
|
+
.replace(/\r/g, ' ') // Preserve carriage returns
|
|
124
|
+
.replace(/\t/g, '	'); // Preserve tabs
|
|
125
|
+
return escaped;
|
|
126
|
+
}
|
|
105
127
|
/**
|
|
106
128
|
* Generate complete Update Set XML for a flow
|
|
107
129
|
* Based on REAL ServiceNow XML structure research
|
|
@@ -157,7 +179,7 @@ class XMLFirstFlowGenerator {
|
|
|
157
179
|
<action>INSERT_OR_UPDATE</action>
|
|
158
180
|
<application>global</application>
|
|
159
181
|
<name>sys_hub_flow_snapshot_${snapshotSysId}</name>
|
|
160
|
-
<payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_flow_snapshot"><sys_hub_flow_snapshot action="INSERT_OR_UPDATE"><sys_id>${snapshotSysId}</sys_id><name>${this.escapeXml(flowDef.name)}</name><flow>${this.flowSysId}</flow><note>Initial version</note><snapshot
|
|
182
|
+
<payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_flow_snapshot"><sys_hub_flow_snapshot action="INSERT_OR_UPDATE"><sys_id>${snapshotSysId}</sys_id><name>${this.escapeXml(flowDef.name)}</name><flow>${this.flowSysId}</flow><note>Initial version</note><snapshot>${this.escapeForXml(JSON.stringify(flowDefinitionJson, null, 2))}</snapshot><sys_created_by>admin</sys_created_by><sys_created_on>${timestamp}</sys_created_on></sys_hub_flow_snapshot></record_update>]]></payload>
|
|
161
183
|
<remote_update_set>${updateSetSysId}</remote_update_set>
|
|
162
184
|
<source_table>sys_hub_flow_snapshot</source_table>
|
|
163
185
|
<type>Flow Designer Snapshot</type>
|
|
@@ -339,7 +361,7 @@ class XMLFirstFlowGenerator {
|
|
|
339
361
|
<action>INSERT_OR_UPDATE</action>
|
|
340
362
|
<application>global</application>
|
|
341
363
|
<name>sys_hub_action_instance_${sysIds[index]}</name>
|
|
342
|
-
<payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_action_instance"><sys_hub_action_instance action="INSERT_OR_UPDATE"><sys_id>${sysIds[index]}</sys_id><flow>${this.flowSysId}</flow><action_type display_value="">${this.getActionTypeId(activity.type)}</action_type><name>${this.escapeXml(activity.name)}</name><order>${activity.order || (index + 1) * 100}</order><active>true</active><inputs
|
|
364
|
+
<payload><![CDATA[<?xml version="1.0" encoding="UTF-8"?><record_update table="sys_hub_action_instance"><sys_hub_action_instance action="INSERT_OR_UPDATE"><sys_id>${sysIds[index]}</sys_id><flow>${this.flowSysId}</flow><action_type display_value="">${this.getActionTypeId(activity.type)}</action_type><name>${this.escapeXml(activity.name)}</name><order>${activity.order || (index + 1) * 100}</order><active>true</active><inputs>${this.escapeForXml(JSON.stringify(activity.inputs))}</inputs><outputs>${this.escapeForXml(JSON.stringify(activity.outputs || {}))}</outputs><condition>${this.escapeXml(activity.condition || '')}</condition><comment_text/><sys_class_name>sys_hub_action_instance</sys_class_name><sys_created_by>admin</sys_created_by><sys_created_on>${timestamp}</sys_created_on><sys_domain>global</sys_domain><sys_domain_path>/</sys_domain_path></sys_hub_action_instance></record_update>]]></payload>
|
|
343
365
|
<remote_update_set>${updateSetSysId}</remote_update_set>
|
|
344
366
|
<source_table>sys_hub_action_instance</source_table>
|
|
345
367
|
<type>Flow Designer Action</type>
|
package/dist/version.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.VERSION_INFO = exports.VERSION = void 0;
|
|
|
7
7
|
exports.getVersionString = getVersionString;
|
|
8
8
|
exports.getLatestFeatures = getLatestFeatures;
|
|
9
9
|
exports.isLatestVersion = isLatestVersion;
|
|
10
|
-
exports.VERSION = '1.3.
|
|
10
|
+
exports.VERSION = '1.3.23';
|
|
11
11
|
exports.VERSION_INFO = {
|
|
12
12
|
version: exports.VERSION,
|
|
13
13
|
name: 'Snow-Flow',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snow-flow",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.23",
|
|
4
4
|
"description": "ServiceNow Queen Agent - Hive-Mind Intelligence for ServiceNow Development inspired by claude-flow. Transform complex workflows into elegant one-command orchestration.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "commonjs",
|