snow-flow 1.1.45 → 1.1.48

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.
@@ -651,6 +651,49 @@ class ServiceNowOperationsMCP {
651
651
  },
652
652
  required: ['catalog_item_id', 'flow_id']
653
653
  }
654
+ },
655
+ {
656
+ name: 'snow_cleanup_test_artifacts',
657
+ description: 'Clean up test artifacts while preserving Update Set audit trail',
658
+ inputSchema: {
659
+ type: 'object',
660
+ properties: {
661
+ artifact_types: {
662
+ type: 'array',
663
+ description: 'Types of artifacts to clean up',
664
+ items: {
665
+ type: 'string',
666
+ enum: ['catalog_items', 'flows', 'users', 'requests', 'all']
667
+ },
668
+ default: ['catalog_items', 'flows', 'users']
669
+ },
670
+ test_patterns: {
671
+ type: 'array',
672
+ description: 'Name patterns that identify test artifacts',
673
+ items: { type: 'string' },
674
+ default: ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock']
675
+ },
676
+ max_age_hours: {
677
+ type: 'number',
678
+ description: 'Only clean artifacts older than this (hours)',
679
+ default: 1
680
+ },
681
+ dry_run: {
682
+ type: 'boolean',
683
+ description: 'Preview what would be deleted without actually deleting',
684
+ default: false
685
+ },
686
+ preserve_update_set_entries: {
687
+ type: 'boolean',
688
+ description: 'Keep Update Set entries as audit trail',
689
+ default: true
690
+ },
691
+ update_set_filter: {
692
+ type: 'string',
693
+ description: 'Only clean from specific Update Set (optional)'
694
+ }
695
+ }
696
+ }
654
697
  }
655
698
  ],
656
699
  };
