@andypai/agent-kanban 0.2.0 → 0.3.1

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 (72) hide show
  1. package/README.md +120 -24
  2. package/package.json +4 -2
  3. package/src/__tests__/activity.test.ts +16 -10
  4. package/src/__tests__/api.test.ts +99 -3
  5. package/src/__tests__/board-utils.test.ts +100 -0
  6. package/src/__tests__/commands/board.test.ts +7 -14
  7. package/src/__tests__/commands/bulk.test.ts +3 -3
  8. package/src/__tests__/commands/column.test.ts +4 -4
  9. package/src/__tests__/conflict.test.ts +64 -0
  10. package/src/__tests__/db.test.ts +2 -2
  11. package/src/__tests__/id.test.ts +1 -1
  12. package/src/__tests__/index.test.ts +233 -56
  13. package/src/__tests__/jira-adf.test.ts +180 -0
  14. package/src/__tests__/jira-cache.test.ts +304 -0
  15. package/src/__tests__/jira-client.test.ts +169 -0
  16. package/src/__tests__/jira-provider-comment.test.ts +281 -0
  17. package/src/__tests__/jira-provider-mutations.test.ts +771 -0
  18. package/src/__tests__/jira-provider-read.test.ts +594 -0
  19. package/src/__tests__/jira-wiring.test.ts +187 -0
  20. package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
  21. package/src/__tests__/linear-provider-comment.test.ts +243 -0
  22. package/src/__tests__/linear-provider-sync.test.ts +488 -0
  23. package/src/__tests__/local-provider-comment.test.ts +60 -0
  24. package/src/__tests__/mcp-core.test.ts +164 -0
  25. package/src/__tests__/mcp-server.test.ts +252 -0
  26. package/src/__tests__/metrics.test.ts +2 -2
  27. package/src/__tests__/output.test.ts +1 -1
  28. package/src/__tests__/provider-capabilities.test.ts +40 -0
  29. package/src/__tests__/server.test.ts +291 -0
  30. package/src/__tests__/webhooks.test.ts +604 -0
  31. package/src/activity.ts +2 -12
  32. package/src/api.ts +156 -21
  33. package/src/commands/board.ts +4 -14
  34. package/src/commands/bulk.ts +4 -4
  35. package/src/commands/column.ts +4 -4
  36. package/src/commands/mcp.ts +87 -0
  37. package/src/config.ts +1 -1
  38. package/src/db.ts +118 -6
  39. package/src/errors.ts +2 -0
  40. package/src/id.ts +1 -1
  41. package/src/index.ts +83 -35
  42. package/src/mcp/core.ts +193 -0
  43. package/src/mcp/errors.ts +109 -0
  44. package/src/mcp/index.ts +13 -0
  45. package/src/mcp/server.ts +512 -0
  46. package/src/mcp/types.ts +72 -0
  47. package/src/metrics.ts +1 -1
  48. package/src/output.ts +1 -1
  49. package/src/providers/capabilities.ts +22 -17
  50. package/src/providers/errors.ts +1 -1
  51. package/src/providers/index.ts +36 -6
  52. package/src/providers/jira-adf.ts +275 -0
  53. package/src/providers/jira-cache.ts +625 -0
  54. package/src/providers/jira-client.ts +390 -0
  55. package/src/providers/jira.ts +773 -0
  56. package/src/providers/linear-cache.ts +250 -71
  57. package/src/providers/linear-client.ts +255 -15
  58. package/src/providers/linear.ts +338 -20
  59. package/src/providers/local.ts +74 -23
  60. package/src/providers/types.ts +19 -3
  61. package/src/server.ts +141 -13
  62. package/src/tunnel.ts +79 -0
  63. package/src/types.ts +19 -2
  64. package/src/webhooks.ts +36 -0
  65. package/ui/dist/assets/index-DBnoKL_k.css +1 -0
  66. package/ui/dist/assets/index-qNVJ6clH.js +40 -0
  67. package/ui/dist/index.html +2 -2
  68. package/src/__tests__/commands/task.test.ts +0 -144
  69. package/src/commands/task.ts +0 -117
  70. package/src/fixtures.ts +0 -128
  71. package/ui/dist/assets/index-B8f9NB4z.css +0 -1
  72. 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'
3
+ import { providerUpstreamError } from './errors'
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
+ }