@andypai/agent-kanban 0.2.0 → 0.3.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.
- package/README.md +89 -22
- package/package.json +4 -2
- package/src/__tests__/activity.test.ts +15 -9
- package/src/__tests__/api.test.ts +96 -0
- package/src/__tests__/board-utils.test.ts +100 -0
- package/src/__tests__/commands/board.test.ts +6 -13
- package/src/__tests__/conflict.test.ts +64 -0
- package/src/__tests__/index.test.ts +233 -56
- package/src/__tests__/jira-adf.test.ts +168 -0
- package/src/__tests__/jira-cache.test.ts +304 -0
- package/src/__tests__/jira-client.test.ts +169 -0
- package/src/__tests__/jira-provider-comment.test.ts +281 -0
- package/src/__tests__/jira-provider-mutations.test.ts +771 -0
- package/src/__tests__/jira-provider-read.test.ts +594 -0
- package/src/__tests__/jira-wiring.test.ts +187 -0
- package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
- package/src/__tests__/linear-provider-comment.test.ts +243 -0
- package/src/__tests__/linear-provider-sync.test.ts +493 -0
- package/src/__tests__/local-provider-comment.test.ts +60 -0
- package/src/__tests__/mcp-core.test.ts +164 -0
- package/src/__tests__/mcp-server.test.ts +252 -0
- package/src/__tests__/server.test.ts +298 -0
- package/src/__tests__/webhooks.test.ts +604 -0
- package/src/activity.ts +1 -11
- package/src/api.ts +154 -19
- package/src/commands/board.ts +1 -11
- package/src/commands/mcp.ts +87 -0
- package/src/db.ts +115 -3
- package/src/errors.ts +2 -0
- package/src/id.ts +1 -1
- package/src/index.ts +72 -18
- package/src/mcp/core.ts +193 -0
- package/src/mcp/errors.ts +109 -0
- package/src/mcp/index.ts +13 -0
- package/src/mcp/server.ts +512 -0
- package/src/mcp/types.ts +72 -0
- package/src/providers/capabilities.ts +15 -0
- package/src/providers/index.ts +31 -1
- package/src/providers/jira-adf.ts +275 -0
- package/src/providers/jira-cache.ts +625 -0
- package/src/providers/jira-client.ts +390 -0
- package/src/providers/jira.ts +778 -0
- package/src/providers/linear-cache.ts +249 -70
- package/src/providers/linear-client.ts +256 -13
- package/src/providers/linear.ts +337 -14
- package/src/providers/local.ts +68 -17
- package/src/providers/types.ts +18 -2
- package/src/server.ts +139 -11
- package/src/tunnel.ts +79 -0
- package/src/types.ts +18 -2
- package/src/webhooks.ts +36 -0
- package/ui/dist/assets/index-DBnoKL_k.css +1 -0
- package/ui/dist/assets/index-qNVJ6clH.js +40 -0
- package/ui/dist/index.html +2 -2
- package/src/__tests__/commands/task.test.ts +0 -144
- package/src/commands/task.ts +0 -117
- package/src/fixtures.ts +0 -128
- package/ui/dist/assets/index-B8f9NB4z.css +0 -1
- package/ui/dist/assets/index-zWp-rB7b.js +0 -40
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { ErrorCode } from '../errors.ts'
|
|
3
|
+
import { providerUpstreamError } from './errors.ts'
|
|
4
|
+
|
|
5
|
+
export interface JiraProject {
|
|
6
|
+
id: string
|
|
7
|
+
key: string
|
|
8
|
+
name: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JiraBoardConfiguration {
|
|
12
|
+
id: number
|
|
13
|
+
name: string
|
|
14
|
+
columnConfig: {
|
|
15
|
+
columns: Array<{ name: string; statuses: Array<{ id: string }> }>
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface JiraProjectStatusCategory {
|
|
20
|
+
id: string
|
|
21
|
+
name: string
|
|
22
|
+
statuses: Array<{
|
|
23
|
+
id: string
|
|
24
|
+
name: string
|
|
25
|
+
statusCategory?: { key?: string }
|
|
26
|
+
}>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface JiraIssue {
|
|
30
|
+
id: string
|
|
31
|
+
key: string
|
|
32
|
+
fields: {
|
|
33
|
+
summary: string
|
|
34
|
+
description?: unknown
|
|
35
|
+
status: { id: string; name: string }
|
|
36
|
+
issuetype: { id: string; name: string }
|
|
37
|
+
priority?: { id: string; name: string } | null
|
|
38
|
+
assignee?: { accountId: string; displayName?: string | null } | null
|
|
39
|
+
labels?: string[]
|
|
40
|
+
comment?: { total?: number } | null
|
|
41
|
+
created: string
|
|
42
|
+
updated: string
|
|
43
|
+
project?: { id: string; key: string }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface JiraSearchPage {
|
|
48
|
+
startAt: number
|
|
49
|
+
maxResults: number
|
|
50
|
+
total: number
|
|
51
|
+
issues: JiraIssue[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface JiraCreatePayload {
|
|
55
|
+
fields: Record<string, unknown>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface JiraUpdatePayload {
|
|
59
|
+
fields?: Record<string, unknown>
|
|
60
|
+
update?: Record<string, unknown>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface JiraCommentPayload {
|
|
64
|
+
body: unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface JiraComment {
|
|
68
|
+
id: string
|
|
69
|
+
body?: unknown
|
|
70
|
+
created?: string
|
|
71
|
+
updated?: string
|
|
72
|
+
author?: { accountId?: string; displayName?: string }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface JiraCommentPage {
|
|
76
|
+
startAt: number
|
|
77
|
+
maxResults: number
|
|
78
|
+
total: number
|
|
79
|
+
comments: JiraComment[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface JiraCreatedIssueRef {
|
|
83
|
+
id: string
|
|
84
|
+
key: string
|
|
85
|
+
self: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface JiraTransition {
|
|
89
|
+
id: string
|
|
90
|
+
name: string
|
|
91
|
+
to: { id: string; name: string }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface JiraUser {
|
|
95
|
+
accountId: string
|
|
96
|
+
displayName: string
|
|
97
|
+
active?: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface JiraPriority {
|
|
101
|
+
id: string
|
|
102
|
+
name: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface JiraIssueType {
|
|
106
|
+
id: string
|
|
107
|
+
name: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface JiraChangelogItem {
|
|
111
|
+
field: string
|
|
112
|
+
fieldtype?: string
|
|
113
|
+
fromString?: string | null
|
|
114
|
+
toString?: string | null
|
|
115
|
+
from?: string | null
|
|
116
|
+
to?: string | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface JiraChangelogEntry {
|
|
120
|
+
id: string
|
|
121
|
+
author?: { accountId?: string; displayName?: string }
|
|
122
|
+
created: string
|
|
123
|
+
items: JiraChangelogItem[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface JiraChangelogPage {
|
|
127
|
+
startAt: number
|
|
128
|
+
maxResults: number
|
|
129
|
+
total: number
|
|
130
|
+
isLast?: boolean
|
|
131
|
+
values: JiraChangelogEntry[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface JiraErrorBody {
|
|
135
|
+
errorMessages?: string[]
|
|
136
|
+
errors?: Record<string, string>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
140
|
+
type QueryParams = Record<string, string | number | undefined>
|
|
141
|
+
|
|
142
|
+
export interface JiraClientOptions {
|
|
143
|
+
baseUrl: string
|
|
144
|
+
email: string
|
|
145
|
+
apiToken: string
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class JiraClient {
|
|
149
|
+
private readonly baseUrl: string
|
|
150
|
+
private readonly authHeader: string
|
|
151
|
+
|
|
152
|
+
constructor(opts: JiraClientOptions) {
|
|
153
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, '')
|
|
154
|
+
const encoded = Buffer.from(`${opts.email}:${opts.apiToken}`).toString('base64')
|
|
155
|
+
this.authHeader = `Basic ${encoded}`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async request<TBody, TResponse>(
|
|
159
|
+
method: HttpMethod,
|
|
160
|
+
path: string,
|
|
161
|
+
body?: TBody,
|
|
162
|
+
query?: QueryParams,
|
|
163
|
+
): Promise<TResponse> {
|
|
164
|
+
let url = `${this.baseUrl}${path}`
|
|
165
|
+
if (query) {
|
|
166
|
+
const params = new URLSearchParams()
|
|
167
|
+
for (const [k, v] of Object.entries(query)) {
|
|
168
|
+
if (v === undefined) continue
|
|
169
|
+
params.append(k, String(v))
|
|
170
|
+
}
|
|
171
|
+
const qs = params.toString()
|
|
172
|
+
if (qs.length > 0) url += `?${qs}`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const headers: Record<string, string> = {
|
|
176
|
+
Authorization: this.authHeader,
|
|
177
|
+
Accept: 'application/json',
|
|
178
|
+
'Content-Type': 'application/json',
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const init: RequestInit = { method, headers }
|
|
182
|
+
if (body !== undefined) {
|
|
183
|
+
init.body = JSON.stringify(body)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const response = await fetch(url, init)
|
|
187
|
+
|
|
188
|
+
if (response.status === 401 || response.status === 403) {
|
|
189
|
+
providerUpstreamError('Jira authentication failed', ErrorCode.PROVIDER_AUTH_FAILED)
|
|
190
|
+
}
|
|
191
|
+
if (response.status === 429) {
|
|
192
|
+
providerUpstreamError('Jira API rate limit exceeded', ErrorCode.PROVIDER_RATE_LIMITED)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
const text = await response.text().catch(() => '')
|
|
197
|
+
let parsed: JiraErrorBody = {}
|
|
198
|
+
if (text.length > 0) {
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(text) as JiraErrorBody
|
|
201
|
+
} catch {
|
|
202
|
+
parsed = {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const parts: string[] = []
|
|
206
|
+
if (parsed.errorMessages && parsed.errorMessages.length > 0) {
|
|
207
|
+
parts.push(parsed.errorMessages.join('; '))
|
|
208
|
+
}
|
|
209
|
+
if (parsed.errors && Object.keys(parsed.errors).length > 0) {
|
|
210
|
+
const entries = Object.entries(parsed.errors)
|
|
211
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
212
|
+
.join('; ')
|
|
213
|
+
parts.push(entries)
|
|
214
|
+
}
|
|
215
|
+
const message =
|
|
216
|
+
parts.length > 0 ? parts.join(' | ') : `Jira API request failed with ${response.status}`
|
|
217
|
+
providerUpstreamError(message)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (response.status === 204) {
|
|
221
|
+
return undefined as TResponse
|
|
222
|
+
}
|
|
223
|
+
const contentLength = response.headers.get('content-length')
|
|
224
|
+
if (contentLength === '0') {
|
|
225
|
+
return undefined as TResponse
|
|
226
|
+
}
|
|
227
|
+
const text = await response.text()
|
|
228
|
+
if (text.length === 0) {
|
|
229
|
+
return undefined as TResponse
|
|
230
|
+
}
|
|
231
|
+
return JSON.parse(text) as TResponse
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getProject(key: string): Promise<JiraProject> {
|
|
235
|
+
return this.request<never, JiraProject>('GET', `/rest/api/3/project/${encodeURIComponent(key)}`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getBoardColumns(boardId: number): Promise<JiraBoardConfiguration> {
|
|
239
|
+
return this.request<never, JiraBoardConfiguration>(
|
|
240
|
+
'GET',
|
|
241
|
+
`/rest/agile/1.0/board/${boardId}/configuration`,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getProjectStatuses(projectKey: string): Promise<JiraProjectStatusCategory[]> {
|
|
246
|
+
return this.request<never, JiraProjectStatusCategory[]>(
|
|
247
|
+
'GET',
|
|
248
|
+
`/rest/api/3/project/${encodeURIComponent(projectKey)}/statuses`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
listIssues(params: {
|
|
253
|
+
jql: string
|
|
254
|
+
startAt: number
|
|
255
|
+
maxResults: number
|
|
256
|
+
fields?: string[]
|
|
257
|
+
}): Promise<JiraSearchPage> {
|
|
258
|
+
const query: QueryParams = {
|
|
259
|
+
jql: params.jql,
|
|
260
|
+
startAt: params.startAt,
|
|
261
|
+
maxResults: params.maxResults,
|
|
262
|
+
}
|
|
263
|
+
if (params.fields && params.fields.length > 0) {
|
|
264
|
+
query.fields = params.fields.join(',')
|
|
265
|
+
}
|
|
266
|
+
return this.request<never, JiraSearchPage>('GET', '/rest/api/3/search/jql', undefined, query)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getIssue(idOrKey: string): Promise<JiraIssue> {
|
|
270
|
+
return this.request<never, JiraIssue>('GET', `/rest/api/3/issue/${encodeURIComponent(idOrKey)}`)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
createIssue(payload: JiraCreatePayload): Promise<JiraCreatedIssueRef> {
|
|
274
|
+
return this.request<JiraCreatePayload, JiraCreatedIssueRef>(
|
|
275
|
+
'POST',
|
|
276
|
+
'/rest/api/3/issue',
|
|
277
|
+
payload,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
updateIssue(idOrKey: string, payload: JiraUpdatePayload): Promise<void> {
|
|
282
|
+
return this.request<JiraUpdatePayload, void>(
|
|
283
|
+
'PUT',
|
|
284
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}`,
|
|
285
|
+
payload,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
addComment(idOrKey: string, payload: JiraCommentPayload): Promise<JiraComment> {
|
|
290
|
+
return this.request<JiraCommentPayload, JiraComment>(
|
|
291
|
+
'POST',
|
|
292
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}/comment`,
|
|
293
|
+
payload,
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getComments(
|
|
298
|
+
idOrKey: string,
|
|
299
|
+
params: { startAt?: number; maxResults?: number } = {},
|
|
300
|
+
): Promise<JiraCommentPage> {
|
|
301
|
+
const query: QueryParams = {}
|
|
302
|
+
if (params.startAt !== undefined) query.startAt = params.startAt
|
|
303
|
+
if (params.maxResults !== undefined) query.maxResults = params.maxResults
|
|
304
|
+
return this.request<never, JiraCommentPage>(
|
|
305
|
+
'GET',
|
|
306
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}/comment`,
|
|
307
|
+
undefined,
|
|
308
|
+
query,
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getComment(idOrKey: string, commentId: string): Promise<JiraComment> {
|
|
313
|
+
return this.request<never, JiraComment>(
|
|
314
|
+
'GET',
|
|
315
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}/comment/${encodeURIComponent(commentId)}`,
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
updateComment(
|
|
320
|
+
idOrKey: string,
|
|
321
|
+
commentId: string,
|
|
322
|
+
payload: JiraCommentPayload,
|
|
323
|
+
): Promise<JiraComment> {
|
|
324
|
+
return this.request<JiraCommentPayload, JiraComment>(
|
|
325
|
+
'PUT',
|
|
326
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}/comment/${encodeURIComponent(commentId)}`,
|
|
327
|
+
payload,
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
getChangelog(
|
|
332
|
+
idOrKey: string,
|
|
333
|
+
params: { startAt?: number; maxResults?: number } = {},
|
|
334
|
+
): Promise<JiraChangelogPage> {
|
|
335
|
+
const query: QueryParams = {}
|
|
336
|
+
if (params.startAt !== undefined) query.startAt = params.startAt
|
|
337
|
+
if (params.maxResults !== undefined) query.maxResults = params.maxResults
|
|
338
|
+
return this.request<never, JiraChangelogPage>(
|
|
339
|
+
'GET',
|
|
340
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}/changelog`,
|
|
341
|
+
undefined,
|
|
342
|
+
query,
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
getTransitions(idOrKey: string): Promise<{ transitions: JiraTransition[] }> {
|
|
347
|
+
return this.request<never, { transitions: JiraTransition[] }>(
|
|
348
|
+
'GET',
|
|
349
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}/transitions`,
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
transitionIssue(
|
|
354
|
+
idOrKey: string,
|
|
355
|
+
transitionId: string,
|
|
356
|
+
fields?: Record<string, unknown>,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
const body: { transition: { id: string }; fields?: Record<string, unknown> } = {
|
|
359
|
+
transition: { id: transitionId },
|
|
360
|
+
}
|
|
361
|
+
if (fields !== undefined) body.fields = fields
|
|
362
|
+
return this.request<typeof body, void>(
|
|
363
|
+
'POST',
|
|
364
|
+
`/rest/api/3/issue/${encodeURIComponent(idOrKey)}/transitions`,
|
|
365
|
+
body,
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
listAssignableUsers(params: {
|
|
370
|
+
projectKey: string
|
|
371
|
+
startAt: number
|
|
372
|
+
maxResults: number
|
|
373
|
+
}): Promise<JiraUser[]> {
|
|
374
|
+
return this.request<never, JiraUser[]>('GET', '/rest/api/3/user/assignable/search', undefined, {
|
|
375
|
+
project: params.projectKey,
|
|
376
|
+
startAt: params.startAt,
|
|
377
|
+
maxResults: params.maxResults,
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
listPriorities(): Promise<JiraPriority[]> {
|
|
382
|
+
return this.request<never, JiraPriority[]>('GET', '/rest/api/3/priority')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
listIssueTypes(params: { projectId: string }): Promise<JiraIssueType[]> {
|
|
386
|
+
return this.request<never, JiraIssueType[]>('GET', '/rest/api/3/issuetype/project', undefined, {
|
|
387
|
+
projectId: params.projectId,
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
}
|