@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
@@ -1,5 +1,5 @@
1
- import { ErrorCode } from '../errors.ts'
2
- import { providerUpstreamError } from './errors.ts'
1
+ import { ErrorCode } from '../errors'
2
+ import { providerUpstreamError } from './errors'
3
3
 
4
4
  interface GraphQLResponse<T> {
5
5
  data?: T
@@ -31,6 +31,16 @@ export interface LinearIssue {
31
31
  assignee?: { id: string; name?: string | null; displayName?: string | null } | null
32
32
  project?: { id: string; name: string; url?: string | null; state?: string | null } | null
33
33
  state: { id: string; name: string; position: number }
34
+ labels?: string[]
35
+ commentCount?: number
36
+ }
37
+
38
+ export interface LinearComment {
39
+ id: string
40
+ body: string
41
+ createdAt: string
42
+ updatedAt: string
43
+ user?: { id: string; name?: string | null; displayName?: string | null } | null
34
44
  }
35
45
 
36
46
  interface LinearIssueNode {
@@ -45,8 +55,43 @@ interface LinearIssueNode {
45
55
  assignee?: { id: string; name?: string | null; displayName?: string | null } | null
46
56
  project?: { id: string; name: string; url?: string | null; state?: string | null } | null
47
57
  state: { id: string; name: string; position: number }
58
+ labels?: { nodes: Array<{ id: string; name: string }> }
59
+ comments?: {
60
+ nodes: Array<{ id: string }>
61
+ pageInfo?: { hasNextPage: boolean; endCursor: string | null }
62
+ } | null
48
63
  }
49
64
 
65
+ interface LinearCommentNode {
66
+ id: string
67
+ body: string
68
+ createdAt: string
69
+ updatedAt: string
70
+ user?: { id: string; name?: string | null; displayName?: string | null } | null
71
+ }
72
+
73
+ function toLinearIssue(node: LinearIssueNode): LinearIssue {
74
+ return {
75
+ ...node,
76
+ assignee: node.assignee
77
+ ? {
78
+ id: node.assignee.id,
79
+ name: node.assignee.displayName || node.assignee.name,
80
+ }
81
+ : null,
82
+ labels: node.labels?.nodes.map((label) => label.name) ?? [],
83
+ commentCount: node.comments?.nodes?.length ?? undefined,
84
+ }
85
+ }
86
+
87
+ const COMMENT_FIELDS = `
88
+ id
89
+ body
90
+ createdAt
91
+ updatedAt
92
+ user { id name displayName }
93
+ `
94
+
50
95
  export class LinearClient {
51
96
  private readonly endpoint = 'https://api.linear.app/graphql'
52
97
 
@@ -234,6 +279,13 @@ export class LinearClient {
234
279
  name
235
280
  position
236
281
  }
282
+ labels {
283
+ nodes { id name }
284
+ }
285
+ comments(first: 250) {
286
+ nodes { id }
287
+ pageInfo { hasNextPage endCursor }
288
+ }
237
289
  }
238
290
  pageInfo {
239
291
  hasNextPage
@@ -244,17 +296,7 @@ export class LinearClient {
244
296
  `,
245
297
  { teamId, after, updatedAfter: updatedAfter ?? '1970-01-01T00:00:00.000Z' },
246
298
  )
247
- issues.push(
248
- ...data.issues.nodes.map((issue: LinearIssueNode) => ({
249
- ...issue,
250
- assignee: issue.assignee
251
- ? {
252
- id: issue.assignee.id,
253
- name: issue.assignee.displayName || issue.assignee.name,
254
- }
255
- : null,
256
- })),
257
- )
299
+ issues.push(...data.issues.nodes.map(toLinearIssue))
258
300
  after = data.issues.pageInfo.hasNextPage ? data.issues.pageInfo.endCursor : null
259
301
  } while (after)
260
302
 
@@ -271,7 +313,7 @@ export class LinearClient {
271
313
  projectId?: string
272
314
  }): Promise<{ success: boolean; issue: LinearIssue | null }> {
273
315
  const data = await this.query<{
274
- issueCreate: { success: boolean; issue: LinearIssue | null }
316
+ issueCreate: { success: boolean; issue: LinearIssueNode | null }
275
317
  }>(
276
318
  `
277
319
  mutation CreateIssue($input: IssueCreateInput!) {
@@ -289,6 +331,11 @@ export class LinearClient {
289
331
  assignee { id name displayName }
290
332
  project { id name url state }
291
333
  state { id name position }
334
+ labels { nodes { id name } }
335
+ comments(first: 250) {
336
+ nodes { id }
337
+ pageInfo { hasNextPage endCursor }
338
+ }
292
339
  }
293
340
  }
294
341
  }
@@ -305,7 +352,11 @@ export class LinearClient {
305
352
  },
306
353
  },
307
354
  )
308
- return data.issueCreate
355
+ const node = data.issueCreate.issue
356
+ return {
357
+ success: data.issueCreate.success,
358
+ issue: node ? toLinearIssue(node) : null,
359
+ }
309
360
  }
310
361
 
311
362
  async updateIssue(
@@ -326,4 +377,193 @@ export class LinearClient {
326
377
  )
327
378
  return data.issueUpdate
328
379
  }
