feat(jsm): add all Forms API endpoints for jira (#4142)

* feat(jsm): add all Forms API endpoints for two-step form workflow

* removed tyoes

* fix(jsm): handle 204 No Content on action endpoints and reject array answers

* fix(jsm): validate formIds is an array in copy_forms route and block

* fix(jsm): add formTemplateId validation and conditional required on formAnswers
This commit is contained in:
Waleed
2026-04-13 19:03:36 -07:00
committed by GitHub
parent c75d7b9ddc
commit 6ce299bb23
27 changed files with 2936 additions and 30 deletions
@@ -758,4 +758,235 @@ List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, su
| ↳ `formTemplateId` | string | Source form template ID \(UUID\) |
| `total` | number | Total number of forms |
### `jsm_attach_form`
Attach a form template to an existing Jira issue or JSM request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key to attach the form to \(e.g., "SD-123"\) |
| `formTemplateId` | string | Yes | Form template UUID \(from Get Form Templates\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `id` | string | Attached form instance ID \(UUID\) |
| `name` | string | Form name |
| `updated` | string | Last updated timestamp |
| `submitted` | boolean | Whether the form has been submitted |
| `lock` | boolean | Whether the form is locked |
| `internal` | boolean | Whether the form is internal only |
| `formTemplateId` | string | Form template ID |
### `jsm_save_form_answers`
Save answers to a form attached to a Jira issue or JSM request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
| `answers` | json | Yes | Form answers using numeric question IDs as keys \(e.g., \{"1": \{"text": "Title"\}, "4": \{"choices": \["5"\]\}\}\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `state` | json | Form state with status \(open, submitted, locked\) |
| `updated` | string | Last updated timestamp |
### `jsm_submit_form`
Submit a form on a Jira issue or JSM request, locking it from further edits
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `status` | string | Form status after submission \(open, submitted, locked\) |
### `jsm_get_form`
Get a single form with full design, state, and answers from a Jira issue
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `design` | json | Full form design with questions, layout, conditions, sections, settings |
| `state` | json | Form state with answers map, status \(o=open, s=submitted, l=locked\), visibility \(i=internal, e=external\) |
| `updated` | string | Last updated timestamp |
### `jsm_get_form_answers`
Get simplified answers from a form attached to a Jira issue or JSM request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID \(from Attach Form or Get Issue Forms\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `answers` | json | Simplified form answers as key-value pairs \(question label to answer text/choices\) |
### `jsm_reopen_form`
Reopen a submitted form on a Jira issue or JSM request, allowing further edits
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID \(from Get Issue Forms\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `status` | string | Form status after reopening \(open, submitted, locked\) |
### `jsm_delete_form`
Remove a form from a Jira issue or JSM request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Deleted form instance UUID |
| `deleted` | boolean | Whether the form was successfully deleted |
### `jsm_externalise_form`
Make a form visible to customers on a Jira issue or JSM request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `visibility` | string | Form visibility after change \(internal or external\) |
### `jsm_internalise_form`
Make a form internal only (not visible to customers) on a Jira issue or JSM request
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `issueIdOrKey` | string | Yes | Issue ID or key \(e.g., "SD-123"\) |
| `formId` | string | Yes | Form instance UUID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueIdOrKey` | string | Issue ID or key |
| `formId` | string | Form instance UUID |
| `visibility` | string | Form visibility after change \(internal or external\) |
### `jsm_copy_forms`
Copy forms from one Jira issue to another
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `sourceIssueIdOrKey` | string | Yes | Source issue ID or key to copy forms from \(e.g., "SD-123"\) |
| `targetIssueIdOrKey` | string | Yes | Target issue ID or key to copy forms to \(e.g., "SD-456"\) |
| `formIds` | json | No | Optional JSON array of form UUIDs to copy \(e.g., \["uuid1", "uuid2"\]\). If omitted, copies all forms. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `sourceIssueIdOrKey` | string | Source issue ID or key |
| `targetIssueIdOrKey` | string | Target issue ID or key |
| `copiedForms` | json | Array of successfully copied forms |
| `errors` | json | Array of errors encountered during copy |
@@ -6868,9 +6868,49 @@
{
"name": "Get Issue Forms",
"description": "List forms (ProForma/JSM Forms) attached to a Jira issue with metadata (name, submitted status, lock)"
},
{
"name": "Attach Form",
"description": "Attach a form template to an existing Jira issue or JSM request"
},
{
"name": "Save Form Answers",
"description": "Save answers to a form attached to a Jira issue or JSM request"
},
{
"name": "Submit Form",
"description": "Submit a form on a Jira issue or JSM request, locking it from further edits"
},
{
"name": "Get Form",
"description": "Get a single form with full design, state, and answers from a Jira issue"
},
{
"name": "Get Form Answers",
"description": "Get simplified answers from a form attached to a Jira issue or JSM request"
},
{
"name": "Reopen Form",
"description": "Reopen a submitted form on a Jira issue or JSM request, allowing further edits"
},
{
"name": "Delete Form",
"description": "Remove a form from a Jira issue or JSM request"
},
{
"name": "Externalise Form",
"description": "Make a form visible to customers on a Jira issue or JSM request"
},
{
"name": "Internalise Form",
"description": "Make a form internal only (not visible to customers) on a Jira issue or JSM request"
},
{
"name": "Copy Forms",
"description": "Copy forms from one Jira issue to another"
}
],
"operationCount": 24,
"operationCount": 34,
"triggers": [],
"triggerCount": 0,
"authType": "oauth",
@@ -0,0 +1,111 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmGetFormAnswersAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/format/answers`
logger.info('Getting form answers:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
answers: data ?? null,
},
})
} catch (error) {
logger.error('Error getting form answers:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,119 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAttachFormAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formTemplateId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formTemplateId) {
logger.error('Missing formTemplateId in request')
return NextResponse.json({ error: 'Form template ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formTemplateIdValidation = validateJiraCloudId(formTemplateId, 'formTemplateId')
if (!formTemplateIdValidation.isValid) {
return NextResponse.json({ error: formTemplateIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form`
logger.info('Attaching form to issue:', { url, issueIdOrKey, formTemplateId })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({
formTemplate: { id: formTemplateId },
}),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
id: data.id ?? null,
name: data.name ?? null,
updated: data.updated ?? null,
submitted: data.submitted ?? false,
lock: data.lock ?? false,
internal: data.internal ?? null,
formTemplateId: (data.formTemplate as Record<string, unknown>)?.id ?? null,
},
})
} catch (error) {
logger.error('Error attaching form:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,126 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmCopyFormsAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const {
domain,
accessToken,
cloudId: cloudIdParam,
sourceIssueIdOrKey,
targetIssueIdOrKey,
formIds,
} = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!sourceIssueIdOrKey) {
logger.error('Missing sourceIssueIdOrKey in request')
return NextResponse.json({ error: 'Source issue ID or key is required' }, { status: 400 })
}
if (!targetIssueIdOrKey) {
logger.error('Missing targetIssueIdOrKey in request')
return NextResponse.json({ error: 'Target issue ID or key is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const sourceValidation = validateJiraIssueKey(sourceIssueIdOrKey, 'sourceIssueIdOrKey')
if (!sourceValidation.isValid) {
return NextResponse.json({ error: sourceValidation.error }, { status: 400 })
}
const targetValidation = validateJiraIssueKey(targetIssueIdOrKey, 'targetIssueIdOrKey')
if (!targetValidation.isValid) {
return NextResponse.json({ error: targetValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(sourceIssueIdOrKey)}/form/copy/${encodeURIComponent(targetIssueIdOrKey)}`
if (formIds !== undefined && !Array.isArray(formIds)) {
return NextResponse.json({ error: 'formIds must be an array of form UUIDs' }, { status: 400 })
}
const requestBody = Array.isArray(formIds) && formIds.length > 0 ? { ids: formIds } : {}
logger.info('Copying forms:', { url, sourceIssueIdOrKey, targetIssueIdOrKey, formIds })
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(requestBody),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
sourceIssueIdOrKey,
targetIssueIdOrKey,
copiedForms: data.copiedForms ?? [],
errors: data.errors ?? [],
},
})
} catch (error) {
logger.error('Error copying forms:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,111 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmDeleteFormAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
logger.info('Deleting form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'DELETE',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
await response.text()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
deleted: true,
},
})
} catch (error) {
logger.error('Error deleting form:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmExternaliseFormAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/external`
logger.info('Externalising form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
visibility: data.visibility ?? 'external',
},
})
} catch (error) {
logger.error('Error externalising form:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,113 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmGetFormAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
logger.info('Getting form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'GET',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
design: data.design ?? null,
state: data.state ?? null,
updated: data.updated ?? null,
},
})
} catch (error) {
logger.error('Error getting form:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmInternaliseFormAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/internal`
logger.info('Internalising form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
visibility: data.visibility ?? 'internal',
},
})
} catch (error) {
logger.error('Error internalising form:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmReopenFormAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/reopen`
logger.info('Reopening form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
status: data.status ?? 'open',
},
})
} catch (error) {
logger.error('Error reopening form:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,118 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmSaveFormAnswersAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId, answers } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
if (!answers || typeof answers !== 'object' || Array.isArray(answers)) {
logger.error('Missing or invalid answers in request')
return NextResponse.json({ error: 'Answers object is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}`
logger.info('Saving form answers:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ answers }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
state: data.state ?? null,
updated: data.updated ?? null,
},
})
} catch (error) {
logger.error('Error saving form answers:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getJsmFormsApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmSubmitFormAPI')
export async function POST(request: NextRequest) {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, formId } = body
if (!domain) {
logger.error('Missing domain in request')
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!issueIdOrKey) {
logger.error('Missing issueIdOrKey in request')
return NextResponse.json({ error: 'Issue ID or key is required' }, { status: 400 })
}
if (!formId) {
logger.error('Missing formId in request')
return NextResponse.json({ error: 'Form ID is required' }, { status: 400 })
}
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const issueIdOrKeyValidation = validateJiraIssueKey(issueIdOrKey, 'issueIdOrKey')
if (!issueIdOrKeyValidation.isValid) {
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
}
const formIdValidation = validateJiraCloudId(formId, 'formId')
if (!formIdValidation.isValid) {
return NextResponse.json({ error: formIdValidation.error }, { status: 400 })
}
const baseUrl = getJsmFormsApiBaseUrl(cloudId)
const url = `${baseUrl}/issue/${encodeURIComponent(issueIdOrKey)}/form/${encodeURIComponent(formId)}/action/submit`
logger.info('Submitting form:', { url, issueIdOrKey, formId })
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('JSM Forms API error:', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const bodyText = await response.text()
const data = bodyText ? JSON.parse(bodyText) : {}
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueIdOrKey,
formId,
status: data.status ?? 'submitted',
},
})
} catch (error) {
logger.error('Error submitting form:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}
@@ -47,6 +47,16 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
{ label: 'Get Form Templates', id: 'get_form_templates' },
{ label: 'Get Form Structure', id: 'get_form_structure' },
{ label: 'Get Issue Forms', id: 'get_issue_forms' },
{ label: 'Attach Form', id: 'attach_form' },
{ label: 'Save Form Answers', id: 'save_form_answers' },
{ label: 'Submit Form', id: 'submit_form' },
{ label: 'Get Form', id: 'get_form' },
{ label: 'Get Form Answers', id: 'get_form_answers' },
{ label: 'Reopen Form', id: 'reopen_form' },
{ label: 'Delete Form', id: 'delete_form' },
{ label: 'Externalise Form', id: 'externalise_form' },
{ label: 'Internalise Form', id: 'internalise_form' },
{ label: 'Copy Forms', id: 'copy_forms' },
],
value: () => 'get_service_desks',
},
@@ -195,6 +205,15 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
'get_approvals',
'answer_approval',
'get_issue_forms',
'attach_form',
'save_form_answers',
'submit_form',
'get_form',
'get_form_answers',
'reopen_form',
'delete_form',
'externalise_form',
'internalise_form',
],
},
},
@@ -211,8 +230,54 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
title: 'Form ID',
type: 'short-input',
required: true,
placeholder: 'Enter form ID (UUID from Get Form Templates)',
condition: { field: 'operation', value: 'get_form_structure' },
placeholder: 'Enter form ID (UUID from Get Form Templates or Attach Form)',
condition: {
field: 'operation',
value: [
'get_form_structure',
'save_form_answers',
'submit_form',
'get_form',
'get_form_answers',
'reopen_form',
'delete_form',
'externalise_form',
'internalise_form',
],
},
},
{
id: 'formTemplateId',
title: 'Form Template ID',
type: 'short-input',
required: true,
placeholder: 'Enter form template UUID (from Get Form Templates)',
condition: { field: 'operation', value: 'attach_form' },
},
{
id: 'sourceIssueIdOrKey',
title: 'Source Issue ID or Key',
type: 'short-input',
required: true,
placeholder: 'Issue to copy forms from (e.g., SD-123)',
condition: { field: 'operation', value: 'copy_forms' },
},
{
id: 'targetIssueIdOrKey',
title: 'Target Issue ID or Key',
type: 'short-input',
required: true,
placeholder: 'Issue to copy forms to (e.g., SD-456)',
condition: { field: 'operation', value: 'copy_forms' },
},
{
id: 'formIds',
title: 'Form IDs to Copy',
type: 'long-input',
placeholder:
'JSON array of form UUIDs (e.g., ["uuid1", "uuid2"]). Leave empty to copy all forms.',
mode: 'advanced',
condition: { field: 'operation', value: 'copy_forms' },
},
{
id: 'summary',
@@ -291,8 +356,8 @@ Return ONLY the description text - no explanations.`,
type: 'long-input',
placeholder:
'JSON object using form question IDs as keys (e.g., {"1": {"text": "Title"}, "4": {"choices": ["5"]}, "14": {"text": "Details"}})',
mode: 'advanced',
condition: { field: 'operation', value: 'create_request' },
required: { field: 'operation', value: 'save_form_answers' },
condition: { field: 'operation', value: ['create_request', 'save_form_answers'] },
},
{
id: 'searchQuery',
@@ -526,6 +591,16 @@ Return ONLY the comment text - no explanations.`,
'jsm_get_form_templates',
'jsm_get_form_structure',
'jsm_get_issue_forms',
'jsm_attach_form',
'jsm_save_form_answers',
'jsm_submit_form',
'jsm_get_form',
'jsm_get_form_answers',
'jsm_reopen_form',
'jsm_delete_form',
'jsm_externalise_form',
'jsm_internalise_form',
'jsm_copy_forms',
],
config: {
tool: (params) => {
@@ -578,6 +653,26 @@ Return ONLY the comment text - no explanations.`,
return 'jsm_get_form_structure'
case 'get_issue_forms':
return 'jsm_get_issue_forms'
case 'attach_form':
return 'jsm_attach_form'
case 'save_form_answers':
return 'jsm_save_form_answers'
case 'submit_form':
return 'jsm_submit_form'
case 'get_form':
return 'jsm_get_form'
case 'get_form_answers':
return 'jsm_get_form_answers'
case 'reopen_form':
return 'jsm_reopen_form'
case 'delete_form':
return 'jsm_delete_form'
case 'externalise_form':
return 'jsm_externalise_form'
case 'internalise_form':
return 'jsm_internalise_form'
case 'copy_forms':
return 'jsm_copy_forms'
default:
return 'jsm_get_service_desks'
}
@@ -865,6 +960,151 @@ Return ONLY the comment text - no explanations.`,
...baseParams,
issueIdOrKey: params.issueIdOrKey,
}
case 'attach_form':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formTemplateId) {
throw new Error('Form template ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formTemplateId: params.formTemplateId,
}
case 'save_form_answers':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
if (!params.formAnswers) {
throw new Error('Form answers are required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
answers: (() => {
try {
return JSON.parse(params.formAnswers)
} catch {
throw new Error('formAnswers must be valid JSON')
}
})(),
}
case 'submit_form':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}
case 'get_form_answers':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}
case 'reopen_form':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}
case 'get_form':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}
case 'delete_form':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}
case 'externalise_form':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}
case 'internalise_form':
if (!params.issueIdOrKey) {
throw new Error('Issue ID or key is required')
}
if (!params.formId) {
throw new Error('Form ID is required')
}
return {
...baseParams,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}
case 'copy_forms':
if (!params.sourceIssueIdOrKey) {
throw new Error('Source issue ID or key is required')
}
if (!params.targetIssueIdOrKey) {
throw new Error('Target issue ID or key is required')
}
return {
...baseParams,
sourceIssueIdOrKey: params.sourceIssueIdOrKey,
targetIssueIdOrKey: params.targetIssueIdOrKey,
formIds: params.formIds
? (() => {
try {
const parsed = JSON.parse(params.formIds)
if (!Array.isArray(parsed)) {
throw new Error('formIds must be a JSON array')
}
return parsed
} catch (e) {
throw e instanceof Error && e.message === 'formIds must be a JSON array'
? e
: new Error('formIds must be valid JSON array')
}
})()
: undefined,
}
default:
return baseParams
}
@@ -916,6 +1156,10 @@ Return ONLY the comment text - no explanations.`,
},
projectIdOrKey: { type: 'string', description: 'Jira project ID or key' },
formId: { type: 'string', description: 'Form ID (UUID)' },
formTemplateId: { type: 'string', description: 'Form template UUID' },
sourceIssueIdOrKey: { type: 'string', description: 'Source issue ID or key for copy' },
targetIssueIdOrKey: { type: 'string', description: 'Target issue ID or key for copy' },
formIds: { type: 'string', description: 'JSON array of form UUIDs to copy' },
searchQuery: { type: 'string', description: 'Filter request types by name' },
groupId: { type: 'string', description: 'Filter by request type group ID' },
expand: { type: 'string', description: 'Comma-separated fields to expand' },
@@ -978,5 +1222,28 @@ Return ONLY the comment text - no explanations.`,
description:
'Array of forms attached to an issue (id, name, updated, submitted, lock, internal, formTemplateId)',
},
formId: { type: 'string', description: 'Form instance UUID' },
formTemplateId: { type: 'string', description: 'Form template ID' },
submitted: { type: 'boolean', description: 'Whether the form has been submitted' },
lock: { type: 'boolean', description: 'Whether the form is locked' },
internal: { type: 'boolean', description: 'Whether the form is internal only' },
state: {
type: 'json',
description: 'Form state with status (open, submitted, locked)',
},
status: { type: 'string', description: 'Form status (open, submitted, locked)' },
answers: {
type: 'json',
description: 'Form answers as key-value pairs (question ID to answer)',
},
deleted: { type: 'boolean', description: 'Whether the form was successfully deleted' },
visibility: {
type: 'string',
description: 'Form visibility (internal or external)',
},
copiedForms: { type: 'json', description: 'Array of successfully copied forms' },
errors: { type: 'json', description: 'Array of errors from copy forms operation' },
sourceIssueIdOrKey: { type: 'string', description: 'Source issue ID or key' },
targetIssueIdOrKey: { type: 'string', description: 'Target issue ID or key' },
},
}
-6
View File
@@ -495,7 +495,6 @@ export const URL_OUTPUT: OutputProperty = {
description: 'URL to view in Confluence',
}
// Page operations
export interface ConfluenceRetrieveParams {
accessToken: string
pageId: string
@@ -573,7 +572,6 @@ export interface ConfluenceDeletePageResponse extends ToolResponse {
}
}
// Search operations
export interface ConfluenceSearchParams {
accessToken: string
domain: string
@@ -595,7 +593,6 @@ export interface ConfluenceSearchResponse extends ToolResponse {
}
}
// Comment operations
export interface ConfluenceCommentParams {
accessToken: string
domain: string
@@ -612,7 +609,6 @@ export interface ConfluenceCommentResponse extends ToolResponse {
}
}
// Attachment operations
export interface ConfluenceAttachmentParams {
accessToken: string
domain: string
@@ -659,7 +655,6 @@ export interface ConfluenceUploadAttachmentResponse extends ToolResponse {
}
}
// Label operations
export interface ConfluenceLabelParams {
accessToken: string
domain: string
@@ -683,7 +678,6 @@ export interface ConfluenceLabelResponse extends ToolResponse {
}
}
// Space operations
export interface ConfluenceSpaceParams {
accessToken: string
domain: string
+122
View File
@@ -0,0 +1,122 @@
import type { JsmAttachFormParams, JsmAttachFormResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmAttachFormTool: ToolConfig<JsmAttachFormParams, JsmAttachFormResponse> = {
id: 'jsm_attach_form',
name: 'JSM Attach Form',
description: 'Attach a form template to an existing Jira issue or JSM request',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key to attach the form to (e.g., "SD-123")',
},
formTemplateId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form template UUID (from Get Form Templates)',
},
},
request: {
url: '/api/tools/jsm/forms/attach',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formTemplateId: params.formTemplateId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
issueIdOrKey: '',
id: '',
name: '',
updated: null,
submitted: false,
lock: false,
internal: null,
formTemplateId: null,
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
id: '',
name: '',
updated: null,
submitted: false,
lock: false,
internal: null,
formTemplateId: null,
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
id: { type: 'string', description: 'Attached form instance ID (UUID)' },
name: { type: 'string', description: 'Form name' },
updated: { type: 'string', description: 'Last updated timestamp', optional: true },
submitted: { type: 'boolean', description: 'Whether the form has been submitted' },
lock: { type: 'boolean', description: 'Whether the form is locked' },
internal: { type: 'boolean', description: 'Whether the form is internal only', optional: true },
formTemplateId: {
type: 'string',
description: 'Form template ID',
optional: true,
},
},
}
+98
View File
@@ -0,0 +1,98 @@
import type { JsmCopyFormsParams, JsmCopyFormsResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmCopyFormsTool: ToolConfig<JsmCopyFormsParams, JsmCopyFormsResponse> = {
id: 'jsm_copy_forms',
name: 'JSM Copy Forms',
description: 'Copy forms from one Jira issue to another',
version: '1.0.0',
oauth: { required: true, provider: 'jira' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
sourceIssueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Source issue ID or key to copy forms from (e.g., "SD-123")',
},
targetIssueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Target issue ID or key to copy forms to (e.g., "SD-456")',
},
formIds: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Optional JSON array of form UUIDs to copy (e.g., ["uuid1", "uuid2"]). If omitted, copies all forms.',
},
},
request: {
url: '/api/tools/jsm/forms/copy',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
sourceIssueIdOrKey: params.sourceIssueIdOrKey,
targetIssueIdOrKey: params.targetIssueIdOrKey,
formIds: params.formIds,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
sourceIssueIdOrKey: '',
targetIssueIdOrKey: '',
copiedForms: [],
errors: [],
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
sourceIssueIdOrKey: '',
targetIssueIdOrKey: '',
copiedForms: [],
errors: [],
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
sourceIssueIdOrKey: { type: 'string', description: 'Source issue ID or key' },
targetIssueIdOrKey: { type: 'string', description: 'Target issue ID or key' },
copiedForms: { type: 'json', description: 'Array of successfully copied forms' },
errors: { type: 'json', description: 'Array of errors encountered during copy' },
},
}
+82
View File
@@ -0,0 +1,82 @@
import type { JsmDeleteFormParams, JsmDeleteFormResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmDeleteFormTool: ToolConfig<JsmDeleteFormParams, JsmDeleteFormResponse> = {
id: 'jsm_delete_form',
name: 'JSM Delete Form',
description: 'Remove a form from a Jira issue or JSM request',
version: '1.0.0',
oauth: { required: true, provider: 'jira' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID to delete',
},
},
request: {
url: '/api/tools/jsm/forms/delete',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), issueIdOrKey: '', formId: '', deleted: false },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
deleted: false,
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Deleted form instance UUID' },
deleted: { type: 'boolean', description: 'Whether the form was successfully deleted' },
},
}
+88
View File
@@ -0,0 +1,88 @@
import type { JsmExternaliseFormParams, JsmExternaliseFormResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmExternaliseFormTool: ToolConfig<
JsmExternaliseFormParams,
JsmExternaliseFormResponse
> = {
id: 'jsm_externalise_form',
name: 'JSM Externalise Form',
description: 'Make a form visible to customers on a Jira issue or JSM request',
version: '1.0.0',
oauth: { required: true, provider: 'jira' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID',
},
},
request: {
url: '/api/tools/jsm/forms/externalise',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), issueIdOrKey: '', formId: '', visibility: '' },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
visibility: '',
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Form instance UUID' },
visibility: {
type: 'string',
description: 'Form visibility after change (internal or external)',
},
},
}
+122
View File
@@ -0,0 +1,122 @@
import type { JsmGetFormParams, JsmGetFormResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmGetFormTool: ToolConfig<JsmGetFormParams, JsmGetFormResponse> = {
id: 'jsm_get_form',
name: 'JSM Get Form',
description: 'Get a single form with full design, state, and answers from a Jira issue',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID (from Attach Form or Get Issue Forms)',
},
},
request: {
url: '/api/tools/jsm/forms/get',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
design: null,
state: null,
updated: null,
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
design: null,
state: null,
updated: null,
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Form instance UUID' },
design: {
type: 'json',
description: 'Full form design with questions, layout, conditions, sections, settings',
optional: true,
},
state: {
type: 'json',
description:
'Form state with answers map, status (o=open, s=submitted, l=locked), visibility (i=internal, e=external)',
optional: true,
},
updated: {
type: 'string',
description: 'Last updated timestamp',
optional: true,
},
},
}
+109
View File
@@ -0,0 +1,109 @@
import type { JsmGetFormAnswersParams, JsmGetFormAnswersResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmGetFormAnswersTool: ToolConfig<JsmGetFormAnswersParams, JsmGetFormAnswersResponse> =
{
id: 'jsm_get_form_answers',
name: 'JSM Get Form Answers',
description: 'Get simplified answers from a form attached to a Jira issue or JSM request',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID (from Attach Form or Get Issue Forms)',
},
},
request: {
url: '/api/tools/jsm/forms/answers',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
answers: null,
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
answers: null,
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Form instance UUID' },
answers: {
type: 'json',
description:
'Simplified form answers as key-value pairs (question label to answer text/choices)',
optional: true,
},
},
}
+20
View File
@@ -3,11 +3,17 @@ import { jsmAddCustomerTool } from '@/tools/jsm/add_customer'
import { jsmAddOrganizationTool } from '@/tools/jsm/add_organization'
import { jsmAddParticipantsTool } from '@/tools/jsm/add_participants'
import { jsmAnswerApprovalTool } from '@/tools/jsm/answer_approval'
import { jsmAttachFormTool } from '@/tools/jsm/attach_form'
import { jsmCopyFormsTool } from '@/tools/jsm/copy_forms'
import { jsmCreateOrganizationTool } from '@/tools/jsm/create_organization'
import { jsmCreateRequestTool } from '@/tools/jsm/create_request'
import { jsmDeleteFormTool } from '@/tools/jsm/delete_form'
import { jsmExternaliseFormTool } from '@/tools/jsm/externalise_form'
import { jsmGetApprovalsTool } from '@/tools/jsm/get_approvals'
import { jsmGetCommentsTool } from '@/tools/jsm/get_comments'
import { jsmGetCustomersTool } from '@/tools/jsm/get_customers'
import { jsmGetFormTool } from '@/tools/jsm/get_form'
import { jsmGetFormAnswersTool } from '@/tools/jsm/get_form_answers'
import { jsmGetFormStructureTool } from '@/tools/jsm/get_form_structure'
import { jsmGetFormTemplatesTool } from '@/tools/jsm/get_form_templates'
import { jsmGetIssueFormsTool } from '@/tools/jsm/get_issue_forms'
@@ -21,6 +27,10 @@ import { jsmGetRequestsTool } from '@/tools/jsm/get_requests'
import { jsmGetServiceDesksTool } from '@/tools/jsm/get_service_desks'
import { jsmGetSlaTool } from '@/tools/jsm/get_sla'
import { jsmGetTransitionsTool } from '@/tools/jsm/get_transitions'
import { jsmInternaliseFormTool } from '@/tools/jsm/internalise_form'
import { jsmReopenFormTool } from '@/tools/jsm/reopen_form'
import { jsmSaveFormAnswersTool } from '@/tools/jsm/save_form_answers'
import { jsmSubmitFormTool } from '@/tools/jsm/submit_form'
import { jsmTransitionRequestTool } from '@/tools/jsm/transition_request'
export {
@@ -29,11 +39,17 @@ export {
jsmAddOrganizationTool,
jsmAddParticipantsTool,
jsmAnswerApprovalTool,
jsmAttachFormTool,
jsmCopyFormsTool,
jsmCreateOrganizationTool,
jsmCreateRequestTool,
jsmDeleteFormTool,
jsmExternaliseFormTool,
jsmGetApprovalsTool,
jsmGetCommentsTool,
jsmGetCustomersTool,
jsmGetFormTool,
jsmGetFormAnswersTool,
jsmGetFormStructureTool,
jsmGetFormTemplatesTool,
jsmGetIssueFormsTool,
@@ -47,5 +63,9 @@ export {
jsmGetServiceDesksTool,
jsmGetSlaTool,
jsmGetTransitionsTool,
jsmInternaliseFormTool,
jsmReopenFormTool,
jsmSaveFormAnswersTool,
jsmSubmitFormTool,
jsmTransitionRequestTool,
}
+89
View File
@@ -0,0 +1,89 @@
import type { JsmInternaliseFormParams, JsmInternaliseFormResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmInternaliseFormTool: ToolConfig<
JsmInternaliseFormParams,
JsmInternaliseFormResponse
> = {
id: 'jsm_internalise_form',
name: 'JSM Internalise Form',
description:
'Make a form internal only (not visible to customers) on a Jira issue or JSM request',
version: '1.0.0',
oauth: { required: true, provider: 'jira' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID',
},
},
request: {
url: '/api/tools/jsm/forms/internalise',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), issueIdOrKey: '', formId: '', visibility: '' },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
visibility: '',
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Form instance UUID' },
visibility: {
type: 'string',
description: 'Form visibility after change (internal or external)',
},
},
}
+106
View File
@@ -0,0 +1,106 @@
import type { JsmReopenFormParams, JsmReopenFormResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmReopenFormTool: ToolConfig<JsmReopenFormParams, JsmReopenFormResponse> = {
id: 'jsm_reopen_form',
name: 'JSM Reopen Form',
description: 'Reopen a submitted form on a Jira issue or JSM request, allowing further edits',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID (from Get Issue Forms)',
},
},
request: {
url: '/api/tools/jsm/forms/reopen',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
status: '',
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
status: '',
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Form instance UUID' },
status: {
type: 'string',
description: 'Form status after reopening (open, submitted, locked)',
},
},
}
+121
View File
@@ -0,0 +1,121 @@
import type { JsmSaveFormAnswersParams, JsmSaveFormAnswersResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmSaveFormAnswersTool: ToolConfig<
JsmSaveFormAnswersParams,
JsmSaveFormAnswersResponse
> = {
id: 'jsm_save_form_answers',
name: 'JSM Save Form Answers',
description: 'Save answers to a form attached to a Jira issue or JSM request',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID (from Attach Form or Get Issue Forms)',
},
answers: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Form answers using numeric question IDs as keys (e.g., {"1": {"text": "Title"}, "4": {"choices": ["5"]}})',
},
},
request: {
url: '/api/tools/jsm/forms/save',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
answers: params.answers,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
state: null,
updated: null,
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
state: null,
updated: null,
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Form instance UUID' },
state: {
type: 'json',
description: 'Form state with status (open, submitted, locked)',
optional: true,
},
updated: { type: 'string', description: 'Last updated timestamp', optional: true },
},
}
+106
View File
@@ -0,0 +1,106 @@
import type { JsmSubmitFormParams, JsmSubmitFormResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmSubmitFormTool: ToolConfig<JsmSubmitFormParams, JsmSubmitFormResponse> = {
id: 'jsm_submit_form',
name: 'JSM Submit Form',
description: 'Submit a form on a Jira issue or JSM request, locking it from further edits',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
issueIdOrKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Issue ID or key (e.g., "SD-123")',
},
formId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Form instance UUID (from Attach Form or Get Issue Forms)',
},
},
request: {
url: '/api/tools/jsm/forms/submit',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
issueIdOrKey: params.issueIdOrKey,
formId: params.formId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
status: '',
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
issueIdOrKey: '',
formId: '',
status: '',
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueIdOrKey: { type: 'string', description: 'Issue ID or key' },
formId: { type: 'string', description: 'Form instance UUID' },
status: {
type: 'string',
description: 'Form status after submission (open, submitted, locked)',
},
},
}
+164 -19
View File
@@ -1,9 +1,5 @@
import type { ToolResponse } from '@/tools/types'
// ---------------------------------------------------------------------------
// Shared output property constants for JSM tools (following Confluence pattern)
// ---------------------------------------------------------------------------
/** Reusable date output properties with ISO 8601, friendly, and epoch formats */
export const DATE_OUTPUT_PROPERTIES = {
iso8601: { type: 'string', description: 'ISO 8601 formatted date' },
@@ -260,10 +256,6 @@ export const ISSUE_FORM_PROPERTIES = {
},
} as const
// ---------------------------------------------------------------------------
// Data model interfaces
// ---------------------------------------------------------------------------
/** Common parameters for all JSM API calls */
export interface JsmBaseParams {
accessToken: string
@@ -427,10 +419,6 @@ export interface JsmRequestTypeField {
jiraSchema: { type: string; system?: string; custom?: string; customId?: number }
}
// ---------------------------------------------------------------------------
// Params interfaces
// ---------------------------------------------------------------------------
export interface JsmGetServiceDesksParams extends JsmBaseParams {
expand?: string
start?: number
@@ -570,10 +558,6 @@ export interface JsmGetRequestTypeFieldsParams extends JsmBaseParams {
requestTypeId: string
}
// ---------------------------------------------------------------------------
// Response interfaces
// ---------------------------------------------------------------------------
export interface JsmGetServiceDesksResponse extends ToolResponse {
output: {
ts: string
@@ -899,9 +883,160 @@ export interface JsmGetIssueFormsResponse extends ToolResponse {
}
}
// ---------------------------------------------------------------------------
// Union type for all JSM responses
// ---------------------------------------------------------------------------
export interface JsmAttachFormParams extends JsmBaseParams {
issueIdOrKey: string
formTemplateId: string
}
export interface JsmAttachFormResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
id: string
name: string
updated: string | null
submitted: boolean
lock: boolean
internal: boolean | null
formTemplateId: string | null
}
}
export interface JsmSaveFormAnswersParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
answers: Record<string, unknown>
}
export interface JsmSaveFormAnswersResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
state: { status: string } | null
updated: string | null
}
}
export interface JsmSubmitFormParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
}
export interface JsmSubmitFormResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
status: string
}
}
export interface JsmGetFormParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
}
export interface JsmGetFormResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
design: Record<string, unknown> | null
state: {
answers: Record<string, unknown>
status: string
visibility: string
} | null
updated: string | null
}
}
export interface JsmDeleteFormParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
}
export interface JsmDeleteFormResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
deleted: boolean
}
}
export interface JsmExternaliseFormParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
}
export interface JsmExternaliseFormResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
visibility: string
}
}
export interface JsmInternaliseFormParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
}
export interface JsmInternaliseFormResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
visibility: string
}
}
export interface JsmCopyFormsParams extends JsmBaseParams {
sourceIssueIdOrKey: string
targetIssueIdOrKey: string
formIds?: string[]
}
export interface JsmCopyFormsResponse extends ToolResponse {
output: {
ts: string
sourceIssueIdOrKey: string
targetIssueIdOrKey: string
copiedForms: Array<Record<string, unknown>>
errors: Array<Record<string, unknown>>
}
}
export interface JsmGetFormAnswersParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
}
export interface JsmGetFormAnswersResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
answers: Record<string, unknown> | null
}
}
export interface JsmReopenFormParams extends JsmBaseParams {
issueIdOrKey: string
formId: string
}
export interface JsmReopenFormResponse extends ToolResponse {
output: {
ts: string
issueIdOrKey: string
formId: string
status: string
}
}
/** Union type for all JSM responses */
export type JsmResponse =
@@ -929,3 +1064,13 @@ export type JsmResponse =
| JsmGetFormTemplatesResponse
| JsmGetFormStructureResponse
| JsmGetIssueFormsResponse
| JsmAttachFormResponse
| JsmSaveFormAnswersResponse
| JsmSubmitFormResponse
| JsmGetFormResponse
| JsmDeleteFormResponse
| JsmExternaliseFormResponse
| JsmInternaliseFormResponse
| JsmCopyFormsResponse
| JsmGetFormAnswersResponse
| JsmReopenFormResponse
+20
View File
@@ -1326,13 +1326,19 @@ import {
jsmAddOrganizationTool,
jsmAddParticipantsTool,
jsmAnswerApprovalTool,
jsmAttachFormTool,
jsmCopyFormsTool,
jsmCreateOrganizationTool,
jsmCreateRequestTool,
jsmDeleteFormTool,
jsmExternaliseFormTool,
jsmGetApprovalsTool,
jsmGetCommentsTool,
jsmGetCustomersTool,
jsmGetFormAnswersTool,
jsmGetFormStructureTool,
jsmGetFormTemplatesTool,
jsmGetFormTool,
jsmGetIssueFormsTool,
jsmGetOrganizationsTool,
jsmGetParticipantsTool,
@@ -1344,6 +1350,10 @@ import {
jsmGetServiceDesksTool,
jsmGetSlaTool,
jsmGetTransitionsTool,
jsmInternaliseFormTool,
jsmReopenFormTool,
jsmSaveFormAnswersTool,
jsmSubmitFormTool,
jsmTransitionRequestTool,
} from '@/tools/jsm'
import {
@@ -3153,9 +3163,19 @@ export const tools: Record<string, ToolConfig> = {
jsm_add_participants: jsmAddParticipantsTool,
jsm_get_approvals: jsmGetApprovalsTool,
jsm_answer_approval: jsmAnswerApprovalTool,
jsm_get_form: jsmGetFormTool,
jsm_get_form_answers: jsmGetFormAnswersTool,
jsm_get_form_templates: jsmGetFormTemplatesTool,
jsm_get_form_structure: jsmGetFormStructureTool,
jsm_get_issue_forms: jsmGetIssueFormsTool,
jsm_attach_form: jsmAttachFormTool,
jsm_save_form_answers: jsmSaveFormAnswersTool,
jsm_submit_form: jsmSubmitFormTool,
jsm_reopen_form: jsmReopenFormTool,
jsm_delete_form: jsmDeleteFormTool,
jsm_externalise_form: jsmExternaliseFormTool,
jsm_internalise_form: jsmInternaliseFormTool,
jsm_copy_forms: jsmCopyFormsTool,
kalshi_get_markets: kalshiGetMarketsTool,
kalshi_get_markets_v2: kalshiGetMarketsV2Tool,
kalshi_get_market: kalshiGetMarketTool,