snow-flow 10.0.1-dev.362 → 10.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.1-dev.362",
3
+ "version": "10.0.2",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -27,3 +27,7 @@ export { toolDefinition as snow_deployment_debug_def, execute as snow_deployment
27
27
  export { toolDefinition as snow_deployment_status_def, execute as snow_deployment_status_exec } from './snow_deployment_status.js';
28
28
  export { toolDefinition as snow_rollback_deployment_def, execute as snow_rollback_deployment_exec } from './snow_rollback_deployment.js';
29
29
  export { toolDefinition as snow_validate_deployment_def, execute as snow_validate_deployment_exec } from './snow_validate_deployment.js';
30
+
31
+ // ==================== GitHub Pipeline Tools (Enterprise) ====================
32
+ export { toolDefinition as snow_github_tree_def, execute as snow_github_tree_exec } from './snow_github_tree.js';
33
+ export { toolDefinition as snow_github_deploy_def, execute as snow_github_deploy_exec } from './snow_github_deploy.js';
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Shared Artifact Constants
3
+ *
4
+ * Centralized mappings for ServiceNow artifact types, identifier fields,
5
+ * and file-to-field mappings used by snow_artifact_manage, snow_github_deploy,
6
+ * and other deployment tools.
7
+ */
8
+
9
+ // ==================== ARTIFACT TYPE → TABLE MAPPING ====================
10
+ export const ARTIFACT_TABLE_MAP: Record<string, string> = {
11
+ sp_widget: 'sp_widget',
12
+ widget: 'sp_widget',
13
+ sp_page: 'sp_page',
14
+ page: 'sp_page',
15
+ sys_ux_page: 'sys_ux_page',
16
+ uib_page: 'sys_ux_page',
17
+ script_include: 'sys_script_include',
18
+ business_rule: 'sys_script',
19
+ client_script: 'sys_script_client',
20
+ ui_policy: 'sys_ui_policy',
21
+ ui_action: 'sys_ui_action',
22
+ rest_message: 'sys_rest_message',
23
+ scheduled_job: 'sysauto_script',
24
+ transform_map: 'sys_transform_map',
25
+ fix_script: 'sys_script_fix',
26
+ table: 'sys_db_object',
27
+ field: 'sys_dictionary',
28
+ flow: 'sys_hub_flow',
29
+ application: 'sys_app'
30
+ };
31
+
32
+ // ==================== TABLE → IDENTIFIER FIELD MAPPING ====================
33
+ export const ARTIFACT_IDENTIFIER_FIELD: Record<string, string> = {
34
+ sp_widget: 'id',
35
+ sp_page: 'id',
36
+ sys_ux_page: 'name',
37
+ sys_script_include: 'name',
38
+ sys_script: 'name',
39
+ sys_script_client: 'name',
40
+ sys_ui_policy: 'short_description',
41
+ sys_ui_action: 'name',
42
+ sys_rest_message: 'name',
43
+ sysauto_script: 'name',
44
+ sys_transform_map: 'name',
45
+ sys_script_fix: 'name',
46
+ sys_db_object: 'name',
47
+ sys_dictionary: 'element',
48
+ sys_hub_flow: 'name',
49
+ sys_app: 'name'
50
+ };
51
+
52
+ // ==================== FILE MAPPINGS FOR IMPORT (artifact_directory / GitHub deploy) ====================
53
+ export const FILE_MAPPINGS: Record<string, Record<string, string[]>> = {
54
+ sp_widget: {
55
+ template: ['template.html', 'index.html', 'widget.html'],
56
+ script: ['server.js', 'server-script.js', 'script.js'],
57
+ client_script: ['client.js', 'client-script.js', 'controller.js'],
58
+ css: ['style.css', 'styles.css', 'widget.css'],
59
+ option_schema: ['options.json', 'option_schema.json', 'schema.json']
60
+ },
61
+ sys_script: { // business_rule
62
+ script: ['script.js', 'index.js', 'main.js'],
63
+ condition: ['condition.js', 'condition.txt']
64
+ },
65
+ sys_script_include: {
66
+ script: ['script.js', 'index.js', 'main.js', '{name}.js']
67
+ },
68
+ sys_script_client: {
69
+ script: ['script.js', 'client.js', 'index.js']
70
+ },
71
+ sp_page: {
72
+ // SP pages don't have script content
73
+ },
74
+ sys_ux_page: {
75
+ // UIB pages don't have script content in same way
76
+ },
77
+ sys_ui_action: {
78
+ script: ['script.js', 'action.js', 'index.js'],
79
+ condition: ['condition.js']
80
+ },
81
+ sysauto_script: { // scheduled_job
82
+ script: ['script.js', 'job.js', 'index.js']
83
+ },
84
+ sys_script_fix: {
85
+ script: ['script.js', 'fix.js', 'index.js']
86
+ }
87
+ };
88
+
89
+ // ==================== FILE MAPPINGS FOR EXPORT ====================
90
+ export const EXPORT_FILE_MAPPINGS: Record<string, Record<string, string>> = {
91
+ sp_widget: {
92
+ template: 'template.html',
93
+ script: 'server.js',
94
+ client_script: 'client.js',
95
+ css: 'style.css',
96
+ option_schema: 'options.json'
97
+ },
98
+ sys_script: { // business_rule
99
+ script: 'script.js',
100
+ condition: 'condition.js'
101
+ },
102
+ sys_script_include: {
103
+ script: 'script.js'
104
+ },
105
+ sys_script_client: {
106
+ script: 'script.js'
107
+ },
108
+ sys_ui_action: {
109
+ script: 'script.js',
110
+ condition: 'condition.js'
111
+ },
112
+ sysauto_script: {
113
+ script: 'script.js'
114
+ },
115
+ sys_script_fix: {
116
+ script: 'script.js'
117
+ }
118
+ };
119
+
120
+ // ==================== DEFAULT FILE EXTENSIONS PER ARTIFACT TYPE ====================
121
+ export const DEFAULT_FILE_EXTENSIONS: Record<string, string> = {
122
+ sys_script_include: '.js',
123
+ sys_script: '.js',
124
+ sys_script_client: '.js',
125
+ sys_ui_action: '.js',
126
+ sysauto_script: '.js',
127
+ sys_script_fix: '.js',
128
+ sp_widget: '', // Widgets use directory mode, not single-file
129
+ sp_page: '',
130
+ sys_ux_page: '',
131
+ };
@@ -15,116 +15,12 @@ import { createSuccessResult, createErrorResult, SnowFlowError, ErrorType } from
15
15
  import * as fs from 'fs/promises';
16
16
  import * as path from 'path';
17
17
  import { existsSync } from 'fs';
18
-
19
- // ==================== ARTIFACT TYPE MAPPING ====================
20
- const ARTIFACT_TABLE_MAP: Record<string, string> = {
21
- sp_widget: 'sp_widget',
22
- widget: 'sp_widget',
23
- sp_page: 'sp_page',
24
- page: 'sp_page',
25
- sys_ux_page: 'sys_ux_page',
26
- uib_page: 'sys_ux_page',
27
- script_include: 'sys_script_include',
28
- business_rule: 'sys_script',
29
- client_script: 'sys_script_client',
30
- ui_policy: 'sys_ui_policy',
31
- ui_action: 'sys_ui_action',
32
- rest_message: 'sys_rest_message',
33
- scheduled_job: 'sysauto_script',
34
- transform_map: 'sys_transform_map',
35
- fix_script: 'sys_script_fix',
36
- table: 'sys_db_object',
37
- field: 'sys_dictionary',
38
- flow: 'sys_hub_flow',
39
- application: 'sys_app'
40
- };
41
-
42
- const ARTIFACT_IDENTIFIER_FIELD: Record<string, string> = {
43
- sp_widget: 'id',
44
- sp_page: 'id',
45
- sys_ux_page: 'name',
46
- sys_script_include: 'name',
47
- sys_script: 'name',
48
- sys_script_client: 'name',
49
- sys_ui_policy: 'short_description',
50
- sys_ui_action: 'name',
51
- sys_rest_message: 'name',
52
- sysauto_script: 'name',
53
- sys_transform_map: 'name',
54
- sys_script_fix: 'name',
55
- sys_db_object: 'name',
56
- sys_dictionary: 'element',
57
- sys_hub_flow: 'name',
58
- sys_app: 'name'
59
- };
60
-
61
- // ==================== FILE MAPPINGS FOR IMPORT (artifact_directory) ====================
62
- const FILE_MAPPINGS: Record<string, Record<string, string[]>> = {
63
- sp_widget: {
64
- template: ['template.html', 'index.html', 'widget.html'],
65
- script: ['server.js', 'server-script.js', 'script.js'],
66
- client_script: ['client.js', 'client-script.js', 'controller.js'],
67
- css: ['style.css', 'styles.css', 'widget.css'],
68
- option_schema: ['options.json', 'option_schema.json', 'schema.json']
69
- },
70
- sys_script: { // business_rule
71
- script: ['script.js', 'index.js', 'main.js'],
72
- condition: ['condition.js', 'condition.txt']
73
- },
74
- sys_script_include: {
75
- script: ['script.js', 'index.js', 'main.js', '{name}.js']
76
- },
77
- sys_script_client: {
78
- script: ['script.js', 'client.js', 'index.js']
79
- },
80
- sp_page: {
81
- // SP pages don't have script content
82
- },
83
- sys_ux_page: {
84
- // UIB pages don't have script content in same way
85
- },
86
- sys_ui_action: {
87
- script: ['script.js', 'action.js', 'index.js'],
88
- condition: ['condition.js']
89
- },
90
- sysauto_script: { // scheduled_job
91
- script: ['script.js', 'job.js', 'index.js']
92
- },
93
- sys_script_fix: {
94
- script: ['script.js', 'fix.js', 'index.js']
95
- }
96
- };
97
-
98
- // ==================== FILE MAPPINGS FOR EXPORT ====================
99
- const EXPORT_FILE_MAPPINGS: Record<string, Record<string, string>> = {
100
- sp_widget: {
101
- template: 'template.html',
102
- script: 'server.js',
103
- client_script: 'client.js',
104
- css: 'style.css',
105
- option_schema: 'options.json'
106
- },
107
- sys_script: { // business_rule
108
- script: 'script.js',
109
- condition: 'condition.js'
110
- },
111
- sys_script_include: {
112
- script: 'script.js'
113
- },
114
- sys_script_client: {
115
- script: 'script.js'
116
- },
117
- sys_ui_action: {
118
- script: 'script.js',
119
- condition: 'condition.js'
120
- },
121
- sysauto_script: {
122
- script: 'script.js'
123
- },
124
- sys_script_fix: {
125
- script: 'script.js'
126
- }
127
- };
18
+ import {
19
+ ARTIFACT_TABLE_MAP,
20
+ ARTIFACT_IDENTIFIER_FIELD,
21
+ FILE_MAPPINGS,
22
+ EXPORT_FILE_MAPPINGS,
23
+ } from './shared/artifact-constants.js';
128
24
 
129
25
  export const toolDefinition: MCPToolDefinition = {
130
26
  name: 'snow_artifact_manage',
@@ -212,31 +108,31 @@ export const toolDefinition: MCPToolDefinition = {
212
108
  },
213
109
  description: {
214
110
  type: 'string',
215
- description: '[create] Artifact description'
111
+ description: '[create/update] Artifact description'
216
112
  },
217
113
  template: {
218
114
  type: 'string',
219
- description: '[create] HTML template (for widgets/pages)'
115
+ description: '[create/update] HTML template (for widgets/pages)'
220
116
  },
221
117
  server_script: {
222
118
  type: 'string',
223
- description: '[create] Server-side script (ES5 only!)'
119
+ description: '[create/update] Server-side script (ES5 only!)'
224
120
  },
225
121
  client_script: {
226
122
  type: 'string',
227
- description: '[create] Client-side script'
123
+ description: '[create/update] Client-side script'
228
124
  },
229
125
  css: {
230
126
  type: 'string',
231
- description: '[create] CSS stylesheet (for widgets)'
127
+ description: '[create/update] CSS stylesheet (for widgets)'
232
128
  },
233
129
  option_schema: {
234
130
  type: 'string',
235
- description: '[create] Option schema JSON (for widgets)'
131
+ description: '[create/update] Option schema JSON (for widgets)'
236
132
  },
237
133
  script: {
238
134
  type: 'string',
239
- description: '[create] Script content (for script includes, business rules, etc.)'
135
+ description: '[create/update] Script content (for script includes, business rules, etc.)'
240
136
  },
241
137
  api_name: {
242
138
  type: 'string',
@@ -1094,8 +990,17 @@ async function executeUpdate(args: any, context: ServiceNowContext, tableName: s
1094
990
  return createErrorResult(fileResolution.error);
1095
991
  }
1096
992
 
1097
- // Merge: config values (highest priority) > file-based content
1098
- const mergedConfig = { ...fileResolution.resolvedFields, ...config };
993
+ // Extract inline params from args (same fields supported by create)
994
+ const inlineParams: Record<string, any> = {};
995
+ const inlineFieldKeys = ['script', 'template', 'server_script', 'client_script', 'css', 'option_schema', 'description', 'active'];
996
+ for (const key of inlineFieldKeys) {
997
+ if (args[key] !== undefined) {
998
+ inlineParams[key] = args[key];
999
+ }
1000
+ }
1001
+
1002
+ // Merge priority: config (highest) > inline params > _file params > artifact_directory (lowest)
1003
+ const mergedConfig = { ...fileResolution.resolvedFields, ...inlineParams, ...config };
1099
1004
 
1100
1005
  if (Object.keys(mergedConfig).length === 0) {
1101
1006
  return createErrorResult('No update content provided. Use config object and/or file parameters (script_file, template_file, artifact_directory, etc.)');
@@ -0,0 +1,630 @@
1
+ /**
2
+ * snow_github_deploy - GitHub → ServiceNow Deployment Pipeline (Enterprise)
3
+ *
4
+ * Deploys files directly from a GitHub repository to ServiceNow without
5
+ * file content passing through the LLM context window. Content flows:
6
+ *
7
+ * tool process → proxyToolCall('github_get_content') → enterprise server → GitHub API
8
+ * tool process → getAuthenticatedClient() → ServiceNow Table API
9
+ *
10
+ * Three deployment modes:
11
+ * 1. Single file: Deploy one file to a specific artifact field
12
+ * 2. Directory → Widget: Auto-map directory files to widget fields via FILE_MAPPINGS
13
+ * 3. Bulk: Deploy all matching files in a directory as separate artifacts
14
+ *
15
+ * Requires enterprise license with 'github' feature enabled.
16
+ * GitHub PAT is managed server-side via the enterprise portal.
17
+ */
18
+
19
+ import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
20
+ import { getAuthenticatedClient } from '../../shared/auth.js';
21
+ import { createSuccessResult, createErrorResult } from '../../shared/error-handler.js';
22
+ import { proxyToolCall } from '../../../enterprise-proxy/proxy.js';
23
+ import {
24
+ ARTIFACT_TABLE_MAP,
25
+ ARTIFACT_IDENTIFIER_FIELD,
26
+ FILE_MAPPINGS,
27
+ DEFAULT_FILE_EXTENSIONS,
28
+ } from './shared/artifact-constants.js';
29
+
30
+ export const toolDefinition: MCPToolDefinition = {
31
+ name: 'snow_github_deploy',
32
+ description: `Deploy files directly from GitHub to ServiceNow — content never passes through LLM context.
33
+
34
+ Three modes:
35
+ 1. Single file: source_path → one artifact field
36
+ 2. Directory → Widget: source_directory auto-maps files (template.html→template, server.js→script, etc.)
37
+ 3. Bulk: source_directory + bulk=true → each file becomes a separate artifact
38
+
39
+ Requires: Enterprise license with 'github' feature. GitHub PAT is managed server-side.
40
+
41
+ Supported target types: widget, script_include, business_rule, client_script, ui_action, scheduled_job, fix_script, etc.`,
42
+ category: 'development',
43
+ subcategory: 'deployment',
44
+ use_cases: ['github', 'deployment', 'pipeline', 'widgets', 'scripts'],
45
+ complexity: 'intermediate',
46
+ frequency: 'high',
47
+
48
+ permission: 'write',
49
+ allowedRoles: ['developer', 'admin'],
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ owner: {
54
+ type: 'string',
55
+ description: 'GitHub owner or organization'
56
+ },
57
+ repo: {
58
+ type: 'string',
59
+ description: 'Repository name'
60
+ },
61
+ branch: {
62
+ type: 'string',
63
+ description: 'Branch name (default: "main")'
64
+ },
65
+ source_path: {
66
+ type: 'string',
67
+ description: '[Mode 1] Path to a single file in the repo (e.g. "scripts/MyScript.js")'
68
+ },
69
+ source_directory: {
70
+ type: 'string',
71
+ description: '[Mode 2/3] Path to a directory in the repo (e.g. "widgets/my_widget/")'
72
+ },
73
+ target_type: {
74
+ type: 'string',
75
+ description: 'ServiceNow artifact type',
76
+ enum: [
77
+ 'sp_widget', 'widget', 'sp_page', 'page', 'sys_ux_page', 'uib_page',
78
+ 'script_include', 'business_rule', 'client_script', 'ui_policy',
79
+ 'ui_action', 'rest_message', 'scheduled_job', 'transform_map',
80
+ 'fix_script', 'flow', 'application'
81
+ ]
82
+ },
83
+ target_identifier: {
84
+ type: 'string',
85
+ description: '[Mode 1/2] Name or ID of the target artifact in ServiceNow'
86
+ },
87
+ target_sys_id: {
88
+ type: 'string',
89
+ description: 'Alternative: sys_id of the target artifact'
90
+ },
91
+ target_field: {
92
+ type: 'string',
93
+ description: '[Mode 1] Which field to update (auto-detected if omitted, e.g. "script", "template")'
94
+ },
95
+ upsert: {
96
+ type: 'boolean',
97
+ description: 'Create artifact if it does not exist (default: true)'
98
+ },
99
+ bulk: {
100
+ type: 'boolean',
101
+ description: '[Mode 3] Deploy each file in directory as a separate artifact (default: false)'
102
+ },
103
+ file_extension_filter: {
104
+ type: 'string',
105
+ description: '[Mode 3] Filter files by extension in bulk mode (e.g. ".js")'
106
+ },
107
+ field_mapping: {
108
+ type: 'object',
109
+ description: '[Mode 2] Custom filename→field mapping override (e.g. {"my-template.html": "template"})'
110
+ },
111
+ dry_run: {
112
+ type: 'boolean',
113
+ description: 'Preview what would be deployed without actually deploying (default: false)'
114
+ }
115
+ },
116
+ required: ['owner', 'repo', 'target_type']
117
+ }
118
+ };
119
+
120
+ export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
121
+ const {
122
+ owner,
123
+ repo,
124
+ branch = 'main',
125
+ source_path,
126
+ source_directory,
127
+ target_type,
128
+ target_identifier,
129
+ target_sys_id,
130
+ target_field,
131
+ upsert = true,
132
+ bulk = false,
133
+ file_extension_filter,
134
+ field_mapping,
135
+ dry_run = false
136
+ } = args;
137
+
138
+ // Enterprise feature gate
139
+ if (!context.enterprise?.features?.includes('github')) {
140
+ return createErrorResult(
141
+ 'GitHub integration requires an enterprise license with the "github" feature enabled.\n\n' +
142
+ 'Upgrade at https://portal.snow-flow.dev or contact support.'
143
+ );
144
+ }
145
+
146
+ // Validate artifact type
147
+ const tableName = ARTIFACT_TABLE_MAP[target_type];
148
+ if (!tableName) {
149
+ return createErrorResult(
150
+ `Unsupported target_type: ${target_type}. Valid types: ${Object.keys(ARTIFACT_TABLE_MAP).join(', ')}`
151
+ );
152
+ }
153
+
154
+ // Determine mode
155
+ if (source_path) {
156
+ return await deploySingleFile(args, context, tableName);
157
+ } else if (source_directory && bulk) {
158
+ return await deployBulk(args, context, tableName);
159
+ } else if (source_directory) {
160
+ return await deployDirectory(args, context, tableName);
161
+ } else {
162
+ return createErrorResult('Provide either source_path (single file) or source_directory (directory/bulk mode).');
163
+ }
164
+ }
165
+
166
+ // ==================== MODE 1: SINGLE FILE ====================
167
+ async function deploySingleFile(args: any, context: ServiceNowContext, tableName: string): Promise<ToolResult> {
168
+ const {
169
+ owner, repo, branch = 'main', source_path, target_type,
170
+ target_identifier, target_sys_id, target_field, upsert = true, dry_run = false
171
+ } = args;
172
+
173
+ if (!target_identifier && !target_sys_id) {
174
+ return createErrorResult('target_identifier or target_sys_id is required for single-file mode.');
175
+ }
176
+
177
+ // Fetch file content from GitHub via enterprise proxy
178
+ let fileContent: string;
179
+ try {
180
+ const result = await proxyToolCall('github_get_content', {
181
+ owner, repo, path: source_path, ref: branch
182
+ });
183
+
184
+ // GitHub API returns base64-encoded content for files
185
+ if (result.content && result.encoding === 'base64') {
186
+ fileContent = Buffer.from(result.content, 'base64').toString('utf-8');
187
+ } else if (typeof result.content === 'string') {
188
+ fileContent = result.content;
189
+ } else if (typeof result === 'string') {
190
+ fileContent = result;
191
+ } else {
192
+ return createErrorResult(`Unexpected response format from GitHub for ${source_path}. Expected file content.`);
193
+ }
194
+ } catch (error: any) {
195
+ return createErrorResult(`Failed to fetch ${source_path} from ${owner}/${repo}: ${error.message}`);
196
+ }
197
+
198
+ // Determine target field
199
+ const resolvedField = target_field || autoDetectField(source_path, tableName);
200
+ if (!resolvedField) {
201
+ return createErrorResult(
202
+ `Cannot auto-detect target field for "${source_path}" on ${target_type}. Specify target_field explicitly.`
203
+ );
204
+ }
205
+
206
+ if (dry_run) {
207
+ return createSuccessResult({
208
+ mode: 'single_file',
209
+ dry_run: true,
210
+ source: { owner, repo, branch, path: source_path, size: fileContent.length },
211
+ target: { type: target_type, table: tableName, identifier: target_identifier || target_sys_id, field: resolvedField },
212
+ message: 'Dry run — no changes made.'
213
+ });
214
+ }
215
+
216
+ // Deploy to ServiceNow
217
+ const client = await getAuthenticatedClient(context);
218
+ const deployResult = await upsertArtifact(
219
+ client, context, tableName, target_type,
220
+ target_sys_id, target_identifier,
221
+ { [resolvedField]: fileContent },
222
+ upsert
223
+ );
224
+
225
+ return createSuccessResult({
226
+ mode: 'single_file',
227
+ deployed: true,
228
+ source: { owner, repo, branch, path: source_path, size: fileContent.length },
229
+ target: {
230
+ type: target_type,
231
+ table: tableName,
232
+ sys_id: deployResult.sys_id,
233
+ name: deployResult.name,
234
+ field: resolvedField,
235
+ action: deployResult.action
236
+ },
237
+ url: `${context.instanceUrl}/nav_to.do?uri=${tableName}.do?sys_id=${deployResult.sys_id}`
238
+ });
239
+ }
240
+
241
+ // ==================== MODE 2: DIRECTORY → WIDGET ====================
242
+ async function deployDirectory(args: any, context: ServiceNowContext, tableName: string): Promise<ToolResult> {
243
+ const {
244
+ owner, repo, branch = 'main', source_directory, target_type,
245
+ target_identifier, target_sys_id, field_mapping, upsert = true, dry_run = false
246
+ } = args;
247
+
248
+ if (!target_identifier && !target_sys_id) {
249
+ return createErrorResult('target_identifier or target_sys_id is required for directory mode.');
250
+ }
251
+
252
+ // List directory contents from GitHub
253
+ let dirEntries: any[];
254
+ try {
255
+ const result = await proxyToolCall('github_get_content', {
256
+ owner, repo, path: source_directory, ref: branch
257
+ });
258
+ dirEntries = Array.isArray(result) ? result : [];
259
+ } catch (error: any) {
260
+ return createErrorResult(`Failed to list ${source_directory} in ${owner}/${repo}: ${error.message}`);
261
+ }
262
+
263
+ // Build file→field mapping: custom override or auto-map via FILE_MAPPINGS
264
+ const fileMappings = FILE_MAPPINGS[tableName] || {};
265
+ const resolvedMapping: Record<string, string> = {}; // filename → field
266
+
267
+ for (const entry of dirEntries) {
268
+ if (entry.type !== 'file') continue;
269
+ const filename = entry.name || entry.path?.split('/').pop();
270
+ if (!filename) continue;
271
+
272
+ // Check custom field_mapping first
273
+ if (field_mapping && field_mapping[filename]) {
274
+ resolvedMapping[filename] = field_mapping[filename];
275
+ continue;
276
+ }
277
+
278
+ // Auto-map via FILE_MAPPINGS
279
+ for (const [field, filenames] of Object.entries(fileMappings)) {
280
+ if ((filenames as string[]).includes(filename)) {
281
+ resolvedMapping[filename] = field;
282
+ break;
283
+ }
284
+ }
285
+ }
286
+
287
+ if (Object.keys(resolvedMapping).length === 0) {
288
+ return createErrorResult(
289
+ `No files in ${source_directory} matched field mappings for ${target_type}. ` +
290
+ `Expected files: ${JSON.stringify(fileMappings)}. ` +
291
+ `Found: ${dirEntries.filter((e: any) => e.type === 'file').map((e: any) => e.name).join(', ')}`
292
+ );
293
+ }
294
+
295
+ if (dry_run) {
296
+ return createSuccessResult({
297
+ mode: 'directory',
298
+ dry_run: true,
299
+ source: { owner, repo, branch, directory: source_directory },
300
+ mapping: resolvedMapping,
301
+ target: { type: target_type, table: tableName, identifier: target_identifier || target_sys_id },
302
+ message: 'Dry run — no changes made.'
303
+ });
304
+ }
305
+
306
+ // Fetch each mapped file and combine into one update payload
307
+ const updatePayload: Record<string, string> = {};
308
+ const fetchedFiles: string[] = [];
309
+
310
+ for (const [filename, field] of Object.entries(resolvedMapping)) {
311
+ const filePath = source_directory.replace(/\/$/, '') + '/' + filename;
312
+ try {
313
+ const result = await proxyToolCall('github_get_content', {
314
+ owner, repo, path: filePath, ref: branch
315
+ });
316
+
317
+ let content: string;
318
+ if (result.content && result.encoding === 'base64') {
319
+ content = Buffer.from(result.content, 'base64').toString('utf-8');
320
+ } else if (typeof result.content === 'string') {
321
+ content = result.content;
322
+ } else if (typeof result === 'string') {
323
+ content = result;
324
+ } else {
325
+ continue;
326
+ }
327
+
328
+ updatePayload[field] = content;
329
+ fetchedFiles.push(`${filename} → ${field} (${content.length} chars)`);
330
+ } catch (error: any) {
331
+ fetchedFiles.push(`${filename} → ${field} (FAILED: ${error.message})`);
332
+ }
333
+ }
334
+
335
+ if (Object.keys(updatePayload).length === 0) {
336
+ return createErrorResult('Failed to fetch any matched files from GitHub.');
337
+ }
338
+
339
+ // Deploy combined payload to ServiceNow
340
+ const client = await getAuthenticatedClient(context);
341
+ const deployResult = await upsertArtifact(
342
+ client, context, tableName, target_type,
343
+ target_sys_id, target_identifier,
344
+ updatePayload,
345
+ upsert
346
+ );
347
+
348
+ return createSuccessResult({
349
+ mode: 'directory',
350
+ deployed: true,
351
+ source: { owner, repo, branch, directory: source_directory },
352
+ files: fetchedFiles,
353
+ fields_updated: Object.keys(updatePayload),
354
+ target: {
355
+ type: target_type,
356
+ table: tableName,
357
+ sys_id: deployResult.sys_id,
358
+ name: deployResult.name,
359
+ action: deployResult.action
360
+ },
361
+ url: `${context.instanceUrl}/nav_to.do?uri=${tableName}.do?sys_id=${deployResult.sys_id}`
362
+ });
363
+ }
364
+
365
+ // ==================== MODE 3: BULK ====================
366
+ async function deployBulk(args: any, context: ServiceNowContext, tableName: string): Promise<ToolResult> {
367
+ const {
368
+ owner, repo, branch = 'main', source_directory, target_type,
369
+ file_extension_filter, upsert = true, dry_run = false
370
+ } = args;
371
+
372
+ // Determine extension filter
373
+ const extFilter = file_extension_filter || DEFAULT_FILE_EXTENSIONS[tableName] || '.js';
374
+
375
+ // List directory contents
376
+ let dirEntries: any[];
377
+ try {
378
+ const result = await proxyToolCall('github_get_content', {
379
+ owner, repo, path: source_directory, ref: branch
380
+ });
381
+ dirEntries = Array.isArray(result) ? result : [];
382
+ } catch (error: any) {
383
+ return createErrorResult(`Failed to list ${source_directory} in ${owner}/${repo}: ${error.message}`);
384
+ }
385
+
386
+ // Filter files by extension
387
+ const matchedFiles = dirEntries.filter((entry: any) => {
388
+ if (entry.type !== 'file') return false;
389
+ const name = entry.name || entry.path?.split('/').pop() || '';
390
+ return name.endsWith(extFilter);
391
+ });
392
+
393
+ if (matchedFiles.length === 0) {
394
+ return createErrorResult(
395
+ `No files matching "${extFilter}" found in ${source_directory}. ` +
396
+ `Found: ${dirEntries.filter((e: any) => e.type === 'file').map((e: any) => e.name).join(', ') || '(empty)'}`
397
+ );
398
+ }
399
+
400
+ if (dry_run) {
401
+ return createSuccessResult({
402
+ mode: 'bulk',
403
+ dry_run: true,
404
+ source: { owner, repo, branch, directory: source_directory },
405
+ files: matchedFiles.map((f: any) => ({
406
+ filename: f.name,
407
+ artifact_name: deriveArtifactName(f.name),
408
+ size: f.size
409
+ })),
410
+ filter: extFilter,
411
+ target: { type: target_type, table: tableName },
412
+ message: `Dry run — would deploy ${matchedFiles.length} files.`
413
+ });
414
+ }
415
+
416
+ // Deploy each file as a separate artifact
417
+ const client = await getAuthenticatedClient(context);
418
+ const results: any[] = [];
419
+ let successCount = 0;
420
+ let failCount = 0;
421
+
422
+ // Determine which field to populate based on artifact type
423
+ const scriptField = getDefaultScriptField(tableName);
424
+
425
+ for (const file of matchedFiles) {
426
+ const filename = file.name || file.path?.split('/').pop();
427
+ const artifactName = deriveArtifactName(filename);
428
+ const filePath = source_directory.replace(/\/$/, '') + '/' + filename;
429
+
430
+ try {
431
+ // Fetch file content
432
+ const result = await proxyToolCall('github_get_content', {
433
+ owner, repo, path: filePath, ref: branch
434
+ });
435
+
436
+ let content: string;
437
+ if (result.content && result.encoding === 'base64') {
438
+ content = Buffer.from(result.content, 'base64').toString('utf-8');
439
+ } else if (typeof result.content === 'string') {
440
+ content = result.content;
441
+ } else if (typeof result === 'string') {
442
+ content = result;
443
+ } else {
444
+ results.push({ filename, artifact_name: artifactName, success: false, error: 'Unexpected response format' });
445
+ failCount++;
446
+ continue;
447
+ }
448
+
449
+ // Upsert to ServiceNow
450
+ const deployResult = await upsertArtifact(
451
+ client, context, tableName, target_type,
452
+ undefined, artifactName,
453
+ { [scriptField]: content, name: artifactName },
454
+ upsert
455
+ );
456
+
457
+ results.push({
458
+ filename,
459
+ artifact_name: artifactName,
460
+ success: true,
461
+ sys_id: deployResult.sys_id,
462
+ action: deployResult.action,
463
+ size: content.length
464
+ });
465
+ successCount++;
466
+ } catch (error: any) {
467
+ results.push({
468
+ filename,
469
+ artifact_name: artifactName,
470
+ success: false,
471
+ error: error.message
472
+ });
473
+ failCount++;
474
+ }
475
+ }
476
+
477
+ return createSuccessResult({
478
+ mode: 'bulk',
479
+ deployed: true,
480
+ source: { owner, repo, branch, directory: source_directory, filter: extFilter },
481
+ target: { type: target_type, table: tableName },
482
+ summary: {
483
+ total: matchedFiles.length,
484
+ success: successCount,
485
+ failed: failCount
486
+ },
487
+ results
488
+ });
489
+ }
490
+
491
+ // ==================== HELPER FUNCTIONS ====================
492
+
493
+ /**
494
+ * Auto-detect the ServiceNow field based on file extension and artifact type
495
+ */
496
+ function autoDetectField(filePath: string, tableName: string): string | null {
497
+ const ext = filePath.split('.').pop()?.toLowerCase();
498
+ const filename = filePath.split('/').pop()?.toLowerCase() || '';
499
+
500
+ // Check FILE_MAPPINGS for exact filename match
501
+ const mappings = FILE_MAPPINGS[tableName];
502
+ if (mappings) {
503
+ for (const [field, filenames] of Object.entries(mappings)) {
504
+ if ((filenames as string[]).some(f => filename === f || filename.endsWith(f))) {
505
+ return field;
506
+ }
507
+ }
508
+ }
509
+
510
+ // Fallback: extension-based detection
511
+ if (ext === 'html') return 'template';
512
+ if (ext === 'css') return 'css';
513
+ if (ext === 'json' && filename.includes('option')) return 'option_schema';
514
+ if (ext === 'js') return getDefaultScriptField(tableName);
515
+
516
+ return null;
517
+ }
518
+
519
+ /**
520
+ * Get the default script field name for a given table
521
+ */
522
+ function getDefaultScriptField(tableName: string): string {
523
+ // Widgets use 'script' for server-side (confusingly, not 'server_script')
524
+ if (tableName === 'sp_widget') return 'script';
525
+ return 'script';
526
+ }
527
+
528
+ /**
529
+ * Derive artifact name from filename (strip extension)
530
+ */
531
+ function deriveArtifactName(filename: string): string {
532
+ // Remove extension
533
+ const lastDot = filename.lastIndexOf('.');
534
+ return lastDot > 0 ? filename.substring(0, lastDot) : filename;
535
+ }
536
+
537
+ /**
538
+ * Find or create an artifact in ServiceNow
539
+ */
540
+ async function upsertArtifact(
541
+ client: any,
542
+ context: ServiceNowContext,
543
+ tableName: string,
544
+ artifactType: string,
545
+ sysId: string | undefined,
546
+ identifier: string | undefined,
547
+ fields: Record<string, string>,
548
+ upsert: boolean
549
+ ): Promise<{ sys_id: string; name: string; action: 'created' | 'updated' }> {
550
+ const identifierField = ARTIFACT_IDENTIFIER_FIELD[tableName] || 'name';
551
+
552
+ // Try to find existing artifact
553
+ let existingArtifact: any = null;
554
+
555
+ if (sysId) {
556
+ try {
557
+ const response = await client.get(`/api/now/table/${tableName}/${sysId}`, {
558
+ params: { sysparm_fields: 'sys_id,name,id' }
559
+ });
560
+ existingArtifact = response.data.result;
561
+ } catch (e) {
562
+ // Not found by sys_id
563
+ }
564
+ }
565
+
566
+ if (!existingArtifact && identifier) {
567
+ const queryParts = [`${identifierField}=${identifier}`];
568
+ if (tableName === 'sp_widget') {
569
+ queryParts.push(`id=${identifier}`);
570
+ }
571
+ queryParts.push(`name=${identifier}`);
572
+
573
+ try {
574
+ const response = await client.get(`/api/now/table/${tableName}`, {
575
+ params: {
576
+ sysparm_query: queryParts.join('^OR'),
577
+ sysparm_fields: 'sys_id,name,id',
578
+ sysparm_limit: 1
579
+ }
580
+ });
581
+
582
+ if (response.data.result && response.data.result.length > 0) {
583
+ existingArtifact = response.data.result[0];
584
+ }
585
+ } catch (e) {
586
+ // Not found
587
+ }
588
+ }
589
+
590
+ if (existingArtifact) {
591
+ // Update existing
592
+ const updateResponse = await client.patch(
593
+ `/api/now/table/${tableName}/${existingArtifact.sys_id}`,
594
+ fields
595
+ );
596
+ return {
597
+ sys_id: existingArtifact.sys_id,
598
+ name: updateResponse.data.result?.name || existingArtifact.name || existingArtifact.id || identifier || '',
599
+ action: 'updated'
600
+ };
601
+ }
602
+
603
+ if (!upsert) {
604
+ throw new Error(`Artifact '${identifier || sysId}' not found and upsert=false.`);
605
+ }
606
+
607
+ // Create new artifact
608
+ // Ensure identifier fields are set
609
+ const createData: Record<string, any> = { ...fields };
610
+ if (identifier) {
611
+ if (identifierField === 'id') {
612
+ createData.id = identifier;
613
+ if (!createData.name) createData.name = identifier;
614
+ } else {
615
+ createData[identifierField] = identifier;
616
+ }
617
+ }
618
+
619
+ const createResponse = await client.post(`/api/now/table/${tableName}`, createData);
620
+ const created = createResponse.data.result;
621
+
622
+ return {
623
+ sys_id: created.sys_id,
624
+ name: created.name || created.id || identifier || '',
625
+ action: 'created'
626
+ };
627
+ }
628
+
629
+ export const version = '1.0.0';
630
+ export const author = 'Snow-Flow Enterprise GitHub Pipeline';
@@ -0,0 +1,120 @@
1
+ /**
2
+ * snow_github_tree - GitHub Repository Tree Browser (Enterprise)
3
+ *
4
+ * Retrieves directory structure of a GitHub repository via the enterprise proxy.
5
+ * Returns metadata only (paths, types, sizes) — no file content enters the LLM context.
6
+ *
7
+ * Requires enterprise license with 'github' feature enabled.
8
+ * GitHub PAT is managed server-side via the enterprise portal.
9
+ */
10
+
11
+ import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
12
+ import { createSuccessResult, createErrorResult } from '../../shared/error-handler.js';
13
+ import { proxyToolCall } from '../../../enterprise-proxy/proxy.js';
14
+
15
+ export const toolDefinition: MCPToolDefinition = {
16
+ name: 'snow_github_tree',
17
+ description: `Browse GitHub repository structure via enterprise proxy. Returns metadata only (paths, types, sizes) — no file content in LLM context.
18
+
19
+ Use this to explore a repo before deploying files with snow_github_deploy.
20
+
21
+ Requires: Enterprise license with 'github' feature. GitHub PAT is managed server-side.`,
22
+ category: 'development',
23
+ subcategory: 'deployment',
24
+ use_cases: ['github', 'deployment', 'browse', 'repository'],
25
+ complexity: 'beginner',
26
+ frequency: 'medium',
27
+
28
+ permission: 'read',
29
+ allowedRoles: ['developer', 'stakeholder', 'admin'],
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ owner: {
34
+ type: 'string',
35
+ description: 'GitHub owner or organization (e.g. "groeimetai")'
36
+ },
37
+ repo: {
38
+ type: 'string',
39
+ description: 'Repository name (e.g. "snow-flow")'
40
+ },
41
+ path: {
42
+ type: 'string',
43
+ description: 'Subdirectory path to browse (default: root "/")'
44
+ },
45
+ branch: {
46
+ type: 'string',
47
+ description: 'Branch name (default: "main")'
48
+ }
49
+ },
50
+ required: ['owner', 'repo']
51
+ }
52
+ };
53
+
54
+ export async function execute(args: any, context: ServiceNowContext): Promise<ToolResult> {
55
+ const { owner, repo, path: dirPath, branch } = args;
56
+
57
+ // Enterprise feature gate
58
+ if (!context.enterprise?.features?.includes('github')) {
59
+ return createErrorResult(
60
+ 'GitHub integration requires an enterprise license with the "github" feature enabled.\n\n' +
61
+ 'Upgrade at https://portal.snow-flow.dev or contact support.'
62
+ );
63
+ }
64
+
65
+ try {
66
+ // Call enterprise proxy to get directory listing from GitHub API
67
+ const result = await proxyToolCall('github_get_content', {
68
+ owner,
69
+ repo,
70
+ path: dirPath || '/',
71
+ ref: branch || 'main'
72
+ });
73
+
74
+ // GitHub API returns an array for directories, an object for files
75
+ const entries = Array.isArray(result) ? result : [result];
76
+
77
+ // Filter to metadata only — strip file content
78
+ let totalSizeBytes = 0;
79
+ let fileCount = 0;
80
+ let dirCount = 0;
81
+
82
+ const tree = entries.map((entry: any) => {
83
+ const item: any = {
84
+ path: entry.path || entry.name,
85
+ type: entry.type, // 'file' or 'dir'
86
+ size: entry.size || 0
87
+ };
88
+
89
+ if (entry.type === 'file') {
90
+ fileCount++;
91
+ totalSizeBytes += entry.size || 0;
92
+ } else if (entry.type === 'dir') {
93
+ dirCount++;
94
+ }
95
+
96
+ return item;
97
+ });
98
+
99
+ return createSuccessResult({
100
+ owner,
101
+ repo,
102
+ path: dirPath || '/',
103
+ branch: branch || 'main',
104
+ entries: tree,
105
+ summary: {
106
+ files: fileCount,
107
+ directories: dirCount,
108
+ total_size_bytes: totalSizeBytes
109
+ }
110
+ });
111
+
112
+ } catch (error: any) {
113
+ return createErrorResult(
114
+ `Failed to browse GitHub repository ${owner}/${repo}: ${error.message}`
115
+ );
116
+ }
117
+ }
118
+
119
+ export const version = '1.0.0';
120
+ export const author = 'Snow-Flow Enterprise GitHub Pipeline';