@shipfox/api-integration-jira 11.0.0 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.turbo/turbo-build.log +17 -2
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +58 -2
  4. package/dist/api/client.d.ts +34 -0
  5. package/dist/api/client.d.ts.map +1 -1
  6. package/dist/api/client.js +131 -0
  7. package/dist/api/client.js.map +1 -1
  8. package/dist/config.d.ts +0 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +1 -4
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/agent-tools-provider.d.ts +26 -0
  13. package/dist/core/agent-tools-provider.d.ts.map +1 -0
  14. package/dist/core/agent-tools-provider.js +130 -0
  15. package/dist/core/agent-tools-provider.js.map +1 -0
  16. package/dist/core/agent-tools.d.ts +16 -0
  17. package/dist/core/agent-tools.d.ts.map +1 -0
  18. package/dist/core/agent-tools.js +441 -0
  19. package/dist/core/agent-tools.js.map +1 -0
  20. package/dist/core/install.d.ts +22 -0
  21. package/dist/core/install.d.ts.map +1 -1
  22. package/dist/core/install.js +68 -20
  23. package/dist/core/install.js.map +1 -1
  24. package/dist/core/tokens.d.ts +2 -0
  25. package/dist/core/tokens.d.ts.map +1 -1
  26. package/dist/core/tokens.js +129 -37
  27. package/dist/core/tokens.js.map +1 -1
  28. package/dist/core/webhook-processor.d.ts +15 -0
  29. package/dist/core/webhook-processor.d.ts.map +1 -0
  30. package/dist/core/webhook-processor.js +137 -0
  31. package/dist/core/webhook-processor.js.map +1 -0
  32. package/dist/core/webhook-registration.d.ts +26 -0
  33. package/dist/core/webhook-registration.d.ts.map +1 -0
  34. package/dist/core/webhook-registration.js +165 -0
  35. package/dist/core/webhook-registration.js.map +1 -0
  36. package/dist/core/webhook.d.ts +18 -0
  37. package/dist/core/webhook.d.ts.map +1 -0
  38. package/dist/core/webhook.js +38 -0
  39. package/dist/core/webhook.js.map +1 -0
  40. package/dist/db/db.d.ts +68 -0
  41. package/dist/db/db.d.ts.map +1 -1
  42. package/dist/db/installations.d.ts +33 -6
  43. package/dist/db/installations.d.ts.map +1 -1
  44. package/dist/db/installations.js +79 -7
  45. package/dist/db/installations.js.map +1 -1
  46. package/dist/db/schema/installations.d.ts +34 -0
  47. package/dist/db/schema/installations.d.ts.map +1 -1
  48. package/dist/db/schema/installations.js +12 -2
  49. package/dist/db/schema/installations.js.map +1 -1
  50. package/dist/index.d.ts +34 -7
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +76 -8
  53. package/dist/index.js.map +1 -1
  54. package/dist/presentation/routes/install.d.ts +22 -0
  55. package/dist/presentation/routes/install.d.ts.map +1 -1
  56. package/dist/presentation/routes/install.js.map +1 -1
  57. package/dist/presentation/routes/webhooks.d.ts +8 -0
  58. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  59. package/dist/presentation/routes/webhooks.js +108 -0
  60. package/dist/presentation/routes/webhooks.js.map +1 -0
  61. package/dist/temporal/activities/index.d.ts +2 -0
  62. package/dist/temporal/activities/index.d.ts.map +1 -0
  63. package/dist/temporal/activities/index.js +3 -0
  64. package/dist/temporal/activities/index.js.map +1 -0
  65. package/dist/temporal/activities/refresh-jira-tokens.d.ts +20 -0
  66. package/dist/temporal/activities/refresh-jira-tokens.d.ts.map +1 -0
  67. package/dist/temporal/activities/refresh-jira-tokens.js +58 -0
  68. package/dist/temporal/activities/refresh-jira-tokens.js.map +1 -0
  69. package/dist/temporal/constants.d.ts +6 -0
  70. package/dist/temporal/constants.d.ts.map +1 -0
  71. package/dist/temporal/constants.js +8 -0
  72. package/dist/temporal/constants.js.map +1 -0
  73. package/dist/temporal/worker.d.ts +9 -0
  74. package/dist/temporal/worker.d.ts.map +1 -0
  75. package/dist/temporal/worker.js +26 -0
  76. package/dist/temporal/worker.js.map +1 -0
  77. package/dist/temporal/workflows/index.bundle.js +25225 -0
  78. package/dist/temporal/workflows/index.bundle.meta.json +1 -0
  79. package/dist/temporal/workflows/index.d.ts +2 -0
  80. package/dist/temporal/workflows/index.d.ts.map +1 -0
  81. package/dist/temporal/workflows/index.js +3 -0
  82. package/dist/temporal/workflows/index.js.map +1 -0
  83. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts +2 -0
  84. package/dist/temporal/workflows/refresh-jira-tokens-cron.d.ts.map +1 -0
  85. package/dist/temporal/workflows/refresh-jira-tokens-cron.js +20 -0
  86. package/dist/temporal/workflows/refresh-jira-tokens-cron.js.map +1 -0
  87. package/dist/tsconfig.test.tsbuildinfo +1 -1
  88. package/drizzle/0001_jira-token-refresh-state.sql +12 -0
  89. package/drizzle/meta/0001_snapshot.json +245 -0
  90. package/drizzle/meta/_journal.json +7 -0
  91. package/package.json +17 -8
  92. package/src/api/client.test.ts +236 -9
  93. package/src/api/client.ts +192 -0
  94. package/src/config.test.ts +0 -1
  95. package/src/config.ts +1 -4
  96. package/src/core/agent-tools-provider.test.ts +299 -0
  97. package/src/core/agent-tools-provider.ts +175 -0
  98. package/src/core/agent-tools.ts +450 -0
  99. package/src/core/install.test.ts +179 -0
  100. package/src/core/install.ts +85 -19
  101. package/src/core/tokens-refresh.test.ts +223 -4
  102. package/src/core/tokens.test.ts +1 -1
  103. package/src/core/tokens.ts +153 -33
  104. package/src/core/webhook-processor.test.ts +418 -0
  105. package/src/core/webhook-processor.ts +166 -0
  106. package/src/core/webhook-registration.test.ts +375 -0
  107. package/src/core/webhook-registration.ts +216 -0
  108. package/src/core/webhook.ts +69 -0
  109. package/src/db/installations.test.ts +148 -2
  110. package/src/db/installations.ts +133 -7
  111. package/src/db/schema/installations.ts +21 -1
  112. package/src/index.test.ts +42 -0
  113. package/src/index.ts +148 -5
  114. package/src/presentation/routes/install.ts +12 -0
  115. package/src/presentation/routes/webhooks.test.ts +132 -0
  116. package/src/presentation/routes/webhooks.ts +115 -0
  117. package/src/temporal/activities/index.ts +5 -0
  118. package/src/temporal/activities/refresh-jira-tokens.test.ts +156 -0
  119. package/src/temporal/activities/refresh-jira-tokens.ts +88 -0
  120. package/src/temporal/constants.ts +8 -0
  121. package/src/temporal/worker.ts +39 -0
  122. package/src/temporal/workflows/index.ts +1 -0
  123. package/src/temporal/workflows/refresh-jira-tokens-cron.ts +19 -0
  124. package/test/env.ts +0 -1
  125. package/test/factories/jira-installation.ts +4 -0
  126. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,175 @@
