@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.
Files changed (59) hide show
  1. package/README.md +89 -22
  2. package/package.json +4 -2
  3. package/src/__tests__/activity.test.ts +15 -9
  4. package/src/__tests__/api.test.ts +96 -0
  5. package/src/__tests__/board-utils.test.ts +100 -0
  6. package/src/__tests__/commands/board.test.ts +6 -13
  7. package/src/__tests__/conflict.test.ts +64 -0
  8. package/src/__tests__/index.test.ts +233 -56
  9. package/src/__tests__/jira-adf.test.ts +168 -0
  10. package/src/__tests__/jira-cache.test.ts +304 -0
  11. package/src/__tests__/jira-client.test.ts +169 -0
  12. package/src/__tests__/jira-provider-comment.test.ts +281 -0
  13. package/src/__tests__/jira-provider-mutations.test.ts +771 -0
  14. package/src/__tests__/jira-provider-read.test.ts +594 -0
  15. package/src/__tests__/jira-wiring.test.ts +187 -0
  16. package/src/__tests__/linear-cache-description-activity.test.ts +142 -0
  17. package/src/__tests__/linear-provider-comment.test.ts +243 -0
  18. package/src/__tests__/linear-provider-sync.test.ts +493 -0
  19. package/src/__tests__/local-provider-comment.test.ts +60 -0
  20. package/src/__tests__/mcp-core.test.ts +164 -0
  21. package/src/__tests__/mcp-server.test.ts +252 -0
  22. package/src/__tests__/server.test.ts +298 -0
  23. package/src/__tests__/webhooks.test.ts +604 -0
  24. package/src/activity.ts +1 -11
  25. package/src/api.ts +154 -19
  26. package/src/commands/board.ts +1 -11
  27. package/src/commands/mcp.ts +87 -0
  28. package/src/db.ts +115 -3
  29. package/src/errors.ts +2 -0
  30. package/src/id.ts +1 -1
  31. package/src/index.ts +72 -18
  32. package/src/mcp/core.ts +193 -0
  33. package/src/mcp/errors.ts +109 -0
  34. package/src/mcp/index.ts +13 -0
  35. package/src/mcp/server.ts +512 -0
  36. package/src/mcp/types.ts +72 -0
  37. package/src/providers/capabilities.ts +15 -0
  38. package/src/providers/index.ts +31 -1
  39. package/src/providers/jira-adf.ts +275 -0
  40. package/src/providers/jira-cache.ts +625 -0
  41. package/src/providers/jira-client.ts +390 -0
  42. package/src/providers/jira.ts +778 -0
  43. package/src/providers/linear-cache.ts +249 -70
  44. package/src/providers/linear-client.ts +256 -13
  45. package/src/providers/linear.ts +337 -14
  46. package/src/providers/local.ts +68 -17
  47. package/src/providers/types.ts +18 -2
  48. package/src/server.ts +139 -11
  49. package/src/tunnel.ts +79 -0
  50. package/src/types.ts +18 -2
  51. package/src/webhooks.ts +36 -0
  52. package/ui/dist/assets/index-DBnoKL_k.css +1 -0
  53. package/ui/dist/assets/index-qNVJ6clH.js +40 -0
  54. package/ui/dist/index.html +2 -2
  55. package/src/__tests__/commands/task.test.ts +0 -144
  56. package/src/commands/task.ts +0 -117
  57. package/src/fixtures.ts +0 -128
  58. package/ui/dist/assets/index-B8f9NB4z.css +0 -1
  59. package/ui/dist/assets/index-zWp-rB7b.js +0 -40
