backlog-mcp 0.6.7 → 0.8.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/README.md +19 -13
- package/dist/backlog.d.ts +26 -0
- package/dist/backlog.js +167 -0
- package/dist/backlog.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +85 -81
- package/dist/server.js.map +1 -1
- package/dist/viewer/components/task-detail.js +27 -11
- package/dist/viewer/components/task-filter-bar.js +11 -14
- package/dist/viewer/icons/index.js +1 -0
- package/dist/viewer/utils/api.js +1 -10
- package/dist/viewer.d.ts +1 -1
- package/dist/viewer.js +45 -116
- package/dist/viewer.js.map +1 -1
- package/package.json +2 -1
- package/viewer/index.html +1 -1
- package/viewer/md-block.js +25 -0
- package/viewer/styles.css +100 -9
- package/dist/storage.d.ts +0 -58
- package/dist/storage.js +0 -279
- package/dist/storage.js.map +0 -1
- package/dist/summary.d.ts +0 -2
- package/dist/summary.js +0 -14
- package/dist/summary.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# backlog-mcp
|
|
2
2
|
|
|
3
|
-
Minimal task backlog MCP server
|
|
3
|
+
Minimal task backlog MCP server. Works with any LLM agent or CLI editor that supports MCP integration (Claude, Kiro, Cursor, Codex, etc).
|
|
4
4
|
|
|
5
5
|
> **Quick start**: Tell your LLM: `Add backlog-mcp to .mcp.json and use it to track tasks`
|
|
6
6
|
|
|
@@ -47,24 +47,30 @@ This came from Slack thread: https://...
|
|
|
47
47
|
|
|
48
48
|
**Status values:** `open`, `in_progress`, `blocked`, `done`, `cancelled`
|
|
49
49
|
|
|
50
|
-
## MCP
|
|
51
|
-
|
|
52
|
-
Single unified tool with action parameter:
|
|
50
|
+
## MCP Tools
|
|
53
51
|
|
|
54
52
|
```
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
backlog_list # List active tasks (open, in_progress, blocked)
|
|
54
|
+
backlog_list status=["done"] # Show completed tasks
|
|
55
|
+
backlog_list counts=true # Get counts by status
|
|
56
|
+
backlog_list limit=10 # Limit results
|
|
57
|
+
|
|
58
|
+
backlog_get id="TASK-0001" # Get single task
|
|
59
|
+
backlog_get id=["TASK-0001","TASK-0002"] # Batch get multiple tasks
|
|
60
|
+
|
|
61
|
+
backlog_create title="Fix bug" # Create task
|
|
62
|
+
backlog_create title="Fix bug" description="Details..."
|
|
63
|
+
|
|
64
|
+
backlog_update id="TASK-0001" status="done" # Mark done
|
|
65
|
+
backlog_update id="TASK-0001" status="blocked" blocked_reason="Waiting on API"
|
|
66
|
+
backlog_update id="TASK-0001" evidence=["Fixed in CR-12345"] # Add completion proof
|
|
67
|
+
|
|
68
|
+
backlog_delete id="TASK-0001" # Permanently delete
|
|
63
69
|
```
|
|
64
70
|
|
|
65
71
|
## Installation
|
|
66
72
|
|
|
67
|
-
Add to your MCP config (`.mcp.json` or
|
|
73
|
+
Add to your MCP config (`.mcp.json` or your MCP client config):
|
|
68
74
|
|
|
69
75
|
```json
|
|
70
76
|
{
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Task, Status } from './schema.js';
|
|
2
|
+
declare class BacklogStorage {
|
|
3
|
+
private dataDir;
|
|
4
|
+
private static instance;
|
|
5
|
+
static getInstance(): BacklogStorage;
|
|
6
|
+
init(dataDir: string): void;
|
|
7
|
+
private get tasksPath();
|
|
8
|
+
private get archivePath();
|
|
9
|
+
private ensureDir;
|
|
10
|
+
private taskFilePath;
|
|
11
|
+
private taskToMarkdown;
|
|
12
|
+
private markdownToTask;
|
|
13
|
+
getFilePath(id: string): string | null;
|
|
14
|
+
get(id: string): Task | undefined;
|
|
15
|
+
getMarkdown(id: string): string | null;
|
|
16
|
+
list(filter?: {
|
|
17
|
+
status?: Status[];
|
|
18
|
+
limit?: number;
|
|
19
|
+
}): Task[];
|
|
20
|
+
add(task: Task): void;
|
|
21
|
+
save(task: Task): void;
|
|
22
|
+
delete(id: string): boolean;
|
|
23
|
+
counts(): Record<Status, number>;
|
|
24
|
+
}
|
|
25
|
+
export declare const storage: BacklogStorage;
|
|
26
|
+
export {};
|
package/dist/backlog.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
const TASKS_DIR = 'tasks';
|
|
5
|
+
const ARCHIVE_DIR = 'archive';
|
|
6
|
+
const TERMINAL_STATUSES = ['done', 'cancelled'];
|
|
7
|
+
class BacklogStorage {
|
|
8
|
+
dataDir = 'data';
|
|
9
|
+
static instance;
|
|
10
|
+
static getInstance() {
|
|
11
|
+
if (!BacklogStorage.instance) {
|
|
12
|
+
BacklogStorage.instance = new BacklogStorage();
|
|
13
|
+
}
|
|
14
|
+
return BacklogStorage.instance;
|
|
15
|
+
}
|
|
16
|
+
init(dataDir) {
|
|
17
|
+
this.dataDir = dataDir;
|
|
18
|
+
}
|
|
19
|
+
get tasksPath() {
|
|
20
|
+
return join(this.dataDir, TASKS_DIR);
|
|
21
|
+
}
|
|
22
|
+
get archivePath() {
|
|
23
|
+
return join(this.dataDir, ARCHIVE_DIR);
|
|
24
|
+
}
|
|
25
|
+
ensureDir(dir) {
|
|
26
|
+
if (!existsSync(dir)) {
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
taskFilePath(id, archived = false) {
|
|
31
|
+
return join(archived ? this.archivePath : this.tasksPath, `${id}.md`);
|
|
32
|
+
}
|
|
33
|
+
taskToMarkdown(task) {
|
|
34
|
+
const { description, ...frontmatter } = task;
|
|
35
|
+
return matter.stringify(description || '', frontmatter);
|
|
36
|
+
}
|
|
37
|
+
markdownToTask(content) {
|
|
38
|
+
const { data, content: description } = matter(content);
|
|
39
|
+
return { ...data, description: description.trim() };
|
|
40
|
+
}
|
|
41
|
+
getFilePath(id) {
|
|
42
|
+
const activePath = this.taskFilePath(id, false);
|
|
43
|
+
if (existsSync(activePath))
|
|
44
|
+
return activePath;
|
|
45
|
+
const archivePath = this.taskFilePath(id, true);
|
|
46
|
+
if (existsSync(archivePath))
|
|
47
|
+
return archivePath;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
get(id) {
|
|
51
|
+
const activePath = this.taskFilePath(id, false);
|
|
52
|
+
if (existsSync(activePath)) {
|
|
53
|
+
return this.markdownToTask(readFileSync(activePath, 'utf-8'));
|
|
54
|
+
}
|
|
55
|
+
const archivePath = this.taskFilePath(id, true);
|
|
56
|
+
if (existsSync(archivePath)) {
|
|
57
|
+
return this.markdownToTask(readFileSync(archivePath, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
getMarkdown(id) {
|
|
62
|
+
const activePath = this.taskFilePath(id, false);
|
|
63
|
+
if (existsSync(activePath)) {
|
|
64
|
+
return readFileSync(activePath, 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
const archivePath = this.taskFilePath(id, true);
|
|
67
|
+
if (existsSync(archivePath)) {
|
|
68
|
+
return readFileSync(archivePath, 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
list(filter) {
|
|
73
|
+
const tasks = [];
|
|
74
|
+
const statusFilter = filter?.status;
|
|
75
|
+
const limit = filter?.limit ?? 20;
|
|
76
|
+
// Load active tasks
|
|
77
|
+
if (existsSync(this.tasksPath)) {
|
|
78
|
+
for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {
|
|
79
|
+
const task = this.markdownToTask(readFileSync(join(this.tasksPath, file), 'utf-8'));
|
|
80
|
+
if (!statusFilter || statusFilter.includes(task.status)) {
|
|
81
|
+
tasks.push(task);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Load archived tasks if needed
|
|
86
|
+
const needsArchived = !statusFilter || statusFilter.some(s => TERMINAL_STATUSES.includes(s));
|
|
87
|
+
if (needsArchived && existsSync(this.archivePath)) {
|
|
88
|
+
for (const file of readdirSync(this.archivePath).filter(f => f.endsWith('.md'))) {
|
|
89
|
+
const task = this.markdownToTask(readFileSync(join(this.archivePath, file), 'utf-8'));
|
|
90
|
+
if (!statusFilter || statusFilter.includes(task.status)) {
|
|
91
|
+
tasks.push(task);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Sort: active first, then by date desc
|
|
96
|
+
const isTerminal = (s) => TERMINAL_STATUSES.includes(s);
|
|
97
|
+
return tasks
|
|
98
|
+
.sort((a, b) => {
|
|
99
|
+
const aTerminal = isTerminal(a.status) ? 1 : 0;
|
|
100
|
+
const bTerminal = isTerminal(b.status) ? 1 : 0;
|
|
101
|
+
if (aTerminal !== bTerminal)
|
|
102
|
+
return aTerminal - bTerminal;
|
|
103
|
+
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
|
104
|
+
})
|
|
105
|
+
.slice(0, limit);
|
|
106
|
+
}
|
|
107
|
+
add(task) {
|
|
108
|
+
this.ensureDir(this.tasksPath);
|
|
109
|
+
const filePath = this.taskFilePath(task.id, false);
|
|
110
|
+
writeFileSync(filePath, this.taskToMarkdown(task));
|
|
111
|
+
}
|
|
112
|
+
save(task) {
|
|
113
|
+
const isTerminal = TERMINAL_STATUSES.includes(task.status);
|
|
114
|
+
const activePath = this.taskFilePath(task.id, false);
|
|
115
|
+
const archivePath = this.taskFilePath(task.id, true);
|
|
116
|
+
// Move to archive if terminal status
|
|
117
|
+
if (isTerminal) {
|
|
118
|
+
this.ensureDir(this.archivePath);
|
|
119
|
+
writeFileSync(archivePath, this.taskToMarkdown(task));
|
|
120
|
+
if (existsSync(activePath))
|
|
121
|
+
unlinkSync(activePath);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.ensureDir(this.tasksPath);
|
|
125
|
+
writeFileSync(activePath, this.taskToMarkdown(task));
|
|
126
|
+
if (existsSync(archivePath))
|
|
127
|
+
unlinkSync(archivePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
delete(id) {
|
|
131
|
+
const activePath = this.taskFilePath(id, false);
|
|
132
|
+
if (existsSync(activePath)) {
|
|
133
|
+
unlinkSync(activePath);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const archivePath = this.taskFilePath(id, true);
|
|
137
|
+
if (existsSync(archivePath)) {
|
|
138
|
+
unlinkSync(archivePath);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
counts() {
|
|
144
|
+
const counts = {
|
|
145
|
+
open: 0,
|
|
146
|
+
in_progress: 0,
|
|
147
|
+
blocked: 0,
|
|
148
|
+
done: 0,
|
|
149
|
+
cancelled: 0,
|
|
150
|
+
};
|
|
151
|
+
if (existsSync(this.tasksPath)) {
|
|
152
|
+
for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {
|
|
153
|
+
const task = this.markdownToTask(readFileSync(join(this.tasksPath, file), 'utf-8'));
|
|
154
|
+
counts[task.status]++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (existsSync(this.archivePath)) {
|
|
158
|
+
for (const file of readdirSync(this.archivePath).filter(f => f.endsWith('.md'))) {
|
|
159
|
+
const task = this.markdownToTask(readFileSync(join(this.archivePath, file), 'utf-8'));
|
|
160
|
+
counts[task.status]++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return counts;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export const storage = BacklogStorage.getInstance();
|
|
167
|
+
//# sourceMappingURL=backlog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backlog.js","sourceRoot":"","sources":["../src/backlog.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { isValidTaskId, parseTaskId, formatTaskId, nextTaskId, STATUSES, type Status, type Task, type CreateTaskInput, createTask, } from './schema.js';
|
|
2
|
-
export {
|
|
2
|
+
export { storage } from './backlog.js';
|
|
3
3
|
export { startViewer } from './viewer.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Schema
|
|
2
2
|
export { isValidTaskId, parseTaskId, formatTaskId, nextTaskId, STATUSES, createTask, } from './schema.js';
|
|
3
3
|
// Storage
|
|
4
|
-
export {
|
|
4
|
+
export { storage } from './backlog.js';
|
|
5
5
|
// Viewer
|
|
6
6
|
export { startViewer } from './viewer.js';
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,SAAS;AACT,OAAO,EACL,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,EACV,QAAQ,EAIR,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,UAAU;AACV,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,SAAS;AACT,OAAO,EACL,aAAa,EACb,WAAW,EACX,YAAY,EACZ,UAAU,EACV,QAAQ,EAIR,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,UAAU;AACV,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,SAAS;AACT,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import 'dotenv/config';
|
package/dist/server.js
CHANGED
|
@@ -1,106 +1,110 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
2
6
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
8
|
import { z } from 'zod';
|
|
5
9
|
import { createTask, STATUSES } from './schema.js';
|
|
6
|
-
import {
|
|
7
|
-
import { loadBacklog, getTask, getTaskMarkdown, listTasks, addTask, saveTask, getTaskCounts } from './storage.js';
|
|
10
|
+
import { storage } from './backlog.js';
|
|
8
11
|
import { startViewer } from './viewer.js';
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
// Read version from package.json
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
15
|
+
// Init storage
|
|
16
|
+
const dataDir = process.env.BACKLOG_DATA_DIR ?? 'data';
|
|
17
|
+
storage.init(dataDir);
|
|
12
18
|
const server = new McpServer({
|
|
13
19
|
name: 'backlog-mcp',
|
|
14
|
-
version:
|
|
20
|
+
version: pkg.version,
|
|
15
21
|
});
|
|
16
|
-
const storageOptions = {
|
|
17
|
-
dataDir: process.env.BACKLOG_DATA_DIR ?? 'data',
|
|
18
|
-
};
|
|
19
22
|
// ============================================================================
|
|
20
23
|
// Tools
|
|
21
24
|
// ============================================================================
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
server.registerTool('backlog_list', {
|
|
26
|
+
description: 'List tasks from backlog. Shows open/in_progress/blocked by default. Use status=["done"] to see completed tasks.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
status: z.array(z.enum(STATUSES)).optional().describe('Filter: open, in_progress, blocked, done, cancelled. Default: open, in_progress, blocked'),
|
|
29
|
+
counts: z.boolean().optional().describe('Return counts per status instead of task list'),
|
|
30
|
+
limit: z.number().optional().describe('Max tasks to return. Default: 20'),
|
|
31
|
+
},
|
|
32
|
+
}, async ({ status, counts, limit }) => {
|
|
33
|
+
const tasks = storage.list({ status, limit });
|
|
34
|
+
if (counts) {
|
|
35
|
+
return { content: [{ type: 'text', text: JSON.stringify(storage.counts(), null, 2) }] };
|
|
36
|
+
}
|
|
37
|
+
const list = tasks.map((t) => ({ id: t.id, title: t.title, status: t.status }));
|
|
38
|
+
return { content: [{ type: 'text', text: JSON.stringify(list, null, 2) }] };
|
|
39
|
+
});
|
|
40
|
+
server.registerTool('backlog_get', {
|
|
41
|
+
description: 'Get full task details by ID. Works for any task regardless of status.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
id: z.union([z.string(), z.array(z.string())]).describe('Task ID like TASK-0001, or array for batch fetch'),
|
|
44
|
+
},
|
|
45
|
+
}, async ({ id }) => {
|
|
46
|
+
const taskIds = Array.isArray(id) ? id : [id];
|
|
47
|
+
if (taskIds.length === 0) {
|
|
48
|
+
return { content: [{ type: 'text', text: 'Required: id' }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
const results = taskIds.map((tid) => storage.getMarkdown(tid) || `Not found: ${tid}`);
|
|
51
|
+
return { content: [{ type: 'text', text: results.join('\n\n---\n\n') }] };
|
|
52
|
+
});
|
|
53
|
+
server.registerTool('backlog_create', {
|
|
54
|
+
description: 'Create a new task in the backlog.',
|
|
25
55
|
inputSchema: {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
status: z.array(z.enum(STATUSES)).optional().describe('Filter by status (list). Include "done" or "cancelled" to see archived tasks'),
|
|
29
|
-
summary: z.boolean().optional().describe('Return counts instead of list (list)'),
|
|
30
|
-
archived_limit: z.number().optional().describe('Max archived tasks when status includes done/cancelled (list). Default: 10'),
|
|
31
|
-
// get/update options
|
|
32
|
-
id: z.string().optional().describe('Task ID (get, update)'),
|
|
33
|
-
// create/update options
|
|
34
|
-
title: z.string().optional().describe('Task title (create, update)'),
|
|
35
|
-
description: z.string().optional().describe('Task description (create, update)'),
|
|
36
|
-
// update-only options
|
|
37
|
-
set_status: z.enum(STATUSES).optional().describe('New status (update)'),
|
|
38
|
-
blocked_reason: z.string().optional().describe('Reason for blocked status (update)'),
|
|
39
|
-
evidence: z.array(z.string()).optional().describe('Evidence of completion (update)'),
|
|
56
|
+
title: z.string().describe('Task title'),
|
|
57
|
+
description: z.string().optional().describe('Task description in markdown'),
|
|
40
58
|
},
|
|
41
|
-
}, async ({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
return { content: [{ type: 'text', text: markdown }] };
|
|
62
|
-
}
|
|
63
|
-
case 'create': {
|
|
64
|
-
if (!title) {
|
|
65
|
-
return { content: [{ type: 'text', text: 'Missing required: title' }], isError: true };
|
|
66
|
-
}
|
|
67
|
-
const backlog = loadBacklog(storageOptions);
|
|
68
|
-
const task = createTask({ title, description }, backlog.tasks);
|
|
69
|
-
addTask(task, storageOptions);
|
|
70
|
-
return { content: [{ type: 'text', text: `Created ${task.id}` }] };
|
|
71
|
-
}
|
|
72
|
-
case 'update': {
|
|
73
|
-
if (!id) {
|
|
74
|
-
return { content: [{ type: 'text', text: 'Missing required: id' }], isError: true };
|
|
75
|
-
}
|
|
76
|
-
const task = getTask(id, storageOptions);
|
|
77
|
-
if (!task) {
|
|
78
|
-
return { content: [{ type: 'text', text: `Not found: ${id}` }], isError: true };
|
|
79
|
-
}
|
|
80
|
-
const updates = { title, description, status: set_status, blocked_reason, evidence };
|
|
81
|
-
const updated = {
|
|
82
|
-
...task,
|
|
83
|
-
...Object.fromEntries(Object.entries(updates).filter(([_, v]) => v !== undefined)),
|
|
84
|
-
updated_at: new Date().toISOString(),
|
|
85
|
-
};
|
|
86
|
-
saveTask(updated, storageOptions);
|
|
87
|
-
return { content: [{ type: 'text', text: `Updated ${id}` }] };
|
|
88
|
-
}
|
|
59
|
+
}, async ({ title, description }) => {
|
|
60
|
+
const task = createTask({ title, description }, storage.list());
|
|
61
|
+
storage.add(task);
|
|
62
|
+
return { content: [{ type: 'text', text: `Created ${task.id}` }] };
|
|
63
|
+
});
|
|
64
|
+
server.registerTool('backlog_update', {
|
|
65
|
+
description: 'Update an existing task.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
id: z.string().describe('Task ID to update'),
|
|
68
|
+
title: z.string().optional().describe('New title'),
|
|
69
|
+
description: z.string().optional().describe('New description'),
|
|
70
|
+
status: z.enum(STATUSES).optional().describe('New status'),
|
|
71
|
+
blocked_reason: z.string().optional().describe('Reason if status is blocked'),
|
|
72
|
+
evidence: z.array(z.string()).optional().describe('Proof of completion when marking done - links to PRs, docs, or notes'),
|
|
73
|
+
},
|
|
74
|
+
}, async ({ id, title, description, status, blocked_reason, evidence }) => {
|
|
75
|
+
const task = storage.get(id);
|
|
76
|
+
if (!task) {
|
|
77
|
+
return { content: [{ type: 'text', text: `Not found: ${id}` }], isError: true };
|
|
89
78
|
}
|
|
79
|
+
const updates = { title, description, status, blocked_reason, evidence };
|
|
80
|
+
const updated = {
|
|
81
|
+
...task,
|
|
82
|
+
...Object.fromEntries(Object.entries(updates).filter(([_, v]) => v !== undefined)),
|
|
83
|
+
updated_at: new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
storage.save(updated);
|
|
86
|
+
return { content: [{ type: 'text', text: `Updated ${id}` }] };
|
|
87
|
+
});
|
|
88
|
+
server.registerTool('backlog_delete', {
|
|
89
|
+
description: 'Permanently delete a task from the backlog.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
id: z.string().describe('Task ID to delete'),
|
|
92
|
+
},
|
|
93
|
+
}, async ({ id }) => {
|
|
94
|
+
const deleted = storage.delete(id);
|
|
95
|
+
if (!deleted) {
|
|
96
|
+
return { content: [{ type: 'text', text: `Not found: ${id}` }], isError: true };
|
|
97
|
+
}
|
|
98
|
+
return { content: [{ type: 'text', text: `Deleted ${id}` }] };
|
|
90
99
|
});
|
|
91
100
|
// ============================================================================
|
|
92
101
|
// Main
|
|
93
102
|
// ============================================================================
|
|
94
103
|
async function main() {
|
|
95
|
-
// Start HTTP viewer in background
|
|
96
|
-
const dataDir = storageOptions.dataDir ?? 'data';
|
|
97
104
|
const viewerPort = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');
|
|
98
|
-
startViewer(
|
|
105
|
+
startViewer(viewerPort);
|
|
99
106
|
const transport = new StdioServerTransport();
|
|
100
107
|
await server.connect(transport);
|
|
101
108
|
}
|
|
102
|
-
main().catch(
|
|
103
|
-
console.error('Fatal error:', error);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
});
|
|
109
|
+
main().catch(console.error);
|
|
106
110
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAa,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { fetchTask } from '../utils/api.js';
|
|
2
|
+
import { copyIcon } from '../icons/index.js';
|
|
3
|
+
function linkify(text) {
|
|
4
|
+
const urlRegex = /(https?:\/\/[^\s<>"']+)/g;
|
|
5
|
+
return text.replace(urlRegex, '<a href="$1" target="_blank" rel="noopener">$1</a>');
|
|
6
|
+
}
|
|
2
7
|
export class TaskDetail extends HTMLElement {
|
|
3
8
|
connectedCallback() {
|
|
4
9
|
this.showEmpty();
|
|
@@ -14,18 +19,24 @@ export class TaskDetail extends HTMLElement {
|
|
|
14
19
|
async loadTask(taskId) {
|
|
15
20
|
try {
|
|
16
21
|
const task = await fetchTask(taskId);
|
|
22
|
+
const headerHtml = `
|
|
23
|
+
<div class="task-header-left">
|
|
24
|
+
<button class="btn-outline task-id-btn" onclick="navigator.clipboard.writeText('${task.id}')" title="Copy ID">${task.id} ${copyIcon}</button>
|
|
25
|
+
<span class="status-badge status-${task.status || 'open'}">${(task.status || 'open').replace('_', ' ')}</span>
|
|
26
|
+
${task.filePath ? `
|
|
27
|
+
<div class="task-meta-path">
|
|
28
|
+
<a href="#" class="open-link" onclick="fetch('http://localhost:3030/open/${task.id}');return false;" title="Open in editor">${task.filePath}</a>
|
|
29
|
+
</div>
|
|
30
|
+
` : ''}
|
|
31
|
+
</div>
|
|
32
|
+
<button class="copy-btn copy-raw btn-outline" title="Copy markdown">Copy Markdown ${copyIcon}</button>
|
|
33
|
+
`;
|
|
34
|
+
const paneHeader = document.getElementById('task-pane-header');
|
|
35
|
+
if (paneHeader) {
|
|
36
|
+
paneHeader.innerHTML = headerHtml;
|
|
37
|
+
}
|
|
17
38
|
const metaHtml = `
|
|
18
39
|
<div class="task-meta-card">
|
|
19
|
-
<div class="task-meta-header">
|
|
20
|
-
<span class="task-meta-id">${task.id || ''}</span>
|
|
21
|
-
<span class="status-badge status-${task.status || 'open'}">${(task.status || 'open').replace('_', ' ')}</span>
|
|
22
|
-
${task.filePath ? `
|
|
23
|
-
<div class="task-meta-path">
|
|
24
|
-
<a href="#" class="open-link" onclick="fetch('http://localhost:3030/open/${task.id}');return false;" title="Open in editor">${task.filePath}</a>
|
|
25
|
-
<button class="copy-btn" onclick="navigator.clipboard.writeText('${task.filePath}')">📋</button>
|
|
26
|
-
</div>
|
|
27
|
-
` : ''}
|
|
28
|
-
</div>
|
|
29
40
|
<h1 class="task-meta-title">${task.title || ''}</h1>
|
|
30
41
|
<div class="task-meta-dates">
|
|
31
42
|
<span>Created: ${task.created_at ? new Date(task.created_at).toLocaleDateString() : ''}</span>
|
|
@@ -34,7 +45,7 @@ export class TaskDetail extends HTMLElement {
|
|
|
34
45
|
${task.evidence?.length ? `
|
|
35
46
|
<div class="task-meta-evidence">
|
|
36
47
|
<div class="task-meta-evidence-label">Evidence:</div>
|
|
37
|
-
<ul>${task.evidence.map((e) => `<li>${e}</li>`).join('')}</ul>
|
|
48
|
+
<ul>${task.evidence.map((e) => `<li>${linkify(e)}</li>`).join('')}</ul>
|
|
38
49
|
</div>
|
|
39
50
|
` : ''}
|
|
40
51
|
</div>
|
|
@@ -47,6 +58,11 @@ export class TaskDetail extends HTMLElement {
|
|
|
47
58
|
article.appendChild(mdBlock);
|
|
48
59
|
this.innerHTML = '';
|
|
49
60
|
this.appendChild(article);
|
|
61
|
+
// Bind copy raw button (in pane header)
|
|
62
|
+
const copyRawBtn = paneHeader?.querySelector('.copy-raw');
|
|
63
|
+
if (copyRawBtn && task.raw) {
|
|
64
|
+
copyRawBtn.addEventListener('click', () => navigator.clipboard.writeText(task.raw));
|
|
65
|
+
}
|
|
50
66
|
}
|
|
51
67
|
catch (error) {
|
|
52
68
|
this.innerHTML = `<div class="error">Failed to load task: ${error.message}</div>`;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
const FILTERS = [
|
|
2
|
+
{ key: 'active', label: 'Active' },
|
|
3
|
+
{ key: 'completed', label: 'Completed' },
|
|
4
|
+
{ key: 'all', label: 'All' },
|
|
5
|
+
];
|
|
1
6
|
export class TaskFilterBar extends HTMLElement {
|
|
2
7
|
currentFilter = 'active';
|
|
3
8
|
connectedCallback() {
|
|
@@ -5,30 +10,22 @@ export class TaskFilterBar extends HTMLElement {
|
|
|
5
10
|
this.attachListeners();
|
|
6
11
|
}
|
|
7
12
|
render() {
|
|
8
|
-
this.
|
|
9
|
-
|
|
10
|
-
<button class="filter-btn ${this.currentFilter === 'active' ? 'active' : ''}" data-filter="active">Active</button>
|
|
11
|
-
<button class="filter-btn ${this.currentFilter === 'done' ? 'active' : ''}" data-filter="done">Completed</button>
|
|
12
|
-
<button class="filter-btn ${this.currentFilter === 'all' ? 'active' : ''}" data-filter="all">All</button>
|
|
13
|
-
</div>
|
|
14
|
-
`;
|
|
13
|
+
const buttons = FILTERS.map(f => `<button class="filter-btn ${this.currentFilter === f.key ? 'active' : ''}" data-filter="${f.key}">${f.label}</button>`).join('');
|
|
14
|
+
this.innerHTML = `<div class="filter-bar">${buttons}</div>`;
|
|
15
15
|
}
|
|
16
16
|
attachListeners() {
|
|
17
17
|
this.querySelectorAll('.filter-btn').forEach(btn => {
|
|
18
18
|
btn.addEventListener('click', (e) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return;
|
|
23
|
-
this.setFilter(filter);
|
|
19
|
+
const filter = e.target.dataset.filter;
|
|
20
|
+
if (filter)
|
|
21
|
+
this.setFilter(filter);
|
|
24
22
|
});
|
|
25
23
|
});
|
|
26
24
|
}
|
|
27
25
|
setFilter(filter) {
|
|
28
26
|
this.currentFilter = filter;
|
|
29
27
|
this.querySelectorAll('.filter-btn').forEach(btn => {
|
|
30
|
-
|
|
31
|
-
btn.classList.toggle('active', btnElement.dataset.filter === filter);
|
|
28
|
+
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
32
29
|
});
|
|
33
30
|
document.dispatchEvent(new CustomEvent('filter-change', { detail: { filter } }));
|
|
34
31
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const copyIcon = `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"/></svg>`;
|