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 +1 -1
- package/src/servicenow/servicenow-mcp-unified/tools/deployment/index.ts +4 -0
- package/src/servicenow/servicenow-mcp-unified/tools/deployment/shared/artifact-constants.ts +131 -0
- package/src/servicenow/servicenow-mcp-unified/tools/deployment/snow_artifact_manage.ts +24 -119
- package/src/servicenow/servicenow-mcp-unified/tools/deployment/snow_github_deploy.ts +630 -0
- package/src/servicenow/servicenow-mcp-unified/tools/deployment/snow_github_tree.ts +120 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
//
|
|
1098
|
-
const
|
|
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';
|