snow-flow 3.3.5 → 3.3.7

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.
@@ -36,7 +36,7 @@ function getDynamicVersion() {
36
36
  console.warn('Warning: Could not read version from package.json:', error);
37
37
  }
38
38
  // Fallback to hardcoded version
39
- return '3.3.5';
39
+ return '3.3.7';
40
40
  }
41
41
  // Export a constant that uses the dynamic version
42
42
  exports.VERSION = getDynamicVersion();
@@ -1316,7 +1316,12 @@ ${is403Error ? '\n⚠️ **Possible False Negative**: Widget may have been crea
1316
1316
  const { updateSetId, updateSetName } = await this.ensureUpdateSet('Portal Page', args.page_id);
1317
1317
  // Validate portal page structure
1318
1318
  if (!args.page_id || !args.title) {
1319
- throw new Error('Portal page must have page_id and title');
1319
+ this.logger.error('Portal page validation failed', {
1320
+ args,
1321
+ hasPageId: !!args.page_id,
1322
+ hasTitle: !!args.title
1323
+ });
1324
+ throw new Error(`Portal page must have page_id and title. Received: page_id=${args.page_id}, title=${args.title}`);
1320
1325
  }
1321
1326
  // Find widget sys_id if widget name is provided
1322
1327
  let widgetSysId = args.widget_sys_id;
@@ -1337,18 +1342,22 @@ ${is403Error ? '\n⚠️ **Possible False Negative**: Widget may have been crea
1337
1342
  }
1338
1343
  }
1339
1344
  // Determine portal sys_id
