prodboard 0.0.0 → 0.1.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.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/bin/prodboard.ts +4 -0
- package/config.schema.json +100 -0
- package/package.json +47 -6
- package/src/commands/comments.ts +86 -0
- package/src/commands/daemon.ts +112 -0
- package/src/commands/init.ts +83 -0
- package/src/commands/install.ts +127 -0
- package/src/commands/issues.ts +268 -0
- package/src/commands/schedules.ts +276 -0
- package/src/config.ts +155 -0
- package/src/confirm.ts +14 -0
- package/src/cron.ts +121 -0
- package/src/db.ts +157 -0
- package/src/format.ts +99 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +227 -0
- package/src/invocation.ts +102 -0
- package/src/logger.ts +84 -0
- package/src/mcp.ts +543 -0
- package/src/queries/comments.ts +31 -0
- package/src/queries/issues.ts +155 -0
- package/src/queries/runs.ts +159 -0
- package/src/queries/schedules.ts +115 -0
- package/src/scheduler.ts +411 -0
- package/src/templates.ts +43 -0
- package/src/types.ts +82 -0
- package/templates/CLAUDE.md +12 -0
- package/templates/config.jsonc +38 -0
- package/templates/mcp.json +8 -0
- package/templates/system-prompt-nogit.md +33 -0
- package/templates/system-prompt.md +31 -0
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
ListResourcesRequestSchema,
|
|
7
|
+
ReadResourceRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { Database } from "bun:sqlite";
|
|
10
|
+
import { ensureDb } from "./db.ts";
|
|
11
|
+
import { loadConfig } from "./config.ts";
|
|
12
|
+
import {
|
|
13
|
+
createIssue, getIssueByPrefix, listIssues, updateIssue,
|
|
14
|
+
deleteIssue, getIssueCounts, validateStatus, resolveIssueId,
|
|
15
|
+
} from "./queries/issues.ts";
|
|
16
|
+
import { createComment, listComments, getCommentCount } from "./queries/comments.ts";
|
|
17
|
+
import type { Config } from "./types.ts";
|
|
18
|
+
|
|
19
|
+
// Lazy-load schedule/run queries (they may not exist yet during early phases)
|
|
20
|
+
// undefined = not tried, null = failed permanently, object = loaded
|
|
21
|
+
let scheduleQueries: any = undefined;
|
|
22
|
+
let runQueries: any = undefined;
|
|
23
|
+
|
|
24
|
+
async function getScheduleQueries() {
|
|
25
|
+
if (scheduleQueries === undefined) {
|
|
26
|
+
try {
|
|
27
|
+
scheduleQueries = await import("./queries/schedules.ts");
|
|
28
|
+
} catch {
|
|
29
|
+
scheduleQueries = null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return scheduleQueries;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getRunQueries() {
|
|
36
|
+
if (runQueries === undefined) {
|
|
37
|
+
try {
|
|
38
|
+
runQueries = await import("./queries/runs.ts");
|
|
39
|
+
} catch {
|
|
40
|
+
runQueries = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return runQueries;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const TOOLS = [
|
|
47
|
+
{
|
|
48
|
+
name: "list_issues",
|
|
49
|
+
description: "List issues with optional filters. Returns id, title, status, comment_count, updated_at (no description for compact output).",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object" as const,
|
|
52
|
+
properties: {
|
|
53
|
+
status: { type: "array" as const, items: { type: "string" as const }, description: "Filter by status(es)" },
|
|
54
|
+
search: { type: "string" as const, description: "Search title and description" },
|
|
55
|
+
include_archived: { type: "boolean" as const, description: "Include archived issues" },
|
|
56
|
+
limit: { type: "number" as const, description: "Max issues to return (default 50)" },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "get_issue",
|
|
62
|
+
description: "Get full issue details including description and all comments. Accepts full ID or unique prefix.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object" as const,
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: "string" as const, description: "Issue ID or unique prefix" },
|
|
67
|
+
},
|
|
68
|
+
required: ["id"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "create_issue",
|
|
73
|
+
description: "Create a new issue.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object" as const,
|
|
76
|
+
properties: {
|
|
77
|
+
title: { type: "string" as const, description: "Issue title" },
|
|
78
|
+
description: { type: "string" as const, description: "Issue description" },
|
|
79
|
+
status: { type: "string" as const, description: "Initial status (default: todo)" },
|
|
80
|
+
},
|
|
81
|
+
required: ["title"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "update_issue",
|
|
86
|
+
description: "Update an existing issue's fields.",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object" as const,
|
|
89
|
+
properties: {
|
|
90
|
+
id: { type: "string" as const, description: "Issue ID or unique prefix" },
|
|
91
|
+
title: { type: "string" as const, description: "New title" },
|
|
92
|
+
description: { type: "string" as const, description: "New description" },
|
|
93
|
+
status: { type: "string" as const, description: "New status" },
|
|
94
|
+
},
|
|
95
|
+
required: ["id"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "delete_issue",
|
|
100
|
+
description: "Delete an issue and all its comments.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object" as const,
|
|
103
|
+
properties: {
|
|
104
|
+
id: { type: "string" as const, description: "Issue ID or unique prefix" },
|
|
105
|
+
},
|
|
106
|
+
required: ["id"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "add_comment",
|
|
111
|
+
description: "Add a comment to an issue.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object" as const,
|
|
114
|
+
properties: {
|
|
115
|
+
issue_id: { type: "string" as const, description: "Issue ID or unique prefix" },
|
|
116
|
+
body: { type: "string" as const, description: "Comment text" },
|
|
117
|
+
author: { type: "string" as const, description: "Comment author (default: claude)" },
|
|
118
|
+
},
|
|
119
|
+
required: ["issue_id", "body"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "board_summary",
|
|
124
|
+
description: "Get a summary of the board: issue counts by status, recent issues, active schedules.",
|
|
125
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "pick_next_issue",
|
|
129
|
+
description: "Pick the next available issue to work on. Moves it to in-progress and adds a 'Work started' comment.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object" as const,
|
|
132
|
+
properties: {
|
|
133
|
+
status: { type: "string" as const, description: "Status to pick from (default: todo)" },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "complete_issue",
|
|
139
|
+
description: "Mark an issue as done and optionally add a completion comment.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object" as const,
|
|
142
|
+
properties: {
|
|
143
|
+
id: { type: "string" as const, description: "Issue ID or unique prefix" },
|
|
144
|
+
comment: { type: "string" as const, description: "Optional completion comment" },
|
|
145
|
+
},
|
|
146
|
+
required: ["id"],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "list_schedules",
|
|
151
|
+
description: "List scheduled tasks with their status and last run info.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object" as const,
|
|
154
|
+
properties: {
|
|
155
|
+
include_disabled: { type: "boolean" as const, description: "Include disabled schedules" },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "create_schedule",
|
|
161
|
+
description: "Create a new scheduled task.",
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: "object" as const,
|
|
164
|
+
properties: {
|
|
165
|
+
name: { type: "string" as const, description: "Schedule name" },
|
|
166
|
+
cron: { type: "string" as const, description: "Cron expression (5 fields)" },
|
|
167
|
+
prompt: { type: "string" as const, description: "Prompt to send to Claude" },
|
|
168
|
+
workdir: { type: "string" as const, description: "Working directory" },
|
|
169
|
+
max_turns: { type: "number" as const, description: "Max turns per run" },
|
|
170
|
+
},
|
|
171
|
+
required: ["name", "cron", "prompt"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "update_schedule",
|
|
176
|
+
description: "Update a scheduled task.",
|
|
177
|
+
inputSchema: {
|
|
178
|
+
type: "object" as const,
|
|
179
|
+
properties: {
|
|
180
|
+
id: { type: "string" as const, description: "Schedule ID or prefix" },
|
|
181
|
+
name: { type: "string" as const },
|
|
182
|
+
cron: { type: "string" as const },
|
|
183
|
+
prompt: { type: "string" as const },
|
|
184
|
+
enabled: { type: "boolean" as const },
|
|
185
|
+
},
|
|
186
|
+
required: ["id"],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "delete_schedule",
|
|
191
|
+
description: "Delete a scheduled task and its run history.",
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: "object" as const,
|
|
194
|
+
properties: {
|
|
195
|
+
id: { type: "string" as const, description: "Schedule ID or prefix" },
|
|
196
|
+
},
|
|
197
|
+
required: ["id"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "list_runs",
|
|
202
|
+
description: "List run history for schedules.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object" as const,
|
|
205
|
+
properties: {
|
|
206
|
+
schedule_id: { type: "string" as const, description: "Filter by schedule ID" },
|
|
207
|
+
status: { type: "string" as const, description: "Filter by run status" },
|
|
208
|
+
limit: { type: "number" as const, description: "Max runs to return" },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const RESOURCES = [
|
|
215
|
+
{
|
|
216
|
+
uri: "prodboard://issues",
|
|
217
|
+
name: "Board Summary",
|
|
218
|
+
description: "Current issue board summary with counts and recent issues",
|
|
219
|
+
mimeType: "application/json",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
uri: "prodboard://schedules",
|
|
223
|
+
name: "Active Schedules",
|
|
224
|
+
description: "Active scheduled tasks with next run times",
|
|
225
|
+
mimeType: "application/json",
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
// Handler functions
|
|
230
|
+
export function handleListIssues(db: Database, params: any) {
|
|
231
|
+
const { issues } = listIssues(db, {
|
|
232
|
+
status: params.status,
|
|
233
|
+
search: params.search,
|
|
234
|
+
includeArchived: params.include_archived,
|
|
235
|
+
limit: params.limit,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return issues.map((i) => ({
|
|
239
|
+
id: i.id,
|
|
240
|
+
title: i.title,
|
|
241
|
+
status: i.status,
|
|
242
|
+
comment_count: getCommentCount(db, i.id),
|
|
243
|
+
updated_at: i.updated_at,
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function handleGetIssue(db: Database, params: any) {
|
|
248
|
+
const issue = getIssueByPrefix(db, params.id);
|
|
249
|
+
const comments = listComments(db, issue.id);
|
|
250
|
+
return { ...issue, comments };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function handleCreateIssue(db: Database, config: Config, params: any) {
|
|
254
|
+
if (params.status) validateStatus(params.status, config);
|
|
255
|
+
return createIssue(db, {
|
|
256
|
+
title: params.title,
|
|
257
|
+
description: params.description,
|
|
258
|
+
status: params.status,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function handleUpdateIssue(db: Database, config: Config, params: any) {
|
|
263
|
+
const id = resolveIssueId(db, params.id);
|
|
264
|
+
const fields: any = {};
|
|
265
|
+
if (params.title !== undefined) fields.title = params.title;
|
|
266
|
+
if (params.description !== undefined) fields.description = params.description;
|
|
267
|
+
if (params.status !== undefined) {
|
|
268
|
+
validateStatus(params.status, config);
|
|
269
|
+
fields.status = params.status;
|
|
270
|
+
}
|
|
271
|
+
return updateIssue(db, id, fields);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function handleDeleteIssue(db: Database, params: any) {
|
|
275
|
+
const id = resolveIssueId(db, params.id);
|
|
276
|
+
deleteIssue(db, id);
|
|
277
|
+
return { deleted: true, id };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function handleAddComment(db: Database, params: any) {
|
|
281
|
+
const id = resolveIssueId(db, params.issue_id);
|
|
282
|
+
return createComment(db, {
|
|
283
|
+
issue_id: id,
|
|
284
|
+
body: params.body,
|
|
285
|
+
author: params.author ?? "claude",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function handleBoardSummary(db: Database) {
|
|
290
|
+
const counts = getIssueCounts(db);
|
|
291
|
+
const { issues: recent } = listIssues(db, { limit: 5 });
|
|
292
|
+
const recentCompact = recent.map((i) => ({
|
|
293
|
+
id: i.id,
|
|
294
|
+
title: i.title,
|
|
295
|
+
status: i.status,
|
|
296
|
+
updated_at: i.updated_at,
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
counts,
|
|
301
|
+
statuses: Object.keys(counts),
|
|
302
|
+
recent_issues: recentCompact,
|
|
303
|
+
total_issues: Object.values(counts).reduce((a, b) => a + b, 0),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function handlePickNextIssue(db: Database, config: Config, params: any) {
|
|
308
|
+
const status = params?.status ?? "todo";
|
|
309
|
+
const { issues } = listIssues(db, {
|
|
310
|
+
status: [status],
|
|
311
|
+
sort: "created_at",
|
|
312
|
+
order: "ASC",
|
|
313
|
+
limit: 1,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (issues.length === 0) {
|
|
317
|
+
return { picked: null, message: `No issues with status '${status}' found.` };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const issue = issues[0];
|
|
321
|
+
validateStatus("in-progress", config);
|
|
322
|
+
updateIssue(db, issue.id, { status: "in-progress" });
|
|
323
|
+
createComment(db, {
|
|
324
|
+
issue_id: issue.id,
|
|
325
|
+
body: "Work started",
|
|
326
|
+
author: "claude",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const updated = getIssueByPrefix(db, issue.id);
|
|
330
|
+
const comments = listComments(db, issue.id);
|
|
331
|
+
return { ...updated, comments };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function handleCompleteIssue(db: Database, config: Config, params: any) {
|
|
335
|
+
const id = resolveIssueId(db, params.id);
|
|
336
|
+
validateStatus("done", config);
|
|
337
|
+
updateIssue(db, id, { status: "done" });
|
|
338
|
+
|
|
339
|
+
if (params.comment) {
|
|
340
|
+
createComment(db, {
|
|
341
|
+
issue_id: id,
|
|
342
|
+
body: params.comment,
|
|
343
|
+
author: "claude",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const updated = getIssueByPrefix(db, id);
|
|
348
|
+
const comments = listComments(db, id);
|
|
349
|
+
return { ...updated, comments };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function handleListSchedules(db: Database, params: any) {
|
|
353
|
+
const sq = await getScheduleQueries();
|
|
354
|
+
const rq = await getRunQueries();
|
|
355
|
+
if (!sq) return [];
|
|
356
|
+
|
|
357
|
+
const schedules = sq.listSchedules(db, {
|
|
358
|
+
includeDisabled: params?.include_disabled,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const result = [];
|
|
362
|
+
for (const s of schedules) {
|
|
363
|
+
let lastRun = null;
|
|
364
|
+
if (rq) {
|
|
365
|
+
lastRun = rq.getLastRun(db, s.id);
|
|
366
|
+
}
|
|
367
|
+
result.push({
|
|
368
|
+
...s,
|
|
369
|
+
last_run: lastRun ? { status: lastRun.status, finished_at: lastRun.finished_at } : null,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function handleCreateSchedule(db: Database, params: any) {
|
|
376
|
+
const sq = await getScheduleQueries();
|
|
377
|
+
if (!sq) throw new Error("Schedule module not available");
|
|
378
|
+
|
|
379
|
+
const { validateCron } = await import("./cron.ts");
|
|
380
|
+
const validation = validateCron(params.cron);
|
|
381
|
+
if (!validation.valid) {
|
|
382
|
+
throw new Error(`Invalid cron expression: ${validation.error}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return sq.createSchedule(db, {
|
|
386
|
+
name: params.name,
|
|
387
|
+
cron: params.cron,
|
|
388
|
+
prompt: params.prompt,
|
|
389
|
+
workdir: params.workdir,
|
|
390
|
+
max_turns: params.max_turns,
|
|
391
|
+
source: "mcp",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function handleUpdateSchedule(db: Database, params: any) {
|
|
396
|
+
const sq = await getScheduleQueries();
|
|
397
|
+
if (!sq) throw new Error("Schedule module not available");
|
|
398
|
+
|
|
399
|
+
const schedule = sq.getScheduleByPrefix(db, params.id);
|
|
400
|
+
const fields: any = {};
|
|
401
|
+
if (params.name !== undefined) fields.name = params.name;
|
|
402
|
+
if (params.cron !== undefined) {
|
|
403
|
+
const { validateCron } = await import("./cron.ts");
|
|
404
|
+
const validation = validateCron(params.cron);
|
|
405
|
+
if (!validation.valid) throw new Error(`Invalid cron expression: ${validation.error}`);
|
|
406
|
+
fields.cron = params.cron;
|
|
407
|
+
}
|
|
408
|
+
if (params.prompt !== undefined) fields.prompt = params.prompt;
|
|
409
|
+
if (params.enabled !== undefined) fields.enabled = params.enabled ? 1 : 0;
|
|
410
|
+
|
|
411
|
+
return sq.updateSchedule(db, schedule.id, fields);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function handleDeleteSchedule(db: Database, params: any) {
|
|
415
|
+
const sq = await getScheduleQueries();
|
|
416
|
+
if (!sq) throw new Error("Schedule module not available");
|
|
417
|
+
|
|
418
|
+
const schedule = sq.getScheduleByPrefix(db, params.id);
|
|
419
|
+
sq.deleteSchedule(db, schedule.id);
|
|
420
|
+
return { deleted: true, id: schedule.id };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function handleListRuns(db: Database, params: any) {
|
|
424
|
+
const rq = await getRunQueries();
|
|
425
|
+
if (!rq) return [];
|
|
426
|
+
|
|
427
|
+
return rq.listRuns(db, {
|
|
428
|
+
schedule_id: params?.schedule_id,
|
|
429
|
+
status: params?.status,
|
|
430
|
+
limit: params?.limit,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function startMcpServer(): Promise<void> {
|
|
435
|
+
const db = ensureDb();
|
|
436
|
+
const config = loadConfig();
|
|
437
|
+
const pkg = await import("../package.json");
|
|
438
|
+
|
|
439
|
+
const server = new Server(
|
|
440
|
+
{ name: "prodboard", version: pkg.version },
|
|
441
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
445
|
+
tools: TOOLS,
|
|
446
|
+
}));
|
|
447
|
+
|
|
448
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
449
|
+
const { name, arguments: params } = request.params;
|
|
450
|
+
try {
|
|
451
|
+
let result: any;
|
|
452
|
+
switch (name) {
|
|
453
|
+
case "list_issues":
|
|
454
|
+
result = handleListIssues(db, params ?? {});
|
|
455
|
+
break;
|
|
456
|
+
case "get_issue":
|
|
457
|
+
result = handleGetIssue(db, params ?? {});
|
|
458
|
+
break;
|
|
459
|
+
case "create_issue":
|
|
460
|
+
result = handleCreateIssue(db, config, params ?? {});
|
|
461
|
+
break;
|
|
462
|
+
case "update_issue":
|
|
463
|
+
result = handleUpdateIssue(db, config, params ?? {});
|
|
464
|
+
break;
|
|
465
|
+
case "delete_issue":
|
|
466
|
+
result = handleDeleteIssue(db, params ?? {});
|
|
467
|
+
break;
|
|
468
|
+
case "add_comment":
|
|
469
|
+
result = handleAddComment(db, params ?? {});
|
|
470
|
+
break;
|
|
471
|
+
case "board_summary":
|
|
472
|
+
result = handleBoardSummary(db);
|
|
473
|
+
break;
|
|
474
|
+
case "pick_next_issue":
|
|
475
|
+
result = handlePickNextIssue(db, config, params ?? {});
|
|
476
|
+
break;
|
|
477
|
+
case "complete_issue":
|
|
478
|
+
result = handleCompleteIssue(db, config, params ?? {});
|
|
479
|
+
break;
|
|
480
|
+
case "list_schedules":
|
|
481
|
+
result = await handleListSchedules(db, params ?? {});
|
|
482
|
+
break;
|
|
483
|
+
case "create_schedule":
|
|
484
|
+
result = await handleCreateSchedule(db, params ?? {});
|
|
485
|
+
break;
|
|
486
|
+
case "update_schedule":
|
|
487
|
+
result = await handleUpdateSchedule(db, params ?? {});
|
|
488
|
+
break;
|
|
489
|
+
case "delete_schedule":
|
|
490
|
+
result = await handleDeleteSchedule(db, params ?? {});
|
|
491
|
+
break;
|
|
492
|
+
case "list_runs":
|
|
493
|
+
result = await handleListRuns(db, params ?? {});
|
|
494
|
+
break;
|
|
495
|
+
default:
|
|
496
|
+
return {
|
|
497
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
498
|
+
isError: true,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
503
|
+
};
|
|
504
|
+
} catch (err: any) {
|
|
505
|
+
return {
|
|
506
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
507
|
+
isError: true,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
513
|
+
resources: RESOURCES,
|
|
514
|
+
}));
|
|
515
|
+
|
|
516
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
517
|
+
const { uri } = request.params;
|
|
518
|
+
if (uri === "prodboard://issues") {
|
|
519
|
+
const summary = handleBoardSummary(db);
|
|
520
|
+
return {
|
|
521
|
+
contents: [{
|
|
522
|
+
uri,
|
|
523
|
+
mimeType: "application/json",
|
|
524
|
+
text: JSON.stringify(summary, null, 2),
|
|
525
|
+
}],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
if (uri === "prodboard://schedules") {
|
|
529
|
+
const schedules = await handleListSchedules(db, {});
|
|
530
|
+
return {
|
|
531
|
+
contents: [{
|
|
532
|
+
uri,
|
|
533
|
+
mimeType: "application/json",
|
|
534
|
+
text: JSON.stringify(schedules, null, 2),
|
|
535
|
+
}],
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const transport = new StdioServerTransport();
|
|
542
|
+
await server.connect(transport);
|
|
543
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type { Comment } from "../types.ts";
|
|
3
|
+
import { generateId } from "../ids.ts";
|
|
4
|
+
|
|
5
|
+
export function createComment(
|
|
6
|
+
db: Database,
|
|
7
|
+
opts: { issue_id: string; body: string; author?: string }
|
|
8
|
+
): Comment {
|
|
9
|
+
const id = generateId();
|
|
10
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
11
|
+
const author = opts.author ?? "user";
|
|
12
|
+
|
|
13
|
+
db.query(
|
|
14
|
+
"INSERT INTO comments (id, issue_id, body, author, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
15
|
+
).run(id, opts.issue_id, opts.body, author, now);
|
|
16
|
+
|
|
17
|
+
return { id, issue_id: opts.issue_id, body: opts.body, author, created_at: now };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function listComments(db: Database, issueId: string): Comment[] {
|
|
21
|
+
return db
|
|
22
|
+
.query("SELECT * FROM comments WHERE issue_id = ? ORDER BY created_at ASC, rowid ASC")
|
|
23
|
+
.all(issueId) as Comment[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getCommentCount(db: Database, issueId: string): number {
|
|
27
|
+
const result = db
|
|
28
|
+
.query("SELECT COUNT(*) as count FROM comments WHERE issue_id = ?")
|
|
29
|
+
.get(issueId) as { count: number };
|
|
30
|
+
return result.count;
|
|
31
|
+
}
|