backlog-mcp 0.2.1 → 0.6.5
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 +44 -15
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server.js +14 -7
- package/dist/server.js.map +1 -1
- package/dist/storage.d.ts +11 -4
- package/dist/storage.js +175 -90
- package/dist/storage.js.map +1 -1
- package/dist/viewer/components/task-detail.js +56 -0
- package/dist/viewer/components/task-filter-bar.js +36 -0
- package/dist/viewer/components/task-item.js +45 -0
- package/dist/viewer/components/task-list.js +60 -0
- package/dist/viewer/main.js +31 -0
- package/dist/viewer/utils/api.js +19 -0
- package/dist/viewer.d.ts +1 -0
- package/dist/viewer.js +188 -0
- package/dist/viewer.js.map +1 -0
- package/package.json +10 -4
- package/viewer/github-markdown.css +1105 -0
- package/viewer/index.html +41 -0
- package/viewer/logo.svg +14 -0
- package/viewer/md-block.js +306 -0
- package/viewer/styles.css +345 -0
package/README.md
CHANGED
|
@@ -4,29 +4,59 @@ Minimal task backlog MCP server for Claude and AI agents.
|
|
|
4
4
|
|
|
5
5
|
> **Quick start**: Tell your LLM: `Add backlog-mcp to .mcp.json and use it to track tasks`
|
|
6
6
|
|
|
7
|
+
## Web Viewer
|
|
8
|
+
|
|
9
|
+
Start the server and open `http://localhost:3030` for a visual task browser.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run dev # Starts MCP server + web viewer with hot reload
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Features:
|
|
16
|
+
- Split pane layout with task list and detail view
|
|
17
|
+
- Filter by Active/Completed/All
|
|
18
|
+
- GitHub-style markdown rendering
|
|
19
|
+
- Click file path to open in editor
|
|
20
|
+
- URL state persistence
|
|
21
|
+
|
|
7
22
|
## Task Schema
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
Tasks are stored as individual markdown files with YAML frontmatter:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
---
|
|
28
|
+
id: TASK-0001
|
|
29
|
+
title: Fix bug in authentication
|
|
30
|
+
status: open
|
|
31
|
+
created_at: '2024-01-10T10:00:00Z'
|
|
32
|
+
updated_at: '2024-01-10T10:00:00Z'
|
|
33
|
+
blocked_reason: Waiting for API access
|
|
34
|
+
evidence:
|
|
35
|
+
- Fixed in CR-12345
|
|
36
|
+
- Validated in beta
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Description
|
|
40
|
+
|
|
41
|
+
The authentication flow has an issue where...
|
|
42
|
+
|
|
43
|
+
## Context
|
|
44
|
+
|
|
45
|
+
This came from Slack thread: https://...
|
|
20
46
|
```
|
|
21
47
|
|
|
48
|
+
**Status values:** `open`, `in_progress`, `blocked`, `done`, `cancelled`
|
|
49
|
+
|
|
22
50
|
## MCP Tool
|
|
23
51
|
|
|
24
52
|
Single unified tool with action parameter:
|
|
25
53
|
|
|
26
54
|
```
|
|
27
|
-
backlog action="list" # List all tasks
|
|
55
|
+
backlog action="list" # List all active tasks
|
|
28
56
|
backlog action="list" summary=true # Get counts by status
|
|
29
57
|
backlog action="list" status=["open"] # Filter by status
|
|
58
|
+
backlog action="list" status=["done"] # Show completed tasks (last 10)
|
|
59
|
+
backlog action="list" status=["done"] archived_limit=20 # Show last 20 completed
|
|
30
60
|
backlog action="get" id="TASK-0001" # Get task details
|
|
31
61
|
backlog action="create" title="Fix bug" # Create task
|
|
32
62
|
backlog action="update" id="TASK-0001" set_status="done" # Update task
|
|
@@ -58,10 +88,9 @@ npm start
|
|
|
58
88
|
|
|
59
89
|
## Storage
|
|
60
90
|
|
|
61
|
-
- Default: `data/
|
|
91
|
+
- Default: `data/tasks/` and `data/archive/` (local to project)
|
|
62
92
|
- Global: Set `BACKLOG_DATA_DIR=~/.backlog` for cross-project persistence
|
|
63
|
-
- Completed/cancelled tasks auto-archive to `archive
|
|
64
|
-
- Atomic writes via temp + rename
|
|
93
|
+
- Completed/cancelled tasks auto-archive to `archive/`
|
|
65
94
|
|
|
66
95
|
## License
|
|
67
96
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { isValidTaskId, parseTaskId, formatTaskId, nextTaskId, STATUSES, type Status, type Task, type CreateTaskInput, createTask, } from './schema.js';
|
|
2
|
-
export { type Backlog, type StorageOptions, loadBacklog, saveBacklog, getTask, listTasks, addTask, saveTask, deleteTask, taskExists, getTaskCounts, } from './storage.js';
|
|
2
|
+
export { type Backlog, type StorageOptions, loadBacklog, saveBacklog, getTask, listTasks, addTask, saveTask, deleteTask, taskExists, getTaskCounts, getTaskMarkdown, } from './storage.js';
|
|
3
|
+
export { startViewer } from './viewer.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Schema
|
|
2
2
|
export { isValidTaskId, parseTaskId, formatTaskId, nextTaskId, STATUSES, createTask, } from './schema.js';
|
|
3
3
|
// Storage
|
|
4
|
-
export { loadBacklog, saveBacklog, getTask, listTasks, addTask, saveTask, deleteTask, taskExists, getTaskCounts, } from './storage.js';
|
|
4
|
+
export { loadBacklog, saveBacklog, getTask, listTasks, addTask, saveTask, deleteTask, taskExists, getTaskCounts, getTaskMarkdown, } from './storage.js';
|
|
5
|
+
// Viewer
|
|
6
|
+
export { startViewer } from './viewer.js';
|
|
5
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,EAGL,WAAW,EACX,WAAW,EACX,OAAO,EACP,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAU,EACV,UAAU,EACV,aAAa,
|
|
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,EAGL,WAAW,EACX,WAAW,EACX,OAAO,EACP,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAU,EACV,UAAU,EACV,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,SAAS;AACT,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -4,7 +4,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { createTask, STATUSES } from './schema.js';
|
|
6
6
|
import { countTasks } from './summary.js';
|
|
7
|
-
import { loadBacklog, getTask, listTasks, addTask, saveTask, getTaskCounts } from './storage.js';
|
|
7
|
+
import { loadBacklog, getTask, getTaskMarkdown, listTasks, addTask, saveTask, getTaskCounts } from './storage.js';
|
|
8
|
+
import { startViewer } from './viewer.js';
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// Server
|
|
10
11
|
// ============================================================================
|
|
@@ -24,8 +25,9 @@ server.registerTool('backlog', {
|
|
|
24
25
|
inputSchema: {
|
|
25
26
|
action: z.enum(ACTIONS).describe('Action to perform'),
|
|
26
27
|
// list options
|
|
27
|
-
status: z.array(z.enum(STATUSES)).optional().describe('Filter by status (list)'),
|
|
28
|
+
status: z.array(z.enum(STATUSES)).optional().describe('Filter by status (list). Include "done" or "cancelled" to see archived tasks'),
|
|
28
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'),
|
|
29
31
|
// get/update options
|
|
30
32
|
id: z.string().optional().describe('Task ID (get, update)'),
|
|
31
33
|
// create/update options
|
|
@@ -36,10 +38,11 @@ server.registerTool('backlog', {
|
|
|
36
38
|
blocked_reason: z.string().optional().describe('Reason for blocked status (update)'),
|
|
37
39
|
evidence: z.array(z.string()).optional().describe('Evidence of completion (update)'),
|
|
38
40
|
},
|
|
39
|
-
}, async ({ action, status, summary, id, title, description, set_status, blocked_reason, evidence }) => {
|
|
41
|
+
}, async ({ action, status, summary, archived_limit, id, title, description, set_status, blocked_reason, evidence }) => {
|
|
40
42
|
switch (action) {
|
|
41
43
|
case 'list': {
|
|
42
|
-
const
|
|
44
|
+
const filter = status || archived_limit ? { status, archivedLimit: archived_limit } : undefined;
|
|
45
|
+
const tasks = listTasks(filter, storageOptions);
|
|
43
46
|
if (summary) {
|
|
44
47
|
const counts = status ? countTasks(tasks) : getTaskCounts(storageOptions);
|
|
45
48
|
return { content: [{ type: 'text', text: JSON.stringify(counts, null, 2) }] };
|
|
@@ -51,11 +54,11 @@ server.registerTool('backlog', {
|
|
|
51
54
|
if (!id) {
|
|
52
55
|
return { content: [{ type: 'text', text: 'Missing required: id' }], isError: true };
|
|
53
56
|
}
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
57
|
+
const markdown = getTaskMarkdown(id, storageOptions);
|
|
58
|
+
if (!markdown) {
|
|
56
59
|
return { content: [{ type: 'text', text: `Not found: ${id}` }], isError: true };
|
|
57
60
|
}
|
|
58
|
-
return { content: [{ type: 'text', text:
|
|
61
|
+
return { content: [{ type: 'text', text: markdown }] };
|
|
59
62
|
}
|
|
60
63
|
case 'create': {
|
|
61
64
|
if (!title) {
|
|
@@ -89,6 +92,10 @@ server.registerTool('backlog', {
|
|
|
89
92
|
// Main
|
|
90
93
|
// ============================================================================
|
|
91
94
|
async function main() {
|
|
95
|
+
// Start HTTP viewer in background
|
|
96
|
+
const dataDir = storageOptions.dataDir ?? 'data';
|
|
97
|
+
const viewerPort = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');
|
|
98
|
+
startViewer(dataDir, viewerPort);
|
|
92
99
|
const transport = new StdioServerTransport();
|
|
93
100
|
await server.connect(transport);
|
|
94
101
|
}
|
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,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAuB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/storage.d.ts
CHANGED
|
@@ -7,29 +7,32 @@ export interface StorageOptions {
|
|
|
7
7
|
dataDir?: string;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Load backlog from disk. Returns empty backlog if
|
|
10
|
+
* Load backlog from disk. Returns empty backlog if directory doesn't exist.
|
|
11
11
|
*/
|
|
12
12
|
export declare function loadBacklog(options?: StorageOptions): Backlog;
|
|
13
13
|
/**
|
|
14
|
-
* Save backlog to disk
|
|
14
|
+
* Save backlog to disk (no-op for file-based storage, kept for compatibility).
|
|
15
15
|
*/
|
|
16
16
|
export declare function saveBacklog(backlog: Backlog, options?: StorageOptions): void;
|
|
17
17
|
/**
|
|
18
|
-
* Load archive from disk. Returns empty backlog if
|
|
18
|
+
* Load archive from disk. Returns empty backlog if directory doesn't exist.
|
|
19
19
|
*/
|
|
20
20
|
export declare function loadArchive(options?: StorageOptions): Backlog;
|
|
21
21
|
/**
|
|
22
22
|
* Get a task by ID. Returns undefined if not found.
|
|
23
|
+
* Searches both active and archived tasks.
|
|
23
24
|
*/
|
|
24
25
|
export declare function getTask(id: string, options?: StorageOptions): Task | undefined;
|
|
25
26
|
/**
|
|
26
27
|
* List all tasks. Optionally filter by status.
|
|
28
|
+
* If status includes 'done' or 'cancelled', includes archived tasks (limited to most recent).
|
|
27
29
|
*/
|
|
28
30
|
export declare function listTasks(filter?: {
|
|
29
31
|
status?: Task['status'][];
|
|
32
|
+
archivedLimit?: number;
|
|
30
33
|
}, options?: StorageOptions): Task[];
|
|
31
34
|
/**
|
|
32
|
-
* Add a new task. Throws if task with same ID already exists.
|
|
35
|
+
* Add a new task. Throws if task with same ID already exists in active or archive.
|
|
33
36
|
*/
|
|
34
37
|
export declare function addTask(task: Task, options?: StorageOptions): void;
|
|
35
38
|
/**
|
|
@@ -49,3 +52,7 @@ export declare function taskExists(id: string, options?: StorageOptions): boolea
|
|
|
49
52
|
* Get count of tasks by status.
|
|
50
53
|
*/
|
|
51
54
|
export declare function getTaskCounts(options?: StorageOptions): Record<Task['status'], number>;
|
|
55
|
+
/**
|
|
56
|
+
* Get raw markdown content for a task by ID.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getTaskMarkdown(id: string, options?: StorageOptions): string | null;
|
package/dist/storage.js
CHANGED
|
@@ -1,150 +1,221 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync,
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { STATUSES } from './schema.js';
|
|
4
5
|
// ============================================================================
|
|
5
6
|
// Storage
|
|
6
7
|
// ============================================================================
|
|
7
8
|
const DEFAULT_DATA_DIR = 'data';
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const TASKS_DIR = 'tasks';
|
|
10
|
+
const ARCHIVE_DIR = 'archive';
|
|
10
11
|
const TERMINAL_STATUSES = ['done', 'cancelled'];
|
|
11
12
|
function ensureDir(dir) {
|
|
12
13
|
if (!existsSync(dir)) {
|
|
13
14
|
mkdirSync(dir, { recursive: true });
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
-
return join(dataDir,
|
|
17
|
+
function getTasksPath(dataDir) {
|
|
18
|
+
return join(dataDir, TASKS_DIR);
|
|
18
19
|
}
|
|
19
20
|
function getArchivePath(dataDir) {
|
|
20
|
-
return join(dataDir,
|
|
21
|
+
return join(dataDir, ARCHIVE_DIR);
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
+
function getTaskFilePath(dataDir, taskId, archived = false) {
|
|
24
|
+
const dir = archived ? getArchivePath(dataDir) : getTasksPath(dataDir);
|
|
25
|
+
return join(dir, `${taskId}.md`);
|
|
26
|
+
}
|
|
27
|
+
function taskToMarkdown(task) {
|
|
28
|
+
const { description, ...frontmatter } = task;
|
|
29
|
+
return matter.stringify(description || '', frontmatter);
|
|
30
|
+
}
|
|
31
|
+
function markdownToTask(content) {
|
|
32
|
+
const { data, content: description } = matter(content);
|
|
33
|
+
// Validate required fields
|
|
34
|
+
if (!data.id || typeof data.id !== 'string') {
|
|
35
|
+
throw new Error('Invalid task: missing or invalid id');
|
|
36
|
+
}
|
|
37
|
+
if (!data.title || typeof data.title !== 'string') {
|
|
38
|
+
throw new Error('Invalid task: missing or invalid title');
|
|
39
|
+
}
|
|
40
|
+
if (!data.status || typeof data.status !== 'string') {
|
|
41
|
+
throw new Error('Invalid task: missing or invalid status');
|
|
42
|
+
}
|
|
43
|
+
if (!STATUSES.includes(data.status)) {
|
|
44
|
+
throw new Error(`Invalid task: status must be one of ${STATUSES.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
if (!data.created_at || typeof data.created_at !== 'string') {
|
|
47
|
+
throw new Error('Invalid task: missing or invalid created_at');
|
|
48
|
+
}
|
|
49
|
+
if (!data.updated_at || typeof data.updated_at !== 'string') {
|
|
50
|
+
throw new Error('Invalid task: missing or invalid updated_at');
|
|
51
|
+
}
|
|
52
|
+
// Validate optional fields
|
|
53
|
+
if (data.blocked_reason !== undefined && typeof data.blocked_reason !== 'string') {
|
|
54
|
+
throw new Error('Invalid task: blocked_reason must be a string');
|
|
55
|
+
}
|
|
56
|
+
if (data.evidence !== undefined && !Array.isArray(data.evidence)) {
|
|
57
|
+
throw new Error('Invalid task: evidence must be an array');
|
|
58
|
+
}
|
|
23
59
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
...data,
|
|
61
|
+
description: description.trim() || undefined,
|
|
26
62
|
};
|
|
27
63
|
}
|
|
64
|
+
function readTaskFile(filePath) {
|
|
65
|
+
if (!existsSync(filePath)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
70
|
+
return markdownToTask(content);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error(`Failed to read task file ${filePath}:`, error instanceof Error ? error.message : error);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function writeTaskFile(filePath, task) {
|
|
78
|
+
const content = taskToMarkdown(task);
|
|
79
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
function listTaskFiles(dir) {
|
|
82
|
+
if (!existsSync(dir)) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return readdirSync(dir)
|
|
86
|
+
.filter(f => f.endsWith('.md'))
|
|
87
|
+
.map(f => join(dir, f));
|
|
88
|
+
}
|
|
28
89
|
/**
|
|
29
|
-
*
|
|
90
|
+
* Read raw markdown content from task file.
|
|
30
91
|
*/
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return emptyBacklog();
|
|
92
|
+
function readTaskMarkdown(dataDir, taskId) {
|
|
93
|
+
const activePath = getTaskFilePath(dataDir, taskId, false);
|
|
94
|
+
if (existsSync(activePath)) {
|
|
95
|
+
return readFileSync(activePath, 'utf-8');
|
|
36
96
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (data.version !== '1') {
|
|
41
|
-
throw new Error(`Unsupported backlog version: ${data.version}`);
|
|
97
|
+
const archivePath = getTaskFilePath(dataDir, taskId, true);
|
|
98
|
+
if (existsSync(archivePath)) {
|
|
99
|
+
return readFileSync(archivePath, 'utf-8');
|
|
42
100
|
}
|
|
43
|
-
return
|
|
101
|
+
return null;
|
|
44
102
|
}
|
|
45
103
|
/**
|
|
46
|
-
*
|
|
104
|
+
* Load backlog from disk. Returns empty backlog if directory doesn't exist.
|
|
47
105
|
*/
|
|
48
|
-
export function
|
|
106
|
+
export function loadBacklog(options = {}) {
|
|
49
107
|
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
108
|
+
const tasksDir = getTasksPath(dataDir);
|
|
109
|
+
const taskFiles = listTaskFiles(tasksDir);
|
|
110
|
+
const tasks = taskFiles
|
|
111
|
+
.map(f => readTaskFile(f))
|
|
112
|
+
.filter((t) => t !== null);
|
|
113
|
+
return {
|
|
114
|
+
version: '1',
|
|
115
|
+
tasks,
|
|
116
|
+
};
|
|
58
117
|
}
|
|
59
118
|
/**
|
|
60
|
-
*
|
|
119
|
+
* Save backlog to disk (no-op for file-based storage, kept for compatibility).
|
|
61
120
|
*/
|
|
62
|
-
export function
|
|
63
|
-
|
|
64
|
-
const path = getArchivePath(dataDir);
|
|
65
|
-
if (!existsSync(path)) {
|
|
66
|
-
return emptyBacklog();
|
|
67
|
-
}
|
|
68
|
-
const content = readFileSync(path, 'utf-8');
|
|
69
|
-
const data = JSON.parse(content);
|
|
70
|
-
if (data.version !== '1') {
|
|
71
|
-
throw new Error(`Unsupported archive version: ${data.version}`);
|
|
72
|
-
}
|
|
73
|
-
return data;
|
|
121
|
+
export function saveBacklog(backlog, options = {}) {
|
|
122
|
+
// No-op: tasks are saved individually
|
|
74
123
|
}
|
|
75
124
|
/**
|
|
76
|
-
*
|
|
125
|
+
* Load archive from disk. Returns empty backlog if directory doesn't exist.
|
|
77
126
|
*/
|
|
78
|
-
function
|
|
127
|
+
export function loadArchive(options = {}) {
|
|
79
128
|
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
129
|
+
const archiveDir = getArchivePath(dataDir);
|
|
130
|
+
const taskFiles = listTaskFiles(archiveDir);
|
|
131
|
+
const tasks = taskFiles
|
|
132
|
+
.map(f => readTaskFile(f))
|
|
133
|
+
.filter((t) => t !== null);
|
|
134
|
+
return {
|
|
135
|
+
version: '1',
|
|
136
|
+
tasks,
|
|
137
|
+
};
|
|
86
138
|
}
|
|
87
139
|
/**
|
|
88
140
|
* Move a task from backlog to archive.
|
|
89
141
|
*/
|
|
90
142
|
function archiveTask(task, options = {}) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const archiveIndex = archive.tasks.findIndex((t) => t.id === task.id);
|
|
100
|
-
if (archiveIndex !== -1) {
|
|
101
|
-
archive.tasks[archiveIndex] = task;
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
archive.tasks.push(task);
|
|
143
|
+
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
144
|
+
ensureDir(getTasksPath(dataDir));
|
|
145
|
+
ensureDir(getArchivePath(dataDir));
|
|
146
|
+
const activePath = getTaskFilePath(dataDir, task.id, false);
|
|
147
|
+
const archivePath = getTaskFilePath(dataDir, task.id, true);
|
|
148
|
+
// Remove from active if exists
|
|
149
|
+
if (existsSync(activePath)) {
|
|
150
|
+
unlinkSync(activePath);
|
|
105
151
|
}
|
|
106
|
-
|
|
107
|
-
|
|
152
|
+
// Write to archive
|
|
153
|
+
writeTaskFile(archivePath, task);
|
|
108
154
|
}
|
|
109
155
|
// ============================================================================
|
|
110
156
|
// Task Operations
|
|
111
157
|
// ============================================================================
|
|
112
158
|
/**
|
|
113
159
|
* Get a task by ID. Returns undefined if not found.
|
|
160
|
+
* Searches both active and archived tasks.
|
|
114
161
|
*/
|
|
115
162
|
export function getTask(id, options = {}) {
|
|
116
|
-
const
|
|
117
|
-
|
|
163
|
+
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
164
|
+
// Try active first
|
|
165
|
+
const activePath = getTaskFilePath(dataDir, id, false);
|
|
166
|
+
const activeTask = readTaskFile(activePath);
|
|
167
|
+
if (activeTask)
|
|
168
|
+
return activeTask;
|
|
169
|
+
// Try archive
|
|
170
|
+
const archivePath = getTaskFilePath(dataDir, id, true);
|
|
171
|
+
return readTaskFile(archivePath) ?? undefined;
|
|
118
172
|
}
|
|
119
173
|
/**
|
|
120
174
|
* List all tasks. Optionally filter by status.
|
|
175
|
+
* If status includes 'done' or 'cancelled', includes archived tasks (limited to most recent).
|
|
121
176
|
*/
|
|
122
177
|
export function listTasks(filter, options = {}) {
|
|
123
178
|
const backlog = loadBacklog(options);
|
|
179
|
+
let tasks = [...backlog.tasks];
|
|
180
|
+
// Check if we need archived tasks (when filtering for done/cancelled)
|
|
181
|
+
const needsArchive = filter?.status?.some(s => s === 'done' || s === 'cancelled');
|
|
182
|
+
if (needsArchive) {
|
|
183
|
+
const archive = loadArchive(options);
|
|
184
|
+
// Sort archived by updated_at descending (most recent first)
|
|
185
|
+
const sortedArchive = [...archive.tasks].sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
|
|
186
|
+
// Apply limit to archived tasks (default 10)
|
|
187
|
+
const archiveLimit = filter?.archivedLimit ?? 10;
|
|
188
|
+
const limitedArchive = sortedArchive.slice(0, archiveLimit);
|
|
189
|
+
tasks = [...tasks, ...limitedArchive];
|
|
190
|
+
}
|
|
191
|
+
// Filter by status if specified
|
|
124
192
|
if (filter?.status && filter.status.length > 0) {
|
|
125
|
-
|
|
193
|
+
tasks = tasks.filter((t) => filter.status.includes(t.status));
|
|
126
194
|
}
|
|
127
|
-
return
|
|
195
|
+
return tasks;
|
|
128
196
|
}
|
|
129
197
|
/**
|
|
130
|
-
* Add a new task. Throws if task with same ID already exists.
|
|
198
|
+
* Add a new task. Throws if task with same ID already exists in active or archive.
|
|
131
199
|
*/
|
|
132
200
|
export function addTask(task, options = {}) {
|
|
133
|
-
const
|
|
134
|
-
|
|
201
|
+
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
202
|
+
ensureDir(getTasksPath(dataDir));
|
|
203
|
+
const activePath = getTaskFilePath(dataDir, task.id, false);
|
|
204
|
+
const archivePath = getTaskFilePath(dataDir, task.id, true);
|
|
205
|
+
if (existsSync(activePath) || existsSync(archivePath)) {
|
|
135
206
|
throw new Error(`Task with ID '${task.id}' already exists`);
|
|
136
207
|
}
|
|
137
|
-
|
|
138
|
-
saveBacklog(backlog, options);
|
|
208
|
+
writeTaskFile(activePath, task);
|
|
139
209
|
}
|
|
140
210
|
/**
|
|
141
211
|
* Update an existing task. Throws if task doesn't exist.
|
|
142
212
|
* Automatically archives tasks with terminal status (done, cancelled).
|
|
143
213
|
*/
|
|
144
214
|
export function saveTask(task, options = {}) {
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
215
|
+
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
216
|
+
const activePath = getTaskFilePath(dataDir, task.id, false);
|
|
217
|
+
const archivePath = getTaskFilePath(dataDir, task.id, true);
|
|
218
|
+
if (!existsSync(activePath) && !existsSync(archivePath)) {
|
|
148
219
|
throw new Error(`Task with ID '${task.id}' not found`);
|
|
149
220
|
}
|
|
150
221
|
// Archive if terminal status
|
|
@@ -152,27 +223,34 @@ export function saveTask(task, options = {}) {
|
|
|
152
223
|
archiveTask(task, options);
|
|
153
224
|
return;
|
|
154
225
|
}
|
|
155
|
-
|
|
156
|
-
|
|
226
|
+
ensureDir(getTasksPath(dataDir));
|
|
227
|
+
writeTaskFile(activePath, task);
|
|
157
228
|
}
|
|
158
229
|
/**
|
|
159
230
|
* Delete a task by ID. Throws if task doesn't exist.
|
|
160
231
|
*/
|
|
161
232
|
export function deleteTask(id, options = {}) {
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
233
|
+
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
234
|
+
const activePath = getTaskFilePath(dataDir, id, false);
|
|
235
|
+
const archivePath = getTaskFilePath(dataDir, id, true);
|
|
236
|
+
if (existsSync(activePath)) {
|
|
237
|
+
unlinkSync(activePath);
|
|
238
|
+
}
|
|
239
|
+
else if (existsSync(archivePath)) {
|
|
240
|
+
unlinkSync(archivePath);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
165
243
|
throw new Error(`Task with ID '${id}' not found`);
|
|
166
244
|
}
|
|
167
|
-
backlog.tasks.splice(index, 1);
|
|
168
|
-
saveBacklog(backlog, options);
|
|
169
245
|
}
|
|
170
246
|
/**
|
|
171
247
|
* Check if a task exists.
|
|
172
248
|
*/
|
|
173
249
|
export function taskExists(id, options = {}) {
|
|
174
|
-
const
|
|
175
|
-
|
|
250
|
+
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
251
|
+
const activePath = getTaskFilePath(dataDir, id, false);
|
|
252
|
+
const archivePath = getTaskFilePath(dataDir, id, true);
|
|
253
|
+
return existsSync(activePath) || existsSync(archivePath);
|
|
176
254
|
}
|
|
177
255
|
/**
|
|
178
256
|
* Get count of tasks by status.
|
|
@@ -191,4 +269,11 @@ export function getTaskCounts(options = {}) {
|
|
|
191
269
|
}
|
|
192
270
|
return counts;
|
|
193
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Get raw markdown content for a task by ID.
|
|
274
|
+
*/
|
|
275
|
+
export function getTaskMarkdown(id, options = {}) {
|
|
276
|
+
const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
|
|
277
|
+
return readTaskMarkdown(dataDir, id);
|
|
278
|
+
}
|
|
194
279
|
//# sourceMappingURL=storage.js.map
|