@@ -0,0 +1,281 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { Database } from 'bun:sqlite'
3
+ import { JiraClient } from '../providers/jira-client.ts'
4
+ import { JiraProvider, type JiraProviderConfig } from '../providers/jira.ts'
5
+ import {
6
+ initJiraCacheSchema,
7
+ saveJiraSyncMeta,
8
+ saveTeamInfo,
9
+ upsertJiraIssues,
10
+ } from '../providers/jira-cache.ts'
11
+
12
+ const baseConfig: JiraProviderConfig = {
13
+ baseUrl: 'https://example.atlassian.net',
14
+ email: 'user@example.com',
15
+ apiToken: 'token',
16
+ projectKey: 'ENG',
17
+ }
18
+
19
+ let db: Database
20
+ let originalFetch: typeof fetch
21
+ let requests: Array<{ url: string; init?: RequestInit }>
22
+
23
+ beforeEach(() => {
24
+ db = new Database(':memory:')
25
+ initJiraCacheSchema(db)
26
+ saveJiraSyncMeta(db, {
27
+ projectKey: 'ENG',
28
+ boardId: null,
29
+ lastSyncAt: new Date().toISOString(),
30
+ lastIssueUpdatedAt: '2026-01-02T00:00:00Z',
31
+ })
32
+ saveTeamInfo(db, { id: '10000', key: 'ENG', name: 'Engineering' })
33
+ upsertJiraIssues(db, [
34
+ {
35
+ id: '10001',
36
+ key: 'ENG-1',
37
+ summary: 'Issue 1',
38
+ descriptionText: '',
39
+ statusId: '10000',
40
+ priorityName: 'High',
41
+ issueTypeName: 'Task',
42
+ assigneeAccountId: null,
43
+ assigneeName: '',
44
+ labels: [],
45
+ commentCount: 0,
46
+ projectKey: 'ENG',
47
+ url: null,
48
+ createdAt: '2026-01-01T00:00:00Z',
49
+ updatedAt: '2026-01-02T00:00:00Z',
50
+ },
51
+ ])
52
+ originalFetch = globalThis.fetch
53
+ requests = []
54
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
55
+ const request = {
56
+ url: typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url,
57
+ init,
58
+ }
59
+ requests.push(request)
60
+
61
+ if (init?.method === 'POST') {
62
+ return new Response(
63
+ JSON.stringify({
64
+ id: 'comment-1',
65
+ body: {
66
+ type: 'doc',
67
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello from jira' }] }],
68
+ },
69
+ created: '2026-01-03T00:00:00Z',
70
+ updated: '2026-01-03T00:00:00Z',
71
+ author: { displayName: 'Jira User' },
72
+ }),
73
+ {
74
+ status: 201,
75
+ headers: { 'content-type': 'application/json' },
76
+ },
77
+ )
78
+ }
79
+
80
+ if (init?.method === 'GET' && /\/comment\/[^/?]+$/.test(request.url)) {
81
+ return new Response(
82
+ JSON.stringify({
83
+ id: 'comment-1',
84
+ body: {
85
+ type: 'doc',
86
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: 'one jira comment' }] }],
87
+ },
88
+ created: '2026-01-03T00:00:00Z',
89
+ updated: '2026-01-05T00:00:00Z',
90
+ author: { displayName: 'Jira User' },
91
+ }),
92
+ {
93
+ status: 200,
94
+ headers: { 'content-type': 'application/json' },
95
+ },
96
+ )
97
+ }
98
+
99
+ if (init?.method === 'GET') {
100
+ return new Response(
101
+ JSON.stringify({
102
+ startAt: 0,
103
+ maxResults: 100,
104
+ total: 2,
105
+ comments: [
106
+ {
107
+ id: 'comment-1',
108
+ body: {
109
+ type: 'doc',
110
+ content: [
111
+ { type: 'paragraph', content: [{ type: 'text', text: 'first jira comment' }] },
112
+ ],
113
+ },
114
+ created: '2026-01-03T00:00:00Z',
115
+ updated: '2026-01-03T00:00:00Z',
116
+ author: { displayName: 'Jira User' },
117
+ },
118
+ {
119
+ id: 'comment-2',
120
+ body: {
121
+ type: 'doc',
122
+ content: [
123
+ { type: 'paragraph', content: [{ type: 'text', text: 'second jira comment' }] },
124
+ ],
125
+ },
126
+ created: '2026-01-04T00:00:00Z',
127
+ updated: '2026-01-04T00:00:00Z',
128
+ author: { displayName: 'Reviewer' },
129
+ },
130
+ ],
131
+ }),
132
+ {
133
+ status: 200,
134
+ headers: { 'content-type': 'application/json' },
135
+ },
136
+ )
137
+ }
138
+
139
+ if (init?.method === 'PUT') {
140
+ return new Response(
141
+ JSON.stringify({
142
+ id: 'comment-1',
143
+ body: {
144
+ type: 'doc',
145
+ content: [
146
+ { type: 'paragraph', content: [{ type: 'text', text: 'edited jira comment' }] },
147
+ ],
148
+ },
149
+ created: '2026-01-03T00:00:00Z',
150
+ updated: '2026-01-04T00:00:00Z',
151
+ author: { displayName: 'Jira User' },
152
+ }),
153
+ {
154
+ status: 200,
155
+ headers: { 'content-type': 'application/json' },
156
+ },
157
+ )
158
+ }
159
+
160
+ if (init?.method === 'DELETE') {
161
+ return new Response(null, { status: 204 })
162
+ }
163
+
164
+ throw new Error(`Unexpected Jira request: ${request.url}`)
165
+ }) as unknown as typeof fetch
166
+ })
167
+
168
+ afterEach(() => {
169
+ globalThis.fetch = originalFetch
170
+ })
171
+
172
+ describe('JiraProvider.comment', () => {
173
+ test('posts an ADF comment to the Jira issue endpoint and advertises comment capability', async () => {
174
+ const client = new JiraClient({
175
+ baseUrl: baseConfig.baseUrl,
176
+ email: baseConfig.email,
177
+ apiToken: baseConfig.apiToken,
178
+ })
179
+ const provider = new JiraProvider(db, baseConfig, client)
180
+
181
+ const comment = await provider.comment('ENG-1', 'hello from jira')
182
+
183
+ expect(requests[0]?.url).toBe('https://example.atlassian.net/rest/api/3/issue/ENG-1/comment')
184
+ expect(requests[0]?.init?.method).toBe('POST')
185
+ const body = JSON.parse(String(requests[0]?.init?.body)) as {
186
+ body: { type: string; content: unknown[] }
187
+ }
188
+ expect(body.body.type).toBe('doc')
189
+ expect(body.body.content).toEqual([
190
+ { type: 'paragraph', content: [{ type: 'text', text: 'hello from jira' }] },
191
+ ])
192
+ expect(comment).toMatchObject({
193
+ id: 'comment-1',
194
+ task_id: 'jira:10001',
195
+ body: 'hello from jira',
196
+ author: 'Jira User',
197
+ })
198
+ expect((await provider.getTask('ENG-1')).comment_count).toBe(1)
199
+
200
+ const context = await provider.getContext()
201
+ expect(context.capabilities.comment).toBe(true)
202
+ })
203
+
204
+ test('gets Jira issue comments and normalizes them into TaskComment rows', async () => {
205
+ const client = new JiraClient({
206
+ baseUrl: baseConfig.baseUrl,
207
+ email: baseConfig.email,
208
+ apiToken: baseConfig.apiToken,
209
+ })
210
+ const provider = new JiraProvider(db, baseConfig, client)
211
+
212
+ const comments = await provider.listComments('ENG-1')
213
+
214
+ expect(requests[0]?.url).toBe(
215
+ 'https://example.atlassian.net/rest/api/3/issue/ENG-1/comment?startAt=0&maxResults=100',
216
+ )
217
+ expect(requests[0]?.init?.method).toBe('GET')
218
+ expect(comments).toEqual([
219
+ {
220
+ id: 'comment-1',
221
+ task_id: 'jira:10001',
222
+ body: 'first jira comment',
223
+ author: 'Jira User',
224
+ created_at: '2026-01-03T00:00:00Z',
225
+ updated_at: '2026-01-03T00:00:00Z',
226
+ },
227
+ {
228
+ id: 'comment-2',
229
+ task_id: 'jira:10001',
230
+ body: 'second jira comment',
231
+ author: 'Reviewer',
232
+ created_at: '2026-01-04T00:00:00Z',
233
+ updated_at: '2026-01-04T00:00:00Z',
234
+ },
235
+ ])
236
+ })
237
+
238
+ test('gets a single Jira issue comment by id', async () => {
239
+ const client = new JiraClient({
240
+ baseUrl: baseConfig.baseUrl,
241
+ email: baseConfig.email,
242
+ apiToken: baseConfig.apiToken,
243
+ })
244
+ const provider = new JiraProvider(db, baseConfig, client)
245
+
246
+ const comment = await provider.getComment('ENG-1', 'comment-1')
247
+
248
+ expect(requests[0]?.url).toBe(
249
+ 'https://example.atlassian.net/rest/api/3/issue/ENG-1/comment/comment-1',
250
+ )
251
+ expect(requests[0]?.init?.method).toBe('GET')
252
+ expect(comment).toMatchObject({
253
+ id: 'comment-1',
254
+ task_id: 'jira:10001',
255
+ body: 'one jira comment',
256
+ author: 'Jira User',
257
+ updated_at: '2026-01-05T00:00:00Z',
258
+ })
259
+ })
260
+
261
+ test('puts an updated ADF comment to the Jira issue comment endpoint', async () => {
262
+ const client = new JiraClient({
263
+ baseUrl: baseConfig.baseUrl,
264
+ email: baseConfig.email,
265
+ apiToken: baseConfig.apiToken,
266
+ })
267
+ const provider = new JiraProvider(db, baseConfig, client)
268
+
269
+ const comment = await provider.updateComment('ENG-1', 'comment-1', 'edited jira comment')
270
+
271
+ expect(requests[0]?.url).toBe(
272
+ 'https://example.atlassian.net/rest/api/3/issue/ENG-1/comment/comment-1',
273
+ )
274
+ expect(requests[0]?.init?.method).toBe('PUT')
275
+ expect(comment).toMatchObject({
276
+ id: 'comment-1',
277
+ task_id: 'jira:10001',
278
+ body: 'edited jira comment',
279
+ })
280
+ })
281
+ })