@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,193 @@
1
+ import type { KanbanProvider } from '../providers/types.ts'
2
+ import type { Task, TaskComment } from '../types.ts'
3
+ import { TrackerMcpError, toTrackerMcpError } from './errors.ts'
4
+ import type { TrackerMcpHooks, TrackerMcpPolicy } from './types.ts'
5
+
6
+ export interface TrackerCore<TScope> {
7
+ notifyAuthFailure(input: {
8
+ request: Request
9
+ durationMs: number
10
+ error: TrackerMcpError
11
+ }): Promise<void>
12
+ notifyToolError(input: {
13
+ scope: TScope | null
14
+ tool: string
15
+ ticketId?: string
16
+ durationMs: number
17
+ error: TrackerMcpError
18
+ }): Promise<void>
19
+ handlers: {
20
+ getTicket(input: { scope: TScope; ticketId: string }): Promise<Task>
21
+ listComments(input: { scope: TScope; ticketId: string }): Promise<TaskComment[]>
22
+ getBoard(input: { scope: TScope }): Promise<Awaited<ReturnType<KanbanProvider['getBoard']>>>
23
+ postComment(input: { scope: TScope; ticketId: string; body: string }): Promise<TaskComment>
24
+ updateComment(input: {
25
+ scope: TScope
26
+ ticketId: string
27
+ commentId: string
28
+ body: string
29
+ }): Promise<TaskComment>
30
+ moveTicket(input: { scope: TScope; ticketId: string; column: string }): Promise<void>
31
+ }
32
+ }
33
+
34
+ interface RunToolInput<TScope, TResult> {
35
+ scope: TScope
36
+ tool: string
37
+ ticketId?: string
38
+ execute(): Promise<TResult>
39
+ resultMeta?: Record<string, unknown> | ((result: TResult) => Record<string, unknown> | undefined)
40
+ }
41
+
42
+ async function filterComments<TScope>(
43
+ scope: TScope,
44
+ comments: TaskComment[],
45
+ policy: TrackerMcpPolicy<TScope>,
46
+ ): Promise<TaskComment[]> {
47
+ if (!policy.filterComment) return comments
48
+ const allowed = await Promise.all(
49
+ comments.map((comment) => policy.filterComment!(scope, comment)),
50
+ )
51
+ return comments.filter((_, index) => allowed[index])
52
+ }
53
+
54
+ export function createTrackerCore<TScope>(input: {
55
+ provider: KanbanProvider
56
+ policy: TrackerMcpPolicy<TScope>
57
+ hooks?: TrackerMcpHooks<TScope>
58
+ }): TrackerCore<TScope> {
59
+ const { provider, policy } = input
60
+ const hooks = input.hooks ?? {}
61
+
62
+ async function runTool<TResult>({
63
+ scope,
64
+ tool,
65
+ ticketId,
66
+ execute,
67
+ resultMeta,
68
+ }: RunToolInput<TScope, TResult>): Promise<TResult> {
69
+ const startedAt = Date.now()
70
+ await hooks.onToolStart?.({ scope, tool, ticketId })
71
+ try {
72
+ const result = await execute()
73
+ const hookResult = typeof resultMeta === 'function' ? resultMeta(result) : resultMeta
74
+ await hooks.onToolResult?.({
75
+ scope,
76
+ tool,
77
+ ticketId,
78
+ durationMs: Date.now() - startedAt,
79
+ result: hookResult,
80
+ })
81
+ return result
82
+ } catch (error) {
83
+ const trackerError = toTrackerMcpError(error)
84
+ await hooks.onToolError?.({
85
+ scope,
86
+ tool,
87
+ ticketId,
88
+ durationMs: Date.now() - startedAt,
89
+ errorCode: trackerError.code,
90
+ error: trackerError,
91
+ })
92
+ throw trackerError
93
+ }
94
+ }
95
+
96
+ return {
97
+ async notifyAuthFailure({ request, durationMs, error }) {
98
+ await hooks.onAuthFailure?.({
99
+ request,
100
+ durationMs,
101
+ errorCode: 'auth_failed',
102
+ error,
103
+ })
104
+ },
105
+
106
+ async notifyToolError({ scope, tool, ticketId, durationMs, error }) {
107
+ await hooks.onToolError?.({
108
+ scope,
109
+ tool,
110
+ ticketId,
111
+ durationMs,
112
+ errorCode: error.code,
113
+ error,
114
+ })
115
+ },
116
+
117
+ handlers: {
118
+ getTicket({ scope, ticketId }) {
119
+ return runTool({
120
+ scope,
121
+ tool: 'getTicket',
122
+ ticketId,
123
+ execute: async () => {
124
+ await policy.canReadTicket(scope, ticketId)
125
+ return provider.getTask(ticketId)
126
+ },
127
+ })
128
+ },
129
+
130
+ listComments({ scope, ticketId }) {
131
+ return runTool({
132
+ scope,
133
+ tool: 'listComments',
134
+ ticketId,
135
+ execute: async () => {
136
+ await policy.canReadTicket(scope, ticketId)
137
+ const comments = await provider.listComments(ticketId)
138
+ return filterComments(scope, comments, policy)
139
+ },
140
+ resultMeta: (comments) => ({ commentCount: comments.length }),
141
+ })
142
+ },
143
+
144
+ getBoard({ scope }) {
145
+ return runTool({
146
+ scope,
147
+ tool: 'getBoard',
148
+ execute: async () => provider.getBoard(),
149
+ })
150
+ },
151
+
152
+ postComment({ scope, ticketId, body }) {
153
+ return runTool({
154
+ scope,
155
+ tool: 'postComment',
156
+ ticketId,
157
+ execute: async () => {
158
+ await policy.canPostComment(scope, ticketId, body)
159
+ return provider.comment(ticketId, body)
160
+ },
161
+ resultMeta: (comment) => ({ commentId: comment.id }),
162
+ })
163
+ },
164
+
165
+ updateComment({ scope, ticketId, commentId, body }) {
166
+ return runTool({
167
+ scope,
168
+ tool: 'updateComment',
169
+ ticketId,
170
+ execute: async () => {
171
+ const existing = await provider.getComment(ticketId, commentId)
172
+ await policy.canUpdateComment(scope, ticketId, existing, body)
173
+ return provider.updateComment(ticketId, commentId, body)
174
+ },
175
+ resultMeta: (comment) => ({ commentId: comment.id }),
176
+ })
177
+ },
178
+
179
+ moveTicket({ scope, ticketId, column }) {
180
+ return runTool({
181
+ scope,
182
+ tool: 'moveTicket',
183
+ ticketId,
184
+ execute: async () => {
185
+ await policy.canMoveTicket(scope, ticketId, column)
186
+ await provider.moveTask(ticketId, column)
187
+ },
188
+ resultMeta: { movedTo: column },
189
+ })
190
+ },
191
+ },
192
+ }
193
+ }
@@ -0,0 +1,109 @@
1
+ import { McpError, ErrorCode as JsonRpcErrorCode } from '@modelcontextprotocol/sdk/types.js'
2
+ import { ErrorCode, type ErrorCodeValue, KanbanError } from '../errors.ts'
3
+
4
+ export type TrackerMcpErrorCode =
5
+ | 'auth_failed'
6
+ | 'policy_denied'
7
+ | 'ticket_not_found'
8
+ | 'comment_not_found'
9
+ | 'validation_failed'
10
+ | 'provider_unavailable'
11
+ | 'internal_error'
12
+
13
+ export class TrackerMcpError extends Error {
14
+ override readonly name = 'TrackerMcpError'
15
+ readonly code: TrackerMcpErrorCode
16
+ override readonly cause?: unknown
17
+ readonly publicMessage?: string
18
+
19
+ constructor(input: {
20
+ code: TrackerMcpErrorCode
21
+ message?: string
22
+ publicMessage?: string
23
+ cause?: unknown
24
+ }) {
25
+ super(input.message ?? input.publicMessage ?? input.code)
26
+ this.code = input.code
27
+ this.cause = input.cause
28
+ this.publicMessage = input.publicMessage
29
+ }
30
+ }
31
+
32
+ function providerError(code: ErrorCodeValue): TrackerMcpErrorCode {
33
+ switch (code) {
34
+ case ErrorCode.TASK_NOT_FOUND:
35
+ return 'ticket_not_found'
36
+ case ErrorCode.COMMENT_NOT_FOUND:
37
+ return 'comment_not_found'
38
+ case ErrorCode.COLUMN_NOT_FOUND:
39
+ case ErrorCode.INVALID_METADATA:
40
+ case ErrorCode.INVALID_POSITION:
41
+ case ErrorCode.INVALID_PRIORITY:
42
+ case ErrorCode.MISSING_ARGUMENT:
43
+ case ErrorCode.UNSUPPORTED_OPERATION:
44
+ case ErrorCode.CONFLICT:
45
+ return 'validation_failed'
46
+ case ErrorCode.PROVIDER_AUTH_FAILED:
47
+ case ErrorCode.PROVIDER_RATE_LIMITED:
48
+ case ErrorCode.PROVIDER_UPSTREAM_ERROR:
49
+ case ErrorCode.PROVIDER_SYNC_REQUIRED:
50
+ case ErrorCode.PROVIDER_NOT_CONFIGURED:
51
+ return 'provider_unavailable'
52
+ default:
53
+ return 'internal_error'
54
+ }
55
+ }
56
+
57
+ export function toTrackerMcpError(error: unknown): TrackerMcpError {
58
+ if (error instanceof TrackerMcpError) return error
59
+ if (error instanceof KanbanError) {
60
+ return new TrackerMcpError({
61
+ code: providerError(error.code),
62
+ message: error.message,
63
+ publicMessage: error.message,
64
+ cause: error,
65
+ })
66
+ }
67
+ if (error instanceof Error) {
68
+ return new TrackerMcpError({
69
+ code: 'internal_error',
70
+ message: error.message,
71
+ publicMessage: error.message,
72
+ cause: error,
73
+ })
74
+ }
75
+ return new TrackerMcpError({
76
+ code: 'internal_error',
77
+ message: String(error),
78
+ publicMessage: String(error),
79
+ cause: error,
80
+ })
81
+ }
82
+
83
+ export function trackerMcpJsonRpcCode(code: TrackerMcpErrorCode): number {
84
+ switch (code) {
85
+ case 'auth_failed':
86
+ return -32001
87
+ case 'policy_denied':
88
+ return -32002
89
+ case 'ticket_not_found':
90
+ case 'comment_not_found':
91
+ return -32003
92
+ case 'validation_failed':
93
+ return JsonRpcErrorCode.InvalidParams
94
+ case 'provider_unavailable':
95
+ return -32010
96
+ case 'internal_error':
97
+ default:
98
+ return JsonRpcErrorCode.InternalError
99
+ }
100
+ }
101
+
102
+ export function toMcpError(error: unknown): McpError {
103
+ const trackerError = toTrackerMcpError(error)
104
+ return new McpError(
105
+ trackerMcpJsonRpcCode(trackerError.code),
106
+ trackerError.publicMessage ?? trackerError.message,
107
+ { trackerMcpCode: trackerError.code },
108
+ )
109
+ }
@@ -0,0 +1,13 @@
1
+ export { createTrackerCore } from './core.ts'
2
+ export { createTrackerMcpServer } from './server.ts'
3
+ export { TrackerMcpError, type TrackerMcpErrorCode } from './errors.ts'
4
+ export type { TrackerCore } from './core.ts'
5
+ export type {
6
+ TrackerMcpAuthResolver,
7
+ TrackerMcpHooks,
8
+ TrackerMcpPolicy,
9
+ TrackerMcpServer,
10
+ TrackerMcpTool,
11
+ TrackerMcpToolHandlerContext,
12
+ } from './types.ts'
13
+ export { defaultTools } from './server.ts'