chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+199
View File
@@ -0,0 +1,199 @@
import { createLogger } from '@sim/logger'
import type {
ServiceNowAggregateParams,
ServiceNowAggregateResponse,
} from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowAggregateTool')
export const aggregateTool: ToolConfig<ServiceNowAggregateParams, ServiceNowAggregateResponse> = {
id: 'servicenow_aggregate',
name: 'Aggregate ServiceNow Records',
description:
'Compute aggregate statistics (count, sum, average, min, max, group by) over a ServiceNow table',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., incident, change_request, task)',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Encoded query string to filter records before aggregating (e.g., "active=true")',
},
count: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Return the count of matching records',
},
groupBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to group results by (e.g., category,priority)',
},
avgFields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated numeric fields to average (e.g., reassignment_count)',
},
sumFields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated numeric fields to sum',
},
minFields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to compute the minimum of',
},
maxFields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated fields to compute the maximum of',
},
having: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter on aggregate results (e.g., "count>5")',
},
displayValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return display values for grouped reference fields: "true", "false", or "all"',
},
},
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const url = `${baseUrl}/api/now/stats/${params.tableName.trim()}`
const queryParams = new URLSearchParams()
if (params.query) {
queryParams.append('sysparm_query', params.query)
}
if (params.count) {
queryParams.append('sysparm_count', 'true')
}
if (params.groupBy) {
queryParams.append('sysparm_group_by', params.groupBy)
}
if (params.avgFields) {
queryParams.append('sysparm_avg_fields', params.avgFields)
}
if (params.sumFields) {
queryParams.append('sysparm_sum_fields', params.sumFields)
}
if (params.minFields) {
queryParams.append('sysparm_min_fields', params.minFields)
}
if (params.maxFields) {
queryParams.append('sysparm_max_fields', params.maxFields)
}
if (params.having) {
queryParams.append('sysparm_having', params.having)
}
if (params.displayValue) {
queryParams.append('sysparm_display_value', params.displayValue)
}
const queryString = queryParams.toString()
return queryString ? `${url}?${queryString}` : url
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
try {
const data = await response.json()
if (!response.ok) {
const error = data.error || data
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
}
const result = data.result ?? null
const grouped = Array.isArray(result)
const count = !grouped && result?.stats?.count != null ? Number(result.stats.count) : null
return {
success: true,
output: {
result,
count,
metadata: {
grouped,
groupCount: grouped ? result.length : null,
},
},
}
} catch (error) {
logger.error('ServiceNow aggregate - Error processing response:', { error })
throw error
}
},
outputs: {
result: {
type: 'json',
description:
'Aggregate result. Ungrouped: {stats: {count, sum, avg, min, max}}. Grouped: array of {stats, groupby_fields}.',
},
count: {
type: 'number',
description: 'Total matching record count (only present for ungrouped count queries)',
optional: true,
},
metadata: {
type: 'json',
description: 'Operation metadata (grouped, groupCount)',
},
},
}
+109
View File
@@ -0,0 +1,109 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowCreateParams, ServiceNowCreateResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowCreateRecordTool')
export const createRecordTool: ToolConfig<ServiceNowCreateParams, ServiceNowCreateResponse> = {
id: 'servicenow_create_record',
name: 'Create ServiceNow Record',
description: 'Create a new record in a ServiceNow table',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., incident, task, sys_user)',
},
fields: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Fields to set on the record as JSON object (e.g., {"short_description": "Issue title", "priority": "1"})',
},
},
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
return `${baseUrl}/api/now/table/${params.tableName.trim()}`
},
method: 'POST',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
'Content-Type': 'application/json',
Accept: 'application/json',
}
},
body: (params) => {
if (!params.fields || typeof params.fields !== 'object') {
throw new Error('Fields must be a JSON object')
}
return params.fields
},
},
transformResponse: async (response: Response) => {
try {
const data = await response.json()
if (!response.ok) {
const error = data.error || data
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
}
return {
success: true,
output: {
record: data.result,
metadata: {
recordCount: 1,
},
},
}
} catch (error) {
logger.error('ServiceNow create record - Error processing response:', { error })
throw error
}
},
outputs: {
record: {
type: 'json',
description: 'Created ServiceNow record with sys_id and other fields',
},
metadata: {
type: 'json',
description: 'Operation metadata',
},
},
}
+108
View File
@@ -0,0 +1,108 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowDeleteParams, ServiceNowDeleteResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowDeleteRecordTool')
export const deleteRecordTool: ToolConfig<ServiceNowDeleteParams, ServiceNowDeleteResponse> = {
id: 'servicenow_delete_record',
name: 'Delete ServiceNow Record',
description: 'Delete a record from a ServiceNow table',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., incident, task, sys_user, change_request)',
},
sysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Record sys_id to delete (e.g., 6816f79cc0a8016401c5a33be04be441)',
},
},
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
return `${baseUrl}/api/now/table/${params.tableName.trim()}/${params.sysId.trim()}`
},
method: 'DELETE',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response, params?: ServiceNowDeleteParams) => {
try {
if (!response.ok) {
let errorData: any
try {
errorData = await response.json()
} catch {
errorData = { status: response.status, statusText: response.statusText }
}
throw new Error(
typeof errorData === 'string'
? errorData
: errorData.error?.message || JSON.stringify(errorData)
)
}
return {
success: true,
output: {
success: true,
metadata: {
deletedSysId: params?.sysId || '',
},
},
}
} catch (error) {
logger.error('ServiceNow delete record - Error processing response:', { error })
throw error
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the deletion was successful',
},
metadata: {
type: 'json',
description: 'Operation metadata',
},
},
}
@@ -0,0 +1,115 @@
import { createLogger } from '@sim/logger'
import type {
ServiceNowDownloadAttachmentParams,
ServiceNowDownloadAttachmentResponse,
} from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowDownloadAttachmentTool')
export const downloadAttachmentTool: ToolConfig<
ServiceNowDownloadAttachmentParams,
ServiceNowDownloadAttachmentResponse
> = {
id: 'servicenow_download_attachment',
name: 'Download ServiceNow Attachment',
description: 'Download an attachment file from ServiceNow by its sys_id',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
attachmentSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'sys_id of the attachment to download (from List Attachments)',
},
},
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
return `${baseUrl}/api/now/attachment/${params.attachmentSysId.trim()}/file`
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: '*/*',
}
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
logger.error('ServiceNow download attachment - request failed', {
status: response.status,
errorText,
})
throw new Error(errorText || `Failed to download attachment: ${response.status}`)
}
const contentType = response.headers.get('content-type') || 'application/octet-stream'
const contentDisposition = response.headers.get('content-disposition')
let fileName = 'attachment'
if (contentDisposition) {
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
if (match?.[1]) {
fileName = match[1].replace(/['"]/g, '')
}
}
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
return {
success: true,
output: {
file: {
name: fileName,
mimeType: contentType,
data: buffer.toString('base64'),
size: buffer.length,
},
content: buffer.toString('base64'),
},
}
},
outputs: {
file: {
type: 'file',
description: 'Downloaded attachment stored in execution files',
},
content: {
type: 'string',
description: 'Base64 encoded file content',
},
},
}
+19
View File
@@ -0,0 +1,19 @@
import { aggregateTool } from '@/tools/servicenow/aggregate'
import { createRecordTool } from '@/tools/servicenow/create_record'
import { deleteRecordTool } from '@/tools/servicenow/delete_record'
import { downloadAttachmentTool } from '@/tools/servicenow/download_attachment'
import { listAttachmentsTool } from '@/tools/servicenow/list_attachments'
import { readRecordTool } from '@/tools/servicenow/read_record'
import { updateRecordTool } from '@/tools/servicenow/update_record'
import { uploadAttachmentTool } from '@/tools/servicenow/upload_attachment'
export {
createRecordTool as servicenowCreateRecordTool,
readRecordTool as servicenowReadRecordTool,
updateRecordTool as servicenowUpdateRecordTool,
deleteRecordTool as servicenowDeleteRecordTool,
aggregateTool as servicenowAggregateTool,
listAttachmentsTool as servicenowListAttachmentsTool,
downloadAttachmentTool as servicenowDownloadAttachmentTool,
uploadAttachmentTool as servicenowUploadAttachmentTool,
}
@@ -0,0 +1,135 @@
import { createLogger } from '@sim/logger'
import type {
ServiceNowListAttachmentsParams,
ServiceNowListAttachmentsResponse,
} from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowListAttachmentsTool')
export const listAttachmentsTool: ToolConfig<
ServiceNowListAttachmentsParams,
ServiceNowListAttachmentsResponse
> = {
id: 'servicenow_list_attachments',
name: 'List ServiceNow Attachments',
description: 'List the attachments on a ServiceNow record',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table that owns the record (e.g., incident, change_request)',
},
recordSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'sys_id of the record whose attachments should be listed',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of attachments to return',
},
},
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
const queryParams = new URLSearchParams()
queryParams.append(
'sysparm_query',
`table_name=${params.tableName.trim()}^table_sys_id=${params.recordSysId.trim()}`
)
if (params.limit) {
queryParams.append('sysparm_limit', params.limit.toString())
}
return `${baseUrl}/api/now/attachment?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
try {
const data = await response.json()
if (!response.ok) {
const error = data.error || data
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
}
const attachments = Array.isArray(data.result) ? data.result : []
return {
success: true,
output: {
attachments,
metadata: {
recordCount: attachments.length,
},
},
}
} catch (error) {
logger.error('ServiceNow list attachments - Error processing response:', { error })
throw error
}
},
outputs: {
attachments: {
type: 'array',
description: 'Attachment metadata records',
items: {
type: 'object',
properties: {
sys_id: { type: 'string', description: 'Attachment sys_id' },
file_name: { type: 'string', description: 'File name' },
content_type: { type: 'string', description: 'MIME type' },
size_bytes: { type: 'string', description: 'File size in bytes' },
download_link: { type: 'string', description: 'Direct download URL for the file' },
},
},
},
metadata: {
type: 'json',
description: 'Operation metadata (recordCount)',
},
},
}
+175
View File
@@ -0,0 +1,175 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowReadParams, ServiceNowReadResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowReadRecordTool')
export const readRecordTool: ToolConfig<ServiceNowReadParams, ServiceNowReadResponse> = {
id: 'servicenow_read_record',
name: 'Read ServiceNow Records',
description: 'Read records from a ServiceNow table',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., incident, task, sys_user, change_request)',
},
sysId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Specific record sys_id (e.g., 6816f79cc0a8016401c5a33be04be441)',
},
number: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Record number (e.g., INC0010001)',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Encoded query string (e.g., "active=true^priority=1")',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of records to return (e.g., 10, 50, 100)',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of records to skip for pagination (e.g., 0, 10, 20)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated list of fields to return (e.g., sys_id,number,short_description,state)',
},
displayValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Return display values for reference fields: "true" (display only), "false" (sys_id only), or "all" (both)',
},
},
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
let url = `${baseUrl}/api/now/table/${params.tableName.trim()}`
const queryParams = new URLSearchParams()
if (params.sysId) {
url = `${url}/${params.sysId.trim()}`
} else if (params.number) {
const numberQuery = `number=${params.number}`
const existingQuery = params.query
queryParams.append(
'sysparm_query',
existingQuery ? `${existingQuery}^${numberQuery}` : numberQuery
)
} else if (params.query) {
queryParams.append('sysparm_query', params.query)
}
if (params.limit) {
queryParams.append('sysparm_limit', params.limit.toString())
}
if (params.offset !== undefined && params.offset !== null) {
queryParams.append('sysparm_offset', params.offset.toString())
}
if (params.fields) {
queryParams.append('sysparm_fields', params.fields)
}
if (params.displayValue) {
queryParams.append('sysparm_display_value', params.displayValue)
}
const queryString = queryParams.toString()
return queryString ? `${url}?${queryString}` : url
},
method: 'GET',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
try {
const data = await response.json()
if (!response.ok) {
const error = data.error || data
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
}
const records = Array.isArray(data.result) ? data.result : [data.result]
return {
success: true,
output: {
records,
metadata: {
recordCount: records.length,
},
},
}
} catch (error) {
logger.error('ServiceNow read record - Error processing response:', { error })
throw error
}
},
outputs: {
records: {
type: 'array',
description: 'Array of ServiceNow records',
},
metadata: {
type: 'json',
description: 'Operation metadata',
},
},
}
+174
View File
@@ -0,0 +1,174 @@
import type { ToolResponse } from '@/tools/types'
interface ServiceNowRecord {
sys_id: string
number?: string
[key: string]: any
}
interface ServiceNowBaseParams {
instanceUrl: string
username: string
password: string
tableName: string
}
export interface ServiceNowCreateParams extends ServiceNowBaseParams {
fields: Record<string, any>
}
export interface ServiceNowCreateResponse extends ToolResponse {
output: {
record: ServiceNowRecord
metadata: {
recordCount: 1
}
}
}
export interface ServiceNowReadParams extends ServiceNowBaseParams {
sysId?: string
number?: string
query?: string
limit?: number
offset?: number
fields?: string
displayValue?: string
}
export interface ServiceNowReadResponse extends ToolResponse {
output: {
records: ServiceNowRecord[]
metadata: {
recordCount: number
}
}
}
export interface ServiceNowUpdateParams extends ServiceNowBaseParams {
sysId: string
fields: Record<string, any>
}
export interface ServiceNowUpdateResponse extends ToolResponse {
output: {
record: ServiceNowRecord
metadata: {
recordCount: 1
updatedFields: string[]
}
}
}
export interface ServiceNowDeleteParams extends ServiceNowBaseParams {
sysId: string
}
export interface ServiceNowDeleteResponse extends ToolResponse {
output: {
success: boolean
metadata: {
deletedSysId: string
}
}
}
export interface ServiceNowAggregateParams extends ServiceNowBaseParams {
query?: string
count?: boolean
groupBy?: string
avgFields?: string
sumFields?: string
minFields?: string
maxFields?: string
having?: string
displayValue?: string
}
export interface ServiceNowAggregateResponse extends ToolResponse {
output: {
result: Record<string, any> | Record<string, any>[] | null
count: number | null
metadata: {
grouped: boolean
groupCount: number | null
}
}
}
export interface ServiceNowAttachment {
sys_id: string
file_name: string
content_type: string
size_bytes?: string
table_name?: string
table_sys_id?: string
download_link?: string
[key: string]: any
}
export interface ServiceNowListAttachmentsParams {
instanceUrl: string
username: string
password: string
tableName: string
recordSysId: string
limit?: number
}
export interface ServiceNowListAttachmentsResponse extends ToolResponse {
output: {
attachments: ServiceNowAttachment[]
metadata: {
recordCount: number
}
}
}
export interface ServiceNowDownloadAttachmentParams {
instanceUrl: string
username: string
password: string
attachmentSysId: string
}
export interface ServiceNowDownloadAttachmentResponse extends ToolResponse {
output: {
file: {
name: string
mimeType: string
data: string
size: number
}
content: string
}
}
export interface ServiceNowUploadAttachmentParams {
instanceUrl: string
username: string
password: string
tableName: string
recordSysId: string
fileName: string
file?: unknown
}
export interface ServiceNowUploadAttachmentResponse extends ToolResponse {
output: {
attachment: ServiceNowAttachment
metadata: {
recordCount: 1
}
}
}
export type ServiceNowResponse =
| ServiceNowCreateResponse
| ServiceNowReadResponse
| ServiceNowUpdateResponse
| ServiceNowDeleteResponse
| ServiceNowAggregateResponse
| ServiceNowListAttachmentsResponse
| ServiceNowDownloadAttachmentResponse
| ServiceNowUploadAttachmentResponse
+115
View File
@@ -0,0 +1,115 @@
import { createLogger } from '@sim/logger'
import type { ServiceNowUpdateParams, ServiceNowUpdateResponse } from '@/tools/servicenow/types'
import { createBasicAuthHeader } from '@/tools/servicenow/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowUpdateRecordTool')
export const updateRecordTool: ToolConfig<ServiceNowUpdateParams, ServiceNowUpdateResponse> = {
id: 'servicenow_update_record',
name: 'Update ServiceNow Record',
description: 'Update an existing record in a ServiceNow table',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name (e.g., incident, task, sys_user, change_request)',
},
sysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Record sys_id to update (e.g., 6816f79cc0a8016401c5a33be04be441)',
},
fields: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Fields to update as JSON object (e.g., {"state": "2", "priority": "1"})',
},
},
request: {
url: (params) => {
const baseUrl = params.instanceUrl.trim().replace(/\/$/, '')
if (!baseUrl) {
throw new Error('ServiceNow instance URL is required')
}
return `${baseUrl}/api/now/table/${params.tableName.trim()}/${params.sysId.trim()}`
},
method: 'PATCH',
headers: (params) => {
if (!params.username || !params.password) {
throw new Error('ServiceNow username and password are required')
}
return {
Authorization: createBasicAuthHeader(params.username, params.password),
'Content-Type': 'application/json',
Accept: 'application/json',
}
},
body: (params) => {
if (!params.fields || typeof params.fields !== 'object') {
throw new Error('Fields must be a JSON object')
}
return params.fields
},
},
transformResponse: async (response: Response, params?: ServiceNowUpdateParams) => {
try {
const data = await response.json()
if (!response.ok) {
const error = data.error || data
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
}
return {
success: true,
output: {
record: data.result,
metadata: {
recordCount: 1,
updatedFields: params ? Object.keys(params.fields || {}) : [],
},
},
}
} catch (error) {
logger.error('ServiceNow update record - Error processing response:', { error })
throw error
}
},
outputs: {
record: {
type: 'json',
description: 'Updated ServiceNow record',
},
metadata: {
type: 'json',
description: 'Operation metadata',
},
},
}
@@ -0,0 +1,105 @@
import { createLogger } from '@sim/logger'
import type {
ServiceNowUploadAttachmentParams,
ServiceNowUploadAttachmentResponse,
} from '@/tools/servicenow/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('ServiceNowUploadAttachmentTool')
export const uploadAttachmentTool: ToolConfig<
ServiceNowUploadAttachmentParams,
ServiceNowUploadAttachmentResponse
> = {
id: 'servicenow_upload_attachment',
name: 'Upload ServiceNow Attachment',
description: 'Attach a file to a ServiceNow record',
version: '1.0.0',
params: {
instanceUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow username',
},
password: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ServiceNow password',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table that owns the record (e.g., incident, change_request)',
},
recordSysId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'sys_id of the record to attach the file to',
},
fileName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name to give the uploaded file (e.g., logs.txt)',
},
file: {
type: 'file',
required: false,
visibility: 'user-only',
description: 'File to upload (UserFile object)',
},
},
request: {
url: '/api/tools/servicenow/upload-attachment',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
instanceUrl: params.instanceUrl,
username: params.username,
password: params.password,
tableName: params.tableName,
recordSysId: params.recordSysId,
fileName: params.fileName,
file: params.file,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('ServiceNow upload attachment failed', { error: data.error })
throw new Error(data.error || 'ServiceNow upload attachment failed')
}
return {
success: true,
output: data.output,
}
},
outputs: {
attachment: {
type: 'json',
description: 'Created attachment metadata (sys_id, file_name, content_type, download_link)',
},
metadata: {
type: 'json',
description: 'Operation metadata',
},
},
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Creates a Basic Authentication header from username and password
* @param username ServiceNow username
* @param password ServiceNow password
* @returns Base64 encoded Basic Auth header value
*/
export function createBasicAuthHeader(username: string, password: string): string {
const credentials = Buffer.from(`${username}:${password}`).toString('base64')
return `Basic ${credentials}`
}