@@ -690,6 +733,8 @@ class ServiceNowOperationsMCP {
690
733
  return await this.handleTestFlowWithMock(args);
691
734
  case 'snow_link_catalog_to_flow':
692
735
  return await this.handleLinkCatalogToFlow(args);
736
+ case 'snow_cleanup_test_artifacts':
737
+ return await this.handleCleanupTestArtifacts(args);
693
738
  default:
694
739
  throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Tool ${name} not found`);
695
740
  }
@@ -1557,6 +1602,34 @@ class ServiceNowOperationsMCP {
1557
1602
  try {
1558
1603
  switch (action) {
1559
1604
  case 'create': {
1605
+ // Get default catalog if none provided
1606
+ let catalogId = params.sc_catalogs || params.catalog_id;
1607
+ let categoryId = params.sc_categories || params.category_id;
1608
+ if (!catalogId) {
1609
+ // Find default Service Catalog
1610
+ const defaultCatalogResult = await this.client.searchRecords('sc_catalog', 'active=true^ORDERBYsys_created_on', 1);
1611
+ if (defaultCatalogResult.success && defaultCatalogResult.data?.result?.length > 0) {
1612
+ catalogId = defaultCatalogResult.data.result[0].sys_id;
1613
+ logger_js_1.logger.info('Using default catalog:', {
1614
+ catalogId,
1615
+ catalogName: defaultCatalogResult.data.result[0].title
1616
+ });
1617
+ }
1618
+ else {
1619
+ logger_js_1.logger.warn('No catalogs found, catalog item may not be visible');
1620
+ }
1621
+ }
1622
+ if (!categoryId) {
1623
+ // Find a default category like "Hardware" or "General"
1624
+ const defaultCategoryResult = await this.client.searchRecords('sc_category', 'active=true^titleLIKEHardware^ORtitleLIKEGeneral^ORtitleLIKEIT^ORDERBYsys_created_on', 1);
1625
+ if (defaultCategoryResult.success && defaultCategoryResult.data?.result?.length > 0) {
1626
+ categoryId = defaultCategoryResult.data.result[0].sys_id;
1627
+ logger_js_1.logger.info('Using default category:', {
1628
+ categoryId,
1629
+ categoryName: defaultCategoryResult.data.result[0].title
1630
+ });
1631
+ }
1632
+ }
1560
1633
  const catalogItem = {
1561
1634
  name: params.name,
1562
1635
  short_description: params.short_description,
@@ -1567,8 +1640,8 @@ class ServiceNowOperationsMCP {
1567
1640
  active: params.active !== false,
1568
1641
  workflow: params.workflow,
1569
1642
  picture: params.picture,
1570
- sc_catalogs: params.sc_catalogs || params.catalog_id,
1571
- sc_categories: params.sc_categories || params.category_id,
1643
+ sc_catalogs: catalogId,
1644
+ sc_categories: categoryId,
1572
1645
  sys_class_name: 'sc_cat_item'
1573
1646
  };
1574
1647
  const result = await this.client.createRecord('sc_cat_item', catalogItem);
@@ -1981,10 +2054,49 @@ class ServiceNowOperationsMCP {
1981
2054
  cleanup_status: null
1982
2055
  };
1983
2056
  try {
1984
- // Step 1: Find the flow
1985
- const flowResult = await this.client.searchRecords('sys_hub_flow', `sys_id=${flow_id}^ORname=${flow_id}`, 1);
2057
+ // Step 1: Find the flow with intelligent fallback search
2058
+ let flowResult = await this.client.searchRecords('sys_hub_flow', `sys_id=${flow_id}^ORname=${flow_id}`, 1);
2059
+ // If not found, try different search strategies
1986
2060
  if (!flowResult.success || !flowResult.data?.result?.length) {
1987
- throw new Error(`Flow not found: ${flow_id}`);
2061
+ logger_js_1.logger.info('Flow not found with exact match, trying fuzzy search...');
2062
+ // Try partial name matching
2063
+ flowResult = await this.client.searchRecords('sys_hub_flow', `nameCONTAINS${flow_id}`, 5);
2064
+ if (!flowResult.success || !flowResult.data?.result?.length) {
2065
+ // Try searching in Business Rules as fallback
2066
+ logger_js_1.logger.info('No flows found, searching for Business Rules as fallback...');
2067
+ const businessRuleResult = await this.client.searchRecords('sys_script', `nameCONTAINS${flow_id}^ORshort_descriptionCONTAINS${flow_id}`, 5);
2068
+ if (businessRuleResult.success && businessRuleResult.data?.result?.length) {
2069
+ testResults.execution_results.status = 'fallback_business_rule';
2070
+ testResults.execution_results.errors.push(`Flow '${flow_id}' not found, but found ${businessRuleResult.data.result.length} related Business Rules. Consider testing the Business Rule directly.`);
2071
+ const businessRules = businessRuleResult.data.result.map((br) => ({
2072
+ name: br.name,
2073
+ sys_id: br.sys_id,
2074
+ table: br.collection,
2075
+ when: br.when
2076
+ }));
2077
+ return {
2078
+ content: [{
2079
+ type: 'text',
2080
+ text: `⚠️ **Flow Test - Intelligent Fallback**\n\nFlow '${flow_id}' not found, but discovered related Business Rules:\n\n${businessRules.map((br, i) => `${i + 1}. **${br.name}**\n - Sys ID: ${br.sys_id}\n - Table: ${br.table}\n - Trigger: ${br.when}\n`).join('\n')}\n\n💡 **Recommendation:** Test the Business Rule directly as it may provide the same functionality as the intended flow.\n\n🔧 **Next Steps:**\n1. Verify the Business Rule logic matches your flow requirements\n2. Test by creating a record on the ${businessRules[0]?.table || 'target'} table\n3. Monitor System Logs for Business Rule execution`
2081
+ }]
2082
+ };
2083
+ }
2084
+ throw new Error(`Flow not found: ${flow_id}. Tried exact match, partial name match, and Business Rule fallback.`);
2085
+ }
2086
+ else {
2087
+ // Found partial matches, ask user to clarify
2088
+ const matches = flowResult.data.result.map((f) => ({
2089
+ name: f.name,
2090
+ sys_id: f.sys_id,
2091
+ active: f.active
2092
+ }));
2093
+ return {
2094
+ content: [{
2095
+ type: 'text',
2096
+ text: `🔍 **Multiple Flow Matches Found**\n\nThe flow identifier '${flow_id}' matched multiple flows:\n\n${matches.map((f, i) => `${i + 1}. **${f.name}** (${f.active ? 'Active' : 'Inactive'})\n - Sys ID: ${f.sys_id}\n`).join('\n')}\n\n💡 **Please specify the exact flow sys_id or unique name to continue testing.**`
2097
+ }]
2098
+ };
2099
+ }
1988
2100
  }
1989
2101
  const flow = flowResult.data.result[0];
1990
2102
  testResults.flow_id = flow.sys_id;
@@ -2491,6 +2603,279 @@ class ServiceNowOperationsMCP {
2491
2603
  };
2492
2604
  }
2493
2605
  }
2606
+ async handleCleanupTestArtifacts(args) {
2607
+ const { artifact_types = ['catalog_items', 'flows', 'users'], test_patterns = ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock'], max_age_hours = 1, dry_run = false, preserve_update_set_entries = true, update_set_filter } = args;
2608
+ logger_js_1.logger.info('Starting test artifact cleanup', {
2609
+ artifact_types,
2610
+ test_patterns,
2611
+ max_age_hours,
2612
+ dry_run
2613
+ });
2614
+ const cleanupResults = {
2615
+ start_time: new Date().toISOString(),
2616
+ dry_run,
2617
+ artifacts_found: {
2618
+ catalog_items: [],
2619
+ flows: [],
2620
+ users: [],
2621
+ requests: []
2622
+ },
2623
+ artifacts_deleted: {
2624
+ catalog_items: 0,
2625
+ flows: 0,
2626
+ users: 0,
2627
+ requests: 0
2628
+ },
2629
+ update_set_entries_preserved: 0,
2630
+ errors: [],
2631
+ summary: ''
2632
+ };
2633
+ try {
2634
+ // Calculate cutoff time
2635
+ const cutoffTime = new Date();
2636
+ cutoffTime.setHours(cutoffTime.getHours() - max_age_hours);
2637
+ const cutoffISO = cutoffTime.toISOString();
2638
+ // Process each artifact type
2639
+ for (const artifactType of artifact_types) {
2640
+ if (artifactType === 'all' || artifact_types.includes('catalog_items')) {
2641
+ await this.cleanupTestCatalogItems(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2642
+ }
2643
+ if (artifactType === 'all' || artifact_types.includes('flows')) {
2644
+ await this.cleanupTestFlows(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2645
+ }
2646
+ if (artifactType === 'all' || artifact_types.includes('users')) {
2647
+ await this.cleanupTestUsers(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2648
+ }
2649
+ if (artifactType === 'all' || artifact_types.includes('requests')) {
2650
+ await this.cleanupTestRequests(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2651
+ }
2652
+ }
2653
+ // Generate summary
2654
+ const totalFound = Object.values(cleanupResults.artifacts_found)
2655
+ .reduce((sum, items) => (typeof sum === 'number' ? sum : 0) + (Array.isArray(items) ? items.length : 0), 0);
2656
+ const totalDeleted = Object.values(cleanupResults.artifacts_deleted)
2657
+ .reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0);
2658
+ cleanupResults.summary = dry_run
2659
+ ? `🔍 Dry Run: Found ${totalFound} test artifacts that would be cleaned up`
2660
+ : `🧹 Cleaned up ${totalDeleted} test artifacts successfully`;
2661
+ // Format results
2662
+ let resultText = `🧹 Test Artifact Cleanup Results\n${'='.repeat(50)}\n\n`;
2663
+ if (dry_run) {
2664
+ resultText += `🔍 **DRY RUN MODE** - No actual deletion performed\n\n`;
2665
+ }
2666
+ resultText += `⏰ **Cleanup Configuration:**\n`;
2667
+ resultText += `- Artifact Types: ${artifact_types.join(', ')}\n`;
2668
+ resultText += `- Test Patterns: ${test_patterns.join(', ')}\n`;
2669
+ resultText += `- Max Age: ${max_age_hours} hours\n`;
2670
+ resultText += `- Cutoff Time: ${cutoffISO}\n`;
2671
+ resultText += `- Preserve Update Set Entries: ${preserve_update_set_entries ? '✅' : '❌'}\n\n`;
2672
+ // Report findings by type
2673
+ ['catalog_items', 'flows', 'users', 'requests'].forEach(type => {
2674
+ const found = cleanupResults.artifacts_found[type] || [];
2675
+ const deleted = cleanupResults.artifacts_deleted[type] || 0;
2676
+ if (found.length > 0) {
2677
+ resultText += `📦 **${type.replace('_', ' ').toUpperCase()}:**\n`;
2678
+ resultText += ` ${dry_run ? 'Found' : 'Deleted'}: ${dry_run ? found.length : deleted}\n`;
2679
+ if (dry_run && found.length > 0) {
2680
+ found.slice(0, 10).forEach((item) => {
2681
+ resultText += ` - ${item.name} (${item.sys_id}) - Created: ${item.sys_created_on}\n`;
2682
+ });
2683
+ if (found.length > 10) {
2684
+ resultText += ` ... and ${found.length - 10} more\n`;
2685
+ }
2686
+ }
2687
+ resultText += '\n';
2688
+ }
2689
+ });
2690
+ if (cleanupResults.update_set_entries_preserved > 0) {
2691
+ resultText += `📋 **Update Set Audit Trail Preserved:** ${cleanupResults.update_set_entries_preserved} entries\n\n`;
2692
+ }
2693
+ if (cleanupResults.errors.length > 0) {
2694
+ resultText += `❌ **Errors:**\n`;
2695
+ cleanupResults.errors.forEach(error => {
2696
+ resultText += ` - ${error}\n`;
2697
+ });
2698
+ resultText += '\n';
2699
+ }
2700
+ resultText += `✅ **${cleanupResults.summary}**\n\n`;
2701
+ if (!dry_run && totalDeleted > 0) {
2702
+ resultText += `💡 **Note:** Update Set entries have been preserved as audit trail.\n`;
2703
+ resultText += `This shows that testing was performed and cleanup was completed.\n\n`;
2704
+ }
2705
+ if (dry_run) {
2706
+ resultText += `▶️ **Next Steps:**\n`;
2707
+ resultText += `1. Review the artifacts that would be deleted\n`;
2708
+ resultText += `2. Run again with dry_run: false to perform actual cleanup\n`;
2709
+ resultText += `3. Verify Update Set entries are preserved as intended\n`;
2710
+ }
2711
+ return {
2712
+ content: [{
2713
+ type: 'text',
2714
+ text: resultText
2715
+ }]
2716
+ };
2717
+ }
2718
+ catch (error) {
2719
+ logger_js_1.logger.error('Error during test artifact cleanup:', error);
2720
+ return {
2721
+ content: [{
2722
+ type: 'text',
2723
+ text: `❌ Test artifact cleanup failed\n\nError: ${error.message}\n\n` +
2724
+ `🔧 Troubleshooting:\n` +
2725
+ `1. Check ServiceNow connection and permissions\n` +
2726
+ `2. Verify test patterns are correct\n` +
2727
+ `3. Ensure artifacts exist and are accessible\n\n` +
2728
+ `Debug Info: ${JSON.stringify(cleanupResults, null, 2)}`
2729
+ }]
2730
+ };
2731
+ }
2732
+ }
2733
+ async cleanupTestCatalogItems(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2734
+ try {
2735
+ // Build query for test catalog items
2736
+ const patternQueries = patterns.map(pattern => {
2737
+ if (pattern.includes('%')) {
2738
+ return `nameLIKE${pattern.replace(/%/g, '')}`;
2739
+ }
2740
+ else {
2741
+ return `name=${pattern}`;
2742
+ }
2743
+ });
2744
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
2745
+ const searchResult = await this.client.searchRecords('sc_cat_item', query, 100);
2746
+ if (searchResult.success && searchResult.data?.result) {
2747
+ results.artifacts_found.catalog_items = searchResult.data.result.map((item) => ({
2748
+ sys_id: item.sys_id,
2749
+ name: item.name,
2750
+ sys_created_on: item.sys_created_on
2751
+ }));
2752
+ if (!dryRun) {
2753
+ // Delete each catalog item
2754
+ for (const item of searchResult.data.result) {
2755
+ const deleteResult = await this.client.deleteRecord('sc_cat_item', item.sys_id);
2756
+ if (deleteResult.success) {
2757
+ results.artifacts_deleted.catalog_items++;
2758
+ }
2759
+ else {
2760
+ results.errors.push(`Failed to delete catalog item ${item.name}: ${deleteResult.error}`);
2761
+ }
2762
+ }
2763
+ }
2764
+ }
2765
+ }
2766
+ catch (error) {
2767
+ results.errors.push(`Error cleaning catalog items: ${error.message}`);
2768
+ }
2769
+ }
2770
+ async cleanupTestFlows(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2771
+ try {
2772
+ const patternQueries = patterns.map(pattern => {
2773
+ if (pattern.includes('%')) {
2774
+ return `nameLIKE${pattern.replace(/%/g, '')}`;
2775
+ }
2776
+ else {
2777
+ return `name=${pattern}`;
2778
+ }
2779
+ });
2780
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
2781
+ const searchResult = await this.client.searchRecords('sys_hub_flow', query, 100);
2782
+ if (searchResult.success && searchResult.data?.result) {
2783
+ results.artifacts_found.flows = searchResult.data.result.map((flow) => ({
2784
+ sys_id: flow.sys_id,
2785
+ name: flow.name,
2786
+ sys_created_on: flow.sys_created_on
2787
+ }));
2788
+ if (!dryRun) {
2789
+ for (const flow of searchResult.data.result) {
2790
+ const deleteResult = await this.client.deleteRecord('sys_hub_flow', flow.sys_id);
2791
+ if (deleteResult.success) {
2792
+ results.artifacts_deleted.flows++;
2793
+ }
2794
+ else {
2795
+ results.errors.push(`Failed to delete flow ${flow.name}: ${deleteResult.error}`);
2796
+ }
2797
+ }
2798
+ }
2799
+ }
2800
+ }
2801
+ catch (error) {
2802
+ results.errors.push(`Error cleaning flows: ${error.message}`);
2803
+ }
2804
+ }
2805
+ async cleanupTestUsers(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2806
+ try {
2807
+ const patternQueries = patterns.map(pattern => {
2808
+ if (pattern.includes('%')) {
2809
+ return `user_nameLIKE${pattern.replace(/%/g, '')}^ORfirst_nameLIKE${pattern.replace(/%/g, '')}`;
2810
+ }
2811
+ else {
2812
+ return `user_name=${pattern}^ORfirst_name=${pattern}`;
2813
+ }
2814
+ });
2815
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}^active=false`;
2816
+ const searchResult = await this.client.searchRecords('sys_user', query, 100);
2817
+ if (searchResult.success && searchResult.data?.result) {
2818
+ results.artifacts_found.users = searchResult.data.result.map((user) => ({
2819
+ sys_id: user.sys_id,
2820
+ name: user.user_name || user.name,
2821
+ sys_created_on: user.sys_created_on
2822
+ }));
2823
+ if (!dryRun) {
2824
+ for (const user of searchResult.data.result) {
2825
+ const deleteResult = await this.client.deleteRecord('sys_user', user.sys_id);
2826
+ if (deleteResult.success) {
2827
+ results.artifacts_deleted.users++;
2828
+ }
2829
+ else {
2830
+ results.errors.push(`Failed to delete user ${user.user_name}: ${deleteResult.error}`);
2831
+ }
2832
+ }
2833
+ }
2834
+ }
2835
+ }
2836
+ catch (error) {
2837
+ results.errors.push(`Error cleaning users: ${error.message}`);
2838
+ }
2839
+ }
2840
+ async cleanupTestRequests(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2841
+ try {
2842
+ const patternQueries = patterns.map(pattern => {
2843
+ if (pattern.includes('%')) {
2844
+ return `short_descriptionLIKE${pattern.replace(/%/g, '')}`;
2845
+ }
2846
+ else {
2847
+ return `short_description=${pattern}`;
2848
+ }
2849
+ });
2850
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
2851
+ const searchResult = await this.client.searchRecords('sc_request', query, 100);
2852
+ if (searchResult.success && searchResult.data?.result) {
2853
+ results.artifacts_found.requests = searchResult.data.result.map((req) => ({
2854
+ sys_id: req.sys_id,
2855
+ name: req.number || req.short_description,
2856
+ sys_created_on: req.sys_created_on
2857
+ }));
2858
+ if (!dryRun) {
2859
+ for (const request of searchResult.data.result) {
2860
+ // Cancel request instead of deleting (safer)
2861
+ const updateResult = await this.client.updateRecord('sc_request', request.sys_id, {
2862
+ state: '4', // Cancelled
2863
+ comments: 'Cancelled by test cleanup automation'
2864
+ });
2865
+ if (updateResult.success) {
2866
+ results.artifacts_deleted.requests++;
2867
+ }
2868
+ else {
2869
+ results.errors.push(`Failed to cancel request ${request.number}: ${updateResult.error}`);
2870
+ }
2871
+ }
2872
+ }
2873
+ }
2874
+ }
2875
+ catch (error) {
2876
+ results.errors.push(`Error cleaning requests: ${error.message}`);
2877
+ }
2878
+ }
2494
2879
  getServiceNowUrl() {
2495
2880
  // This would need to get the instance URL from credentials
2496
2881
  return 'https://instance.service-now.com';