snow-flow 1.3.22 โ 1.3.24
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/base-mcp-server.js +183 -15
- package/dist/mcp/servicenow-deployment-mcp.js +258 -9
- package/dist/mcp/servicenow-flow-composer-mcp.js +218 -32
- package/dist/mcp/servicenow-intelligent-mcp.js +354 -25
- package/dist/mcp/servicenow-security-compliance-mcp-refactored.js +9 -9
- package/dist/memory/memory-client.js +30 -2
- package/dist/memory/memory-operations.js +63 -11
- package/dist/memory/memory-system.js +89 -17
- package/dist/utils/servicenow-client.js +178 -48
- package/dist/utils/xml-first-flow-generator.js +24 -2
- package/dist/version.js +1 -1
- package/npm-publish-summary.txt +35 -0
- package/package.json +1 -1
|
@@ -417,34 +417,106 @@ class MemorySystem extends events_1.EventEmitter {
|
|
|
417
417
|
await this.store('_schema_version', { version: targetVersion });
|
|
418
418
|
this.logger.info(`Database migrated to version ${targetVersion}`);
|
|
419
419
|
}
|
|
420
|
+
// ๐ด SNOW-003 FIX: More aggressive cleanup to prevent memory exhaustion
|
|
420
421
|
startCleanupTimer() {
|
|
421
|
-
// Run cleanup every hour
|
|
422
|
+
// ๐ด CRITICAL: Run cleanup every 15 minutes instead of 1 hour
|
|
422
423
|
this.cleanupTimer = setInterval(async () => {
|
|
423
424
|
await this.cleanup();
|
|
424
|
-
},
|
|
425
|
+
}, 900000); // 15 minutes
|
|
426
|
+
// ๐ด CRITICAL: Additional memory pressure cleanup every 5 minutes
|
|
427
|
+
setInterval(async () => {
|
|
428
|
+
await this.memoryPressureCleanup();
|
|
429
|
+
}, 300000); // 5 minutes
|
|
425
430
|
}
|
|
431
|
+
// ๐ด SNOW-003 FIX: Enhanced cleanup with memory pressure monitoring
|
|
426
432
|
async cleanup() {
|
|
427
433
|
if (!this.db)
|
|
428
434
|
return;
|
|
429
435
|
const now = Date.now();
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
436
|
+
const startTime = Date.now();
|
|
437
|
+
try {
|
|
438
|
+
// ๐ด CRITICAL: Clean expired memory entries with better logging
|
|
439
|
+
const stmt = this.db.prepare('DELETE FROM memory_store WHERE expires_at < ?');
|
|
440
|
+
const result = stmt.run(now);
|
|
441
|
+
if (result.changes > 0) {
|
|
442
|
+
this.logger.info(`๐งน Cleaned up ${result.changes} expired memory entries`);
|
|
443
|
+
}
|
|
444
|
+
// ๐ด CRITICAL: Clean cache with size monitoring
|
|
445
|
+
let cacheCleanedCount = 0;
|
|
446
|
+
const cacheEntriesBefore = this.cache.size;
|
|
447
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
448
|
+
if (entry.expiresAt < now) {
|
|
449
|
+
this.cache.delete(key);
|
|
450
|
+
cacheCleanedCount++;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (cacheCleanedCount > 0) {
|
|
454
|
+
this.logger.info(`๐งน Cleaned ${cacheCleanedCount} expired cache entries (${cacheEntriesBefore} โ ${this.cache.size})`);
|
|
455
|
+
}
|
|
456
|
+
// ๐ด CRITICAL: More frequent vacuum for high activity
|
|
457
|
+
const lastVacuum = await this.get('_last_vacuum');
|
|
458
|
+
const shouldVacuum = !lastVacuum || now - lastVacuum > 21600000; // 6 hours instead of 24
|
|
459
|
+
if (shouldVacuum) {
|
|
460
|
+
this.logger.info('๐๏ธ Starting database vacuum...');
|
|
461
|
+
this.db.exec('VACUUM');
|
|
462
|
+
await this.store('_last_vacuum', now);
|
|
463
|
+
this.logger.info(`โ
Database vacuumed in ${Date.now() - startTime}ms`);
|
|
464
|
+
}
|
|
465
|
+
// ๐ด CRITICAL: Log memory stats after cleanup
|
|
466
|
+
const memUsage = process.memoryUsage();
|
|
467
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
468
|
+
if (heapUsedMB > 100) { // Log if significant memory usage
|
|
469
|
+
this.logger.info(`๐ Memory after cleanup: ${heapUsedMB}MB heap, ${this.cache.size} cache entries`);
|
|
470
|
+
}
|
|
435
471
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
472
|
+
catch (error) {
|
|
473
|
+
this.logger.error('โ Cleanup failed:', error);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* ๐ด SNOW-003 FIX: Emergency cleanup when memory pressure is high
|
|
478
|
+
*/
|
|
479
|
+
async memoryPressureCleanup() {
|
|
480
|
+
if (!this.db)
|
|
481
|
+
return;
|
|
482
|
+
try {
|
|
483
|
+
const memUsage = process.memoryUsage();
|
|
484
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
485
|
+
const cacheSize = this.cache.size;
|
|
486
|
+
// ๐ด CRITICAL: Trigger aggressive cleanup if memory is high
|
|
487
|
+
if (heapUsedMB > 300 || cacheSize > 1000) {
|
|
488
|
+
this.logger.warn(`โ ๏ธ High memory pressure detected: ${heapUsedMB}MB heap, ${cacheSize} cache entries`);
|
|
489
|
+
// 1. Aggressive cache cleanup - remove 50% of oldest entries
|
|
490
|
+
if (cacheSize > 500) {
|
|
491
|
+
const entries = Array.from(this.cache.entries())
|
|
492
|
+
.sort((a, b) => a[1].expiresAt - b[1].expiresAt);
|
|
493
|
+
const toRemove = Math.floor(entries.length * 0.5);
|
|
494
|
+
for (let i = 0; i < toRemove; i++) {
|
|
495
|
+
this.cache.delete(entries[i][0]);
|
|
496
|
+
}
|
|
497
|
+
this.logger.info(`๐งน Emergency cache cleanup: removed ${toRemove} entries (${cacheSize} โ ${this.cache.size})`);
|
|
498
|
+
}
|
|
499
|
+
// 2. Clean old database entries more aggressively
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
const aggressiveCleanupTime = now - (7 * 24 * 60 * 60 * 1000); // 7 days old
|
|
502
|
+
const aggressiveStmt = this.db.prepare('DELETE FROM memory_store WHERE created_at < ? AND expires_at IS NULL');
|
|
503
|
+
const aggressiveResult = aggressiveStmt.run(aggressiveCleanupTime);
|
|
504
|
+
if (aggressiveResult.changes > 0) {
|
|
505
|
+
this.logger.info(`๐งน Emergency database cleanup: removed ${aggressiveResult.changes} old entries`);
|
|
506
|
+
}
|
|
507
|
+
// 3. Force garbage collection if available
|
|
508
|
+
if (global.gc) {
|
|
509
|
+
this.logger.info('๐๏ธ Forcing garbage collection due to memory pressure');
|
|
510
|
+
global.gc();
|
|
511
|
+
// Check memory after GC
|
|
512
|
+
const afterGC = process.memoryUsage();
|
|
513
|
+
const afterGCMB = Math.round(afterGC.heapUsed / 1024 / 1024);
|
|
514
|
+
this.logger.info(`๐ Memory after GC: ${heapUsedMB}MB โ ${afterGCMB}MB`);
|
|
515
|
+
}
|
|
440
516
|
}
|
|
441
517
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (!lastVacuum || now - lastVacuum > 86400000) { // 24 hours
|
|
445
|
-
this.db.exec('VACUUM');
|
|
446
|
-
await this.store('_last_vacuum', now);
|
|
447
|
-
this.logger.info('Database vacuumed');
|
|
518
|
+
catch (error) {
|
|
519
|
+
this.logger.error('โ Memory pressure cleanup failed:', error);
|
|
448
520
|
}
|
|
449
521
|
}
|
|
450
522
|
checkCacheSize() {
|
|
@@ -20,6 +20,9 @@ const flow_structure_builder_1 = require("./flow-structure-builder");
|
|
|
20
20
|
class ServiceNowClient {
|
|
21
21
|
constructor() {
|
|
22
22
|
this.credentials = null;
|
|
23
|
+
// ๐ด SNOW-003 FIX: Token refresh synchronization to prevent race conditions
|
|
24
|
+
this.tokenRefreshPromise = null;
|
|
25
|
+
this.lastTokenRefresh = 0;
|
|
23
26
|
this.logger = new logger_1.Logger('ServiceNowClient');
|
|
24
27
|
this.oauth = new snow_oauth_1.ServiceNowOAuth();
|
|
25
28
|
this.client = axios_1.default.create({
|
|
@@ -38,17 +41,27 @@ class ServiceNowClient {
|
|
|
38
41
|
const method = (config.method || 'GET').toLowerCase();
|
|
39
42
|
const url = config.url || config.endpoint;
|
|
40
43
|
const data = config.data || config.body;
|
|
44
|
+
// CRITICAL FIX: Properly merge headers to allow content-type overrides for XML requests
|
|
45
|
+
const requestConfig = {
|
|
46
|
+
...config,
|
|
47
|
+
headers: {
|
|
48
|
+
...this.client.defaults.headers.common,
|
|
49
|
+
...this.client.defaults.headers[method],
|
|
50
|
+
...config.headers // This ensures custom headers (like Content-Type: application/xml) override defaults
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
this.logger.debug('๐ง Final request config headers:', requestConfig.headers);
|
|
41
54
|
switch (method) {
|
|
42
55
|
case 'get':
|
|
43
|
-
return this.client.get(url, { params: config.params, ...
|
|
56
|
+
return this.client.get(url, { params: config.params, ...requestConfig });
|
|
44
57
|
case 'post':
|
|
45
|
-
return this.client.post(url, data,
|
|
58
|
+
return this.client.post(url, data, requestConfig);
|
|
46
59
|
case 'put':
|
|
47
|
-
return this.client.put(url, data,
|
|
60
|
+
return this.client.put(url, data, requestConfig);
|
|
48
61
|
case 'patch':
|
|
49
|
-
return this.client.patch(url, data,
|
|
62
|
+
return this.client.patch(url, data, requestConfig);
|
|
50
63
|
case 'delete':
|
|
51
|
-
return this.client.delete(url,
|
|
64
|
+
return this.client.delete(url, requestConfig);
|
|
52
65
|
default:
|
|
53
66
|
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
54
67
|
}
|
|
@@ -74,26 +87,11 @@ class ServiceNowClient {
|
|
|
74
87
|
// Add response interceptor for error handling with retry logic
|
|
75
88
|
this.client.interceptors.response.use((response) => response, async (error) => {
|
|
76
89
|
const originalRequest = error.config;
|
|
77
|
-
//
|
|
90
|
+
// ๐ด SNOW-003 FIX: Prevent token refresh race conditions
|
|
78
91
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
79
92
|
originalRequest._retry = true;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (refreshResult.success && refreshResult.accessToken) {
|
|
83
|
-
this.logger.info('โ
Token refreshed successfully, retrying request...');
|
|
84
|
-
// Update local credentials
|
|
85
|
-
if (this.credentials) {
|
|
86
|
-
this.credentials.accessToken = refreshResult.accessToken;
|
|
87
|
-
}
|
|
88
|
-
// Update the authorization header with new token
|
|
89
|
-
originalRequest.headers['Authorization'] = `Bearer ${refreshResult.accessToken}`;
|
|
90
|
-
// Retry the original request
|
|
91
|
-
return this.client.request(originalRequest);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
console.error('โ Token refresh failed:', refreshResult.error);
|
|
95
|
-
this.logger.warn('๐ก Please run "snow-flow auth login" to re-authenticate');
|
|
96
|
-
}
|
|
93
|
+
// ๐ด CRITICAL: Use singleton token refresh to prevent multiple concurrent refreshes
|
|
94
|
+
return this.handleTokenRefreshWithLock(originalRequest);
|
|
97
95
|
}
|
|
98
96
|
// Log other errors for debugging
|
|
99
97
|
if (error.response?.status === 403) {
|
|
@@ -111,6 +109,79 @@ class ServiceNowClient {
|
|
|
111
109
|
get credentialsInstance() {
|
|
112
110
|
return this.credentials;
|
|
113
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* ๐ด SNOW-003 FIX: Handle token refresh with locking to prevent concurrent refreshes
|
|
114
|
+
* This prevents cascade failures caused by multiple simultaneous token refresh attempts
|
|
115
|
+
*/
|
|
116
|
+
async handleTokenRefreshWithLock(originalRequest) {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
// ๐ด CRITICAL: If another refresh is in progress, wait for it
|
|
119
|
+
if (this.tokenRefreshPromise) {
|
|
120
|
+
this.logger.info('๐ Token refresh already in progress, waiting...');
|
|
121
|
+
try {
|
|
122
|
+
await this.tokenRefreshPromise;
|
|
123
|
+
// After waiting, check if we have valid credentials now
|
|
124
|
+
if (this.credentials?.accessToken) {
|
|
125
|
+
this.logger.info('โ
Using refreshed token from concurrent refresh');
|
|
126
|
+
originalRequest.headers['Authorization'] = `Bearer ${this.credentials.accessToken}`;
|
|
127
|
+
return this.client.request(originalRequest);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.logger.warn('Concurrent token refresh failed:', error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ๐ด CRITICAL: Rate limit token refresh to prevent excessive API calls
|
|
135
|
+
if (now - this.lastTokenRefresh < 5000) { // 5 second rate limit
|
|
136
|
+
this.logger.warn('โ ๏ธ Token refresh rate limited - too many attempts');
|
|
137
|
+
throw new Error('Token refresh rate limited. Please wait before retrying.');
|
|
138
|
+
}
|
|
139
|
+
// ๐ด CRITICAL: Create new refresh promise with timeout and error handling
|
|
140
|
+
this.tokenRefreshPromise = this.performTokenRefreshWithTimeout();
|
|
141
|
+
this.lastTokenRefresh = now;
|
|
142
|
+
try {
|
|
143
|
+
this.logger.info('๐ Starting token refresh process...');
|
|
144
|
+
const refreshResult = await this.tokenRefreshPromise;
|
|
145
|
+
if (refreshResult.success && refreshResult.accessToken) {
|
|
146
|
+
this.logger.info('โ
Token refreshed successfully, retrying original request...');
|
|
147
|
+
// Update local credentials
|
|
148
|
+
if (this.credentials) {
|
|
149
|
+
this.credentials.accessToken = refreshResult.accessToken;
|
|
150
|
+
this.credentials.expiresAt = refreshResult.expiresAt;
|
|
151
|
+
}
|
|
152
|
+
// Update the authorization header with new token
|
|
153
|
+
originalRequest.headers['Authorization'] = `Bearer ${refreshResult.accessToken}`;
|
|
154
|
+
// Clear the promise since we're done
|
|
155
|
+
this.tokenRefreshPromise = null;
|
|
156
|
+
// Retry the original request
|
|
157
|
+
return this.client.request(originalRequest);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Clear the promise on failure
|
|
161
|
+
this.tokenRefreshPromise = null;
|
|
162
|
+
const errorMsg = `Token refresh failed: ${refreshResult.error || 'Unknown error'}`;
|
|
163
|
+
this.logger.error('โ ' + errorMsg);
|
|
164
|
+
console.error('๐ก Please run "snow-flow auth login" to re-authenticate');
|
|
165
|
+
throw new Error(errorMsg);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
// Clear the promise on any error
|
|
170
|
+
this.tokenRefreshPromise = null;
|
|
171
|
+
this.logger.error('๐ด Token refresh process failed:', error);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* ๐ด SNOW-003 FIX: Token refresh with timeout to prevent hanging requests
|
|
177
|
+
*/
|
|
178
|
+
async performTokenRefreshWithTimeout() {
|
|
179
|
+
const timeout = 15000; // 15 second timeout for token refresh
|
|
180
|
+
return Promise.race([
|
|
181
|
+
this.oauth.refreshAccessToken(),
|
|
182
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Token refresh timeout')), timeout)),
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
114
185
|
/**
|
|
115
186
|
* Ensure we have valid authentication with improved error handling
|
|
116
187
|
*/
|
|
@@ -289,9 +360,19 @@ class ServiceNowClient {
|
|
|
289
360
|
};
|
|
290
361
|
try {
|
|
291
362
|
const createResponse = await this.client.post(`${this.getBaseUrl()}/api/now/table/sp_widget`, testWidget);
|
|
363
|
+
// CRITICAL FIX: Add null safety for response processing
|
|
364
|
+
if (!createResponse || !createResponse.data || !createResponse.data.result || !createResponse.data.result.sys_id) {
|
|
365
|
+
throw new Error('Widget creation succeeded but response structure is unexpected - unable to verify sys_id');
|
|
366
|
+
}
|
|
292
367
|
const sys_id = createResponse.data.result.sys_id;
|
|
293
|
-
// Immediately delete the test widget
|
|
294
|
-
|
|
368
|
+
// Immediately delete the test widget with error handling
|
|
369
|
+
try {
|
|
370
|
+
await this.client.delete(`${this.getBaseUrl()}/api/now/table/sp_widget/${sys_id}`);
|
|
371
|
+
}
|
|
372
|
+
catch (deleteError) {
|
|
373
|
+
this.logger.warn(`Test widget created but cleanup failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`);
|
|
374
|
+
// Don't fail the test just because cleanup failed
|
|
375
|
+
}
|
|
295
376
|
return { success: true, data: { message: 'Widget write permissions confirmed' } };
|
|
296
377
|
}
|
|
297
378
|
catch (error) {
|
|
@@ -304,7 +385,19 @@ class ServiceNowClient {
|
|
|
304
385
|
description: 'Check user roles and permissions',
|
|
305
386
|
test: async () => {
|
|
306
387
|
const response = await this.client.get(`${this.getBaseUrl()}/api/now/table/sys_user_role?sysparm_query=user=javascript:gs.getUserID()`);
|
|
307
|
-
|
|
388
|
+
// CRITICAL FIX: Add null safety for response processing
|
|
389
|
+
if (!response || !response.data || !response.data.result) {
|
|
390
|
+
return { success: true, data: { roles: [], warning: 'Unable to retrieve user roles - response structure unexpected' } };
|
|
391
|
+
}
|
|
392
|
+
// Safely process roles with null checks
|
|
393
|
+
const roles = Array.isArray(response.data.result)
|
|
394
|
+
? response.data.result.map((r) => {
|
|
395
|
+
if (!r || typeof r !== 'object')
|
|
396
|
+
return 'Unknown Role';
|
|
397
|
+
return r.role?.display_value || r.role || 'Unknown Role';
|
|
398
|
+
}).filter(role => role && role !== 'Unknown Role')
|
|
399
|
+
: [];
|
|
400
|
+
return { success: true, data: { roles } };
|
|
308
401
|
}
|
|
309
402
|
}
|
|
310
403
|
];
|
|
@@ -330,17 +423,27 @@ class ServiceNowClient {
|
|
|
330
423
|
};
|
|
331
424
|
}
|
|
332
425
|
}
|
|
333
|
-
// Analyze results
|
|
334
|
-
const failedTests =
|
|
335
|
-
|
|
426
|
+
// Analyze results with null safety
|
|
427
|
+
const failedTests = diagnostics.tests && typeof diagnostics.tests === 'object'
|
|
428
|
+
? Object.values(diagnostics.tests).filter((test) => test && test.status && test.status.includes('FAIL'))
|
|
429
|
+
: [];
|
|
430
|
+
const passedTests = diagnostics.tests && typeof diagnostics.tests === 'object'
|
|
431
|
+
? Object.values(diagnostics.tests).filter((test) => test && test.status && test.status.includes('PASS'))
|
|
432
|
+
: [];
|
|
336
433
|
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'
|
|
434
|
+
total_tests: tests.length || 0,
|
|
435
|
+
passed: passedTests.length || 0,
|
|
436
|
+
failed: failedTests.length || 0,
|
|
437
|
+
overall_status: failedTests.length === 0 && passedTests.length > 0 ? 'โ
ALL SYSTEMS GO' : 'โ ๏ธ ISSUES DETECTED'
|
|
341
438
|
};
|
|
342
|
-
// Generate recommendations
|
|
343
|
-
|
|
439
|
+
// Generate recommendations with null safety
|
|
440
|
+
try {
|
|
441
|
+
diagnostics.recommendations = this.generateAuthRecommendations(diagnostics.tests);
|
|
442
|
+
}
|
|
443
|
+
catch (recError) {
|
|
444
|
+
this.logger.error('Error generating recommendations:', recError);
|
|
445
|
+
diagnostics.recommendations = ['โ ๏ธ Unable to generate recommendations due to error'];
|
|
446
|
+
}
|
|
344
447
|
return {
|
|
345
448
|
success: failedTests.length === 0,
|
|
346
449
|
data: diagnostics,
|
|
@@ -359,24 +462,51 @@ class ServiceNowClient {
|
|
|
359
462
|
*/
|
|
360
463
|
generateAuthRecommendations(tests) {
|
|
361
464
|
const recommendations = [];
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
465
|
+
// CRITICAL FIX: Add comprehensive null safety checks
|
|
466
|
+
if (!tests || typeof tests !== 'object') {
|
|
467
|
+
recommendations.push('โ ๏ธ Unable to analyze test results due to missing test data');
|
|
468
|
+
return recommendations;
|
|
469
|
+
}
|
|
470
|
+
// Safe check for Widget Write Test
|
|
471
|
+
const widgetTest = tests['Widget Write Test'];
|
|
472
|
+
if (widgetTest?.status && typeof widgetTest.status === 'string' && widgetTest.status.includes('FAIL')) {
|
|
473
|
+
const error = widgetTest.error;
|
|
474
|
+
if (error && typeof error === 'string') {
|
|
475
|
+
if (error.includes('403')) {
|
|
476
|
+
recommendations.push('๐ Widget deployment failed with 403 Forbidden. Check OAuth scopes: ensure \'useraccount\' and \'glide_system_administration\' scopes are enabled');
|
|
477
|
+
recommendations.push('๐ค Verify user has sp_portal_manager or admin role in ServiceNow');
|
|
478
|
+
recommendations.push('๐ก๏ธ Check if instance has deployment restrictions for external applications');
|
|
479
|
+
}
|
|
480
|
+
if (error.includes('401')) {
|
|
481
|
+
recommendations.push('๐ Authentication failed. Re-run: snow-flow auth login');
|
|
482
|
+
}
|
|
368
483
|
}
|
|
369
|
-
|
|
370
|
-
recommendations.push('
|
|
484
|
+
else {
|
|
485
|
+
recommendations.push('๐ Widget write test failed with unknown error. Check ServiceNow permissions');
|
|
371
486
|
}
|
|
372
487
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
488
|
+
// Safe check for User Role Check
|
|
489
|
+
const roleTest = tests['User Role Check'];
|
|
490
|
+
if (roleTest?.status && typeof roleTest.status === 'string' && roleTest.status.includes('PASS')) {
|
|
491
|
+
const roles = roleTest.result?.roles;
|
|
492
|
+
if (Array.isArray(roles)) {
|
|
493
|
+
const hasRequiredRole = roles.some((role) => {
|
|
494
|
+
if (typeof role === 'string') {
|
|
495
|
+
return role.includes('admin') || role.includes('sp_portal');
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
});
|
|
499
|
+
if (!hasRequiredRole) {
|
|
500
|
+
recommendations.push('โ ๏ธ User lacks admin or portal management roles. Contact ServiceNow admin to assign appropriate roles');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
recommendations.push('โ ๏ธ Unable to verify user roles. Check if user has admin or portal management permissions');
|
|
377
505
|
}
|
|
378
506
|
}
|
|
379
|
-
|
|
507
|
+
// Safe check for Update Set Access
|
|
508
|
+
const updateSetTest = tests['Update Set Access'];
|
|
509
|
+
if (updateSetTest?.status && typeof updateSetTest.status === 'string' && updateSetTest.status.includes('FAIL')) {
|
|
380
510
|
recommendations.push('๐ฆ Update Set access failed. Ensure user has update_set_manager or admin role');
|
|
381
511
|
}
|
|
382
512
|
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',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
๐ Snow-Flow v1.3.24 - Ready for NPM Publish!
|
|
2
|
+
|
|
3
|
+
โ
GITHUB STATUS:
|
|
4
|
+
- All changes committed and pushed to main branch
|
|
5
|
+
- Git tag v1.3.24 created and pushed
|
|
6
|
+
- Repository: https://github.com/groeimetai/snow-flow
|
|
7
|
+
|
|
8
|
+
โ
FIXES COMPLETED:
|
|
9
|
+
- SNOW-001: Silent Deployment Failures โ FIXED with mandatory verification
|
|
10
|
+
- SNOW-002: Search System Failures โ FIXED with retry logic
|
|
11
|
+
- SNOW-003: High Failure Rate (19%) โ FIXED with enhanced resilience
|
|
12
|
+
- SNOW-004: Security Compliance Module โ FIXED variable shadowing
|
|
13
|
+
|
|
14
|
+
โ
NPM PACKAGE READY:
|
|
15
|
+
- Version: 1.3.24
|
|
16
|
+
- Package size: 1.6 MB
|
|
17
|
+
- Unpacked size: 9.0 MB
|
|
18
|
+
- Files: 740 total
|
|
19
|
+
|
|
20
|
+
๐ฆ TO PUBLISH TO NPM:
|
|
21
|
+
1. Make sure you're logged in to npm:
|
|
22
|
+
npm whoami
|
|
23
|
+
|
|
24
|
+
2. If not logged in:
|
|
25
|
+
npm login
|
|
26
|
+
|
|
27
|
+
3. Publish the package:
|
|
28
|
+
npm publish
|
|
29
|
+
|
|
30
|
+
4. Verify publication:
|
|
31
|
+
npm view snow-flow version
|
|
32
|
+
|
|
33
|
+
๐ Snow-Flow is now production-ready with all critical beta test issues resolved!
|
|
34
|
+
|
|
35
|
+
Release notes available at: RELEASE_NOTES_1.3.24.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snow-flow",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.24",
|
|
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",
|