380
+
381
+ async listIssueHistory(params: {
382
+ issueId: string
383
+ first?: number
384
+ after?: string | null
385
+ }): Promise<{
386
+ nodes: Array<{
387
+ id: string
388
+ createdAt: string
389
+ fromState?: { id: string } | null
390
+ toState?: { id: string } | null
391
+ }>
392
+ pageInfo: { hasNextPage: boolean; endCursor: string | null }
393
+ }> {
394
+ const data = await this.query<{
395
+ issue: {
396
+ history: {
397
+ nodes: Array<{
398
+ id: string
399
+ createdAt: string
400
+ fromState?: { id: string } | null
401
+ toState?: { id: string } | null
402
+ }>
403
+ pageInfo: { hasNextPage: boolean; endCursor: string | null }
404
+ }
405
+ } | null
406
+ }>(
407
+ `
408
+ query IssueHistory($issueId: String!, $first: Int, $after: String) {
409
+ issue(id: $issueId) {
410
+ history(first: $first, after: $after) {
411
+ nodes {
412
+ id
413
+ createdAt
414
+ fromState { id }
415
+ toState { id }
416
+ }
417
+ pageInfo { hasNextPage endCursor }
418
+ }
419
+ }
420
+ }
421
+ `,
422
+ {
423
+ issueId: params.issueId,
424
+ first: params.first ?? 50,
425
+ after: params.after ?? null,
426
+ },
427
+ )
428
+ if (!data.issue) {
429
+ return { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } }
430
+ }
431
+ return data.issue.history
432
+ }
433
+
434
+ async getIssueTeam(issueId: string): Promise<{ id: string; key: string } | null> {
435
+ const data = await this.query<{
436
+ issue: {
437
+ team: {
438
+ id: string
439
+ key: string
440
+ } | null
441
+ } | null
442
+ }>(
443
+ `
444
+ query IssueTeam($issueId: String!) {
445
+ issue(id: $issueId) {
446
+ team {
447
+ id
448
+ key
449
+ }
450
+ }
451
+ }
452
+ `,
453
+ { issueId },
454
+ )
455
+
456
+ return data.issue?.team ?? null
457
+ }
458
+
459
+ async listComments(issueId: string): Promise<LinearComment[]> {
460
+ let after: string | null = null
461
+ const comments: LinearComment[] = []
462
+
463
+ do {
464
+ const data: {
465
+ issue: {
466
+ comments: {
467
+ nodes: LinearCommentNode[]
468
+ pageInfo: PageInfo
469
+ }
470
+ } | null
471
+ } = await this.query(
472
+ `
473
+ query IssueComments($issueId: String!, $after: String) {
474
+ issue(id: $issueId) {
475
+ comments(first: 100, after: $after) {
476
+ nodes { ${COMMENT_FIELDS} }
477
+ pageInfo {
478
+ hasNextPage
479
+ endCursor
480
+ }
481
+ }
482
+ }
483
+ }
484
+ `,
485
+ { issueId, after },
486
+ )
487
+ if (!data.issue) {
488
+ providerUpstreamError(`Linear issue '${issueId}' was not found`)
489
+ }
490
+ comments.push(...data.issue.comments.nodes)
491
+ after = data.issue.comments.pageInfo.hasNextPage
492
+ ? data.issue.comments.pageInfo.endCursor
493
+ : null
494
+ } while (after)
495
+
496
+ return comments
497
+ }
498
+
499
+ async getComment(commentId: string): Promise<LinearComment> {
500
+ const data = await this.query<{ comment: LinearCommentNode | null }>(
501
+ `
502
+ query Comment($id: String!) {
503
+ comment(id: $id) { ${COMMENT_FIELDS} }
504
+ }
505
+ `,
506
+ { id: commentId },
507
+ )
508
+ if (!data.comment) {
509
+ providerUpstreamError(`Linear comment '${commentId}' was not found`)
510
+ }
511
+ return data.comment
512
+ }
513
+
514
+ async commentCreate(
515
+ issueId: string,
516
+ body: string,
517
+ ): Promise<{ success: boolean; comment: LinearComment | null }> {
518
+ const data = await this.query<{
519
+ commentCreate: { success: boolean; comment: LinearCommentNode | null }
520
+ }>(
521
+ `
522
+ mutation CommentCreate($input: CommentCreateInput!) {
523
+ commentCreate(input: $input) {
524
+ success
525
+ comment { ${COMMENT_FIELDS} }
526
+ }
527
+ }
528
+ `,
529
+ {
530
+ input: {
531
+ issueId,
532
+ body,
533
+ },
534
+ },
535
+ )
536
+ return {
537
+ success: data.commentCreate.success,
538
+ comment: data.commentCreate.comment,
539
+ }
540
+ }
541
+
542
+ async commentUpdate(
543
+ commentId: string,
544
+ body: string,
545
+ ): Promise<{ success: boolean; comment: LinearComment | null }> {
546
+ const data = await this.query<{
547
+ commentUpdate: { success: boolean; comment: LinearCommentNode | null }
548
+ }>(
549
+ `
550
+ mutation CommentUpdate($id: String!, $input: CommentUpdateInput!) {
551
+ commentUpdate(id: $id, input: $input) {
552
+ success
553
+ comment { ${COMMENT_FIELDS} }
554
+ }
555
+ }
556
+ `,
557
+ {
558
+ id: commentId,
559
+ input: {
560
+ body,
561
+ },
562
+ },
563
+ )
564
+ return {
565
+ success: data.commentUpdate.success,
566
+ comment: data.commentUpdate.comment,
567
+ }
568
+ }
329
569
  }