backlog-mcp 0.2.1 → 0.6.7

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 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
- ```typescript
10
- {
11
- id: string; // TASK-0001
12
- title: string;
13
- description?: string;
14
- status: 'open' | 'in_progress' | 'blocked' | 'done' | 'cancelled';
15
- created_at: string; // ISO8601
16
- updated_at: string; // ISO8601
17
- blocked_reason?: string;
18
- evidence?: string[];
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,11 +88,14 @@ npm start
58
88
 
59
89
  ## Storage
60
90
 
61
- - Default: `data/backlog.json` (local to project)
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.json`
64
- - Atomic writes via temp + rename
93
+ - Completed/cancelled tasks auto-archive to `archive/`
65
94
 
66
95
  ## License
67
96
 
68
97
  MIT
98
+
99
+ <a href="https://glama.ai/mcp/servers/@gkoreli/backlog-mcp">
100
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@gkoreli/backlog-mcp/badge" alt="backlog-mcp MCP server" />
101
+ </a>
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,GACd,MAAM,cAAc,CAAC"}
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 tasks = listTasks(status ? { status } : undefined, storageOptions);
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 task = getTask(id, storageOptions);
55
- if (!task) {
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: JSON.stringify(task, null, 2) }] };
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
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 file doesn't exist.
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 atomically (write to temp, then rename).
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 file doesn't exist.
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, renameSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { randomUUID } from 'node:crypto';
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 BACKLOG_FILE = 'backlog.json';
9
- const ARCHIVE_FILE = 'archive.json';
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 getBacklogPath(dataDir) {
17
- return join(dataDir, BACKLOG_FILE);
17
+ function getTasksPath(dataDir) {
18
+ return join(dataDir, TASKS_DIR);
18
19
  }
19
20
  function getArchivePath(dataDir) {
20
- return join(dataDir, ARCHIVE_FILE);
21
+ return join(dataDir, ARCHIVE_DIR);
21
22
  }
22
- function emptyBacklog() {
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
- version: '1',
25
- tasks: [],
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
- * Load backlog from disk. Returns empty backlog if file doesn't exist.
90
+ * Read raw markdown content from task file.
30
91
  */
31
- export function loadBacklog(options = {}) {
32
- const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
33
- const path = getBacklogPath(dataDir);
34
- if (!existsSync(path)) {
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 content = readFileSync(path, 'utf-8');
38
- const data = JSON.parse(content);
39
- // Basic version check
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 data;
101
+ return null;
44
102
  }
45
103
  /**
46
- * Save backlog to disk atomically (write to temp, then rename).
104
+ * Load backlog from disk. Returns empty backlog if directory doesn't exist.
47
105
  */
48
- export function saveBacklog(backlog, options = {}) {
106
+ export function loadBacklog(options = {}) {
49
107
  const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
50
- ensureDir(dataDir);
51
- const path = getBacklogPath(dataDir);
52
- const tempPath = join(dataDir, `.backlog.${randomUUID()}.tmp`);
53
- const content = JSON.stringify(backlog, null, 2);
54
- // Write to temp file first
55
- writeFileSync(tempPath, content, 'utf-8');
56
- // Atomic rename
57
- renameSync(tempPath, path);
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
- * Load archive from disk. Returns empty backlog if file doesn't exist.
119
+ * Save backlog to disk (no-op for file-based storage, kept for compatibility).
61
120
  */
62
- export function loadArchive(options = {}) {
63
- const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
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
- * Save archive to disk atomically.
125
+ * Load archive from disk. Returns empty backlog if directory doesn't exist.
77
126
  */
78
- function saveArchive(archive, options = {}) {
127
+ export function loadArchive(options = {}) {
79
128
  const dataDir = options.dataDir ?? DEFAULT_DATA_DIR;
80
- ensureDir(dataDir);
81
- const path = getArchivePath(dataDir);
82
- const tempPath = join(dataDir, `.archive.${randomUUID()}.tmp`);
83
- const content = JSON.stringify(archive, null, 2);
84
- writeFileSync(tempPath, content, 'utf-8');
85
- renameSync(tempPath, path);
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 backlog = loadBacklog(options);
92
- const archive = loadArchive(options);
93
- // Remove from backlog
94
- const index = backlog.tasks.findIndex((t) => t.id === task.id);
95
- if (index !== -1) {
96
- backlog.tasks.splice(index, 1);
97
- }
98
- // Add to archive (replace if exists)
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
- saveBacklog(backlog, options);
107
- saveArchive(archive, options);
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 backlog = loadBacklog(options);
117
- return backlog.tasks.find((t) => t.id === id);
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
- return backlog.tasks.filter((t) => filter.status.includes(t.status));
193
+ tasks = tasks.filter((t) => filter.status.includes(t.status));
126
194
  }
127
- return backlog.tasks;
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 backlog = loadBacklog(options);
134
- if (backlog.tasks.some((t) => t.id === task.id)) {
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
- backlog.tasks.push(task);
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 backlog = loadBacklog(options);
146
- const index = backlog.tasks.findIndex((t) => t.id === task.id);
147
- if (index === -1) {
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
- backlog.tasks[index] = task;
156
- saveBacklog(backlog, options);
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 backlog = loadBacklog(options);
163
- const index = backlog.tasks.findIndex((t) => t.id === id);
164
- if (index === -1) {
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 backlog = loadBacklog(options);
175
- return backlog.tasks.some((t) => t.id === id);
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