1
+ import type {
2
+ AgentToolCatalogEntry,
3
+ AgentToolSession,
4
+ AgentToolsProvider,
5
+ IntegrationConnection,
6
+ OpenAgentToolsSessionInput,
7
+ } from '@shipfox/api-integration-spi';
8
+ import type {JiraAgentToolResponse, JiraAgentToolsClient} from '#api/client.js';
9
+ import type {JiraTokenStore} from '#core/tokens.js';
10
+ import {
11
+ JIRA_TOOL_OPERATIONS,
12
+ type JiraAgentToolRequiredScope,
13
+ jiraAgentToolCatalog,
14
+ jiraAgentToolSelectionCatalog,
15
+ } from './agent-tools.js';
16
+ import {JiraIntegrationProviderError} from './errors.js';
17
+
18
+ type JiraIntegrationConnection = IntegrationConnection<'jira'>;
19
+
20
+ export type JiraToolCallResult = {
21
+ isError?: boolean | undefined;
22
+ content: readonly {type: 'text'; text: string}[];
23
+ structuredContent?: Record<string, unknown> | undefined;
24
+ };
25
+
26
+ export interface JiraAgentToolsProviderOptions {
27
+ jira: Pick<JiraAgentToolsClient, 'request'>;
28
+ tokenStore: Pick<JiraTokenStore, 'getAccessToken'>;
29
+ }
30
+
31
+ export class JiraAgentToolsProvider
32
+ implements
33
+ AgentToolsProvider<
34
+ JiraIntegrationConnection,
35
+ JiraAgentToolRequiredScope,
36
+ unknown,
37
+ JiraToolCallResult
38
+ >
39
+ {
40
+ constructor(private readonly options: JiraAgentToolsProviderOptions) {}
41
+
42
+ catalog() {
43
+ return jiraAgentToolCatalog;
44
+ }
45
+
46
+ selectionCatalog() {
47
+ return jiraAgentToolSelectionCatalog;
48
+ }
49
+
50
+ async openSession(
51
+ input: OpenAgentToolsSessionInput<
52
+ JiraIntegrationConnection,
53
+ JiraAgentToolRequiredScope,
54
+ unknown
55
+ >,
56
+ ): Promise<AgentToolSession<JiraToolCallResult>> {
57
+ const accessToken = await this.options.tokenStore.getAccessToken({
58
+ connectionId: input.connection.id,
59
+ });
60
+ const cloudId = input.connection.externalAccountId;
61
+
62
+ return {
63
+ call: async (call) => {
64
+ const tool = input.tools.find((candidate) => candidate.id === call.toolId);
65
+ if (!tool) return jiraToolError(`Unknown Jira tool: ${call.toolId}`);
66
+
67
+ const operation = JIRA_TOOL_OPERATIONS[call.toolId as keyof typeof JIRA_TOOL_OPERATIONS];
68
+ if (!operation) return jiraToolError(`Unknown Jira tool: ${call.toolId}`);
69
+
70
+ const missingParameter = missingRequiredParameter(tool, call.arguments);
71
+ if (missingParameter) {
72
+ return jiraToolError(`Missing required parameter: ${missingParameter}`);
73
+ }
74
+
75
+ let response: JiraAgentToolResponse;
76
+ try {
77
+ response = await this.options.jira.request({
78
+ accessToken,
79
+ cloudId,
80
+ method: operation.method,
81
+ path: operation.path(call.arguments),
82
+ ...(operation.query === undefined ? {} : {query: operation.query(call.arguments)}),
83
+ ...(operation.body === undefined ? {} : {body: operation.body(call.arguments)}),
84
+ operation: call.toolId,
85
+ });
86
+ } catch (error) {
87
+ if (error instanceof JiraIntegrationProviderError) {
88
+ return jiraToolError(error.message, {
89
+ code: error.reason,
90
+ retryAfterSeconds: error.retryAfterSeconds,
91
+ });
92
+ }
93
+ throw error;
94
+ }
95
+
96
+ if (response.status === 400 || response.status === 404) {
97
+ return jiraRequestError(
98
+ response.body,
99
+ response.status === 404 ? 'Jira resource was not found' : 'Jira request was rejected',
100
+ );
101
+ }
102
+ if (response.status < 200 || response.status >= 300) {
103
+ return jiraToolError(`Jira request returned HTTP ${response.status}`);
104
+ }
105
+ return jiraToolResult(response.body, response.status);
106
+ },
107
+ close: () => Promise.resolve(),
108
+ };
109
+ }
110
+ }
111
+
112
+ function missingRequiredParameter(
113
+ tool: AgentToolCatalogEntry<JiraAgentToolRequiredScope>,
114
+ args: Record<string, unknown>,
115
+ ): string | undefined {
116
+ const required = tool.inputSchema.required;
117
+ if (!Array.isArray(required)) return undefined;
118
+ return required.find(
119
+ (parameter) => typeof parameter === 'string' && args[parameter] === undefined,
120
+ );
121
+ }
122
+
123
+ function jiraToolResult(body: unknown, status: number): JiraToolCallResult {
124
+ const structuredContent = body === undefined ? {status} : isRecord(body) ? body : {result: body};
125
+ return {
126
+ content: [{type: 'text', text: JSON.stringify(structuredContent)}],
127
+ structuredContent,
128
+ };
129
+ }
130
+
131
+ function jiraRequestError(body: unknown, fallbackMessage: string): JiraToolCallResult {
132
+ return jiraToolError(jiraErrorMessage(body, fallbackMessage));
133
+ }
134
+
135
+ function jiraErrorMessage(body: unknown, fallbackMessage: string): string {
136
+ if (typeof body === 'string' && body.length > 0) return body;
137
+ if (!isRecord(body)) return fallbackMessage;
138
+
139
+ const errorMessages = body.errorMessages;
140
+ if (Array.isArray(errorMessages)) {
141
+ const message = errorMessages.filter((value): value is string => typeof value === 'string');
142
+ if (message.length > 0) return message.join('; ');
143
+ }
144
+ if (typeof body.message === 'string' && body.message.length > 0) return body.message;
145
+ const errors = body.errors;
146
+ if (isRecord(errors)) {
147
+ const messages = Object.entries(errors).filter(
148
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
149
+ );
150
+ if (messages.length > 0)
151
+ return messages.map(([field, message]) => `${field}: ${message}`).join('; ');
152
+ }
153
+ return fallbackMessage;
154
+ }
155
+
156
+ function jiraToolError(
157
+ message: string,
158
+ options: {code?: string | undefined; retryAfterSeconds?: number | undefined} = {},
159
+ ): JiraToolCallResult {
160
+ const structuredContent = {
161
+ ...(options.code === undefined ? {} : {code: options.code}),
162
+ ...(options.retryAfterSeconds === undefined
163
+ ? {}
164
+ : {retryAfterSeconds: options.retryAfterSeconds}),
165
+ };
166
+ return {
167
+ isError: true,
168
+ content: [{type: 'text', text: message}],
169
+ ...(Object.keys(structuredContent).length === 0 ? {} : {structuredContent}),
170
+ };
171
+ }
172
+
173
+ function isRecord(value: unknown): value is Record<string, unknown> {
174
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
175
+ }
@@ -0,0 +1,450 @@
1
+ import type {
2
+ JiraAgentToolId,
3
+ JiraAgentToolRequiredScope,
4
+ JiraAgentToolCatalogEntry as JiraDtoAgentToolCatalogEntry,
5
+ } from '@shipfox/api-integration-jira-dto';
6
+ import type {
7
+ AgentToolCatalogEntry,
8
+ AgentToolJsonSchema,
9
+ AgentToolSelectionCatalog,
10
+ AgentToolSelector,
11
+ } from '@shipfox/api-integration-spi';
12
+ import type {JiraAgentToolHttpMethod, JiraAgentToolQueryValue} from '#api/client.js';
13
+
14
+ export type JiraAgentToolCatalogEntry = JiraDtoAgentToolCatalogEntry;
15
+ export type {JiraAgentToolId, JiraAgentToolRequiredScope};
16
+
17
+ interface JiraAgentToolCatalogInput {
18
+ id: JiraAgentToolId;
19
+ description: string;
20
+ sensitivity: 'read' | 'write';
21
+ sensitive: boolean;
22
+ requiredScope: JiraAgentToolRequiredScope;
23
+ inputSchema: AgentToolJsonSchema;
24
+ }
25
+
26
+ const issueIdOrKeySchema = stringSchema('Jira issue ID or key, such as ENG-123');
27
+ const projectIdOrKeySchema = stringSchema('Jira project ID or key, such as ENG');
28
+ const fieldsSchema = recordSchema('Jira issue fields keyed by their REST API field name');
29
+ const updateSchema = recordSchema('Jira issue field update operations');
30
+ const fieldNamesSchema = arraySchema(stringSchema('Jira field name'));
31
+ const propertiesSchema = arraySchema(stringSchema('Jira issue property key'));
32
+
33
+ export const jiraAgentToolCatalog = [
34
+ tool({
35
+ id: 'get_issue',
36
+ description: 'Retrieve a Jira issue by its ID or key.',
37
+ sensitivity: 'read',
38
+ sensitive: false,
39
+ requiredScope: 'read',
40
+ inputSchema: objectSchema(
41
+ {
42
+ idOrKey: issueIdOrKeySchema,
43
+ fields: fieldNamesSchema,
44
+ fieldsByKeys: booleanSchema('Interpret fields by their keys instead of IDs'),
45
+ expand: stringSchema('Comma-separated Jira issue expansions'),
46
+ properties: propertiesSchema,
47
+ updateHistory: booleanSchema('Record this issue as viewed by the authorizing user'),
48
+ },
49
+ ['idOrKey'],
50
+ ),
51
+ }),
52
+ tool({
53
+ id: 'search_issues',
54
+ description: 'Search Jira issues with a JQL query.',
55
+ sensitivity: 'read',
56
+ sensitive: false,
57
+ requiredScope: 'read',
58
+ inputSchema: objectSchema(
59
+ {
60
+ jql: stringSchema('Jira Query Language expression'),
61
+ maxResults: integerSchema('Maximum number of issues to return'),
62
+ nextPageToken: stringSchema('Pagination token from a previous search'),
63
+ fields: fieldNamesSchema,
64
+ fieldsByKeys: booleanSchema('Interpret fields by their keys instead of IDs'),
65
+ expand: stringSchema('Comma-separated Jira issue expansions'),
66
+ properties: propertiesSchema,
67
+ reconcileIssues: arraySchema(integerSchema('Issue ID to reconcile for read-after-write')),
68
+ },
69
+ ['jql'],
70
+ ),
71
+ }),
72
+ tool({
73
+ id: 'get_issue_comments',
74
+ description: 'List the comments on a Jira issue.',
75
+ sensitivity: 'read',
76
+ sensitive: false,
77
+ requiredScope: 'read',
78
+ inputSchema: objectSchema(
79
+ {
80
+ idOrKey: issueIdOrKeySchema,
81
+ startAt: integerSchema('Zero-based comment offset'),
82
+ maxResults: integerSchema('Maximum number of comments to return'),
83
+ orderBy: stringSchema('Comment ordering expression'),
84
+ expand: stringSchema('Comma-separated Jira comment expansions'),
85
+ },
86
+ ['idOrKey'],
87
+ ),
88
+ }),
89
+ tool({
90
+ id: 'get_issue_transitions',
91
+ description: 'List the workflow transitions available for a Jira issue.',
92
+ sensitivity: 'read',
93
+ sensitive: false,
94
+ requiredScope: 'read',
95
+ inputSchema: objectSchema(
96
+ {
97
+ idOrKey: issueIdOrKeySchema,
98
+ expand: stringSchema('Comma-separated transition expansions, such as transitions.fields'),
99
+ },
100
+ ['idOrKey'],
101
+ ),
102
+ }),
103
+ tool({
104
+ id: 'get_project',
105
+ description: 'Retrieve a Jira project by its ID or key.',
106
+ sensitivity: 'read',
107
+ sensitive: false,
108
+ requiredScope: 'read',
109
+ inputSchema: objectSchema(
110
+ {
111
+ idOrKey: projectIdOrKeySchema,
112
+ expand: stringSchema('Comma-separated Jira project expansions'),
113
+ properties: propertiesSchema,
114
+ },
115
+ ['idOrKey'],
116
+ ),
117
+ }),
118
+ tool({
119
+ id: 'get_user',
120
+ description: 'Retrieve a Jira user by Atlassian account ID.',
121
+ sensitivity: 'read',
122
+ sensitive: false,
123
+ requiredScope: 'read',
124
+ inputSchema: objectSchema(
125
+ {
126
+ accountId: stringSchema('Atlassian account ID'),
127
+ expand: stringSchema('Comma-separated Jira user expansions'),
128
+ },
129
+ ['accountId'],
130
+ ),
131
+ }),
132
+ tool({
133
+ id: 'create_issue',
134
+ description:
135
+ 'Create a Jira issue using Jira REST issue fields. If both a project key and project ID or both an issue type name and issue type ID are supplied, the ID takes precedence.',
136
+ sensitivity: 'write',
137
+ sensitive: false,
138
+ requiredScope: 'write',
139
+ inputSchema: objectSchema(
140
+ {
141
+ projectKey: stringSchema(
142
+ 'Jira project key; projectId takes precedence if both are supplied',
143
+ ),
144
+ projectId: stringSchema('Jira project ID; takes precedence over projectKey'),
145
+ summary: stringSchema('Issue summary'),
146
+ issueType: stringSchema(
147
+ 'Issue type name; issueTypeId takes precedence if both are supplied',
148
+ ),
149
+ issueTypeId: stringSchema('Issue type ID; takes precedence over issueType'),
150
+ assigneeAccountId: stringSchema('Atlassian account ID to assign'),
151
+ priority: stringSchema('Jira priority name'),
152
+ labels: arraySchema(stringSchema('Jira label')),
153
+ body: stringSchema('Plain-text issue description'),
154
+ fields: fieldsSchema,
155
+ update: updateSchema,
156
+ },
157
+ ['summary'],
158
+ ),
159
+ }),
160
+ tool({
161
+ id: 'update_issue',
162
+ description: 'Update a Jira issue using Jira REST issue fields.',
163
+ sensitivity: 'write',
164
+ sensitive: false,
165
+ requiredScope: 'write',
166
+ inputSchema: objectSchema(
167
+ {
168
+ idOrKey: issueIdOrKeySchema,
169
+ summary: stringSchema('Replacement issue summary'),
170
+ issueType: stringSchema(
171
+ 'Replacement issue type name; issueTypeId takes precedence if both are supplied',
172
+ ),
173
+ issueTypeId: stringSchema('Replacement issue type ID; takes precedence over issueType'),
174
+ assigneeAccountId: stringSchema('Atlassian account ID to assign'),
175
+ priority: stringSchema('Replacement Jira priority name'),
176
+ labels: arraySchema(stringSchema('Replacement Jira labels')),
177
+ body: stringSchema('Plain-text replacement issue description'),
178
+ fields: fieldsSchema,
179
+ update: updateSchema,
180
+ },
181
+ ['idOrKey'],
182
+ ),
183
+ }),
184
+ tool({
185
+ id: 'add_comment',
186
+ description: 'Add a plain-text comment to a Jira issue.',
187
+ sensitivity: 'write',
188
+ sensitive: false,
189
+ requiredScope: 'write',
190
+ inputSchema: objectSchema(
191
+ {
192
+ idOrKey: issueIdOrKeySchema,
193
+ body: stringSchema('Plain-text comment body'),
194
+ },
195
+ ['idOrKey', 'body'],
196
+ ),
197
+ }),
198
+ tool({
199
+ id: 'transition_issue',
200
+ description: 'Move a Jira issue through a workflow transition.',
201
+ sensitivity: 'write',
202
+ sensitive: false,
203
+ requiredScope: 'write',
204
+ inputSchema: objectSchema(
205
+ {
206
+ idOrKey: issueIdOrKeySchema,
207
+ transitionId: stringSchema('Jira workflow transition ID'),
208
+ fields: fieldsSchema,
209
+ update: updateSchema,
210
+ },
211
+ ['idOrKey', 'transitionId'],
212
+ ),
213
+ }),
214
+ tool({
215
+ id: 'assign_issue',
216
+ description: 'Assign or unassign a Jira issue by Atlassian account ID.',
217
+ sensitivity: 'write',
218
+ sensitive: false,
219
+ requiredScope: 'write',
220
+ inputSchema: objectSchema(
221
+ {
222
+ idOrKey: issueIdOrKeySchema,
223
+ accountId: nullableStringSchema('Atlassian account ID, or null to unassign'),
224
+ },
225
+ ['idOrKey', 'accountId'],
226
+ ),
227
+ }),
228
+ ] as const satisfies readonly JiraAgentToolCatalogEntry[];
229
+
230
+ export const jiraAgentToolSelectionCatalog =
231
+ buildJiraAgentToolSelectionCatalog(jiraAgentToolCatalog);
232
+
233
+ export interface JiraToolOperation {
234
+ method: JiraAgentToolHttpMethod;
235
+ path: (args: Record<string, unknown>) => string;
236
+ query?: ((args: Record<string, unknown>) => Record<string, JiraAgentToolQueryValue>) | undefined;
237
+ body?: ((args: Record<string, unknown>) => unknown) | undefined;
238
+ }
239
+
240
+ export const JIRA_TOOL_OPERATIONS: Record<JiraAgentToolId, JiraToolOperation> = {
241
+ get_issue: {
242
+ method: 'GET',
243
+ path: (args) => issuePath(args),
244
+ query: (args) =>
245
+ definedArguments(args, ['fields', 'fieldsByKeys', 'expand', 'properties', 'updateHistory']),
246
+ },
247
+ search_issues: {
248
+ method: 'POST',
249
+ path: () => '/search/jql',
250
+ body: (args) =>
251
+ definedArguments(args, [
252
+ 'jql',
253
+ 'maxResults',
254
+ 'nextPageToken',
255
+ 'fields',
256
+ 'fieldsByKeys',
257
+ 'expand',
258
+ 'properties',
259
+ 'reconcileIssues',
260
+ ]),
261
+ },
262
+ get_issue_comments: {
263
+ method: 'GET',
264
+ path: (args) => issuePath(args, 'comment'),
265
+ query: (args) => definedArguments(args, ['startAt', 'maxResults', 'orderBy', 'expand']),
266
+ },
267
+ get_issue_transitions: {
268
+ method: 'GET',
269
+ path: (args) => issuePath(args, 'transitions'),
270
+ query: (args) => definedArguments(args, ['expand']),
271
+ },
272
+ get_project: {
273
+ method: 'GET',
274
+ path: (args) => `/project/${encodeURIComponent(stringArgument(args, 'idOrKey'))}`,
275
+ query: (args) => definedArguments(args, ['expand', 'properties']),
276
+ },
277
+ get_user: {
278
+ method: 'GET',
279
+ path: () => '/user',
280
+ query: (args) => definedArguments(args, ['accountId', 'expand']),
281
+ },
282
+ create_issue: {
283
+ method: 'POST',
284
+ path: () => '/issue',
285
+ body: (args) => issueWriteBody(args),
286
+ },
287
+ update_issue: {
288
+ method: 'PUT',
289
+ path: (args) => issuePath(args),
290
+ body: (args) => issueWriteBody(args),
291
+ },
292
+ add_comment: {
293
+ method: 'POST',
294
+ path: (args) => issuePath(args, 'comment'),
295
+ body: (args) => ({body: jiraPlainTextToAdf(stringArgument(args, 'body'))}),
296
+ },
297
+ transition_issue: {
298
+ method: 'POST',
299
+ path: (args) => issuePath(args, 'transitions'),
300
+ body: (args) => transitionBody(args),
301
+ },
302
+ assign_issue: {
303
+ method: 'PUT',
304
+ path: (args) => issuePath(args, 'assignee'),
305
+ body: (args) => ({accountId: args.accountId}),
306
+ },
307
+ };
308
+
309
+ export function jiraPlainTextToAdf(body: string): Record<string, unknown> {
310
+ return {
311
+ type: 'doc',
312
+ version: 1,
313
+ content: [
314
+ {
315
+ type: 'paragraph',
316
+ content: [{type: 'text', text: body}],
317
+ },
318
+ ],
319
+ };
320
+ }
321
+
322
+ function buildJiraAgentToolSelectionCatalog(
323
+ catalog: readonly AgentToolCatalogEntry<JiraAgentToolRequiredScope>[],
324
+ ): AgentToolSelectionCatalog {
325
+ return {
326
+ selectors: catalog.map(
327
+ (entry): AgentToolSelector => ({
328
+ token: entry.id,
329
+ kind: 'standalone',
330
+ sensitivity: entry.sensitivity,
331
+ sensitive: entry.sensitive,
332
+ }),
333
+ ),
334
+ };
335
+ }
336
+
337
+ function issuePath(args: Record<string, unknown>, suffix = ''): string {
338
+ return `/issue/${encodeURIComponent(stringArgument(args, 'idOrKey'))}${suffix ? `/${suffix}` : ''}`;
339
+ }
340
+
341
+ function issueWriteBody(args: Record<string, unknown>): Record<string, unknown> {
342
+ const body: Record<string, unknown> = {};
343
+ const fields = issueFields(args);
344
+ if (Object.keys(fields).length > 0) body.fields = fields;
345
+ if (isRecord(args.update)) body.update = args.update;
346
+ return body;
347
+ }
348
+
349
+ function issueFields(args: Record<string, unknown>): Record<string, unknown> {
350
+ const fields = isRecord(args.fields) ? {...args.fields} : {};
351
+ const projectKey = stringArgumentOrUndefined(args, 'projectKey');
352
+ const projectId = stringArgumentOrUndefined(args, 'projectId');
353
+ const summary = stringArgumentOrUndefined(args, 'summary');
354
+ const issueType = stringArgumentOrUndefined(args, 'issueType');
355
+ const issueTypeId = stringArgumentOrUndefined(args, 'issueTypeId');
356
+ const assigneeAccountId = args.assigneeAccountId;
357
+ const priority = stringArgumentOrUndefined(args, 'priority');
358
+
359
+ if (projectKey !== undefined) fields.project = {key: projectKey};
360
+ if (projectId !== undefined) fields.project = {id: projectId};
361
+ if (summary !== undefined) fields.summary = summary;
362
+ if (issueType !== undefined) fields.issuetype = {name: issueType};
363
+ if (issueTypeId !== undefined) fields.issuetype = {id: issueTypeId};
364
+ if (typeof assigneeAccountId === 'string') fields.assignee = {accountId: assigneeAccountId};
365
+ if (priority !== undefined) fields.priority = {name: priority};
366
+ if (Array.isArray(args.labels)) fields.labels = args.labels;
367
+ if (typeof args.body === 'string') fields.description = jiraPlainTextToAdf(args.body);
368
+
369
+ return fields;
370
+ }
371
+
372
+ function transitionBody(args: Record<string, unknown>): Record<string, unknown> {
373
+ const body: Record<string, unknown> = {
374
+ transition: {id: stringArgument(args, 'transitionId')},
375
+ };
376
+ const fields = isRecord(args.fields) ? args.fields : undefined;
377
+ if (fields !== undefined) body.fields = fields;
378
+ if (isRecord(args.update)) body.update = args.update;
379
+ return body;
380
+ }
381
+
382
+ function definedArguments(
383
+ args: Record<string, unknown>,
384
+ names: readonly string[],
385
+ ): Record<string, JiraAgentToolQueryValue> {
386
+ return Object.fromEntries(
387
+ names.map((name) => [name, args[name]] as const).filter(([, value]) => value !== undefined),
388
+ ) as Record<string, JiraAgentToolQueryValue>;
389
+ }
390
+
391
+ function stringArgument(args: Record<string, unknown>, name: string): string {
392
+ const value = args[name];
393
+ return typeof value === 'string' ? value : String(value ?? '');
394
+ }
395
+
396
+ function stringArgumentOrUndefined(
397
+ args: Record<string, unknown>,
398
+ name: string,
399
+ ): string | undefined {
400
+ const value = args[name];
401
+ return typeof value === 'string' ? value : undefined;
402
+ }
403
+
404
+ function isRecord(value: unknown): value is Record<string, unknown> {
405
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
406
+ }
407
+
408
+ function tool(input: JiraAgentToolCatalogInput): JiraAgentToolCatalogEntry {
409
+ return input;
410
+ }
411
+
412
+ function objectSchema(
413
+ properties: Record<string, AgentToolJsonSchema>,
414
+ required: string[] = [],
415
+ ): AgentToolJsonSchema {
416
+ return {
417
+ type: 'object',
418
+ additionalProperties: false,
419
+ properties,
420
+ ...(required.length > 0 ? {required} : {}),
421
+ };
422
+ }
423
+
424
+ function recordSchema(description?: string): AgentToolJsonSchema {
425
+ return {
426
+ type: 'object',
427
+ additionalProperties: true,
428
+ ...(description === undefined ? {} : {description}),
429
+ };
430
+ }
431
+
432
+ function stringSchema(description?: string): AgentToolJsonSchema {
433
+ return {type: 'string', ...(description === undefined ? {} : {description})};
434
+ }
435
+
436
+ function nullableStringSchema(description?: string): AgentToolJsonSchema {
437
+ return {type: ['string', 'null'], ...(description === undefined ? {} : {description})};
438
+ }
439
+
440
+ function integerSchema(description?: string): AgentToolJsonSchema {
441
+ return {type: 'integer', ...(description === undefined ? {} : {description})};
442
+ }
443
+
444
+ function booleanSchema(description?: string): AgentToolJsonSchema {
445
+ return {type: 'boolean', ...(description === undefined ? {} : {description})};
446
+ }
447
+
448
+ function arraySchema(items: AgentToolJsonSchema): AgentToolJsonSchema {
449
+ return {type: 'array', items};
450
+ }