@andypai/agent-kanban 0.6.4 → 0.7.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/package.json +18 -3
- package/src/__tests__/index.test.ts +27 -1
- package/src/__tests__/jira-cache.test.ts +68 -0
- package/src/__tests__/jira-provider-read.test.ts +130 -0
- package/src/__tests__/jira-wiring.test.ts +41 -0
- package/src/__tests__/linear-provider-comment.test.ts +81 -0
- package/src/__tests__/linear-provider-sync.test.ts +107 -0
- package/src/__tests__/linear-state-resolution.test.ts +60 -0
- package/src/__tests__/local-provider-core.test.ts +245 -0
- package/src/__tests__/postgres-jira-provider.test.ts +55 -0
- package/src/__tests__/postgres-linear-provider.test.ts +90 -0
- package/src/__tests__/postgres-local-provider.test.ts +26 -0
- package/src/__tests__/provider-sync-core.test.ts +150 -0
- package/src/__tests__/provider-team-info.test.ts +37 -0
- package/src/__tests__/ui-board-slice.test.ts +14 -0
- package/src/__tests__/use-cases.test.ts +21 -42
- package/src/api.ts +78 -132
- package/src/column-roles.ts +3 -2
- package/src/index.ts +135 -92
- package/src/mcp/core.ts +7 -8
- package/src/provider-runtime.ts +12 -56
- package/src/providers/cache-task-mappers.ts +123 -0
- package/src/providers/factory.ts +103 -0
- package/src/providers/index.ts +4 -25
- package/src/providers/jira-cache.ts +41 -99
- package/src/providers/jira-core.ts +78 -80
- package/src/providers/linear-cache.ts +42 -69
- package/src/providers/linear-core.ts +41 -55
- package/src/providers/local-core.ts +226 -0
- package/src/providers/local.ts +6 -182
- package/src/providers/postgres-batch.ts +10 -0
- package/src/providers/postgres-jira-cache.ts +255 -213
- package/src/providers/postgres-jira.ts +2 -20
- package/src/providers/postgres-linear-cache.ts +279 -183
- package/src/providers/postgres-linear.ts +2 -20
- package/src/providers/postgres-local.ts +22 -47
- package/src/providers/sqlite-local-store.ts +121 -0
- package/src/providers/sync-core.ts +91 -0
- package/src/providers/team-info.ts +24 -0
- package/src/providers/warn-once.ts +6 -0
- package/src/use-cases.ts +7 -131
- package/src/webhook-events.ts +29 -1
- package/ui/dist/assets/index-DcZH7fI3.js +40 -0
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/index-Cigv8a9S.js +0 -40
package/src/index.ts
CHANGED
|
@@ -10,13 +10,13 @@ import { columnAdd, columnDelete, columnList, columnRename, columnReorder } from
|
|
|
10
10
|
import { bulkClearDoneCmd, bulkMoveAllCmd } from './commands/bulk'
|
|
11
11
|
import { getConfigPath, loadConfig, saveConfig } from './config'
|
|
12
12
|
import { parsePositiveInt } from './transport-input'
|
|
13
|
-
import type { CliOutput, Priority } from './types'
|
|
13
|
+
import type { CliOutput, Priority, ProviderCapabilities } from './types'
|
|
14
14
|
import { unsupportedOperation } from './providers/errors'
|
|
15
15
|
import { openKanbanRuntime } from './provider-runtime'
|
|
16
16
|
import { trackerConfigFromEnv } from './tracker-config'
|
|
17
17
|
import type { KanbanProvider } from './providers/types'
|
|
18
18
|
import { resolvePollingSyncIntervalMs } from './sync-config'
|
|
19
|
-
import
|
|
19
|
+
import { normalizeCreateTaskInput } from './use-cases'
|
|
20
20
|
|
|
21
21
|
interface ParsedArgs {
|
|
22
22
|
values: Record<string, unknown>
|
|
@@ -24,30 +24,46 @@ interface ParsedArgs {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function parseCliArgs(argv: string[]): ParsedArgs {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
try {
|
|
28
|
+
return parseArgs({
|
|
29
|
+
args: argv,
|
|
30
|
+
options: {
|
|
31
|
+
pretty: { type: 'boolean', default: false },
|
|
32
|
+
db: { type: 'string' },
|
|
33
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
34
|
+
d: { type: 'string' },
|
|
35
|
+
c: { type: 'string' },
|
|
36
|
+
p: { type: 'string' },
|
|
37
|
+
a: { type: 'string' },
|
|
38
|
+
m: { type: 'string' },
|
|
39
|
+
l: { type: 'string' },
|
|
40
|
+
label: { type: 'string', multiple: true },
|
|
41
|
+
labels: { type: 'string', multiple: true },
|
|
42
|
+
sort: { type: 'string' },
|
|
43
|
+
title: { type: 'string' },
|
|
44
|
+
position: { type: 'string' },
|
|
45
|
+
color: { type: 'string' },
|
|
46
|
+
project: { type: 'string' },
|
|
47
|
+
role: { type: 'string' },
|
|
48
|
+
},
|
|
49
|
+
strict: true,
|
|
50
|
+
allowPositionals: true,
|
|
51
|
+
})
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new KanbanError(
|
|
54
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
55
|
+
err instanceof Error ? err.message : String(err),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requireCapability(
|
|
61
|
+
capabilities: ProviderCapabilities,
|
|
62
|
+
capability: keyof ProviderCapabilities,
|
|
63
|
+
feature: string,
|
|
64
|
+
): void {
|
|
65
|
+
if (!capabilities[capability])
|
|
66
|
+
unsupportedOperation(`${feature} is not supported by this provider`)
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
function requireLocalProvider(providerType: string, feature: string): void {
|
|
@@ -65,21 +81,23 @@ async function routeTask(
|
|
|
65
81
|
const title = positionals[2]
|
|
66
82
|
if (!title) throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task title is required')
|
|
67
83
|
return success(
|
|
68
|
-
await
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
await provider.createTask(
|
|
85
|
+
normalizeCreateTaskInput({
|
|
86
|
+
title,
|
|
87
|
+
description: values.d as string | undefined,
|
|
88
|
+
column: values.c as string | undefined,
|
|
89
|
+
priority: values.p as Priority | undefined,
|
|
90
|
+
assignee: values.a as string | undefined,
|
|
91
|
+
project: values.project as string | undefined,
|
|
92
|
+
labels: [values.label, values.labels],
|
|
93
|
+
metadata: values.m as string | undefined,
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
78
96
|
)
|
|
79
97
|
}
|
|
80
98
|
case 'list':
|
|
81
99
|
return success(
|
|
82
|
-
await
|
|
100
|
+
await provider.listTasks({
|
|
83
101
|
column: values.c as string | undefined,
|
|
84
102
|
priority: values.p as string | undefined,
|
|
85
103
|
assignee: values.a as string | undefined,
|
|
@@ -91,13 +109,13 @@ async function routeTask(
|
|
|
91
109
|
case 'view': {
|
|
92
110
|
const id = positionals[2]
|
|
93
111
|
if (!id) throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
94
|
-
return success(await
|
|
112
|
+
return success(await provider.getTask(id))
|
|
95
113
|
}
|
|
96
114
|
case 'update': {
|
|
97
115
|
const id = positionals[2]
|
|
98
116
|
if (!id) throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
99
117
|
return success(
|
|
100
|
-
await
|
|
118
|
+
await provider.updateTask(id, {
|
|
101
119
|
title: values.title as string | undefined,
|
|
102
120
|
description: values.d as string | undefined,
|
|
103
121
|
priority: values.p as Priority | undefined,
|
|
@@ -110,7 +128,7 @@ async function routeTask(
|
|
|
110
128
|
case 'delete': {
|
|
111
129
|
const id = positionals[2]
|
|
112
130
|
if (!id) throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
113
|
-
return success(await
|
|
131
|
+
return success(await provider.deleteTask(id))
|
|
114
132
|
}
|
|
115
133
|
case 'move': {
|
|
116
134
|
const id = positionals[2]
|
|
@@ -118,7 +136,7 @@ async function routeTask(
|
|
|
118
136
|
if (!id || !column) {
|
|
119
137
|
throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Usage: kanban task move <id> <column>')
|
|
120
138
|
}
|
|
121
|
-
return success(await
|
|
139
|
+
return success(await provider.moveTask(id, column))
|
|
122
140
|
}
|
|
123
141
|
case 'assign': {
|
|
124
142
|
const id = positionals[2]
|
|
@@ -129,7 +147,7 @@ async function routeTask(
|
|
|
129
147
|
'Usage: kanban task assign <id> <assignee>',
|
|
130
148
|
)
|
|
131
149
|
}
|
|
132
|
-
return success(await
|
|
150
|
+
return success(await provider.updateTask(id, { assignee }))
|
|
133
151
|
}
|
|
134
152
|
case 'prioritize': {
|
|
135
153
|
const id = positionals[2]
|
|
@@ -140,7 +158,7 @@ async function routeTask(
|
|
|
140
158
|
'Usage: kanban task prioritize <id> <level>',
|
|
141
159
|
)
|
|
142
160
|
}
|
|
143
|
-
return success(await
|
|
161
|
+
return success(await provider.updateTask(id, { priority: priority as Priority }))
|
|
144
162
|
}
|
|
145
163
|
default:
|
|
146
164
|
throw new KanbanError(ErrorCode.UNKNOWN_COMMAND, `Unknown task command '${action}'`)
|
|
@@ -156,7 +174,7 @@ async function routeComment(
|
|
|
156
174
|
case 'list': {
|
|
157
175
|
const id = positionals[2]
|
|
158
176
|
if (!id) throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Task ID is required')
|
|
159
|
-
return success(await
|
|
177
|
+
return success(await provider.listComments(id))
|
|
160
178
|
}
|
|
161
179
|
case 'add': {
|
|
162
180
|
const id = positionals[2]
|
|
@@ -167,7 +185,7 @@ async function routeComment(
|
|
|
167
185
|
'Usage: kanban comment add <task-id> <body>',
|
|
168
186
|
)
|
|
169
187
|
}
|
|
170
|
-
return success(await
|
|
188
|
+
return success(await provider.comment(id, body))
|
|
171
189
|
}
|
|
172
190
|
case 'update': {
|
|
173
191
|
const id = positionals[2]
|
|
@@ -179,7 +197,7 @@ async function routeComment(
|
|
|
179
197
|
'Usage: kanban comment update <task-id> <comment-id> <body>',
|
|
180
198
|
)
|
|
181
199
|
}
|
|
182
|
-
return success(await
|
|
200
|
+
return success(await provider.updateComment(id, commentId, body))
|
|
183
201
|
}
|
|
184
202
|
default:
|
|
185
203
|
throw new KanbanError(ErrorCode.UNKNOWN_COMMAND, `Unknown comment command '${action}'`)
|
|
@@ -188,12 +206,12 @@ async function routeComment(
|
|
|
188
206
|
|
|
189
207
|
function routeColumn(
|
|
190
208
|
db: Database,
|
|
191
|
-
|
|
209
|
+
capabilities: ProviderCapabilities,
|
|
192
210
|
action: string | undefined,
|
|
193
211
|
positionals: string[],
|
|
194
212
|
values: Record<string, unknown>,
|
|
195
213
|
): CliOutput {
|
|
196
|
-
|
|
214
|
+
requireCapability(capabilities, 'columnCrud', 'Column commands')
|
|
197
215
|
switch (action) {
|
|
198
216
|
case 'add':
|
|
199
217
|
return columnAdd(db, {
|
|
@@ -216,11 +234,11 @@ function routeColumn(
|
|
|
216
234
|
|
|
217
235
|
function routeBulk(
|
|
218
236
|
db: Database,
|
|
219
|
-
|
|
237
|
+
capabilities: ProviderCapabilities,
|
|
220
238
|
action: string | undefined,
|
|
221
239
|
positionals: string[],
|
|
222
240
|
): CliOutput {
|
|
223
|
-
|
|
241
|
+
requireCapability(capabilities, 'bulk', 'Bulk commands')
|
|
224
242
|
switch (action) {
|
|
225
243
|
case 'move-all':
|
|
226
244
|
return bulkMoveAllCmd(db, { from: positionals[2], to: positionals[3] })
|
|
@@ -233,25 +251,21 @@ function routeBulk(
|
|
|
233
251
|
|
|
234
252
|
async function routeConfig(
|
|
235
253
|
provider: KanbanProvider,
|
|
254
|
+
capabilities: ProviderCapabilities,
|
|
236
255
|
dbPath: string,
|
|
237
256
|
action: string | undefined,
|
|
238
257
|
positionals: string[],
|
|
239
258
|
values: Record<string, unknown>,
|
|
240
259
|
): Promise<CliOutput> {
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
return success(await useCases.getConfig(provider))
|
|
244
|
-
}
|
|
245
|
-
unsupportedOperation('Config mutation is only available in local mode')
|
|
260
|
+
if (action === 'show' || action === undefined) {
|
|
261
|
+
return success(await provider.getConfig())
|
|
246
262
|
}
|
|
263
|
+
requireCapability(capabilities, 'configEdit', 'Config mutation')
|
|
247
264
|
|
|
248
265
|
const configPath = getConfigPath(dbPath)
|
|
249
266
|
const config = loadConfig(dbPath)
|
|
250
267
|
|
|
251
268
|
switch (action) {
|
|
252
|
-
case 'show':
|
|
253
|
-
case undefined:
|
|
254
|
-
return success(await useCases.getConfig(provider))
|
|
255
269
|
case 'set-member': {
|
|
256
270
|
const name = positionals[2]
|
|
257
271
|
if (!name) throw new KanbanError(ErrorCode.MISSING_ARGUMENT, 'Member name is required')
|
|
@@ -294,7 +308,7 @@ async function routeConfig(
|
|
|
294
308
|
}
|
|
295
309
|
}
|
|
296
310
|
|
|
297
|
-
async function
|
|
311
|
+
async function routeLocalBoard(
|
|
298
312
|
db: Database,
|
|
299
313
|
provider: KanbanProvider,
|
|
300
314
|
action: string | undefined,
|
|
@@ -310,7 +324,7 @@ async function routeBoard(
|
|
|
310
324
|
initSchema(db)
|
|
311
325
|
seedDefaultColumns(db, columnNames)
|
|
312
326
|
}
|
|
313
|
-
return success(await
|
|
327
|
+
return success(await provider.getBoard())
|
|
314
328
|
case 'reset':
|
|
315
329
|
requireLocalProvider(provider.type, 'Board reset')
|
|
316
330
|
return boardReset(db, columnNames)
|
|
@@ -332,29 +346,28 @@ async function run(argv: string[]): Promise<{ output: CliOutput; exitCode: numbe
|
|
|
332
346
|
})
|
|
333
347
|
|
|
334
348
|
try {
|
|
335
|
-
const { provider, sqliteDb, dbPath, trackerConfig } = runtime
|
|
349
|
+
const { provider, sqliteDb, dbPath, trackerConfig, capabilities } = runtime
|
|
336
350
|
const group = positionals[0]
|
|
337
351
|
const action = positionals[1]
|
|
338
352
|
const defaultColumns =
|
|
339
353
|
trackerConfig.provider === 'local' ? trackerConfig.defaultColumns : undefined
|
|
340
354
|
|
|
341
355
|
if (!group) {
|
|
342
|
-
if (sqliteDb)
|
|
356
|
+
if (sqliteDb && provider.type === 'local')
|
|
343
357
|
return {
|
|
344
|
-
output: await
|
|
358
|
+
output: await routeLocalBoard(sqliteDb, provider, undefined, defaultColumns),
|
|
345
359
|
exitCode: 0,
|
|
346
360
|
}
|
|
347
|
-
return { output: success(await
|
|
361
|
+
return { output: success(await provider.getBoard()), exitCode: 0 }
|
|
348
362
|
}
|
|
349
363
|
|
|
350
364
|
let output: CliOutput
|
|
351
365
|
switch (group) {
|
|
352
366
|
case 'board':
|
|
353
367
|
if (sqliteDb) {
|
|
354
|
-
output = await
|
|
368
|
+
output = await routeLocalBoard(sqliteDb, provider, action, defaultColumns)
|
|
355
369
|
} else {
|
|
356
|
-
if (action === 'view' || action === undefined)
|
|
357
|
-
output = success(await useCases.getBoard(provider))
|
|
370
|
+
if (action === 'view' || action === undefined) output = success(await provider.getBoard())
|
|
358
371
|
else unsupportedOperation(`board ${action} is not available with KANBAN_STORAGE=postgres`)
|
|
359
372
|
}
|
|
360
373
|
break
|
|
@@ -367,12 +380,12 @@ async function run(argv: string[]): Promise<{ output: CliOutput; exitCode: numbe
|
|
|
367
380
|
case 'column':
|
|
368
381
|
if (!sqliteDb)
|
|
369
382
|
unsupportedOperation('Column commands are not available with KANBAN_STORAGE=postgres')
|
|
370
|
-
output = routeColumn(sqliteDb,
|
|
383
|
+
output = routeColumn(sqliteDb, capabilities, action, positionals, values)
|
|
371
384
|
break
|
|
372
385
|
case 'bulk':
|
|
373
386
|
if (!sqliteDb)
|
|
374
387
|
unsupportedOperation('Bulk commands are not available with KANBAN_STORAGE=postgres')
|
|
375
|
-
output = routeBulk(sqliteDb,
|
|
388
|
+
output = routeBulk(sqliteDb, capabilities, action, positionals)
|
|
376
389
|
break
|
|
377
390
|
case 'config':
|
|
378
391
|
// routeConfig persists to the SQLite-side config file, so it only runs
|
|
@@ -380,10 +393,10 @@ async function run(argv: string[]): Promise<{ output: CliOutput; exitCode: numbe
|
|
|
380
393
|
// repository (configEdit:false), so edits are refused here — matching the
|
|
381
394
|
// HTTP API, which fails through the provider's patchConfig.
|
|
382
395
|
if (sqliteDb) {
|
|
383
|
-
output = await routeConfig(provider, dbPath, action, positionals, values)
|
|
396
|
+
output = await routeConfig(provider, capabilities, dbPath, action, positionals, values)
|
|
384
397
|
} else {
|
|
385
398
|
if (action === 'show' || action === undefined)
|
|
386
|
-
output = success(await
|
|
399
|
+
output = success(await provider.getConfig())
|
|
387
400
|
else
|
|
388
401
|
unsupportedOperation(`config ${action} is not available with KANBAN_STORAGE=postgres`)
|
|
389
402
|
}
|
|
@@ -455,19 +468,27 @@ export interface ServeOptions {
|
|
|
455
468
|
}
|
|
456
469
|
|
|
457
470
|
export function parseServeArgs(argv: string[]): ServeOptions {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
let values: Record<string, unknown>
|
|
472
|
+
try {
|
|
473
|
+
values = parseArgs({
|
|
474
|
+
args: argv,
|
|
475
|
+
options: {
|
|
476
|
+
db: { type: 'string' },
|
|
477
|
+
port: { type: 'string' },
|
|
478
|
+
'sync-interval-ms': { type: 'string' },
|
|
479
|
+
tunnel: { type: 'boolean', default: false },
|
|
480
|
+
token: { type: 'string' },
|
|
481
|
+
'allowed-origin': { type: 'string' },
|
|
482
|
+
},
|
|
483
|
+
strict: true,
|
|
484
|
+
allowPositionals: true,
|
|
485
|
+
}).values
|
|
486
|
+
} catch (err) {
|
|
487
|
+
throw new KanbanError(
|
|
488
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
489
|
+
err instanceof Error ? err.message : String(err),
|
|
490
|
+
)
|
|
491
|
+
}
|
|
471
492
|
const port = values.port
|
|
472
493
|
? parseInt(values.port as string, 10)
|
|
473
494
|
: parseInt(process.env['PORT'] || '3000', 10)
|
|
@@ -496,20 +517,42 @@ export interface McpOptions {
|
|
|
496
517
|
}
|
|
497
518
|
|
|
498
519
|
export function parseMcpArgs(argv: string[]): McpOptions {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
520
|
+
let values: Record<string, unknown>
|
|
521
|
+
try {
|
|
522
|
+
values = parseArgs({
|
|
523
|
+
args: argv,
|
|
524
|
+
options: { db: { type: 'string' } },
|
|
525
|
+
strict: true,
|
|
526
|
+
allowPositionals: true,
|
|
527
|
+
}).values
|
|
528
|
+
} catch (err) {
|
|
529
|
+
throw new KanbanError(
|
|
530
|
+
ErrorCode.INVALID_ARGUMENT,
|
|
531
|
+
err instanceof Error ? err.message : String(err),
|
|
532
|
+
)
|
|
533
|
+
}
|
|
505
534
|
return { db: values.db as string | undefined }
|
|
506
535
|
}
|
|
507
536
|
|
|
537
|
+
// Bad serve/mcp flags should print the same structured error envelope as the
|
|
538
|
+
// plain CLI branch, not an uncaught stack trace.
|
|
539
|
+
function parseEntryArgs<T>(parse: () => T): T {
|
|
540
|
+
try {
|
|
541
|
+
return parse()
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (err instanceof KanbanError) {
|
|
544
|
+
console.error(formatOutput(error(err.code, err.message), false))
|
|
545
|
+
process.exit(1)
|
|
546
|
+
}
|
|
547
|
+
throw err
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
508
551
|
if (import.meta.main) {
|
|
509
552
|
const argv = process.argv.slice(2)
|
|
510
553
|
|
|
511
554
|
if (argv[0] === 'mcp') {
|
|
512
|
-
const opts = parseMcpArgs(argv)
|
|
555
|
+
const opts = parseEntryArgs(() => parseMcpArgs(argv))
|
|
513
556
|
const runtime = await openKanbanRuntime({ dbPath: opts.db ?? getDbPath() })
|
|
514
557
|
const { startStdioMcpServer } = await import('./commands/mcp')
|
|
515
558
|
try {
|
|
@@ -518,7 +561,7 @@ if (import.meta.main) {
|
|
|
518
561
|
await runtime.close()
|
|
519
562
|
}
|
|
520
563
|
} else if (argv[0] === 'serve') {
|
|
521
|
-
const opts = parseServeArgs(argv)
|
|
564
|
+
const opts = parseEntryArgs(() => parseServeArgs(argv))
|
|
522
565
|
|
|
523
566
|
// A tunnel exposes the dashboard publicly, so refuse to start one without a
|
|
524
567
|
// token. Plain localhost serve stays open for backward compatibility.
|
package/src/mcp/core.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { KanbanProvider } from '../providers/types'
|
|
2
2
|
import type { BoardView, Task, TaskComment } from '../types'
|
|
3
|
-
import * as useCases from '../use-cases'
|
|
4
3
|
import { TrackerMcpError, toTrackerMcpError } from './errors'
|
|
5
4
|
import type { TrackerMcpHooks, TrackerMcpPolicy } from './types'
|
|
6
5
|
|
|
@@ -138,7 +137,7 @@ export function createTrackerCore<TScope>(input: {
|
|
|
138
137
|
ticketId,
|
|
139
138
|
execute: async () => {
|
|
140
139
|
await policy.canReadTicket(scope, ticketId)
|
|
141
|
-
return
|
|
140
|
+
return provider.getTask(ticketId)
|
|
142
141
|
},
|
|
143
142
|
})
|
|
144
143
|
},
|
|
@@ -150,7 +149,7 @@ export function createTrackerCore<TScope>(input: {
|
|
|
150
149
|
ticketId,
|
|
151
150
|
execute: async () => {
|
|
152
151
|
await policy.canReadTicket(scope, ticketId)
|
|
153
|
-
const comments = await
|
|
152
|
+
const comments = await provider.listComments(ticketId)
|
|
154
153
|
return filterComments(scope, comments, policy)
|
|
155
154
|
},
|
|
156
155
|
resultMeta: (comments) => ({ commentCount: comments.length }),
|
|
@@ -163,7 +162,7 @@ export function createTrackerCore<TScope>(input: {
|
|
|
163
162
|
tool: 'getBoard',
|
|
164
163
|
execute: async () => {
|
|
165
164
|
await policy.canReadBoard?.(scope)
|
|
166
|
-
const board = await
|
|
165
|
+
const board = await provider.getBoard()
|
|
167
166
|
return filterBoard(scope, board, policy)
|
|
168
167
|
},
|
|
169
168
|
resultMeta: (board) => ({
|
|
@@ -179,7 +178,7 @@ export function createTrackerCore<TScope>(input: {
|
|
|
179
178
|
ticketId,
|
|
180
179
|
execute: async () => {
|
|
181
180
|
await policy.canPostComment(scope, ticketId, body)
|
|
182
|
-
return
|
|
181
|
+
return provider.comment(ticketId, body)
|
|
183
182
|
},
|
|
184
183
|
resultMeta: (comment) => ({ commentId: comment.id }),
|
|
185
184
|
})
|
|
@@ -191,9 +190,9 @@ export function createTrackerCore<TScope>(input: {
|
|
|
191
190
|
tool: 'updateComment',
|
|
192
191
|
ticketId,
|
|
193
192
|
execute: async () => {
|
|
194
|
-
const existing = await
|
|
193
|
+
const existing = await provider.getComment(ticketId, commentId)
|
|
195
194
|
await policy.canUpdateComment(scope, ticketId, existing, body)
|
|
196
|
-
return
|
|
195
|
+
return provider.updateComment(ticketId, commentId, body)
|
|
197
196
|
},
|
|
198
197
|
resultMeta: (comment) => ({ commentId: comment.id }),
|
|
199
198
|
})
|
|
@@ -206,7 +205,7 @@ export function createTrackerCore<TScope>(input: {
|
|
|
206
205
|
ticketId,
|
|
207
206
|
execute: async () => {
|
|
208
207
|
await policy.canMoveTicket(scope, ticketId, column)
|
|
209
|
-
await
|
|
208
|
+
await provider.moveTask(ticketId, column)
|
|
210
209
|
},
|
|
211
210
|
resultMeta: { movedTo: column },
|
|
212
211
|
})
|
package/src/provider-runtime.ts
CHANGED
|
@@ -2,18 +2,16 @@ import { Database } from 'bun:sqlite'
|
|
|
2
2
|
import postgres from 'postgres'
|
|
3
3
|
|
|
4
4
|
import { getDbPath, migrateSchema, openDb } from './db'
|
|
5
|
-
import {
|
|
6
|
-
import { createProvider } from './providers/index'
|
|
7
|
-
import { PostgresJiraProvider } from './providers/postgres-jira'
|
|
8
|
-
import { PostgresLinearProvider } from './providers/postgres-linear'
|
|
9
|
-
import { PostgresLocalProvider } from './providers/postgres-local'
|
|
5
|
+
import { createPostgresProvider, createSqliteProvider } from './providers/factory'
|
|
10
6
|
import type { KanbanProvider } from './providers/types'
|
|
7
|
+
import type { ProviderCapabilities } from './types'
|
|
11
8
|
import { resolveKanbanStorageConfig } from './storage-config'
|
|
12
9
|
import type { KanbanStorageConfig } from './storage-config'
|
|
13
10
|
import { trackerConfigFromEnv, type TrackerConfig } from './tracker-config'
|
|
14
11
|
|
|
15
12
|
export interface KanbanRuntime {
|
|
16
13
|
provider: KanbanProvider
|
|
14
|
+
capabilities: ProviderCapabilities
|
|
17
15
|
dbPath: string
|
|
18
16
|
trackerConfig: TrackerConfig
|
|
19
17
|
sqliteDb?: Database
|
|
@@ -36,50 +34,12 @@ export async function openKanbanRuntime(
|
|
|
36
34
|
|
|
37
35
|
if (storage.mode === 'postgres') {
|
|
38
36
|
const sql = postgres(storage.databaseUrl, { max: 5, onnotice: () => {} })
|
|
39
|
-
|
|
40
|
-
const provider =
|
|
41
|
-
await provider.initialize()
|
|
42
|
-
return {
|
|
43
|
-
provider,
|
|
44
|
-
dbPath,
|
|
45
|
-
trackerConfig,
|
|
46
|
-
syncIntervalMs: trackerConfig.syncIntervalMs,
|
|
47
|
-
async close() {
|
|
48
|
-
await sql.end({ timeout: 1 })
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (trackerConfig.provider === 'jira') {
|
|
53
|
-
const provider = new PostgresJiraProvider(sql, {
|
|
54
|
-
baseUrl: trackerConfig.baseUrl,
|
|
55
|
-
email: trackerConfig.email,
|
|
56
|
-
apiToken: trackerConfig.apiToken,
|
|
57
|
-
projectKey: trackerConfig.projectKey,
|
|
58
|
-
...(trackerConfig.boardId !== undefined ? { boardId: trackerConfig.boardId } : {}),
|
|
59
|
-
defaultIssueType: trackerConfig.defaultIssueType ?? 'Task',
|
|
60
|
-
pollingSyncIntervalMs: trackerConfig.syncIntervalMs,
|
|
61
|
-
})
|
|
62
|
-
await provider.initialize()
|
|
63
|
-
return {
|
|
64
|
-
provider,
|
|
65
|
-
dbPath,
|
|
66
|
-
trackerConfig,
|
|
67
|
-
syncIntervalMs: trackerConfig.syncIntervalMs,
|
|
68
|
-
async close() {
|
|
69
|
-
await sql.end({ timeout: 1 })
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (trackerConfig.provider === 'linear') {
|
|
74
|
-
const provider = new PostgresLinearProvider(
|
|
75
|
-
sql,
|
|
76
|
-
trackerConfig.teamId,
|
|
77
|
-
trackerConfig.apiKey,
|
|
78
|
-
trackerConfig.syncIntervalMs,
|
|
79
|
-
)
|
|
37
|
+
try {
|
|
38
|
+
const { provider, capabilities } = createPostgresProvider(sql, trackerConfig)
|
|
80
39
|
await provider.initialize()
|
|
81
40
|
return {
|
|
82
41
|
provider,
|
|
42
|
+
capabilities,
|
|
83
43
|
dbPath,
|
|
84
44
|
trackerConfig,
|
|
85
45
|
syncIntervalMs: trackerConfig.syncIntervalMs,
|
|
@@ -87,13 +47,6 @@ export async function openKanbanRuntime(
|
|
|
87
47
|
await sql.end({ timeout: 1 })
|
|
88
48
|
},
|
|
89
49
|
}
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const _exhaustive: never = trackerConfig
|
|
93
|
-
unsupportedOperation(
|
|
94
|
-
`KANBAN_STORAGE=postgres currently supports KANBAN_PROVIDER=local, linear, or jira in agent-kanban.`,
|
|
95
|
-
)
|
|
96
|
-
void _exhaustive
|
|
97
50
|
} catch (err) {
|
|
98
51
|
await sql.end({ timeout: 1 })
|
|
99
52
|
throw err
|
|
@@ -102,10 +55,13 @@ export async function openKanbanRuntime(
|
|
|
102
55
|
|
|
103
56
|
const db = openDb(storage.sqlitePath)
|
|
104
57
|
migrateSchema(db)
|
|
58
|
+
const { provider, capabilities } = createSqliteProvider(db, trackerConfig, {
|
|
59
|
+
dbPath: storage.sqlitePath,
|
|
60
|
+
seedLocalColumns: opts.seedLocalColumns,
|
|
61
|
+
})
|
|
105
62
|
return {
|
|
106
|
-
provider
|
|
107
|
-
|
|
108
|
-
}),
|
|
63
|
+
provider,
|
|
64
|
+
capabilities,
|
|
109
65
|
dbPath: storage.sqlitePath,
|
|
110
66
|
trackerConfig,
|
|
111
67
|
sqliteDb: db,
|