1340
- let portalSysId = '';
1341
- try {
1342
- // Default to Employee Service Portal if available, otherwise standard Service Portal
1343
- const portalQuery = args.portal === 'esc' ? 'url_suffix=esc' : 'url_suffix=sp';
1344
- const portalResult = await this.client.searchRecords('sp_portal', portalQuery, 1);
1345
- if (portalResult.success && portalResult.data?.length > 0) {
1346
- portalSysId = portalResult.data[0].sys_id;
1347
- this.logger.info('Found portal', { portal: args.portal, sys_id: portalSysId });
1345
+ let portalSysId = args.sp_portal || '';
1346
+ // If sp_portal is already a sys_id (32 char hex), use it directly
1347
+ if (!/^[a-f0-9]{32}$/.test(portalSysId)) {
1348
+ // Not a sys_id, try to look it up
1349
+ try {
1350
+ // Default to Employee Service Portal if available, otherwise standard Service Portal
1351
+ const portalQuery = args.portal === 'esc' ? 'url_suffix=esc' : 'url_suffix=sp';
1352
+ const portalResult = await this.client.searchRecords('sp_portal', portalQuery, 1);
1353
+ if (portalResult.success && portalResult.data?.result?.length > 0) {
1354
+ portalSysId = portalResult.data.result[0].sys_id;
1355
+ this.logger.info('Found portal', { portal: args.portal, sys_id: portalSysId });
1356
+ }
1357
+ }
1358
+ catch (error) {
1359
+ this.logger.warn('Failed to lookup portal, using provided value', { portal: args.portal, error });
1348
1360
  }
1349
- }
1350
- catch (error) {
1351
- this.logger.warn('Failed to lookup portal, using default', { portal: args.portal, error });
1352
1361
  }
1353
1362
  // Create portal page
1354
1363
  let pageResult;
@@ -1381,8 +1390,35 @@ ${is403Error ? '\n⚠️ **Possible False Negative**: Widget may have been crea
1381
1390
  const credentials = await this.oauth.loadCredentials();
1382
1391
  // Create widget instances on the page if widget is provided
1383
1392
  const widgetInstances = [];
1384
- if (widgetSysId && args.widgets && args.widgets.length > 0) {
1385
- for (const widgetConfig of args.widgets) {
1393
+ // Support both widgets array and single widget
1394
+ const widgetsToCreate = args.widgets || [];
1395
+ // If no widgets array but a single widget is specified, create one widget
1396
+ if (widgetsToCreate.length === 0 && widgetSysId) {
1397
+ widgetsToCreate.push({
1398
+ widget: args.widget_name || widgetSysId,
1399
+ width: 12,
1400
+ row: 1,
1401
+ column: 1,
1402
+ title: args.widget_title || args.title || '',
1403
+ options: args.widget_options || {}
1404
+ });
1405
+ }
1406
+ if (widgetsToCreate.length > 0) {
1407
+ for (const widgetConfig of widgetsToCreate) {
1408
+ // Determine widget sys_id for this widget
1409
+ let currentWidgetSysId = widgetSysId; // Default to the main widget
1410
+ // If widget config specifies a different widget, look it up
1411
+ if (widgetConfig.widget && widgetConfig.widget !== args.widget_name) {
1412
+ try {
1413
+ const widgetLookup = await this.client.searchRecords('sp_widget', `sys_id=${widgetConfig.widget}^ORname=${widgetConfig.widget}`, 1);
1414
+ if (widgetLookup.success && widgetLookup.data?.result?.length > 0) {
1415
+ currentWidgetSysId = widgetLookup.data.result[0].sys_id;
1416
+ }
1417
+ }
1418
+ catch (lookupError) {
1419
+ this.logger.warn('Could not find widget, using default', { widget: widgetConfig.widget });
1420
+ }
1421
+ }
1386
1422
  try {
1387
1423
  // Create container
1388
1424
  const containerResult = await this.client.createRecord('sp_container', {
@@ -1417,10 +1453,10 @@ ${is403Error ? '\n⚠️ **Possible False Negative**: Widget may have been crea
1417
1453
  // Create widget instance
1418
1454
  const instanceResult = await this.client.createRecord('sp_instance', {
1419
1455
  sp_column: columnResult.data.sys_id,
1420
- sp_widget: widgetSysId,
1456
+ sp_widget: currentWidgetSysId, // Use the current widget sys_id, not the main one
1421
1457
  order: widgetConfig.order || 100,
1422
1458
  title: widgetConfig.title || '',
1423
- options: JSON.stringify(widgetConfig.options || {}),
1459
+ options: JSON.stringify(widgetConfig.options || widgetConfig.widget_parameters || {}),
1424
1460
  class_name: widgetConfig.instance_class || '',
1425
1461
  color: widgetConfig.color || 'default',
1426
1462
  active: true,
@@ -7260,7 +7296,23 @@ Use individual deployment tools like \`snow_deploy_${args.type}\` with manual co
7260
7296
  case 'widget':
7261
7297
  return await this.deployWidget(scopedConfig);
7262
7298
  case 'portal_page':
7263
- return await this.deployPortalPage(scopedConfig);
7299
+ // Map config fields to expected parameter names for portal_page
7300
+ const portalPageArgs = {
7301
+ page_id: scopedConfig.id || scopedConfig.page_id,
7302
+ title: scopedConfig.title,
7303
+ widget_name: scopedConfig.widget_name,
7304
+ widget_sys_id: scopedConfig.widget_sys_id,
7305
+ description: scopedConfig.summary || scopedConfig.description,
7306
+ page_css: scopedConfig.css,
7307
+ portal: scopedConfig.portal || scopedConfig.sp_portal || 'sp',
7308
+ public: scopedConfig.public !== undefined ? scopedConfig.public : true,
7309
+ requires_authentication: scopedConfig.requires_authentication,
7310
+ draft: scopedConfig.draft || false,
7311
+ // Map containers to widgets format
7312
+ widgets: this.mapContainersToWidgets(scopedConfig.containers),
7313
+ ...scopedConfig // Include any other fields
7314
+ };
7315
+ return await this.deployPortalPage(portalPageArgs);
7264
7316
  case 'application':
7265
7317
  return await this.deployApplication(scopedConfig);
7266
7318
  case 'xml_update_set':
@@ -8851,6 +8903,36 @@ c.$onInit = function() {
8851
8903
  await this.server.connect(transport);
8852
8904
  this.logger.info('ServiceNow Deployment MCP Server started');
8853
8905
  }
8906
+ /**
8907
+ * Map container structure to widget structure for portal page deployment
8908
+ */
8909
+ mapContainersToWidgets(containers) {
8910
+ if (!containers || !Array.isArray(containers)) {
8911
+ return [];
8912
+ }
8913
+ const widgets = [];
8914
+ for (const container of containers) {
8915
+ if (container.widget_instance) {
8916
+ widgets.push({
8917
+ widget: container.widget_instance.widget,
8918
+ width: container.width || 12,
8919
+ row: container.row || 1,
8920
+ column: container.column || 1,
8921
+ title: container.widget_instance.title || '',
8922
+ options: container.widget_instance.widget_parameters || {},
8923
+ container_title: container.title || '',
8924
+ class_name: container.class_name || '',
8925
+ background_color: container.background_color || '',
8926
+ background_image: container.background_image || '',
8927
+ background_style: container.background_style || 'default',
8928
+ instance_class: container.widget_instance.class_name || '',
8929
+ color: container.widget_instance.color || 'default',
8930
+ order: container.widget_instance.order || 100
8931
+ });
8932
+ }
8933
+ }
8934
+ return widgets;
8935
+ }
8854
8936
  /**
8855
8937
  * Escape XML special characters
8856
8938
  */
@@ -868,7 +868,12 @@ class ServiceNowOperationsMCP {
868
868
  logger_js_1.logger.info(`⚠️ No specific context detected - using conservative limit. Consider specifying limit for your use case!`);
869
869
  return 500; // Conservative default
870
870
  };
871
- const { table, query, include_content = false, fields, include_display_values = false, group_by, order_by } = args;
871
+ const { table, query = '', include_content = false, fields, include_display_values = false, group_by, order_by, offset = 0 // ✅ NEW: Support pagination!
872
+ } = args;
873
+ // Validate table name
874
+ if (!table) {
875
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'Table name is required');
876
+ }
872
877
  // Apply intelligent limit strategy
873
878
  const limit = determineSmartLimit(args.limit, table, query, include_content || !!fields, fields);
874
879
  // For analytics, we want NO limit at all
@@ -880,19 +885,22 @@ class ServiceNowOperationsMCP {
880
885
  if (isMLTrainingContext && limit < 1000) {
881
886
  logger_js_1.logger.warn(`⚠️ ML Training detected with low limit (${limit}). Consider setting limit=5000+ for better training data!`);
882
887
  }
883
- logger_js_1.logger.info(`Universal query on table '${table}' with: ${query} (limit: ${limit === undefined ? 'UNLIMITED' : limit}, include_content: ${include_content})`);
888
+ logger_js_1.logger.info(`Universal query on table '${table}' with: ${query} (limit: ${limit === undefined ? 'UNLIMITED' : limit}, offset: ${offset}, include_content: ${include_content})`);
884
889
  try {
885
890
  // Convert natural language to ServiceNow query if needed
886
- const processedQuery = this.processNaturalLanguageQuery(query, table);
887
- // Build the query with order_by if specified
891
+ const processedQuery = this.processNaturalLanguageQuery(query || '', table);
892
+ // Build the query with CORRECT order_by syntax
888
893
  let finalQuery = processedQuery;
889
894
  if (order_by) {
895
+ // ✅ FIX: Use correct ServiceNow ordering syntax
890
896
  const orderDirection = order_by.startsWith('-') ? 'DESC' : '';
891
897
  const orderField = order_by.replace(/^-/, '');
892
- finalQuery += `^ORDERBY${orderDirection}${orderField}`;
898
+ finalQuery += `^ORDERBY${orderDirection ? orderDirection : ''}${orderField}`;
893
899
  }
894
- // Query the table
895
- const records = await this.client.searchRecords(table, finalQuery, effectiveLimit);
900
+ // NEW: Use searchRecordsWithOffset when offset is provided
901
+ const records = offset > 0
902
+ ? await this.client.searchRecordsWithOffset(table, finalQuery, effectiveLimit, offset)
903
+ : await this.client.searchRecords(table, finalQuery, effectiveLimit);
896
904
  let result = {
897
905
  table: table,
898
906
  total_results: records.success ? records.data.result.length : 0,
@@ -908,33 +916,62 @@ class ServiceNowOperationsMCP {
908
916
  result.grouped_counts = grouped;
909
917
  result.unique_values = Object.keys(grouped).length;
910
918
  }
911
- // 🎯 SMART CONTENT DECISION: Only include full data if explicitly requested
912
- if (include_content || (fields && fields.length > 0)) {
919
+ // IMPROVED: Clear content decision logic
920
+ const shouldIncludeContent = include_content === true || (fields && fields.length > 0);
921
+ if (shouldIncludeContent) {
913
922
  // Include full record data when specifically requested
914
923
  result.records = records.success ? records.data.result : [];
915
- // If specific fields requested, filter them
916
- if (fields && fields.length > 0 && records.success) {
924
+ // If specific fields requested, filter and validate them
925
+ if (fields && fields.length > 0 && records.success && records.data.result.length > 0) {
926
+ // ✅ NEW: Validate fields exist
927
+ const sampleRecord = records.data.result[0];
928
+ const validFields = fields.filter((field) => {
929
+ const exists = sampleRecord.hasOwnProperty(field) ||
930
+ sampleRecord.hasOwnProperty(`${field}_display_value`);
931
+ if (!exists) {
932
+ logger_js_1.logger.warn(`⚠️ Field '${field}' not found in table '${table}'`);
933
+ }
934
+ return exists;
935
+ });
936
+ if (validFields.length === 0 && fields.length > 0) {
937
+ // All requested fields are invalid
938
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `None of the requested fields exist in table '${table}'. Available fields: ${Object.keys(sampleRecord).slice(0, 10).join(', ')}...`);
939
+ }
917
940
  result.records = records.data.result.map((record) => {
918
941
  const filtered = {};
919
942
  // Always include sys_id and number/name if available
920
943
  if (record.sys_id)
921
944
  filtered.sys_id = record.sys_id;
922
- if (record.number)
945
+ if (record.number && !validFields.includes('number'))
923
946
  filtered.number = record.number;
924
- if (record.name && !fields.includes('name'))
947
+ if (record.name && !validFields.includes('name'))
925
948
  filtered.name = record.name;
926
949
  // Add requested fields
927
- fields.forEach((field) => {
950
+ validFields.forEach((field) => {
928
951
  if (record[field] !== undefined) {
929
952
  filtered[field] = record[field];
930
- // Add display value if requested and available
931
- if (include_display_values && record[`${field}_display_value`]) {
932
- filtered[`${field}_display`] = record[`${field}_display_value`];
953
+ // FIX: Properly handle display values
954
+ if (include_display_values) {
955
+ // Check for standard display value pattern
956
+ const displayField = `dv_${field}`; // Some tables use dv_ prefix
957
+ const displayFieldAlt = `${field}_display_value`; // Others use _display_value suffix
958
+ if (record[displayField]) {
959
+ filtered[`${field}_display`] = record[displayField];
960
+ }
961
+ else if (record[displayFieldAlt]) {
962
+ filtered[`${field}_display`] = record[displayFieldAlt];
963
+ }
933
964
  }
934
965
  }
935
966
  });
936
967
  return filtered;
937
968
  });
969
+ // Add metadata about fields
970
+ result.fields_info = {
971
+ requested: fields,
972
+ valid: validFields,
973
+ invalid: fields.filter((f) => !validFields.includes(f))
974
+ };
938
975
  }
939
976
  }
940
977
  else {
@@ -955,18 +992,40 @@ class ServiceNowOperationsMCP {
955
992
  };
956
993
  }
957
994
  }
995
+ // ✅ NEW: Add pagination info when offset is used
996
+ if (offset > 0) {
997
+ result.pagination = {
998
+ offset: offset,
999
+ limit: effectiveLimit,
1000
+ has_more: records.success && records.data.result.length === effectiveLimit,
1001
+ next_offset: offset + effectiveLimit
1002
+ };
1003
+ }
958
1004
  return {
959
1005
  content: [
960
1006
  {
961
1007
  type: 'text',
962
- text: `Found ${records.success ? records.data.result.length : 0} ${table} records matching query: "${query}"\n\n${JSON.stringify(result, null, 2)}`
1008
+ text: `Found ${records.success ? records.data.result.length : 0} ${table} records${offset > 0 ? ` (offset: ${offset})` : ''} matching query: "${query || 'all'}"\n\n${JSON.stringify(result, null, 2)}`
963
1009
  }
964
1010
  ]
965
1011
  };
966
1012
  }
967
1013
  catch (error) {
1014
+ // ✅ IMPROVED: Better error messages
968
1015
  logger_js_1.logger.error(`Error querying ${table}:`, error);
969
- throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to query ${table}: ${error}`);
1016
+ if (error.code === 'ECONNREFUSED') {
1017
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Cannot connect to ServiceNow. Please check your instance URL and network connection.`);
1018
+ }
1019
+ if (error.response?.status === 401) {
1020
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, `Authentication failed. Please run: snow-flow auth login`);
1021
+ }
1022
+ if (error.response?.status === 404) {
1023
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Table '${table}' not found. Please check the table name.`);
1024
+ }
1025
+ if (error.response?.status === 400) {
1026
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Invalid query syntax: ${query}. Check ServiceNow encoded query format.`);
1027
+ }
1028
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to query ${table}: ${error.message || error}`);
970
1029
  }
971
1030
  }
972
1031
  detectCommonFields(table, records) {
@@ -256,44 +256,84 @@ class ServiceNowPlatformDevelopmentMCP {
256
256
  */
257
257
  async discoverTableFields(args) {
258
258
  try {
259
- const tableName = args.tableName;
259
+ const tableName = args.tableName || args.table_name; // Support both parameter names
260
+ if (!tableName) {
261
+ throw new Error('Table name is required (use tableName or table_name parameter)');
262
+ }
260
263
  this.logger.info(`Discovering fields for table: ${tableName}`);
261
264
  // First, resolve table name to sys_id if needed
262
265
  const tableInfo = await this.getTableInfo(tableName);
263
266
  if (!tableInfo) {
264
267
  throw new Error(`Table not found: ${tableName}`);
265
268
  }
266
- // Get all fields for this table
267
- const fieldsResponse = await this.client.searchRecords('sys_dictionary', `nameSTARTSWITH${tableInfo.name}^element!=NULL`, 100);
269
+ // Get all fields for this table with CORRECT query syntax
270
+ // FIX: Use proper ServiceNow query for dictionary
271
+ const fieldsResponse = await this.client.searchRecords('sys_dictionary', `name=${tableInfo.name}^element!=NULL^ORname=${tableInfo.name}^elementISNOTEMPTY`, 500 // Increased limit for tables with many fields
272
+ );
268
273
  if (!fieldsResponse.success || !fieldsResponse.data) {
269
274
  throw new Error(`Failed to get fields for table: ${tableName}`);
270
275
  }
271
- const fields = fieldsResponse.data.result.map((field) => ({
276
+ // IMPROVED: Better field mapping with validation
277
+ const fields = fieldsResponse.data.result
278
+ .filter((field) => field.element && field.element !== 'null' && field.element !== 'NULL')
279
+ .map((field) => ({
272
280
  name: field.element,
273
- type: field.internal_type,
274
- label: field.column_label,
275
- mandatory: field.mandatory === 'true',
276
- display: field.display === 'true',
277
- max_length: field.max_length,
278
- reference: field.reference,
279
- choice: field.choice
280
- }));
281
+ type: field.internal_type || field.data_type || 'string',
282
+ label: field.column_label || field.element,
283
+ mandatory: field.mandatory === 'true' || field.mandatory === true,
284
+ display: field.display === 'true' || field.display === true,
285
+ max_length: parseInt(field.max_length) || null,
286
+ reference: field.reference || null,
287
+ choice: field.choice || null,
288
+ default_value: field.default_value || null,
289
+ read_only: field.read_only === 'true' || field.read_only === true
290
+ }))
291
+ .sort((a, b) => {
292
+ // Sort: sys_id first, then mandatory fields, then alphabetically
293
+ if (a.name === 'sys_id')
294
+ return -1;
295
+ if (b.name === 'sys_id')
296
+ return 1;
297
+ if (a.mandatory && !b.mandatory)
298
+ return -1;
299
+ if (!a.mandatory && b.mandatory)
300
+ return 1;
301
+ return a.name.localeCompare(b.name);
302
+ });
281
303
  // Cache the table info
282
304
  this.tableCache.set(tableName, {
283
305
  name: tableInfo.name,
284
306
  label: tableInfo.label,
285
307
  fields: fields
286
308
  });
309
+ // ✅ NEW: Group fields by category for better readability
310
+ const mandatoryFields = fields.filter((f) => f.mandatory);
311
+ const referenceFields = fields.filter((f) => f.reference);
312
+ const regularFields = fields.filter((f) => !f.mandatory && !f.reference);
287
313
  return {
288
314
  content: [{
289
315
  type: 'text',
290
- text: `📋 Fields for ${tableInfo.label} (${tableInfo.name}):\n\n${fields.map((field) => `- **${field.name}** (${field.label})\n Type: ${field.type}${field.mandatory ? ' *Required*' : ''}${field.reference ? ` -> ${field.reference}` : ''}`).join('\n')}\n\n🔍 Total fields: ${fields.length}\n✨ All fields discovered dynamically from ServiceNow schema!`
316
+ text: `📋 Fields for ${tableInfo.label} (${tableInfo.name}):\n\n` +
317
+ (mandatoryFields.length > 0 ? `**Required Fields:**\n${mandatoryFields.map((field) => `- **${field.name}** (${field.label})\n Type: ${field.type}${field.max_length ? ` [max: ${field.max_length}]` : ''}${field.default_value ? ` = '${field.default_value}'` : ''}`).join('\n')}\n\n` : '') +
318
+ (referenceFields.length > 0 ? `**Reference Fields:**\n${referenceFields.map((field) => `- **${field.name}** (${field.label})\n → ${field.reference}${field.mandatory ? ' *Required*' : ''}`).join('\n')}\n\n` : '') +
319
+ (regularFields.length > 0 ? `**Other Fields:**\n${regularFields.slice(0, 20).map((field) => `- ${field.name} (${field.type}${field.read_only ? ', read-only' : ''})`).join('\n')}${regularFields.length > 20 ? `\n ... and ${regularFields.length - 20} more fields` : ''}\n\n` : '') +
320
+ `🔍 Total: ${fields.length} fields (${mandatoryFields.length} required, ${referenceFields.length} references)\n` +
321
+ `✨ All fields discovered dynamically from ServiceNow!\n\n` +
322
+ `💡 Tip: Use these field names in snow_query_table with fields parameter`
291
323
  }]
292
324
  };
293
325
  }
294
326
  catch (error) {
295
327
  this.logger.error('Failed to discover table fields:', error);
296
- throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to discover fields: ${error}`);
328
+ // IMPROVED: Better error messages
329
+ if (error.message?.includes('Table not found')) {
330
+ const tableName = args.tableName || args.table_name;
331
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Table '${tableName}' does not exist in ServiceNow. Please check the table name.`);
332
+ }
333
+ if (error.response?.status === 401) {
334
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, `Authentication required. Please run: snow-flow auth login`);
335
+ }
336
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to discover fields for ${args.tableName || args.table_name}: ${error.message || error}`);
297
337
  }
298
338
  }
299
339
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "snow-flow",
3
- "version": "3.3.5",
4
- "description": "Snow-Flow v3.3.5: CRITICAL ML FIX - Machine Learning MCP now fully functional with TensorFlow.js! Fixed pagination, ServiceNow API integration, data parsing, and training workflow. Real progress tracking, intelligent error messages, and automatic fallback when PA/PI not available. Train incident classifiers, predict change risks, forecast volumes, and detect anomalies - all working with real ServiceNow data. Enhanced MCP servers with real-time progress indicators and comprehensive operation logging. 180+ MCP tools across 17 specialized servers.",
3
+ "version": "3.3.7",
4
+ "description": "Snow-Flow v3.3.7: PORTAL PAGE FIX - Fixed portal_page deployment field mapping! Now correctly maps id->page_id, handles sp_portal sys_id, and converts containers to widgets format. Fixed widget instance creation with proper sys_id handling. Portal pages with AI chatbots and complex widgets now deploy successfully. Improved validation messages and support for both single widgets and widget arrays. 180+ MCP tools across 17 specialized servers.",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
7
7
  "bin": {